ブラウザで画像ファイルのリサイズ処理

2022年12月20日 修正
resize処理を書き換え。
・imgタグを生成するのをやめて、window.createImageBitmap関数に変更。
・OffScreenCanvasが使えるブラウザでは OffScreenCanvasを使用し、使えない場合は cavas要素を生成して使用するように変更。


2021年9月11日 修正
ソース中の冗長的な構文を修正。


2021年9月15日
つづき。Web Worker。


最近のお話。あくまでフィクションです💦 進行中案件の担当さんにある日相談を受ける。


画像をウェブサーバーにアップロードしたいけど、大きいサイズはリサイズしてサーバーに保存したいんだよねー
でも、外注先の人が・・・
『サーバーで画像縮小するんで負荷が大きいし、画像処理にGD使うからPHPのmemory_limitを上げてくれないと・・・』
って言われてるんです!サーバーは共用のレンタルサーバーでmemory_limitなんて変更してくれそうもないし・・・。
『memory_limitを仮に上げてもアクセスが重なるとサーバー落ちてしまうかもしれないけど、責任持てない』
とも言われます!

何か方法ない?


んー、アップロードする前に JavaScript でリサイズすればいいんじゃね?って単純に思ったわけですけど・・・
そんなことも分かんねー外注先使ってんのか・・・っていうのが一つ目。
つーか、重くなるのが分かってんならサーバーで処理させるな! っていうのが2つ目

ってなわけで、サンプルコードを渡して「後は良きに計らえ」コース。
サンプルを作ってやれば、あとはなんとかしてくれるんじゃねーか? つか、なんでこっちがサンプル作んなきゃいけないんだよ!!!
本来外注先のシステム制作会社がやるべき仕事だろーよ!
まぁ、怒ってもしょうがない。そのレベルの外注先しかやってくれるところないんだからしょうがない。
こうやってこのブログのネタにできるんなら、怪我の功名というやつです。(ん?使い方間違ってないか?)

本題です。要は ブラウザで画像をリサイズしてBlobオブジェクトとして取得できれば、あとは FormDataを作ってappend するなり、してやれば万事解決です。

ググればJavaScriptとCanvas APIを使ったコードがゴロゴロ転がってるので参考にしてこちらの用途に合うように手を加えていく。
画像リサイズ処理自体は Canvas API を使えば簡単にできるので、後は File オブジェクトを FileReader で読み込んで、リサイズ後に canvasオブジェクトの toBlobメソッドで Blobオブジェクトを取得してやればいい。

ややこしいのが、それらすべてが非同期で処理しなければならない事。Promiseを使って一連の画像処理をしてやれば、await構文で同期処理のように待つことができる。

再利用できるように モジュールとして書いた。
簡単に説明すると、リサイズが必要な画像のFileオブジェクトと短辺の最大サイズを引数にして、コールするとリサイズされた画像がblobオブジェクトとして resolve される。

当然ブラウザのバージョンに依存してしまうが・・・ゴチャゴチャ言われたら ターゲットのブラウザ用に Babel で変換しちまえばいいし💦

これを下記コードのように input[type=file]のonChangeイベントハンドラでゴニョゴニョしてサーバーにアップロードする。
僕は jQuery が大好きなので jQuery を使う。何度も言うけど、やっぱり document.querySelector(‘p’) とかタイプするより、$(‘p’) の方がラクなんだよね。

当然ながら Internet Explorer は全バージョンエラーになる。
chrome/firefox や safari でもバージョンによっては動かない。。。これじゃダメだ!って言われたら、最終手段 Babel のご登場(⌒∇⌒)

僕は下記のような .babelrc を書いて・・・

 >> browserify ./index.js --transform babelify | uglifyjs -c -m --output ./es5/bundle.min.js 

とかやると、とりあえず IE11でもシンタックスエラーにはならないようになる。だけど、結局 FormData.appendメソッドでエラーになるけどね😫

まぁ、今更なんですが、Canvas APIではリサイズ処理だけじゃなく図形を描画したり、ピクセル単位で演算処理を行うことでいろんなことが可能なので、サーバーサイドで画像処理を行わず、余力のあるクライアント(フロントエンド)で前処理させてから・・・というのがイマドキのやり方なんだろうな、と思います。

編集可能な簡易グリッドビュー画面をイチから作ってみる

直接関係はありませんが、これ の続きです。

ここにきて、カンタンなグリッドを表示する仕事があって、それならセルの内容を書き換えるだけの簡易的なセル編集機能もあればなぁ、と家に帰ってから唐突に思いつき、テレビを見ながら数時間ぐらいで突貫工事で作った。

仕様的には・・・
(1)Excelファイル(もしくはUTF-8なCSVファイル)をブラウザに読込み、
(2)簡単なグリッドビュー表示を行い、
(3)セルをクリックしたら編集し、
(4)リターンキー押下で編集確定、
(5)エスケープキーで編集取消
という感じ。

これならHTMLとCSS、JavaScriptで百数十行ぐらいでいけそうです。
実際業務で使うには、変更内容を追跡し、サーバーへ送信してデータベースに反映して・・・というような事が考えられますが・・・そこまでやっとれん!w

まずは、見てくれ、画面デザインですが、あくまでサンプル的なものなので、ほとんど素のHTMLです。
CSSのGridなんて初めて書いてみました。普段はBootstrapのレイアウトグリッドを利用して画面を作っているんですが・・・CSSのグリッドって、なんか難解ですね・・・ドキュメントを一回読んだだけではオッサンには理解できません(笑) エクセルの読込には、みんな大好き SheetJS ライブラリを利用しました。

ただ、セルを結合しまくったりしてるような複雑なエクセルファイルとか読み込むとたぶん滅茶苦茶になります。そういう複雑なエクセルファイルを持っていないので試していないんだけど・・・

デモ画面はこちら

CSS自体はそんなに凝ったことしてないので、読めば大体やろうとしていることが分かるでしょう。

続いて、肝心のJavaScript ですが、ほとんど各種イベントの処理ぐらいで、これといって特筆するべき事はありません。
ところどころ jQuery 使って見苦しいのですが・・・jQueryだと明らかにタイプ量が減るので僕は多用しています。世間の流れとしてはjQueryは排除されつつあるのですが・・・やっぱりjQueryに慣れきってしまうと document.querySelector なんてタイプするのがすんげぇ面倒。jQueryだと $ の一文字ですよ?イベント登録も on の二文字ですよ? addEventListener とかタイプするのがアホらしくなりますけどね、オッサンは。

いつも利用させてもらってる疑似テストデータを利用してエクセルデータを流し込んでやると、こんな感じです。「こんな感じです」って言われてもね。。。

いちおう、エクセルファイルから簡易グリッドビューにデータを流し込み最低限の編集はできるようになりました。
・・・が、グリッドのHTML的な構造は見直す必要がありますよねぇ。

<div class="row body">
  <span class="cell"></span> ....延々とセルが続く
</div>

一つの要素(div.row.body)にすべてのセル(span.cell)を全部詰め込んで無理くりCSSでグリッド表示させてるだけなんで、<table>要素みたいに、行で分かれていないので、任意の行を選択したいときは、いちいち計算しないといけません。

ま、ちょっとだけグリッド表示したい、って時に使う用、と割り切る。

HTMLフォームでエンターキー押して入力要素のフォーカスを移動させたい、いや、しろよ!

2020年4月8日 間違い訂正を追記


HTMLでフォームをチマチマ組んでテストしていると、テキストボックス(input[type=text]要素)とかでついついエンターキー(改行キー)を押して、ムカッ!!!!!!とかしません? するよね、絶対するよね? 僕は自己嫌悪に陥って仕事を中断して帰りたくなる衝動に駆られます!

そこでググってjQueryのプラグイン探すなり、テック系の記事をコピペしたりしますよね。でも僕は探すより、自分で書く方が早いんですよ、結果的に。適当なjQueryプラグインなりを探してもそれの使い方を調べてたり、要らない機能があったりして、結局ドキュメントをよく読まないとわからないことが多い。

要は FORM要素内のINPUT要素に onkeyup  onkeydownイベントハンドラを仕込んでエンターキーをトラップすれば済む話。
たかだか数十行のスクリプトです。え?自分で組むとバグが!!!って? バグが出たらその都度直せばいいんです。ラクしちゃいけません。ラクするのはチラシの印刷だけでよろしい。

僕は jQuery 好き好き人間なので、適当にプラグインを書きました。

で、これを以下のようなフォームで使うと、このままでは失敗して、エンターキー押すとフォームが送信されてしまいます。

失敗の原因は、FORM要素内に input[type=submit]要素が存在するのが原因。
chromiumしか確認していないけど、どうやら input[type=submit]要素が存在すると、エンターキー押下で問答無用でFORMのアクションが走るようです。このinput[type=submit]要素を消すと送信されない、という挙動になってます。違ったらごめん。

追記:2020年4月8日
※ FORM要素内にinput[type=”text”]要素が一つしかない場合は、やっぱり問答無用でFORMのアクションが走るみたいです。嘘情報申し訳ない。上記はinput[type=”text”]が二つ以上ある場合のようです。一つしかないFORMの場合はダミーのinput[type=”text”]をどこかに配置して display: none; とかしとけばいいみたい。よく分からん仕様だなぁ。。。

じゃぁ、FORM要素にinput[type=button]要素なりa要素なりを書いて、そのonclickイベントハンドラ内で FORM要素のsubmitメソッドをコールすればいいんじゃない?ということになるんですが、残念ながらそうは問屋は降ろさない。
input[type=submit]要素がないと、FORM要素のvalidityチェックが走らないんだな、これが。せっかくinput要素に requireだのpatternだのを書いても無視されてしまう。

解決方法は2つ。

(1)
input[type=button]要素のonclickハンドラ内で、FORM要素のsubmitメソッドをコールする前に、reportValidityというメソッドをコールしてその返り値を判断して submitメソッドをコールするかどうかを決定する。

    <script type="text/javascript"><!--
      (function($) {
        $('form').enterNext();
        $('form [type=button]').click(function(ev) {
          if(this.form.reportValidity())
            this.form.submit();
        });

      })(jQuery);

    //--></script>

(2)
input[type=submit]要素を隠すか非表示(display: none;とか)にしておき、input[type=button]要素のonclickハンドラとFORM要素のonsubmitイベントハンドラを仕込んでおいてエンターキー押下かではなくクリックされたかをチェックさせる。

    <form>
    ....
    <input type="submit" style="display:none;" />
    <input type="button" value="送信" />
    </form>
    <script type="text/javascript"><!--
      (function($) {
        $('form').enterNext();

        $('form').submit(function(ev) {
          if(!'isClick' in this || this.isClick !== true)
          {
            ev.preventDefault();
            return false;
          }
        });
        $('form [type=button]').click(function(ev) {
          $('form').prop('isClick',true).find('[type=submit]').click();
        });

      })(jQuery);

    //--></script>

最初(2)の方を使ってたんだけど、(1)の方がカンタンなので、そっちにした。単に reportValidityというメソッドを後から知ったんだけどね💦

ちなみに、FORMのvalidityチェックの際、エラーメッセージをカスタマイズするプラグインもついで書いた。プラグインにするほどのものでもないけど、一回一回書くのはめんどっちーのでプラグインにした方がラク、というだけの理由ですけどね。

【jQuery】 jquery.simplePopup 書き直し

【2015年8月26日 ver2.0.9 更新】

仕事で使うために作ったプラグイン。

リンク先の画像などをブラウザ・ウィンドウ内にポップアップ表示するJavascriptライブラリはたくさんありますが、数が多すぎてどれがいいのか分からないのと、本体以外にCSSファイルや画像が必要であったりとややこしい。

最低限、一つのファイル(プラグイン本体のJSファイル)で設置できて、自分にとって使い勝手がいいものを勉強がてら作ってみよう、というのが4年前に作り出したきっかけ。

ドキュメント作成・備忘録も兼ねてアップ。
リンク先をウィンドウ内にポップアップ表示するjQueryプラグイン

jQuery.simplePopup-【jQueryプラグイン】

iframeのloadイベント 【書き直し】

今月の初めに書いた投稿が、完全に勘違いだったので、削除して改めて書き直し。

jQuery(1.7.2)でiframeを動的に生成して、そのloadイベントを書いてたときに、どうしても理解できないことが一つあります。
たぶん、僕が無知なだけかもしれませんが・・・

// &lt;a id=&quot;test&quot; href=&quot;http://somewhere.url/&quot;&gt;iframeにリンク先をロードさせる&lt;/a&gt;
// というような、HTMLがあって、

;(function($)
  {
    $('a#test').click(function(ev)
                      {
                        ev.preventDefault();
                        ev.stopPropagation();
                                          
                        //クリックされたアンカータグのhref属性を保存。
                        var href = this.href;
                        
                        //デバッグ用のカウンタ
                        var count = 1

                        //iframeを生成して、そこに読み込ませます。
                        var $iframe = $(document.createElement('iframe')).appendTo(document.body);
                        
                        //onloadイベントでチェック
                        $iframe.load(function()
                                    {
                                      console.log([count++,this.src]);
                                    });

                        //普通に読み込むと、ログには [1,'http://somewhere.url/'] が表示されます。
                        $iframe.attr('src',href);
                        
                        //window.setTimeout(function(){$iframe.attr('src',href);},100);
                        //しかし、IEやFireFoxだとsetTimeoutメソッドで強制的に非同期で読み込ませると、
                        //ログには・・・
                        //---------------------------
                        //[1,'']
                        //[2,'http://somewhere.url/']
                        //---------------------------
                        //と表示され、2回コールされています。
                        //しかし、safariやchromeなどのwebkit系のブラウザでは1回しかコールされません。
                      });
  })(jQuery);

単純にsrc属性を設定してロードした場合だと、どのブラウザでも1回しかコールされない。
だけど、setTimeoutを使用してロードを遅延(非同期)実行させると、IE/Mozilla系ブラウザは2回コールされる。
ただ、タイムアウトミリ秒を1とか10とかのようにすると1回しかコールされないときもある。

よく分からない。そういうものなの???