どうしてもWindows10の起動サウンドを鳴らしたい

期限が迫っているのに、どーでもいいことが気になって時間食ってしまう💦
どーでもいいプログラムを書く時間です。

歴代のWindowsで一番気に入ってるスタートアップサウンドは、Windows2000とMe の時のピアノ音。あれが一番心地よい。まぁ人それぞれなんですが・・・

最近のWindowsって起動サウンドが鳴らないんですね・・・ググると Windows8ぐらいからデフォルトで無効にされているみたいですね。まぁ仕事場のパソコンは別にいいとして、家で使うPCぐらい鳴らしてもいいんじゃないすかねぇ。

で、そもそも コンパネのサウンドの設定で、起動時のサウンドを鳴らす設定が消されています。なんで?
これもググるとレジストリをいじることで有効になるみたいですが、結局はシステムにブロックされて起動サウンドは鳴らないみたいでつね。。。

しかたないので、タスクスケジューラに登録することにした。起動サウンドのWAVファイルは Windows2000のインストールCDから抜いた。ライセンス的に不味いかもしれん。が、別にばら撒くわけでもなし。

基本タスクを作成し、トリガーとして「ログイン時」を選択。次に登録するプログラムなんですが・・・Windows標準で、サウンドファイルを再生するコマンドはないみたいです・・・
これもググると Powershell のワンライナーがヒットします。。。というか、これしかヒットしません。
要点は・・・コマンドプロンプトで下記コマンドを打つだけ。

>> powershell -WindowStyle hidden -c (New-Object Media.SoundPlayer "wavファイルのパス").PlaySync();

このコマンドをそのままタスクスケジューラに登録してもいいんですが・・・コンソールウィンドウが立ち上がってしまいます。

なんか、すっげぇカッコ悪りぃ!

標準でできないならプログラムを組むしかない。 上記のコマンドラインは .NET Frameworkのクラスライブラリ System.Media.SoundPlayer のインスタンスを生成してサウンドファイルを渡すだけなので、C#でも数行で済みそうです。というわけで組んでみる・・・組むというレベルでもない。

/*******************************************
  * JUST ONLY PLAY SOUND FILE(WAV)

  File name : playsound.cs
  build: 
     >> csc -target:winexe  playsound.cs
  
  usage:
     >> playsound logon.wav

  namespaceは適当に変えてね。
*******************************************/
using System.IO;
using System.Media;

namespace jp.ptsv.windows
{
  class PlaySound
  {
    static void Main(string [] args)
    {
      if(args.Length > 0 && File.Exists(args[0]))
        (new SoundPlayer(args[0])).PlaySync();
    }
  }
}

主要部分は一行ですね(笑)
これを、-target:winexe 付きでビルドするとコンソールウィンドウなしのプログラムができるので、このプログラムを適当のフォルダに配置してサウンドファイルとともにタスクスケジューラに登録します。
c:\your\binary\path\playsound.exe logon.wav

やっとこさ、ログイン時に あの Windows2000 の心地よい起動サウンドが聞けるようになったとさ。おわり。

一応実行バイナリを置いておきますが、自己責任で。いかなる損害も補償は一切いたしません。あしからず。

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

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

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

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

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

/********************************************************************
  filename: jquery.enterNext.js
  usage: call bellow...

   $('form').enterNext();

********************************************************************/
(function($,undefined)
 {
   $.enterNext =
     {
       config:
         {
           selectors: [
             'input[type="text"]',
             'input[type="password"]',
             'input[type="email"]',
             'input[type="number"]',
             'select',
             'textarea'
           ]
         }
     };

   var enterNextInternal = function(setting)
   {
     var selectors = setting.selectors.join(',');
     var $controls = $(selectors,this);
     var lastIndex = $controls.length - 1;
     var onKey =  function(ev) 
     {
       if(this.tagName.match(/input/i) && ev.keyCode == 13)
       {
         var currentIndex = $controls.index(this);
         var nextIndex = currentIndex + 1;
         if(currentIndex == lastIndex)
           nextIndex = 0;

         var nextObject = $controls.get(nextIndex);
         nextObject.focus();
       }
     };
     $(this).on('keydown',selectors,onKey);
     // 修正: onkeydown へ変更。onkeyupだと validityチェックでフォーカスが移動しなくなった。。。なんで?よくわからん。
   };
   
   $.fn.enterNext = function(options)
   {
     return this.each( function() { enterNextInternal.call(this,$.extend(true,{},$.enterNext.config,options)); });
   };
   
 })(jQuery);

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

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <style type="text/css"><!--
      form input[type=text] {
        display: block;
        margin: 1em 0;
        padding: 5px;
      }
    --></style>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script type="text/javascript" src="./jquery.enternext.js"></script>
  </head>
  <body>
    <div id="main">

      <!--メインコンテンツ-->
      <div id="contents">
        <div class="inner">
          <form method="post">
            <input type="text" name="a" value="" required />
            <input type="text" name="b" value="" />
            <input type="text" name="c" value="" />
            <input type="text" name="d" value="" />
            <input type="text" name="e" value="" />
            <input type="submit" value="送信">
          </form>
        </div>
      </div>

    </div><!-- #main -->
    <script type="text/javascript"><!--
      (function($) {
        $('form').enterNext();
      })(jQuery);

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

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

じゃぁ、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チェックの際、エラーメッセージをカスタマイズするプラグインもついで書いた。プラグインにするほどのものでもないけど、一回一回書くのはめんどっちーのでプラグインにした方がラク、というだけの理由ですけどね。

/******************************************************************
  Filename: jquery.validity.js
  入力必須の際のエラーメッセージをカスタマイズします。

   usage:
   <input type="text" pattern="^[0-9]*$" name="hogehoge" />

   $('input[type=text]').validity('半角数字で入力してください');
*******************************************************************/
;
(function($,undefined)
 {
   //デフォルト値
   $.validityMessage = 'この項目は入力必須です。';

   var validity = function(message)
   {
     if(message.length == 0)
       message = $.validityMessage;

     $(this)
       .on('invalid',function(ev) {
         if(this.validity.valueMissing || this.validity.patternMismatch || this.validity.typeMismatch)
         {
           this.setCustomValidity(message);
         }
         else
         {
           this.setCustomValidity('');
           ev.preventDefault();
         }
       })
      .on('input',function(ev) {
        this.setCustomValidity('');
      });
   };
   
   //plugin body
   $.fn.validity = function(message)
     {
       return this.each(function()
                        {
                          validity.call(this,message);
                        });
     };
   
 })(jQuery);

ウェブブラウザ上のJavaScriptでExcelファイルをゴニョゴニョしたい

特に需要はないと思いますが、極たま~~~にウェブでエクセルファイルを扱うことがあります。
なんつっても、世の中の文書フォーマットは、マイクロソフトのエクセル(Excel)がデファクトスタンダードです💦
これはもうどうにもなりません。CSVで、つっても、エクセルファイルをよこしやがります。

まぁ、やっぱり業務システムにはエクセルは欠かせません、というか、これなしには、日本は動きません。日本の企業とか国、地方公共団体は、エクセルで動いているんです。これはもう動かしようのない事実であり当分の間は変わりませんし、変化の兆しも見えません。

しょうがない。

と、開発者の方が思ったかどうかわかりませんが、ブラウザのJavaScriptでエクセルファイルをパースして編集して、出力してくれる ライブラリがあるんですね。サーバーサイドのNode.jsでも使えます、というかそっちがメインのライブラリなのかも。

一般的にMS-Officeをインストールしたパソコンにはおまけ?として、EXCELのオートメーションが使えるようになってます。あくまでOfficeを買えば!の話ですが。
Windows Scripting HostなどからVBScriptやJScriptを使ってカンタンにエクセルファイルをアーダコーダできます。

・・・が、今回はこういう噺ではありません。
一般的なブラウザ上で、エクセルファイルをアーダコーダできるライブラリがあったんです。需要がなかったので今まで知らなかった!

SheetJS Spreadsheets simplified

で、ググったりしていろいろ調べると、意外にカンタンに使えちゃいますね。ただし、javascriptの blobとかFileReaderとかArrayBufferとかUint8Arrayとか・・・そういうちょっとややこしめの知識が必要です。

昔と違ってブラウザでローカルファイルとか普通に扱えます。ですが、その時は必ず、上記のFile/Blob/FileReader/ArrayBuffer/TypedArrayとかが絡んできます。
FileとBlobの関係、ArrayBufferとTypedArray(Uint8Arrayとか)の関係、さらにはFileReaderとFile/Blobの関係。このあたりは鬼門です。なんでこんなめんどくさいんだよ!っていつも思います。

MDNとかのリファレンスを読むのが手っ取り早いのですが・・・とりあえず、MDNのサイトでは、こういう場合は、こうする、という「お約束」の手順が書かれているので、まずそれを丸覚えするのがいいと思います。公文式です(笑) 理屈は後から学べばいいんです。ただ、ググってブログ記事を参考にするのは結局は理解するのが遅くなってしまうので、リファレンスとサンプルを読んで、書いて、試してみたほうがいいと思ってます。

※ たぶんIEでは動かないと思う。試してないけど。IEは既にMSも認めたオワコンなんで、どーでもいい。

下のデモ(テストコード)

さて、HTMLファイルをサクッと書きます。(Ryzenの価格表コピペしました。)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style type="text/css">
      table,th,td {
        border-collapse: collapse;
        border: 1px solid #aaa;
      }
      th,td {
        padding: 5px;
        text-align: center;
        font-size: 85%;
      }
      th { 
        background-color: #f6f6f6;
      }
    </style>

  <body>
    <h2>sheet-js サンプル</h2>
    <table id="table-0">
      <tr>
        <th>モデルナンバー</th>
        <th>プロセスルール</th>
        <th>コア/スレッド数</th>
        <th>TDP</th>
        <th>周波数(ブースト時/ベース)</th>
        <th>合計キャッシュサイズ(MB)</th>
        <th>GPU</th>
        <th>PCIe 4.0 レーン(X570利用時)</th>
        <th>店頭予想価格(税別)</th>
        <th>提供開始時期</th>
      </tr>
      <tr>
        <td>Ryzen 9 3950X</td>
        <td>7nm</td>
        <td>16/32</td>
        <td>105W</td>
        <td>4.7/3.5GHz</td>
        <td>72MB</td>
        <td>-</td>
        <td>40</td>
        <td>不明</td>
        <td>9月</td>
      </tr>
      <tr>
        <td>Ryzen 9 3900X</td>
        <td>7nm</td>
        <td>12/24</td>
        <td>105W</td>
        <td>4.6/3.8GHz</td>
        <td>70MB</td>
        <td>-</td>
        <td>40</td>
        <td>59,800円</td>
        <td>7月7日</td>
      </tr>
      <tr>
        <td>Ryzen 7 3800X</td>
        <td>7nm</td>
        <td>8/16</td>
        <td>105W</td>
        <td>4.5/3.9GHz</td>
        <td>36MB</td>
        <td>-</td>
        <td>40</td>
        <td>46,980円</td>
        <td>7月7日</td>
      </tr>
      <tr class="y5 odd">
        <td>Ryzen 7 3700X</td>
        <td>7nm</td>
        <td>8/16</td>
        <td>65W</td>
        <td>4.4/3.6GHz</td>
        <td>36MB</td>
        <td>-</td>
        <td>40</td>
        <td>39,800円</td>
        <td>7月7日</td>
      </tr>
      <tr>
        <td>Ryzen 5 3600X</td>
        <td>7nm</td>
        <td>6/12</td>
        <td>95W</td>
        <td>4.4/3.8GHz</td>
        <td>35MB</td>
        <td>-</td>
        <td>40</td>
        <td>29,800円</td>
        <td>7月7日</td>
      </tr>
      <tr>
        <td>Ryzen 5 3600</td>
        <td>7nm</td>
        <td>6/12</td>
        <td>65W</td>
        <td>4.2/3.6GHz</td>
        <td>35MB</td>
        <td>-</td>
        <td>40</td>
        <td>23,980円</td>
        <td>7月7日</td>
      </tr>
      <tr>
        <td>Ryzen 5 3400G</td>
        <td>12nm</td>
        <td>4/8</td>
        <td>65W</td>
        <td>4.2/3.7GHz</td>
        <td>6MB</td>
        <td>Radeon RX Vega 11</td>
        <td>-</td>
        <td>18,800円</td>
        <td>7月7日</td>
      </tr>
      <tr>
        <td>Ryzen 3 3200G</td>
        <td>12nm</td>
        <td>4/4</td>
        <td>65W</td>
        <td>4/3.6GHz</td>
        <td>6MB</td>
        <td>Radeon RX Vega 8</td>
        <td>-</td>
        <td>11,800円</td>
        <td>7月7日</td>
      </tr>
    </table>
        
    <p>
      <label for="select-file">テーブルをエクセルファイルに追加します。</label>
      <input type="file" id="select-file">
    </p>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.14.3/xlsx.full.min.js"></script>
    <script type="text/javascript" src="./index.js"></script><!-- 下記 javascriptコード -->
  </body>
</html>

JSライブラリは適当にCDNから引っ張ってきましょう。僕は jQuery好き好き人間なので、jQueryを使用します。すみません。
INPUT[type=file]タグでローカルファイルの口としましょう。ドラッグ&ドロップを仕込んでもいいのですが、コードをカンタンにするため、普通のファイル選択にしました。ここでローカルファイルを選んでjavascriptコードに放り込みます。

で、次に実際の処理を書いていきます。まず、基本。INPUT[type=file]のonchangeイベントハンドラを起点にしています。(13行目付近)
ローカルファイルを読み込んでゴニョゴニョするときは、必ずFileReaderのインスタンスを作って、onloadイベントで処理を行います。(18行目付近)
下記例では、FileReader.readAsArrayBuffer() していますが、単純に Data URIが必要であれば FileReader.readAsDataUrl() を使用します。
ローカルの画像ファイルを読み込んで表示するときは、readAsDataUrlメソッドを使いますよねぇ。

ちなみにreadAsArrayBufferメソッドを使うと、onloadイベントハンドラ内で ev.target.result によってArrayBufferオブジェクトを得ることができますが、直接このArrayBufferオブジェクトにアクセスすることができません。必ず、TypedArray・・・たとえば、Uint8Arrayなどのオブジェクトのインスタンスからアクセスします。めんどくさいですねぇ。

/*******************************************************************************
  filename:  index.js
 
  description: 
   ローカルのエクセルファイルを選択すると(<input type="file" id="select-file">、
   選択したエクセルファイルへシートを追加し、テーブル要素(<table id="table-0">)を書込み、
   そのエクセルシートをダウンロードするためのリンクをdocument.body に追加します。

   ※このままのコードだと、セルの書式属性は全部消えます。
*******************************************************************************/
(function($) {

  $('input#select-file[type=file]').on('change',function(ev) {
    var files = this.files;
    var f = files[0];
    var reader = new FileReader();

    reader.onload = function(e) {
     
      // 読み込んだエクセルファイル(ArrayBuffer)をUint8Array配列にし、XLSX.readに渡します。
      var data = new Uint8Array(e.target.result);
      var workbook = XLSX.read(data, {type: 'array'});

      // 上記 HTMLのテーブルを table_to_sheetメソッドに渡しエクセルシート(ブック?)を作って、
      var new_workbook = XLSX.utils.table_to_sheet( $('#table-0').get(0) );
      
      //読み込んだエクセルに上記テーブルを変換したシート(ブック?)を新しいシートとして追加します。
      XLSX.utils.book_append_sheet(workbook, new_workbook, 'Ryzen price');

      // 新しく作成するエクセルファイルの作成オプションを設定します。
      var options = {
          bookType: 'xlsx',
          bookSST: false,
          type: 'array',
          compression: true
        };
      
      // 上記オプションを使って Blobオブジェクトに出力します。
      var blob = new Blob(
        [XLSX.write(workbook, options)],
        {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}
      );

      // Blobオブジェクトをダウンロードさせるための仮想的な?URLを作って A要素をdocument.bodyに追加します。
      // クリックすると、読み込んだエクセルファイルに表をシートに追加し、新しく作ったエクセルファイルをダウンロードできます。
      $('<a>')
        .attr({'href':window.URL.createObjectURL(blob),'download': 'シートを追加したエクセルファイル.xlsx'})
        .appendTo(document.body)
        .text('シートを追加したエクセルファイル.xls');
    };
    reader.readAsArrayBuffer(f);
  });

})(jQuery);

FileReader.onload内で、ev.target.result を そのまま Uint8Arrayコンストラクタに渡して、ArrayBufferへアクセスするための Uint8Arrayオブジェクトを作り(21行目付近)、それを XLSX.readメソッドに渡します。ただそれだけで、エクセルファイルをパースしてくれます。あとは、XLSX.utils オブジェクトのメソッドをコールしていけば、だいたいのことはできると思います。
CSVデータが欲しければ、XLSX.utils.sheet_to_csv, JSONデータが欲しければ XLSX.utils.sheet_to_json、シートを増やしたければ、XLSX.utils.append_sheet、他にも javascript配列をシートに、DOM TABLE要素をシートに・・・とか対応するメソッドがあるようです。この辺のドキュメントはgithubをたよりに試行錯誤するしかないのかな。。。

エクセルファイルにシートを追加して、そのエクセルファイルをダウンロードする、これだけのことをクライアント内(ブラウザ内)で完結することができてしまいます。
ローカル内でサーバーを立てる必要もありません。上記二つのHTMLとJSを任意のフォルダに適当な名前で保存して、file://スキームで試すことも可能です。

(1) HTMLファイル ローカルで開いたところ。

(2) エクセルを選択すると、ダウンロードリンクが追加されます。

(3) 元のエクセルはいつもダミーで使わせてもらってる疑似会社情報のエクセルファイル
( http://hogehoge.tk/personal/ )

(4) HTMLのテーブルデータをエクセルシートにぶっこんでくれます。
ただしセル書式は全部吹っ飛びます。

今回は、エクセルシートを追加してみましたけど、セルの属性とかは全部ぶっとんでしまいました💦
が、単にJSONやCSVが取れればいい、というのが大半の需要かと思いますので問題はないでしょう。

僕は エクセルファイルをサーバー側で変換するのではなく、クライアントで一旦CSVに変換してから、サーバーにアップロードして、処理を行う用途に使用しました。サーバーでエクセルファイルを処理すると重くなるんで・・・。
エクセルを読むだけなら、カンタンにできるので、本当にありがたいライブラリです。

ただ一点、ちゃんとしたリファレンスドキュメントがありません。。。これはPro版を買えってことなのかなぁ。。。Community版はApache License 2.0なんで、「おまえら、ソース読んで、自分でなんとかしろよ」ってことなんでしょうね。

gVimでのシェル切り替え

Visual Studio 2019 Community インストール。
まぁ、たまにC#とか、昔のC++で書いたツールをビルドするのにやっぱ必要。
IDEはほとんど立ち上げず、gVimで編集、編集。もうカーソル移動が HJKLバインドじゃないと苦痛を感じるレベルまで悪化。
xkeymacs ならぬ、xkeyvim ってググるおっさんがここにいる。

そんなことはさておき。

gVimでC#とかC,C++(Win32 API)のコードを叩いていると、terminal でビルドしたくなります。だけど、おっさんはgVimのshellオプションをWSL(Bash)に変えてあるので、困った、困った、こまどり姉妹になるわけです。

困るので、gVimの複数のオプションを一括変更するだけのコマンドを書く・・・なんか激しく無駄なことをしている気がしないでもないが・・・CMD と WSLを行ったり来たりするにはこれしかない。

" Set CMD 既定値に戻す
function! Fsetcmd()
  set shell&
  set shellcmdflag&
  set shellslash&
  set shellquote&
  set shellxquote&
  set shellxescape&
  set grepprg&

  echo 'change shell to default windows cmd'
endfunction

" Set WSL
function! Fsetwsl()
  let &shell = 'bash'
  let &shellcmdflag = '-c'
  let &shellslash = 1
  let &shellquote='"'
  let &shellxquote = ''
  let &shellxescape = ''
  let &grepprg = 'grepwsl -n'

  echo 'change shell to WSL bash'
endfunction

command! Setcmd call Fsetcmd() 
command! Setwsl call Fsetwsl() 

vimスクリプトで、オプション変数を初期値に戻すにはどう書けばいいのかなぁ・・・???
追記:
オプション変数を規定値に戻すのは set {option}& にすればいいようで・・・ヘルプに書いてますね・・・反省

普段は シェルを wsl-bash にしているので、 :Setcmd とすれば、:termや:shell や :r !hogehoge でcmd.exeを使うデフォルトに戻れる。
こんなアホなことしてんのおっさんだけだよなぁ。。。

Creating screenshot with Firefox + selenium + Node.js

これのつづきです。

前回はヘッドレスモードでブラウザからスクリーンショットを取りました。これで特に問題はなかったのですが、欲が出てきてしまい、実現するにはブラウザからオプションを指定するだけではできなくなりました。
ということで、node.js から selenium-webdriver を使ってのスクリーンショット生成の自動化のコードを書くことにしました。

実現するには、下記が必要です。

試用環境は、Windows10 Pro(1809) + WSL(ubuntu 1604) + Node.js + FireFox(ubuntu) です。
WSLでのFireFoxのインストールは、apt install firefox で普通にできます。また日本語フォントは、一つ前にも書きましたが、/mnt/c/Windows/Fonts ディレクトリのシンボリックリンクを/usr/share/fontsへ作って フォントキャッシュを更新。

また、FireFoxのwebdriverのインストールです。が、これはダウンロードしてきたものをパスの通ったディレクトリ(例えば /usr/local/binとか)に配置すればOK。

$ wget -nd https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-linux64.tar.gz
$ tar xvzf geckodriver-v0.24.0-linux64.tar.gz
$ sudo cp geckodriver /usr/local/bin/ && sudo chmod 0755 /usr/local/bin/geckodriver

適当にディレクトリを作り、まずはこの中で作ります。
selenium-webdriver のインストールです。

$ npm install selenium-webdriver

ググると、簡単なサンプルコードが出てきますが、ページ全体のスクリーンショットを得るためには、ウィンドウの幅・高さを設定しないといけないみたいで・・・下記コードでは、document.bodyのページコンテンツを保存するために、ページロードが終わった後、document.documentElement.scrollHeightで得られた高さを Window.setRect するようにしました。(29行目付近)
幅を1024ピクセルにハードコードしてますが・・・手抜きです💦 コマンドラインのパース一切手抜きです。すみません。

また、余計なお世話的な機能なんですが、CSSセレクタを使用して、指定要素だけのスクリーンショットも取れるようにしました。これは適当に findElement()してその要素に対して takeScreenshot()メソッドをコールしてやるだけ。(35行目付近)

selenium-webdriverのAPIリファレンスを読めば大概のことはできると思います。ブラウザの各種設定を行うabout:configと同じことをしたい場合は、firefoxドライバのインスタンス生成時に、firefox.Options.setPreferenceインスタンスメソッドで変更もしくは追加したOptionsをsetFirefoxOptionsで設定してあげればいいですし(66行目付近)、いつも使っているプロファイルをコピペして、setProfileメソッドでコピペしたプロファイルのディレクトリパスを指定してあげればいい。(試してはいないけど。。。)

下記コードでは ユーザーエージェントを変えられるようにもしてます(ハードコードですけど。。。)

/*******************************************************************************

  Create screenshot for specified url.  if success, output PNG binary data to STDOUT.
  Script file name : screenshot.js

  usage:
  $ node screenshot.js URL [CSS SELECTOR] > screenshot.png

  * if you want to get jpeg file ,use ImageMagick(convert) with pipe.

  for example,
  $ node screenshot.js URL | convert - screenshot.jpg
  
*******************************************************************************/
const { Builder, By, Key, promise, until } = require('selenium-webdriver');
const firefox = require('selenium-webdriver/firefox');
const { createInterface } = require('readline');
let userAgent = null;

async function takeScreenshot(drv,s) 
{
  await drv.wait(async () => {
    const readyState = await drv.executeScript('return document.readyState;');
    return readyState === 'complete';
  });
  let png = null;
  if(s === null)
  {
    const dHeight = await drv.executeScript('return document.documentElement.scrollHeight;');
    await drv.manage().window().setRect({'width': 1024,'height': dHeight});
    png = await drv.takeScreenshot();
  }
  else
  {
    const el = await drv.findElement(By.css(s));
    png = await el.takeScreenshot();
  }
  
  await drv.quit();
  return png;
}

(function() {
  let len = process.argv.length;
  if(len <= 2)
  {
    console.error('too few command option');
    process.exit(1);
  }

  let url = process.argv[2];
  if(!url.match(/^https?:\/\//))
    {
      console.error('specified argv[1] is not url format.');
      process.exit(1)
    }

  let selector = null;
  if(len > 3)
    selector = process.argv[3];

  var firefoxOptions = new firefox.Options();
  firefoxOptions.headless();

  if('string' === typeof(userAgent) && userAgent !== '')
    firefoxOptions.setPreference('general.useragent.override',userAgent);

  let drv = new Builder()
            .forBrowser('firefox')
            .setFirefoxOptions(firefoxOptions)
            .build();

  drv.get(url);

  // takeScreenshot(drv,selector).then((png) => writeSync(1,png,0,'base64'));
  // writeFileSync を使うと、パイプで convertコマンドに出力を渡すとき、エラーになってしまうので、
  // Bufferを作って process.stdout で出力するようにしました。
  takeScreenshot(drv,selector).then((png) => {
    const buf = Buffer.from(png,'base64');
    process.stdout.write(buf);
  });

})();

(75行目付近)上記コメントにも残しましたが、得られたPNGデータを標準出力に書き込むとき、fs.writeSyncを使うとエラーになってしまいます。

$ node screenshot.js 'http://localhost/hogehoge/' | convert PNG:- -resize 600x screenshot.jpg
(node:14444) UnhandledPromiseRejectionWarning: Error: ESPIPE: invalid seek, write
    at writeSync (fs.js:568:3)
    at takeScreenshot.then (/mnt/c/Users/ddk5010/Desktop/temp/screenshot.js:75:39)
    at process._tickCallback (internal/process/next_tick.js:68:7)
(node:14444) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function witho
ut a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)
(node:14444) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will te
rminate the Node.js process with a non-zero exit code.
convert: improper image header `/tmp/magick-14445jzJc3SLocfrG' @ error/png.c/ReadPNGImage/3940.
convert: no images defined `screenshot.jpg' @ error/convert.c/ConvertImageCommand/3210.

Node.js固有の問題なのか、WSLが悪さをしているのか分かりません。パイプじゃなくて単にファイルにリダイレクトさせてあげるとエラーは出ません。僕には原因がわからないので、とりあえず、process.stdout のStreamに書き込むとうまく動きました。
一応エラーでググって見たのですが、よくわかんなかったです。いまいち非同期処理が理解てきていないのかも。

selenium-webdriverの takeScreenshotメソッドでは、PNGファイルが取得できますが、「jpgファイルが欲しい!」「リサイズしたもが欲しい!」とかだと、ImageMagickのconvertコマンドに頼る方がよりUNIXライクな方法ではないでしょうか。。。node.jsでも画像処理のモジュールを組みこめばワンストップでできそうですけど。。。

# リサイズしてJPGファイルに
$ node screenshot.js https://www.instagram.com/xxxx/ | convert - -resize 600x insta.jpg

# 特定のセレクタの画像を取得
$ node screenshot.js https://www.yahoo.co.jp/ "#navi" > yahoo-navi.png

エラーハンドリングしてないので、エラーが起こったら適当に 例外処理入れてね。

google-chromeを使う場合もwebdriverのインスタンスを作成するところ以外(具体的には上記コードの async function takeScreenshot()のところ)はほとんど同じ手順ではないかと思います。