UserScriptでカイゼンを始めてみた

◆ Live配信スケジュール ◆
サイオステクノロジーでは、Microsoft MVPの武井による「わかりみの深いシリーズ」など、定期的なLive配信を行っています。
⇒ 詳細スケジュールはこちらから
⇒ 見逃してしまった方はYoutubeチャンネルをご覧ください
【4/18開催】VSCode Dev Containersで楽々開発環境構築祭り〜Python/Reactなどなど〜
Visual Studio Codeの拡張機能であるDev Containersを使ってReactとかPythonとかSpring Bootとかの開発環境をラクチンで構築する方法を紹介するイベントです。
https://tech-lab.connpass.com/event/311864/

こんにちは、サイオステクノロジーの馬場です。

私の携わっているプロジェクトでは、
飲食店のオペレーション全般をサポートするシステムを、スクラッチから開発しています。

その中には、商品メニューを管理するためのウェブアプリもあるのですが、
このウェブアプリは、規定の寸法から外れる商品画像を「拒絶する」という硬派な仕様になっています。

商品メニュー編集を代行しているメンバー (私じゃない) は、この仕様がツラそうなんですが、
限られた予算の中で、このツラさが解決されるまでの道のりは遠く険しそう、という状況です。

この記事では、それを見た私がとった行動を時系列で晒しつつ、最後に、これによる成果を妄想します。

時系列

アイディア

以下のように考えました:

  • サイズが違ったら、自動リサイズしてあげれば良いだろう。
    • 「メニューに掲載するような写真を、システムで自動的に加工して良いの?」
      という懸念が、現状の硬派な仕様を生んでいると思われるが、
      そういうことを気にする場合は、きちんと手動リサイズしたものを上げれば良い。
      (サイズが合っていれば自動リサイズされないようにする)
  • そのリサイズは、クライアントサイドで済ませられるだろう。
    • JavaScript ライブラリがあるはず。
  • ブラウザの UserScript で試してみよう。
    • まずは、 “Proof of Concept” として、やる。
      • 本体に組み込むのは、担当エンジニアに任せる。
    • 何なら、「運用上の工夫」として、やる。
      • メニュー編集担当メンバーが UserScript を育て続けても良い。

つまり「 UserScript で試すというステップを設ければ、世界が拡がるかもしれない」と考えました。

JS の画像処理ライブラリを探す

GitHub でのスター数とかを参考に、これにしました:

Jimp:
https://github.com/oliver-moran/jimp

“Chrome Greasemonkey” でググる

チームの標準ブラウザは Chrome です。
ググってみると、Tampermonkey というものがあるようです:

Tampermonkey:
https://www.tampermonkey.net/

Greasemonkey の後継で、主要なブラウザには大体対応しています。Chrome にインストールしました。

Tampermonkey を試す

簡単に試したところ、
ブラウザで閲覧中の任意のウェブページを、JavaScript (UserScript) で
好きなように加工できることが分かりました。

早速、上記 Jimp を使ったコードを書き始めました。

UserScript を書く

方針としては、
最初に説明したとおり、アップロードされたファイルをチェック & 拒絶している箇所があるわけなので、
その部分を丸ごと差し替えるイメージで進めました。

その箇所は、 input[type=file] の onChange のハンドラーとして実装されていたので、
元のハンドラーを解除 (off) して、書き換えたほうのハンドラーを登録 (on) すれば良いはずです。

最終的には、こんな感じになりました:

/* 
 * コンセプトを伝えるために、簡略化しています。
 * コピペで動作するものではありません。 
 */

// ウェブページ側のブロックスコープで隠蔽されていて、 UserScript から参照できない変数は、
// UserScript 側でも定義。
const sizes = {
  small: {width: 120, height: 120},
  medium: {width: 510, height: 340},
  large: {width: 1200,height: 800},
}
const photoShadow = 'input[type=file]#photo-shadow';
$(document)
  .off('change', photoShadow) // 元のハンドラーを解除 (off)
  .on('change', photoShadow, function(){ // 新しいハンドラーを登録 (on) 
    const fileReader = new FileReader();
    fileReader.onload = function (event) {
      const data = event.target.result;
      const base64Prefix = 'data:image\\/jpeg;base64,';
      const dataBase64 = data.substring(base64Prefix.length - 1);
      if (data.match(base64Prefix)) {
        const img = new Image();
        img.onload = function() {
          EXIF.getData(img, function() {
            const colorSpace = EXIF.getTag(this, 'ColorSpace');
            if (dataBase64.slice(0, 4) !== "/9j/") {
              // JPEG じゃない!
              window.alert("JPEGファイルであることを確認してください");
            } else if (colorSpace && colorSpace != 1) {
              // カラープロファイルが sRGB じゃない!
              window.alert('カラープロファイルがsRGB(1)であることを確認してください');
            } else if (img.width !== sizes[size].width || img.height !== sizes[size].height) {
              // サイズが違う! (size はグローバル変数)

              /* ▼ ここから書き換えた部分 */
              Jimp.read(data)
                .then(function(image){
                  return image
                    .cover(sizes[size].width, sizes[size].height) // sizes[size] どおりにリサイズ
                    .getBase64(Jimp.MIME_JPEG, function(err, d){
                      $(dataForm).val(d.substring(base64Prefix.length - 1)); // リサイズ後のデータを保持
                      $(preview).attr('src', d); // プレビュー表示
                    });
                })
                .catch(function(err){
                  console.error(err);
                });
              /* ▲ ここまで書き換えた部分 */

            } else {
              // オールオッケー!
              $(dataForm).val(dataBase64);
              $(preview).attr('src', data);
            }
          });
        };
        img.src = data;
      }
    };
    fileReader.readAsDataURL(this.files[0]);
    
    // $(photoShadow) はリセットして使い回す。
    $(photoShadow).replaceWith($(photoShadow).val('').clone(true));
  });

 

(Tips 1) 変数のスコープについて

これはTampermonkey のエディタのスクリーンショットです。
warnings

UserScript からウェブページ側の変数を参照している箇所で、
エディタにとっては未定義の変数なので、エディタがその旨をワーニングしています。

これを見て「// @grant unsafeWindow が必要なのかな?」と思って、試したりしましたが、
結局は不要で、ウェブページ側のグローバル変数は普通に UserScript から参照できるようです。

unsafeWindow:
https://www.tampermonkey.net/documentation.php#unsafeWindow

それとは別に、
ウェブページ側で { let ... } { const ... } とブロック内で宣言されている変数は、
言語仕様上(ブロックスコープ)、ブロック外である UserScript からは見えません。
これは、いずれ本体(ウェブページ)側に組み込まれれば解決する問題なので、
素直に UserScript 側にコピペしました。

(Tips 2) @match について

Tampermonkey では、
どのウェブページで UserScript を有効にするかを指定する @match 記法が用意されています。

@match はワイルドカード記号 * をサポートしていて便利ですが、
この * は、正規表現とも(シェル, glob の)ワイルドカードとも違う独自のものなので、
注意が必要です: https://developer.chrome.com/extensions/match_patterns

チーム内でシェアして完了

ひとまず PoC としては完成したので、メニュー編集担当メンバーにシェアしました。
担当メンバーは「あの時、これがあれば…!」と言っていたので、PoC としては成功でしょう。

チーム内の課題管理システム上で、この件のチケットに UserScript を添付して、あとは託しました。

期待している成果

UserScript を活用することで、以下のような変化を期待しています:

  • 担当メンバーが「手元で工夫すれば業務改善できる」という認識をもつ。
  • 手元(こちら側)で試すというステップを経ることで、
    本体(あちら側)に取り込まれるプロセスがスムーズになる。

つまり、「こちら側」と「あちら側」の架け橋となって、
この手のカユいツラいところの解決への道筋を示すものになれば、と期待しています。

次回予告

また、そのうち、
何かやってみた時の行動を時系列で晒しつつ、最後に成果物も晒し、「あるある」的な共感を得る
というフォーマットで、書こうと思います。 (=「◯◯てみた」シリーズ)

 

アバター画像
About baba 8 Articles
emacs で javascript と css を書いていれば幸せなオジサン: github.com/tonextone , facebook.com/foofoobarbar
ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

1人がこの投稿は役に立ったと言っています。


ご覧いただきありがとうございます。
ブログの最新情報はSNSでも発信しております。
ぜひTwitterのフォロー&Facebookページにいいねをお願い致します!



>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!

Be the first to comment

Leave a Reply

Your email address will not be published.


*


質問はこちら 閉じる