スマホ向け「撮ってアップロード」機能をつくる(部分切り取りも可)

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、最近の開発はどちらかというとPCメインで、それをレスポンシブ・デザインでスマホでも使えるように、という流れが多いのですが、たまにはスマートフォンにしかない「ある機能」を試してみたくなりました。

そのある機能とは・・・・・・

「撮ってアップロード📷」

機能です。

この「撮ってアップロード」とは(個人的に呼んでる名前ですが😂)、例えば以下のような機能です。

  1. ブラウザでボタンをタッチ
  2. (ブラウザ上ではなく、)スマホ側のカメラが起動
  3. 写真をとると、そのデータをJavaScript側に送ってくれる
  4. アップロードする

つまり、ファイルを選択するのではなく、リアルタイムで撮影した写真をアップロードするというもので、持ち運びを基本とするスマホに備わっている強力な機能ですね。

そこで❗

今回はこの「撮ってアップロード」機能をLaravel + Vueで実装してみたいと思います。(さらに、画像の一部分だけを範囲指定し、そこだけ切り取ってアップロードする機能もつけます👍)

ぜひ皆さんのお役に立てると嬉しいです😊✨
(最後に、今回実際に開発したソースコード一式をダウンロードできますよ)

「そういえば、Blackberryって
その後どうなったんだろう??」

開発環境: Laravel 8.x、Vue 2.6、Cropper 1.5.9、javascript-canvas-to-blob 3.28

やりたいこと

まず、今回やりたいことの詳細は次のとおりです。

  • スマホのカメラから写真データを取得
  • 写真データを取得したらプレビュー表示
  • プレビュー表示した画像を指でなぞると、その部分が選択できる
  • 選択した部分だけを切り取ってアップロードする

では、楽しくやっていきましょう❗

前提として

カメラ機能を使うためにはローカルであっても必ずhttpsアクセスが必要です。
もしまだの方は以下のページを参考にしてみてください。

📝 コピペでOK!ローカル環境にHTTPSを導入する(nginx編)

コントローラーをつくる

では、最初に「撮ってアップロード」の専用コントローラーをつくります。
以下のコマンドを実行してください。

php artisan make:controller CameraCaptureController

すると、ファイルが作成されるので、中身を次のように変更してください。

app/Http/Controllers/CameraCaptureController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class CameraCaptureController extends Controller
{
    public function create() {

        return view('camera_capture');

    }

    public function store(Request $request) {

        $request->validate([
            'image' => ['required', 'file', 'image']
        ]);

        $result = false;

        try {

            $request->file('image')->store('cropped_images'); // storage/app/cropped_images フォルダへ保存
            $result = true;

        } catch (\Exception $e) {

            // エラーの場合

        }

        return [
            'result' => $result
        ];

    }
}

中身としては、

  • create() ・・・ ブラウザで実際にアクセスするメソッド
  • store() ・・・ Ajax送信で画像データがアップロードされるメソッド

となります。

なお、アップロードされた画像は「storage/app/cropped_images」フォルダの中に保存されていくことになります。(ファイル名はランダムです)

ビューをつくる

次に、先ほどコントローラーでセットしたビューをつくっていきます。
ここが今回のメインになります。

以下のファイルを作成してください。

resources/views/camera_capture.blade.php

<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.9/cropper.min.css">
    <style>

        [v-cloak] {
            display: none;
        }

    </style>
</head>
<body>
<div id="app" v-cloak>

    <!-- ① スマホのカメラを起動する部分 -->
    <div class="p-3" v-if="isStatusReady">
        <label class="btn btn-info">
            &#x1F4F8; 写真を撮ってアップロードする
            <input type="file" class="d-none" accept="image/*" capture="camera" @change="onCaptureImage">
        </label>
    </div>

    <!-- ② 写真撮影後に表示する部分 -->
    <div style="background:#000" v-if="isStatusCropping">
        <div class="text-center text-white font-weight-bold bg-dark p-1"
             style="position:fixed;top:0;width:100%;z-index:10000;opacity:0.8;font-size:80%;">
            画像をドラッグして範囲指定できます
        </div>
        <div class="p-3"
             style="position:fixed;bottom:0;width:100%;z-index:10000;">
            <button type="button" class="btn btn-info btn-lg text-nowrap float-right" @click="onSubmit">アップロードする</button>
            <button type="button" class="btn btn-link btn-lg text-nowrap" @click="onCancel">戻る</button>
        </div>

        <!-- ③ プレビュー画像 -->
        <img ref="preview" style="display:block;max-width:100%;">

    </div>

</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.9/cropper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/javascript-canvas-to-blob/3.28.0/js/canvas-to-blob.min.js"></script>
<script>

    new Vue({
        el: '#app',
        data: {
            status: 'ready', // ready -> cropping -> (ready)
            imageFile: null,
            cropper: null
        },
        methods: {
            // ④ 撮影された画像データを取得
            onCaptureImage(e) {

                const files = e.target.files;

                if(files.length > 0) {

                    this.status = 'cropping';
                    this.imageFile = files[0];

                    Vue.nextTick(() => {

                        this.setPreviewImage()

                    });

                }

            },
            // ⑤ 撮影された写真をプレビュー表示
            setPreviewImage() {

                const reader = new FileReader();
                reader.addEventListener('load', () => {

                    this.$refs.preview.src = reader.result;
                    this.setCropper();

                });
                reader.readAsDataURL(this.imageFile);

            },
            // ⑥ Cropperを実行して画像の一部を選択できるようにする
            setCropper() {

                if(this.cropper !== null) {

                    this.cropper.destroy();

                }

                this.cropper = new Cropper(this.$refs.preview, {
                    background: false,
                    zoomable: false,
                    autoCrop: false,
                });

            },
            onCancel() {

                this.cropper.destroy();
                this.status = 'ready';

            },
            // ⑦ 画像をアップロード
            onSubmit() {

                const croppedCanvas = this.cropper.getCroppedCanvas();
                croppedCanvas.toBlob(blob => {

                    const url = '/camera_capture';
                    const formData = new FormData();
                    formData.append('image', blob);

                    axios.post(url, formData)
                        .then(response => {

                            if(response.data.result === true) {

                                alert('アップロードが完了しました');

                            } else {

                                alert('ファイルの保存に失敗しました。');

                            }

                        })
                        .catch(error => {

                            alert(error);

                        });

                }, 'image/jpeg');

            }
        },
        computed: {
            isStatusReady() {

                return (this.status === 'ready');

            },
            isStatusCropping() {

                return (this.status === 'cropping');

            },
            isStatusSubmitting() {

                return (this.status === 'submitting');

            }
        },
        mounted() {

            // ⑧ 環境チェック
            const canvas = document.createElement('canvas')

            if(!canvas.toBlob) {

                alert('このブラウザはサポート外です・・・。');

            }

        }
    });

</script>
</body>
</html>

では、少しコードが長いのでひとつずつご紹介していきます。

① スマホのカメラを起動する部分

ここでスマホのカメラを起動することになりますが、重要なのは以下の部分です。

capture="camera"

このプロパティを設定しておくと、リアルタイム撮影に対応してる場合、直接カメラが開いて撮影できるようになります。

ちなみに、accept="image/*" をつけていない場合、環境によっては以下のようにファイルとの選択になる場合がありますので、カメラを起動するだけの場合は忘れずセットしておいてください。

なお、capture="camera"がついていてもPCから実行はできます。(通常のファイル選択になります)

② 写真撮影後に表示する部分

ここは、変数statuscroppingになったら(= 写真を撮影したら)表示される部分になります。

③ プレビュー画像

ここで撮影された写真のプレビューを表示します。
後でご紹介しますが、このプレビュー画像をドラッグすると、長方形の範囲指定ができるようにします。

なお、この範囲指定のためにスタイルシートの以下の部分は忘れずにつけておいてください。

<img ref="preview" style="display:block;max-width:100%;">

④ 撮影された画像データを取得

では、ここからがJavaScript部分になります。

onCaptureImage() は、写真が撮影されたら実行されるメソッドで、この中ではstatusの変更とプレビュー表示の実行をしています。

また、写真データはVue内のどこからでもアクセスできるようimageFileにセットしておきます。

ちなみにVue.nextTick()は、画面がVueによって書き換えられたら実行されることになります。これをつけておかないとv-ifで囲んだ部分がまだ存在していないのにthis.$refs.previewが呼ばれることになるため、エラーになってしまいます。

※ 代替案としては、v-showでもいいかもしれません。v-ifとの違いは以下のURLを参考にしてください。

📝 Vue.js、全13ディレクティブ実例:v-show

⑤ 撮影された写真をプレビュー表示

setPreviewImage()は、前項目のVue.nextTick()内で実行されることになります。

ここでは、$refsを通して<img>タグを取得し、srcに写真データをセットしています。

また、プレビューが表示されると同時にCropperという画像の切り取りができるパッケージも起動しています。(このパッケージ、ホントすごいです。しかもMITライセンスです👍)

⑥ Cropperを実行して画像の一部を選択できるようにする

ここでは、Cropperという画像の切り取りができるパッケージを起動しています。

実行すると、指やマウスでドラッグするだけで以下のように青枠で画像の一部分を選択でき、しかも、この選択した部分の画像データだけを取得することもできます😊✨

なお、Cropperのオプションについては以下のURLをご覧ください。

📝 Cropper の GitHubページ

⑦ 画像をアップロード

ここで画像データをLaravel側へ送信することになるのですが、その画像データはCropperの用意してくれているgetCroppedCanvas()というメソッドを使って取得します。

getCroppedCanvas()は、いま範囲指定している部分の画像をCanvasとして返してくれるという便利なメソッドです。

そのため、データ送信は次の手順で行っています。

  1. Cropper からキャンバスを取得
  2. キャンバスの toBlob() でバイナリデータを作成
  3. そのデータを axios で Ajax送信(アップロード)

なお、CanvastoBlob()は、第2引数にmimeType(ファイルの種類)、第3引数に画質(0 〜 1)で指定することができます。

以下はJPEG画像で画質を0.7にした例です。

canvas.toBlob(blob => {

    // 省略

}, 'image/jpeg', 0.7); // 👈 こちらです

※ ちなみにimage/pngはデータサイズが大きくなりすぎる傾向にあるので、あまりオススメできません。axiosのリミットを超えるとデータ送信に失敗するようです。また、画質はJPEGの場合、0.7でもそれほど劣化はしませんでしたので、実装したいコンテンツによって使い分けてみてください。

ルートをつくる

では最後にルートです。
とはいってもコントローラーに追加した2つのルートだけでOKです。

<?php

use Illuminate\Support\Facades\Route;

// 省略

// Camera Capture
Route::get('camera_capture', [\App\Http\Controllers\CameraCaptureController::class, 'create']);
Route::post('camera_capture', [\App\Http\Controllers\CameraCaptureController::class, 'store']);

ちなみに – 1

当初はVue 3を使って実装しようとしていましたが、検証用に中古で買ったiPhone(5c…)が古いからか、まったく動きませんでした。

そのため、Vue 2で試してみたところうまくいったのでこちらを採用しました。(疑ってゴメンね、アロー関数さん。😂)

そろそろバージョンが上の検証機を買わなきゃダメですね・・・7か8あたりでいいかな❓❓

ちなみに – 2

今回、javascript-canvas-to-blobというパッケージも使っていますが、これはtoBlobをサポートしていないブラウザのための保険です。

これも先ほどの「ちなみに – 1」と同じく古い機種だからのようです。

そのため、もしかすると最新のiOSでは、このパッケージがなくても問題なく動く可能性もあります。(よろしければ、どなたか情報をいただけますと嬉しいですm(_ _)m)

テストしてみる

では、実際にテストしてみましょう❗

まず、スマホで「https://*******/camera_capture」へアクセスし、表示されるボタンをタッチします。

すると、カメラが起動しますので写真を撮って確定します。

写真を確定すると、プレビューが表示されますので、今回は帽子のロゴ部分だけを切り取ってみましょう。

そして、「アップロード」ボタンをタッチすると・・・・・・

はい❗
どうやら、アップロードがうまくいったようです。

では、実際にフォルダを見てみましょう。

cropped_imagesフォルダの中にファイルが保存されています。

では、最後に実際にアップロードされた画像を見てみましょう。(ブログ用に縮小してます)

成功です😊👍✨

ダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードできます。

「撮ってアップロード」機能をつくる

※ CDNを使っているのですぐに使えると思います👍

おわりに

ということで、今回はスマホ向けの「撮ってアップロード」機能を実装してみました。

正直なところ、テスト機のiPhone 5c ではさすがに難しいだろうと考えていましたが、処理速度は少し遅いものの、問題なく実装することができました。やっぱりデバイスの進化ってスゴイですね。

そして、やはり開発してみて感動したのがCropperですね。
実は今回は使う設定にはしていませんが、なんと画像の回転や拡大縮小なんてこともできるスグレモノです!

ちなみに私はこういった素晴らしいパッケージには必ずGitHubでスターをつけるようにしているんですが、今回ちゃんとクリックしたのに「スター解除」状態になってました。

・・・❓❓❓と思った数秒後、理解しました。

すでに以前スターをつけていたんですね・・・。つまり、Cropperの存在を忘れていたようです。ITの世界は広すぎるんですよね😂

ではでは〜❗

「塩パンがウマくて
仕方ないです✨」

今回の技術をつかった開発のご依頼、お待ちしております😊✨ お問い合わせ また、個人レッスンや、わかりにくい部分がありましたらからお気軽にご連絡ください。 どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly