章 4 · 部 II · 技法
ハイフネーションと両端そろえ
PostextがKnuth-Plassアルゴリズムを使って行を分割し、テキストを両端そろえで組み、語間を制御するしくみ
かんたんな説明
このページでは、Postextが1行の中に単語をどう並べるかを説明します。段落の左右の端をまっすぐそろえると、単語と単語のすき間が広がりすぎて見苦しくなることがあります。Postextは段落全体を見てから各行の終わりを決めるので、すき間が均一に保たれます。長い単語を正しい位置でハイフンで区切ることもできます。すき間をどこまで広げてよいかを決めたり、まだ間延びして見える行に印を付けたりできます。段の終わりの高さをそろえるしくみも紹介します。
アマチュアの組版とプロの組版の差は、語と語のあいだのスペースに表れます。
手もとにあるペーパーバックの小説を開いてみてください。本文は両端そろえで、どの段落も左右の端がきれいにそろっています。しかし、よく見てください。語間はどの行もほぼ均一です。ページを縦に流れる白い川(リバー)もなければ、広大な空白を挟んで2語だけがぽつんと取り残された行もありません。これを実現するのは見た目よりはるかに難しく、組版の技術開発でほかのどの問題よりも多くの労力が注がれてきた問題です。
Postextはこれを、TeXが1981年から使っているのと同じアルゴリズム、Knuth-Plassの最適行分割で解きます。TeX品質のハイフネーションパターン、設定できる語間の上限と下限、問題のある行を見つけるための視覚的なデバッグ機能と組み合わせることで、エンジンは出版物の水準を満たす両端そろえのテキストを組みます。
#問題
textAlign: 'justify'でテキストを組むと、最終行以外のすべての行は、段の幅にぴったり合うように伸ばすか縮めるかしなければなりません。エンジンは、内容の自然な幅と段の幅との差を、その行の語間に配分します(最終行は自然な幅のまま行末不ぞろいで残します。例外が1つあり、最終行で説明します)。
行に語が多ければ、各スペースが吸収する調整はわずかで、読者には見えません。しかし語の少ない行(長い語のために早めに改行せざるをえなかった行)では、各スペースを大きく伸ばす必要があります。その結果がゆるい行です。語間が広すぎて読むリズムが崩れ、見苦しいすき間ができた行のことです。
逆の問題もあります。エンジンが1行に語を詰め込みすぎると、スペースが自然な幅より狭くなり、語どうしが窮屈に感じられるきつい行になります。
素朴な行分割アルゴリズム(CSSが使っている種類のもの)は、1行ずつ判断します。現在の行にできるだけ多くの語を詰め、改行して次へ進みます。このfirst-fitの貪欲法には根本的な弱点があります。先が見えないことです。5行目にとって最適に見える判断が、6行目にひどい改行を強いることがあります。アルゴリズムが6行目に着いたときにはもう手遅れで、5行目はすでに確定しています。
#Knuth-Plass:段落全体を見渡す
Donald KnuthとMichael Plassが1981年に発表したKnuth-Plassアルゴリズムは、まったく異なる方法をとります。1行ずつ分割するのではなく、段落全体を分割するあらゆる方法を検討し、全行の「悪さ」(badness)の合計が最小になる組み合わせを選びます。TeXを動かしているのはこのアルゴリズムで、TeXで組んだ文書が40年以上にわたって両端そろえの手本とされてきた理由もここにあります。
#ボックス・グルー・ペナルティのモデル
Knuth-Plassは語とスペースという単位では考えません。テキストを次の3つの基本要素の列として扱います。
| 基本要素 | 表すもの | ふるまい |
|---|---|---|
| ボックス | 語またはテキストの断片 | 固定の幅をもちます。伸ばすことも縮めることも、分割することもできません。 |
| グルー | 語間のスペース | 自然な幅、伸びの量、縮みの量をもちます。エンジンは行を埋めるため、この範囲内でグルーを調整できます。 |
| ペナルティ | 分割できる可能性のある位置 | コストをもちます。ペナルティが低ければ安い分割、高ければ高くつく分割です。フラグ付きのペナルティはハイフネーション位置を表します(使うと目に見えるハイフンが加わります)。 |
段落は次のような列になります。
[box "The"] [glue] [box "quick"] [glue] [box "brown"] [glue] [box "fox"]
[penalty -∞] ← forced break at paragraph end
ハイフネーションが有効なとき、長い語はペナルティで区切られた断片に分かれます。
[box "ty"] [penalty 50, flagged] [box "pog"] [penalty 50, flagged] [box "raphy"]
フラグ付きのペナルティはそれぞれコスト50をもちます。ただではありませんが、ゆるい行を作るよりは安くつきます。
#最適解の求め方
このアルゴリズムは動的計画法を使います。新しい行の始まりになりうる分割候補であるアクティブノードの集合を保ち、各アクティブノードから可能なすべての分割を評価します。分割の候補ごとに、次の値を計算します。
-
調整比(r):この行のグルーをどれだけ伸ばすか縮めるか。
r = 0は行がぴったり収まることを、r > 0は伸ばすこと(ゆるい)を、r < 0は縮めること(きつい)を意味します。 -
バッドネス(badness):間隔の不ぞろいの度合いで、
100 × |r|³で計算します。3乗で増えるため、少しゆるい行は許容されますが、とてもゆるい行には重い罰がかかります。r = 2の行のバッドネスは800、r = 0.5の行は12です。 -
適合クラス(fitness class):各行は、きつい(
r < -0.5)、標準(-0.5 ≤ r < 0.5)、ゆるい(0.5 ≤ r < 1.0)、とてもゆるい(r ≥ 1.0)に分類されます。きつい行ととてもゆるい行が隣り合うと落差が目立つため、クラスが2段階以上離れた隣接行にはペナルティがかかります。 -
デメリット(demerits):ここで分割したときのコストの合計。TeXにならい、バッドネスと分割位置のペナルティは2乗の式で組み合わせます。ペナルティが0以上なら
demerits = (1 + badness + penalty)²、負のペナルティ(望ましい分割)ではその2乗を差し引いて(1 + badness)² − penalty²とします。そのうえで、固定のデメリットを2つ加えます。- 連続ハイフンのデメリット(既定値3000):ハイフンで終わる行が2行続くと加わります。ハイフンが縦に重なると目障りだからです。
- 適合クラスのデメリット(既定値100):この行の適合クラスが前の行から2段階以上離れているときに加わります。
後述するラント(最終行に残った短い一語)のペナルティもこの式に入り、追加のバッドネスとして加わります。オーファンとウィドウのペナルティは後の段階、つまり段落が段をまたいで分かれるときに働きます。
アルゴリズムは、段落全体でデメリットの合計がもっとも小さい分割の並びを選びます。貪欲法に対する利点はこれに尽きます。
アルゴリズムはアクティブノードをさかのぼり、デメリットの合計が最小になる経路を見つけます。これが、段落全体にとって大域的に最適な分割位置の組です。
オーファン、ウィドウ、ラントのペナルティ
Postextは標準のコストモデルを拡張し、段落や段の終わりが見苦しくなるレイアウトからエンジンを遠ざける、3つの編集上のペナルティを加えています。
- オーファンのペナルティは、分割の候補が次の段の先頭に残す行が
orphanMinLines行より少なくなるときにかかります。orphanPenaltyの既定値は1000です。 - ウィドウのペナルティは、分割によって現在の段の末尾に残る行が
widowMinLines行より少なくなるときにかかります。widowPenaltyの既定値は1000です。 - ラントのペナルティは、段落の最終行がおよそ
runtMinCharacters × normalSpaceWidthピクセルより短くなるときにかかります。これは文字数ではなく語間スペースいくつ分かで表した長さで、既定値の20はおよそ8〜12文字の最終行にあたります。runtPenaltyの既定値は1000で、どのラントにも同じ値がかかります。gradedRuntPenaltyを使うと、行の不足分に応じてruntPenalty × (1 − width / threshold)に縮められます。そのため2語で終わる行は1語で終わる行より安くつき、上の行から1語を送れる場合はそちらが選ばれます。オーファンとウィドウ(分割のデメリットに線形に加算されます)とは違い、ラントはKnuth–Plassの2乗の式の中に等価なバッドネスとして入ります。そのため、行のバッドネス(10000で頭打ちになります)と同じ尺度で競い合い、それに埋もれることがありません。
ペナルティを課してもラントが残るとき、つまりほかの分割がどれも成り立たないときは、エンジンは植字工が手作業でしてきたことに頼ります。段落を1行短く組むのです(tightenRunts)。すべての行の語間を詰めますが、minWordSpacingを下回ることはありません。それだけでは取り残された語を収めきれないときは、わずかな負のトラッキングを加えます。うまくいく最小の量を選び、maxRuntTracking(1/1000 em単位)を超えることはありません。短くした組み方もmaxWordSpacingを守らなければなりません。語を1行少なく収めるとほとんどの行は詰まりますが、分割位置が動いて、別の行を大きく伸ばす必要が出ることがあります。行をmaxWordSpacingより広げてしまう組み方や、段落にすでにもっとゆるい行がある場合にその行より広げてしまう組み方は解決にならず、ラントはそのまま残ります。数えるのは両端そろえのまま残る行だけです。通常のスペースの3倍より広がるために段落が行末不ぞろいで組む行(行分割で埋めきれない行を参照)は基準を引き上げず、短くした組み方で行末不ぞろいの行が元の段落より増えてもいけません。行末不ぞろいの行を、その基準を超える両端そろえの行と引き換えにすることもしません。その場合、段落は行末不ぞろいの行もラントもそのまま残します。段末そろえも逆方向で同じ規則に従います。短い段を埋めるために段落を1行長く組むとき、増えた行がラントで終わるなら、その段落には手を付けません。段を埋めるために1音節を取り残す理由はないからです。
ラントのペナルティは行分割の内部で候補ノードのデメリットに加わるため、ソルバーは少しゆるい行と引き換えに最終行を長くすることができます。ただしそれはmaxWordSpacingの範囲内に限ります。この上限を超えて伸びた行には、どのラントやハイフンのペナルティよりも大きな追加のバッドネスがかかるため、行分割は上限を超える語間よりも、短い最終行やハイフンで分けた最終語を選びます。オーファンとウィドウのペナルティは、段落が段の境界をまたぐときに走る別の最適化に入ります。分割の候補ごとに、余り(slack)、オーファン、ウィドウのデメリットを合計して採点し、もっとも安い分割を採用します。そのため、エンジンは両方を避けられる分割を可能なかぎり自然に選びます。この兼ね合いはorphanPenalty、widowPenalty、runtPenaltyで調整します。どれかを0にすると、その規則は完全に無効になります。リスト項目にはavoidOrphansInLists、avoidWidowsInLists、avoidRuntsInListsで個別に適用します(既定値はいずれもtrue)。
#なぜ重要なのか
差は目に見えます。貪欲法のレイアウトでは、ある行だけが隣の行より目立ってゆるい段落が見つかります。よく見ると、前の行が1語多く取りすぎたせいだとわかります。Knuth-Plassは結果を見通せるので、現在の行を少し悪くしてでも次の行をずっとよくする選択をし、これを避けます。
#Postextの実装
Postextは、コアパッケージのknuthPlass/モジュールにKnuth-Plassアルゴリズムを完全に実装しています。動的計画法の中核(アクティブノード、デメリット、トレースバック)に加え、テキストをボックス・グルー・ペナルティのモデルに変換する2つのアダプター経路があります。
- プレーンテキストの経路:DOMを使わないテキスト計測に
@chenglou/pretextを使います。Pretextがセグメントの幅と任意ハイフン(discretionary hyphen)の幅を返し、PostextがそれをKPの要素に変換します。 - リッチテキストの経路:Canvasによる計測で太字やイタリックのスパンを扱います。スタイル付きのトークンはそれぞれ1つ以上のボックスになり、ハイフネーションの分割位置はペナルティとして挿入されます。
どちらの経路も行ごとにjustifiedSpaceRatioを計算します。実際のスペース幅を自然なスペース幅で割った値で、後述するゆるい行のデバッグ機能に使われます。この比は最終行以外についてだけ計算します。最終行の組み方は最終行で説明します。
行分割のまわりでは、テキストの計測がmeasure/モジュールにあります。プレーンとリッチの計測経路、Canvasのグリフメトリクス、フォント文字列の処理を含み、明示的な計測キャッシュの背後に置かれています。clearMeasurementCacheは下層のテキスト幅キャッシュもクリアするため、Webフォントの読み込みが終わった後も計測は正確に保たれます。ソーステキストをブロックとインラインのスパンに変換する段階である構文解析は、parse/モジュールにあります。これらのモジュールのファイル構成はアーキテクチャーのページを参照してください。
行分割は新しい種類のコンテンツとも関わります。リソースのフロートは段やページの上端または下端の帯を占め、オーファン、ウィドウ、余りのペナルティが反応する段の範囲を短くします。キャプションと表のセルは、キャプションと表のフォントを使い、折り返したリッチテキストとして計測します。別行立ての数式の行は中央にそろえた分割できない単位で、両端そろえも分割もしません。詳しくは文書形式 › リソースと設定 › 表スタイル/キャプションスタイルを参照してください。
フォールバックの動作:Knuth-Plassが有効な分割を1つも出せない場合(極端に狭い段やとても長い語で起こりえます)、エンジンはPretextの貪欲なlayoutNextLine()に切り替えます。これで、レイアウトは必ず完了します。
#ハイフネーション
ハイフネーションと両端そろえは切り離せません。ハイフネーションがなければ、エンジンがゆるい行を避ける手段は語を次の行へ送ることしかなく、たいていは問題をずらすだけです。ハイフネーションは検討できる分割位置を大幅に増やし、両端そろえのテキストの品質を大きく高めます。
#TeX品質のパターン
PostextはハイフネーションにHypher(hypher v0.2.5)を使い、その基盤はTeX/Liangのハイフネーションパターンです。TeXが1983年から使っているのと同じパターンで、Frank Liangのパターン生成アルゴリズムが大規模な語のコーパスから導いた音節境界の規則を、コンパクトに表したものです。
パターンは番号付きの規則の集まりで、語に重ねると、分割できる位置(奇数)と禁止される位置(偶数)を示します。各言語のパターンファイルにあるleftminとrightminのパラメーターは、分割位置の前後に最低限必要な文字数を保証します。英語(en-us)ではそれぞれ通常2と3で、ハイフンの前に少なくとも2文字、後に3文字が必要です。
#対応するロケール
| ロケールコード | 言語 |
|---|---|
'en-us' | 英語(米国) |
'es' | スペイン語 |
'fr' | フランス語 |
'de' | ドイツ語 |
'it' | イタリア語 |
'pt' | ポルトガル語 |
'ca' | カタルーニャ語 |
'nl' | オランダ語 |
ロケールごとに専用のパターンセットを読み込みます。Hypherのインスタンスは必要になった時点で作られ、キャッシュされます。あるロケールを初めて呼び出すときだけ初期化のコストがかかり、2回目以降は即座に返ります。地域付きのロケールはその言語のパターンを使います('es-ES'は'es'で、'en-GB'は'en-us'でハイフネーションします)。ここにパターンのない言語はen-usにフォールバックし、エンジンはコンソールに一度だけ警告を出します(設定リファレンスのハイフネーションを参照)。中国語・日本語・韓国語は例外で、ハイフネーションなしで、警告も出さずに組まれ、行は文字と文字のあいだで分割されます(後述の中国語・日本語・韓国語を参照)。
カタルーニャ語は、カタルーニャ学術院(Institut d'Estudis Catalans)の規則(『Llibre d'estil』第VI章)に従います。これはpostext 1.14から適用されています。
- 分割位置の両側には少なくとも2文字を残します(
ter-ra、cai-xa、plu-ja)。 - ela geminadaは2つのlのあいだで分割し、ハイフンが中点の代わりになります。il·lusióは
il-|lusióになります。行は中点を除いて計測するので、両端そろえとKnuth–Plassは実際の幅を扱います。 - 母音接続(hiatus)の2つの母音は分けません(
cièn-cia、ca-mions)。母音にはさまれたiやuは子音として扱い、次の音節の頭になります(fe-ia、ve-ient)。 - アポストロフィの直後では分割しません(
s'ha-via。s'-haviaにはしません)。 - よく使われる接頭辞付きの語は、接頭辞のところで分割します(
vos-altres、cel-obert、ben-estar)。 - 接語代名詞の付いた語を、それ自体のハイフンの位置(
portar-|lo)以外では分けずに保つには、IECの推奨どおりhyphenation.compoundsをfalseにします。
#Hypherを使う理由(独自アルゴリズムではなく)
当初のPostextのハイフネーションは、母音にもとづく独自のヒューリスティックを使っていました。母音のまとまり、よくある接頭辞(over-、under-、inter-)、よくある接尾辞(-tion、-ment、-sion)を見つけて音節の境界を検出するものです。単純で高速でしたが、根本的な限界がありました。
| 観点 | 独自のヒューリスティック | Hypher(TeXのパターン) |
|---|---|---|
| 精度 | よく使われる語には十分ですが、珍しい語では当てになりません。母音の検出では、正しい分割位置の多くを見落とし、誤った分割位置を作ります。 | ほぼ完璧です。パターンは大規模なコーパスから生成され、40年以上にわたって改良されてきました。 |
| 対応言語 | 言語ごとに母音の集合、接頭辞、接尾辞を手作業で定義する必要があり、手間がかかり誤りも起きやすいものでした。 | 50以上の言語のパターンファイルがあり、TeXコミュニティーが保守しています。言語を加えるにはimportを1つ追加するだけです。 |
| 業界標準 | 認められた標準ではありません。ツールもコミュニティーの支援もありません。 | TeX、LibreOffice、Firefox、Chrome、そして事実上すべてのプロ向け組版システムと同じパターンです。 |
| 保守 | 特殊なケースはどれも、手で直すべきバグになります。 | パターンファイルはコミュニティーが保守しています。バグの修正は上流から届きます。 |
| バンドルサイズ | 約170行、依存関係なし。 | Hypherの本体は約3 KB。言語ごとのパターンファイルで20〜80 KB(gzip圧縮後5〜20 KB)増えます。同梱の8つのロケールすべてで約300 KB(gzip圧縮後約80 KB)増えます。 |
| 性能 | 非常に高速(単純な文字列の走査)。 | 高速(1文字ごとのトライ木の検索)。実際には無視できる程度で、ハイフネーションがボトルネックになることはありません。 |
トレードオフは明らかです。バンドルが大きくなる代わりに、正確さが大幅に向上し、保守の負担はなくなります。出版品質の出力を目指す組版エンジンでは、正確さが優先します。印刷された本のハイフネーションの誤り1つは、パターンの数キロバイトよりも高くつきます。
#ハイフネーションとKnuth-Plassの連携
テキストがKnuth-Plassアルゴリズムに入る前に、エンジンはHypherで前処理し、分割できるすべての位置にソフトハイフン(Unicodeの\u00AD)を挿入します。この見えない文字は、コスト50でflagged: trueのKPのペナルティに対応づけられます。
アルゴリズムは、ハイフネーションによる分割を、自然な語の境界(グルーが分割を許す位置)と並ぶ選択肢の1つとして評価します。ハイフンを使ったほうが行をゆるいままにするよりデメリットの合計が小さければハイフンを選び、そうでなければ語をそのまま残します。
連続ハイフンのデメリット(既定値3000)により、アルゴリズムは隣り合う2行にハイフンを置くことを強く避けます。これは事実上すべてのスタイルガイドが求める組版の慣習です。
段やページの最終行にハイフンがあると、読者は語の途中で次の段へ移ることになります。bodyText.hyphenateAcrossColumns: falseにすると、そうした行にハイフンを置きません。段を閉じる行がハイフンで終わるとき、その位置のハイフンにラントと同じ値をつけて段落を分割し直すので、上の行の語間がmaxWordSpacingとminWordSpacingの範囲で差を吸収できるかぎり、その行は語の切れ目で終わります。吸収できなければハイフンは残ります。対象は段落の最初の段の切れ目と、それ以降の切れ目のうち、段がいっぱいになって終わる位置にあるものです。それ以外の位置に来る後の切れ目(ウィドウを避けるために短くした段、高さをそろえて切った締めくくりの帯など)は、その段から2度目の分割し直しを受けます。前の段ですでに組んだ行は分割位置を保ち、段落の残りだけを分割し直します。そのため、ハイフンが残るのは、範囲内のどの組み方でも避けられない場所だけで、実際には段落の最初の段の切れ目です。行分割は、ある行にいたる経路を、それが守る最後の行までしか区別しないため、やり直しのコストは最初の計測とほぼ同じで、分割し直す段落ごとに計測が1回増えるだけです。
既定では、ハイフネーションはbodyText.hyphenation.enabledがtrueで、かつbodyText.textAlignが'justify'のときにだけ適用されます。行末不ぞろいの端は、もともと不ぞろいであるべきものだからです。行末不ぞろいのテキストもbodyText.hyphenation.ragged: trueでハイフネーションできます。行末不ぞろいの行にはならすべき語間がないので、代わりにハイフネーションゾーンが判断します。収まらない語を分割するのは、丸ごと次の行へ送るとhyphenation.zone(既定値3 em)より広いすき間が残る場合だけです。Knuth-Plassは行末不ぞろいの本文も組みます(bodyText.optimalRagged、既定でオン)。語間は幅を保ち、各行は行長に足りない分だけコストを払い、どの音節をとれるかはゾーンが決めます。音節の切れ目で終わる行が2行続くと、連続ハイフンのデメリットがかかります。1行ずつ組む場合(optimalRagged: false)は、音節の切れ目で終わる行が3行以上続くことはありません。行末不ぞろいのテキストを参照してください。
この設定にかかわらず、分割できる機会が2つあります。2つの文字にはさまれたハードハイフンは、enseñanza-aprendizaje、físico-químicaのように、行を終えてよい位置です。語がもともとハイフンをもっているので、何も加わりません。bodyText.breakAfterHyphens(既定でオン)のとき、Knuth-Plassはどの段落でもこの位置を音節と同じ値で選びます。インライン書式のない両端そろえの段落では、ハイフンの両側に2文字以上あるときに限るので、e-mailのe-で行が終わることはありません。オフにすると、postext 1.4までと同じく、インライン書式のない両端そろえの段落はこうしたハイフンの直後では分割しませんが、同じ段落でもどこかにイタリックの語が1つあれば分割します。postext 1.5より前に保存した本は、オフとして読み込まれます。行はテキスト自身のハイフンで終わり、それをhyphenatedと並べてhardHyphenとして記録します。そのため、追加されたハイフンを取り除いて読む処理(柱、Sandboxのキャレット)でも、このハイフンは残ります。語のあいだにアキなしで置いたemダッシュやenダッシュも、say—that’s、riddles.—I、Hamburg–Berlinのように、bodyText.breakAfterDashes(既定でオン)のとき、どちらの行分割でも行を終えてよい位置です。行はダッシュで終わり、何も加わらず、コストもかかりません。あるスタイルの連なりを終えるダッシュが、別のスタイルの語の前にある場合(see—*and*)も同様です。このとき行はhyphenatedになり、hardHyphenは付きません。ただし、挿入句や会話の行を始めるダッシュ(—dijo、said "—Hola。語を閉じる引用符はダッシュの前に置けます。"no"—andやドイツ語の„nein“—undがその例です)の後、句読点の前、引用符や括弧の前(thinking—" and、says—“no”)、ダッシュの連続の中、数の範囲の中(1914–1918)では分割しません。そして、行長全体より広い語(狭い表のセル、狭い段の長い複合語)が行長からはみ出すことはありません。エンジンは収まる最後の音節でハイフンを付けて分け、辞書がそこに音節を示さない場合は収まる最後の文字で分けます。語がもっているハイフンのそばで切る場合は、そのハイフンの後で切ってハイフンを加えないので、語にハイフンが2つ並ぶことはありません。語の残りは自分の分割位置を保ちます。複合語は引き続きハイフンの位置で、Webアドレスは区切りの位置で分割されます。postext 1.4までは、残りを辞書の音節で分けていました。インライン書式のない段落は、ハイフネーションしない場合、収まる最後の文字でハイフンなしに切られます。こうした語を含む分割の組はKnuth-Plassにとって成り立たないので、こうした語を含む両端そろえの段落は1行ずつ組まれます。
#複合語
ハイフンで結ばれた複合語(after-dinner、teórico-práctico)は、2種類の位置で分割できます。それ自体のハイフンの後と、各部分の辞書上の音節(af-ter-dinner)です。TeXや『The Chicago Manual of Style』は各部分を分けずに保ち、複合語はハイフンの位置でだけ分割します。bodyText.hyphenation.compounds: falseも同じ動作をします。2つの文字のあいだにハイフンをもつ語には辞書を適用しないので、そのハイフンの後でだけ分割します。語の中に入力したソフトハイフンでは引き続き分割でき、行全体より広い複合語は必要な位置で分けられます。既定値のtrueでは各部分の分割を続けるため、両端そろえの狭い段で行を埋める手段が増えます。
ポルトガル語の正書法では、行末で分けた複合語のハイフンを次の行の頭にも繰り返すよう求めています(vencer- · -se)。スペイン王立アカデミーの規則も2010年から同じです(léxico- · -semántico)。こうすると、読者はそのハイフンが語の一部だとわかります。bodyText.repeatHyphen: trueでこれを行います。繰り返したハイフンはその行に含めて計測するので、行分割はその分の場所を空けます。描画もほかのテキストと同じです。PDFでは、そのハイフンを除く置換テキストをもたせるため、PDFからコピーしたり抽出したりしたテキストでは語が1回だけ読まれます(vencer-se)。行はこれをrepeatedHyphenとして記録し、そのplainStartとsourceStartはハイフンの後を指します。そのため、ソースの対応づけ、リンク、柱は書かれたとおりの語を読みます。Webアドレスには付きません。本文、見出し、リスト、引用、囲みに適用されます。このとき、書式のない段落でも複合語を含むものは、書式付きテキストを組む行分割で分割されます。
#行を分割しない箇所
次の箇所は、どちらの行分割でも分割の機会になりません。
- ノーブレークスペース。U+00A0、幅の狭いノーブレークスペースU+202F、数字幅のスペースU+2007は、両側の語を結びつけます。数とその単位、ページ参照、位取りの区切りなどです。キャプション、表のセル、囲み、デザインのテキスト(柱、章扉)でも同じです。それぞれ計測したとおりの幅を保ち、両端そろえで伸びるのは語間のスペースだけです。U+202FやU+2007のグリフをもたない書体では、ブラウザーが与える幅(語間スペースの半分と数字1つ分)を、PDFを含むすべての出力で使います。U+00A0は事実上どの書体にもあります。こうして結びつけたまとまりが行全体より広い場合は例外で、インライン書式の有無にかかわらず、語の内側で切る代わりに最後のノーブレークスペースで分割します。単語結合子(word joiner)U+2060は場所をとらずに結びつけ、中国語のテキストでも働きます。語義番号
**①**の後に置くと、その番号は次の文字と同じ行にとどまります。U+FEFF(ゼロ幅ノーブレークスペース)も同じ働きをしますが、この用途のための文字は単語結合子です。 - 接している文字列。太字やイタリックの語とその後の句読点(
**osmosis**.)、インライン参照を囲む括弧((:ref{id="fig-3"}))、2つのランに分けて組まれた語など、あいだにスペースのないテキストは、1語であるかのように1つの単位になります。そうした単位が行の残りに収まらないときは、丸ごと次の行へ送られます。行頭に来てもなお収まらないときは、句読点が語の後半と一緒に下りるよう、最後の語を音節でハイフネーションします。分割できる音節がない単位だけが、句読点を次の行の頭に残します。 - 連結する文字体系の語の内側。アラビア文字の語(シリア文字、ンコ文字、モンゴル文字の語も)は、語中に入力したソフトハイフンがあってもハイフネーションせず、文字と文字のあいだで切ることもありません。文字どうしが連結しているため、1片だけを組むと別の字形と別の幅になるからです。アラビア語などの右から左へ書く言語では、ハイフネーションは既定でオフです。オンにした場合(アラビア語の本のラテン文字の語のため、あるいはアラビア語を引用する英語の本で)、辞書はこうした語を飛ばします。2つのスタイルで組んだ語(
كتا**ب**)は1語のままで、全体として計測します。行全体より広い語は行長からはみ出し、ビルドがunbreakableWordOverflowのコンテンツ警告を出します。
postext 1.4までは、インライン書式や:refを含む段落はノーブレークスペースで分割されることがあり、行末不ぞろいの行が参照の前の(で終わったり、太字の語の後のピリオドで始まったりすることがありました。
#語間の上限と下限
グルーのモデルは、語間のスペースがどこまで伸び縮みできるかの明示的な範囲をエンジンに与えます。この範囲はbodyTextの2つの設定プロパティで制御します。
| プロパティ | 既定値 | 説明 |
|---|---|---|
maxWordSpacing | 2 | 語間の上限。通常のスペース幅に対する倍率で指定します。既定値では、スペースは自然な幅の200%まで伸びます。 |
minWordSpacing | 0.6 | 語間の下限。通常のスペース幅に対する倍率で指定します。既定値では、スペースは自然な幅の60%まで縮みます。 |
これらの倍率は、Knuth-Plassモデルのグルーのstretchとshrinkの値に直接変換されます。
stretchPerSpace = normalSpaceWidth × (maxWordSpacing - 1)
shrinkPerSpace = normalSpaceWidth × (1 - minWordSpacing)
既定値(2 / 0.6)で、通常のスペース幅が4 pxの場合:
- 各スペースは4 px伸びます(4 pxから8 pxへ)
- 各スペースは1.6 px縮みます(4 pxから2.4 pxへ)
範囲を狭くすると(例:maxWordSpacing: 1.2)語間はそろいますが、アルゴリズムの選択の余地が減り、ハイフネーションが増えたり、極端な場合ははみ出したりします。範囲を広くすると(例:maxWordSpacing: 2.5)アルゴリズムの自由度は増しますが、一部の行で語間のばらつきが目に見えるようになります。
既定値の2と0.6は、アルゴリズムの自由度を優先しています。狭い段でもはみ出し、ハイフネーション、ラントを避けられる余地をKnuth-Plassに与えつつ、組版の文献が許容範囲とする範囲に十分収まっています。
#行分割で埋めきれない行
Knuth-Plassは、選択の余地があるかぎり行をmaxWordSpacingより広げません。ラントの解消も同じです(オーファン、ウィドウ、ラントのペナルティを参照)。上限を超えた行は、超えた時点で、比較するほかのどの欠点(ハイフン、短い最終行、隣より少しゆるい行)よりも高くつき、そのコストは伸び率の2乗で増えていきます。そのため行分割は、語をハイフネーションしたり、余りをまわりの行に分散したり、1行をひどくゆるくする代わりに2行を少しゆるくしたりします。
範囲内に収まる分割の組がないこともあります。次の語を受け入れられない行は、余りを分け合うスペースが少なすぎるのです。たとえば、段落の字下げの直後の1行目で、続く語が長すぎて上がらず、短すぎてハイフネーションもできない場合。分割できない長い用語が並ぶ行。それ自身の区切りでしか分割できない長いURL。最後の語が上がらず、どのハイフネーションでもラントが残ってしまうリスト項目。どのくらいの頻度で起こるかは、行長、書体、言語によります。9.3 ptの英語を83 mmの段で組んだある章では、行の2〜8%がmaxWordSpacingを超えました(書体によって異なり、字幅の広い書体ほど多い)。同じ章をスペイン語で組むと、ハイフネーションの分割位置がずっと多いため、超えた行はありませんでした。こうした場合、Knuth-Plassはもっとも悪くない行を受け入れ、そのスペースは上限を超えて伸びます。通常のスペース幅の3倍より広がる場合、エンジンはその行を代わりに行末不ぞろいで組みます。自然な語間で描き、段落の最終行のように右端をそろえません。このしきい値はSandbox(ブラウザーで動く編集環境)のゆるい行の警告と同じなので、それを超える行は警告される前に修正されます。しきい値以内の行は計測どおりに残ります。狭い行長でいちばん起こりやすい囲みの中でも同じです。postext 1.4までは、囲みはこうした行を、スペースがどれだけ伸びても両端そろえのままにしていました。
こうした行を減らすには、次の順に試します。ハイフネーションがオンで、テキストの言語に設定されているか確認する。行長を広げるか文字サイズを小さくする。行にわずかなトラッキングを許す(次の節)。あるいはmaxWordSpacingを上げる。これは同じ行の組み方を変えずに、範囲内とみなすようにするだけです。
#最後の手段としてのトラッキング
bodyText.maxJustifyTrackingを使うと、そうした行は語間をさらに広げる代わりに、わずかなトラッキング(字間)をとれます。量は1文字あたり最大で指定した値の1/1000 emです(10 = 0.01 em、InDesignの単位)。これは両方向に働きます。スペースがmaxWordSpacingより広がってしまう行は、スペースが上限に戻るまで字間を広げ、スペースをminWordSpacingより狭くしないと収まらない行は、語を次の行へ送る代わりに字間を詰めます。Knuth-Plassは分割位置を選ぶときにこれを考慮します。トラッキングをかけた行は、範囲内の行より少し高く、範囲を超えた行よりずっと安くつくので、トラッキングは語間だけでは失敗する場所でだけ使われます。範囲内の行、段落の最終行(はみ出す場合を除く)、1語だけの行、チップを含む行には一切かかりません。行はこれをVDTLine.letterSpacingとして、ブロック自体のトラッキングに上乗せして記録し、Canvas、HTML、PDFが描画します。連結する文字体系(アラビア文字など)の語には、このトラッキングもほかのトラッキング(スタイルのletterSpacing、段末そろえ、ラントの解消)も一切かかりません。文字を離すと連結が切れるからです。行はそれらの文字を数に入れず、レンダラーはトラッキングなしで描画します。そうしたテキストにletterSpacingを設定したスタイルには、joiningScriptLetterSpacingのコンテンツ警告が出ます。
bodyText: {
maxWordSpacing: 2,
maxJustifyTracking: 10, // at most 0.01 em a letter, either way
}既定ではオフなので、求めないかぎり文書の行の組み方は変わりません。前述の英語の章では、10‰でmaxWordSpacingを超える行が書体ごとに1行以下になり、20‰では4書体のうち3書体でゼロになりました。値は小さく保ってください。20‰あたりを超えると、トラッキングが明るい行や暗い行として目に付きはじめます。
#アラビア文字のカシーダ
両端そろえのアラビア文字の行は字間を広げません。文字どうしが連結していて、離すと連結が切れるからです。代わりに2か所で伸ばします。語間のスペースと、カシーダです。カシーダは、書家がつながった2つの文字のあいだに引く、引き伸ばした連結線です(كتاب → كتـاب)。Postextはこれをタトウィール(U+0640、ـ)として1個単位で挿入し、フォントはそれを1本の線として描きます。Amiriは1〜7個の連なりを曲線のカシーダに変えます。
bodyText.kashidaは、アラビア文字で書く言語(ar、fa、urなど)の文書では既定で'auto'、それ以外では'none'です。別の言語の本で引用するアラビア語の語を伸ばすには'auto'にします。段落の最終行以外の各行では、まず語間のスペースを幅の4分の1まで広げ、残りの余りをカシーダで埋めます。タトウィールを1個ずつ、最適な位置から順に、何巡も挿入し、そのつど語をタトウィールごと全体として計測し直します。残った1タトウィール分未満の余りはスペースに戻します。Knuth-Plassは各語の伸ばせる量を伸びとして数えるので、アラビア語の語の行はそこで広げられることを踏まえて分割位置を選びます。
カシーダを置ける位置は、raqim-kashida(MIT)の規則によります。置けるのは連結する2つの文字のあいだだけで、次の文字へ連結しない文字(ا د ذ ر ز و ة)の後、語の末尾、ラーム・アリフの内側には置かず、前の文字に付く記号の後に置きます。kashidaPatterns: 'naskh'は古典的なナスフ体の規則(Benatiaの行列とAfifiの禁止則。カーフやラームの後には置かない、サード、アイン、ワーウの前には置かない、語末の代名詞ハーはその直前で伸ばす)に従います。'simple'は単純な現代書体向けのMicrosoftの優先順位、'nastaliq'はナスタアリーク体向けに調整したナスフ体の規則です。既定の'auto'は本文のフォントから判断します。Aref Ruqaaのようなルクア体やディーワーニー体の書体ではカシーダなし、ナスタアリーク体の書体ではナスタアリーク体の規則、それ以外ではナスフ体の規則です。1語あたりkashidaPerWord回(既定値1)まで、長さkashidaMaxLength em(0.6、Amiriのタトウィール3個分)までの伸ばしをとります。著者が入力したタトウィールを含む語は、その位置で伸ばします。ラテン文字の語、数字、見出し、行末不ぞろいの行、段落の最終行には一切かかりません。
bodyText: {
kashida: 'auto', // the default in an Arabic book
kashidaPatterns: 'naskh', // 'auto' picks it from the font
kashidaMaxLength: 0.6, // em
}タトウィールは描画されるテキストの一部です(各セグメントはVDTLineSegment.kashidaにそれを並べ、行はVDTLine.kashidaにその数を記録します)。プレーンテキスト、ソースの対応づけ、リンク、PDFからコピーしたテキストには含まれません。
#最終行
ボックス・グルー・ペナルティのモデルでは、どの段落も、幅0で無限に伸びるグルーと強制分割で終わります。この末尾のグルーが最終行に残った余白をコストなしで吸収するので、最終行は自然に行末不ぞろいになります。最終行は自然な幅で描画され、justifiedSpaceRatioは最終行以外についてだけ計算されます。
例外が1つあります。Knuth-Plassは、自然な内容が行長より広い最終行を、語間のグルーが縮むという前提で受け入れることがあります。これはTeXの標準的なグルー設定の意味論です。3つのバックエンドはいずれもこの場合(行の内容の自然な幅が実効の行長を超える場合)を検出し、はみ出させる代わりに、その最終行の語間を詰めて行長にぴったり収めます。この判定はCanvas、PDF、HTMLのバックエンドで同じように適用されます。
#最適な行分割と貪欲な行分割
bodyText.optimalLineBreakingプロパティ(既定値:true)で、エンジンが使う行分割アルゴリズムを選びます。
true:Knuth-Plassの動的計画法アルゴリズム。可能なすべての分割の組を評価し、大域的に最適なものを選びます。両端そろえのテキストにはこの設定を推奨します。行末不ぞろいの本文もbodyText.optimalRagged(既定でオン)でこれを使います。語間は幅を保ち、行分割は代わりに各行が行長にどれだけ足りないかを比較します。3 em足りない行のコストはmaxWordSpacingまで広げた両端そろえの行と同じになるので、行末の不ぞろいがならされ、ラントの規則も適用されます。false:PretextのlayoutNextLine()による、first-fitの貪欲法。高速ですが品質は下がります。組版の品質より性能が重要な場合(文字数が非常に多い場合のリアルタイムプレビューなど)にだけ使ってください。
Knuth-Plassが有効で、有効な分割を1つも出せない場合(極端に狭い段や、段の幅より長い語で起こりえます)、エンジンはその段落について自動的に貪欲な行分割に切り替えます。
#中国語・日本語・韓国語
語間のスペースよりCJKの文字を多く含む段落は、Knuth-PlassではなくCJKの組版処理が組みます。2つの文字のあいだはどこでも行を分割できる位置なので(cjk.lineBreakの行分割規則が禁じる場所を除く)、最適化で比較するものがありません。行は1行ずつ順に埋めていきます。文字が収まらないとき、行はまず約物のアキとスペースを詰めて取り込もうとします(追い込み。語間スペースは4分の1 emまで、次に中点類、括弧、読点類、漢字とラテン文字のあいだのスペースは8分の1 emまで、そして句点類を、cjk.punctuationWidthが許す範囲で詰めます)。それでも足りない場合にだけ、行頭に置けない文字が前の文字を連れて次の行へ下ります。規則は東アジアの組版を参照してください。中国語の組版のほかの面(地域ごとの約物の幅、文字グリッド、縦組み)は中国語の組版で扱います。日本語の文書は代わりにJLReqに従います(日本語の組版を参照)。禁則処理のレベル、JLReq §3.8.3の順序でアキを返す行(まず行末の約物の後ろの二分アキを、全部か無しかで返し、行の中にある。の後ろのアキは返さない)、そして行を広げるときに始め括弧の後、終わり括弧の前、および「、。・:;?!」の隣にはアキを加えないことです。
このような段落の両端そろえの行(最終行を除く)は、clreqが定める順序(§6.2.2.4)で行長まで広げます。
- 欧文の語間のスペースを均等に、それぞれ二分まで。
- 漢字とラテン文字のあいだのスペース(
cjk.latinSpacing)を均等に、それぞれ二分まで。 - 文字と文字のあいだのすべての間隔を均等に。漢字とラテン文字のあいだのスペースも含みます。漢字どうし、漢字と約物、漢字とラテン文字の語のあいだです。ラテン文字の語、数、2倍ダーシや2倍の省略記号の内側には入れず、連結記号(~、–、単独の—)や斜線の隣にも入れません。行末からはみ出してぶら下げた約物(
cjk.hangingPunctuation)は行長に含めません。
段落の最終行はベタ組みにします。間隔は行のセグメントごとに記録する(VDTLineSegment.tracking)ので、Canvas、HTML、PDFは同じ位置に描画し、Sandboxはクリックした文字にカーソルを置きます。
文字のあいだに二分を超える間隔、あるいはbodyText.maxJustifyTrackingを設定している場合はその値を超える間隔が必要になる行は、その上限の間隔で行長に満たないまま組まれ、cjkLooseLineのコンテンツ警告として報告されます(Sandboxでは行長に満たないCJKの行)。よくある原因は、上がってこられなかった長いラテン文字の語やWebアドレスです。長いWebアドレスの先頭のように、CJKの文字を1つも含まない行は、警告なしで行末不ぞろいに組まれます。
ゆるい行の強調表示では、CJKの行は間隔によってゆるいかどうかを判定します。比は1に、間隔を8分の1 em単位で表した値を足したものなので、既定のしきい値3では、文字が4分の1 emより広く空いた行に印が付きます(lineLooseness(line, fontSizePx)がこの比を返します)。段末そろえでは、CJKの段落を1行長く組むことがあります。行長より少し短く(8分の1 emずつ、最大2 emまで)行を分割し、行長まで広げ戻します。
#ゆるい行のデバッグ
Knuth-Plassとハイフネーションを使っても、理想よりゆるい行はいくらか残ります。長い語の多い狭い段や、ハイフネーションの機会が少ない言語ではとくにそうです。デバッグ機能のゆるい行の強調を使うと、こうした問題のある行をすぐに見つけられます。
#仕組み
仮想文書ツリー(Virtual Document Tree)のすべての行はjustifiedSpaceRatioをもちます。両端そろえで実際に使われたスペース幅と、フォント本来のスペース幅との比です。1.0はスペースが自然な幅であることを、2.5はスペースが通常の2.5倍の幅であることを意味します。
debug.looseLineHighlight.enabledがtrueのとき、Sandboxは、justifiedSpaceRatioが設定したthresholdを超えるすべての行に、Canvasプレビュー上で半透明のオーバーレイを重ねます。既定のしきい値は3.0で、スペースが通常の3倍より広い行だけを強調します。これは意図的に高い基準で、ここまでゆるい行は本当に組版上の問題です。これはまた、エンジンが両端そろえの行を伸ばす代わりに行末不ぞろいで組む幅でもあります(行分割で埋めきれない行を参照)。そのため既定値では、強調表示もlooseLinesの警告も本文ではほとんど何も見つけません。そうした行は警告される前に修正されているからです。ゆるいもののまだ両端そろえのままの行を見つけるには、しきい値を1.5や2に下げてください。
オーバーレイはページの一部ではないので、書き出したページには表示されません。自分のcanvasに描くには、renderPageToCanvasの直後にdrawLooseLines(ctx, page, doc, { threshold })を呼び出します。findLooseLines(doc, { threshold })は同じ行をデータとして返します(自前のcanvasでのゆるい行を参照)。
#設定
ゆるい行の強調はPostextConfigのdebugセクションに含まれます。
| プロパティ | 型 | 既定値 | 説明 |
|---|---|---|---|
looseLineHighlight.enabled | boolean | false | ゆるい行を強調するかどうか。 |
looseLineHighlight.color | ColorValue | #ff000040 | 強調オーバーレイの色。既定値は半透明の赤です。 |
looseLineHighlight.threshold | number | 3 | 行をゆるいとみなす、通常のスペース幅に対する倍率。値を小さくするとより多くの行を検出し、大きくすると最悪の行だけを強調します。3以上では本文にほとんど何も表示されません。3倍を超える行は行末不ぞろいで組まれるからです。 |
debug: {
looseLineHighlight: {
enabled: true,
threshold: 2.5,
color: { hex: '#ff660040', model: 'hex' },
},
}#結果の読み方
ゆるい行の強調を有効にして、いくつかの行に赤い帯が見えたら、エンジンが語間を広げすぎずにその行を組む方法を見つけられなかったということです。上から順に確認してください。原因は可能性の高い順に並んでいます。
| 原因 | 対処 |
|---|---|
| 段が文字サイズに対して狭すぎる | 段の幅を広げるか、文字サイズを小さくするか、1段組みに切り替えます。 |
| ハイフネーション位置の少ない長い語 | ハイフネーションが有効で、正しいロケールが設定されているか確認します。専門用語や固有名詞には、正しい分割位置がまったくないものもあります。 |
| ハイフネーションが無効 | bodyText.hyphenation.enabledを有効にします。ハイフネーションのない両端そろえは、ほぼ必ず質が落ちます。 |
| 書体を変えても数行がゆるいまま残る | bodyText.maxJustifyTracking: 10で、それらの行にわずかなトラッキングを許します。最後の手段としてのトラッキングを参照してください。 |
| 語間の範囲が狭すぎる | maxWordSpacingを少し上げます(例:2から2.4へ)。アルゴリズムの余地が増えます。 |
| 長い複合語の多い言語(ドイツ語など) | 正しいロケールが設定されているか確認します。ドイツ語のハイフネーションパターンは複合語をうまく扱いますが、それはエンジンがドイツ語だと知っている場合に限ります。 |
#縦方向の両端そろえ:段末そろえ
Knuth-Plassは横方向の問題、つまり各行をどこで分割するかを解きます。段末そろえは、それに対応する縦方向の問題、つまり各段をどこで終えるかを解きます。出版社は、ページのどの段も上端から始まってページの下端ぴったりで終わり、見開き全体で行がそろうことを求めます。ところが、テキストの品質を守る規則そのものがこれを妨げます。オーファンとウィドウの保護、keepWithNextの見出し、分割できない図は、いずれも現在の段が完全に埋まる前に内容を次の段へ押し出し、段の下にベースライングリッドの空き行を1行以上残します。
headings.balancingが有効なとき(既定)、エンジンは人間の植字工と同じようにそのすき間を埋めます。手段は編集上の厳格な優先順位で適用し、各手段は前の手段で吸収しきれなかった分にだけ働きます。
-
段を閉じる囲み。短い段を終える囲みは、その下に空いた分だけ正確に下へ移動し、下端が最後のグリッド線に合って隣の段とそろいます。空きは1行に満たないこともあります。この手段は最初に働き、
closingBoxで別の指定をしないかぎりすき間全体をとります。'last'にすると、下の手段が先に行単位の空きをとり、残った分だけを囲みに渡すので、上の段落に注釈を付ける囲みはその段落の近くにとどまります。'off'にすると囲みを動かしません。 -
セーフエリアをもつ画像。リソースにセーフエリア(
Resource.safeArea。常に見えていなければならない部分。文書形式 › セーフエリアを参照)が指定された画像は、短い段の中にインラインで置かれているとき、あるいはその段だけにかかるフロートとして段の頭か足に置かれているとき、グリッドの行単位で大きくなります。エンジンはセーフエリアの外側をトリミングして、全幅のまま背を高くします。背の高くなった画像はページのどこにも穴を残さないので、スペースを加える前に適用します。段の頭をそろえる最終ページや締めくくりの帯で段の頭に置かれたフロートは大きくなりません。 -
見出しの上のスペース。短い段の中の見出しの上マージンに、ベースライングリッドの行単位でスペースを加えます。複数行が必要で段に見出しが複数ある場合は、重要度の順に1行ずつ順番に配分するので、もっとも重要な見出しが常にいちばん多く受け取ります。
h2とh3に3行を配分すると、h2の上に+2行、h3の上に+1行となります。段のいちばん上にある見出しにはスペースを加えず(段はページの上端から始まるまま)、段の末尾にある見出しを段の下へ押しやることもありません。 -
リストの終わりの後のスペース。リストや列挙の終わりにグリッドを1行加えます。リストの後の空きは自然に読めます。リストの終わりごとの上限は
maxLinesAfterList(既定値1)です。次に、別行立ての数式や、後にテキストが続く囲みの下に1行、段の頭にある図や表の下に1行加えます(stretchAfterFloats、maxLinesAfterFloat)。図の下のテキストが前のページから続く段落の残りであってもかまいません。その場合も、段の足で分割規則が空けた行(ウィドウの規則で空のまま残った行、後にテキストを置く余地のない段落間のスペース)の分だけ下へ移動します。 -
ゆるい段落。最後の手段として、段の中の段落を1行長く分割し直します(TeXの
\looseness=+1)。対象は最大maxLooseParagraphs個(既定値2)の段落で、それぞれ1行ずつ増やします。行分割はちょうど1行多くなるようにKnuth-Plassを再実行し、ゆるくした結果のすべての行がmaxWordSpacing未満に収まる場合にだけ受け入れます。版面の濃度(タイプカラー)が、すでに設定した上限を超えることはありません。エンジンは段の中でいちばん長い段落を優先します。追加の空きがもっとも多くの語間グルーに薄まって目立たなくなるからです。optimalLineBreaking(既定)が必要で、段をまたいで分かれた段落は対象外です。ラントの解消で1行短く組んだ段落は、その組み方から追加の1行を数えます。解消で取った行を取り戻すことはできますが、ラントがなく、maxWordSpacingを超える行もない組み方に限ります(postext 1.4までは、上限を大きく超える行を含んだまま戻ることがありました)。語間だけでは1行を稼げないとき、段落はわずかな正のトラッキングをとることもできます。植字工の古典的な手法です。エンジンは最小の量から試し(
maxTrackingの半分、次にmaxTracking。単位は1文字あたり1/1000 emで、既定値は10 = 0.01 em)、1行を稼げた最初の量を採用します。このときも同じ語間の条件が適用されます。トラッキングは段落の行に組み込んで計測され、すべてのバックエンドが描画します(CanvasのletterSpacing、CSSのletter-spacing、PDFの文字間隔)。trackParagraphs: falseでオフにできます。
#調整が局所的で済む理由
段の切れ目は要素に結びついています。次の段を始める要素がそこにあるのは、すき間に収まらなかったからです。したがって段の末尾を、その段自身のすき間以内で下へ押しても、内容が次の段やページへ移ることはありません。各段はその場で確定し、連鎖的な再配置は起きません。それでもエンジンはこれを実際に検証します。提案した調整で配置をやり直し(最大8回)、残ったすき間の合計を測り、見つかった中で最良のレイアウトを常に残します。どのトラッキングでも語間の上限内で1行を稼げなかったゆるい段落は除外リストに入り、次の候補を試します。
#短い段をそのままにする場合
短い段が正しい出力である場合もあり、段末そろえはそれを見分けて手を引きます。
- ページの最後の段は、そのページが次のページへ自然に流れる場合にだけそろえます。
:::pagebreak、見出しのbreakBefore、章扉で終わるページは、最後の段が短いまま残ります。章がページの途中で終わるのは正当なことだからです。 - 文書の最後のページはそろえません。
- 使える伸ばし位置がない段(対象になる見出し、リストの終わり、ゆるくできる段落がない段)は、組版の質を落とすよりもすき間を残します。
const doc = buildDocument(content, {
headings: {
balancing: {
enabled: true, // default
maxLinesPerHeading: 4, // cap per heading
stretchAfterLists: true, // lever 4
maxLinesAfterList: 1, // cap per list end
looseParagraphs: true, // lever 5 — bounded by bodyText.maxWordSpacing
maxLooseParagraphs: 2, // loose paragraphs per short column
trackParagraphs: true, // let a loose paragraph take a little tracking
maxTracking: 10, // ‰ em per character (0.01 em)
closingBox: 'first', // lever 1: 'first' | 'last' | 'off'
},
},
});Sandboxでは、これらはデザイン → 見出しと目次の見出しセクションにあります(「段末そろえ」の下に「リストの後で広げる」「図の下で広げる」「段末の囲み」「ゆるい段落」が入れ子になっています)。ベースライングリッドをオンにして(デザイン → 詳細設定 → 画面表示の補助)、Canvasビューで効果を確かめてください。段末そろえがオフだと短い段は最後のグリッド線より上で終わり、オンにすると、そろえられるすべての段が同じ線で終わります。すべてのオプションは設定のページを参照してください。
#完全な例
ハイフネーションと両端そろえの設定をすべて使った構成例です。
import { buildDocument } from 'postext';
const vdt = buildDocument(content, {
bodyText: {
fontFamily: 'EB Garamond',
fontSize: { value: 9, unit: 'pt' },
textAlign: 'justify',
// Knuth-Plass optimal line breaking (default: true)
optimalLineBreaking: true,
// Hyphenation
hyphenation: {
enabled: true,
locale: 'es',
},
// Word spacing bounds (multipliers of normal space width)
maxWordSpacing: 2, // spaces stretch up to 200%
minWordSpacing: 0.6, // spaces shrink down to 60%
},
// Debug: highlight lines with excessive spacing
debug: {
looseLineHighlight: {
enabled: true,
threshold: 2.5,
color: { hex: '#ff000040', model: 'hex' },
},
},
});本文の設定項目の一覧は設定のページを、レイアウトの処理の流れがテキストの計測でこれらの設定をどう使うかはアーキテクチャーのページを参照してください。