質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.04%

CanvasにdrawImageで描写した画像をtoDataURLで取得したい

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 231

clipsite_n.a

score 10

複数のcanvasを別のcanvas上でdrawImageを使って結合し、結合したcanvasのbase64を取得してサーバーに送信、png画像として保存したいのですが結合したcanvasのbase64がうまく取得できません。
base64っぽい文字列は取得できているのですが、保存した画像を見ると真っ白になっています。

let c = document.getElementById("canvas");
let ctx = c.getContext("2d");
let y = 0;//結合する画像のcanvasでの表示位置

//結合に使用するcanvasの大きさを設定
$("#canvas").attr({ height: ($("#img").height() * items.length) });//高さは保存した全ページを縦に表示できる高さにする
$("#canvas").attr({ width: $("#img").width() }); 

_.forEach(items, (data) => {//itemsに結合したいcanvasのデータが格納されている
                let img = new Image();
                img.src = data.imageData;//結合したいcanvasのbase64
                img.onload = function () {
                    ctx.drawImage(img, 0, y);
                    y += $("#canvas").height();
                }
            });

result =  c.toDataURL("image/png");

色々試したところ、最後のbase64の取得を5秒待って実行されるようにすると正常に画像が保存できました。

setTimeout(function () {
       result = c.toDataURL("image/png");
},5000)

おそらくdrawImageでの描写が完全に終わる前に、base64を取得していたのが原因だと思われますが、
描写が完全に終わってからtoDataURLを実行する方法はあるのでしょうか?
上記のように5秒も待ってしまうと処理が遅くなりますし、待ち時間を短くすると画像の描写が完全に終わる前にbase64を取得してしまうことがあるかもしれないので、そのようなイベントやメソッドがあればいいのですが見つかりませんでした…

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+2

_ というのは Lodash(underscore.js?) を使っているのでしょうか?
https://lodash.com/docs/4.17.4#forEach
items が配列であれば第二引数にインデックスが来るので取得しておきます。

_.forEach(items, (data, index) => {


すべての画像の load を待ってからでないと実行できないので、Promise(Deferred) を使います。
drawImage() の dy の計算を間違えていないか確認してください。

const height = $("#img").height();
const promises = [];
                const deferred = new $.Deferred();
                promises.push(deferred.promise());
                let img = new Image();
                img.onload = function () {
                    ctx.drawImage(img, 0, height * index);
                    deferred.resolve();
                }

                img.src = data.imageData;
$.when.apply($, promises).done(() => {
  result = c.toDataURL("image/png");
});


https://api.jquery.com/category/deferred-object/
https://foreignkey.toyao.net/archives/1147

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/08/23 17:29

    ご回答ありがとうございます!
    _ は Lodashです。
    ちょっと来週になってしまいそうですが、
    promissの使い方などもあまりわかっていないので、調べて試してみます!

    キャンセル

  • 2019/08/26 15:03

    教えていただいた正常にbase64形式の文字列が取得できました!
    迅速にご回答いただき助かりました。
    promiseって複数の結果を受け取れるんですね。
    ありがとうございました!

    キャンセル

+1

以下の記事は参考になりませんか?

canvas の画像をアップロード
http://surferonwww.info/BlogEngine/post/2015/07/02/upload-image-drawn-on-html5-canvas.aspx

不明な点があれば聞いてください。

【追記】

下の 2019/08/26 13:14 の私のコメントに「もう少し考えてみました・・・回答欄にそのあたりを追記しておきます」と書きましたが、それを以下に追記します。

コメント欄に「複数の画像を扱うということが何か関係してる」と書きましたがそんなことはなくて、ごく単純に一枚だけ扱った場合でも canvas.toDataURL("image/png"); を image.onload のリスナの外に記述すると Data Url は取得できません。

逆に言うと、一枚でも複数でも image.onload のリスナの中に canvas.toDataURL("image/png"); を書けば Data Url は取得できます。もちろん setTimeout で待つ必要は有りません。

複数の画像の場合、複数の image.onload イベントに複数のリスナがアタッチされますが、そのリスナにどのように  canvas.toDataURL("image/png"); を記述するかが考えどころです。

画像の読み込みにかかる時間が画像のサイズなどによって異なるので、load イベント発生の順番は不定となりますが、それに対応するために最後の load イベントの発生を待って、そのイベントリスナで希望する順番に各 image を drawImage で canvas に書き込むのがよさそうです。

そうすれば、そのリスナで canvas に書き込んだ後、同じリスナ内で canvas.toDataURL("image/png"); によって Data Url を取得できます。

検証に使ったコードを以下にアップしておきます。元の画像は 226px x 150px の .png または .jpg 画像です。(ASP.NET のページを利用していますが、そこは本題とは関係ないので気にしないでください)

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeFile="0076-Canvas.aspx.cs" Inherits="_0076_Canvas" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <script type="text/javascript">
        window.onload = function () {
            var srcs = [
                "/Images/image1.png",
                "/Images/image2.jpg",
                "/Images/image3.jpg",
                "/Images/image4.jpg",
                "/Images/image5.jpg"
            ];

            var canvas = document.getElementById('mycanvas');
            var context = canvas.getContext('2d');


            // 単純に一枚の画像の場合
            // これは OK だが、canvas.toDataURL("image/png") をリスナの外に出すとダメ
            //var image = new Image();
            //image.src = "/Images/image1.png";
            //image.onload = function () {
            //    context.drawImage(image, 0, 0);
            //    var result = canvas.toDataURL("image/png");
            //    document.getElementById("image1").src = result;
            //};


            // 質問者さんのコードと似たような感じにしてみましたが、load イベントの発生タイミング
            // は i の順番にならず、canvas に描かれる順番が保証されないのでダメ
            //var y = 0;
            //for (let i = 0; i < srcs.length; i++) {
            //    let image = new Image();
            //    image.src = srcs[i];
            //    image.onload = function () {
            //        context.drawImage(image, 0, y);
            //        y += 150;
            //    }
            //}


            // canvas に描かれる順番を保証するには以下のようにするのがよさそう。そうすれば
            // リスナの中に canvas.toDataURL("image/png"); を記述でき Data Url が取得できる
            var images = [];
            var loadCount = 0;
            for (let i = 0; i < srcs.length; i++)
            {
                images[i] = new Image();
                images[i].src = srcs[i];
                images[i].onload = function () {
                    loadCount++;
                    if (loadCount == images.length)
                    {
                        for (let j = 0; j < images.length; j++)
                        {
                            context.drawImage(images[j], 0, j * 150);
                        }

                        // image.onload のリスナの中でないと Data Url が取得できない
                        var result = canvas.toDataURL("image/png");
                        document.getElementById("image1").src = result;
                    }
                };
            }

            // ここではダメ。Data Url は取得できない
            //var result = canvas.toDataURL("image/png");
            //document.getElementById("image1").src = result;
        }
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <div style="float: right;">
            <canvas id="mycanvas" width="266" height="750"></canvas>
        </div>
        <div>
            <img id="image1" alt="" />
       </div>
    </form>
</body>
</html>

結果は以下のようになります。

イメージ説明

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/08/26 13:14

    もう少し考えてみました。

    上に「複数の画像を扱うということが何か関係してる」と書きましたがそんなことはなくて、ごく単純に一枚だけ扱った場合でも canvas.toDataURL("image/png"); を image.onload のリスナの外に記述すると Data Url は取得できません。

    回答欄にそのあたりを追記しておきます。

    キャンセル

  • 2019/08/26 15:05

    上記の方法で正常に画像を保存できました。
    また、詳しくご説明いただいたおかげでやっと原因を理解できました。
    複数のimage.onloadイベントのリスナに記述されたcanvas.toDataURL("image/png")がすべて実行される前に
    canvas.toDataURL("image/png")でbase64形式の文字列を取得していたため正常な文字列が取得できなかった、ということですね。
    複数のImageオブジェクトに対して一括でonloadイベントを呼び出せるのは初めて知りました。
    ありがとうございました!

    キャンセル

  • 2019/08/26 17:36

    > 複数のimage.onloadイベントのリスナに記述されたcanvas.toDataURL("image/png")がすべて実行される前にcanvas.toDataURL("image/png")でbase64形式の文字列を取得していたため正常な文字列が取得できなかった、ということですね。

    いえ、そうではなくて、質問者さんのコードで言うと ctx.drawImage(img, 0, y); が実行される前に result = c.toDataURL("image/png"); が実行されたので Data Url が取得できなかったということだと思います。

    forEach ループで各 image の onload イベントにリスナ function () { ... } をアタッチしていますが、それは単にリスナをアタッチしただけで、リスナの中の ctx.drawImage(img, 0, y); が実行されるのはもっと後の onload イベントが発生した時です。

    質問者さんのコードは、リスナをアタッチした後、イベントの発生を待たず、即 c.toDataURL("image/png"); 実行することになります。その時点では canvas に画像が drawImage されていないので Data Url が取得できないということです。

    それから、回答にも書きましたが、「画像の読み込みにかかる時間が画像のサイズなどによって異なるので、load イベント発生の順番は不定となります」という点には十分注意してください。今の質問者さんのコードではその問題が回避できません。Promise(Deferred) を使っても、そこは同じだと思われます。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.04%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる