TeX Alchemist Online

TeX を使って化学のお仕事をしています。

macOS のデフォルト状態でコマンドラインからQRコード画像を生成する ~ AppleScriptObjC版 ~

先日,Swift → Objective-C → Python (PyObjC) → ワンライナースクリプト と変換することで,追加のプログラム/ライブラリの事前インストールを必要とせず,macOS環境であればどこでもシェルから1行で呼び出せるQRコード生成コマンドを作成する試みを行いました。

doratex.hatenablog.jp

WWDC 2019 での衝撃の発表

ところが,現在開催中の WWDC 2019 において発表された Xcode 11 Beta の Release Notes に衝撃的な一文が書かれており,界隈に激震が走りました。

Deprecations

  • Scripting language runtimes such as Python, Ruby, and Perl are included in macOS for compatibility with legacy software. In future versions of macOS, scripting language runtimes won’t be available by default, and may require you to install an additional package. If your software depends on scripting languages, it’s recommended that you bundle the runtime within the app.

これによると,(次の macOS 10.15 Catalina には互換性のため同梱されるものの)将来の macOS のバージョンでは,Python / Ruby / Perl の実行環境が標準添付されない予定だそうです。「macOS 環境ならばどこでも Python / Ruby / Perl の実行環境が備わっていることが仮定できる」というのは,配布を前提とするソフトウェアの開発においてとても大きな強みとなっていました。それがなくなってしまうのはとても痛いですね……。*1

また,TeX Live についても対応が必要になります。TeX Live には,tlmgr, fmtutil, updmap のような根幹をなすツール群,および latexmk, epstopdf, pdfcrop, cjk-gs-integrate, jfmutil の周辺ツール群など,多数のコマンドが Perl スクリプトとなっています。Perl 実行環境が標準添付されない Windows 環境向けには,TeX Live には tlperl という Perl の実行環境が同梱されて配布されています。今後,macOS に Perl が同梱されない時代が来れば,TeX Live にも tlperl を同梱して配布せねばならないことになるでしょう。

さて,macOS に Python が同梱されない時代が来ると,当然 PyObjC も標準で使えなくなります。よって,「PyObjC を使えばどの macOS 環境でもコマンドラインから macOS の API が呼び出せる!」という便利な手法が封じられることになります。その時代の到来を見据えて,早速対応を考えてみました。

今後のポータブルな言語は何か?

今後,Python / Ruby / Perl の実行環境が標準添付されない時代になれば,macOS で標準インストールされるインタプリタとしては,

  • bash / zsh などのシェル
  • osascript で実行できる AppleScript / Javascript

のあたりということになります。そこで,以前のPDFページカウントの記事で使った,AppleScriptObjC を使う方法を検討してみました。この手法は OS X 10.10 Yosemite 以降で有効です。

doratex.hatenablog.jp

そこで今回は,新時代の到来に備えて,今のうちに AppleScriptObjC を使って macOS の CoreImage API を呼び出すことで QR コード画像を生成する という手段を探っておくことにしました。

AppleScript 版のQRコード生成スクリプト

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

--  QRコード生成のパラメータ
set message to "TeXはアレ☃︎"
set outputPosixPath to "/private/tmp/QR.png"
set scale to 5
set correctionLevel to "H"

my genQR(message, outputPosixPath, scale, correctionLevel)

on genQR(message, outputPosixPath, scale, correctionLevel)
    -- QRコード生成フィルターの準備
    set theData to (current application's NSString's stringWithString:message)'s dataUsingEncoding:(current application's NSUTF8StringEncoding)
    set transform to (current application's CGAffineTransformMake(scale, 0, 0, scale, 0, 0))
    
    set filter to current application's CIFilter's filterWithName:"CIQRCodeGenerator"
    filter's setValue:theData forKey:"inputMessage"
    filter's setValue:correctionLevel forKey:"inputCorrectionLevel"
    -- QRコードのCIImageを生成
    set theCIImage to filter's outputImage's imageByApplyingTransform:transform
    -- PNG保存
    my saveToPng(theCIImage, outputPosixPath)
end genQR

on saveToPng(theCIImage, outputPosixPath)
    -- CIImageRep に変換
    set theCIImageRep to current application's NSCIImageRep's imageRepWithCIImage:theCIImage
    -- NSImage に変換
    set theNsImage to current application's NSImage's alloc's initWithSize:(theCIImageRep's |size|)
    theNsImage's addRepresentation:theCIImageRep
    -- TIFF形式の NSData に変換
    set tiffData to theNsImage's TIFFRepresentation
    -- NSBitmapImageRep に変換
    set theBitmapImageRep to current application's NSBitmapImageRep's imageRepWithData:tiffData
    -- PNG形式の NSData に変換
    set theProps to current application's NSDictionary's dictionaryWithObject:1.0 forKey:(current application's NSImageCompressionFactor)
    set pngData to (theBitmapImageRep's representationUsingType:(current application's NSPNGFileType) |properties|:theProps)
    -- ファイル書き込み
    pngData's writeToFile:outputPosixPath atomically:false
end saveToPng

このスクリプトを Script Editor.app から実行すれば,/private/tmp/QR.png にQRコード画像が生成されるのが確認できるはずです。また,上記ソースをテキストファイルで保存し,コマンドラインから

$ osascript hoge.scpt

のようにして osascript コマンドの引数に渡すことでも実行できます。

ワンライナーに変換する

AppleScript をワンライナー化するには

次に上記ソースをワンライナー化します。複数行からなる AppleScript のソースをコマンドラインから直接実行するには,

  1. インデントのタブは削る。
  2. "\" とエスケープする。
  3. 各行を "~" で囲む。
  4. そのそれぞれを -e オプションの引数にする。
  5. 複数の -e オプションを並べて osascript コマンドの引数に渡す。

という処理をすれば OK です。

例えば,

$ osascript -e "say \"Hello, world! \"" -e "display notification \"⛄️\" with title \"アレ\" sound name \"Glass\"" 

と実行すれば,まず “Hello, world!” と発声した後に,通知音 “Glass” とともに次のような通知が表示されます。

f:id:doraTeX:20190605233823p:plain

ワンライナー変換結果

上記のQRコード生成スクリプトをワンライナー化すると以下のようになります(最後に実行した文の返値が標準出力に出てしまうので > /dev/null して捨てています)。

$ osascript -e "set message to \"TeXはアレ☃︎\"" -e "set outputPosixPath to \"/private/tmp/QR.png\"" -e "set scale to 5" -e "set correctionLevel to \"H\"" -e "use AppleScript version \"2.4\"" -e "use framework \"Foundation\"" -e "use scripting additions" -e "my genQR(message, outputPosixPath, scale, correctionLevel)" -e "on genQR(message, outputPosixPath, scale, correctionLevel)" -e "set theData to (current application's NSString's stringWithString:message)'s dataUsingEncoding:(current application's NSUTF8StringEncoding)" -e "set transform to (current application's CGAffineTransformMake(scale, 0, 0, scale, 0, 0))" -e "set filter to current application's CIFilter's filterWithName:\"CIQRCodeGenerator\"" -e "filter's setValue:theData forKey:\"inputMessage\"" -e "filter's setValue:correctionLevel forKey:\"inputCorrectionLevel\"" -e "set theCIImage to filter's outputImage's imageByApplyingTransform:transform" -e "my saveToPng(theCIImage, outputPosixPath)" -e "end genQR" -e "on saveToPng(theCIImage, outputPosixPath)" -e "set theCIImageRep to current application's NSCIImageRep's imageRepWithCIImage:theCIImage" -e "set theNsImage to current application's NSImage's alloc's initWithSize:(theCIImageRep's |size|)" -e "theNsImage's addRepresentation:theCIImageRep" -e "set tiffData to theNsImage's TIFFRepresentation" -e "set theBitmapImageRep to current application's NSBitmapImageRep's imageRepWithData:tiffData" -e "set theProps to current application's NSDictionary's dictionaryWithObject:1.0 forKey:(current application's NSImageCompressionFactor)" -e "set pngData to (theBitmapImageRep's representationUsingType:(current application's NSPNGFileType) |properties|:theProps)" -e "pngData's writeToFile:outputPosixPath atomically:false" -e "end saveToPng" > /dev/null

シェル関数化する

このコードにおいて,変数 outputPosixPath の内容としては絶対パスを渡さねばなりません。macOS 標準では realpath コマンドがインストールされていないので,Stack Exchange の記事 に従い,汎用的に使える realpath 関数をシェル関数として用意しておきます。

#!/bin/bash
function realpath ()
{
  f=$@;
  if [ -d "$f" ]; then
    base="";
    dir="$f";
  else
    base="/$(basename "$f")";
    dir=$(dirname "$f");
  fi;
  dir=$(cd "$dir" && /bin/pwd);
  echo "$dir$base"
}
function genQR () {
  (
    QRSCALE=${QRSCALE:-5}    
    QRCORRECTIONLEVEL=${QRCORRECTIONLEVEL:-H}
    ABSPATH=$(realpath $2)
    /usr/bin/osascript -e "set message to \"$1\"" -e "set outputPosixPath to \"${ABSPATH}\"" -e "set scale to ${QRSCALE}" -e "set correctionLevel to \"${QRCORRECTIONLEVEL}\"" -e "use AppleScript version \"2.4\"" -e "use framework \"Foundation\"" -e "use scripting additions" -e "my genQR(message, outputPosixPath, scale, correctionLevel)" -e "on genQR(message, outputPosixPath, scale, correctionLevel)" -e "set theData to (current application's NSString's stringWithString:message)'s dataUsingEncoding:(current application's NSUTF8StringEncoding)" -e "set transform to (current application's CGAffineTransformMake(scale, 0, 0, scale, 0, 0))" -e "set filter to current application's CIFilter's filterWithName:\"CIQRCodeGenerator\"" -e "filter's setValue:theData forKey:\"inputMessage\"" -e "filter's setValue:correctionLevel forKey:\"inputCorrectionLevel\"" -e "set theCIImage to filter's outputImage's imageByApplyingTransform:transform" -e "my saveToPng(theCIImage, outputPosixPath)" -e "end genQR" -e "on saveToPng(theCIImage, outputPosixPath)" -e "set theCIImageRep to current application's NSCIImageRep's imageRepWithCIImage:theCIImage" -e "set theNsImage to current application's NSImage's alloc's initWithSize:(theCIImageRep's |size|)" -e "theNsImage's addRepresentation:theCIImageRep" -e "set tiffData to theNsImage's TIFFRepresentation" -e "set theBitmapImageRep to current application's NSBitmapImageRep's imageRepWithData:tiffData" -e "set theProps to current application's NSDictionary's dictionaryWithObject:1.0 forKey:(current application's NSImageCompressionFactor)" -e "set pngData to (theBitmapImageRep's representationUsingType:(current application's NSPNGFileType) |properties|:theProps)" -e "pngData's writeToFile:outputPosixPath atomically:false" -e "end saveToPng" > /dev/null
  )
}

このように定義しておくと,

$ genQR TeXはアレ☃︎ QR.png

のようにしてコマンドラインから簡単にQRコードを生成できるようになります。

スケールや誤り訂正レベルの変更は,

$ QRSCALE=20 QRCORRECTIONLEVEL=L genQR TeXはアレ☃︎ QR.png

のようにしてできます。

TeX ソースからワンライナーを呼び出す

ワンライナー化したものを -shell-escape された TeX ソース中から \immediate\write18 で呼び出せば,次の記事と同様にして TeX からQRコード画像を動的生成することもできます。

doratex.hatenablog.jp

*1:開発者は自分で開発環境をインストールすれば済む話ですが,アプリを配布される側に「事前に○○のインストールを済ませておいてください」と要求するのでは配布における大きなハードルとなります。「アプリにランタイムを同梱しておくことを推奨」とありますが,各アプリがそれぞれ Python やら Ruby やらの実行環境を個別に同梱すると,Mac 内が「何重もの Python 実行環境だらけ」みたいな状態になって,無駄が多い感じがしますね……。