Vue 3 + Cropper.js でプライバシー保護のための「ぼかし」機能をつくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、私は以前から「ちょっと変わった場所を旅行する」という街角コレクションという、(100パー自己満足の)個人サイトを運営しています。

全く利益は出ていないのですが、このサイトを運営していると訪問者さんが写真を投稿してくれたりして「おっ、仲間がいるぞ😄」となりして、日々が充実していたりしまう。

そして、そういった投稿写真で気をつけているのが、

プライバシー保護

です。

つまり、写真に写り込んでいる以下のような部分は「ぼかし」をいれないといけないわけです。

  • 人の顔
  • 車のナンバー
  • 家の表札

などなど。

そして、そんなときはいつも愛用している画像編集ソフトgimpを使っているのですが、正直写真がたくさんあると「うーん、今は忙しいのにまいったな…😅」となる場合もあります。

※ いや、投稿していただくのはめちゃくちゃ嬉しいんで、今後もぜひお願いします❗m(_ _)m < ぷりーず

そこで作業効率をあげるために、ウェブ上で「ぼかし」に特化したページをつくろうということになったので、ここでそのコードを共有したいと思います。

ぜひ何かの参考になりましたら嬉しいです。😄✨

「影響力の武器(第3版)
を読み始めました。
文が簡単になったそうです❗」

開発環境: Laravel 10.x、Vue 3.4.18(Options API)、Cropper.js 1.6.1、TailwindCSS 3.4.1

やりたいこと

今回実装したいのは以下のとおりです。

  • 表示された画像をマウスで範囲指定するとそこに「ぼかし」が入る
  • 作業が失敗することもあるので、1つずつ前に戻れるようにする
  • ぼかしの強さを「弱い」「中ぐらい」「強い」の3つから選べるようにする

それでは今回も楽しんでやっていきましょう❗

ルートをつくる

今回はほぼLaravelは関係ないのでシンプルにビューを呼び出す形でルートをセットします。

routes/web.php

Route::get('canvas_blur', fn() => view('canvas_blur'));

ビューをつくる

では、ルートにセットしたビューをつくります。
以下のコマンドを実行してください。

php artisan make:view canvas_blur

すると、ファイルが作成されるので中身を以下のようにします。

resources/views/canvas_blur.blade.php

<html>
<head>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.css">
    <style>

        /* ① Cropper の見た目を変更する */
        #canvas-container {
            max-width: 100%;
        }

        #canvas {
            width: 100%;
        }

    </style>
</head>
<body>
    <div id="app" class="p-5">
        <div class="mb-5">
            <label class="bg-blue-500 text-white px-3 py-2 rounded cursor-pointer mr-3">
                画像を選択する<input type="file" class="hidden" accept="image/*" @change="onFileChange">
            </label>
            <a href="#" type="button" class="text-blue-500 underline mr-3" v-if="imageData" @click.prevent="downloadImage">画像をダウンロード</a>
            <a href="#" type="button" class="text-blue-500 underline" v-if="cropHistories.length > 0" @click.prevent="revertCanvas">ひとつ前に戻る</a>
        </div>
        <div class="mb-5">
            ぼかしの強さ
            <select class="block w-32 p-2 border border-gray-300 rounded" v-model="blurStrengthIndex">
                <option v-for="(strength,index) in blurStrengths" :value="index" :key="index" v-text="strength.label"></option>
            </select>
        </div>
        <div id="canvas-container">
            <canvas id="canvas"></canvas>
        </div>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.js"></script>
    <script src="https://unpkg.com/vue@3.4.18/dist/vue.global.prod.js"></script>
    <script src="https://cdn.tailwindcss.com/3.4.1"></script>
    <script>

        Vue.createApp({
            data() {
                return {
                    imageData: null,
                    canvas: null,
                    ctx: null,
                    cropper: null,
                    cropHistories: [],
                    blurStrengthIndex: 1,
                    blurStrengths: [ // ぼかしの強さ
                        { value: 5, label: '強い' },
                        { value: 15, label: '中ぐらい' },
                        { value: 30, label: '弱い' },
                    ]
                }
            },
            methods: {
                setCropper() { // ② Cropper を設定する

                    if(this.cropper !== null) {

                        this.cropper.destroy();

                    }

                    this.cropper = new Cropper(this.canvas, {
                        background: false,
                        zoomable: false,
                        autoCrop: false,
                        cropend: this.onCropEnd,
                    });

                    document.getElementById('canvas').classList.remove('cropper-hidden');

                },
                renderCanvas() { // ③ Canvas に描画する

                    this.ctx.drawImage(this.imageData, 0, 0, this.canvas.width, this.canvas.height);

                    this.cropHistories.forEach((history) => {

                        const { x, y, width, height, blurStrength } = history;

                        this.ctx.filter = `blur(${blurStrength}px)`;
                        this.ctx.drawImage(this.canvas, x, y, width, height, x, y, width, height);
                        this.ctx.filter = 'none'; // フィルターをリセットする

                    });

                    this.setCropper();

                },
                revertCanvas() { // ④ ひとつ前に戻る

                    this.cropHistories = this.cropHistories.slice(0, -1);
                    this.renderCanvas();

                },
                downloadImage() { // ⑤ 画像をダウンロードする

                    const date = new Date();
                    const year = date.getFullYear();
                    const month = date.getMonth() + 1;
                    const day = date.getDate();
                    const hour = date.getHours();
                    const minute = date.getMinutes();
                    const second = date.getSeconds();
                    const fileName = `${year}-${month}-${day}_${hour}-${minute}-${second}.png`;
                    const link = document.createElement('a');
                    link.href = this.canvas.toDataURL('image/png');
                    link.download = fileName;
                    link.click();

                },
                onCropEnd() { // ⑥ ぼかす位置が範囲選択されたとき

                    const cropperData = this.cropper.getData();
                    const blurStrength = this.blurStrengths[this.blurStrengthIndex].value;
                    this.cropHistories.push({
                        x: cropperData.x,
                        y: cropperData.y,
                        width: cropperData.width,
                        height: cropperData.height,
                        blurStrength: blurStrength,
                    });
                    this.renderCanvas();

                },
                onFileChange(e) { // ⑦ 画像が選択されたとき

                    const file = e.target.files[0];

                    if(file instanceof File) {

                        const reader = new FileReader();
                        reader.onload = (e) => {

                            this.imageData = new Image();
                            this.imageData.onload = () => {

                                let width = this.imageData.width;
                                let height = this.imageData.height;

                                if(width > this.maxWidth) {

                                    height = Math.round(height * this.maxWidth / width);
                                    width = this.maxWidth;

                                }

                                this.canvas.width = width;
                                this.canvas.height = height;

                                this.ctx.drawImage(this.imageData, 0, 0, width, height);
                                this.setCropper();

                            }
                            this.imageData.src = e.target.result;

                        }
                        reader.readAsDataURL(file);

                    }

                },
            },
            mounted() {

                this.canvas = document.getElementById('canvas');
                this.ctx = this.canvas.getContext('2d');

            }
        })
        .mount('#app')

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

はい❗
今回はこのコードだけですので、ひとつずつ見ていきましょう。

① Cropper の見た目を変更する

今回ぼかす範囲を指定するのは Cropper.js という有名パッケージを使っているのですが、デフォルトの設定では選択した画像が真ん中に来てしまうので、カスタムのスタイルシートをセットしています。

② Cropper を設定する

ここで、Cropper.jsを起動しています。

コードを見ていただくと分かるとおり、もしすでにCropper.jsが起動している場合は一旦破棄してから再度新しく起動することになります。

また、オプションとして設定されたcropendは範囲指定が完了したときに呼ばれるイベントになります。(中身に関しては後でご紹介します👍)

③ Canvas に描画する

この部分で重要なのが「やりたいこと」でも書いた「これまでの作業を1つずつ前に戻れるようにする」という部分です。

そのため、cropHistoriesには「どの範囲を、どの強さでぼかすのか?」という情報が1つずつ溜まっていき、それを元にして画像を描画&ぼかしを行っているという訳です。

つまり、複数ヶ所の「ぼかし」もできるようになっています😄

④ ひとつ前に戻る

③でも紹介した「作業内容」が溜まっていくcropHistoriesの最後のデータを削除し、再度画像を描画することで「1つ前に戻す」ことができるようになっています。

⑤ 画像をダウンロードする

現在表示されている画像をダウンロードする部分です。
流れとしては以下になります。

  1. キャンバスから画像データ(dataURL)を取得
  2. リンクを作って href にその画像データをセット
  3. リンクに download パラメータをつける
  4. 最後にそのリンクを「クリックしたことにする」

⑥ ぼかす位置が範囲選択されたとき

ここで、「作業内容」を追加することになります。

つまり、cropHistoriesの配列に新しく「どの範囲を、どの強さでぼかすのか」のデータが追加になるわけです。

⑦ 画像が選択されたとき

これは「ぼかし処理」をしたい画像を選択したときの処理になります。

中身としてはいろいろとwidthheightを変更していますが、基本的な流れは以下になります。

  1. 指定されたファイルから画像データを取得
  2. それをキャンバスへ描画
  3. Cropper.js を起動する

今回は短いですがこれで作業は終了です。
お疲れ様でした😄✨

テストしてみる

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

まず「https://********/canvas_blur」へアクセスしてみましょう。
すると、以下のようなフォームが表示されるので、まずは画像を選択してみます。

すると・・・・・・

はい❗
画像が表示されました。(私の写真ですが、もう近影ではなくなってしまいました。そろそろ変え時かもですね…😅)

では、顔部分に「ぼかし」を入れてみましょう❗

どうなるでしょうか・・・・・・

はい❗
顔に「ぼかし」が入りました。

成功です😄

では、作業を間違えたことを想定して手の部分をぼかしてから「ひとつ前に戻る」で元に戻せるか確認してみましょう。

「ひとつ前に戻る」をクリック。

すると・・・・・・

はい❗
元に戻すことができました。

これも成功です😄

では最後に「ぼかしの強さ」を変えて2箇所実行してみましょう。

うまくいくでしょうか・・・・・・

はい❗
ちょっとわかりにくいかもしれませんが、顔の右半分と左半分で「ぼかしの強さ」を変えています。

これも成功です😄✨

実際のページはこちら❗

今回のコードを使用して「街角コレクション」に新しく作ったページはこちらです。

ぜひ皆さんも試してみてくださいね。

📝 写真のぼかしツール(街角コレクション)

企業様へのご提案

今回のように日々の業務をショートカットするページを作成することで、作業効率を一段と上げることができます。

また、各企業様の独特な仕様にも対応できるかと思いますので、もしそういった機能をご希望の場合はぜひお問い合わせからご相談ください。

お待ちしております。😄✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はほぼJavaScript部分だけの記事を書いてみました。

ちなみに私はこういった「うーん、何回もおんなじ作業めんどうだな…😅」というものは作業効率を上げるために個人的にシステムをつくっています。

例えば、メールの返信もクリックするだけで定型テキストに差し込んでくれるようにしたり、承認作業はクリックだけでOKにしたり、写真投稿があったときにメッセージをクリックだけで作成できるようにしたり。

ホントにプログラマで良かったと思う瞬間ですね👍

ぜひ皆さんも「うーん、毎回面倒❗❗」を自分で解決できるシステムをつくってみてくださいね。

ではでは〜❗

「とはいえ、自分でつくった
PHPフレームワークは
史上最低の出来でした…Laravelすげえ😄」

このエントリーをはてなブックマークに追加       follow us in feedly