この記事はディップ株式会社 Advent Calendar 2022 の14日目です。
12月ももう半分ですか…時の流れの速さは恐ろしいですね(白目)
概要
写真などをアップロードしてもらう際に、サイズや容量を規定値以下にしたい時の処理になります。
だいぶ昔に一度実装したことがあるのですが、その頃とはだいぶ環境も変わったのでこれを機に書き直してみました。
ざっくりとした手順
- ファイルinputから画像ファイルを取得
- 取得したファイルをプレビューとしてimgタグへ挿入、表示
- imgタグに表示した画像をimageオブジェクトとして取得
- サイズを調整してcanvas要素に描画
- canvasに描画したデータをバイナリ化
- 加工後容量以上かチェック
- 容量以上なら容量を減らす
やっていきます
ファイルの取得
FileReaderコンストラクタを使用します。
// プレビューimgタグの取得
const previewBox = document.getElementById('preview');
// FileReaderコンストラクタを使用
const fileReader = new FileReader();
fileReader.onload = () => {
//選択した画像を一旦imgタグに表示
previewBox.src = fileReader.result;
// 圧縮処理
compressImage();
};
imgタグに表示した画像をimageオブジェクトとして取得
imageコンストラクタを使用します。
// imgタグに表示した画像をimageオブジェクトとして取得
const image = new Image();
image.onload = () => {
// この中にサイズ調整処理
);
サイズ等を調整
サイズ調整時に画像のEXIF情報を取得して元の画像が回転しているかどうか確認します。
(回転している場合縦横比が変わるため)
しかし、EXIF情報は編集ができますし、iPhoneは画像ファイルのタイプがHEIFなので、その後どのアプリを経由して圧縮されるかによりEXIF情報が残ったり残らなかったりするので、必ずしも画像にEXIF情報が存在するわけではありません。ですのでEXIF情報がない場合の処理も入れておくと良いと思います。
ちなみに…
今回iPhoneでとった写真を使用して実装していたのですが、
iPhone(使用したのは11pro)で写真を撮った場合
縦向きで撮るとorientationの値が6になり、90度回転している判定
横向きで撮るとorientationの値が1になり、何も回転していない判定
となりました…。
昔実装した際はこちらが逆の判定だったのでその時と仕様がかわったようです。
なのでiPhoneの場合この処理は必要ないかもしれません。
※ Androidは実機が手元にないため未確認です。申し訳ございません。
サイズ調整処理
EXIF情報を取得するのにはexifrを使用しています。
ファイルのデータ取得するときにtypeでjpegなりpngなりの確認する処理は割愛しております。
// exifr読み込み
import exifr from 'exifr';
//加工後の横幅を800pxに設定
const limitWidth = 800;
//回転している場合の縦幅を800pxに設定
const limitHeight = 800;
//加工後の容量を3MB以下に設定
const limitCapacity = 3000000;
let height;
let width;
// canvasタグの取得
const canvas = document.getElementById('canvas');
// canvasにグラフィックを描画する
const context = canvas.getContext('2d');
// Exifのorientation読み取り
exifr.orientation(image).then(res => {
// 回転値の初期値を0にしておく
let rotate = 0;
// 回転しているかorientationの値で判定
if (res) {
if (res == 8) {
rotate = 90;
} else if (res == 3) {
rotate = 180;
} else if (res == 6) {
rotate = 270;
}
}
// 回転している場合は圧縮した時の縦横比が変わるためwidthを入れ替える
if (rotate == 90 || rotate == 270) {
if (limitHeight < image.height) {
width = image.width * (limitHeight / image.height);
height = limitHeight;
} else {
width = image.width;
height = image.height;
}
canvas.width = height;
canvas.height = width;
} else {
if (limitWidth < image.width) {
width = limitWidth;
height = image.height * (limitWidth / image.width);
} else {
width = image.width;
hight = image.height;
}
// canvasに反映
canvas.width = width;
canvas.height = height;
}
// 画像の向きを回転値に合わせて戻す
if (rotate && rotate > 0) {
context.rotate((rotate * Math.PI) / 180);
if (rotate == 90) {
context.translate(0, -height);
} else if (rotate == 180) {
context.translate(-width, -height);
} else if (rotate == 270) {
context.translate(-width, 0);
}
}
// canvasに描画する
context.drawImage(image, 0, 0, width, height);
}).then(() => {
// 次の処理
});
canvasに描画したデータをバイナリ化
// canvasに描画したデータを取得
let canvasImage = canvas;
// canvasに描画したデータをバイナリ化
const originalDataURL = canvasImage.toDataURL('image/jpeg');
const originalBlob = base64ToBlob(originalDataURL);
// Base64をバイナリデータへ戻す処理
const base64ToBlob = (base64) => {
let base64Data = base64.split(',')[1], // DataURLからBase64のデータ部分のみを取得
data = window.atob(base64Data), // base64形式の文字列をデコード
buffer = new ArrayBuffer(data.length),
array = new Uint8Array(buffer),
blob,
i,
dataLength;
// blobの生成
for (i = 0, dataLength = data.length; i < dataLength; i++) {
array[i] = data.charCodeAt(i);
}
blob = new Blob([array], { type: 'image/jpeg' });
return blob;
}
バイナリ化したデータが容量以上かチェック
// 容量チェック
if (limitCapacity <= originalBlob['size']) {
// 加工後容量以下に落とす
const capacityRatio = limitCapacity / originalBlob['size'];
const compressedDataURL = canvasImage.toDataURL(
'image/jpeg',
capacityRatio
);
// バイナリ化
const uploadBlob = base64ToBlob(compressedDataURL);
}
感想
前述したとおりEXIF情報に関しては少し不確定なところがあるので、今回は処理に入れましたがあまり当てにしないようにしたほうが良いかもしれません。
アップロードしてもらった画像をプレビューに表示して、意図した状態になってるか自身で見てもらったほうが確実かと。
回転値のもっといい取り方があればぜひご教示いただけますと助かります。