BDF (Browser-specific Document Format) 仕様 v1.0
状態: 安定版。この仕様 v1.0 は BDF wire format version 1 と命令セット opset 1 を定義する。wire format と opset の番号は独立して進める。Go の module/import 名
github.com/shibukawa/bdfはこの仕様名と独立しており、変更しない。
この仕様への適合と互換性
この文書で 必須、禁止、推奨、してもよい は、それぞれ規範要件を表す。適合する書き手は必須の出力規則を満たし、適合する読み手は必須の入力規則を満たす。
- BDF v1 の single 形式では、ヘッダーの
versionと manifest のbdfはともに1でなければならない。split 形式にはヘッダーがないため、manifest のbdfが1でなければならない。 - 通常の manifest を出力する書き手は
opset: 1を記録しなければならない。暗号化文書の外側の manifest はopsetを持たず、復号後の manifest がopset: 1を持つ。読み手は、manifest のopsetが対応する最大値より大きければ拒否しなければならない。省略または0は宣言なしとして扱い、実際に各 Object を読むときはその Object のopsetを適用する。 - Object Part の
opsetは1でなければならない。読み手は、自分が対応する最大値より大きい format version または opset を拒否しなければならない。BDF v1 の適合実装は1以外の値を拒否しなければならない。 - 同じ意味を保つ任意の manifest メンバーは、format version を変えずに追加できる。読み手は未知の任意メンバーを無視してもよい。v1 読み手が知らない必須の動作を導入する変更には、新しい format version が必要である。
- コンテナ、manifest、暗号化、Part の解釈を変える変更は format version を上げる。Object 命令の意味やオペランドを変える変更は opset を上げる。既存の命令番号の再利用は禁止する。
- 未知の opcode はエラーであり、読み手はその Object の描画を続けてはならない。
EXTは長さ付きで読み飛ばせる拡張領域である。拡張データだけで描画や安全性に必須の意味を与えてはならない。 - 書き手は予約済みビットと予約済みバイトを
0で出力しなければならない。v1 読み手は未定義の予約ビットが立った入力を拒否しなければならない。
この仕様はワイヤ形式を定める。Go の converter API、コマンド、ビューア UI の互換性は別の API 契約である。
1. 目的と非目的
目的
- Office 系ファイル(PowerPoint / Excel / Word 等)や図(draw.io)をサーバー側で変換し、ブラウザ(Worker)で Canvas 2D にそのまま描画できる中間フォーマット。
- 命令セットは
CanvasRenderingContext2Dの API に 1:1 に近い形で対応させ、デコーダが「バイト列を読んで ctx のメソッドを呼ぶだけ」になるようにする。 - 圧縮・展開は
CompressionStream/DecompressionStream(deflate-raw)に任せ、独自実装を持たない。 - 1ファイル(配布・キャッシュ向け)と分割ファイル群(オンデマンド取得・CDN 向け)を同じ論理構造で表現できる。
- スライドマスターやページ背景など同一内容の繰り返しは共有オブジェクトとして1回だけ格納し、サイズを抑える。
- 3種類のレイアウトモデルを表現できる。
- 固定サイズページの列(PowerPoint、PDF)
- 無限サイズの平面(Excel のシート)
- ページ分割されつつ「縦一枚」としても閲覧できる文書(Word)。ページを持たずにレイアウトした 1 枚の長い面も持てる
- パスワードで保護された入力からは、同じパスワードで暗号化した BDF を作れる。暗号化しても Part 単位の取得はそのまま使える(§3.5)。
- サーバーが文書を数ページずつ、要求ごとに読み手の使い捨ての鍵で封印して渡せる(保護モード、区間の文書、§3.6)。記録された通信は、後でサーバーや読み手の鍵が漏れても開けない。
非目的
- 編集可能な文書モデル(DOM のような意味構造)を持つこと。BDF は「描画済み表示リスト」であり、再レイアウトはしない。ただし読み上げに必要な最小限の構造(見出し・リスト・表・図の代替テキスト・言語、§7.8)は描画に影響しない
MARKとして持つ。 - 印刷用の高度な色管理(CMYK、ICC)。sRGB のみ。
- 署名。必要なら配布層で行う。暗号化(§3.5)は保護された入力の保護を保ち続けるためのもので、配布層のアクセス制御の代わりではない。
2. 用語
| 用語 | 意味 |
|---|---|
| Part | コンテナに格納される1つのバイト列。内容のハッシュで識別される。 |
| Object | 描画命令列を含む Part。ページ本体・マスター・シートのタイルなど、描けるものはすべて Object。 |
| View | 閲覧単位。スライド集、シート、文書、図のページなど。1 BDF に複数の View を持てる(Excel の複数シート、draw.io の複数ページ等)。 |
| Page | 固定サイズの矩形。fixed / flow View を構成する。 |
| Tile | sheet View を格子状に分割した矩形。1 Tile = 1 Object。 |
| Resource | Object から参照される Font / Image / Path / Paint。 |
3. 全体構造
BDF
├── manifest (JSON) … View 一覧、ページ寸法、Part 一覧
└── parts/<hash> … 内容アドレスの Part 群
├── Object(描画命令列)
├── Font(WOFF2 等、そのままのバイト列)
├── Image(PNG/JPEG/WebP/AVIF/SVG など、そのままのバイト列)
├── Path collection(パス群のバイナリ)
├── Index(大きなページ表・タイル表・テキスト索引・再生の cue)
└── Seq(View が演奏する音楽。Standard MIDI File)
3.1 内容アドレス(content addressing)
- Part 名は SHA-256 の先頭 16 バイトを小文字 hex にした 32 文字。
- 同一内容の Part は自動的に 1 つに畳まれる。マスタースライド、ヘッダー/フッター、繰り返し画像などはエンコーダ側で「同じバイト列を作る」だけで共有される。
- 読み手はハッシュ検証をしてもよい(
crypto.subtle.digest)が必須ではない。
3.2 圧縮
- Part ごとに独立して圧縮する。方式は
identity(無圧縮)またはdeflate-raw。 - 読み手は
new DecompressionStream("deflate-raw")で展開する。 - 独立圧縮にするのは Range 取得・部分取得・タイル単位キャッシュを可能にするため。Part をまたぐ冗長性は 3.1 の共有で吸収する。
- 目安: 展開後 512 バイト未満、または画像(PNG、JPEG など)・WOFF2 のような既圧縮データは
identity。SVG と BMP(ICO の中のものを含む)は圧縮されていないのでdeflate-rawが効く。 audioの Part(§4.4)は符号化によらずidentityで格納する。読み手はそのバイト列をそのままブラウザのプレイヤーに渡す。- 読み手は Part を manifest の
sizeを超えて展開しない。超えるデータは不正とする。小さなファイルが、manifest に書いてある以上のメモリを使わせることはできない(deflate は 1 バイトを最大 1032 バイトに展開できる)。大きさを持たない manifest 自身は 256 MiB までとする。
3.3 1ファイル形式(single)
先頭にディレクトリを置く。ストリーミング読み(fetch().body を先頭から読み進めながら描画開始)と Range 取得の両方に対応する。
offset 0 : magic u8[4] "bdf\0"(62 64 66 00)
offset 4 : version u16 フォーマットバージョン(1)
offset 6 : flags u16 bit 0: 暗号化(§3.5)。他のビットは予約(0)
offset 8 : manifestOff u64 manifest の先頭オフセット(通常 32)
offset 16 : manifestLen u64 manifest の圧縮後長
offset 24 : manifestEnc u8 0=identity, 1=deflate-raw
offset 25 : reserved u8[7]
offset 32 : manifest
offset ... : parts 領域(manifest 直後から始まり、manifest.parts に記載された順に連続配置)
- すべてリトルエンディアン。
- マジックは小文字の
bdfに NUL を 1 バイト続けたもの。NUL があるのでテキストファイルと取り違えない。バージョンはversionで表し、マジックには含めない。 versionは1。読み手は1以外を拒否する。flagsは bit 0 だけを定義し、他のビットは0でなければならず、読み手は未定義のビットが立っていれば拒否する。manifestEncは0(identity)または1(deflate-raw)。reservedはすべて0でなければならない。manifest.parts[i]はoff(parts 領域の先頭、すなわちmanifestOff + manifestLenからのオフセット)とlenを持つ。manifest の内容が自身の長さに依存しないようにするため。- manifest と各 Part の範囲(
offとlen)はファイルの中に収まる。負の値や、足すとあふれる値を持つファイルは不正。 - 推奨配置順: manifest → Font → 共有 Object(マスター等)→ View の先頭ページから順。先頭から読むだけで 1 ページ目が描けるようにする。
- 書き手は全 Part を確定してから書き出す(サーバー変換なので問題ない)。
3.4 分割形式(split)
<base>/manifest.json
<base>/parts/<hash>
manifest.jsonと Part の内容は single と同一。offは無視される(存在しなくてよい)。- 各 Part ファイルは
encに従って圧縮済みのまま置く。HTTP のContent-Encodingに依存しないので、S3 等のオブジェクトストレージや CDN にそのまま置ける。 - 読み手は
fetch(base + "/parts/" + hash)で必要な Part だけを取得する。 - single ↔ split は Part の再圧縮なしに相互変換できる(ディレクトリの組み替えだけ)。
3.5 暗号化
パスワードで保護された入力(Office の読み取りパスワード、PDF のユーザーパスワード)から作った文書は、同じパスワードで暗号化する。Part を 1 つずつ封印するので、Range 取得・split 形式・CDN への配置はそのまま使える。
外側の manifest(平文) … 暗号方式、鍵スロット、封印された Part の一覧
封印された Part
├── manifest … 文書の manifest(§4。views、meta、parts)
└── 各 Part … Part の格納バイト列(圧縮済み)を暗号化したもの
外側の manifest
{
"bdf": 1,
"encryption": {
"cipher": "A256GCM",
"keys": [
{ "type": "password", "kdf": "PBKDF2-SHA256", "iter": 600000,
"salt": "<base64, 16 バイト>",
"key": "<base64: AES-KW でラップしたコンテンツ鍵, 40 バイト>" }
],
"manifest": { "part": "<hash>", "enc": "deflate-raw" } // 封印された manifest とその符号化
},
"parts": [
{ "h": "<hash>", "t": "sealed", "enc": "identity", "len": 1474, "size": 1474, "off": 0 },
…
]
}
- views・meta は持たない。題名などの Dublin Core も封印された manifest の中にある。
- single 形式ではヘッダの
flagsの bit 0 を立てる(§3.3)。bit 0 とencryptionの有無が食い違うファイルは不正。 - 封印された Part の名前は、格納バイト列(暗号文) の SHA-256 の先頭 16 バイト。平文のハッシュを外に出すと、よく使われるフォントのサブセットや画像が含まれているかを外から照合できてしまう。格納バイト列のハッシュなので、鍵がなくても名前を検証でき、single ↔ split も鍵なしで組み替えられる(§3.4)。
- 並び順は、封印された manifest が先頭で、その後は §3.3 の推奨順。
鍵
- コンテンツ鍵: 文書ごとにランダムな 256 ビットの鍵。manifest とすべての Part をこの鍵で封印する。
- 鍵スロット(
keys): コンテンツ鍵を AES-KW(RFC 3394)でラップしたもの。passwordスロットのラップ鍵は PBKDF2-HMAC-SHA-256 で導く。入力はパスワードを NFKC に正規化して UTF-8 にしたもの、salt は 16 バイト、反復回数はiter(書き手の既定は 600,000)。NFKC なので、全角と半角の違い、合成済みの字と結合文字で書いた字の違いは、同じパスワードになる(passとpass、ガとガ)。大文字と小文字、ひらがなとカタカナは区別する。v0.1.6 までの実装は NFC で正規化していた。その版が、NFKC で変わる文字(全角の英数字、半角カタカナ、丸数字など)を含むパスワードで封印した文書は、今の読み手では開かない。 - 読み手はスロットを順に試し、アンラップの完全性検査を通ったものを使う。どれも通らなければパスワード違い。
- 読み手は
iterが 1〜10,000,000 の範囲外なら拒否する。開くだけで長く計算させられるのを防ぐため。スロットを並べても同じことができるので、試したスロットのiterの合計が 10,000,000 を超えるところでも拒否する。 - スロットを複数持てるのは、パスワードの変更(スロットの差し替えだけで済み、Part は暗号化し直さない)や、回復用の鍵などを後から足すため。
ecdh スロット: 1 人の読み手の鍵ペアだけが開けるスロット。§3.6 の区間の文書を、要求ごとに読み手が作った鍵で封印するのに使う。
{ "type": "ecdh", "kdf": "HKDF-SHA256", "crv": "P-256",
"epk": "<base64: 書き手の公開鍵, 非圧縮の点 65 バイト>",
"key": "<base64: AES-KW でラップしたコンテンツ鍵, 40 バイト>" }
- 書き手は文書ごとに P-256 の鍵ペアを作り、読み手の公開鍵との ECDH で共有秘密 Z(x 座標、32 バイト)を得る。ラップ鍵は HKDF-SHA-256 で導く。IKM は Z、salt は
epk ‖ 読み手の公開鍵(どちらも非圧縮の点 65 バイト)の SHA-256、info は ASCII のbdf ecdh v1、長さは 32 バイト。書き手はラップしたら自分の秘密鍵を捨てる。 - 読み手は自分の秘密鍵と
epkで同じラップ鍵を導く。WebCrypto では ECDH の結果を HKDF の基底鍵にするderiveKeyで、Z は JavaScript に出てこない。 - 読み手の鍵ペアを 1 回の要求のためだけに作り、文書を開いたら捨てれば、その文書を開ける鍵はどこにも残らない。後でサーバーの鍵、TLS の鍵、読み手のパスワードやセッションが漏れても、記録された文書は開けない(前方秘匿性)。
- 読み手が試す
ecdhスロットは 16 個まで。スロットごとに鍵の合意が要るので、iterの上限と同じく、開くだけで長く計算させられるのを防ぐ。
封印
- 封印した Part の格納バイト列は
nonce(12 バイト)‖ 暗号文 ‖ tag(16 バイト)。AES-256-GCM で、nonce はランダム。 - 追加認証データ(AAD)は、Part なら平文の Part 名(16 バイト)、manifest なら ASCII の
manifest。Part の差し替えや manifest との取り違えは復号で失敗する。 - 圧縮してから暗号化する。封印を解いた中身は、封印された manifest の
parts[].encに従って展開する。
封印された manifest
§4 の manifest と同じ形で、parts[] の各エントリに sealed(その Part を封印した外側の Part の名前)を持つ。off は使わず、外側のエントリの off/len で取得する。
読み手の手順は、外側の manifest を読む → パスワード(または鍵ペア)からラップ鍵を導いてコンテンツ鍵をアンラップ → encryption.manifest.part を復号・展開 → 以降は Part 名で引き、sealed が指す外側の Part を取得して復号・展開、となる。必要なのは WebCrypto の PBKDF2・AES-KW・AES-GCM(ecdh スロットではさらに ECDH・HKDF)だけ。コンテンツ鍵は抽出できない CryptoKey として Worker の中に置く。
守るもの・守らないもの
- 守る: 配布層(ストレージ、CDN、キャッシュ)からの漏洩。パスワードを知らない人にわかるのは、Part の数と大きさだけ。
- 守らない: パスワードの総当たり。ファイルを手に入れればオフラインで試せるのは元の Office ファイルや PDF と同じ。PBKDF2-SHA-256 の 60 万回は、元のファイル(Office の Agile 暗号化は SHA-512 を 10 万回)より弱い経路を作らないために選んだ。
- 変換はパスワードと平文を扱う。変換の後でパスワードを残さないのは運用側の責務。
3.6 保護モード(区間の文書、segment)
ログインした読み手に本を読ませるサイトのように、サーバーが文書を持ち、読み手には開いたページのあたりだけを数ページずつ渡す使い方のための形。区間の文書は、ある View の連続したページ(区間)だけを運ぶ、それ自体は普通の BDF である。
{
"bdf": 1, "opset": 1, "unit": "pt", "meta": { … },
"segment": { "view": "pages", "from": 10, "to": 20 }, // 運ぶページ [from, to)(0 始まり)
"views": [ { "id": "pages", "kind": "fixed",
"pages": [ { "w": 419.5, "h": 595.3, "layers": [] }, // 区間の外: 大きさだけ
…,
{ "w": 419.5, "h": 595.3, "layers": [ { "role": "body", "obj": "…" } ] }, // 区間のページ
… ] } ],
"parts": [ … ] // 区間のページが使う Part のうち、読み手がまだ持っていないもの
}
- 輪郭: すべての View とページを、大きさ・
body・directionとともに持つ。レイヤーを持つのはsegmentのページだけで、ほかのページのlayersは空。どの区間からでも文書全体を並べられる。 - Part: 区間のページのレイヤーの Object と、そこから参照されるもの(Object、フォント、画像、パス集)。読み手が前の区間で受け取ったものは省いてよい。省かれた区間は単独では描けず、前の区間と合わせて読む。
- テキスト索引を持たない(
textIndexなし)。View 全体の本文になるため。本の中の検索は読み手が受け取ったページか、サーバーで行う。 - 区間にできるのはページの View(
fixed・flow・scroll)だけで、play(§4.4)を持つ View もない文書。シートは区間にしない。 - 読み手は最初に受け取った区間を文書として開き、以後の区間の
segmentのページを差し込み、持っていない Part を足していく。View の並び・id・ページ数が合わない区間は別の文書のものとして拒否する。 - 区間は普通は §3.5 の
ecdhスロットで、要求ごとに読み手が作った鍵ペアに封印する。封印された manifest の中にsegmentもある。 - 読み手が区間をどう頼むか(HTTP の形)は配布層の決めることで、フォーマットには含めない。参照実装(Go の
segmentパッケージと@bdfkit/coreのSegmentLoader)の形は design.md §3.30 にある。
4. Manifest
JSON。読みやすさとツールでの扱いやすさを優先する。巨大になる部分(ページ表・タイル表)は Index Part に逃がす。
{
"bdf": 1, // wire format version
"opset": 1, // Object 命令セット(§7)
"unit": "pt", // 1 unit = 1/72 inch。座標はすべてこの単位
"meta": { // §4.3
"dc": { "title": "…", "creator": ["…", "…"], "language": "ja", "created": "2026-09-25T07:21:16+09:00" },
"source": "pptx", "generator": "bdf-go/0.1" },
"views": [
{ "id": "slides", "kind": "fixed", "title": "スライド",
"pages": [ { "w": 960, "h": 540, "layers": [
{ "role": "master", "obj": "3f9a…" },
{ "role": "body", "obj": "b1c2…" } ] },
… ] },
{ "id": "doc", "kind": "flow", "title": "本文",
"pages": [ { "w": 595.3, "h": 841.9,
"body": { "x": 72, "y": 72, "w": 451.3, "h": 697.9 },
"layers": [
{ "role": "header", "obj": "…" },
{ "role": "body", "obj": "…" },
{ "role": "footer", "obj": "…" } ] }, … ],
"continuous": { "gap": 24 },
"textIndex": "<hash>" }, // 任意: テキスト索引 Part(§7.9)
{ "id": "scroll", "kind": "scroll", "title": "本文",
"pages": [ { "w": 523.3, "h": 1014.2, "layers": [ { "role": "body", "obj": "…" } ] },
{ "w": 523.3, "h": 988.5, "layers": [ { "role": "body", "obj": "…" } ] }, … ] },
{ "id": "sheet1", "kind": "sheet", "title": "Sheet1",
"tile": 2048,
"cols": [[26, 64], [1, 120], [16358, 64]], // [count, size] のランレングス
"rows": [[1048576, 20]],
"freeze": { "cols": 1, "rows": 1 },
"gridlines": true,
"tiles": { "0,0": "…", "1,0": "…" } // 小さければインライン
// "tilesRef": "<hash>" // 大きければ Index Part
}
],
"parts": [
{ "h": "3f9a…", "t": "obj", "enc": "deflate-raw", "len": 1234, "size": 5678, "off": 0 },
{ "h": "c0ff…", "t": "font", "enc": "identity", "len": 40210, "size": 40210, "off": 1234 },
…
]
}
平文の完全な manifest は bdf: 1 と opset: 1 を持つ。暗号化文書の外側の manifest は bdf: 1 と暗号化情報、封印された Part 一覧だけを持ち、opset と views は封印された内側の manifest に置く。manifest の opset は読み手の早期判定用の補助情報で、省略または 0 なら読み手は Object ごとの値を使う。読み手は未知の任意メンバーを無視してよいが、この仕様で必須と定めたメンバーの欠落や不正値を受け入れてはならない。
parts[].t の値: obj / font / img / path / idx / seq / audio(§4.4)/ tpl と src(§4.5)。暗号化した文書の外側の manifest では sealed(§3.5)。読み手は、どの View や Object からも参照されない Part を読み込まなくてよい。知らない t の Part も同じで、文書を開く妨げにしてはならない。
数値の範囲 — manifest の数値は、読み手のループの回数と確保する大きさ(範囲に入る Tile、シートのグリッド線、連続モードの帯)を決める。読み手は文書を開くときに一度だけ確かめ、範囲の外の値を持つ文書は不正として開かない。
viewsとpagesの要素はnullでない。- ページの
w・h、bodyのw・hは 0 以上 2^24 以下、bodyのx・yとcontinuous.gapは絶対値が 2^24 以下。Object の座標は f32 で、それを超えると 1 unit を区別できない。 tileは 1 以上 2^24 以下。0 以下と省略は既定の 2048。cols・rowsのcountは 0 以上の整数で、合計は 2^24 まで(Excel は 1,048,576 行)。sizeは 0(非表示)か、1/64 以上 2^24 以下。freezeのcols・rowsは 0 以上の整数。parts[].hとsealedは 32 文字の小文字 hex。分割形式では Part 名がそのまま取得する URL とファイル名になるので、読み手は名前を確かめてから使う。
4.1 View の種類
fixed — 固定サイズページの列。ページごとに w,h が異なってよい。ビューアはページを縦または横に並べる、あるいは 1 ページずつ表示する。
direction(任意、fixedとflow): ページを横に並べる・見開きにするときの順序。"ltr"(既定)は左から右、"rtl"は右から左で、右綴じの本(縦書きの日本語の本、右から読むマンガ)に使う。見開きではrtlの本の奇数ページが左、偶数ページが右に来る(ltrは逆)。ページを縦に並べる・1 ページずつ見せるときには関係しない。
flow — fixed に加え、各ページが body(余白を除いた本文矩形)を持ち、レイヤーに役割(role)が付く。ビューアは 2 つの表示モードを提供できる。
- ページモード: 紙の形で全レイヤーを描く。
- 連続モード:
role: bodyのレイヤーだけをbody矩形で切り出し、continuous.gapを挟んで縦に積む。ヘッダー・フッター・背景は描かない。
連続モードは再レイアウトではなく本文矩形の積み上げである(BDF は表示リストであり、テキストの折り返し位置は変えられない)。「1 枚の長いページとして再レイアウトしたもの」も欲しい場合、変換側が別 View(kind: scroll)を追加で生成する。共有 Object により画像やフォントは二重に格納されない。
scroll — ページを持たない縦に長い 1 枚の面(Word の下書き・Web レイアウト表示のように、用紙の高さを無限にしてレイアウトした文書)。pages はこの面を上から順に水平に切った帯(strip)で、形は fixed のページと同じ(w、h、layers)。
- ビューアは帯を隙間なく縦に積み、それぞれ帯の矩形で切り抜いて描く(
flowの連続モードでcontinuous.gapが 0、bodyが帯全体の場合と同じ)。帯はページではないので、ページとして 1 枚ずつ見せたりページ番号を示したりしない。 - 帯に切るのは、Canvas の大きさの上限を超えないため、また見えている範囲の Part だけを読み込むためである。書き手は帯の境界を行の間に置き(テキストの行を横切らない)、境界をまたぐ図・塗り・罫線は両方の帯に描く。帯の高さはそろっていなくてよい。帯の幅はすべて同じ。
- 開いている構造(§7.8)は帯ごとに閉じるので、帯をまたぐ表やリストは次の帯で開き直す。LINK の
#page=Nは N 番目の帯を指す。
sheet — 原点 (0,0) から右下に伸びる無限平面。
cols/rowsはランレングス[count, size]の配列。ビューアは行・列ヘッダー、グリッド線、固定ペイン(freeze)をこの情報から自前で描く。- 内容は
tileunit 四方の Tile に分割され、各 Tile は 1 Object。Tile 内の座標は Tile 原点を基準にする(f32 の精度限界を超えないため)。 - 複数 Tile にまたがるオブジェクト(結合セル、画像、グラフ)は共有 Object にして、触れる各 Tile から
USEで参照する。ビューアは Tile 矩形でクリップして描くので重複描画は正しく処理される。小さな描画(セルの塗りや罫線、隣のセルにはみ出したテキスト)は共有せず、触れる各 Tile にその Tile の原点で描いてもよい。 - テキストの抽出(テキスト索引、テキスト層、検索)では、run はアンカー(
FILL_TEXTなどの x, y を変換したもの)が含まれる Tile に属し、他の Tile が重ねて描いた同じ run は除く。Tile の範囲は左上の辺を含み、右下の辺を含まない。 - 空 Tile は表に載せない。
tilesの Index Part 形式(t: idx):[u32 tx, u32 ty, u8[16] hash]を (ty, tx) 昇順に並べた固定長レコード列。二分探索で引く。
複数の View — views の順が表示の順。View が 2 つ以上あるとき、ビューアは表計算ソフトのシート見出しのように View をタブで並べて切り替える(Excel のシート、draw.io のページ)。View の id は文書内で一意な任意の文字列で、LINK の #view= で参照される(§7.7)。
4.2 レイヤーの役割(role)
background / master / header / footer / body / notes / annotation。ビューアは役割ごとの表示切替に使える。未知の role は body として扱う。
4.3 メタデータ(meta)
| キー | 意味 |
|---|---|
dc |
文書そのものの記述。Dublin Core(下記) |
source |
変換元の形式(pdf / ai / psd / pptx / xlsx / csv / parquet / vsdx / drawio / dxf / jww / sfc / p21 / cgm / hpgl / gerber / kicad / emf / wmf / tiff / png / jpeg / gif / webp / avif / bmp / ico / svg / html / markdown / epub / mml / midi / musicxml / font / mp3 / m4a / aac / flac / ogg / wav / aiff / fixture …)。Dublin Core の source とは別物 |
generator |
書き出したソフトウェア(例 bdf-go/0.1) |
meta.dc は Dublin Core Metadata Element Set 1.1 の 15 要素に、DCMI Metadata Terms の created と modified を加えたもの。キーは要素名(名前空間接頭辞なし)。
"dc": {
"title": "四半期報告",
"creator": ["山田太郎", "佐藤花子"],
"subject": ["売上", "予算"],
"language": "ja",
"created": "2026-09-25T07:21:16+09:00"
}
- どの要素も省略でき、繰り返せる。値は文字列、または文字列の配列。書き手は値が 1 つなら文字列、2 つ以上なら配列で書く。読み手はどちらも受け付ける。空の配列は要素がないのと同じ。
- 未知のキーは無視する。
| 要素 | 内容 | 推奨する書き方 |
|---|---|---|
title |
題名 | |
creator |
作成者 | 人・組織ごとに 1 値 |
subject |
主題・キーワード | キーワードごとに 1 値 |
description |
概要 | |
publisher |
発行者 | |
contributor |
寄与者 | |
date |
何らかの日付(発行日など) | W3CDTF(ISO 8601 の部分集合)。2026、2026-09、2026-09-25、2026-09-25T07:21:16+09:00 |
type |
種類 | DCMI Type Vocabulary(Text、Image など) |
format |
形式 | MIME タイプ |
identifier |
識別子 | URI、ISBN、DOI など |
source |
派生元の資源 | |
language |
言語 | BCP 47(ja、en-US)。最初の値が文書のテキストの既定の言語(§7.8 の LANG で空の run の言語) |
relation |
関連する資源 | |
coverage |
範囲(場所・期間) | |
rights |
権利 | |
created |
作成日時 | date と同じ書き方 |
modified |
更新日時 | date と同じ書き方 |
変換器は入力文書のメタデータを次のように写す。
| 要素 | PDF(文書情報辞書) | PowerPoint、Excel、Word(コアプロパティ) | draw.io | TIFF(先頭のページのタグ) | EPUB(パッケージ文書) |
|---|---|---|---|---|---|
title |
Title |
dc:title |
– | DocumentName |
dc:title(EPUB 3 の title-type が main のものが先) |
creator |
Author |
dc:creator |
– | Artist(; で分割) |
dc:creator |
subject |
Keywords(, ; 、 などで分割) |
dc:subject、cp:keywords(同様に分割) |
– | – | dc:subject |
description |
Subject |
dc:description |
– | ImageDescription |
dc:description(HTML ならその文字列) |
identifier |
– | dc:identifier |
– | – | dc:identifier(unique-identifier が指すものが先) |
language |
– | dc:language(なければ PowerPoint は既定のテキストスタイルの、Word は既定の run の言語。Word は本文の多くが和文なら東アジアの言語) |
– | – | dc:language |
rights |
– | – | – | Copyright |
dc:rights |
created |
CreationDate(W3CDTF に変換) |
dcterms:created |
– | – | EPUB 2 の opf:event="creation" の dc:date |
modified |
ModDate(W3CDTF に変換) |
dcterms:modified |
mxfile の modified |
DateTime(W3CDTF に変換) |
dcterms:modified の meta(EPUB 2 は opf:event="modification" の dc:date) |
EPUB はほかの Dublin Core の要素(publisher、contributor、date、type、format、source、relation、coverage)もそのまま写す。
PDF と TIFF の対応は、XMP が文書情報辞書と TIFF のタグを写す方法に合わせている(TIFF の DocumentName は XMP にないので題名にした)。
画像(ブラウザがそのまま表示できる画像をそのまま格納したもの)は、XMP、EXIF、IPTC、形式自身の情報の順に、要素ごとに先にあるものを使う。XMP(SVG では metadata 要素の RDF)の dc:* はその要素に写す(rdf:Alt は既定の言語の値)。Photoshop の文書の XMP も同じ読み方をする。
| 要素 | XMP | EXIF | IPTC | 形式自身 |
|---|---|---|---|---|
title |
dc:title |
XPTitle |
Object Name(なければ Headline) | PNG の Title、SVG の title 要素 |
creator |
dc:creator(なければ tiff:Artist) |
Artist(なければ XPAuthor。; で分割) |
By-line | PNG の Author |
subject |
dc:subject |
XPKeywords(; で分割) |
Keywords | – |
description |
dc:description(なければ tiff:ImageDescription) |
ImageDescription、XPSubject、XPComment、UserComment の順(カメラが既定で書く文字列は除く) |
Caption/Abstract | PNG の Description(なければ Comment)、GIF のコメント、SVG の desc 要素 |
rights |
dc:rights(なければ tiff:Copyright) |
Copyright(撮影者と編集者) |
Copyright Notice | PNG の Copyright |
language |
dc:language |
– | – | SVG のルートの xml:lang(なければ lang) |
created |
dcterms:created、photoshop:DateCreated、exif:DateTimeOriginal(なければ xmp:CreateDate、exif:DateTimeDigitized) |
DateTimeOriginal(なければ DateTimeDigitized)と OffsetTime* |
Date Created と Time Created | PNG の Creation Time |
modified |
xmp:ModifyDate、dcterms:modified(なければ tiff:DateTime) |
DateTime と OffsetTime |
– | PNG の tIME |
音声ファイル(MP3、M4A、AAC、FLAC、Ogg、WAV、AIFF)はタグを次のように写す(MP4 の uuid ボックスの XMP があれば、画像と同じくそれが先)。type は常に Sound、format はメディアタイプ(audio/mpeg など)。1 つのフレームやフィールドに入った複数の値(ID3v2.4 の NUL 区切り、セミコロン区切り)は複数の値にする。
| 要素 | ID3v2 | MP4(iTunes の ilst) |
Vorbis コメント(FLAC、Ogg) | RIFF INFO(WAV)、AIFF |
|---|---|---|---|---|
title |
TIT2 |
©nam |
TITLE |
INAM、NAME |
creator |
TPE1 |
©ART |
ARTIST |
IART、AUTH |
contributor |
TPE2(アルバムアーティスト)、TCOM、TEXT、TPE3、TPE4 |
aART、©wrt |
ALBUMARTIST、COMPOSER、LYRICIST、CONDUCTOR、ARRANGER、REMIXER |
IMUS、IWRI |
subject |
TCON(ID3v1 の番号は名前に) |
©gen、gnre |
GENRE |
IGNR |
description |
COMM(iTunes などプログラム自身のものは除く) |
©cmt、desc |
COMMENT、DESCRIPTION |
ICMT、bext の説明、ANNO |
publisher |
TPUB |
----:…:LABEL |
PUBLISHER、LABEL、ORGANIZATION |
– |
date |
TDRC、または TYER + TDAT + TIME、または TDRL |
©day |
DATE |
ICRD、bext の作成日 |
identifier |
TSRC(ISRC) |
----:…:ISRC |
ISRC |
– |
relation |
TALB(アルバム) |
©alb |
ALBUM |
IPRD |
language |
TLAN(ISO 639-2 を BCP 47 に直す) |
トラックの mdhd |
LANGUAGE |
ILNG |
rights |
TCOP |
cprt |
COPYRIGHT、LICENSE |
ICOP、(c) |
bdf generate の -dc 要素名=値(繰り返し可)で要素を上書きでき、-dc 要素名= でその要素を消せる。
4.4 演奏(play)
View は鳴らすものを play に持てる。楽譜(MML、MIDI、MusicXML から組んだもの)の View はその音楽を、音声ファイルのカードの View はその録音を持つ。BDF に音楽の命令はなく、楽譜は線・パス・文字の描画になる(§7)。play は描画とは別に、ビューアが音を鳴らし、鳴っている位置をページの上に示すためのもの。
{ "id": "score", "kind": "fixed", "title": "楽譜",
"pages": [ … ],
"play": { "seq": "<hash>", // t: seq の Part(Standard MIDI File)
"cues": "<hash>" } } // 任意: t: idx の cue 索引 Part
{ "id": "card", "kind": "fixed", "title": "曲名",
"pages": [ … ],
"play": { "audio": "<hash>", // t: audio の Part(音声ファイルそのもの)
"type": "audio/mpeg", // そのメディアタイプ
"cues": "<hash>" } } // 任意: t: idx の cue 索引 Part
play は seq か audio のどちらか一方を持つ。audio を知らない読み手はカードを描くだけで、音は鳴らさない。
seq Part — Standard MIDI File(SMF、フォーマット 0 か 1)のバイト列そのもの。MIDI の入力はそのファイルをそのまま格納し、MML と MusicXML は変換器が書き出す。ビューアは SMF を読み、テンポ(FF 51)に従って音を合成する(Web Audio API)。音色は General MIDI のプログラム番号(チャンネル 10 は打楽器)で、ビューアはそれに近い音で鳴らせばよい(音源は規定しない)。次のコントローラーも解釈してよい。
| CC | 意味 |
|---|---|
| 7 | チャンネルの音量 |
| 10 | パン |
| 11 | エクスプレッション |
| 70 | 矩形波のデューティー比(プログラム 80 Square Lead のとき。0〜31 = 50%、32〜63 = 12.5%、64〜95 = 25%、96〜127 = 75%)。MML の矩形波の音色の区別 |
audio Part — 音声ファイル(MP3、M4A、FLAC、Ogg、WAV、AIFF など)のバイト列そのもの。タグもカバーアートも入力のまま残す。再エンコードも圧縮もしない(§3.2)。ビューアはデコードをブラウザに任せる(<audio> 要素)。type はそのファイルのメディアタイプで、audio/ で始まる。読み手は audio/ で始まらない type を使ってはならない(無視して、ブラウザに中身から判別させる)。どの符号化を鳴らせるかはブラウザによる(AIFF と Apple Lossless は Safari だけ)。鳴らせないファイルの View も、カードはそのまま描く。
cue 索引 Part — 演奏の時刻とページの上の位置を結ぶ表。
u8[4] "BCUE"
u16 version (1)
varuint nSystems
system ×n:
varuint page View のページ番号(0 始まり)
f32 x y w h 鳴っている位置が動く矩形(楽譜の段、歌詞の行。unit)
varuint nCues
cue ×n(時刻順):
varuint dt 前の cue からの時刻の差(最初は 0 から)。単位は seq の SMF の tick(ヘッダーの division)、audio ではミリ秒
varuint system 段の番号
f32 x その時刻に鳴っている位置の x(unit、ページの座標)
seq の cue:
- 時刻 t の位置は、tick が t 以下の最後の cue の段の上にある。次の cue が同じ段にあれば x は 2 つの cue の間を tick で線形に補間し、別の段にある(または次がない)なら x のまま動かない。同じ tick の cue が続くときは後のものが効く(小節の終わりと次の段の始まり)。
- 繰り返し記号などで同じ場所を 2 回以上演奏するときは、その回数だけ cue が並ぶ。
- ビューアは位置を段の矩形の高さの縦線などで示し、位置のある段が見えるようにページを送る。
audio の cue:
- system は歌われる歌詞の 1 行(折り返した行はまとめて 1 つの矩形)、または 1 つの章。cue はその行が始まる時刻で、
xは矩形の左端。 - 時刻 t の位置は、時刻が t 以下の最後の cue の矩形の全体である(x の補間はしない)。最初の cue より前(前奏)には位置がない。同じ時刻の cue が続くときは後のものが効く。
- 同じ行を 2 回以上歌うときは、その回数だけ cue が並ぶ(system は同じでよい)。
- ビューアは位置を矩形の強調(行の背景の色など)で示し、その行が見えるようにページを送る。行をクリックされたら、その行の cue の時刻(複数あれば今の時刻に最も近いもの)から鳴らす。
4.5 コンパイル済みテンプレート(tpl と src)
帳票のテンプレート(タグを書いた Excel のブック、入力欄のある PDF など)を検査して配るための、任意の Part。この 2 つを持つ文書をコンパイル済みテンプレートと呼び、拡張子は .bdft を使う。コンテナと manifest はふつうの文書と同じで、View はテンプレートをサンプルデータで穴埋めしたページ(サンプルがなければタグのままのページ)を見せる。したがってビューア・サムネイル・テキスト抽出は、テンプレートをふつうの文書として扱える。
t: src— テンプレートそのもの(元のファイルのバイト列)。t: tpl— テンプレートの記述。UTF-8 の JSON。
{
"version": 1, // この記述の版
"format": "xlsx", // src の形式("xlsx" / "docx" / "pptx" / "pdf" / "pdfme")
"name": "invoice.xlsx", // 元のファイル名(任意)
"source": "cbd3…", // src の Part のハッシュ
"fields": [ // タグが引く名前
{ "name": "customer", "kind": "value", "at": ["Invoice!A2"] },
{ "name": "items", "kind": "section", "mode": "rows", "at": ["Invoice!A4"] },
{ "name": "price", "kind": "value", "in": ["items"], "at": ["Invoice!D4"] } ],
"sample": { … }, // ページが見せているサンプルデータ(任意)
"schema": { … } // データの JSON Schema(任意)
}
kindはvalue/section/image。imageのcodeは、その場所に画像ではなく値の記号を描くことを示す(qr、ean13、ean8、upca、code39、code128、gs1-128、itf、itf14、nw7)。inはそのタグを囲む区間の名前(外側から)。modeは区間が行(段落、スライド)を繰り返す方法(rows/slots/pages)、slotsはslotsの区間が持つ行数。atはタグのある場所で、書き方はsrcの形式による(Excel はセルの番地、Word はparagraph 3やtable 1, row 2, cell 1、PowerPoint はslide 2: 図形の名前)。nameが"."のvalueは、それを囲む区間の項目そのものを書くタグ({{.}})で、その区間の項目がオブジェクトではなく値であることを示す。formatがpdf(入力欄を持つ PDF)とpdfme(pdfme のテンプレートの JSON)のsrcは、タグではなく場所の決まったフィールドを持つ。fieldsはそのフィールドで、nameのドットはオブジェクトの入れ子を表す(applicant.name)。フィールドが自分について知っていることを、次の任意の項目で書く:type(値の型。string/number/boolean/date/date-time)、options(選択肢の値)とlabels(その表示名。optionsと同じ順)、label(人に見せる名前)、default(テンプレートに入っている値)、maxLength(最大の文字数)、multiline(複数行のテキスト)。atはpage 2のように書く。schemaは、データの形を JSON Schema(2020-12)で書いたもの。fieldsとsampleから作られ、プロパティはfieldsの順に並ぶ。区間は配列・オブジェクト・真偽値のどれか、値は文字列か数値として書かれ、各プロパティのx-bdfにfieldsのkind・format・mode・slots・code・at・labels・multilineが入る(typeは型とformatに、optionsはenumに、default・maxLengthは同じ名前のキーワードに、labelはtitleになる)。入力フォームを作る処理系は、srcを読まずにこれを使える。schemaのない記述では、fieldsから作る。- どの View もこの 2 つの Part を参照しない。文書の読み手はこれらを無視する。穴埋めをする処理系だけが読み、
srcをデータで穴埋めして、別の文書を作る。 - 穴埋めの規則(タグの書き方、行の繰り返し)はこの仕様の範囲外で、
srcの形式ごとに処理系が定める(Go の実装は packageform、形式ごとにはconverter/xlsx・converter/docx・converter/pptx・converter/pdfform・converter/pdfmeのFill)。 - 1 つの文書が持つ
tplは 1 つまで。暗号化した文書では、ほかの Part と同じく封印される。
5. Object Part
描画命令列と、それが参照するリソース表を持つ自己完結した Part。ページ、マスター、Tile、部品はすべてこの形式。
u8[4] "BOBJ"
u16 opset バージョン
u16 flags(予約)
f32×4 bbox (x, y, w, h) … この Object が描く範囲。キャッシュ用。原点は Object 自身のローカル座標
varuint nStrings ; UTF-8 文字列 [ varuint len, bytes ]×n
varuint nPaths ; パス [ u8 kind(0=inline,1=ext) , inline: パスデータ(§6.3) / ext: u8[16] hash, varuint index ]×n
varuint nPaints ; グラデーション・パターン定義(§6.4)×n
varuint nFonts ; フォント参照(§6.1)×n
varuint nImages ; 画像参照 [ u8[16] hash ]×n
varuint nObjects ; 子 Object 参照 [ u8[16] hash ]×n
varuint opsLen ; 命令列のバイト長
u8[opsLen] ; 命令列(§7)
- 命令内のリソース参照(
pathRef,fontRef…)は上記各表のローカルインデックス。 - 文字列は UTF-8。先頭の BOM(U+FEFF)は文字列の一部ではない。書き手は先頭に BOM を置かず、読み手は先頭の BOM を 1 つ取り除く(ブラウザの
TextDecoderが既定でそうする)。途中の U+FEFF はそのまま文字である。フォント参照(§6.1)のfamilyとテキスト索引(§7.9)のtextも同じ。 opsetは1、flagsは0。読み手は1以外のopsetと、0 以外の予約済みflagsを拒否する。- 外部参照のハッシュは Manifest の
partsに存在しなければならない。 - 座標は Object のローカル座標。
USEする側が変換行列で配置する。
6. リソース
6.1 Font
u8 kind 0=embedded, 1=system
u8[16] hash embedded のときのみ(Font Part。WOFF2 推奨、TTF/OTF も可)
string family CSS フォントファミリー文字列(フォールバック列を含む)。embedded では空でよく、空でなければ埋め込みフォントの後ろにフォールバックとして付ける
u16 weight 100–900
u8 style 0=normal, 1=italic, 2=oblique
- Worker 内では
new FontFace("bdf-" + hash, buffer)を生成しself.fonts.add()する。ctx.fontにはこの合成名を使う。 - サブセット化はエンコーダの責務。ライセンス上埋め込めないフォントは
systemにするか、テキストをグリフのアウトライン(Path)に落とす。
6.2 Image
Part の生バイト列。ブラウザが画像要素で表示できる形式(PNG / JPEG / GIF / WebP / AVIF / BMP / ICO / SVG)で、再エンコードしない。読み手はビットマップ画像を createImageBitmap(new Blob([bytes])) でデコードする。デコードした大きさが画像の画素の大きさで、EXIF の向き(JPEG、PNG)や AVIF の irot はブラウザが適用したものになる。
SVG(先頭が <。バイト順マークと空白は飛ばす。UTF-16 のバイト順マークで始まるものも)は createImageBitmap ではデコードできない(Blob からはビットマップ画像だけ)。読み手は画像要素(HTMLImageElement)で読み込み、Canvas に drawImage で描いてビットマップにする。拡大してもぼけないよう、描くたびに必要な画素数(現在の変換で画像の 1 画素が覆うデバイス画素)で描き直してよい。Worker には画像要素がないので、ページに描かせる。SVG は画像要素の中で描かれるので、スクリプトは動かず、外部の資源は読み込まれない。
画像のヘッダーは大きさを自分で言い、デコードするとその分のメモリを使う(30 KB の PNG が 1 GiB になりうる)。読み手は、デコードする前にヘッダーから大きさを読み、自分の上限より大きい画像をデコードしなくてよい。その画像は何も描かずに残りを描く(TypeScript のビューアの既定は 2^28 画素、Go のラスタライザは 100 × 2^20 画素)。
SVG の画素の大きさ(IMAGE_SUB の元の矩形と、パターンの 1 画素の単位)は、画像要素での大きさ(CSS px)とする: ルート要素の width と height(CSS の絶対長さ。単位がなければ px)。一方がないか相対値なら、もう一方と viewBox の縦横比から求める。両方なければ viewBox の幅と高さ、viewBox もなければ 300 × 150(置換要素の既定の大きさ)。
6.3 Path
Path2D に変換されるバイナリ。
varuint nVerbs
u8[nVerbs] verbs M=0 L=1 Q=2 C=3 Z=4 R=5(rect x y w h) E=6(ellipse x y rx ry rot a0 a1 ccw) A=7(arcTo x1 y1 x2 y2 r) O=8(roundRect x y w h r)
f32[...] verb ごとの引数を verbs と同じ順で連結
nPathsのextは Path collection Part(t: path)のindex番目を指す。Path collection はvaruint nに続けてパスデータを n 個並べたもの。グリフアウトラインを全ページで共有する用途。- 読み手は Path2D をハッシュ+index でキャッシュする。
6.4 Paint
u8 kind 0=linear 1=radial 2=conic 3=pattern
linear : f32 x0 y0 x1 y1
radial : f32 x0 y0 r0 x1 y1 r1
conic : f32 angle x y
上記3種: varuint nStops ; [ f32 offset, u32 rgba ]×n
pattern: varuint imageRef ; u8 repeat(0=repeat 1=repeat-x 2=repeat-y 3=no-repeat) ; f32×6 matrix
7. 命令セット(opset 1)
- 命令は
u8 opcodeに続けてオペランドを並べる。オペランド型:f32,u8,u32(色は0xRRGGBBAA),varuint(参照・個数),str(文字列表への varuint インデックス)。 - 描画状態(変換・クリップ・スタイル)は Canvas と同じくスタックで管理する。
USEは暗黙にsave/restoreで包まれる。 - 未知の opcode はエラー。前方互換は
opsetバージョンとEXT命令で確保する。
7.1 状態
| op | 名前 | オペランド | Canvas 対応 |
|---|---|---|---|
| 0x01 | SAVE | – | save() |
| 0x02 | RESTORE | – | restore() |
| 0x03 | TRANSFORM | f32 a b c d e f | transform(a,b,c,d,e,f) |
| 0x04 | TRANSLATE | f32 x y | translate |
| 0x05 | SCALE | f32 sx sy | scale |
| 0x06 | CLIP_PATH | varuint pathRef, u8 rule | clip(path2d, rule) |
| 0x07 | CLIP_RECT | f32 x y w h | beginPath(); rect(); clip() |
rule: 0=nonzero, 1=evenodd(以下同じ)。
7.2 スタイル
| op | 名前 | オペランド | Canvas 対応 |
|---|---|---|---|
| 0x10 | FILL_COLOR | u32 rgba | fillStyle = "#…" |
| 0x11 | FILL_PAINT | varuint paintRef | fillStyle = gradient/pattern |
| 0x12 | STROKE_COLOR | u32 rgba | strokeStyle |
| 0x13 | STROKE_PAINT | varuint paintRef | strokeStyle |
| 0x14 | LINE | f32 width, u8 cap, u8 join, f32 miter | lineWidth/lineCap/lineJoin/miterLimit |
| 0x15 | DASH | varuint n, f32[n], f32 offset | setLineDash / lineDashOffset |
| 0x16 | ALPHA | f32 | globalAlpha |
| 0x17 | BLEND | u8 | globalCompositeOperation(表は付録 A) |
| 0x18 | SHADOW | u32 rgba, f32 blur dx dy | shadowColor/Blur/OffsetX/OffsetY |
| 0x19 | FILTER | str | filter(任意。非対応環境では無視)。CSS のフィルタ関数(blur、drop-shadow など)と、その長さ・数値・色だけ。url() などそれ以外を含む文字列は、読み手が none として扱う |
| 0x1A | FONT | varuint fontRef, f32 size | font = "<style> <weight> <size>px <family>" |
| 0x1B | TEXT_STYLE | u8 align, u8 baseline, u8 dir, f32 letterSpacing | textAlign/textBaseline/direction/letterSpacing |
SHADOW の blur・dx・dy は unit で表す。Canvas の影は変換行列の影響を受けないので、読み手は設定時の変換の拡大率(行列式の平方根)を掛けて渡す。オフセットの向きはページの向きで、回転には追従しない。
cap: 0=butt 1=round 2=square。join: 0=miter 1=round 2=bevel。align: 0=left 1=right 2=center 3=start 4=end。baseline: 0=alphabetic 1=top 2=middle 3=bottom 4=hanging 5=ideographic。
7.3 図形
| op | 名前 | オペランド | Canvas 対応 |
|---|---|---|---|
| 0x20 | FILL_RECT | f32 x y w h | fillRect |
| 0x21 | STROKE_RECT | f32 x y w h | strokeRect |
| 0x22 | FILL_PATH | varuint pathRef, u8 rule | fill(path2d, rule) |
| 0x23 | STROKE_PATH | varuint pathRef | stroke(path2d) |
| 0x24 | FILL_PATH_AT | varuint pathRef, u8 rule, f32 x y | translate して fill。グリフや繰り返し部品用 |
| 0x25 | FILL_PATH_RUN | u8 rule, varuint n, [varuint pathRef, f32 x y]×n | 連続するグリフ描画を 1 命令で |
| 0x26 | CLEAR_RECT | f32 x y w h | clearRect |
7.4 テキスト
| op | 名前 | オペランド | Canvas 対応 |
|---|---|---|---|
| 0x30 | FILL_TEXT | str s, f32 x y, f32 advance | fillText(s, x, y) |
| 0x31 | STROKE_TEXT | str s, f32 x y, f32 advance | strokeText |
advance はエンコーダが計算した期待幅(unit)。読み手は measureText(s).width と比較し、差が閾値(推奨 0.5%)を超える場合は advance / measured で x 方向にスケールして描く(pdf.js と同じ手法)。これでフォント代替や文字送りの差異による行のはみ出し・ずれを抑える。advance が 0 のときは補正しない。
文字単位の正確な配置が必要な場合(カーニング・詰めのある見出し、埋め込み不可フォント等)は、エンコーダが 1 文字ずつ FILL_TEXT を出すか、アウトライン化して FILL_PATH_RUN を使う。
7.5 画像
| op | 名前 | オペランド | Canvas 対応 |
|---|---|---|---|
| 0x40 | IMAGE | varuint imgRef, f32 x y w h | drawImage(img, x, y, w, h) |
| 0x41 | IMAGE_SUB | varuint imgRef, f32 sx sy sw sh dx dy dw dh | 9 引数版 drawImage |
| 0x42 | SMOOTHING | u8 enabled, u8 quality | imageSmoothingEnabled/Quality |
7.6 合成
| op | 名前 | オペランド | 意味 |
|---|---|---|---|
| 0x50 | USE | varuint objRef | 子 Object を現在の変換で描く。暗黙 save/restore |
| 0x51 | USE_AT | varuint objRef, f32 x y | translate(x,y) して USE |
| 0x52 | GROUP_BEGIN | f32 alpha, u8 blend, f32 x y w h | グループ透過。読み手は一時キャンバスに描いて合成する |
| 0x53 | GROUP_END | – | |
| 0x54 | MASK_BEGIN | u8 kind, u32 backdrop, varuint n, u8[n] transfer | ソフトマスクの描画を始める |
| 0x55 | MASK_END | – | グループの内容にソフトマスクを掛ける |
USEの対象 Object は読み手が(hash, 現在の拡大率)をキーにビットマップキャッシュしてよい(マスタースライド等の高速化)。GROUP_BEGINとMASK_BEGINは合わせて 64 段まで開ける。一時キャンバスは、描く先のキャンバスに載る部分と、合成するときの影とフィルタが届く範囲(4096 デバイス画素まで)に切り詰めてよい。グループの矩形の大きさは文書が決めるので、そのままの大きさで確保しない。USEの入れ子は 64 段まで。読み手はそれより深いUSEを描かない。Object がどの Object を何回描くかは文書が決めるので、10 個の Object が次の Object を 10 回ずつ描くだけで、最後の Object は 10^10 回描かれる。読み手は、2 回目以降に描く Object から読む命令の数に上限を置いてよい(Go の実装は、テキストの抽出で View ごとに 2^27 命令まで)。1 回目に読む命令は文書の大きさで決まるので数えない。GROUP_BEGIN/ENDは入れ子可。x y w hは一時キャンバスの範囲。一時キャンバスは現在の変換を引き継ぎ、そのほかの状態(塗り・線の色、線幅、フォントを除く)は初期状態から始まる。合成は 1 回の描画で、透明度とブレンドはグループのものを使い、GROUP_BEGINを囲む状態の SHADOW と FILTER はグループ全体に掛かる(重なった図形が影を 1 つだけ落とす)。MASK_BEGIN〜MASK_ENDはソフトマスク(PDF の ExtGState/SMask)で、グループの中の最後、GROUP_ENDの直前に 1 つだけ置く。読み手は最も内側のグループの一時キャンバスと同じ大きさのマスク用キャンバスを用意し、現在の変換だけを引き継いで初期状態から、間の命令をそこに描く。MASK_ENDでマスク用キャンバスの各画素から値 m(0〜255)を求め、グループの一時キャンバスの各画素のアルファに m/255 を掛ける(source-inで合成してよい)。kind0(alpha): m はマスクのアルファ。マスク用キャンバスは透明から始まる。kind1(luminosity): マスク用キャンバスをbackdropの色(0xRRGGBBAA、アルファは無視)で塗ってから描き、m はその輝度0.3 R + 0.59 G + 0.11 B(PDF の非分離ブレンドモードと同じ重み)。transferは空か 256 バイトで、256 バイトなら m をtransfer[m]に置き換える(PDF の/TR。反転したマスクなど)。- マスク用キャンバスのクリップと状態は
MASK_ENDで捨てる。グループに残ったクリップはマスクの範囲を狭めない。 - マスクの中の命令は内容ではないので、テキスト抽出(MARK・LINK を含む)は
MASK_BEGIN〜MASK_ENDを読み飛ばす。
7.7 メタ・拡張
| op | 名前 | オペランド | 意味 |
|---|---|---|---|
| 0x70 | LINK | f32 x y w h, str url | リンク領域(現在の座標系の矩形)。描画には影響しない |
| 0x71 | MARK | u8 kind, str payload | 構造マーク(§7.8)。描画には影響しない |
| 0xFF | EXT | u32 len, u8[len] | 拡張。未知なら読み飛ばす |
LINK の矩形は他の命令と同じく現在の変換行列の座標系で表す。読み手は 4 隅を変換した外接矩形をリンク領域とし、その領域に重なる run をリンクの文字列とする。url は次のいずれか。読み手はそれ以外の url をリンクにしない。
http:/https:/mailto:の絶対 URL#page=N— 同じ View のページ(1 始まり。scrollView では帯)#view=IDまたは#view=ID&page=N— 別の View(IDは View のidを URL エンコードしたもの)とそのページ。ビューアはその View に切り替える(draw.io のページ間リンクなど)
7.8 MARK の種類とテキストの構造
MARK は描画に影響しない境界情報で、検索・選択・コピーのために命令列に読み順を与え、読み上げのために最小限の構造(見出し・リスト・表・図・言語)を与える。
| kind | 名前 | payload | 意味 |
|---|---|---|---|
| 0 | PARAGRAPH | 任意 | 段落の開始。以降の run は前の run と結合されない |
| 1 | LINE | 任意 | 同じ段落内の行の開始。前の run との間に空白 1 つがあるものとして扱う |
| 2 | CELL | セル参照(例 B12) |
表・シートのセルの開始。PARAGRAPH と同じ境界 |
| 3 | BOX | 任意 | テキストボックス・図形内テキストの開始。PARAGRAPH と同じ境界 |
| 4 | ALT_TEXT | 文字列 | 直後の描画命令 1 つ(FILL_TEXT / STROKE_TEXT / FILL_PATH_AT / FILL_PATH_RUN / USE / USE_AT)が表す文字列。テキスト抽出ではその命令の文字列の代わりにこの文字列を、位置はその命令の位置を使う(USE / USE_AT では、子 Object の bbox の x 方向の範囲をこの文字列の範囲とし、位置を bbox の左端に、字送りを bbox の幅にする)。合字や私用領域の文字で描いた run の本来の文字列、アウトライン化した文字、1 文字ずつ描いた縦書きなどに使う。描画命令が続かない場合は位置なしの run になる |
| 5 | WRAP | 任意 | 同じ段落内の行の折り返し。前の run との間に区切りを入れずに結合する。和文など語を空白で区切らない文字の間で折り返した行に使い、行をまたぐ検索を可能にする |
| 6 | HEADING | レベル 1〜6 |
見出しの段落の開始。PARAGRAPH と同じ境界 |
| 7 | LIST | 任意 | リストを開く(END で閉じる) |
| 8 | LIST_ITEM | 任意 | 最も内側の LIST の項目の開始。次の LIST_ITEM か LIST の END までが項目の中身(入れ子の LIST や続きの段落を含められる)。最も内側が LIST でなければ PARAGRAPH と同じ |
| 9 | TABLE | 任意 | 表を開く(END で閉じる)。中のセルは CELL で始める |
| 10 | FIGURE | 代替テキスト | 図を開く(END で閉じる)。範囲は中の描画命令の外接矩形。代替テキストが空なら装飾で、読み上げない |
| 11 | END | 任意 | 開いている LIST / TABLE / FIGURE のうち最も内側を閉じる。開いていなければ何もしない |
| 12 | LANG | 言語(BCP 47)。空なら文書の既定(meta.dc.language の最初の値) |
以降の run の言語 |
MARKの効果は次のFILL_TEXT/STROKE_TEXT/ALT_TEXTに及び、run どうしの結合規則を決める。同じ行で書式だけが変わった run の間にはMARKを置かない(結合される)。MARKを出さないエンコーダのために、読み手は位置に基づく推定(y が変われば行、同じ行で字送りの 0.2 倍以上の隙間があれば空白)で補ってよい。ただし推定は不正確なので、エンコーダはMARKを出すことが強く推奨される。- run どうしの区切りは kind だけで決まる。LINE は空白、WRAP は連結、PARAGRAPH / CELL / BOX / HEADING / LIST / LIST_ITEM / TABLE / FIGURE / END は段落境界。ALT_TEXT と LANG は区切りに影響しない(LANG の後の run は位置による推定のままで、保留中の ALT_TEXT もそのまま次の描画命令に付く)。
構造
読み上げ用の構造は、MARK を走査順にたどって組み立てる。
- 葉の MARK(PARAGRAPH、BOX、HEADING、LIST の外の LIST_ITEM、TABLE の外の CELL)は次の run が属する段落の種類を予約する。run を挟まずに続いた場合は最後のものが決める(BOX の直後の HEADING は見出しの段落)。
- 構造の MARK(LIST、TABLE、FIGURE、END、最も内側が LIST のときの LIST_ITEM、最も内側が TABLE のときの CELL)はその場で効き、予約を取り消す。LIST や TABLE の中で項目・セルの前に現れた run は、それぞれ暗黙の項目、表の見出し(caption)になる。
- 最も内側が TABLE のときの CELL の payload は、表の左上を
A1とする参照か範囲(B2:C2、結合セル)で、後ろにcol(列見出し)かrow(行見出し)を付けられる(例A1 col)。セルは左上の位置の行優先の順に並べる。TABLE の外の CELL はシートのセル参照(例B12、結合セルは範囲B2:C3)で、同じくcol/rowを付けて見出しのセルにできる(シートの中の表の見出し行)。 - 開いている構造と LANG は、
USEした子 Object や SAVE/RESTORE をまたいで走査順に一直線に続き、最上位の Object(ページのレイヤー、シートのタイル)ごとに初期状態(構造なし、言語は文書の既定)に戻る。最上位 Object の終わりで開いている構造は閉じる。共有プレフィックス(§5 のUSE)で命令列が分かれても同じ結果になるための規則である。エンコーダは開いた構造を END で閉じ、言語を持つ子 Object をUSEした後は必要なら LANG を出し直す。 - FIGURE の範囲は、中の描画命令(画像、矩形、パス、子 Object の中身、テキスト)を現在の変換で移した外接矩形を、その時点のクリップの外接矩形と交わらせたもの。中身が空なら大きさ 0 とする。FIGURE の中のテキストは検索・選択・コピーの対象のままだが、代替テキストがあれば読み上げには代替テキストを使う。
7.9 テキスト索引 Part
View ごとに任意で持てる索引。View.textIndex にハッシュを置く(t: idx)。全 Object をデコードせずに全文検索するため、またサーバー側の検索エンジンに同じデータを流すためのもの。
u8[4] "BTXT"
u16 version (1)
varuint nRuns
run ×n:
varuint a fixed/flow/scroll: ページ(帯)番号 sheet: タイル x
varuint b fixed/flow/scroll: レイヤー番号 sheet: タイル y
varuint ordinal その Object のテキスト抽出(§8 のテキストバックエンド、USE の子を含む走査順)における run の通し番号。シートでは §4.1 の規則でその Tile に属さない run を載せないので、番号は飛ぶことがある
u8 sep 前の run との結合: 0=連結 1=空白 2=段落境界(一致は境界をまたがない)
str text run の文字列(ALT_TEXT の場合はその文字列)
- 順序は読み順。読み手は
textをsepに従って連結した平文に対して検索し、ヒットを (a, b, ordinal, 文字範囲) に戻す。 - ヒットの矩形は該当 Object をデコードして run の位置・フォント・
advanceから計算する(部分一致は接頭辞幅の計測で求める)。 - 正規化(NFKC、大文字小文字、かな、数式のマイナス記号 U+2212 →
-)は索引には施さず、検索時に読み手が行う。NFKC は 1 文字ずつではなく run の文字列全体 に掛ける。文字とその後ろの結合文字(eと U+0301、半角のカと濁点゙、ハングルの字母)は合成された 1 文字として一致し、ヒットの文字範囲はその並び全体を指す。run をまたいでは合成しない。大文字小文字、かな、マイナス記号は、NFKC の後で 1 文字ずつ畳む。
8. 読み手(ビューア)アーキテクチャ
main thread worker
────────────── ────────────────────────────────────
Viewer UI Loader fetch / DecompressionStream / Part キャッシュ
スクロール・ズーム ──msg──▶ Decoder Object → 命令配列(またはその場実行)
<canvas> or 転送済み ◀─bitmap─ Renderer OffscreenCanvas に描画。Path2D / ImageBitmap / FontFace / USE ビットマップのキャッシュ
テキストレイヤー(DOM) ◀─runs── TextExtractor 同じ命令列を走査して文字列と矩形を返す
- ページやタイルなど最上位の Object は Canvas の初期状態(黒の塗り・線、線幅 1、
10px sans-serif、不透明、source-over)から描き始める。USEされた子 Object は親の状態を引き継ぐ(塗りを指定しないパターンセルなどに使う)。 - 描画は
(Object, 変換行列, クリップ矩形)の純関数。Tile・ページ・高倍率時の部分再描画をすべて同じ経路で処理する。 - 命令列は「実行できるデータ」なので、バックエンドを差し替えられる: Canvas レンダラ、テキスト抽出(検索・選択・読み上げ用の DOM レイヤー。MARK の構造と LINK のリンクも含む)、将来的な WebGL/WebGPU レンダラ。テキストレイヤーのために別の Part を持つ必要はない。
- 検索はビューアではなくライブラリ(Worker)が提供する。索引 Part(§7.9)があればそれを、なければ Object を走査して同じ形の run 列を作り、正規化して検索し、ヒットの矩形を返す。ビューアはヒット一覧とハイライトの描画だけを担当する。
- 大きな文書ではページ表を Index Part にして、可視範囲のページだけ Part を取得する。single 形式でも
off/lenにより Range 取得できる。
必要なブラウザ機能(いずれも 2023 年時点の主要ブラウザで利用可能): DecompressionStream("deflate-raw")、Worker 内 OffscreenCanvas、Worker 内 FontFace / self.fonts、createImageBitmap、Path2D、roundRect。SVG の画像にはページの画像要素が要る(§6.2。Worker はページに描かせる)。暗号化した文書(§3.5)には crypto.subtle が要る(HTTPS か localhost の安全なコンテキストでだけ使える)。letterSpacing、filter は任意機能とし、非対応環境では無視または代替描画する。
9. 書き手(エンコーダ)の責務
- レイアウトは完了させてから出力する(BDF に再レイアウトはない)。
- 同一内容の描画は同じバイト列になるよう決定的に出力する(浮動小数の丸め、文字列表の順序、リソースの並びを正規化)。共有はハッシュ一致で自動的に起きる。
- フォントはサブセット化して WOFF2 で埋め込む。埋め込めない場合は
systemかアウトライン化。 FILL_TEXTのadvanceを必ず埋める。- シートは Tile 原点相対で出力し、またがるオブジェクトは共有 Object +
USEにする。 - Part の推奨順(§3.3)に従って並べる。
- パスワードで保護された入力は、同じパスワードで暗号化して出力する(§3.5)。
付録 A. BLEND 値
0=source-over 1=multiply 2=screen 3=overlay 4=darken 5=lighten 6=color-dodge 7=color-burn 8=hard-light 9=soft-light 10=difference 11=exclusion 12=hue 13=saturation 14=color 15=luminosity 16=destination-over 17=destination-in 18=destination-out 19=source-in 20=source-out 21=source-atop 22=destination-atop 23=xor 24=copy 25=lighter
付録 B. 命令列の例
幅 960×540 のスライド。マスター(Object #0)を敷き、タイトルを描く。
50 00 USE obj[0]
1A 00 00 00 C0 41 FONT font[0], 24.0
10 20 20 20 FF FILL_COLOR #202020FF
30 00 00 00 A0 42 00 00 F0 42 00 00 34 43
FILL_TEXT str[0] x=80 y=120 advance=180
付録 C. 検討したが採用しなかったもの
- ZIP コンテナ: ツールが豊富だが、セントラルディレクトリが末尾にあり先頭からのストリーミング描画と相性が悪い。ローカルヘッダの重複や ZIP64 も不要な複雑さ。single 形式は「ディレクトリ先頭の単純な連結」で十分。
- ファイル全体の 1 本圧縮: Range・Tile 単位取得ができなくなる。
- 可変長整数の固定小数点座標: f32 より小さくなりうるが、デコードが複雑になる。deflate で差はかなり縮む。必要なら opset 2 で列指向(opcode 列 / f32 列 / 参照列を分離)を検討する。
- テキスト専用 Part: 命令列を別バックエンドで走査すれば得られるので不要。
- ファイル全体の暗号化: 配布層に任せれば仕様は変えずに済むが、Range 取得・split 形式・CDN への配置という BDF の中心の性質を失う。
- Part 名を鍵付き HMAC にする: 平文のハッシュは隠せるが、名前の検証にも single ↔ split の組み替えにも鍵が要る。格納バイト列のハッシュで足りる。
- Argon2 などメモリハードな鍵導出: 総当たりには強いが WebCrypto にないので、ビューアに wasm が要る。PBKDF2 は WebCrypto でそのまま使える。