TeX Alchemist Online

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

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

去年の記事において,連番のQRコード画像を一括作成するSwiftコードの例を示しました。

doratex.hatenablog.jp

このSwiftコードをちょっと改変すれば,「コマンドライン引数に与えられた文字列からQRコード画像を生成する」コマンドラインツールは簡単に作れます。もちろん,Homebrewなどを用いて qrencode のような専用ツールをインストールしてもよいでしょう。ただし,自分一人で使う環境ならば何でもいいのですが,広く配布するスクリプトなどを想定する場合,デプロイ時の可搬性という観点で考えると,追加のプログラム/ライブラリの事前インストールを必要とせず,macOS環境であればどこでもシェルから1行で呼び出せるコマンドが存在する嬉しいですよね。

「macOSのAPIをコマンドラインから直接呼び出す」ためには,PyObjC が有効な手立てとなります。この手法は以前のPDFページカウントの記事でも使いました。

doratex.hatenablog.jp

では,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

のようにしてできます。