設計メモ: 判断の理由と実装方針

spec.md が「何を作るか」、このファイルが「なぜそうしたか」と「どう作るか」。

1. PDF と何が違うのか

PDF BDF
描画モデル PostScript 由来。独自のグラフィックス状態、フォント機構、色空間 Canvas 2D そのもの。ブラウザが持っていない概念は入れない
フォント 埋め込み形式が多数(Type1/CFF/TrueType/Type3)。ラスタライザが必要 WOFF2 を FontFace に渡すだけ。テキスト描画はブラウザ任せ
画像 独自フィルタ(DCT/Flate/CCITT/JBIG2…) ブラウザがデコードできる形式をそのまま格納
圧縮 Flate 等を自前実装 DecompressionStream
構造 xref テーブル、オブジェクトグラフ 内容アドレスの Part 群 + JSON manifest
共有 Form XObject Object(内容アドレスで自動 dedup)
無限平面 なし sheet View + Tile
部分取得 Linearized PDF(複雑、対応まちまち) Part 単位の Range / 個別ファイル

pdf.js が数万行かけてやっていること(フォントラスタライズ、フィルタ、パーサ)をブラウザの標準 API に肩代わりさせるのが狙い。デコーダは「バイト列を読んで ctx を呼ぶループ」だけになる。

2. Go と wasm について

結論: サーバー側エンコーダ・変換 CLI は Go、ブラウザ側ビューアは TypeScript。ビューアで Go/wasm は使わない(少なくとも v1 では)。

理由:

wasm が意味を持つケース:

したがって:

ブラウザ内変換(cmd/bdfwasm)

変換器は GOOS=js GOARCH=wasm でそのままビルドでき、testdata の PDF・Word・PowerPoint・Excel・CSV・Visio はネイティブと同じバイト列に変換される。cmd/bdfwasm はページから渡されたバイト列を変換し、単一ファイル形式の BDF を返す wasm モジュールである(API はパッケージのコメントを参照)。デモサイト(site/build.mjs、GitHub Pages で公開)はこれを Worker で動かし、結果を {kind: "buffer"} としてレンダラの Worker に渡す。

フォールバック探索では、指定フォントに無い文字について各候補の cmap を調べてからフォント全体を取得する。どのフォントにも無い文字(絵文字など)を探す場合でも cmap の先読みデータだけで探索が終わり、候補フォント全体の連続取得を避けられる。

ページ単位の変換(converter.OpenStream)

文書を丸ごと変換してから渡すと、最初のページが見えるまで全ページの変換を待つことになる。Chrome で作った 237 ページの PDF(テキスト主体、1.8 MB)は wasm で丸ごと約 3.5 秒かかる。converter.Stream はページ単位の変換で、ビューアはページの大きさだけの outline をすぐ開き、変換できたページから描く。同じ PDF で outline まで 82 ms、どのページでも最初の 1 ページは約 110 ms、全ページで 1.4 秒、そのあと完成した文書を作るのに 1.4 秒かかる(Node の wasm で計測)。

3. 変換パイプライン(現実的な順序)

Office ファイルを直接 BDF にするには Word 相当のレイアウトエンジンが要る。ここが一番重いので、段階を踏む。

  1. PDF → BDF(最初のターゲット)
    • PDF の内容ストリーム演算子(m l c re f S q Q cm Tf Tj Do …)は BDF の命令にほぼ 1:1 で落ちる。
    • LibreOffice headless で Office → PDF にしてから BDF 化すれば、それだけで PowerPoint / Word のプレビューが高忠実度で成立する。
    • フォントは PDF 埋め込みフォントを抽出して WOFF2 に再パッケージ(CFF/TrueType ならほぼ機械的。Type3 はアウトライン化)。
    • 難点: PDF ではマスター共有が失われている(各ページに同じ命令が展開される)。同一バイト列になる Object は自動 dedup されるが、微妙に違うページでは効かない。ページ内の「先頭の共通プレフィックス」を切り出して共有 Object にするヒューリスティックで補う(§3.1)。
  2. XLSX → BDF(直接変換)
    • PDF 経由だと無限シートが失われるので、こちらは直接。セルのレイアウトは行列の格子なので、文書レイアウトほど難しくない。実装済み(§3.6)。
    • 図形・画像・グラフは PowerPoint と同じ DrawingML なので、PowerPoint の変換器と描画処理を共有する(画像化はしない)。
    • CSV・TSV は値だけの表として同じシートの描画に渡す(§3.10)。Parquet も値を文字にして同じように渡す(§3.26)。
  3. PPTX / DOCX → BDF(直接変換)
    • PPTX は絶対配置なので DOCX より先に手が届く(テキストボックス内の折り返しは必要)。実装済み(§3.4)。
    • マスター・レイアウト・スライドの継承構造が BDF の共有 Object にそのまま対応するので、直接変換するとサイズ面の効果が最も大きい。
    • DOCX は Word 相当のレイアウトエンジンを変換側に持って実装した(§3.9)。紙面のページと、ページを持たずにレイアウトし直した 1 枚の長い面の 2 つの View を作る。
  4. HTML / Markdown → BDF(リーダー表示)
    • ページの CSS は捨て、内容を固定のスタイルシートで DOCX と同じレイアウトエンジンに組ませる。Markdown は HTML にしてから同じ道を通す。実装済み(§3.16)。

3.1 PDF → BDF 変換器(converter/pdf)の構造

converter/pdf パッケージは PDF オブジェクト層に pdfcpu を使い、内容ストリームの解釈は自前で行う。CLI は bdf generate(入力の形式は中身から判別する)。

テスト用 PDF は Chromium(Skia)と reportlab で生成し、CJK の定義済み CMap と JPEG 2000・JBIG2 の画像のものは標準ライブラリだけの Python で書く(npm run test:pdf:gen。画像は opj_compress を使う)、変換結果は testdata/pdf/ に置く。

3.2 画像の格納と変換(imgconv)

画像の扱いは「そのまま」と「変換あり」の 2 モードで、pdf.Options.Images / pptx.Options.Images(CLI は -images keep|convert)で選ぶ。今後の Office 変換器も同じパッケージを使う。

デコードした画素の格納は、任意の画像型を受け取る EncodePixels でも行える。Keep では、可逆の元の画素は色数に合わせた最小の PNG(二色なら 1 ビットのパレット、グレー、256 色までのパレット)に、JPEG など非可逆の元の画素は JPEG(Quality)にする。Convert では、それより小さければ WebP にする。二値のスキャンの 1 ページ(A4 300dpi)は、RGB の PNG では 131KB、1 ビットの PNG では 67KB(元の G4 は 58KB)、WebP 可逆では 53KB になる。

デコードする画像の大きさ: 画像のヘッダーは大きさを自分で言い、デコーダは画素を読む前にその分のメモリを確保する。100 バイトの PNG が 20000 × 20000 画素だと言えば、それだけで 380 MiB になる。imgconv.Decode はヘッダーを先に読み、MaxDecodePixels(1 億 485 万 7600 画素、RGBA で 400 MiB)を超える画像はデコードしない(ErrTooLarge)。Optimize はそういう画像を変換せずにそのまま格納し、文書の中の画像をデコードする変換器(メタファイルの中の PNG・JPEG、Visio のビットマップ、マスクの付いた PDF の JPEG、色を変える Office の画像)とラスタライザも同じ上限に従う。デコーダのパニックはエラーにする。

JPEG のスキャンの数: JPEG のデコーダは、スキャン 1 つごとに画像の全ブロックを 1 周する。スキャンがどれだけ小さくてもそうなので、スキャンだけを並べたファイルは、大きさのわりに桁違いの時間がかかる。8192 × 8192 画素でスキャンが 500 個のファイルは 65 KB しかないのに、デコードに 12 秒かかった(Go の image/jpeg、Apple M 系)。1 MB なら数分になる。imgconv.Decode は JPEG を 最初の MaxJPEGScans(100)個のスキャンまで 読む(LimitJPEGScans がその後ろを切って画像の終わりを付ける)。断るのではなく、そこまでのスキャンが描く絵にする。プログレッシブ JPEG を読み込み途中で表示するブラウザと同じ見え方で、エンコーダが書くスキャンは 10 個前後(プログレッシブでない JPEG は多くて 4 個)なので、普通の画像は何も変わらない。スキャンの位置は image/jpeg がセグメントの間でマーカーを探すのと同じ規則で数える(セグメントの中、たとえば EXIF のサムネイルのスキャンは数えない)。ラスタライザは切ったときに警告を出す。

解像度の上限: ページ上の大きさが分かっているラスター入力(TIFF のページ、Photoshop の文書のページ)は、Options.MaxDPI(既定 192dpi)と MaxPixels(既定 3840 × 3840 画素)の両方を超えないよう縮小する(FitSize が大きさを決め、Resize が縮小する)。Keep と Convert のどちらでも効き、負の値で上限をなくす。考え方と実測は §3.15。

コーデックは libwebp(エンコーダのみ)を wasi-sdk で wasm にし、shibukawa/wasm2go-fork(pgmem ブランチ)で純 Go に変換したもので、cgo も wasm ランタイムも使わない。生成物は image/imgconv/internal/webpw(スカラー、約 5MB、44 ファイル)と image/imgconv/internal/webpwsimd(SIMD、GOEXPERIMENT=simd 専用、後述)で、tools/gen-codecs.sh で再生成する。フォークの -symbol-names(関数名を wasm の name セクションから付ける)、-group-files(vp8_enc.go のように主題ごとのファイルに分ける)、-addr-consts(静的データのアドレスを名前付き定数にする)を使い、libwebp を更新しても差分が小さく収まるようにしている。gen2brain 同梱の wasm は name セクションが落とされているので、自前でビルドしている。

ビルドタグ

タグ 効果
(なし) WebP 可逆・非可逆を同梱
bdf_noconv コーデックを一切リンクしない(WOFF2 の Brotli エンコーダも含む。§3.3)。Convert を指定しても Keep として動き、ErrNotAvailable を返す。tinygo でブラウザ向けに wasm 化するときはこれを使う

測定(1024×768、純 Go、4 コアのコンテナ)

画像 PNG JPEG q80 WebP 可逆 WebP q80 m6 AVIF q60 s10 AVIF q60 s6 AVIF 可逆
図版(少色数) 6.7 KB 64 KB 0.3 KB / 87 ms 14 KB / 150 ms 28 KB / 155 ms 3.2 KB / 1.0 s 63 KB / 0.2 s
写真風 1463 KB 90 KB 1405 KB / 0.5 s 43 KB / 0.35 s 41 KB / 0.16 s 39 KB / 1.35 s 1422 KB / 0.4 s

フォークで変換したコードは、gen2brain 同梱の Go 化コードより WebP で 1.6〜2 倍、wazero 実行の AVIF より 3〜4 倍速く、出力はバイト単位で同一だった。

3.3 フォントの格納(woff2)

woff2 パッケージが組み直した TrueType/OpenType を WOFF2 にする。仕様書の既定どおり glyf/loca には WOFF2 の glyf 変換をかけ(輪郭の点・フラグ・命令・合成グリフを別々のストリームに分け、点は差分を 1〜4 バイトの三つ組で書き、bbox は点から計算できるものは省き、loca は送らない)、それ以外の表はそのまま並べて全体を Brotli で圧縮する。Brotli は純 Go の andybalholm/brotli。変換で表せないグリフ(3 次曲線の glyf、壊れたグリフ)があるフォントは glyf/loca を無変換で入れる。loca は長形式で復元させ(送らないので大きさは変わらない)、head.indexToLocFormat も 1 にそろえる。

同梱の wasm はスカラーでビルドしている(測定の結果)。wasm SIMD を有効にした 2 種類のビルド(clang の自動ベクトル化のみ、および emscripten の SSE 互換ヘッダで libwebp 自身の SSE2/SSE4.1 経路を有効化したもの)を、wasm2go の純 Go モードと asm バックエンド(-fuse-loops、SIMD 命令 10,866 箇所をインライン展開)で変換して比べたが、いずれもスカラーより遅かった。v128 を [2]uint64 の組で運ぶこれらのバックエンドでは、4×4〜16×16 ブロック単位の短い SIMD カーネルの命令 1 つごとに汎用レジスタとの往復が入り、ベクトル化の利得を上回る。

1024×768 写真風 q80 m6 スカラー 自動ベクトル化 SSE2/SSE4.1 経路
純 Go モード(GOAMD64=v2) 0.35 s 0.75 s 2.4 s
asm バックエンド 0.35 s 0.39 s 0.52 s

この往復をなくすため、フォークに -simd=go127 を追加した。Go 1.27 の GOEXPERIMENT=simd で入る simd/archsimd の 128 bit ベクトル型で v128 を運び、SIMD 命令ごとにインライン展開される base.Simd_g_* メソッド連鎖を生成する(amd64 は AVX/AVX2、arm64 は NEON)。v128 に触れる関数は [2]uint64 版と archsimd 版の 2 本が出力され、goexperiment.simd && go1.27 && !go1.28 && (amd64 || arm64) のビルドタグで切り替わる。archsimd は互換性保証がなく 1.26 と 1.27 で API が変わったため、対象の Go リリースを明示し、生成コード本体は base.V128 と base.Simd_g_* だけを参照する。移植性を重視した simd パッケージも検討したが、ベクトル長が実行時に決まり(AVX2 機で 256 bit)、shuffle や narrow などの命令が無く、simd 型を使う関数が 4 本にクローンされるため、128 bit 固定の v128 には使えなかった。

SSE 経路の libwebp を -simd=go127 で変換し、GOEXPERIMENT=simd でビルドして測った結果(各 5 回、出力はスカラー版とバイト単位で同一):

スカラー(同梱) SSE 経路 + archsimd ネイティブ C(SIMD なし → あり)
写真風 q80 m6 0.33〜0.35 s 0.30〜0.31 s 226 → 185 ms
図版 q80 m6 0.15 s 0.11〜0.12 s 88 → 62 ms
図版 可逆 38〜41 ms 32〜41 ms 25 → 23 ms
写真風 可逆 0.48〜0.53 s 0.46〜0.50 s 305 → 248 ms

非可逆は 1.1〜1.3 倍速くなり、可逆は同等である。最初の計測では可逆が 1.5 倍遅かったが、原因は libwebp の可逆 SSE 経路ではなく(ネイティブ C では SSE ありの方が可逆も速い)、Go 1.27 側の 2 つの挙動の組み合わせだった。gc はベクトル型のゼロ値をレガシー SSE の MOVUPS X15, Xn で作り、ランタイムの非同期プリエンプションは AVX-512 機でレジスタを VMOVDQU64 Z0..Z31 で復元して VZEROUPPER を実行しないため、最初のプリエンプション以降はレガシー SSE 命令 1 つごとに SSE/AVX 状態遷移(実測で約 300 サイクル)が発生する。可逆の予測器はピクセルごとに i8x16.narrow_i16x8_u を呼び、そのエミュレーション中のゼロ値がこれに当たっていた(GODEBUG=asyncpreemptoff=1 で 30 倍速くなることで確認)。ヘルパーからゼロ値ベクトルをなくして解消したが、gc がスピル/リロードに使う MOVUPS は残る(生成コード中に約 1 万箇所)。この libwebp では影響は小さかったが、AVX-512 機で GOEXPERIMENT=simd を使う場合は //go:debug asyncpreemptoff=1 を検討すること。なお i8x16.narrow_i16x8_u(VPACKUSWB)自体は Go 1.27 の archsimd が AVX の範囲で公開しておらず 7 命令でエミュレートしているので、ここも将来の改善余地である。

同じ wasm から出る [2]uint64 版は 2.4 s のもので、GOEXPERIMENT=simd を付けない通常のビルドが 7 倍遅くなる。そのため 2 つのパッケージを同梱し、ビルドタグで切り替えている。image/imgconv/internal/webpw はスカラーの libwebp を [2]uint64 で運ぶ版(約 5MB)、image/imgconv/internal/webpwsimd は SSE 経路の libwebp を archsimd で運ぶ版で、goexperiment.simd && go1.27 && !go1.28 && (amd64 || arm64) のときだけコンパイルされる([2]uint64 側の関数は落としてあり約 8.6MB、git 上は gzip で 1.3MB)。imgconv の webp_scalar.go / webp_simd.go が同じタグで束ね直し、imgconv.SIMD() がどちらが入ったかを返す。利用者は Go 1.27 で GOEXPERIMENT=simd go build するだけで SIMD 版になる(amd64 は実行時に AVX2 が必要で、無ければ init で panic する)。再生成は tools/gen-codecs.sh(スカラー)と SIMD=1 tools/gen-codecs.sh(SIMD)。AVIF も評価したが採用しなかった。可逆は図版で WebP の 200 倍大きく、非可逆は speed 6 で図版に効くものの 3〜4 倍遅く、生成コードが 47MB になるためである。

3.4 PowerPoint → BDF 変換器(converter/pptx)の構造

converter/pptx は .pptx(OPC の zip)を読み、DrawingML を直接 BDF の命令に落とす。Office から PDF を経由する経路と比べると、マスター・レイアウトの共有とテキストの構造(段落・箇条書き・行の継ぎ目)が残る。XML は構造体にデコードせず汎用の要素木として読む(DrawingML は省略可能な要素と多段の継承が多いため)。mc:AlternateContent は Fallback を使う(Word は Choice を選ぶ、§3.9)。OPC パッケージ(パート、リレーションシップ)と要素木の読み込みは converter/internal/ooxml にあり、Excel・Word・Visio の変換器も同じものを使う。

以下のうち DrawingML の描画(テーマと配色、図形、テキスト、表、グラフ、画像、SmartArt の描画パート、メタファイル)は converter/internal/ooxml/drawingml にあり、Word・Excel の図形やグラフにも使える。文書の種類に依存するもの、つまりプレースホルダーの継承元、マスターのテキストスタイルと既定のテキストスタイル、スライド間のリンク(#page=N)、スライド番号のフィールドは、drawingml.Host インターフェースとして converter/pptx が与える。スライド・レイアウト・マスターの構成と背景(p:bg)の探索も pptx 側にある。描画先の Object は converter/internal/canvas で組み立てる。フォントの参照は文書全体のレイアウトが終わってサブセットが決まるまで仮のもので、最後にまとめて解決する。

テスト用のデッキは python-pptx で生成し(npm run test:pptx:gen、test/pptx/gen.py。数式のデッキ math.pptx は PowerPoint と同じ形の XML を差し込む)、変換結果は testdata/pptx/ に置く。フォントは converter/pptx/testdata/fonts の M PLUS 1p のサブセット(数式には STIX Two Math のサブセット)だけを使うので、出力は実行環境に依存しない。開発中は Apache POI のテストデータ(PowerPoint で作られた実ファイル約 90 本)でも変換を確かめ、LibreOffice の描画(PPTX → PDF → BDF)と見比べた。lumMod/lumOff と alpha を併用した色、グラデーションの線、縦書きなどでは LibreOffice の方が崩れる。

3.5 入力形式の登録と EMF/WMF 変換器(converter/emf)

入力形式は static plugin 方式で登録する。converter パッケージが形式の登録簿を持ち、各変換器のパッケージは init で converter.Register に名前・拡張子・判別関数・変換関数・形式固有のオプション(Params)を渡す。プログラムは import したパッケージの形式だけを扱えるので、PDF だけのサーバーは pptx やフォント処理をリンクせずに済む(全部なら converter/all)。converter.Detect は登録された形式の判別関数を名前順に試し(先頭 1 KiB と入力全体を渡す)、どの形式も中身から判別できない入力は、ファイル名(Options.FileName)の拡張子で決める(1 行の CSV、どんなテキストでもありうる Markdown、HTML の断片)。ほかの形式の特殊な場合である形式(PDF である Illustrator の .ai、画像である draw.io の PNG・SVG 書き出し)は Format.Refines にその形式を書き、先に試される。converter.Options は各形式に共通の設定(ページ指定、画像、フォント、入力の隣のファイルを読むディレクトリ Dir)と、名前で引く形式固有の設定(PDF の kind・box・no-share、Illustrator の box、Photoshop の artboards、PowerPoint の hidden、Word の views、HTML と Markdown の width・remote など)を持つ。CLI の bdf generate はこの登録簿で形式を判別・選択し、-h で登録された形式とその -param を一覧する。各パッケージの Convert と Options はそのまま直接使える。

converter/emf は Windows メタファイル(.emf、.wmf)を 1 ページの文書にする。再生は Office 文書の中の図と同じ converter/internal/metafile(§3.4 の EMF/WMF)。ページの大きさは EMF ならヘッダーの frame(0.01 mm 単位)、placeable WMF なら範囲と 1 インチあたりの単位数から求め、単位の分からない WMF は 96 dpi のピクセルとみなす。EMF ヘッダーの説明文字列にある図の名前を題名にする。テキストは PowerPoint と同じくフォントを解決してレイアウトし、サブセットを埋め込む(-font-dir、-fonts system なども同じ)。

メタファイルのレコードが言う数は信用しない。点の数はレコードに入っている分まで読み、深さが 1・2・4・8・16・24・32 ビットでないビットマップは確保する前に断る。有効なクリップは 64 個、書き出すクリップの点はメタファイル全体で 400 万個(描画のグループごとに、有効なクリップをすべて書き直すので、長いクリップと多くの図形の積になる)、保存する状態(SAVEDC)は 65,536 個までで、超えたら警告して続ける。保存した状態はクリップの並びを共有し、WMF のオブジェクト表の空きはヒープで持つ(どちらも、レコードの数の 2 乗の時間やメモリを使っていた)。

3.6 Excel → BDF 変換器(converter/xlsx)の構造

converter/xlsx は .xlsx(OPC の zip)を読み、ワークシートごとに sheet View を作る。セルの並び・書式・表示形式をこちらで解釈してレイアウトし(BDF に再レイアウトはない)、Tile に描く。セルの値は保存されているもの(数式の計算結果のキャッシュ)を使い、数式は計算しない。図形・画像・グラフは PowerPoint と同じ converter/internal/ooxml/drawingml で描き、フォントの選択・計測・サブセット埋め込み(fontset)と Object の組み立て(canvas)も共有する。

テスト用のブックは openpyxl で生成し(npm run test:xlsx:gen、test/xlsx/gen.py。openpyxl が書かない DrawingML の図形は後から描画パートに足す)、変換結果は testdata/xlsx/ に置く。フォントは PowerPoint のテストと同じ M PLUS 1p のサブセットだけを使う。開発中は Apache POI のテストデータ(Excel などで作られた実ファイル 355 本)をすべて変換し、壊れた zip と暗号化されたファイル以外が内部エラーなしに変換できること、条件付き書式や表示形式の見本が期待どおりに描かれることを確かめた。

印刷のページ(print.go、headerfooter.go): Options.Views(-param views=print|both)で、シートを Excel が印刷するページの flow View にも組む。帳票として作られたブックの本来の見た目で、テンプレートの穴埋め(§3.34)の出力でもある。

sheet View の出力はこの追加で変わらない(testdata/xlsx の既存の 3 文書はバイト単位で同じ)。both では 2 つの View を別々にレイアウトして描く(セルは共有し、sheet View が行ごとにセルを手放す最適化は止める)。

3.7 パスワードで保護された入力

保護された入力は、変換のときだけパスワードで開き、出力の BDF を同じパスワードで暗号化する(spec §3.5)。パスワードを知っている人だけが読める、という元のファイルの性質をプレビューでも保つためで、サーバーはパスワードを保存しない。表示のときはブラウザが入力されたパスワードで復号するので、サーバーは閲覧時にパスワードに関わらず、暗号化した BDF をそのまま配ればよい。

暗号化した文書はサーバー側の全文検索の対象にしない(索引が平文になるため)。文書の中の検索は、テキスト索引 Part も暗号化されて BDF の中にあるので、復号した後にブラウザでこれまでどおり動く。サムネイルのように平文が要るものは、サーバーが変換のとき(パスワードと平文を持っている間)に作るしかない。ただしサムネイルも 1 ページ目の中身を見せるので、bdf コマンドは暗号化した文書のサムネイルと検索用テキストを、頼まれたとき(-allow-plaintext)だけ書く(§3.25)。

3.8 Visio → BDF 変換器(converter/visio)の構造

converter/visio は Visio 2013 以降のパッケージ(.vsdx、.vsdm、.vstx、.vstm。OPC の zip で [MS-VSDX])を読む。Visio 2003〜2010 の XML 図面(.vdx、.vtx)と旧バイナリの .vsd は読まない(.vsdx に保存し直してもらう。.xls を読まないのと同じ。古い形式を残すと、読み込むコードと攻撃面が増えるだけで得るものが少ない)。シート(図形・マスターの図形・スタイルシート・ページシート)はセル(計算済みの値 V、式 F、表示単位 U)と、行を持つセクションからなる。Cell・Section・Row 要素をそのまま読む。テキストは文字と書式のマーカーが交互に並ぶ混在内容なので、ooxml.Node が文字と子要素の順序を覚えるようにした(Segments)。

テスト用の図面は test/visio/gen.py(標準ライブラリだけ)で 1 つのモデルから書き出す(npm run test:visio:gen)。shapes.vsdx はテーマ、ジオメトリの各行、塗り・線・矢印、テキスト、線の飛び越しを含む。flow.vsdx はテーマなしのフローチャートで、マスター・スタイル・画像・背景ページを含む。変換結果は testdata/visio/ に置く(フォントは PowerPoint と同じ M PLUS 1p のサブセット)。開発中は Apache POI と libvisio のテストデータでも変換を確かめた。比べる相手には、Visio が図面に保存するサムネイル(docProps/thumbnail.emf)を converter/emf で描いたものを使った。ただし手で編集されたテストファイルには、中身と合わない古いサムネイルもあった。

3.9 Word → BDF 変換器(converter/docx)の構造

converter/docx は .docx を読み、WordprocessingML を変換側でレイアウトする。BDF に再レイアウトはないので、Word がファイルを開くたびに行っている組版(行分割とページ分割)を変換時に済ませる。図(画像・図形・グループ・描画キャンバス・グラフ・SmartArt・メタファイル)は drawingml の Drawing.DrawGraphic が wp:inline / wp:anchor の枠に描き、Word の図形のテキストボックス(wps:txbx の w:txbxContent)は drawingml.TextBoxHost として docx 側が段落をレイアウトして描く。Word は DrawingML の図形を mc:AlternateContent の Choice(wps、wpg …)に、VML を Fallback に書くので、docx では対応する名前空間の Choice を選ぶ(ooxml.Package.Supported)。フォントの選択・計測・埋め込みは fontset、禁則は pptx と共通の converter/internal/linebreak。レイアウトエンジンは HTML・Markdown・EPUB の変換器と共有するので converter/internal/wordproc にあり(WordprocessingML の読み込みも同じパッケージ)、converter/docx は公開 API の薄い層になっている(§3.16)。

テスト用の文書は test/docx/gen.py が WordprocessingML を直接書いて作り(npm run test:docx:gen、標準ライブラリのみ)、変換結果は testdata/docx/ に置く。フォントは converter/docx/testdata/fonts の M PLUS 1p のサブセットだけを使う。開発中は Apache POI のテストデータ(Word などで作られた実ファイル約 130 本)がすべて変換できること(暗号化・破損したファイルを除く)と、描画が文書の内容どおりであることを確かめた。5,000 段入れ子の表のような極端な文書では、入れ子の深さに比例して op を写すため時間がかかる(10 秒程度)。

3.10 CSV・TSV → BDF 変換器(converter/csv)の構造

converter/csv はテキストの表を読み、Excel で開いたときと同じ見た目の sheet View を 1 枚作る。レイアウトと描画は converter/xlsx の ConvertGrid に任せる(書式のない値の表 xlsx.Grid を新規ブックの既定の書式で描く。セルのレイアウト、はみ出し、折り返し、Tile 化、読み上げ用の MARK は §3.6 と同じもの)。このパッケージが受け持つのは、ファイルに書かれていないことの推定である。推定の結果はどれも -param で上書きでき、変換の要約(Result、CLI の最終行)に出る。

テスト用のファイル(converter/csv/testdata/: Excel の「CSV UTF-8」と同じく BOM と CRLF の付いた basic.csv と、Shift_JIS の japanese.tsv)の変換結果は testdata/csv/ に置く。フォントは Excel と同じく PowerPoint のテストのものだけを使う。

3.11 draw.io → BDF 変換器(converter/drawio)の構造

converter/drawio は draw.io(diagrams.net)の図を読み、mxGraphModel の XML から直接 BDF の命令を作る。draw.io 自身の SVG・PDF 出力を経由しないのは、HTML ラベルが SVG の foreignObject(HTML)で出力され、そのままでは Canvas に描けないことと、ページ・レイヤー・リンク・テキストの構造を残すため。

テスト用の図は converter/drawio/testdata/ にあり(数式のラベルは math.drawio)、draw.io デスクトップ版のコマンドライン書き出し(draw.io -x -f svg|png)の結果と見比べて調整した。エッジの経路は、書き出した SVG(testdata/route/*.svg)のパスと 15 の図の 684 本で比べ、最大の差は 0.007px(Loop スタイルで draw.io 自身の結果が表示位置に依存する 2 本を除く)。ラベルの行の位置は Chrome で同じ HTML をレイアウトした結果と比べた。移植したコードの出典は converter/drawio/NOTICE。

3.12 DXF → BDF 変換器(converter/dxf)の構造

converter/dxf は AutoCAD の DXF(テキスト形式とバイナリ形式、R12〜2018)を読む。ネイティブの DWG は仕様が公開されておらず、読めるライブラリ(LibreDWG は GPLv3、ODA は有償の会員制)もこのリポジトリのライセンスでは使えないので扱わない(DXF に保存し直してもらう)。CAD 図面を描く部分は、続く JWW・SXF の変換器と共有するため converter/internal/cad に分けた。読み手は図面の座標(y 上向き、float64)のまま線・塗り・文字・クリップのグループを cad.Drawing に入れ、cad.Plotter がページの座標に写して Object に書く。座標を float32 にするのはページに写すときだけなので、平面直角座標のような大きな座標でも精度が落ちない。円弧・楕円・膨らみのある線分はベジェ曲線にして入れるので、非一様な拡大を含むどんなアフィン変換でも形が崩れない。線の太さは縮尺によらず用紙上の太さ(pt)で、プロッタが描くのと同じになる。

テスト用の図面は test/dxf/gen.py が ezdxf で作る(npm run test:dxf:gen、要 ezdxf。ezdxf の固定のメタデータで書くので毎回同じファイルになる)。shapes.dxf は各種の図形・文字・寸法・ハッチング・ブロックを、layout.dxf は平面図と A3 のレイアウト(表題欄と縮尺の違う 2 つのビューポート、片方で凍結した画層)を、r12-sjis.dxf は Shift_JIS の R12 図面を持つ。shapes-bin.dxf は shapes.dxf のバイナリ形式で、Go のテストは両者が同じ Object になることを確かめる。変換結果は testdata/dxf/ に置く(フォントは PowerPoint と同じ M PLUS 1p のサブセット)。開発中は、公開されている AutoCAD 2004 形式の図面(Shift_JIS の日本語、寸法、ブロック、マルチ引出線を含む)でも変換を確かめた。

3.13 Jw_cad → BDF 変換器(converter/jww)の構造

converter/jww は Jw_cad の図面ファイル(.jww)を読む。形式は作者が公開している「Jw_cad のデータ形式」(Jw_cad 7.02 の説明、jwdatafmt.txt)による。DOS 版の .jwc と、図形ファイルの .jws・.jwk は読まない。描くのは DXF と同じ converter/internal/cad である。

テスト用の図面は test/jww/gen.py が仕様書どおりに CArchive を書いて作る(npm run test:jww:gen、標準ライブラリのみ)。shapes.jww は Jw_cad 7 の形式(データ版数 700。SXF の拡張線色・線種、図形ごとの線幅、8 バイトの CTime)で、各種の図形・線色・線種・文字・寸法・ソリッド・入れ子のブロックと、非表示のレイヤグループとレイヤ、補助線を持つ。old.jww は Jw_cad 3 の形式(版数 300、4 バイトの CTime)である。変換結果は testdata/jww/ に置く。開発中は、公開されている JWW の図面(版数 220 で書かれたもの)でも変換を確かめ、同じ図面の DXF 版と図形と文字の位置が合うことを見た。

3.14 SXF → BDF 変換器(converter/sxf)の構造

converter/sxf は電子納品の CAD データ交換標準 SXF(Ver.2〜Ver.3.1)の図面を読む。仕様書は電子納品に関する要領・基準の Web サイト(cals-ed.go.jp)で公開されている「SXF Ver.3.1 仕様書・同解説」(フィーチャ仕様編、附属書 SFC 編、STEP AP202 サブセット編、共通既定義要素編)による。SXF には 2 つの書き方があり、納品に使う STEP AP202 の Part 21 ファイル(.p21。ZIP 圧縮したものが .p2z)と、CAD 間の受け渡しに使うフィーチャコメントのファイル(.sfc)である。どちらも読み、同じ converter/internal/cad で描く。属性ファイル(.saf)と、図面に添える画像(.tif など)は読まない。

テスト用の図面は test/sxf/gen.py が作る(npm run test:sxf:gen、標準ライブラリのみ)。1 つの図面を SFC(shapes.sfc)と、SCADEC の書き方をまねた P21(shapes.p21)と、それを ZIP にした P2Z(shapes.p2z)で書き、既定義の線種・色・線幅とユーザ定義のもの、各種の図形、9 つの配置基点・回転・スラント・字間・縦書きの文字、複合図形(回転と尺度、入れ子、測地座標系)、寸法・引出し線・バルーン、塗りつぶし・穴のあるハッチング・Area_control、非表示のレイヤ、背景色の属性を持つ。Go のテストは SFC と P21 が同じ cad.Drawing になることを確かめる。変換結果は testdata/sxf/ に置く。開発中は、国土交通省の CAD 製図基準に載っている図面作成例(SFC と P21 の両方がある縦断図、標準横断図、平面図、中間対傾構図)でも変換を確かめ、SFC と P21 で文字と線が同じ位置に描かれることを見た。

3.15 TIFF → BDF 変換器(converter/tiff)の構造

converter/tiff は TIFF のページごとに fixed View のページを作り、解像度から決まる大きさの画像 1 枚で描く。多ページの TIFF の多くはスキャナの出力と FAX の受信ファイルで、中身は画像だけ(テキストはない)。読み取りは converter/internal/tiff に自前で書いた。golang.org/x/image/tiff は先頭の IFD しか読まず、JPEG 圧縮も YCbCr も読まない。FAX ソフトの既定である G3 の 2 次元符号と詰め物ビットも読めず、BlackIsZero の CCITT データは白黒を反転して読む。Office 文書と Visio 図面の中の TIFF の画像も、同じ読み取りで先頭のページを読む(tiff.Picture。Orientation タグは掛けず、解像度の上限も掛けない)。

テスト用のファイルは test/tiff/gen.sh が ImageMagick と libtiff のツールで作る(npm run test:tiff:gen)。converter/internal/tiff/testdata には、同じ絵を格納方法を変えて各ページに入れたファイルと、ImageMagick が libtiff で読んだ参照 PNG を置く。可逆のページはすべて参照と画素単位で一致する。converter/tiff/testdata にはスキャン、FAX、8 通りの向きのファイルを置き、変換結果を testdata/tiff/ に置く。向きは Go のテストでも確かめる。保存された画像をページの行列で紙面に写し、1 ページ目と比べる。

3.16 HTML・Markdown → BDF 変換器(converter/html、converter/markdown)の構造

ブラウザのリーダー表示と同じく、ページのデザイン(作者の CSS)は捨てて内容だけを固定のスタイルシートで組む。一般の CSS を解釈しないので、必要なのは HTML の要素を段落・表・画像に対応づける読み手と、それを組むレイアウトエンジンだけになる。BDF は再レイアウトしないので、出力は決まった幅(既定は本文の 36 字分、432 pt)の scroll View で、-param views=pages で A4 のページ、both で両方を作る。

テスト用の文書は converter/markdown/testdata/basic.md、SVG の converter/markdown/testdata/svg.md(ページ幅の svg 要素、SVG ファイル、スプライトの symbol)、数式の math.md と converter/html/testdata/article.html・math.html(画像は test/markdown/gen_images.py が標準ライブラリだけで書く PNG)で、変換結果は testdata/markdown/・testdata/html/ に置く。Word のテスト用フォントの字幅で組み、フォントは名前で参照し、ブラウザにあるフォントで描く。

3.17 Illustrator → BDF 変換器(converter/ai)

converter/ai は Illustrator 9 以降の .ai を読む。この .ai は PDF に Illustrator 独自のデータ(ページの /PieceInfo /Illustrator の下の非公開ストリーム。ネイティブ形式そのもので、新しい版は Zstandard で圧縮する)を添えたもので、「PDF 互換ファイルを作成」(既定でオン)で保存すると PDF 部分のページがアートボードになる。変換は PDF 部分を converter/pdf で描き、独自データは読まない。

テスト用の .ai は test/ai/gen.py が Illustrator の書き方をまねた PDF を直接書いて作り(npm run test:ai:gen、標準ライブラリのみ)、変換結果は testdata/ai/ に置く。開発中は Illustrator CC 2015 と 2024 で保存された実ファイルで、非表示のレイヤーが消えることと、描画が Ghostscript のトリムボックスでの描画(-dUseTrimBox)と合うことを確かめた。

3.18 Photoshop → BDF 変換器(converter/psd)

converter/psd は Photoshop の文書(.psd と、大きな文書の形式 .psb)を読み、Photoshop が合成した画像をページの画像にする。レイヤーの構造は BDF の命令に写さない。見た目は合成済みの画像で決まっており、テキストやベクトルのレイヤーも Photoshop が描いたピクセルとして持っているからである。

テスト用の文書は test/psd/gen.py が Photoshop の書き方で直接書いて作る(npm run test:psd:gen、標準ライブラリのみ)。合成画像はスクリプトの中の小さな合成器で求め、Go のテストは合成画像が無いことにしたときの変換器の合成をそれと比べる。開発中は Photoshop などで保存された実ファイル(CMYK、グループ、クリッピングマスク、スマートオブジェクト、82 レイヤーのスプライトシートなど)で、変換器の合成が Photoshop の合成画像と各サンプル 2/255 以内で一致することを確かめた。

3.19 画像 → BDF 変換器(converter/image)の構造

converter/image は、ブラウザが画像要素でそのまま表示できる画像(PNG・APNG、JPEG、GIF、WebP、AVIF、BMP、ICO、SVG)を、画像の大きさのページ 1 枚の文書にする。画像はパススルーで、デコードも再エンコードもせずに Image Part に格納し(-images convert でも変換しない)、ページは IMAGE 命令 1 つでそれを描く。ブラウザはファイルを開いたときと同じデコーダで描くので、見た目はブラウザでファイルを表示したときと同じになる。変換器が読むのは大きさとメタデータだけで、メタデータはほかの形式の文書のプロパティと同じく manifest の Dublin Core にする(spec §4.3)。こうすると、画像もほかの文書と同じ形(View・ページ・meta・図の代替テキスト)で扱え、ビューア・検索・一覧の側に画像専用の経路を作らずに済む。

SVG の描画(@bdfkit/render): createImageBitmap は SVG の Blob をデコードしない(Chrome で Worker でもページでも失敗する。仕様上、Blob からはビットマップ画像だけ)。画像要素で読み込んだ SVG を Canvas に drawImage すればビットマップにできるが、Worker には画像要素がない。そこで ResourceCache は SVG を VectorImage として持ち、描くときに現在の変換から 1 画素が覆うデバイス画素を求めて、その大きさのラスタを要求する。ラスタはページ(BdfWorkerClient)が画像要素と OffscreenCanvas で描いて ImageBitmap を Worker に転送する。描画は同期なので、1 回目の描画で足りなかった大きさを集め、ページに描かせてから描き直す(ラスタがそろっていれば 1 回)。ラスタは必要な大きさの 1.25 倍までは使い回し、画像ごとに最近使った 4 つを残し、4096×4096 画素を上限にする。拡大してもぼけず、ズームを変えるたびに SVG をブラウザが描き直す。SVG は画像要素の中で描かれるので、スクリプトは動かず、外部の資源も読み込まれない(ブラウザで画像として開いたときと同じ)。これで draw.io や PowerPoint の文書に含まれる SVG の画像も描けるようになった(以前は createImageBitmap の失敗でそのページの描画が失敗していた)。

createImageBitmap(img, {resizeWidth, resizeHeight}) は大きさを持たない SVG(viewBox だけ)を描かないので、使わない。SVG をパスに変換して命令にする方法もあるが、SVG の描画系(CSS、テキスト、フィルタ、マスク)をもう 1 つ実装することになり、ブラウザが描けるものをそのまま渡すという狙いに反する。

テスト用の画像(converter/image/testdata/)は test/image/gen.sh(ImageMagick、exiftool、cwebp、avifenc)で作り、変換結果を testdata/image/ に置く。EXIF の向きや SVG の描画は、各変換器・レンダラの機能テストで確かめる。

3.20 CGM → BDF 変換器(converter/cgm)の構造

converter/cgm は ISO/IEC 8632 の Computer Graphics Metafile(CGM、バージョン 1〜4)を読む。CAD のプロッタ出力や、技術図(S1000D・ATA の図、WebCGM)の交換に使われる形式で、バイナリ符号化(Part 3)とクリアテキスト符号化(Part 4)の両方を読み、gzip で圧縮したもの(.cgz)も読む。文字符号化(Part 2)は 1999 年版で廃止されたので読まない。描くのは DXF・JWW・SXF と同じ converter/internal/cad である。

テスト用のメタファイルは test/cgm/gen.py が作る(npm run test:cgm:gen、標準ライブラリのみ)。要素を型付きのパラメータの列として 1 度書き、バイナリとクリアテキストに書き出す。shapes.cgm(とそのクリアテキスト shapes-text.cgm)は A3 の CAD 風の図面で、線種・線幅・端点、5 種のマーカー、各内部様式の塗り、穴と見えない辺のある POLYGON SET、各種の円弧と曲線、穴のある閉じた図形、9 つの揃えの文字と、回転・拡幅・字間・縦書き・箱に収める文字、APPEND TEXT、JIS X 0208 の和文、セル配列、クリップ、セグメントの複写を持つ。Go のテストは両者が同じ cad.Drawing になることを確かめる。illustration.cgm は WebCGM 風の図(実数の VDC で y が下向き、abstract、直接色、UTF-8、非表示のアプリケーション構造、タイル、2 つのピクチャ)、sjis.cgm は宣言のない Shift_JIS の文字を持つ A4 の図面である。変換結果は testdata/cgm/ に置く。符号化の細部(SCALING MODE の縮尺、タイル配列のパラメータ、SDR の形、セル配列の行の揃え)は、公開されている実装(jcgm)の読み方とも照らし合わせた。

3.21 Gerber・Excellon → BDF 変換器(converter/gerber)の構造

converter/gerber はプリント基板の製造データを読む。Gerber は Ucamco の「The Gerber Layer Format Specification」(拡張 Gerber=RS-274X と X2 属性)による。旧版の非推奨の命令(G54、G70/G71、G90/G91、G74、%IP・%IR・%MI・%OF・%SF・%AS、末尾のゼロの省略、増分座標)も読む。アパーチャの定義が別ファイルにある RS-274-D は描けない。穴あけデータは Excellon で、KiCad・Altium・Eagle などが書く方言(数値の書式、ヘッダーのコメントの属性、長穴とルーティング)を読む。1 枚の基板は層ごとのファイルと穴あけファイルの組で、製造業者には ZIP で渡すのが普通なので、ZIP に入ったファイルの組を 1 つの基板として変換する。1 つのファイルだけでも変換でき、その層の View になる。

テスト用のデータは test/gerber/gen.py が作る(npm run test:gerber:gen、標準ライブラリのみ)。board.zip は KiCad 7 の出力をまねた 2 層基板(X2 属性の Gerber 8 枚、PTH と NPTH のドリルファイル、ジョブファイル)で、角の丸い外形と切り抜き、SOIC とチップ部品の角丸パッド(KiCad の RoundRect マクロ)、ピンヘッダーと長穴のパッド、取り付け穴、円弧の配線、カットインで穴をあけた銅箔、裏面のベタとクリアランス(クリアの極性)とサーマルのマクロ、表と裏のシルクを持つ。features.gbr は仕様の要素(標準アパーチャと穴、全プリミティブと式、G74・G75 の円弧、全円、点、大きさ 0 のアパーチャ、領域とカットイン、極性、SR、AB と LM・LR・LS、G54)を 1 ファイルに並べたもの。変換結果は testdata/gerber/ に置く。

3.22 HP-GL/2 → BDF 変換器(converter/hpgl)

converter/hpgl は HP のプロッタとプリンタのプロットファイル(.plt など)を読む。仕様は「The HP-GL/2 and HP RTL Reference Guide」(Hewlett-Packard、第 2 版、1996)による。HP-GL/2 と、それ以前のペンプロッタやカッティングプロッタの HP-GL(HP-GL/2 で廃止された目盛りの XT・YT・TL も)を読み、大判プロッタや PCL プリンタに送るファイルに付く PJL のジョブと PCL・HP RTL のエスケープシーケンスもたどって、HP RTL のラスター画像も描く。描くのは他の CAD 変換器と同じ converter/internal/cad で、ラスター画像と RF のパターン塗りには CGM の変換器(§3.20)と同じ cad.Image と cad.Fill.Pattern を使い、線の留め継ぎ限度(cad.Pen.Miter)を足した。

テスト用のプロットは test/hpgl/gen.py が HP のリファレンスどおりに書く(npm run test:hpgl:gen、標準ライブラリのみ)。shapes.plt は A3 のプロットサイズにほぼすべての命令(パレット、線種、線端、円弧、粗い弦角の円、ベジェ曲線、PE、穴のある多角形、塗りの種類、パイチャート、網掛けのベクトル、ユーザ単位とウィンドウ、記号モード、ラベルの大きさ・方向・斜体・原点・進む向き・フォント、Shift_JIS と 16 ビット JIS の日本語)を描く。job.plt は大判プロッタのジョブ(PJL、ESC %-1B、RO 90 で縦長の A4 にしたページに置換デルタ行の 24 ビットの画像、2 ページ目は適応圧縮の白黒の画像だけ)、hpgl1.plt はペンプロッタの HP-GL(装置制御、P1・P2、ユーザ単位、SI と DI のラベル、目盛り)である。変換結果は testdata/hpgl/ に置く。開発中は GhostPCL(gpcl6)で同じ HP-GL/2 を PCL のページに包んで描き、線・円・円弧・ハッチング・線種・多角形・回転・スケール・ウィンドウ・PE・ラスター画像(画素単位で一致)が重なることを確かめた(ラベルは上に書いた違いのほかは合う)。GNU plotutils の出力(graph -T hpgl、-T pcl、plotfont)でも変換を確かめた。

3.23 数式(internal/mathlayout、converter/internal/equation、formula)

Word・PowerPoint・Excel の Office Math(OMML)、HTML の MathML、Markdown と draw.io(math=1)の LaTeX を 1 つのエンジンで組む。3 つの記法を同じ木(presentation MathML の形: トークン、行、分数、根号、添字、上下の極限、表、囲み)に読み、TeX と MathML Core の組み方で、数式フォントの OpenType MATH 表の定数とグリフの異体字を使って組む。BDF に数式の命令はなく、組んだ結果は文字と線の描画になる。

テストは、TeX・MathML・OMML の読み手を線形表記で、組版を STIX Two Math のサブセット(converter/docx/testdata/fonts、MATH 表を保って fontTools で作ったもの、OFL)で確かめ、MATH 表とグリフの境界(TrueType と CFF)を fontTools の値と比べる。converter/docx/testdata/math.docx(test/docx/gen.py)、converter/pptx/testdata/math.pptx(test/pptx/gen.py)、converter/xlsx/testdata/math.xlsx(test/xlsx/gen.py)、converter/drawio/testdata/math.drawio、converter/markdown/testdata/math.md、converter/html/testdata/math.html の変換結果を testdata/ に置き、ブラウザのテストで数式の検索を確かめる。

3.24 EPUB → BDF 変換器(converter/epub)の構造

EPUB は ZIP に入った XHTML と CSS と画像なので、リフロー型の本は HTML の変換器(§3.16)と同じリーダー表示で組める。HTML と違うのは 4 点。本は複数の文書(spine)からなる。読み手が求めるのは Web ページの帯ではなく本のページ。日本語の本の多くは縦書き。マンガや写真集は文字を組まない固定レイアウトで作られている。

テスト用の本は test/epub/gen.py(npm run test:epub:gen、標準ライブラリのみ)が書く 3 冊。basic.epub(英語。表紙、spine の中の目次、見出し・リスト・引用・図・表・コード・脚注、章をまたぐリンク、空要素のページ位置、CSS のクラスで隠す段落と中央寄せ、SVG ファイルと、隠したスプライトのグラデーションとシンボルを使うインラインの SVG、spine の外の注)、vertical.epub(電書協の作り方の縦書きの本。@import した CSS の vrtl / hltr、縦中横、傍点、ルビ、外字の画像)、fixed.epub(右綴じの固定レイアウトのマンガ。SVG の文書の表紙、画像のページ、インラインの SVG と SVG ファイルのページ)。変換結果は testdata/epub/ に置く。和文は Word のテスト用フォントのサブセットにある漢字だけで書いた。開発中は、市販の技術書(日本語と英語、数百ページ)が変換でき、ページの内容が元の本どおりであることも確かめた(1 冊 0.2 秒ほど)。

3.25 サーバー側のサムネイルと検索用テキスト(imagebdf、thumbnail、SearchText)

文書の一覧にはサムネイルが、サーバー側の全文検索には文書のテキストが要る。どちらも BDF を作るのと同時に作れば、元のファイルを読み直さずに済む。パスワード付きの入力なら、平文を持っているのは変換の間だけでもある(§3.7)。

描き方: ヘッドレス Chromium で @bdfkit/render を動かせば、ビューアと 1 画素も違わない画像が得られる(手動描画用の test/render.mjs がそうしている)。しかしそれにはサーバーに Chromium(数百 MB)を入れ、プロセスを起動・維持・隔離しなければならない。LibreOffice をやめる理由(README の Why BDF)と同じ重さを、サムネイルのために背負い直すことになる。そこで純 Go のラスタライザ imagebdf を書いた。変換器と同じく cgo も外部プログラムも使わず、変換と同じプロセスで、変換した *bdf.Document をそのまま描く。§2 の「ビューアで Go/wasm は使わない」は変わらない。ビューアは TS のままで、imagebdf はサーバーで使う第 2 の描き手である(デモサイトは、サーバーが作るものを見せるためにこれも wasm で動かす。後述)。

命令の解釈は TS の CanvasRenderer と同じ状態機械にした。SAVE・RESTORE のスタック、USE の暗黙の save/restore、トップレベルの Object(ページのレイヤー、シートのタイル)は変換とクリップ以外を初期状態から始めること、グループの一時キャンバスは変換と塗り・線・線幅・フォントだけを引き継ぐこと、ソフトマスク(輝度・アルファ・転送関数)、影の大きさを設定時の変換の拡大率で決めること(spec §7.2)、グループを囲む状態の影をグループ全体の影として合成のときに掛けること(Canvas が一時キャンバスを drawImage するときの影。draw.io の図形はこれで 1 つの影を落とす。影が届く分だけ、キャンバスの外にあるグループの中身も描く)。どれも TS の実装を写した。

確かめ方: Go のテストは、各描画命令の画素・寸法・クリップ・色・透明度、フォント読み込み、資源制限と不正入力を直接検証する。キャッシュの有無と並列描画は、現在の実装の各経路が同じ結果になることを確かめる。ブラウザ側は描画の成立、検索、テキスト選択、文書構造を機能テストで検証する。

サムネイルのレイアウト(thumbnail)は文書の種類で決める。種類は変換器が書く Meta.Source(既存の BDF からでも分かる)と View の種類で見る。

種類 レイアウト
Word、HTML、Markdown 1 ページ目(scroll View は先頭)の左上から、ページの幅の正方形(Crop)
音声(MP3、M4A、AAC、FLAC、Ogg、WAV、AIFF) カードの上端の正方形: ページの幅いっぱいのカバーアート(なければプレースホルダー)(Crop)
Excel、CSV、Parquet 最初のシートの A1 から、サムネイルの大きさを 72 dpi(1 単位が 1 画素)で描く正方形。96〜480 単位で、シートより大きくはしない。既定の行の高さ(15 単位)なら 64 画素で 6 行、128 画素で 8 行、256 画素で 17 行、512 画素以上で 32 行ほど。枠線はビューアと同じく描く
PDF、TIFF 縦長のページは文書として Crop、横長のページはスライドとして全体(Fit)
PowerPoint、Visio、draw.io、CAD、プリント基板、Illustrator、Photoshop、画像、EPUB 1 ページ目の全体を、長辺が指定の大きさになるように(Fit)。EPUB は 1 ページ目が表紙

サムネイルが見せるのは最初の View の 1 ページ目だけなので、サムネイルのためだけに変換するなら 1 ページ目だけを変換すればよい(converter.Options{Pages: converter.PageList(1)})。bdf thumbnail に BDF でない入力を渡したとき、デモサイトのサムネイルのページ、light-server はそうしている。全形式のテストデータと手元の実データで、全体を変換したときとサムネイルが画素まで同じになることを確かめた。545 ページの PDF で 17 秒が 30 ミリ秒になる。

-thumbnail-mode crop|fit(Options.Mode)で指定もできる。シートの範囲を初めはシートの短い辺(480 単位まで)にしていたが、256 画素のサムネイルでは 1 行が 8 画素ほどになり字が潰れた。行や列の数ではなく解像度で決めれば、小さいサムネイルほど狭い範囲を、同じ大きさの字で見せられる。下限は 64 画素でも表に見えるだけの行と列(既定の列幅で 1 列半)を残すため、上限は大きいサムネイルでも A1 の角を見せるためである。解像度は -thumbnail-sheet-dpi(Options.SheetDPI)で変えられる。Fit の出力は正方形にしない(縦横比を保つ)。並べるときは CSS の object-fit: contain で収めればよい。出力は PNG、JPEG、WebP(imgconv の純 Go のエンコーダ)。

検索用テキスト(Document.SearchText)はテキスト索引 Part(§4、spec §7.9)から作る。ビューアの検索と同じものなので、変換器が MARK で付けた区切り(行・段落・セル)がそのまま使える。索引のない View は Object から取り出す。ページ番号(1 始まり)は検索のヒットから #page=N で開くのに使える。シートはシート全体で 1 つにする。Word・Markdown・EPUB は同じ本文を page View と scroll View の両方に組むことがあるので、flow View がある文書では scroll View を除く。正規化(NFKC、かな)は検索エンジンに任せ、テキストはそのまま出す。

パスワード付きの入力: サムネイルもテキストも暗号化されない。サムネイルは 1 ページ目の中身を見せるので、テキストと同じ扱いにした。bdf generate は、暗号化して書き出す文書(既定ではパスワード付きの入力のもの)について、-allow-plaintext がなければどちらも書かない。bdf thumbnail・text・render も、暗号化された BDF は $BDF_PASSWORD と -allow-plaintext の両方がなければ描かない。ライブラリの API(thumbnail.Make、SearchText)は復号した文書を受け取るだけなので、作るかどうかは呼び出し側が Result.Protected や Document.Lock を見て決める。

wasm の引数: cmd/bdfwasm のメソッドは、引数が Uint8Array でないときや、引数を読む間にパニックしたときに、Promise を reject する(以前は Go のプログラムが終了し、その後の呼び出しがすべて失敗していた)。

デモサイト: cmd/bdfwasm を -tags previewonly でビルドした 5 つ目のモジュール(bdf-preview.wasm、約 9.5 MB、gzip 2.8 MB)は変換器を持たず、imagebdf・thumbnail・SearchText で単一ファイル形式の BDF のサムネイルとテキストを作る。ビューアの「Thumbnail and text」を開くと、表示中の文書(変換したもの、開いた BDF、?src= の単一ファイル)を渡し、そのとき初めてモジュールを読み込む。大きさ(64・128・256・512)、レイアウト(auto・crop・fit)、形式を選べる。ビューアの TS の描画で作らないのは、サーバーが作るのと同じものを見せるためである。形式は PNG と JPEG だけにした。WebP のエンコーダを入れるとモジュールが gzip で約 1 MB 増えるので、ほかのモジュールと同じく bdf_noconv でビルドしている。ブラウザにはシステムのフォントがないので、名前で参照するフォント(HTML・Markdown・EPUB)はサイトのフォントのディレクトリ(imagebdf.Options.FontFS)から探す。パスワード付きの入力から変換した文書と暗号化した BDF のものも作る(パスワードを入れて開いた人が自分で頼むのが -allow-plaintext にあたる)が、暗号化されないことをパネルに書く。暗号化した BDF にはビューアが開いたときのパスワードを渡す。

PDF の保存とフォームのページ: 同じモジュールに pdf(vector/pdfbdf)を足し、デモサイトのビューアは「Download PDF」で表示中の文書を PDF にする(約 1 MB、gzip 0.26 MB の増加)。押したときに初めてモジュールを読み込む。このモジュールだけは WOFF2 の伸張を入れてビルドする(タグ bdf_woff2dec。Brotli の伸張だけで約 1.8 MB、gzip 0.4 MB)。bdf コマンドが作った文書のフォントは WOFF2 で、これがないと PDF とサムネイルから文字が抜けるためである。ブラウザの中で変換した文書のフォントは圧縮していないので、ほかのモジュールには要らない。暗号化した BDF には PDF の保存を出さない(平文の PDF になるため)。

フォームのページ(site/form)は、帳票のテンプレートを試すためのもので、6 つ目のモジュール(-tags formonly、bdf-form.wasm、約 26 MB、gzip 7.0 MB)を使う。Word・Excel・PowerPoint・PDF の変換器と form・pdfbdf だけを入れ、fields(タグやフィールドの一覧と JSON Schema、形式、テンプレートが持つサンプル)・fill・pdf を出す。PDF フォームと pdfme の土台の PDF を読むために PDF の変換器が入り、それまでの約 18 MB(gzip 5.1 MB)から増えた。Office 系のモジュールに足さなかったのは、ビューアの利用者の読み込みを増やさないためと、フォームのページだけを開く人が 30 MB のモジュールを読まずに済むようにするためである。ページは、スキーマから入力欄を組み立てる(オブジェクトは枠、値だけの項目の配列は表、入れ子の配列は項目ごとの枠、真偽値はチェックボックス、日付は日付の入力、画像はファイルの選択、enum は選択肢。default があれば最初の値にし、maxLength は入力の上限にする)。値の型が決まっていない欄(["string", "number"])は、JSON の数値の書き方なら数値として渡す(Excel の数式がその値で計算できるように。007 は文字列のまま)。入力が止まって 0.25 秒後に穴埋めし直し、描いてあるページは新しい絵ができるまで残す(ちらつかない)。サンプルは 3 つの形態 × 3 つの形式に、PDF フォーム 1 つと pdfme のテンプレート 2 つを足したもので、テストのテンプレートとデータをそのまま使う。PDF は、通常のもの、タグ付き、PDF/A(2b・2u・2a)から選んで保存できる。

3.26 Parquet → BDF 変換器(converter/parquet)の構造

Parquet は分析用のデータを列ごとに圧縮して持つ形式で、pandas・Polars・DuckDB・Spark が書き、Hugging Face のデータセットもこの形で配られる。中身は 1 つの表なので、CSV(§3.10)と同じく xlsx.Grid にして sheet View を 1 枚作る。CSV と違って推定するものはない。型はファイルに書いてあり、値をどう文字にするかを決めればよい。

Parquet 以外の列指向の形式も調べた(2026-09)。Arrow IPC(Feather)、ORC、Avro は読む側の仕様がそろっている。Lance はデータセットがディレクトリで、ファイル形式(2.0、2.1、2.2、不安定な 2.3)が数週間ごとに変わり、2.0 のエンコーディングは文書がなく Rust の実装が仕様になっている。Vortex は 1.0 前で、ファイルの互換は新しい版が古いファイルを読むことしか保証されず、30 余りのエンコーディングに言語によらない仕様がない。Nimble は安定性を保証しておらず、仕様もなく、Meta の中の圧縮で書いたファイルは外では読めない。いずれも見送った。

テスト用のファイルは test/parquet/gen.py(npm run test:parquet:gen。pyarrow 25.0.1 と DuckDB 1.5.5)が書く 9 つ。basic(pyarrow の既定の出力: Snappy、辞書、v1。英語と日本語、改行を含む値、null、decimal、日付、ミリ秒のタイムスタンプ、リスト、構造体)、types(Zstandard。整数の端の値、float16、NaN と無限大、INT32・INT64・固定長の decimal、UUID、JSON、全単位の時刻とタイムスタンプ、辞書の文字列、null の列)、nested(空・null・null を含むリスト、リストのリスト、構造体のリスト、マップ、固定長のリスト)、encodings(v2 のデータページ、gzip。各エンコーディングの列と同じ値の PLAIN の列)、codecs(同じ値を圧縮ごとの列に)、legacy(INT96、準拠しない要素名のリスト)、duckdb(分解保存された Variant、UUID、INTERVAL、GEOMETRY、PLAIN_DICTIONARY)、variant(オブジェクトとリストに分解保存された Variant)、geo(GeoParquet の WKB)。今の書き手が作らない構造(2 段のリスト、MAP_KEY_VALUE、BIT_PACKED のレベル、Hadoop の LZ4)は、テストの中の小さな書き出し器で作る。開発中は、これらと Apache の parquet-testing(77 ファイル、約 23 万セル)の値を pyarrow が読む値とセルごとに突き合わせ(pyarrow 自身が読めない列と、int64 に収まらない INT96 を除いてすべて一致)、Variant は parquet-testing の分解保存の 138 例がすべて一致することも確かめた。変換結果は testdata/parquet/ に置く(CI が変換し直して比べる)。フォントは CSV と同じく PowerPoint のテストのものだけを使う。

3.27 楽譜と演奏(converter/mml、converter/midi、converter/musicxml、converter/internal/music)

MML、MIDI、MusicXML を五線譜のページにし、View にその音楽を持たせてビューアで演奏する。BDF に音楽の命令はなく、楽譜は変換器が組んだ線・パス・文字の描画になる。音楽は描画とは別の Part(seq、Standard MIDI File)と、演奏の時刻とページの上の位置を結ぶ cue の索引で、View.play に置く(spec §4.4)。

テストは、記譜(Notate)の結果を音符の並びの文字列で、Build の cue(時刻の順、段の矩形の中、繰り返しの展開と分解能の変換)と SMF の書き出しを確かめ、読み手ごとに MML・MIDI・MusicXML の小さな入力(パブリックドメインの曲を手で書いたもの。converter/musicxml/testdata/gen.py)から Performance と Score を確かめる。converter/mml/testdata/frere.mml(PPMCK の 4 声の輪唱)、ode.mml(マビノギ)、converter/midi/testdata/twinkle.kar(歌詞)、converter/musicxml/testdata/minuet.musicxml(大譜表)、ode_to_joy.mxl の変換結果を testdata/music/ に置く。

3.28 フォントファイル → BDF 変換器(converter/font)の構造

フォントファイル(TrueType・OpenType、そのコレクション、WOFF・WOFF2)を、フォントビューアのように見せる。どの文字とグリフがあり、どの OpenType フィーチャーが何をするかが分かる文書にする。問題は BDF が Canvas 2D の上にあることだ。fillText はブラウザの既定のフィーチャー(liga、kern など)しか適用せず、文書が font-feature-settings を指定する手段がない。フィーチャーの働きはシェーピングした文ではなく、グリフの前後として描く。

3.29 KiCad → BDF 変換器(converter/kicad)の構造

converter/kicad は KiCad 6 以降の回路図(.kicad_sch)と基板(.kicad_pcb)を読む。どちらも S 式のテキストで、ファイルの version(日付)が 20211014 より前のもの(KiCad 5 の S 式の基板)と KiCad 5 以前の独自形式(.sch・.lib)は「KiCad 6 以降で開いて保存し直す」ように案内して断る。入力は回路図、基板、プロジェクトファイル(.kicad_pro。JSON で、隣の同名の回路図と基板を読む)、プロジェクトを入れた ZIP のどれでもよい。色は KiCad の既定のテーマ、文字は KiCad の描き方を再現する。

テストは、S 式、記法、シンボルの変換、ラベルの向き、ReadRef と FileMap(fstest.TestFS)の単体テストと、KiCad の出力から取った座標(KiCad のデモの文字と自作の小さな回路図)で文字の線の範囲を確かめるテスト、test/kicad/gen.py が書くテスト用のプロジェクト(2 回使う階層のシート、全種類のピンとラベル、ハッチ、表、テキストボックス、日本語、4 層の基板、独自の図枠)の変換、数値を極端な値に置き換えたファイルと自分を含むシートの変換、fuzz である。

3.30 保護モード: 区間ごとに封印して配る(segment、ログインした読者向け)

ログインした読者に本や PDF を読ませるサイトでは、文書をファイルとして配りたくない。§3.5 の暗号化をファイル全体にかけても、鍵を渡した時点で読者の手元に「いつでも開ける暗号文と鍵」が揃い、通信を記録した人も後で鍵が漏れれば全部開ける。そこで、サーバーは変換した BDF を手元に置いたまま、読者が開いたあたりの数ページ(既定で 10 ページ、区間)だけを、要求ごとに読者が作った使い捨ての鍵で封印して渡す(spec §3.6、§3.5 の ecdh スロット)。

守るもの

後で漏れたもの 記録された通信(封印された区間)
読者のパスワード、セッションの Cookie 開けない。新しい区間を頼めるだけで、権利と速さはサーバーが確かめる
サーバーの TLS の秘密鍵、TLS を終端する CDN・WAF のログ 開けない。区間を開ける鍵は要求ごとの鍵の合意からしか出てこない
ログアウト後・タブを閉じた後の端末 何も残っていない(Cache-Control: no-store、鍵は Worker のメモリの中だけ)
読者が閲覧中に抜き出した鍵 その区間 1 つにしか効かない

守らないもの: サーバーのディスク(本は平文で置く。変換は 1 回だけで、封印は要求のたびにする)、閲覧中の画面とメモリ(描けるものは写せる)、TLS を終端して応答を書き換えられる者(ビューアのスクリプト自体を差し替えられる)。オフラインの閲覧はしない(端末に鍵を残すことになり、前方秘匿性と両立しない)。

鍵: ブラウザの Worker が要求ごとに P-256 の鍵ペアを extractable: false で作り、公開鍵だけを送る。サーバーは区間ごとに自分の鍵ペアを作って ECDH で鍵を合意し、区間のコンテンツ鍵をラップして自分の秘密鍵を捨てる(bdf.NewECDHLock)。Worker は deriveKey で同じ鍵を導いて区間を開き、秘密鍵への参照を持たない(ECDH の結果は HKDF の基底鍵のまま WebCrypto の中にあり、JavaScript に出てこない)。ラチェットは使わない。要求ごとに両側が新しい鍵を作れば、どの応答も単独で前方秘匿になり、順序の食い違いや再送を考えなくてよい。サーバーの長期の鍵で状態を包むこともしない(その鍵が漏れれば過去が開く)。HPKE(RFC 9180)は WebCrypto にないので、共有秘密を JavaScript のバイト列として扱うことになるため採らなかった。

区間の中身(Reader.WriteSegment): 輪郭(すべての View とページの大きさ、メタデータ)と、区間のページのレイヤー、そのページが使う Part。格納バイト列(圧縮済み)をそのままコピーして封印するので、要求のたびの仕事は読み出しと封印だけで、圧縮し直さない。どのページがどの Part を使うかは、ページごとに一度だけ Object をデコードして調べ、Reader が覚えておく(並行に呼んでよい)。読者は持っている区間を要求に書き(have)、サーバーはそれらのページが使う Part を省く。フォントや共有の Object(PDF の Form XObject、スライドのマスター)を区間ごとに送り直さないためで、サーバーは読者ごとの状態を持たない(読者が嘘をついても、困るのは読者自身)。テキスト索引は View 全体の本文なので区間には入れない。本の中の検索は受け取ったページだけが対象になる。シートと演奏のある View は区間にしない。

通信の形(Go の segment.Handler と @bdfkit/core の SegmentLoader): POST、Content-Type: application/json(フォームと違い、他のサイトからは CORS の問い合わせなしには送れない)で、本体は次の形。

{ "key": "<base64: 読者の公開鍵、非圧縮の点 65 バイト>",
  "view": "pages",            // 省略時は最初の View
  "page": 37,                 // この要求で欲しいページ(0 始まり)
  "have": [ { "view": "pages", "from": 0, "to": 10 } ] }   // 持っている区間

応答はそのページを含む区間(ページ ⌊page/10⌋·10 から 10 ページ)の単一ファイル形式で、Cache-Control: no-store。ログインと権利はアプリケーションの Open(どの文書か、読んでよいか)と Allow(区間ごとの規則: 立ち読みは最初の区間だけ、読む速さの上限)が決め、エラーは *segment.StatusError の状態(未ログインの 401、速すぎる 429)、fs.ErrNotExist(404)、fs.ErrPermission(403)で返す。SegmentLoader は 1 つの区間を同時に 2 度頼まない(区間の大きさは最初の満ちた区間から知る)、頼まれたページの 3 ページ先を先読みする。レンダラの Worker は { kind: "segments", url } で開いた文書について、ページの描画・テキスト・連続表示の範囲を処理する前に要るページを取りに行くので、ビューアは他の文書と同じように使える。サーバーが拒んだ区間は Worker のエラーコード not-allowed・rate-limited になる。

費用(Apple M 系の 1 コア、WriteSegment と鍵の合意を合わせて): 実物の本 2 冊(EPUB から、439 ページと 362 ページ)で 1 区間 0.6 ms 前後(最大 1.1 ms)、ページごとの Part の表を作る最初の一巡が本全体で 30〜50 ms。大半は輪郭の JSON の圧縮で、鍵の合意は 50 µs、AES-GCM は 10〜15 µs。区間の大きさは中央値 25〜50 KB、最大 0.5 MB(大きな図のあるページ)。封印したものは読者ごとに違うので CDN の共有キャッシュは使えないが、本 1 冊が数 MB なら配信の負荷としては小さい。

サンプル: examples/secure-reader はこれを使ったログイン付きの PDF 閲覧サイトである(パスワードは PBKDF2 で持ち、セッションの Cookie は HttpOnly・SameSite=Strict でサーバーはそのハッシュだけを持つ、http.CrossOriginProtection、CSP、立ち読みと読む速さの上限)。

確かめたこと: Go のテスト(鍵の往復、別の鍵とパスワードを拒むこと、区間のページと Part、have で省かれること、シートを拒むこと、ハンドラの状態)、Go が封印した区間を TS が開いて合わせると元の文書のページと Part に一致するテスト(testdata/segments/、テスト専用の鍵で封印)、SegmentLoader が同じ区間を 1 度だけ頼み have を付けるテスト。ブラウザでは Chrome 153 と WebKit 26 でサンプルの本の 3 つ目の区間まで描けること、立ち読みの境界で 403、21 区間目で 429、ログアウト後に 401 になることを確かめた(Firefox は手元の Playwright で起動しないため未確認)。

3.31 音声ファイル → BDF 変換器(converter/audio)

音声ファイル(MP3、MP4 の AAC つまり M4A、生の ADTS の AAC、FLAC、Ogg Vorbis・Opus、WAV、AIFF)を 1 ページの「カード」の文書にする。音声ファイルにページはないが、ファイル一覧・サムネイル・検索に要るものはある。カバーアートとタグだ。ページの上端にカバーをページの幅いっぱいに置き(なければ音符を描いたプレースホルダー)、その下に題名・アーティスト・アルバム、ほかの項目の表、歌詞、章を組む。View は play(spec §4.4)に音声ファイルそのものを持ち、ビューアはブラウザの <audio> で鳴らす。歌詞が歌われる時刻を持っていれば cue にして、歌っている行をカードの上で示す。

3.32 InDesign → BDF 変換器(converter/idml)の構造

converter/idml は InDesign が書き出す IDML(InDesign Markup Language。XML を zip に詰めたパッケージで、Adobe が仕様を公開している)を読む。ネイティブの .indd はバイナリで仕様がなく、版ごとに構造が変わるので読まない(IDML に書き出してもらう)。判別は zip の mimetype(application/vnd.adobe.indesign-idml-package)か designmap.xml の有無。パッケージの読み取りは Office と同じ ooxml.Package(部品名の大文字小文字を区別しない zip)で、XML は ooxml.Node の木にする。InDesign が XML に書けない文字(ページ番号マーカー U+0018 など)は処理命令 <?ACE 18?> で書くので、xmltree はこれを文字として読む。

テスト用の文書は test/idml/gen.py(標準ライブラリだけ)が書く(npm run test:idml:gen)。basic.idml は 1 ページのスプレッドと見開きのスプレッド、マスター(罫線とページ番号)、各種のスウォッチと線種、角丸・グループ・非表示のレイヤーとアイテム、埋め込みとリンク(隣の link.png)の画像、スタイルの連鎖・揃え・タブ・箇条書き・番号・文字属性を持つストーリーが 2 段組みの枠から次のページの枠へ流れるもの。vertical.idml は右綴じの縦組みのストーリーが 2 ページにわたるもの。Go のテストはページとレイヤー、ストーリーがページをまたいで流れること、ページ番号、リンク画像を Dir と Files から読むこと、縦組み、ページの選択と判別を確かめ、変換結果は testdata/idml/ に置く(フォントは PowerPoint と同じ M PLUS 1p のサブセット)。InDesign 自身が書き出した IDML は SimpleIDML(BSD 3 条項)の回帰テストのもの 7 件を converter/idml/testdata/simpleidml/ に置き(README に出典)、CS5.5・CC 2014・InDesign 2020 の書き出しで、レイヤーの並び(設計マップは前面から)、リンク画像の Contents が XMP であること、BasedOn の種類なしの表記、連結フレーム、回転したフレーム、インセットがフレームより大きいフレーム(何も表示しない)を確かめた。配置画像のファイルは入れていない(リンク切れの警告の確認に使う)。テキストの溢れ(オーバーセット)は代替フォントの幅に依るので、テストでは数えない。

3.33 適合テスト(conformance、他の読み手のために)

BDF v1 の読み手が Go 以外にも書かれる(C の libbdf)ので、「この入力をどう扱うべきか」を機械可読で配る。conformance パッケージが suite を組み、bdf conformance -testdata testdata <dir> が書き出す。

最初の suite が見つけたのは参照実装の癖で、ファイル末尾の長さ 0 の Part を ReadAt が io.EOF で読み損なっていた(直した)。

3.34 帳票テンプレートと穴埋め(form、form/tag、converter/xlsx・docx・pptx の Fill、converter/pdfform、converter/pdfme)

請求書・納品書・証明書のような帳票を、テンプレートとデータから作る。テンプレートは利用者が Excel・Word・PowerPoint で作ったファイルで、文字のところにタグ({{name}}、{{#items}}…{{/items}})を書く。出力は BDF だけで、Office のファイルは書かない。3 つの形式は帳票の形が違う。Excel は印刷のページで、行が増えればページが増える。Word は流れるページで、長くなった分は次のページへ送られる。PowerPoint は固定のページで、図形は動かない。タグとデータは共通にした。

どこで穴埋めするか: 検討した案は 3 つ。(1) ブックの XML を書き換えて穴埋め済みの .xlsx を作り、それを変換する。再レイアウトがただで手に入り、Excel で開いて確かめられるが、出力に Office のファイルが混ざり、共有文字列や mc:AlternateContent のような XML の層の罠をすべて踏む。(2) テンプレートを静的な Object と「箱」の表に事前に落とし、穴埋めは BDF の上で行う。静的な部分が文書をまたいで共有されるが、行が増える帳票では結局組版が要り、Word と PowerPoint では組版エンジンを丸ごと持ち込むことになる。(3) 変換器が XML を読み終えてレイアウトする直前に持っている内部の形(Excel なら行とセル)を書き換え、既存のレイアウトをそのまま走らせる。採ったのは (3)。行の高さの自動調整、ページ分割、印刷タイトルの繰り返し、条件付き書式は今あるコードのままで、同じ内容の Object は内容アドレスで共有される。

タグ(form/tag): Mustache 風の値・区間・逆の区間・画像と、表示形式(| fmt:"#,##0"。書式は Excel の表示形式コードで、Word と PowerPoint のテンプレートでも同じもの。xlsx.Formatter を form が渡す)。式は持たない。テンプレートは信頼できない入力で、名前を引いて書式を付けるだけなら実行するコードがない。合計・税・繰越とその端数処理はデータの側の仕事にした(端数の規則は帳票ごとに違い、テンプレートに持たせると際限がない)。日本語入力と Word が置き換える全角の波括弧・記号・曲がった引用符も読む。データを引く Scope は、項目から外側へ名前を探す。項目のない枠の行は「空のスコープ」で、データの名前は外側にあっても見えず(外の name が空行に出ないように)、変数(_blank など)だけが見える。

Excel での区間(template.go、template_expand.go、template_calc.go): 開始と終了のタグが同じセルにあれば文字の中の繰り返しか条件、別のセルにあれば行の区間になる。行の区間は 3 通り。

区間は木になる(行の範囲で入れ子。兄弟は同じ行を繰り返せない)。穴埋めはこの木をたどって新しい行の列を作り、同時に「テンプレートの行 → 穴埋め後の行」の対応を、区間の複製の木(secInst・itemInst)として残す。

行に追随するもの: 結合セル、条件付き書式の範囲、リンク、オートフィルター、印刷範囲と印刷タイトル、手動の改ページ、テーブルと図形のアンカーは、対応の木で写す。複製された行の中にあるものは複製ごとに 1 つ、外にあって複製された行を含む範囲は最初の複製から最後の複製まで(Excel で範囲の中に行を挿入したときと同じ)。条件付き書式の数式は範囲の先頭からの相対なので書き換えない。

数式の再計算: 変換器はふだん数式を計算せず保存された値を描くが、穴埋めすると値が変わる。テンプレートのときだけセルの数式を読み(共有数式は持ち主の式を行と列の差だけずらして使う)、穴埋めの後で計算し直す。評価は条件付き書式の評価器で、参照を読むところだけを差し替える(fenv.remap)。数式の参照はテンプレートの番地のままで、「その数式がいる複製から見た行」に写して読む: 同じ複製の中の行は同じ複製の行、数式のいない区間の行は最初の複製(範囲の終わりは最後の複製)。こうすると、明細行の =C4*D4 は自分の行を、ページの中の =SUM(D4:D13) は自分のページを、外の =SUM(E4:E4) は全部の明細を読む。項目が 0 で行が消えた範囲は空(合計は 0)、単独の参照は #REF!。計算の順は、数式が読むセルの数式を先に計算する深さ優先(再帰ではなくスタックで。数式の鎖はシートの長さになりうる)。評価器が知らない数式のセルは空にして警告する。

値の型: タグ 1 つだけのセルは値をそのまま受け取る(数値は数値、真偽値は真偽値)。日付は、YAML とブックのデータでは time.Time、JSON と CSV では文字列で来るので、文字列は「セルかタグの表示形式が日付のとき」だけ日付として読む("2026-10-13" という文字列を勝手に日付にしない)。表示形式のないセルに入った日付には、Excel がセルに日付を打ったときと同じ既定の書式を付ける。fmt 付きのタグは、セルに新しい表示形式のスタイルを足す(数値のまま残るので、数式から読める)。

コンパイル済みテンプレート(.bdft、spec §4.5): テンプレートを検査して配る形。BDF の文書そのもので、ページはサンプルデータで穴埋めしたもの、Part にテンプレートの本体(src)と、タグが引く名前の一覧(tpl、JSON)を持つ。既存の読み手は 2 つの Part を参照しないので、ビューア・サムネイル・検索はテンプレートをふつうの文書として扱える。幾何を事前に落とした独自の形にしなかったのは、変換器の内部の形に結び付いた形式を版ごとに保つより、元のファイルから読み直す方が安いため(帳票の大きさのブックなら読むのは数ミリ秒)。.bdft を穴埋めした結果は、元のブックを穴埋めした結果とバイト単位で同じになる。

信頼できないテンプレートへの備え: 穴埋め後のセルは合わせて 4,194,304 個まで、行はシートの行数まで(超えたらエラー)。行の区間の入れ子は 32 段まで。1 セルの文字は Excel と同じ 32,767 文字で切る。複製で増える結合セル・条件付き書式の範囲・リンクはそれぞれ 1,048,576 個まで。数式の順序づけで見るセルは 16,777,216 個まで(超えたらセルの順に計算する)。データが名前で指す画像はテンプレートに渡したファイルの中だけから読み(ネットワークは読まない)、32 MiB まで。

確かめ方: タグの字句解析と、4 つのセルとデータを与える穴埋めには fuzz ターゲットがある(FuzzParse、形式ごとの FuzzFill)。Word と PowerPoint は、テストの中で小さな文書を組み立てて穴埋め後の文字を突き合わせ、3 つの形態のテンプレート(test/docx/gen.py・test/pptx/gen.py の forms)を Excel と同じデータで穴埋めし、ページ数と検索テキストを確かめる。区間・値の型・追随・数式は、数行のシートを XML で書いて穴埋め後のセルを突き合わせる表駆動のテストで、3 つの形態(1 ページの帳票、行が増える表、10 行の伝票)は openpyxl で作ったブック(test/xlsx/gen.py の forms)を穴埋めし、ページ数と検索テキストを確かめる。

Word と PowerPoint での穴埋め(converter/internal/wordproc/template.go、converter/pptx/template.go): この 2 つの変換器が「レイアウトする直前に持っている形」は、部品の XML を読んだ要素の木(ooxml.Node)そのものなので、穴埋めは木を複製しながらタグを置き換え、できた木を既存の読み手に渡す。ファイルは書かない。

QR コードとバーコード(image/barcode): 帳票は機械に読ませる記号を載せる(伝票、ラベル、請求書の支払い用のコード)。画像のタグに qr か barcode:種類 を付けると、値を符号化した記号を画像の場所に描く({{%url | qr:H}}、{{%jan | barcode:jan}}、余白は quiet:N)。

データの JSON Schema(form.Schema): テンプレートが求めるデータの形を JSON Schema(2020-12)で書き出す。入力フォームを自動で作る仕組み(デモサイトのフォームのページ、利用者のアプリの JSON Schema フォーム)と、データの検証のためである。タグは型を持たないので、スキーマはタグから推し量る。中に名前か {{.}} がある区間と、繰り返し方を書いた区間は配列、それ以外の区間は真偽値、数値の表示形式が付いた値は数値、日付の表示形式なら format: date の文字列、ほかの値は文字列か数値にする。そのために、{{.}} もフィールドの一覧に載せるようにした(名前 "."。区間の項目が値であることを示す)。プロパティはテンプレートの順に並べ、タグの情報は x-bdf に入れる。

区間の中の名前は項目とその外側の両方から探されるので、どちらのプロパティかはタグだけでは決まらない(伝票の見出しの {{customer}} は、ページの区間の中にあるがデータではその外にある)。外側でも同じ名前を使っていれば外側のもの、そうでなければ項目のものとする。サンプルデータがあれば、名前はサンプルにある場所に置き、値の型と、区間が配列かオブジェクトか真偽値かもサンプルで決める。コンパイル済みテンプレートはサンプルを持つので、コンパイルのときにスキーマを作って tpl に入れておく(spec §4.5)。フォームを作る側はテンプレートを読み直さなくてよい。

場所の決まったフィールドを埋めるテンプレート(converter/pdfform、converter/pdfme、converter/internal/formbox): タグを書いた Office のファイルのほかに、フィールドが場所を持っている形式もテンプレートにする。入力欄のある PDF(AcroForm)と、pdfme のテンプレート(デザイナーが保存する JSON)である。どちらもタグの代わりにフィールドの名前がデータの名前になり、form.Fill・form.Fields・form.Schema・.bdft はタグのテンプレートと同じに扱う(形式は中身で見分ける。%PDF- と、schemas か basePdf を持つ JSON のオブジェクト)。値を描く部分(フォントの選択と計測、行の折り返し、文字・画像・記号・印の描画)は formbox に置いて共有し、値を枠のどこに置くかは形式ごとの規則なので、それぞれの埋め手が決める。

まだないもの: 列方向の繰り返し、フォントをテンプレートに固定すること(組むマシンのフォントに依存する)、Word の図形が文字の量で押し合う可変レイアウト。

3.35 PDF への書き出し(vector/pdfbdf、bdf pdf)

帳票は最後に紙か PDF になる。BDF の文書を PDF に書き出す書き手を持つ。入力の変換器とは逆向きで、BDF の命令列(spec §7)を同じ順に PDF の内容ストリームへ写す。

対応: SAVE / RESTORE は q / Q、変換は cm、パスは PDF のパス演算子(楕円弧・arcTo・角丸は 3 次ベジェにする。Canvas のパスの規則はラスタライザと同じコードの写し)、クリップは W n、線と破線はそのまま。色のアルファと ALPHA は ExtGState の ca・CA、BLEND の 16 のブレンドモードは BM。線形・放射のグラデーションは軸・放射シェーディングのパターン(Canvas の 2 円の放射は PDF のタイプ 3 そのもの)、扇形は中心のまわりの三角形の網(タイプ 4)、画像のパターンはタイリングパターン。グループは透明グループのフォーム、ソフトマスクはグループを描くときの SMask。停止点のアルファが違うグラデーションは、アルファを輝度のソフトマスクにする。リンクはリンク注釈、文書情報は Info と XMP。

文字は文字のまま: 文書のフォント(WOFF2 は戻す)を、使っているグリフだけ残して CID フォント(Identity-H)として埋め込み、グリフから文字への対応(ToUnicode)を付けるので、PDF で検索とコピーができる。グリフ番号を保つ刈り込み(PruneGlyphs・PruneCFF)を使うので、コードはグリフ番号のまま(CID キーの CFF は CID)。名前で参照するフォントは書き出すマシンのフォントを探して埋め込む。埋め込みを許さないフォント(OS/2 fsType)と、PDF が持てないフォントは、グリフのアウトラインを描いて ActualText を付ける。文字の配置はラスタライザと同じ(シェーピングなし、advance に合わせて横に伸縮、太字と斜体の合成)。共通の部分は internal/textrun に出した。

すべてのフォームをページの座標系で定義する: ビューアは、フォームの中のパターンとソフトマスクの座標系の解釈が揃っていない。Ghostscript は仕様どおりフォームの座標系を基準にするが、Quartz(macOS のプレビュー)はフォームの中のパターン行列をページの座標系への行列として読み、別の座標系で設定したソフトマスクをページの矩形で切る。そこで、グループ・ソフトマスク・共有レイヤーのフォームはすべてページの既定の座標系で描き、フォームの中身の先頭に変換を持たせる。こうするとどちらの解釈でも同じ行列になる。小さな PDF を両方のビューアで描いて確かめた。

ビュー: ページのあるビュー(fixed・flow)のページを書く。ページのない文書は、scroll の帯を積んだ 1 ページ、sheet のタイルが届く範囲の 1 ページにする(14,400 pt を超えたら UserUnit)。複数のページが共有するレイヤー(スライドのマスター)は 1 度だけ書く。SkipRoles でレイヤーを役割ごとに省ける(印刷済みの用紙に重ねて刷る帳票)。

影は影だけを画像にする: PDF には影もぼかしもない。影を落とす図形(パス、文字、画像、グループ)を 1 つだけ、同じ状態(変換、線、フォント)でラスタライザ(raster/imagebdf)に描かせ、そのアルファをラスタライザと同じぼかし(箱形 3 回、internal/gauss)にかけて、影の色のソフトマスクつき画像として図形の下に置く。図形そのものはベクタのまま上に描く。影はページに平行な格子で作り(回転した図形でもぼかしは等方)、解像度は 300 dpi、1 つの影は 4,194,304 画素、文書全体で 2.56 億画素まで(超えたら解像度を下げ、それでも収まらなければ省いて警告)。グループの影は、グループを閉じるときに中身を描いて作る。

タグ付き PDF(Options.Tagged、-tagged): 文書の MARK(§7.8)を、PDF の構造木に写す。MARK をたどる状態機械は、検索用テキストを作るもの(packages/core/src/text.ts の Extraction)と同じ規則にした。葉の MARK(PARAGRAPH、HEADING、BOX、LIST の外の LIST_ITEM、TABLE の外の CELL)は次の文字が属する要素を予約し、構造の MARK(LIST、TABLE、FIGURE、END)はその場で要素を開閉する。

PDF/A(Options.PDFA、-pdfa): 第 2 部と第 3 部の、レベル b・u・a を書く(2b・2u・2a・3b・3u・3a)。レベル a はタグ付きを兼ねる。

書けないもの: FILTER、ブレンドモードでない Canvas の合成(destination-out など。通常の重ね描きにする)、グループの中の CLEAR_RECT、AVIF の画像。警告を出す。暗号化はまだ書かない。

信頼できない文書への備え: 再利用される Object は描かれる回数だけ書き出すので、書き直す命令は 4,194,304 個まで。開いているグループとソフトマスクは 64 段、保存する状態は 1,024 個まで。書き出しの中の panic はエラーにする。

確かめ方: テストは、書き出した PDF を自前の PDF 変換器で読み直し、元の文書と同じ絵になることをラスタライザで比べる(文字は読み直した文書の検索用テキストでも確かめる)。testdata の全文書は Ghostscript で描いて突き合わせ、ソフトマスクとパターンは Quartz でも確かめた。同じ文書からは同じバイト列ができる(amd64 と arm64 でも同じ)。FuzzWrite は任意の命令列を書き出す。

4. テキストの扱い

一番忠実度を左右する部分。3 段階を用意する。

方法 忠実度 サイズ 検索/選択 用途
FILL_TEXT + 埋め込み WOFF2 + advance 補正 高 小 可 標準
FILL_TEXT + システムフォント + advance 補正 中 最小 可 埋め込み不可のフォント、和文のフォールバック
アウトライン化(FILL_PATH_RUN + Path collection) 完全 大 不可(MARK で文字列を別途持てば可) ロゴ、特殊フォント、Type3

advance 補正は pdf.js が長年使っている手法で、フォント代替時の行崩れを実用上ほぼ消せる。文字単位の位置がずれても行の右端は揃う。

Canvas にはグリフ ID で描く API がないので、「サブセットフォントを埋め込んでも、文字列→グリフの対応はブラウザのシェーピングに任せる」ことになる。合字やコンテキスト依存の字形が原文と違うことはありうる。それが許容できない箇所だけアウトライン化する。

4.1 検索と選択

検索と選択は別の問題として扱い、検索はライブラリ(Worker)が提供する。

4.2 読み上げ(アクセシビリティ)

Canvas はアクセシビリティツリーに出ないので、支援技術が読むのはテキスト層だけになる。そこで層を単なる span の並びではなく、構造を持つ DOM にする。

5. 共有オブジェクトの効き方

内容アドレスなので、エンコーダが「同じものを同じバイト列で出す」だけで共有される。効く条件は決定的な出力:

さらに読み手側では USE 対象を (hash, scale) でビットマップキャッシュできるので、共有はサイズだけでなくスクロール時の描画コストも下げる。

5.1 デコードした画像の保持(ビューア)

レンダラ(@bdfkit/render の ResourceCache)は、Path2D・FontFace・ImageBitmap を文書ごとにキャッシュする。このうち ImageBitmap は桁違いに大きい。A4 のスキャンを 192dpi にしたページ 1 枚は、RGBA にデコードすると 14MB になる。スキャンの TIFF や PDF を読み進めると 100 ページで 1.4GB になり、タブが落ちる。そこで、デコードした画像だけは予算の中で持つ。

文書が決める大きさへの備え(ビューア): 画像はデコードする前にヘッダー(PNG、JPEG、GIF、WebP、BMP)から大きさを読み、maxImagePixels(既定 2^28 画素)を超えるものはデコードせず、何も描かない(ResourceCache.image() は undefined を返す)。大きな写真はそのまま格納してブラウザに描かせる方針(§3.19)なので、カメラの 1〜2 億画素の写真が入るよう、Go の 100 × 2^20 画素より大きくしてある。1 回の描画が押さえる画像は holdLimit(既定 4 GiB)までで、超える描画は失敗にする(読み込みの順で結果が変わらないように、画像を選んで落とすことはしない)。グループとソフトマスクの一時キャンバスは、描く先のキャンバスと影・フィルタの届く範囲に切り詰め、合成したら大きさを 0 にしてすぐに手放す。ページのキャンバスは 2^28 画素、1 辺 32,767 画素まで。USE の入れ子と繰り返し、Part の展開、manifest の数値、鍵スロットの反復回数は Go の読み手と同じ上限で(spec §3.2、§3.5、§4、§7.6)、超えたら BdfFormatError にする。分割形式の Part 名は URL に入るので、32 文字の hex であることを確かめてから取得する。

6. Excel シートの Tile 化

7. Word の「縦一枚」表示

再レイアウトはしないと決めた。理由は BDF が表示リストであること、そして再レイアウトを許すとフォーマットが「文書モデル」になって PDF との差別化(軽いデコーダ)が消えること。

代わりに 2 つの手段を用意し、Word の変換器(§3.9)は両方を作る。

  1. flow View の連続モード: body レイヤーを本文矩形で切り出して積む。ページ境界で段落が割れるのは残るが、ヘッダー・フッター・余白は消える。多くのプレビュー用途はこれで足りる。
  2. 変換側で「用紙高さ無限」の設定でもう一度レイアウトし、別 View(scroll、spec §4.1)として同梱する。画像・フォント・繰り返し部品は共有されるので、増えるのは本文の命令列だけ。

2 は当初「ページ 1 枚の fixed View」とするつもりだったが、帯に切った scroll にした。1 枚のページでは、長い文書で Canvas の大きさの上限(一辺 32,767 px 程度)を超えてページ単位の描画ができず、見えていない部分まで 1 つの Object として読み込むことになるため。帯は flow の連続モードと同じ描き方(隙間 0、帯全体が本文)で積めるので、レンダラとテキスト層・検索はそのまま使える。帯の境界は行の間に置き、境界をまたぐ図や塗りだけを両方の帯に描く(シートのタイルと同じ考え方)。

8. ロードマップ

  1. 仕様固め: spec.md の opset 1 を確定。Go のエンコーダ + TS のデコーダを最小実装し、手書きの Object で Canvas に描けるところまで。
  2. フィクスチャとブラウザの機能テスト: Go でフィクスチャを生成し、ブラウザで描画・検索・選択・文書構造を検証する。
  3. PDF → BDF: 最初の実用変換(実装済み、§3.1)。
  4. ビューア: Worker + OffscreenCanvas、ページ/連続/シートの 3 モード、テキストレイヤー、検索。
  5. XLSX → BDF: 直接変換、Tile 化、固定ペイン(実装済み、§3.6)。CSV・TSV も同じ描画で(実装済み、§3.10)。
  6. PPTX 直接変換: マスター共有の本領(実装済み、§3.4)。
  7. Visio 直接変換: .vsdx。背景ページの共有とテーマの解決(実装済み、§3.8)。
  8. DOCX 直接変換: 変換側のレイアウトエンジン、紙面と scroll の 2 つの View(実装済み、§3.9)。
  9. draw.io 直接変換: ページごとの View とシートのような切り替え(実装済み、§3.11)。
  10. CAD 図面: DXF(実装済み、§3.12)、JWW(Jw_cad、実装済み、§3.13)、SXF(電子納品の P21 と SFC、実装済み、§3.14)、CGM(バイナリとクリアテキスト、実装済み、§3.20)、HP-GL/2 のプロットファイル(HP RTL の画像も、実装済み、§3.22)。いずれも共通の converter/internal/cad の上に作った。
  11. TIFF: 多ページのスキャンと FAX、画像の入力に共通の解像度の上限(実装済み、§3.15)。
  12. HTML / Markdown: リーダー表示。DOCX のレイアウトエンジンを共有し、固定幅の scroll View を作る(実装済み、§3.16)。
  13. Illustrator・Photoshop: アートボードをページに(実装済み、§3.17・§3.18)。
  14. 画像: ブラウザが表示できる画像はそのまま格納し、メタデータだけを読む。SVG はページが描く(実装済み、§3.19)。
  15. プリント基板: Gerber(RS-274X、X2)と Excellon。ZIP にまとめたファイルの組を、基板の表と裏の見た目と層ごとの View に(実装済み、§3.21)。
  16. 数式: Office Math・MathML・LaTeX を OpenType MATH のフォントで組む共通のエンジン(Word・PowerPoint・Excel・HTML・Markdown・draw.io・EPUB は実装済み、§3.23)。
  17. EPUB: リフロー型の本は同じリーダー表示で本のページに組み(縦書きを含む)、固定レイアウトの本は画像のページにする(実装済み、§3.24)。
  18. サーバー側のプレビュー: 純 Go のラスタライザで描くサムネイルとページの画像、検索エンジン向けのテキスト(実装済み、§3.25)。
  19. Parquet: 列指向のデータファイルを表のシートに。読み込みは仕様から自前で書く(実装済み、§3.26)。
  20. 楽譜と演奏: MML・MIDI・MusicXML を五線譜に組み、View に SMF と cue を持たせてビューアで演奏する(実装済み、§3.27)。
  21. フォントファイル: 文字・グリフ・OpenType フィーチャーのプレビュー。GSUB・GPOS は自前で読む(実装済み、§3.28)。
  22. KiCad: KiCad 6 以降の回路図と基板。階層のシートをページに、基板を表・裏と層ごとの View に、KiCad の線の字体(CC0 の newstroke)で描く。参照するファイルはサーバーでは列挙して渡し、ウェブでは ZIP で(実装済み、§3.29)。
  23. 区間の配信: ログインした読者に文書を 10 ページずつ、要求ごとの使い捨ての鍵で封印して渡す。記録された通信は後で鍵が漏れても開けない(実装済み、§3.30)。
  24. 化学構造式(予定): MOL・SDF(SDF は表のシート)、ChemDraw の CDXML・CDX を ACS 1996 の描き方で。
  25. 音声ファイル: MP3・M4A・AAC・FLAC・Ogg・WAV・AIFF のカバーアートとタグを 1 ページのカードに。サムネイルはカバー、タグは Dublin Core。ファイルをそのまま持ってビューアで再生し、時刻つきの歌詞(SYLT、LRC)は歌っている行を示す(実装済み、§3.31)。
  26. InDesign: IDML のページ・マスター・図形・画像と、連結したフレームをページをまたいで流れるストーリー。テキストは DrawingML のテキストエンジンに TextFlow を足して組む(実装済み、§3.32)。
  27. 適合テスト: 他の言語の読み手(C の libbdf)のために、入力と期待する扱いを機械可読で配る。期待値は参照実装を走らせて記録する(実装済み、§3.33)。
  28. 帳票: Excel の印刷のページ(印刷範囲・ページ設定・印刷タイトル・ヘッダーとフッター)と、タグを書いたブックをデータで穴埋めするテンプレート。固定ページ・行が増える表・決まった行数のページ複製の 3 形態を、Excel・Word・PowerPoint のテンプレートで(実装済み、§3.6・§3.34)。BDF から PDF への書き出し(影、タグ付き PDF、PDF/A も含めて実装済み、§3.35)。QR コードとバーコード(実装済み、§3.34)。入力欄のある PDF と pdfme のテンプレートも、テンプレートとして埋められる(実装済み、§3.34)。フォントの対応規則は予定。

9. リポジトリ構成(案)

bdf/
├── docs/              spec.md, design.md
├── *.go               Go パッケージ bdf(module github.com/shibukawa/bdf): Object builder、Part エンコード、コンテナ I/O、デコーダ
├── cmd/bdf/           CLI: generate / fill / template / pdf / thumbnail / text / render / ls / manifest / disasm / extract / split / join / encrypt / decrypt / segment / demo
├── form/              帳票のテンプレートをデータで穴埋めする(§3.34)。form/tag はタグの読み手とデータのスコープ
├── converter/pdfform/ 入力欄のある PDF(AcroForm)をテンプレートとして埋める(§3.34)
├── converter/pdfme/   pdfme のテンプレートを埋める(§3.34。式の評価器と動的レイアウトを持つ)
├── segment/           区間を要求ごとに封印して返す HTTP ハンドラ(§3.30。wasm に net/http を持ち込まないよう bdf とは別のパッケージ)
├── cmd/bdfwasm/       ブラウザ内変換用の wasm モジュール(§2)
├── image/imgconv/     画像の格納方針と WebP/AVIF 変換(internal/ は wasm2go で生成した純 Go コーデック)
├── image/svg/         SVG の読み手: 要素の木、スタイルシートの適用、色・長さ・変換の値(§3.25。描くのは raster/imagebdf)
├── image/barcode/     QR コードとバーコードを矩形のパスとして作る(§3.34。帳票のテンプレートの画像タグが使う)
├── font/woff2/        TrueType/OpenType ↔ WOFF2(glyf 変換と Brotli)
├── raster/imagebdf/   ページと単体の Object を画像に描く純 Go のラスタライザと SVG レンダラ(§3.25)
├── vector/pdfbdf/     文書を PDF に書き出す(§3.35。タグ付き PDF と PDF/A も)
├── raster/ebitenginebdf/  パスだけの Object を Ebitengine で描く(別モジュール。§3.23)。raster/internal/shapes が Object を図形の並びにする
├── image/formula/     LaTeX・MathML の数式をパスだけの Object にする公開パッケージ(§3.23。STIX Two Math を同梱)
├── thumbnail/         文書のサムネイル(文書の種類によるレイアウト、PNG・JPEG・WebP)
├── internal/          fontdb(フォントの探索・解決・計測・サブセット)、sfnt(TrueType/OpenType の読み書きとグリフの輪郭)、
│                      cff(CFF の読み取りとサブセット化)、otlayout(GSUB・GPOS・GDEF の読み取り)。変換器と imagebdf が共有する。
│                      textrun(FILL_TEXT の並べ方: 表示順、ベースライン、合成の太字と斜体。imagebdf と pdfbdf が共有する)
│                      gauss(影のぼかし。箱形 3 回のガウス近似。imagebdf と pdfbdf が共有する)
│                      mathlayout(数式の木・読み手・組版。§3.23)、xmltree(XML の読み手。tinygodriver の xmlro で読む。Office の XML の木と、ほかの形式の XML の開き方)
├── converter/         入力形式の登録(static plugin)、共通のオプション、形式の判別、ページ指定
│   ├── pdf/           PDF → BDF 変換器(testdata/ にテスト用 PDF)
│   ├── ai/            Illustrator(.ai)→ BDF 変換器(PDF 部分を pdf で描く。testdata/ にテスト用 .ai)
│   ├── psd/           Photoshop(.psd、.psb)→ BDF 変換器(testdata/ にテスト用文書)
│   ├── pptx/          PowerPoint → BDF 変換器(testdata/ にテスト用デッキとフォント)
│   ├── xlsx/          Excel → BDF 変換器(testdata/ にテスト用ブック。フォントは pptx のものを使う)
│   ├── csv/           CSV・TSV → BDF 変換器(描画は xlsx。testdata/ にテスト用ファイル)
│   ├── parquet/       Apache Parquet → BDF 変換器(描画は xlsx。testdata/ にテスト用ファイル)
│   ├── docx/          Word → BDF 変換器(testdata/ にテスト用文書とフォント)
│   ├── html/          HTML(.html、.xhtml、.mhtml)→ BDF 変換器、リーダー表示(testdata/ にテスト用のページ)
│   ├── markdown/      Markdown → BDF 変換器、HTML を経由(testdata/ にテスト用文書と画像)
│   ├── epub/          EPUB → BDF 変換器、リフロー型はリーダー表示、固定レイアウトは画像のページ(testdata/ にテスト用の本)
│   ├── emf/           Windows メタファイル(.emf、.wmf)→ BDF 変換器
│   ├── visio/         Visio(.vsdx)→ BDF 変換器(testdata/ にテスト用図面)
│   ├── idml/          InDesign(.idml)→ BDF 変換器(テキストは pptx と同じ DrawingML のエンジン。testdata/ にテスト用文書)
│   ├── drawio/        draw.io → BDF 変換器(testdata/ にテスト用の図、stencils/ に同梱のステンシル)
│   ├── dxf/           AutoCAD DXF → BDF 変換器(testdata/ にテスト用図面)
│   ├── jww/           Jw_cad(.jww)→ BDF 変換器(testdata/ にテスト用図面)
│   ├── sxf/           SXF(.p21、.p2z、.sfc)→ BDF 変換器(testdata/ にテスト用図面)
│   ├── cgm/           CGM(.cgm、.cgz)→ BDF 変換器(testdata/ にテスト用メタファイル)
│   ├── gerber/        Gerber・Excellon(とその ZIP)→ BDF 変換器(testdata/ にテスト用の基板とファイル)
│   ├── kicad/         KiCad の回路図・基板・プロジェクト(とその ZIP)→ BDF 変換器(CC0 の newstroke の字形を埋め込む。testdata/ にテスト用のプロジェクト)
│   ├── tiff/          TIFF(.tif、.tiff)→ BDF 変換器(testdata/ にテスト用のスキャン・FAX・向きのファイル)
│   ├── image/         画像(PNG・JPEG・GIF・WebP・AVIF・BMP・ICO・SVG)→ BDF 変換器(そのまま格納してメタデータを読む。testdata/ にテスト用画像)
│   ├── mml/           MML(汎用・マビノギ・PPMCK)→ BDF 変換器、楽譜にする(testdata/ にテスト用の曲)
│   ├── midi/          Standard MIDI File → BDF 変換器、楽譜にする(testdata/ にテスト用の曲)
│   ├── musicxml/      MusicXML(.musicxml、.mxl)→ BDF 変換器(testdata/ にテスト用の楽譜と gen.py)
│   ├── font/          フォントファイル → BDF 変換器(testdata/ にテスト用フォント。test/font/gen.py が作る)
│   ├── all/           すべての形式を登録する
│   └── internal/      Office 系の変換器で共有する ooxml(OPC パッケージと XML の要素木・トークンの読み手)、ziputil(zip の展開。klauspost の inflate)と
│                      ooxml/drawingml(DrawingML の図形・テキスト・表・グラフ)、fontset(レイアウト用の
│                      フォント選択・計測・サブセット埋め込み。draw.io も使う)、canvas(組み立て中の Object。draw.io も使う)、
│                      metafile(EMF/WMF の再生)、
│                      暗号化された Office 文書を開く cfb(複合ファイル)と offcrypto(Agile / Standard 暗号化の復号)、
│                      linebreak(行分割の規則)、CAD の変換器で共有する cad(図面をページに描く)、tiff(TIFF の読み取りと CCITT のデコーダ)、wordproc(Word・HTML・Markdown・EPUB の組版エンジン)、xmp(XMP の Dublin Core)、
│                      equation(数式の読み手と組版)、music(楽譜の記譜と組版、演奏の SMF と cue。smufl/ に Bravura の字形)、
│                      webdoc(HTML と XHTML の読み込み、svg 要素の SVG 文書化、KaTeX・MathJax の数式の書き方の正規化、data: URL。HTML・Markdown・EPUB が使う)
├── fixture/           フィクスチャ生成(埋め込みフォント、計測、サンプル文書)
├── packages/
│   ├── core/          @bdfkit/core  デコーダ・コンテナ読み込み・テキスト抽出(依存なし)
│   └── render/        @bdfkit/render Canvas バックエンド、ページ/連続/シート描画(scroll View は連続描画)、Worker とクライアント(SVG の画像はクライアントが描く)
├── examples/
│   ├── viewer/         デモビューア(Worker 描画、テキストレイヤー)
│   ├── common/         ビューアと site/ が共有するもの(変換 Worker のクライアント、ビルドの補助)
│   ├── miniviewer/     埋め込み用の小さなビューア(examples/ のサンプルが使う)
│   ├── light-server/、preview-server/、search/  構成のサンプル(変換をブラウザで・サーバーで、検索エンジンと)
│   └── secure-reader/  ログインした読者に PDF を区間ごとに封印して読ませるサンプル(§3.30)
├── site/               デモサイト(トップページ、ビューア、サムネイル・検索テキストのページ、ドキュメント)。GitHub Pages で公開
├── testdata/          Go が生成した demo.bdf / demo-split / demo-encrypted.bdf、区間の文書(segments/、テスト専用の鍵で封印)、PDF・Illustrator・Photoshop・PowerPoint・Excel・Visio・Word・DXF・JWW・SXF・CGM・Gerber・TIFF・Markdown・HTML・画像・EPUB・楽譜の変換結果
└── test/              ブラウザの機能テストとフィクスチャ生成スクリプト

将来の変換器(DOCX など)も converter/ のサブパッケージとして追加し、bdf generate から形式を判別して呼ぶ。