展示会の案内PDFから、出展やセミナーの申込先を一覧にしようとしました。まず pdftotext で文字を抜いてURLを探しましたが、1本も出てきません。「申込先が書かれていない案内」として報告しかけたところで、紙面にQRコードが並んでいることに気づきました。
数えると、申込の入口は7本。そのうち6本はQRコードの中にしかURLが無く、本文の文字から拾えたものは0本でした。チラシの面は画像1枚で、文字として抜けるものがそもそもありません。
原因
pdftotext が取り出すのは PDF の中の文字だけです。QRコードは画像なので、中のURLは出てきません。案内のチラシは「詳しくはQRから」で済ませてURLを書かないことが多く、文字だけ見ると「申込先なし」に見えます。
直し方1:ページを画像にする
poppler の pdftoppm で、ページを高めの解像度で PNG にします(Homebrew なら brew install poppler)。
pdftoppm -r 400 -png flyer.pdf page
# page-1.png, page-2.png ... ができる
直し方2:Swift の Vision で QR を読む
macOS なら、標準の Vision フレームワークで QR を読めます。swiftc は Command Line Tools に入っているので、追加で入れるものはありません。
import Foundation
import Vision
import AppKit
// 使い方: ./readqr page-1.png page-2.png ...
for path in CommandLine.arguments.dropFirst() {
guard let img = NSImage(contentsOfFile: path),
let cg = img.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
print("\(path)\t読めない"); continue
}
let req = VNDetectBarcodesRequest()
req.symbologies = [.qr]
try VNImageRequestHandler(cgImage: cg).perform([req])
let found = req.results ?? []
print("\(path)\tQR \(found.count) 個")
for r in found {
let b = r.boundingBox // 左下が原点・0〜1 の割合
print(String(format: " x=%.2f y=%.2f\t%@", b.midX, b.midY, r.payloadStringValue ?? "(文字なし)"))
}
}
swiftc -O readqr.swift -o readqr
./readqr page-*.png
# page-1.png QR 2 個
# x=0.71 y=0.44 https://example.com/apply-b
# x=0.21 y=0.44 https://example.com/apply-a
直し方3:数と位置を紙面と突き合わせる
- 読めた個数が、紙面のQRの個数と合うかを数えます。 足りなければ解像度を上げるか、QRのあたりを切り抜いてから読み直します。小さいQRが並んでいると、400dpiでも取りこぼすことがあります
- 出力の順番は紙面の順番ではありません。
boundingBoxの位置(x が左右、y が上下。y は下から数える)で、「どのQRがどの申込か」を紙面の見出しと対応させます。URLだけ並べても、どれが何の申込か分かりません - 短縮URLは、行き先を確かめてから使います
curl -sSL -o /dev/null -w '%{url_effective}\n' https://短縮URL
読めたら、開いて確かめる
URLが読めたことと、申し込めることは別です。今回は読み出した申込先を実際に開くと、3本はもう締切か満席でした。一覧にするなら「開いて受付中か」まで見て書きます。
まとめ
- 案内PDFの申込先は、QRコードの中にしか無いことがある。
pdftotextで何も出なくても「書かれていない」とは言えない -
pdftoppmで画像にして、Swift のVNDetectBarcodesRequestで読む。macOS なら追加のライブラリは要らない - 読めた個数を紙面と数え合わせ、位置で対応を付け、開いて受付中かを確かめる
生成AI(Claude Code)に書かせて一人で会社を回している記録を note に書いています。
https://note.com/kaito_cometa