承認これくしょん

my black histories

HTA + JScriptでExcel方眼紙に絵を描こう

f:id:old_horizon:20150302222615p:plain
Excelのセルをドットに見立てて絵を描くマクロです。
似たようなものは沢山あるわけですが、今回はWindows標準のHTA + JScriptExcelを操作して作ります。
HTAといえば、よくワンクリック詐欺のポップアップに使われていることで有名ですよね。
早すぎたElectronやNW.jsという感じです…

HTAHTML5を使うには

デフォルトではIE7互換で動作するので、以下の記述でドキュメントモードを最新バージョンに指定します。

<meta http-equiv="X-UA-Compatible" content="IE=edge">

IE9以降のChakraエンジンでもActiveXObjectは生成できました。
HTML5の表現力と強力なCOMが両方そなわり最強に見える。

ソース

ウインドウにD&Dされた画像をCanvasに描画し、そこから各ピクセルの色を取得して対応するセルの背景色に指定します。(アルファチャンネルは無視していますが…)
処理の進行状況はプログレスバーと%表示で確認できます。

img2xlsx.hta
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
</head>
<title>img2xlsx</title>
<body>
<progress style="width:200px;height:10px;" id="progress-bar" max="100" value="0"></progress>
<output id="progress-label">0</output>%
<script type="text/jscript">
(function() {

  window.resizeTo(280, 80);

  var isProcessing;
  var width;
  var height;
  var pixels;

  var progressBar = document.getElementById('progress-bar');
  var progressLabel = document.getElementById('progress-label');

  document.addEventListener("dragover", function(e) {
      e.preventDefault();
  });

  document.addEventListener("drop", function(e) {
    e.preventDefault();
    if (!isProcessing) {
      isProcessing = true;
      var file = e.dataTransfer.files[0];
      var image = new Image();
      image.src = window.URL.createObjectURL(file);
      image.onload = function(e) {
        createExcel(getPixelArray(this));
      }
    }
  });

  function getPixelArray(image) {
    width = image.width;
    height = image.height;
    pixels = width * height;

    var canvas = document.createElement('canvas');
    var context = canvas.getContext('2d');
    canvas.width = width;
    canvas.height = height;

    context.drawImage(image, 0, 0);
    return context.getImageData(0, 0, width, height).data;
  }

  function createExcel(pixelArray) {
    var excel = new ActiveXObject('Excel.Application');
    excel.Application.ScreenUpdating = false;

    var book = excel.Workbooks.Add();
    var sheet = book.WorkSheets(1);
    sheet.Cells.ColumnWidth = 0.77;
    sheet.Cells.RowHeight = 7.5;
    excel.ActiveWindow.Zoom = 10;

    var x = 0;
    var y = 0;
    var counter = 0;

    var worker = new Worker('worker.js');
    worker.addEventListener('message', function(e) {
      sheet.Cells(x + 1, y + 1).Interior.Color = e.data;
      counter++;
      y++;
      if (y == width) {
        x++;
        y = 0;
      }
      updateProgress(Math.floor((counter / pixels) * 100));

      if (counter == pixels) {
        excel.Application.ScreenUpdating = true;
        excel.Visible = true;
        excel = null;
        setTimeout(CollectGarbage, 1);

        isProcessing = false;
        updateProgress(0);
      }
    }, false);

    worker.postMessage(pixelArray);
  }

  function updateProgress(value) {
    progressBar.value = value;
    progressLabel.innerText = value;
  }

})();
</script>
</body>
</html>
worker.js
self.addEventListener('message', function(e) {
  var pixels = e.data;
  for (var i = 0, max = pixels.length - 4; i <= max; i += 4) {
    postMessage(getColorValue(pixels[i], pixels[i + 1], pixels[i + 2]));
  }
  self.close();
}, false);

function getColorValue(r, g, b) {
  return r + g * 256 + b * 256 * 256;
}

現状、IEではWorkerをBlob URLから作成できないため別ファイルに切り出す必要があります。
手間はかかりますがその威力は大きく、シングルスレッドで処理するよりも高速です。長時間回しても警告が出ないし。
なおExcelのようなアウトプロセスサーバを利用すると、nullを代入しても正しく解放されないことがあります。そのためCollectGarbageで強制GCしましょうとMSが言ってました。

参考

出来上がりはこんな感じです

f:id:old_horizon:20150302222639j:plain
絡めた右手がいい感じだなあと思いました。