去年の記事において,連番のQRコード画像を一括作成するSwiftコードの例を示しました。
このSwiftコードをちょっと改変すれば,「コマンドライン引数に与えられた文字列からQRコード画像を生成する」コマンドラインツールは簡単に作れます。もちろん,Homebrewなどを用いて qrencode のような専用ツールをインストールしてもよいでしょう。ただし,自分一人で使う環境ならば何でもいいのですが,広く配布するスクリプトなどを想定する場合,デプロイ時の可搬性という観点で考えると,追加のプログラム/ライブラリの事前インストールを必要とせず,macOS環境であればどこでもシェルから1行で呼び出せるコマンドが存在する嬉しいですよね。
「macOSのAPIをコマンドラインから直接呼び出す」ためには,PyObjC が有効な手立てとなります。この手法は以前のPDFページカウントの記事でも使いました。
では,Swift を使って書いたQRコード生成ルーチンを,PyObjC に書き換えていきましょう。
1. Swiftコードをフラットに書く
連番QRコード生成の記事で書いたSwiftコードは,extension
を使っていたりと,Swiftの機能を使って構造化されていました。しかし,後でPythonワンライナー化する都合上,あえて構造化しないバッチファイルのようなフラットな構造のコードにしておくと好都合です。Pythonのインデントはワンライナー化における障害になりますからね。
ここでは,QRコード画像生成のルーチンを次のようなフラットなSwiftコードにしてみました。(なお,ここではエラー処理は全くやっていません。null安全などSwift言語の機構を使ってしまうと別言語に移植しづらくなってしまいますし……)
import Quartz // QRコード生成のパラメータ let message = "TeXはアレ☃︎" let outputPath = "QR.png" let scale: CGFloat = 5 let dpi: CGFloat = 144 let correctionLevel = "H" // QRコード生成フィルターの準備 let data = message.data(using: .utf8) let transform = CGAffineTransform(scaleX: scale, y: scale) let filter = CIFilter(name: "CIQRCodeGenerator")! filter.setValue(data, forKey: "inputMessage") filter.setValue(correctionLevel, forKey: "inputCorrectionLevel") // QRコード生成 let ciImage = filter.outputImage!.transformed(by: transform) let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent)! // PNGデータに変換 let outputData = NSMutableData() let destination = CGImageDestinationCreateWithData(outputData, kUTTypePNG, 1, nil)! let properties: [CFString : Any] = [kCGImagePropertyIPTCImageType: 1.0, kCGImagePropertyDPIWidth: dpi, kCGImagePropertyDPIHeight: dpi] CGImageDestinationAddImage(destination, cgImage, properties as CFDictionary) CGImageDestinationFinalize(destination) // ファイルに書き込み outputData.write(toFile: outputPath, atomically: false)
2. Objective-C に変換する
上記Swiftコードを,対応する Objective-C のコードに変換します。
// clang generateQRcode.m -framework Foundation -framework Quartz -framework ImageIO -framework CoreServices #import <Quartz/Quartz.h> #import <ImageIO/ImageIO.h> #import <CoreServices/CoreServices.h> int main(int argc, char *argv[]) { @autoreleasepool { // QRコード生成のパラメータ NSString *message = @"TeXはアレ☃︎"; NSString *outputPath = @"QR.png"; CGFloat scale = 5; CGFloat dpi = 144; NSString *correctionLevel = @"H"; // QRコード生成フィルターの準備 NSData *data = [message dataUsingEncoding: NSUTF8StringEncoding]; CGAffineTransform transform = CGAffineTransformMake(scale, 0, 0, scale, 0 ,0); CIFilter *filter = [CIFilter filterWithName: @"CIQRCodeGenerator"]; [filter setValue: data forKey: @"inputMessage"]; [filter setValue: correctionLevel forKey: @"inputCorrectionLevel"]; // QRコード生成 CIImage *ciImage = [[filter outputImage] imageByApplyingTransform: transform]; CGImageRef cgImage = [[[CIContext alloc] init] createCGImage: ciImage fromRect: [ciImage extent]]; // PNGデータに変換 NSMutableData *outputData = [NSMutableData data]; CFMutableDataRef cfOutputData = (CFMutableDataRef)CFBridgingRetain(outputData); CGImageDestinationRef destination = CGImageDestinationCreateWithData(cfOutputData, kUTTypePNG, 1, nil); NSDictionary *properties = @{(NSString*)kCGImagePropertyIPTCImageType: @(1.0), (NSString*)kCGImagePropertyDPIWidth: @(dpi), (NSString*)kCGImagePropertyDPIHeight: @(dpi)}; CFDictionaryRef cfProperties = (CFDictionaryRef)CFBridgingRetain(properties); CGImageDestinationAddImage(destination, cgImage, cfProperties); CGImageDestinationFinalize(destination); // ファイルに書き込み [outputData writeToFile: outputPath atomically: NO]; CFRelease(cfOutputData); CFRelease(cfProperties); CFRelease(cgImage); CFRelease(destination); } return 0; }
3. PyObjC に変換する
上記の Objective-C のコードを PyObjC を用いた Python コードに変換します。macOS に標準インストールされている Python は未だに 2.x ですので,Python 2 で書きます。
#!/usr/bin/python # -*- coding: utf-8 -*- message = 'TeXはアレ☃︎'.decode('utf-8') outputPath = 'QR.png'.decode('utf-8') scale = 5 dpi = 144 correctionLevel = 'H' import Foundation as fd import Quartz as qz data = fd.NSString.stringWithString_(message).dataUsingEncoding_(fd.NSUTF8StringEncoding) transform = qz.CGAffineTransformMake(scale, 0, 0, scale, 0, 0) filter = qz.CIFilter.filterWithName_('CIQRCodeGenerator') filter.setValue_forKey_(data, 'inputMessage') filter.setValue_forKey_(correctionLevel, 'inputCorrectionLevel') ciImage = filter.outputImage().imageByApplyingTransform_(transform) cgImage = qz.CIContext.alloc().init().createCGImage_fromRect_(ciImage, ciImage.extent()) outputData = fd.NSMutableData.alloc().init() destination = qz.CGImageDestinationCreateWithData(outputData,'public.png', 1, None) properties = { qz.kCGImagePropertyIPTCImageType: 1.0, qz.kCGImagePropertyDPIWidth: dpi, qz.kCGImagePropertyDPIHeight: dpi } qz.CGImageDestinationAddImage(destination, cgImage, properties) qz.CGImageDestinationFinalize(destination) outputData.writeToFile_atomically_(outputPath, False)
4. Python ワンライナーに変換する
上記の Python コードをワンライナー化し,シェルから直接実行できるようにすると,こうなります。
$ /usr/bin/python -c "message = 'TeXはアレ☃'.decode('utf-8');outputPath = 'QR.png'.decode('utf-8');scale = 5;dpi = 144;correctionLevel = 'H';import Foundation as fd;import Quartz as qz;data = fd.NSString.stringWithString_(message).dataUsingEncoding_(fd.NSUTF8StringEncoding);transform = qz.CGAffineTransformMake(scale, 0, 0, scale, 0, 0);filter = qz.CIFilter.filterWithName_('CIQRCodeGenerator');filter.setValue_forKey_(data, 'inputMessage');filter.setValue_forKey_(correctionLevel, 'inputCorrectionLevel');ciImage = filter.outputImage().imageByApplyingTransform_(transform);cgImage = qz.CIContext.alloc().init().createCGImage_fromRect_(ciImage, ciImage.extent());outputData = fd.NSMutableData.alloc().init();destination = qz.CGImageDestinationCreateWithData(outputData,'public.png', 1, None);properties = { qz.kCGImagePropertyIPTCImageType: 1.0, qz.kCGImagePropertyDPIWidth: dpi, qz.kCGImagePropertyDPIHeight: dpi };qz.CGImageDestinationAddImage(destination, cgImage, properties);qz.CGImageDestinationFinalize(destination);outputData.writeToFile_atomically_(outputPath, False)"
これを使えば,shell-escape されたTeXソース中から \immediate\write18
でQRコード画像を動的に生成するということも可能になります。
5. シェル関数化する
毎回上記のコードを打ち込むのは大変ですので,シェル関数化しておくとシェルスクリプト中などで使い勝手がよいでしょう。
#!/bin/bash function genQR () { ( QRSCALE=${QRSCALE:-5} QRDPI=${QRDPI:-144} QRCORRECTIONLEVEL=${QRCORRECTIONLEVEL:-H} /usr/bin/python -c "message = '$1'.decode('utf-8');outputPath = '$2'.decode('utf-8');scale = ${QRSCALE};dpi = ${QRDPI};correctionLevel = '${QRCORRECTIONLEVEL}';import Foundation as fd;import Quartz as qz;data = fd.NSString.stringWithString_(message).dataUsingEncoding_(fd.NSUTF8StringEncoding);transform = qz.CGAffineTransformMake(scale, 0, 0, scale, 0, 0);filter = qz.CIFilter.filterWithName_('CIQRCodeGenerator');filter.setValue_forKey_(data, 'inputMessage');filter.setValue_forKey_(correctionLevel, 'inputCorrectionLevel');ciImage = filter.outputImage().imageByApplyingTransform_(transform);cgImage = qz.CIContext.alloc().init().createCGImage_fromRect_(ciImage, ciImage.extent());outputData = fd.NSMutableData.alloc().init();destination = qz.CGImageDestinationCreateWithData(outputData,'public.png', 1, None);properties = { qz.kCGImagePropertyIPTCImageType: 1.0, qz.kCGImagePropertyDPIWidth: dpi, qz.kCGImagePropertyDPIHeight: dpi };qz.CGImageDestinationAddImage(destination, cgImage, properties);qz.CGImageDestinationFinalize(destination);outputData.writeToFile_atomically_(outputPath, False)" ) }
このように定義しておくと,
$ genQR TeXはアレ☃︎ QR.png
のようにしてコマンドラインから簡単にQRコードを生成できるようになります。
スケールや誤り訂正レベルなどのパラメータ値の変更は,
$ QRSCALE=20 QRDPI=72 QRCORRECTIONLEVEL=L genQR TeXはアレ☃︎ QR.png
のようにしてできます。