画像をリサイズ&クロップしてFirebase CloudStorageにアップロードする

## やりたいこと
Vue.jsで下記の動作を実現する:
1. formから画像をアップロード
2. アップロードした画像のアスペクト比を保ったまま正方形にリサイズ & 円形にクロップ
3. 加工した画像をFirebase ColudStrageにアップロード 

画像のリサイズはなんとなくサーバーサイドでやるもの？と思っていたのですが、これくらいの操作であれば、canvasを使えばいけました。

## formから画像をアップロード
Templete内のform部分はこんな感じ。
```vue:upload.vue
<form @submit.prevent="submitImg" class="form">
			<label class="input_label" :class="{ disabled: uploading }">
				<canvas id="preview" class="preview" ref="canvas"></canvas>
				<div class="uploading" v-if="uploading">
					<img src="../assets/bars.svg" class="loading" />
				</div>
				<input
					class="input_file"
					type="file"
					accept="image/*"
					@change="changeImg"
				/>
			</label>
			<button v-if="isAttached" type="submit" class="button">
				<span v-if="!uploading">これにする</span>
				<span v-else>アップロード中...</span>
			</button>
		</form>
```
dataには、下記データを用意します。
```javascript:upload.vue
data() {
			return {
				thumbnail: new Blob(), //画像を格納する
				isAttached: false, //画像が添付されたかどうか
				uploading: false, //アップロード中かどうか(ローディング表示につかう)
				getData: {
					thumbnail: "", //firebaseからのレスポンスを格納する
				},
			};
		},
```
画像プレビューをcanvasにしているのがポイントです。
CSSは割愛しますが、UIはこんなです。
円をタップすると画像ファイルを選択できる。
![スクリーンショット 2023-11-23 8.39.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522080/ef7ad741-e7fd-15a8-a72e-6234d95bbf2c.png)

## formで選択した画像を正方形にクロップ & リサイズ
上記で画像を選択すると`changeImg`が発火します。
下記をmethodsに追加します。
画像をアスペクト比を保ったまま512x512の正方形にリサイズし、円にクロップします。

```javascript:upload.vue
changeImg(e: any) {
				this.thumbnail = e.target.files[0];
				const image = new Image();
				if (this.thumbnail) {
					this.isAttached = true;
					const reader = new FileReader();
					reader.readAsDataURL(this.thumbnail);
					reader.onload = () => {
						image.src = URL.createObjectURL(this.thumbnail);
						const canvas = this.$refs["canvas"] as any;
						const ctx = canvas.getContext("2d");
      
						canvas.width = 512;
						canvas.height = 512;

						image.addEventListener("load", () => {
							const imageWidth = image.naturalWidth;
							const imageHeight = image.naturalHeight;

							const iconSize = canvas.width;

                            //画像をアスペクト比を保ち正方形にリサイズ
							const aspectRatio = imageWidth / imageHeight;
							let imageViewWidth, imageViewHeight, offsetX, offsetY;

							if (aspectRatio >= 1) {
								imageViewWidth = iconSize * aspectRatio;
								imageViewHeight = iconSize;
								offsetX = (iconSize - imageViewWidth) / 2;
								offsetY = 0;
							} else {
								imageViewWidth = iconSize;
								imageViewHeight = iconSize / aspectRatio;
								offsetX = 0;
								offsetY = (iconSize - imageViewHeight) / 2;
							}
							ctx.clearRect(0, 0, canvas.width, canvas.height);
       
                           //円にくりぬく
							ctx.beginPath();
							ctx.arc(iconSize / 2, iconSize / 2, iconSize / 2, 0, Math.PI * 2);
							ctx.closePath();
							ctx.clip();

                            // 描画する
							ctx.drawImage(
								image,
								0,
								0,
								imageWidth,
								imageHeight,
								offsetX,
								offsetY,
								imageViewWidth,
								imageViewHeight
							);

							ctx.clip("evenodd");

                            //アップロードする画像にcanvasで描画している内容を格納
							this.thumbnail = canvas.toDataURL();
						});
					};
				}
			},
```
## Firebase CloudStorageの準備
ライブラリのインストールやConfig、アップロードはこの記事を参考にしました。

https://qiita.com/azukiazusa/items/e418aaa6205b08848850


## canvasで加工した画像をFirebase CloudStorageにアップロード
フォームをsubmitするとsubmitImgが発火します。
下記をmethodsに追加します。

```javascript:upload.vue
submitImg() {
				let storage = getStorage();

				const canvas = this.$refs["canvas"] as any;

				const storageRefImagesRef = ref(
					storage,
                     //画像の格納先とファイル名を決めている
					"images/" + this.fileID + ".png"
				);
				const metadata = {
					contentType: "image/png",
				};
				canvas.toBlob((blob: any) => {
					const uploadTask = uploadBytesResumable(
						storageRefImagesRef,
						blob,
						metadata
					);

					uploadTask.on(
						"state_changed",
						(snapshot) => {
							const progress =
								(snapshot.bytesTransferred / snapshot.totalBytes) * 100;
							console.log("Upload is " + progress + "% done");
							switch (snapshot.state) {
								case "paused":
									console.log("アップロード停止");
									break;
								case "running":
									this.uploading = true;
									console.log("アップロード中");
									break;
							}
						},
						(error) => {
							console.log("Upload error!");
							alert("ごめんなさい。アップロードエラーです！");
						},
						() => {
							getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
								this.uploading = false;
								this.$emit("uploaded", downloadURL);
								this.getData.thumbnail = downloadURL;
								console.log(downloadURL);
							});
						}
					);
				});
			},
```
`getData.thumbnail`にアップロードされたURLが返ってきます。

`this.fileID`で画像名を決めていますが、今回は画像名をユニークIDにしたかったので、親コンポーネントでユニークIDを発行してそれをpropsとして渡しました。
そのあたりについては本筋とそれるので別記事にでも！

## 実行結果
こんな感じの縦長の画像をアップロードします。
![pexels-valeria-boltneva-1805164.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522080/2f126f1d-e897-9549-3004-7dc6aa8d16f4.jpeg)

![スクリーンショット 2023-11-23 9.10.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522080/1032002e-48c4-1e11-133a-aa09bb10a12e.png)

正円にクロップされました。

![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522080/0a7022ed-af90-a342-65d5-5a7289e16d94.png)

横長でももちろん。
![pexels-inge-wallumrød-126407.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522080/90577e5f-d6d2-109c-1702-2529d4779ce3.jpeg)
![スクリーンショット 2023-11-23 9.12.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522080/861299b5-308b-016f-0e63-b56ec7045e92.png)
![download-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2522080/dfa2bfcc-36a7-e51c-4ecc-bd6ef0189ade.png)

## さらにアプデしたいこと
今回は最低限でいいやと思い、トリミング範囲については自動的に中央基準になるように実装しています。
今後はトリミング範囲も任意に設定できるようにしたいですね。