2022.09.02
STAFF BLOG
スタッフブログ
TECHNICAL
テクログ
私はJavaScriptとCanvasでトリミング処理したDataURLの画像をPHPで受け取り、DataURLの宣言を取り除く処理ではまりました。
このブログで言いたいことの9割をまず先に言っておこうと思います。
DataURL宣言の削除をPHPで行う際、preg_replace() はやめときましょう。
DataURL について
多分このブログに行き着いた人はPHPでDataURLをどうにかしたい人で、いくつかのサイトを渡り歩いた人なのではないでしょうか?
DataURLについては既にある程度ご存じだと思うので詳しい説明はmdn様にお任せすることにします。
https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/Data_URLs
想定する処理の流れ
新規のアカウント作成でプロフィール画像の登録が必要という想定にします。
- HTMLのフォームから、DataURLを送信
- PHPでDataURLを受け取る
- DataURLからMIMEタイプ(メディアタイプ)を抽出
- DataURLの文字列から、先頭のDataURL 宣言を削除
- Base64デコード
このブログのメインは
3. DataURLからMIMEタイプ(メディアタイプ)を抽出
4. DataURLから先頭のDataURL 宣言を削除
この2つです。
他については参考になるサイトが既に色々あると思いますのでさらっと触れる程度です。
1. HTMLのフォームから、DataURLを送信
JavaScriptとCanvasでトリミング処理した画像のDataURLを、HTMLのフォーム input: hidden にセット。
(下記のサンプルコードは雰囲気を出すために書いているだけなので動きません。画像トリミングができるライブラリ Cropper.js で画像をトリミングしフォームにセットするイメージで書きました。)
<img id="js-preview" src="">
<label for="js-fileInput">画像選択</label>
<input type="file" accept=".jpg, .jpeg, .png" required name="original_img" id="js-fileInput">
<input type="hidden" id="js-dataURL" name="data_url" value="">
// cropper.js でトリミングした canvas要素を DataURLにする
const croppedDataURL = cropper.getCroppedCanvas().toDataURL('image/jpeg');
// プレビュー用のimgタグに DataURLをセット
const preview = document.getElementById('js-preview');
preview.src = croppedDataURL;
// フォームの input:hidden要素にトリミング画像の DataURL をセット
const dataURLInputHidden = document.getElementById('js-dataURL');
dataURLInputHidden.value = croppedDataURL;
上記のサンプルコードでは input:hidden にセットしsubmitボタンクリックで他の入力要素と一緒にpostするという想定です。
この場合、私の環境ではこれといった問題は起きませんでした。
ただ fetchAPIなどを使用し、JavaScriptからpostする際にdataURL内の ‘+’ が半角スペースになる問題があるらしいです。
何かがおかしいというときはその辺りを調べてみるといいかと思います。この辺りの情報は多いですね。
2. PHPでDataURLを受け取る
$data_url = $_POST["data_url"];
3. DataURLからMIMEタイプ(メディアタイプ)を抽出
// ↓ DataURL文字列の例 ↓
// data:image/png;base64,iVBORw0KGgoAAAANS..
// DataURLの宣言が含まれる先頭部分をまず抽出 (手抜きでかなり余裕を持って先頭から50)
$extracted_head = substr($data_url, 0, 50);
// 正規表現でMIMEタイプ抽出
$pattern = '/data:(?P<mime_type>.*\/.*);base64,/';
$matches = [];
preg_match($pattern, $extracted_head, $matches)
$mime_type = $matches['mime_type'];
なぜ substr($data_url, 0, 50);
で先頭部分だけ取り出しているのか。
DataURL全体に対してpreg_match()を実行すると、正規表現に問題がないにもかかわらずfalsyな値が返ってきました。
これは preg_match()の限界を超えたからのようです。(実行環境により上限は異なる)
また、エラーになるわけではないので画像のサイズによっては問題ないという、タチの悪いケースも起こり得ます。
なのでDataURL全体に対してpreg_match()を実行するのはやめときましょう。
4. DataURLの文字列から、先頭のDataURL 宣言を削除
// ↓ DataURL文字列の例 ↓
// data:image/png;base64,iVBORw0KGgoAAAANS..
// DataURLの宣言を除いた base64データの開始位置をoffsetとして定義
$needle = 'base64,';
$offset = strpos($data_url, $needle) + mb_strlen($needle);
// base64データの開始位置から最後までの文字列を取得
$base64_data = substr($data_url, $offset);
MIMEタイプの抽出をした時と同じ理由で preg_replace() を使用していません。
preg_replace('/data:.*\/.*;base64,/', '', $data_url);
上記の処理ではデータサイズが大きかった際、正規表現がマッチせず意図しない結果となります。
なのでDataURL全体に対してpreg_replace()を実行するのはやめときましょう。
4. Base64デコード
$decoded_data = base64_decode($base64_data);
ここまで来たらbase64デコードするだけで終了です。
MIMEタイプの抽出も済んでいるのでS3などへのアップロードも問題ないはず。
まとめ
大事なことなので繰り返します。
DataURL全体に対してpreg系関数(正規表現マッチ)を実行するのはやめときましょう。
正規表現は処理として重いので、ケースによっては他の文字列マッチ関数を使用するべきということでした。
最終的に文字列マッチのブログになってしまいました。
でもいいんです、今回ケースで私にとって一番の学びはそこなので。