技術解説¶
typeset がどのように日本語を組んでいるかを、上から下へ説明します。型ごとの詳細は C++ ガイド と C++ リファレンス にあります。
全体の流れ¶
Flow(ブロックの列)
│ page::FlowLayouter — ページマスタ(判型・余白・段)に沿って段へ流し込む
▼
段落 → inl::ParagraphLayouter — 1 段落を行の列(LineBox)にする
│ shaper(HarfBuzz) → item_builder(Box / Glue / Penalty) → line_breaker(Greedy / Knuth–Plass)
▼
dl::DisplayList(GlyphRun / Path / Rect / Image / Group / Bookmark)
│
▼
backend: RasterRenderer(PNG) / PdfWriter / SvgWriter
1 つのエンジンで縦書きと横書き¶
行内は論理座標で組みます。inline_ は行頭からの送り、block は行の中心線からのずれ(縦組みは右が正、横組みは下が正)。
行が確定してから emitParagraph の 1 箇所で書字方向に応じて物理座標(ページ左上原点・y 下向き・pt)に写します。
ルビ・圏点の付く側は annotationSide()(縦: 右、横: 上)で決まり、縦組み中の欧文や数字は UAX #50 に従って
横倒し(−90°)になります。ブロック軸の基準は「em box の中心=行の中心線」で、横組みのベースラインは第一候補フォントの
ascender / descender から決めるため、フォールバック先のフォントも同じベースラインに乗ります。
和文を Box / Glue / Penalty で解く¶
「1 文字 1 マス」で並べるのではなく、TeX と同じ箱・のり・ペナルティの列を作って行分割を解きます。
- 文字は JLReq 附属書 A の文字クラス(始め括弧・終わり括弧・句点・読点・中点・和字・欧文…)に分類され、 隣り合うクラスの組み合わせから JLReq 表 3 のアキ量(自然値・伸び・縮み)が Glue になる
- 禁則(行頭禁則・行末禁則・分離禁止)は Glue の直前の Penalty(∞)。ぶら下げは幅が負の Penalty (句読点が行末で版面の外へ出る)。約物の詰めは仮想ボディ(半角の箱)で表す
- 追い出し(字間を空けて行末を揃える)は和字間の Glue に小さな伸びを持たせることで実現する。 揃えない段落(ragged)では縮みを使わずに収まりを判定する(TeX の ragged-right と同じ)
- 欧文は UAX #14 の分割機会(libunibreak)で切る。
BreakOptions::hyphenationに辞書(TeX の Liang パターン)を渡すと 単語の中でも切ってハイフンを出す。辞書が無くても本文中のソフトハイフン U+00AD は常に分割位置 - 行長は
LineShapeProviderから行ごとに取るので(TeX の\parshape)、回り込み・ラベル付き段落・段の形が そのまま行分割に反映される。Knuth–Plass は行番号をノードに持ち、行ごとに違う行長でも最適解を出す
注記(ルビなど)¶
ルビ・圏点・縦中横・割注・字取りは、クラスタ(HarfBuzz の単位)に付随するグリフか、クラスタを置き換える箱として Box に載せます。グループルビはルビの方が長ければ隣の字に掛け(掛けられる字種のみ)、それでも足りなければ親文字列を 広げます。熟語ルビは JLReq 3.3.8 の 3 段階(各字に収まる/熟語に収まる/グループにする)。行頭・行末では ルビを行の外に出しません。行内の画像や数式(外部オブジェクト)も同じ「箱」で、横組みでは本文のベースラインに揃え、 行より高ければその行の前後だけ行送りを広げます(TeX の lineskip 相当)。
ページ組版¶
FlowLayouter は Flow のブロックを順に段(Region)へ置きます。段落は「段に入る行数」だけ組み、残りは
次の段へ文字位置から続けます。orphans / widows、keepWithNext(見出しが段末に残ったら巻き取って次の段へ。
罫線やスペーサーを挟んでも効く)、keepTogether、改ページ・改段、背景と余白を持ちます。
- 段組と段抜き: 段組ページの途中で全幅のブロックを置くときと、最終ページの段の高さを揃えるときは、 「ページの再開点から段の長さを縮めて組み直す」試行を繰り返して段の高さを揃える
- 表: owner グリッド(colspan / rowspan)で罫線を決め、ヘッダ行はページをまたいで繰り返す。 段より高い行(や rowspan の塊)はセルごとに続きから流して分ける
- 回り込み: 画像の排除領域を段が持ち、
RegionLineShapeが行ごとに「排除領域を避けた最大区間」を行長として返す。 行内オブジェクトで行送りが広がった行の後ろでも、実際の位置で判定する - 脚注: 段落を組む前に記号を番号にし、その断片に載る注の高さを段末に確保して、ページを閉じるときに罫と注を描く
- 多パス: 見出し番号・図表番号・式番号・相互参照(
{ref:}{page:})・目次・索引・総ページ数は、 前のパスで集めた情報を次のパスに渡して 2〜3 回組み、安定したら止める
表示リストと 3 つの出力先¶
組版結果は描画命令の列(表示リスト)で、backend は組版の知識を持ちません。グリフ固有の変形(回転・平体長体・斜体・ フェイクボールド)の行列の組み立ては 1 箇所で、どの出力先でも同じ行列を使います。
- PDF: Identity-H でグリフ ID を直接書く(縦組みでも Identity-V は使わない)。hb-subset でサブセット化、Flate 圧縮、 画像 XObject(JPEG は素通し)、ToUnicode、しおり(アウトライン)。フォントは TTC も face 番号で扱い、 OS/2 の fsType(埋め込み許可)を見て、許可されないものは埋め込まず警告する
- SVG: グリフのアウトラインを
<defs>に置いて<use>で参照。画像は data URI - ラスタ: glyphware のカバレッジマスクを自前で合成。パスは自前のスキャンライン AA
3 つの一致は、Inkscape で SVG / PDF(poppler)を PNG に描き直して ImageMagick で比較して確認しています。
外部オブジェクト¶
数式・グラフ・図など本体が知らないものは、ObjectRegistry に登録したハンドラが「箱の大きさ・ベースライン・描画命令」を
返す形で差し込みます。ハンドラは C++ / Python の関数か、要求(JSON)を受けて SVG を標準出力に書く外部コマンド。
SVG サブセットの読み込み(path・基本図形・defs/use・transform・塗り線)を本体が持つので、MathJax / dvisvgm / Typst /
matplotlib などの出力をそのまま貼れます。MicroTeX(LaTeX 数式)は描画抽象を実装したサンプルハンドラで、
数式のグリフがフォントとして PDF に埋め込まれます。
フォント¶
フォント層は glyphware(FreeType + HarfBuzz)。シェイピング用の HarfBuzz フォントはフォントファイルのバイト列から
自前に作り、スケールを unitsPerEm にしてフォントユニットで位置を受け取ります(ピクセルサイズに縛られない)。
FontSet は family の列と文字カバレッジでフォールバックを解決します。minikin と ICU は使いません
(UAX #14 は libunibreak、双方向は glyphware 内の SheenBidi、スクリプト判定は HarfBuzz)。