【Vue 3】複数画像を1枚にまとめる機能をつくる(DLも可!)

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

さてさて、私も開発者のひとりとして優秀なプログラマさんが多数いらっしゃるTwitterはかかさずチェックするようにしています。

そして、その影響でプライベートの写真もたまにツイートするようになったのですが、この間「あること」に気がつきました。

それは・・・・・・

画像が4枚までしかアップロードできない😫

というものです。

個人的に旅行などに行くとたくさん写真をとるので、ぜひもっとアップロードしたい気持ちはあるのですが、制限があってはどうしようもありません。

すると、ふと思いつきました。

だったら、画像を1枚にまとめればいいじゃないか! 

と。

そこで❗

今回は、Vue 3を使って複数画像をひとまとめにする機能をつくってみたいと思います。

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

【追記:2023.8.25】このページのコードを元にして改良したものを私が運営する「街角コレクション」でツールとして公開しています。もしよろしければそちらもぜひ!

📝 ひとまとめ画像ツール

「リアル・天空の城ラピュタ、
最高でした❗」

開発環境: Vue 3

やりたいこと

今回やりたいことは次のとおりです。

  • 1つの画像の長さが変更できる
  • 何列に配置するか変更できる
  • 作成した画像をダウンロードできる

なお、写真は縦長&横長があるので、今回は中心部分を切り抜いて正方形にし、それらを並べて1枚の画像にまとめてみます。

では実際にやっていきましょう❗

実際のコード

…とは言っても、今回はVue 3のみで完結しているので、いきなりコードのご紹介です(笑)

merge_image.html

<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div id="app" class="p-5">
    <h1>複数画像を1つにまとめるサンプル</h1>
    <div class="p-3">
        <label>1つの画像の長さ(px)</label>
        <input type="text" class="form-control w-50" v-model="blockImageLength">
    </div>
    <div class="p-3">
        <label>何列?</label>
        <input type="text" class="form-control w-50" v-model="columnCount">
    </div>
    <div class="p-3">
        <label>
            画像
            <span v-if="showDownloadLink">
                (<a href="#" @click.prevent="downloadImage">ダウンロードする</a>)
            </span>
        </label><br>
        <label class="btn btn-primary mt-2">
            画像を選択する
            <input type="file" multiple accept="image/jpeg,image/png" class="d-none" @change="onFileChange">
        </label>
    </div>

    <!-- 写真をまとめるキャンバス -->
    <canvas id="canvas"></canvas>

    <!--ダウンロード用リンク-->
    <a id="download_link"></a>
    
</div>
<script src="https://unpkg.com/vue@3.2.31/dist/vue.global.prod.js"></script>
<script>

    const { createApp, ref, computed } = Vue;

    createApp({
        setup() {

            // Data
            const blockImageLength = ref(200); // ひとつの画像の長さ(px)
            const columnCount = ref(3); // 何列?
            const showDownloadLink = ref(false); // ダウンロードリンクを表示するかどうか

            // Canvas
            const getSourceConfig = img => { // 選択画像の設定を取得する

                const width = img.width;
                const height = img.height;
                let sourceImageX = 0;
                let sourceImageY = 0;
                let sourceLength = 0;

                if(width > height) { // 横長の場合

                    sourceImageX = width / 2 - height / 2;
                    sourceLength = height;

                } else if(width < height) { // 縦長の場合

                    sourceImageY = height / 2 - width / 2;
                    sourceLength = width;

                }

                return {
                    sourceImageX,
                    sourceImageY,
                    sourceLength
                };

            };
            const getDestinationConfig = index => { // キャンバスの設定を取得する

                const destinationLength = blockImageLength.value;
                const columns = columnCount.value;
                const destinationX = index % columns * destinationLength; // X座標が1回ずつずれていく
                const destinationY = Math.floor(index / columns) * destinationLength; // Y座標が1回ずつずれていく

                return {
                    destinationX,
                    destinationY,
                    destinationLength
                };

            };

            // File
            const onFileChange = e => {

                const files = e.target.files;
                const fileLength = files.length;

                if(fileLength > 0) {

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

                    // キャンバスの初期化
                    canvas.width = blockImageLength.value * columnCount.value;
                    canvas.height = blockImageLength.value * Math.ceil(fileLength / columnCount.value);
                    ctx.fillStyle = '#000';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);

                    for(let i = 0; i < fileLength; i++) {

                        const file = files[i];
                        let reader = new FileReader();  // ファイルを読み込む
                        reader.readAsDataURL(file);
                        reader.onload = e => {

                            let img = new Image(); // 画像を読み込む
                            img.onload = () => {

                                // 各種設定を取得する
                                const { sourceImageX, sourceImageY, sourceLength } = getSourceConfig(img);
                                const { destinationX, destinationY, destinationLength } = getDestinationConfig(i);

                                ctx.drawImage( // キャンバスへ描画
                                    img,
                                    sourceImageX,
                                    sourceImageY,
                                    sourceLength,
                                    sourceLength,
                                    destinationX,
                                    destinationY,
                                    destinationLength,
                                    destinationLength,
                                );

                            };
                            img.src = e.target.result;

                        }

                        if(i === fileLength - 1) { // 最後の描画をするときにダウンロードリンクを有効化

                            showDownloadLink.value = true;

                        }

                    }

                }

            };

            // Download
            const downloadImage = () => { // キャンバスの内容を JPEG としてダウンロード

                const link = document.getElementById('download_link');
                link.href = canvas.toDataURL('image/jpeg');
                link.download = 'merge_image_'+ new Date().getTime() +'.jpg';
                link.click();

            };

            return {
                blockImageLength,
                columnCount,
                showDownloadLink,
                onFileChange,
                downloadImage
            }

        }
    }).mount('#app');

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

この中で一番重要なのが、選択された画像をキャンバスに描画するctx.drawImage()の部分です。

パラメータはそれぞれ、以下のようになっています。

drawImage(
    image, // 描画する画像データ
    sx, // 画像データの切り抜く位置(X座標)
    sy, // 画像データの切り抜く位置(Y座標)
    sWidth, // 画像データの切り抜く横の長さ
    sHeight, // 画像データの切り抜く縦の長さ
    dx, // 切り抜いた画像データを描画するキャンバスの位置(X座標)
    dy, // 切り抜いた画像データを描画するキャンバスの位置(Y座標)
    dWidth, // 切り抜いた画像データを描画する横の長さ
    dHeight // 切り抜いた画像データを描画する縦の長さ
)

つまり、位置と長さを指定することで自動的に縮小/拡大できるということになります。

詳しくは以下のページをご覧ください。(説明画像がわかりやすかったです👍)

📝 参考ページ: CanvasRenderingContext2D.drawImage()

また、ダウンロード部分は以前に公開した以下の記事をご覧ください。

📝 参考ページ: たった8行!キャンバスをワンクリックで保存させるJavaScriptコード(IEも大丈夫)

※ 今回IEはもう対応してません。あしからず m(_ _)m

今回は速いですが、これで完了です。
お疲れ様でした❗

テストしてみる

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

今回テストで使うのは、この間ついに行くことができた「リアル・天空の城ラピュタ」と呼ばれる 和歌山県・友ヶ島 の写真です。

では、まずブラウザで「https://******/merge_image」へアクセスします。

すると、以下のフォームが表示されるので、設定はそのまま「画像を選択する」ボタンをクリックします。

画像を選択するダイアログが表示されるので「リアル・天空の城ラピュタ」の写真をいくつか選択すると・・・・・・・・

はい❗
1枚にまとまった画像が作成できました。

では、「ダウンロードする」リンクをクリックしてダウンロードができるかチェックします。

どうなったでしょうか・・・・・・???

はい❗
うまくダウンロードできました。

ダウンロードした実際の画像はこちらです。
↓ ↓ ↓

成功です😊👍

デモページを用意しました

せっかくなので、今回のコードを使ったデモページをご用意しました。
ぜひ実際に使って試してみてくださいね。

📝 デモページ

企業様へのご提案

このように、最近のブラウザでは(ちょっとした数学知識は必要ですが)画像加工をすることも簡単にできるようになってきています。

そのため、もしシステム上で画像を操作したい場合はぜひお問い合わせからご相談ください。

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

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

おわりに

ということで、今回はVue 3で複数写真をひとまとめにしてみました。
結構実用性があると思うので、ぜひデモページも利用していただけると嬉しいです。

なお、もしサービスとして実際に運用するとしたら、以下のような機能があったほうがいいかもしれません。

  • 画像の順番を変更できるようにする
  • 数が合っていないと黒い背景になるので、最後の画像は大きくするなどして埋める
  • 画像をいつでも追加・削除できる

皆さんはどんな機能を追加してみたいでしょうか。
ぜひやってみてくださいね。

ではでは〜❗

「和歌山のクラフトビールも
うまかった🍺✨」

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