承認これくしょん

my black histories

Nashorn + JavaFXでQRコードを表示する

Java 8から新しいJavaScriptエンジンであるNashornが同梱されています。
実行シェルであるjjsでは、-fxオプションを使うと簡単にJavaFXアプリケーションが作成できます。
これを使って、コマンドライン引数の値でQRコードを生成して表示するスクリプトを書きました。

実行方法

ZXingライブラリを使用しています。Mavenリポジトリから各jarをダウンロードしてください。

  • core-3.2.0.jar
  • javase-3.2.0.jar

それぞれスクリプトと同じディレクトリに配置し、以下のコマンドで実行します。

jjs -cp core-3.2.0.jar;javase-3.2.0.jar -fx qr.js -- (QRコードの内容)

ソース (qr.js)

var Hashtable = java.util.Hashtable;

var ImageView = javafx.scene.image.ImageView;
var StackPane = javafx.scene.layout.StackPane;
var Scene = javafx.scene.Scene;
var SwingFXUtils = javafx.embed.swing.SwingFXUtils;

var EncodeHintType = com.google.zxing.EncodeHintType;
var ErrorCorrectionLevel = com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
var QRCodeWriter = com.google.zxing.qrcode.QRCodeWriter;
var BarcodeFormat = com.google.zxing.BarcodeFormat;
var MatrixToImageWriter = com.google.zxing.client.j2se.MatrixToImageWriter;

var content = arguments[0]; // コマンドライン引数
var width = 230;
var height = 230;

function start(stage) {
  if (content) {
    // エンコード設定
    var encodeHint = new Hashtable();
    encodeHint.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
    // QRコード生成
    var writer = new QRCodeWriter();
    var bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, encodeHint);
    var img = SwingFXUtils.toFXImage(MatrixToImageWriter.toBufferedImage(bitMatrix), null);
    // ウインドウに表示
    stage.title = content;
    var root = new StackPane();
    var imageView = new ImageView(img);
    root.children.add(imageView);
    stage.scene = new Scene(root, width, height);
    stage.show();
  } else {
    exit(); // 引数が無ければ終了
  }
}

javafx.application.Applicationを継承せずにサクッと作れます。
あらかじめ用意されているユーティリティを使えば、昔ながらのBufferedImageも表示できます。

参考

複数画像を一括トリミングするやつ

特定アプリケーションのスクリーンショットとか、余白の位置が固定されているときに。
jrunscriptで実行するとコマンドライン引数で渡された画像ファイルを処理して上書き保存します。
とりあえず形式はpngで。

var File = java.io.File;
var ImageIO = Packages.javax.imageio.ImageIO;

// 余白(ピクセル)
var margin = {top: 111, left: 8, right: 8, bottom: 25};

for each (var arg in arguments) {
  var file = new File(arg);
  var baseImage = ImageIO.read(file);
  var newImage = baseImage.getSubimage(margin.left, margin.top,
    baseImage.getWidth() - (margin.left + margin.right), baseImage.getHeight() - (margin.top + margin.bottom));
  ImageIO.write(newImage, "png", file);
}

年度末ですねー。
おちこんだりもするけれど、私はげんきです。

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
絡めた右手がいい感じだなあと思いました。

クラスが含まれるjarを検索するスクリプト

既存のJavaプロジェクトをビルドしようとしたらjarの参照が切れていた。
しかも必要なクラスがどのjarに入ってるかわからず、大量のjarの山から探さなければ…という辛い状況で便利かもしれません。
JDK付属のjrunscriptを使います。

使い方

jrunscript classFinder.js (検索対象クラス) (検索対象ディレクトリ)…

検索対象クラスは完全限定名でもそうじゃなくてもヒットするはず。
検索対象ディレクトリは複数指定可。それぞれのディレクトリとそのサブディレクトリ内のjarについて該当のクラスが含まれるか調べます。

ソース (classFinder.js)

var File = java.io.File;
var JarFile = java.util.jar.JarFile;
var JarEntry = java.util.jar.JarEntry;
var Collections = java.util.Collections;

var matcher;

if (arguments.length != 0) {
  matcher = new RegExp('^(.+/)*' + arguments[0].replace(/\./g,'/') + '\.class$');
  findJars(arguments.slice(1));
}

function findJars(dirPaths) {
  while (dirPaths.length != 0) {
    var dir = new File(dirPaths.pop());
    var items = dir.listFiles();
    for each (var item in items) {
      if (item.isFile() && item.getName().endsWith('.jar')) {
        findClass(item);
      } else if (item.isDirectory()) {
        dirPaths.push(item);
      }
    }
  }
}

function findClass(file) {
  var jarFile = new JarFile(file);
  var entries = Collections.list(jarFile.entries());
  for each (var entry in entries) {
    if (matcher.test(entry)) print(file + ': '+ entry);
  }
}

JarFile.entries()の戻り値がEnumerationでどうすんのこれ…って思ったけどArrayListに変換できてよかった。Collections便利。
なんかfor-each文がAS3(というか幻のES4?)っぽいですね。

IEでフォームの入力内容を保存して自動入力できるようにしたい

定型業務とかテストとかで、Webアプリのフォームに何度も同じ内容を入力する機会があると思う。これを保存しておいて自動入力できるようにしたい。
IEのアドオン開発は敷居が高そうだけど、右クリック時のコンテキストメニューは簡単に編集できるらしい。これを使ってみよう。

コンテキストメニューの編集

こんな感じでregファイルを作ってレジストリに登録する。

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MenuExt\フォームの内容をブックマークレットに保存(&B)]
"Contexts"=dword:00000001
@="D:\\Works\\MenuExt\\main.html"

規定値に実行するhtmlのパスを、Contextsは規定の0x1にしておく。
画像や文字列と言った選択中のオブジェクトの種類を指定して表示条件を設定できるそうだ。

htmlの作成

このhtmlは画面上には表示されないし、新規タブにも現れない。
しかしscriptタグ内の処理は実行されるので、ここにロジックを書けばOK。

<script language="JScript">
var shell = new ActiveXObject('WScript.Shell');
var fso = new ActiveXObject('Scripting.FileSystemObject');
// ブックマークレットの保存先(ユーザーのお気に入りフォルダ)
var path = shell.SpecialFolders('Favorites') + '\\';

var oDocument = window.external.menuArguments.document;
var script = getBookmarklet(oDocument);
if (script) {
   var name = oDocument.title || oDocument.location.href.replace(/.+\//, '');
   saveBookmarklet(script, name, path);
   alert('ブックマークレットを保存しました。');
} else {
   alert('名称つきフォームが存在しないか、保存可能な値が入力された項目が存在しません。');
}

shell = null;
fso = null;

function getBookmarklet(document) {
   var script = 'javascript:(function (a){for(var b in a)for(var c in a[b])document.forms[b].elements[c].value=a[b][c]})(';
   var outerScript = '';
   for (var i = 0; i < document.forms.length; i++) {
       if (document.forms[i].name != '[object HTMLInputElement]' && document.forms[i].name != '') {
           outerScript && (outerScript += ',');
           var innerScript = '';
           for (var j = 0; j < document.forms[i].elements.length; j++) {
               if (document.forms[i].elements[j].name
                   && document.forms[i].elements[j].type != 'hidden'
                   && document.forms[i].elements[j].type != 'file'
                   && document.forms[i].elements[j].type != 'submit'
                   && document.forms[i].elements[j].type != 'image'
                   && document.forms[i].elements[j].type != 'reset'
                   && document.forms[i].elements[j].type != 'button'
                   && document.forms[i].elements[j].value != '') {
                       innerScript && (innerScript += ',');
                       innerScript += document.forms[i].elements[j].name + ':"' + document.forms[i].elements[j].value + '"';
                   }
           }
           outerScript += innerScript ? '{' + document.forms[i].name + ':{' + innerScript + '}' : '';
       }
   }
   script += outerScript + '});'
   return outerScript ? script : '';
}

function saveBookmarklet(script, name, path) {
   fso.FolderExists(path) || fso.CreateFolder(path);
   if (!fso.FileExists(path + name + '.url')) {
       var sc = shell.CreateShortcut(path + name + '.url');
   } else {
       var i = 2;
       while(fso.FileExists(path + name + ' (' + i + ').url')) {
           i++;
       }
       var sc = shell.CreateShortcut(path + name + ' (' + i + ').url');
   }
   sc.TargetPath = script;
   sc.Save();
}
</script>

JScriptから呼び出し元のDOMを取得し、フォームに入力された値を自動入力するためのブックマークレットを生成する。
ブックマークレットはpathで指定したフォルダ内にショートカットファイルとして作成されます。
name属性のないフォームおよびその子要素や、文字列として保持できないものなどは記録対象外です。

参考

Internet Exploreにコンテキストメニューを追加して機能を追加する

Chromeの新規タブの「よくアクセスするページ」を非表示にする

新規タブをリダイレクトさせるんじゃなくて、単に非表示にしたい時に。
簡単な拡張機能を作ってインストールする。

manifest.json
{
  "manifest_version":2,
  "version":"1.0",
  "name": "Hide Most Visited",
  "content_scripts": [
    {
      "js": ["hidemv.js"],
      "matches": ["https://www.google.co.jp/_/chrome/newtab*"]
    }
  ]
}
hidemv.js
document.getElementById('mv-tiles').style.display = 'none';

上記2ファイルを適当なフォルダに入れて「パッケージ化されていない拡張機能を読み込む…」からインストールする。
なおデベロッパーモードを有効にしないとこのボタンは出ない。

問題ステップ記録ツールが出力するMHTMLからスクリーンショットを抽出する

問題ステップツールとは

Windows 7からは標準で「問題ステップ記録ツール」ことpsr.exeがインストールされている。
ユーザーのマウス操作などに応じて自動でスクリーンショットを撮ってくれるツールだ。
出力されるzipに入ってるMHTMLを展開してブラウザで開くとスクリーンショット付きのレポートが見られる。
以下のコマンドラインオプションを指定するとGUIを出さずに使えて便利。

記録開始
psr.exe /start /output (出力するzipのフルパス) /gui 0 /sc 1 /maxsc (撮影するスクリーンショットの上限)
記録終了
psr.exe /stop

7で試したらスクリーンショットの上限は100だったけど、8は999までいけるらしい。
上限に達した時点で撮影終了するのではなく、古いものから順に消えるので最大値を指定するのが無難。

スクリーンショットの抽出

開発環境が入れられない人でも安心のJScript
下記のスクリプトと同じフォルダに、ツールが出力した「psr.zip」が置いてある前提。
zipを自動で解凍してmhtを読み込み、記録日時を名前とするサブフォルダにスクリーンショットを抽出する。
処理実行後にzipとmhtを削除するので注意。

// 設定
var zipName = 'psr.zip'; // 問題ステップ記録ツールが出力するzip

// zip解凍
var fso = new ActiveXObject('Scripting.FileSystemObject');
var explorer = new ActiveXObject('shell.application');
var currentPath = String(WScript.ScriptFullName).replace(WScript.ScriptName,'');
var inputZip = explorer.NameSpace(currentPath + zipName);
var mhtName = inputZip.Items().Item(0).Name; // mhtファイル
var dirName = mhtName.replace(/Problem_|\.mht/g, ''); // 出力先フォルダ
var outPath = currentPath + dirName + '\\';
fso.CreateFolder(outPath);
var outputDir = explorer.NameSpace(outPath);
outputDir.CopyHere(inputZip.Items(), 0);
explorer = null;

// mht読み込み
var sr = new ActiveXObject('ADODB.Stream');
sr.Type = 2; //adTypeText
sr.charset = 'utf-8';
sr.Open();
sr.LoadFromFile(outPath + mhtName);
var xml = new ActiveXObject('Microsoft.XMLDOM');
var boundary; // boundary文字列
var fileName; // ファイル名
var fileContent = ''; // ファイル内容(Base64)

// 一行ずつ末尾まで読み込み
while (!sr.EOS) {
    var line = sr.ReadText(-2); // adReadLine
    // boundaryが未取得
    if (typeof boundary == 'undefined') {
        boundary = (line.match(/boundary=\"(\S+)\"/)||[])[1];
    // boundaryを含む行
    } else if (line.indexOf(boundary) != -1) {
        // ファイル名と内容が取得済み
        if (fileName && fileContent) {
            // Base64をバイナリに変換
            var node = xml.createElement(fileName);
            node.dataType = 'bin.base64';
            node.text = fileContent;
            // バイナリを保存
            var sw = new ActiveXObject('ADODB.Stream');
            sw.Type = 1; // adTypeBinary
            sw.Open();
            sw.Write(node.nodeTypedValue);
            sw.SaveToFile(outPath + fileName);
            sw.Close();
            sw = null;
        };
        // ファイル名と内容のクリア
        fileName = undefined;
        fileContent = '';
    // boundaryを含まない行
    } else {
        // ファイル名が未定義
        if (typeof fileName == 'undefined') {
            fileName = (line.match(/Content-Location: (\S+\.jpeg)/)||[])[1]; // ファイル名を取得
        // 出力ファイル名が定義済み
        } else {
            fileContent += line; // 出力ファイル内容に追記
        };
    };
};

xml = null;
sr.Close();
sr = null;

// ファイル削除
fso.DeleteFile(outPath + mhtName); // mhtファイル
fso.DeleteFile(currentPath + zipName); // zipファイル
fso = null;

これでExcelに貼り付けたりできますね!ちょっと画質悪いけど…