MejiroPDF 1.2.0
MejiroPDF 1.2.0
Books. Vertical and horizontal, in Japanese — and a correction to the memory
model that was never as constant as it claimed.
composer require 2bo-velocity/mejiropdf:^1.21.0 and 1.1 could set Japanese text. Neither could set a Japanese book: there
was no way to pour a novel's worth of prose into a vertical page, the vertical
letterforms were wrong, and the apparatus a book needs — front-matter
numbering, running heads that differ across a spread, footnotes — did not
exist. This release adds all of it, in both directions.
Everything is additive. A document that does not ask for any of it produces the
same bytes it did under 1.1.0, apart from /Producer, and the example PDFs are
compared byte for byte on every change. Two behaviours do change; both are
the removal of a defect. See Upgrade notes.
Vertical books
Vertical text can now paginate
flowText() refused vertical writing outright — it threw. Since it is the only
entrance to automatic pagination, a 229-page vertical novel simply could not be
made.
The fix was not new typesetting logic but a value object. Layout\FlowRegion
states a region in the two axes text actually has — the inline axis along a
line and the block axis from line to line — and maps them onto the page.
Horizontal text stacks lines downward; vertical text stacks columns leftward.
The line breaker, the typesetter and the paginator did not change at all.
$pdf->flowInto(
FlowRegion::vertical(rightX: 108, topY: 20, columnHeight: 142, leftX: 20),
$novel,
font: CidFontJapanese::HEISEI_MIN_W3,
options: TypesetterOptions::novel(),
);flowText() — the older four-scalar spelling — now accepts vertical writing
too, and reads its arguments in the reading direction as typeset() always has.
flowInto() is the one to prefer when writing vertically, because a region says
which corner it starts from instead of leaving it to argument order.
Vertical letterforms are correct, and the file is a fraction of the size
Non-embedded Japanese CID fonts now go through the UniJIS-UTF16-V CMap,
which maps straight onto Adobe-Japan1's vertical CIDs. The engine stops
substituting glyphs by hand — the font supplies the shapes — and a line is
written as one Tj instead of one per character.
On an eight-page sample:
| 1.1.0 | 1.2.0 | |
|---|---|---|
| File size | 249 KB | 27.9 KB |
| Text-showing operators | 3,123 | 161 |
ー「」()〜『』【】… come out in their vertical forms, and the punctuation and
small kana sit where they belong. Checked against poppler.
Switching the CMap alone only took the operator count from 3,123 to 3,027,
because line breaking keeps one unit per character for kinsoku. The rest comes
from coalescing a run of plain full-width cells into a single text object — a
run ends at ruby, tate-chu-yoko, a background colour, an emphasis dot, an
underline or justification space.
For embedded TrueType, where no vertical CMap is available, Text\VerticalForm
rotates, shifts or re-sizes each character as needed. Its classification is
derived from Unicode's Vertical_Orientation property (UAX #50), not written
by hand: an earlier hand-written table, made from the same intent, disagreed
with the standard in four places.
A book as a document, not just as marks on paper
Page\DocumentInfo— title, author, subject, keywords, creator, given to
the constructor likeCreditOptions. Mirrored into the XMP packet so PDF/A
output keeps Info and XMP in agreement; verified with veraPDF.Page\ReadingDirectionwrites/ViewerPreferences << /Direction /R2L >>.
Without it a right-bound book opens with its pages side-swapped in spread
view, which is fatal to a vertical novel. The default isAUTO: typeset
anything vertically and the document declares R2L./PageLayoutis
deliberately not written —/Directionsettles the order of a spread, while
/PageLayoutwould force two-up on a reader who did not ask for it./Lang, andPage\OpenFitfor/OpenAction.Page\BindingEdge— page numbers on the fore edge, changing side with the
spread, and mirrored margins throughflowInto(gutter: …). The credit line
moves to the gutter side on its own, because it has always resolved to
"opposite the page number".Layout\Section— a chapter as one thing: heading, prose, bookmark,
running head. This is not sugar. The body begins below the heading on the
first page and at the top of every page after it, and a single region cannot
express that; a region shortened to fit the heading would leave the gap on
every page.Text\LatinInVertical—UPRIGHT(default),FULLWIDTH,ROTATEor
TCYfor Latin runs inside a vertical column. Automatic tate-chu-yoko still
wins for two-digit numbers.
A manuscript-paper grid was specified, then measured and dropped. Full-width
CJK glyphs are 1000/1000 units, so proportional setting already lands on the
grid: twelve configurations all came out with exactly the intended character
count, and prose with punctuation never exceeded it. The abstraction would have
changed nothing.
Japanese horizontal books
The abstractions above are direction-neutral, so flowing, chapters, spreads,
running heads and contents already worked horizontally. This is not the mirror
image of the vertical work: there the problem was letterform correctness, here
it is page apparatus and the rules for mixing scripts.
Mixed Japanese and Latin — Text\JapaneseSpacing
LineBreaker added up glyph widths and had no notion of spacing between
character classes. It is the largest single reason Japanese horizontal text
looked amateurish, and it stayed hidden in vertical writing, which is usually
set solid.
new TypesetterOptions(japaneseSpacing: JapaneseSpacing::JIS_X_4051)Two rules, one mechanism: a quarter em opens between Japanese and Latin
(Rails を使う), and the empty half of consecutive closing punctuation
closes up ()。 no longer spans two full ems). Both are an adjustment attached
after a character, so both are carried by one field on the line unit.
The default is NONE. Turning it on would change the output of every
existing Japanese horizontal document, which is the one thing this release
promises not to do. The class table is derived from Unicode's Line_Break
property; no separate table of "Japanese versus Latin" was written, because
UAX #50's upright letters already draw exactly that line — a full-width A
occupies a cell of its own, so no space is added beside it.
Footnotes — Text\FootnoteStyle
Written into the body as ((…)), numbered before line breaking (the mark has
width, so what it is called has to be settled first), set at the foot of
whichever page the mark landed on.
$pdf->flowInto($region, $body, footnotes: new FootnoteStyle());The band's depth is worked out per page by default. Footnotes are discovered
while a page is being filled, but the room for them has to exist before filling
starts. The flow already buffered exactly one page of lines, and that buffer is
the footing for a retry: plan the page, total the notes, measure them without
drawing, raise the foot, plan again. The reserve only ever grows, so it settles;
one page is held, so the memory profile is unchanged.
Pass a number for a fixed band, or 0.0 for none. If a fixed band overflows,
the engine says so on the PSR-3 logger rather than printing over the body.
Notes can instead be set in the fore-edge margin — 傍注, the Japanese textbook
convention — with NotePlacement::SIDE_MARGIN. The markup does not change:
where notes go is a decision about the book, not about each note. A margin note
costs the body nothing, since the margin is outside the region, and notes too
close together are pushed apart rather than overprinted.
Calls that cannot place a note at all — typeset() and its relatives paginate
nothing — put the ((…)) back as written. Swallowing it would delete the
author's words without saying so.
Page apparatus
Front matter numbering. Page\NumberStyle and restartPageNumbering():
preface in i, ii, iii and a body that starts again at 1, so material added to
either does not renumber the other. Also ROMAN_UPPER, KANJI, and NONE for
plates. {TOTAL} counts the current sequence — "ii of 47" would tell a reader
nothing. A number a style cannot spell falls back to digits rather than throwing
at finalize, which would lose the whole document.
$pdf->setPageNumbers(new PageNumberOptions(format: '{PAGE}', style: NumberStyle::ROMAN_LOWER));
// … front matter …
$pdf->restartPageNumbering(NumberStyle::ARABIC);Running heads that differ across the spread. An opened book shows two pages
at once and printing the same words on both wastes one of them. Japanese
practice — and the default — is the chapter on the verso and the section on the
recto:
$pdf->setRunningHead(new RunningHeadOptions()); // verso: CHAPTER, recto: SECTIONSection titles come from .subhead paragraphs in the flow. A side with nothing
to say takes the other's words, so a book without subheadings sets its chapter
on every page exactly as before. A chapter clears the section as it opens, or
the last section of one chapter would name the rectos of the next.
Subheadings that stay with their text. .keepnext marks a paragraph that
must not be parted from what follows; .subhead does that and names the page.
TypesetterOptions::$keepWithNextLines (default 2) says how much of the
following text has to come with it. The rule is "the opening of what follows",
not "two lines": a following paragraph that fits whole has stranded nothing.
Columns. FlowRegion::withColumns(2, gap: 8.0) — standard for a Japanese
paper. The division is along the inline axis, which is what lets one
implementation serve both directions: horizontally the columns stand side by
side, vertically they are bands stacked down the page. The paginator is again
unchanged, because a column break is a page break one step shallower. Note that
columns shorten the line; they do not save paper.
A line grid. TypesetterOptions::$lineGrid rounds each line's advance up to
a whole number of steps so facing pages align. Adopted after measuring: uniform
prose is already on a grid of its own line advance, but a single ruby line puts
the rest of its page 2.2 mm out of step. The room the grid adds goes above
the line — rounding the slot alone leaves the baseline off it, because ruby
pushes its parent down.
Memory: O(1) is now true
While making the vertical flow incremental it emerged that constant-memory
streaming was not actually holding for flowed text. A 320-page document
peaked at 210 MB, in proportion to its length — the opposite of what this
engine exists to claim.
Two causes. Neither changed a single output byte, so no test could see either:
LineBreaker::toSegments()built every line of the whole text before the
first page was drawn. Making the outer method a generator does not help while
the inner one returns an array. 210 MB → 37 MB.SmartPunctuation::apply()split the entire text into single-character
strings. The tokenizer hands undecorated prose over as one token, so half a
megabyte of a novel became roughly 500,000 PHP strings. Replaced with a byte
scan that jumps to the next quote. 37 MB → 4.4 MB.
64× the pages for 2.5× the peak: 3,200 paragraphs over 320 pages, producing a
47.77 MB PDF, in 4.44 MB of memory. The rewritten SmartPunctuation was checked
against the old one across 60,014 cases, and a regression test now measures
the peak instead of the output — a defect that changes nothing visible cannot be
caught any other way.
Upgrade notes
flowText() and flowInto() no longer throw for vertical writing. This was
a documented limitation rather than a contract, and lifting a restriction is
backward compatible — but code that caught that LogicException to fall back
to something else will now take the vertical path in silence. If you have such a
fallback, remove it.
Running heads are now set in the face the words were written in. They were
drawn in the document's default face, so a book set in a CJK face passed to
section(font: …) had its own chapter title emitted as a Latin literal string —
rubble on the page. If you worked around this by passing
RunningHeadOptions(font: …), that still wins and nothing changes for you; if
you did not, your running heads will start rendering instead of failing.
Everything else is byte-compatible. DocumentInfo, japaneseSpacing,
lineGrid, footnotes, margin notes, columns, .keepnext, .subhead, page-number
restarts and the two running-head levels are all off, absent or fall back to the
previous behaviour by default.
Two combinations are refused rather than approximated, each with a
LogicException naming the reason: footnotes beside columns (the band
belongs to the page, so its depth cannot be settled until every column is full,
and the columns already drawn would have to be laid out again) and margin
notes in vertical writing (the lines advance across the very margin the note
would occupy, so where it belongs is undefined, not merely unbuilt).
Everything else
Unchanged from 1.1.0: constant-memory streaming, Western typesetting, font
subsetting and custom fonts, GridTable, report blocks, rotation, compiled
.mpb templates, AES-128, PKCS#7 signatures and PDF/A-1b. The .mpb format
does not change — everything in this release lives inside the engine and the
Document API, so templates built by Studio 1.0 load and render identically.
See the 1.1.0 release notes
for rotation, the 1.0.0 notes
for what the engine does, and https://mejiropdf.com for the manual.
Requirements are unchanged: PHP 8.3+ (enforced at runtime), ext-mbstring,
psr/log, 64-bit.
Quality gates
PHPStan level 8 across src/, and a PHPUnit suite of 76 test classes (652
cases, 1,488 assertions). Signatures and PDF/A output are verified in Docker
against openssl / pdfsig and veraPDF; vertical letterforms are verified by
rendering with poppler. The memory figures above are held by a test that
measures the peak rather than the output. The 22 scripts in
examples/ are the working reference — new in this release are
examples/vertical_book.php and
examples/horizontal_book.php.
Contributing and support
Pull requests are not accepted — the dual license requires the copyright to
stay with one author. Bug reports are welcome and are used to improve the
library; the fix is then written by the author. There is no support
commitment. See CONTRIBUTING.md, and report security issues
privately to support@2bo-velocity.com.
日本語
MejiroPDF 1.2.0 は書籍を組めるようにしたリリースです。縦組みと横組みの両方を対象と
し、あわせて**「O(1) メモリ」が実際には成り立っていなかった**点を是正しました。
1.0 / 1.1 も日本語の文字は組めましたが、日本語の本は組めませんでした。小説 1 冊
分を縦組みのページへ流し込む入口が無く、縦組みの字形が誤っており、本に必要な装置
(前付けのノンブル、見開きで左右に出し分ける柱、脚注)が存在しなかったためです。
追加のみのリリースです。新機能を使わない文書の出力は /Producer を除いて 1.1.0 と同一
で、examples の PDF は変更のたびにバイト単位で比較しています。挙動が変わる点が 2 つ
ありますが、いずれも不具合の解消です(後述)。
縦組み書籍
縦書きが自動ページネーションできるようになりました。 flowText() は縦書きを例外で
拒否しており、そこが自動ページネーションの唯一の入口だったため、229 ページの小説が
作れませんでした。
解決したのは新しい組版ロジックではなく値オブジェクトです。Layout\FlowRegion が、
テキストが実際に持つ 2 軸——行に沿って進むインライン軸と、行から行へ進む
ブロック軸——で領域を表し、それをページ座標へ写します。横書きは行を下へ、縦書きは
列を左へ積みます。行分割器・組版器・ページネータは 1 行も変えていません。
$pdf->flowInto(
FlowRegion::vertical(rightX: 108, topY: 20, columnHeight: 142, leftX: 20),
$novel,
font: CidFontJapanese::HEISEI_MIN_W3,
options: TypesetterOptions::novel(),
);縦組みの字形が正しくなり、ファイルが桁違いに小さくなりました。 非埋込の和文
CIDFont が UniJIS-UTF16-V CMap を通るようになり、Adobe-Japan1 の縦組み用 CID へ
直接対応づきます。字形の置換をエンジンが手で行う必要が無くなり(フォントが供給します)、
1 行を 1 つの Tj で書けます。8 ページのサンプルで 249KB → 27.9KB、テキスト描画命令
3,123 → 161。ー「」()〜『』【】… が縦組み字形になり、句読点と小書き仮名が正しい
位置に来ます(poppler で確認)。
CMap を替えただけでは 3,123 → 3,027 にしかなりません。禁則のため行分割は 1 文字 1 単位
だからです。残りは、平体セルが同一スタイルで続く区間を 1 つのテキストオブジェクトへ
束ねることで得ています(ルビ・縦中横・背景色・傍点・下線・両端揃えの空きが区間を切ります)。
埋込 TrueType のように縦組み CMap が使えない場合は Text\VerticalForm が回転・移動・
縮小を行います。その分類表は Unicode の Vertical_Orientation(UAX #50)から導出
しています。同じ意図で手書きした初版の表は、標準と 4 か所 食い違っていました。
本としての体裁:
Page\DocumentInfo— 表題・著者・主題・キーワード・作成者。CreditOptionsと同じ
流儀でコンストラクタに渡します。PDF/A では XMP と同値に揃え、veraPDF で検証済みです。Page\ReadingDirectionが/ViewerPreferences << /Direction /R2L >>を書きます。
これが無いと見開き表示でページが左右逆に並び、縦書きの本としては致命的です。既定は
AUTOで、縦組みを 1 度でも行えば R2L を宣言します。/PageLayoutは意図的に書きま
せん——/Directionは見開きの並び順だけを決めますが、/PageLayoutは求めていない
読者にも二丁付けを強制するためです。/Lang、/OpenAction(Page\OpenFit)。Page\BindingEdge— 小口側のノンブル(見開きで左右が入れ替わる)と、
flowInto(gutter:)によるミラーマージン。クレジットは「ページ番号と反対側」という
従来の規則のまま自動的にノド側へ回ります。Layout\Section— 章を 1 つの概念に。見出し・本文・しおり・柱をまとめます。これは
糖衣ではありません。本文は 1 ページ目だけ見出しの下から始まり、2 ページ目以降は先頭
から始まるからで、単一の領域では表現できません(見出し分だけ縮めた領域では、その空き
が全ページに出ます)。Text\LatinInVertical— 縦組み中のラテン文字をUPRIGHT(既定)/FULLWIDTH/
ROTATE/TCYから選べます。2 桁数字の自動縦中横は引き続き優先されます。
原稿用紙グリッドは仕様に挙げたうえで、測定して不採用にしました。 全角 CJK は
1000/1000 unit なので、比例組みのままでも実質グリッドに乗ります。12 通りの設定すべてで
過不足なく意図した字数になり、句読点入りの散文でも字数超過はゼロでした。抽象を足しても
出力は変わりません。
横組み書籍(日本語)
上記の抽象は方向中立に作ったので、流し込み・章・見開き・柱・目次は横組みでも既に動いて
いました。これは縦組みの鏡像ではありません。 縦組みの課題が「字形の正しさ」だったの
に対し、横組み和書の課題は「ページ装置」と「和欧混植の作法」です。
和欧混植(Text\JapaneseSpacing)。 LineBreaker はフォントメトリクスを足すだけで、
文字クラス間のアキという概念を持っていませんでした。 日本語の横組みが下手に見える
最大の原因で、全角ベタで組むことの多い縦組みでは表面化していませんでした。
規則は 2 つですが仕組みは 1 つです。和欧間に四分アキ(Rails を使う)を開き、
連続する閉じ約物の空き半分を詰めます()。 が全角 2 つ分空かなくなります)。どちらも
「文字の後ろに付く調整量」なので、行ユニットの 1 つのフィールドで表せます。
既定は NONE です。 既定化すると既存の日本語横組み文書の出力が全部変わり、この
リリースが唯一守ると約束したことを破るためです。クラス表は Unicode の Line_Break から
導出しています。「和字か欧字か」の表は別に作っていません——UAX #50 の正立文字がちょうど
その境界を引いており、全角の A は既に 1 セルを占めているのでアキを足しません。
脚注(Text\FootnoteStyle)。 本文に ((…)) と書きます。帯の深さは既定で
ページごとに算出します。 脚注はページを埋めている最中に見つかるのに、その領域は埋め
始める前に確保しておく必要があります。流し込みは元々 1 ページ分の行だけをバッファして
おり、そのバッファがそのまま再試行の足場になりました——ページを計画し、注を合計し、
描かずに測り、下端を上げて計画し直す。リザーブは増える方向にしか動かないので収束し、
保持は 1 ページ分のままなのでメモリ特性は変わりません。数値を渡せば固定帯、0.0 で無効。
固定帯に入りきらないときは本文に重ねず PSR-3 に警告します。
NotePlacement::SIDE_MARGIN を指定すると、注は小口側の余白=傍注になります。記法は
変わりません——下端に置くか余白に置くかは「本の作り」の決定であって注ごとの決定では
ないからです。余白は版面の外なので本文は 1 行も譲らず、近接した注は重ね刷りせず下へ
送ります。注を置けない呼び出し(typeset() などページを繰らないもの)では ((…)) を
書かれたまま出します。飲み込めば著者の言葉が黙って消えるからです。
ページ装置:
- 前付けのノンブル —
Page\NumberStyleとrestartPageNumbering()。まえがきを
i, ii, iii とし本文で 1 から振り直せば、どちらに材料を足しても他方の番号が動きません。
ROMAN_UPPER/KANJI/NONE(口絵・白丁)も指定できます。{TOTAL}はその系列の
枚数です(「ii / 47」は読者に何も伝えません)。様式が表せない値は例外ではなく算用数字
へ落とします——finalize で例外を投げれば書きかけの文書ごと失われるためです。 - 柱の左右分け — 開いた本は 2 ページを同時に見せるのに、両面へ同じ語を刷ると片方が
無駄になります。和書の慣行、そして既定は偶数ページ=章題・奇数ページ=節題。節題は
流し込み中の.subhead段落から採ります。片側に出す語が無ければもう片側の語を採るので、
小見出しの無い本は従来どおり全ページに章題が出ます。章が始まるときに節題を解除するので、
前章の最後の節が次章の recto を名乗ることはありません。 - 小見出しと keep-with-next —
.keepnextは「後続と離すな」、.subheadはそれに加えて
柱に名前を与えます。TypesetterOptions::$keepWithNextLines(既定 2)が必要な行数です。
規則は「2 行」ではなく「後続の冒頭」——後続段落が丸ごと収まれば取り残しはありません。 - 段組み —
FlowRegion::withColumns(2, gap: 8.0)。分割はインライン軸に沿うので、
横組みでは段が左右に並び、縦組みでは上下に積まれた帯になります(実装は 1 つです)。
ここでもページネータは無改変で、段送りは改ページの一段浅い版にすぎません。なお段組み
が変えるのは行の長さであって紙の消費量ではありません。 - 行グリッド —
TypesetterOptions::$lineGrid。測ってから採用しました。 一様な
散文はすでにグリッドに乗っている(0.003mm)のに対し、ルビが 1 行入るとそのページの
以降が 2.2mm ずれます。 加える余白は行の上に置きます——枠だけ丸めてもルビは親文字
を下へ押し下げるので、ベースラインはグリッドに乗りません。
メモリ ― O(1) が本当になりました
縦書きの流し込みをインクリメンタル化する過程で、流し込みテキストについて定数メモリが
実際には成り立っていなかったことが分かりました。320 ページの文書で一過性ピークが
210MB、ページ数にきれいに比例——このエンジンが存在理由として掲げているものの正反対
です。
原因は 2 つで、どちらも出力を 1 バイトも変えないため、どのテストにも映りませんでした。
LineBreaker::toSegments()が最初の 1 ページを描く前に全文の行を構築していました。
外側をジェネレータにしても、内側が配列を返しては遅延しません。210MB → 37MB。SmartPunctuation::apply()が全文を 1 文字ずつの文字列に分割していました。トークナイザ
は装飾の無い本文をトークン 1 個にまとめるため、小説 0.5MB が約 50 万個の PHP 文字列に
なります。引用符へ跳ぶバイト走査に置き換えました。37MB → 4.4MB。
ページ数 64 倍でピーク 2.5 倍(3,200 段落 / 320 ページ / 47.77MB の PDF を 4.44MB で
生成)。書き換えた SmartPunctuation は旧実装と 60,014 ケースで突き合わせて一致を確認
し、回帰テストは出力ではなくピークを測ります——出力を変えない欠陥は、それ以外の方法
では捕まえられません。
アップグレード時の注意
flowText() / flowInto() が縦書きで例外を投げなくなりました。 これは契約ではなく
明記された制限であり、制限の解除は後方互換です。ただしその LogicException を
catch して別の処理へ逃がしていたコードは、無音で縦書き経路に進みます。 該当する
フォールバックがあれば外してください。
柱が「その語が書かれたフェイス」で描かれるようになりました。 従来は文書既定の
フェイスで描いていたため、section(font: …) に和文フェイスを渡した本でも章題が
ラテン文字のリテラル文字列として出力され、紙面では文字化けしていました。
RunningHeadOptions(font: …) で回避していた場合はそちらが優先されるので変化はありません。
回避していなかった場合、柱が表示されなかった状態から表示される状態に変わります。
それ以外はバイト互換です。 DocumentInfo・和欧混植・行グリッド・脚注・傍注・段組み・
.keepnext・.subhead・ノンブルの振り直し・柱の 2 階層は、すべて既定で無効か、従来の
挙動に落ちます。
2 つの組み合わせは近似せず拒否します(いずれも理由を述べる LogicException):
段組みと脚注(注釈欄はページのものなので全段を埋めるまで深さが決まらず、先に描いた段
を組み直す必要がある)と、縦組みと傍注(縦組みは行が小口方向へ進むので、注の置き場所
が「未実装」ではなく未定義)。
その他
O(1) ストリーミング、欧文組版、フォントサブセット埋込とカスタムフォント、GridTable、
帳票ブロック、回転、.mpb、AES-128、PKCS#7 署名、PDF/A-1b は 1.1.0 から変更ありません。
.mpb フォーマットは変わりません——本リリースはすべてエンジン内部と Document の
API 層で閉じているので、Studio 1.0 が出力したテンプレートは同一に読み込まれ、同一に描画
されます。動作要件も同じ(PHP 8.3+ / ext-mbstring / psr/log / 64bit)です。
品質: src/ 全体に PHPStan level 8、PHPUnit 76 クラス 652 ケース(1,488 アサーション)。
署名と PDF/A は Docker 上で openssl / pdfsig / veraPDF により検証し、縦組み字形は poppler
で描画して確認しています。上記のメモリ値は出力ではなくピークを測るテストが守ります。
examples/ の 22 本が動作するリファレンスで、本リリースでは
examples/vertical_book.php と
examples/horizontal_book.php を追加しました。
PR は受け付けていません(デュアルライセンスは著作権の単独保有が前提のため)。
不具合報告は歓迎しますが、サポートの約束はありません。詳細は
CONTRIBUTING.md、脆弱性は support@2bo-velocity.com へ非公開でご連絡
ください。