Skip to content

Releases: 2bo-velocity/mejiropdf

MejiroPDF 1.2.0

Choose a tag to compare

@2bo-velocity 2bo-velocity released this 09 Aug 11:09

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.2

1.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 like CreditOptions. Mirrored into the XMP packet so PDF/A
    output keeps Info and XMP in agreement; verified with veraPDF.
  • Page\ReadingDirection writes /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 is AUTO: typeset
    anything vertically and the document declares R2L. /PageLayout is
    deliberately not written — /Direction settles the order of a spread, while
    /PageLayout would force two-up on a reader who did not ask for it.
  • /Lang, and Page\OpenFit for /OpenAction.
  • Page\BindingEdge — page numbers on the fore edge, changing side with the
    spread, and mirrored margins through flowInto(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\LatinInVerticalUPRIGHT (default), FULLWIDTH, ROTATE or
    TCY for 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
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: SECTION

Section 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.

...

Read more

MejiroPDF 1.1.0

Choose a tag to compare

@2bo-velocity 2bo-velocity released this 09 Aug 13:45

MejiroPDF 1.1.0

Rotation — and a fix for imported pages that were coming out sideways.

composer require 2bo-velocity/mejiropdf:^1.1

This release is additive: nothing was removed, no signature changed
incompatibly, and a document that does not use the new arguments produces the
same bytes it did under 1.0.0. One behaviour does change, and it is a bug
fix — see Upgrade notes if you import existing PDFs.

Rotation

"Put this at an angle" turns out to be four different operations, depending on
what is being turned. 1.0.0 shipped only the first of them, and only for text.

One element

image() now takes a rotation, turning the picture about the centre of the
box you gave it — the same pivot textObject() has always used, so a caption
set at the same angle stays parallel to the picture.

$pdf->image('photo.jpg', x: 20, y: 60, width: 80, rotation: 45);

The capability already existed inside the template renderer but was private to
it, so anyone writing PHP directly had to hand-assemble a cm matrix and pass
it to appendContentOps(). Both paths now share one implementation.

A whole group

rotated() turns everything a callback draws as one piece, about a pivot in
millimetres:

$pdf->rotated(-90.0, x: 25, y: 60, draw: function (Document $pdf): void {
    $pdf->rect(25, 60, 180, 22, fill: '#1a3a7a');
    $pdf->line(29, 76, 201, 76, color: '#FFFFFF');
    $pdf->text(31, 72, 'Bound edge', color: '#FFFFFF');
});

This is the one that was actually missing. What usually needs turning is not a
lone element but an assembly — a label, an obi band, a sideways table — whose
frame, rules and text have to keep their positions relative to each other.
Passing an angle to each of them separately cannot express that, because each
would spin about its own centre and the assembly would come apart. So there is
deliberately no rotation argument on rect(), line(), grid() or
drawBox(): rotated() is the answer for all of them.

Inside the group you write the coordinates you would have used unturned. Groups
nest, and the angles add up.

Three consequences follow from this being a single PDF transform, and all three
are handled explicitly rather than left to be discovered:

  • strictCanvas stands down inside the group. Only the pivot is checked, on
    the way in. A turned frame's page coordinates no longer describe where the
    marks land, so the check has nothing true to say about them.
  • Opening a page inside the group is refused with a LogicException. A page
    break would split the transform across two content streams and leave the first
    one unbalanced.
  • Link areas do not turn. PDF pins an annotation's /Rect to the page axes,
    so a link() added inside a group stays an upright rectangle over turned text.
    This cannot be fixed, so instead the engine says so once per link on the PSR-3
    logger — the mismatch is otherwise invisible until someone tries to click it.

If the callback throws, the transform is still closed before the exception
leaves. A caller that catches it and carries on gets an upright page, not a
document where everything after the failure is at an angle.

The paper itself

addPage() now takes a rotation, written to the page dictionary's /Rotate:

$pdf->addPage(PageSize::a4(Orientation::LANDSCAPE), rotation: 90);

This turns the sheet, not the marks — an instruction to the viewer and the
printer. It is how a document made of portrait pages carries one wide page,
laid out landscape and presented turned, without the reader's page width jumping
about.

It does not move the drawing origin. Every coordinate on such a page is
still measured on the unturned sheet exactly as on any other page. Values must
be multiples of 90; anything else raises InvalidArgumentException rather than
being rounded, because a viewer would ignore it and the page would quietly come
out upright. A page that does not ask to be turned carries no /Rotate entry at
all.

Fixed: imported pages ignored /Rotate

Scanners and office suites routinely store a landscape page as a portrait
MediaBox plus /Rotate 90
rather than as a landscape box. /Rotate is an
inheritable page attribute, and a producer may set it once on the page tree root
and leave every page silent about it.

1.0.0 resolved /MediaBox and /Resources through the page tree but never read
/Rotate at all, so such a source was imported on its side.

All three import paths now honour it:

Method What changes
editTemplatePage() The new page takes the source's displayed size, so a rotated scan opens landscape with its content upright — and the millimetres you stamp in are the ones you measured on screen
importPage() Native size and aspect ratio are the displayed ones, so asking for a width gives the height you expect
importPageAsBackground() Fills the page in its display orientation

Negative values (-90) are normalised. Values that are not multiples of 90 are
ignored rather than guessed at — leaving a broken producer's page as written
loses less than turning it by a guess.

The rotation is expressed as a placement matrix, not baked into the Form
XObject
. The imported page stays byte-identical to its source, which is the
whole premise of the non-destructive overlay model.

All four paths were checked against an independent renderer: for /Rotate 270
(inherited), 90, and 45 (invalid), poppler's rendering of the source and this
engine's output are pixel-identical.

Upgrade notes

If you import PDFs that carry /Rotate, your output changes — for the
better.
Pages that used to come out sideways now come out upright, and for
editTemplatePage() the page dimensions swap, which moves the millimetre
coordinate space you stamp into. If you built a workaround on top of the old
behaviour — rotating the imported page yourself, or swapping x/y in your field
coordinates — remove it.

Sources without /Rotate, which is most of them, are unaffected.

Everything else is byte-compatible. New arguments all default to no
rotation, and pages that are not turned emit no /Rotate.

One internal detail is visible if you diff raw PDF bytes: the cm that places an
imported page is now always written as six explicit values (1.0000 0.0000 0.0000 1.0000 …) instead of the shorthand 1 0 0 1 …, because one code path now
serves rotated and unrotated placement alike. The matrix is the same and so is
the rendering. Quarter turns also emit exact 0 and ±1 now, rather than the
1e-17 residue deg2rad() leaves behind.

Everything else

Unchanged from 1.0.0: constant-memory streaming, Japanese and Western
typesetting, font subsetting, GridTable, report blocks, compiled .mpb
templates, AES-128, PKCS#7 signatures and PDF/A-1b. See the
1.0.0 release 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 60 test classes (456
cases). Signatures and PDF/A output are verified in Docker against openssl /
pdfsig and veraPDF. The rotation behaviour above is additionally verified by
rasterising the output and comparing it against poppler. The 20 scripts in
examples/ are the working reference — examples/rotation.php
demonstrates all three rotation scopes side by side.

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.1.0 は回転を追加し、取り込んだ PDF が横倒しになる不具合を修正した
リリースです。追加のみで、削除も非互換なシグネチャ変更もありません。新しい引数を使わない
限り、出力は 1.0.0 と同一です。ただし既存 PDF を取り込んでいる場合は挙動が変わります
(後述)。

回転 ― 回す対象の違う 4 つの操作

「角度をつける」は、何を回すかで別の操作になります。1.0.0 が持っていたのはこのうち
テキスト 1 ブロックだけでした。

① 単一要素: image()rotation を追加しました。軸は与えたボックスの中心で、
textObject() と同じピボットです。この機能はテンプレート描画側に実装がありながら
private で、直接 PHP を書く利用者からは cm 行列を手で組んで appendContentOps()
渡すしかありませんでした。両経路が同一実装を共有するようになりました。

$pdf->image('photo.jpg', x: 20, y: 60, width: 80, rotation: 45);

② グループ: rotated() が、コールバックの描画をまとめて 1 つの変換で回します。

$pdf->rotated(-90.0, x: 25, y: 60, draw: function (Document $pdf): void {
    $pdf->rect(25, 60, 180, 22, fill: '#1a3a7a');
    $pdf->text(31, 72, 'Bound edge', color: '#FFFFFF');
});

これが本当に欠けていたものです。 実務で回したいのは単独の要素ではなく、枠・罫線・
テキストからなる組み立て(ラベル、帯、横向きの表)であり、各要素に個別の角度引数を
配る方式ではそれぞれが自分の中心で回って組み立てが崩れます。ゆえに rect() /
line() / grid() / drawBox()rotation 引数は意図的に追加していません
グループ内の座標は「回していなければ書いたであろう座標」で、入れ子にでき、角度は加算
されます。

1 つの PDF 変換であることから来る 3 つの帰結を、発見に委ねず明示的に扱っています。

  • strictCanvas はグループ内で停止し、入口でピボットのみを検査します。回った枠では
    ページ座標がインクの落ちる位置を表さず、検査が真を述べられないためです。
  • グループ内での改ページは LogicException で拒否します。変換が 2 つのコンテンツ
    ストリームに割れ、前者が不均衡なまま残るためです。
  • リンク注釈は回りません。 PDF は注釈の /Rect をページ軸に固定するため原理的に
    不可能です。修正できない以上、リンク 1 件につき 1 度だけ PSR-3 ロガーへ警告します
    ——クリックしてみるまで気づけないズレだからです。

コールバックが例外を投げても変換は必ず閉じます。catch して処理を続けた呼び出し側が、
以降の描画がすべて回ったままの文書を受け取ることはありません。

③ 用紙そのもの: addPage()rotation を追加し、ページ辞書の /Rotate を書きます。

$pdf->addPage(PageSize::a4(Orientation::LANDSCAPE), rotation: 90);

これはマークではなく紙を回す指示で、ビューアと印刷の双方に効きます。縦向きの文書へ
横長のページを 1 枚差し込むとき、読者から見たページ幅を揃えるための機能です。
描画原点は動きません——回転ページでも...

Read more

MejiroPDF 1.0.0 — First public release

Choose a tag to compare

@2bo-velocity 2bo-velocity released this 31 Jul 13:06

MejiroPDF 1.0.1 — the first public release.

A packaging fix. The engine is unchanged: no behaviour, no API and no output
differs from 1.0.0 apart from the version string described below.

composer require 2bo-velocity/mejiropdf

Fixed

  • The 1.0.0 archive was missing tests/fixtures/pdf/. Seven small hand-made
    PDFs that the import tests read were caught by an ignore rule meant for the
    examples' output, so the suite could not be run from the released tag. They
    are source, not artefacts, and are now part of the package.

The fix does not touch src/. If 1.0.0 works for you, it will keep working.

If you compare output bytes

Every generated PDF records the engine version in its /Producer entry, so
documents made with this release read MejiroPDF 1.0.1 where 1.0.0 wrote
MejiroPDF 1.0.0. For PDF/A output the same value appears in the XMP packet.
Nothing else about the bytes changed — if you keep reference files for
regression testing, that one string is the whole diff.

Upgrading

composer update. There is no reason to rush: take this release if you want to
run the test suite from a released tag, or simply on the next update you were
going to make anyway.

Contributing and support

Unchanged from 1.0.0. 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.0.1 はパッケージングの修正版です。エンジンの挙動・API・出力はいずれも
1.0.0 から変わっていません
(後述のバージョン文字列を除く)。

修正内容

  • 1.0.0 のアーカイブに tests/fixtures/pdf/ が入っていませんでした。 インポート
    テストが読む 7 個の小さな PDF が、examples の出力を対象にした ignore 規則に巻き込まれて
    いたためで、公開タグからテストを走らせることができませんでした。これらは生成物ではなく
    ソースなので、パッケージに含めました。

src/ には触れていません。1.0.0 で動いているなら、そのまま動き続けます。

出力バイト列を比較している場合: 生成 PDF の /Producer にエンジンのバージョンが
記録されるため、本リリースで作った文書は MejiroPDF 1.0.1 になります(PDF/A では XMP
パケットにも同じ値が入ります)。変化はこの 1 箇所だけです。

アップグレード: composer update のみ。急ぐ理由はありません。公開タグからテストを
走らせたい場合か、次に更新するついでで十分です。

PR は受け付けていません(デュアルライセンスは著作権の単独保有が前提のため)。
不具合報告は歓迎しますが、サポートの約束はありません。詳細は
CONTRIBUTING.md、脆弱性は support@2bo-velocity.com へ非公開でご連絡
ください。