Skip to main content

M30 — Advanced typesetting

State: to do — Depends on: M12, M28 — Text analysis ours and shaping HarfBuzz's, per ADR 43; PDF/UA-2 output on the PDF 2.0 writer, per ADR 40; fonts and faces loaded as ADR 11 and ADR 38 say; every bound a template can reach classified as ADR 34 classifies the reader's

Goal​

Typeset what the business target does not need first but some documents do — Japanese, Chinese and Korean set in vertical lines, ruby annotations over or beside their base, and mathematical formulae written in MathML Core — laid out as Chromium lays them out, extracted in logical reading order, and tagged so that a PDF/UA-2 document carries each formula's MathML where assistive technology can read it.

M12 lays out none of the three: it sets writing-mode: vertical-* horizontally, prints <ruby> inline with its <rp> parentheses — HTML's own fallback for user agents without ruby layout —, leaves <math> unrendered, and reports each. M13 and M28 left the structure that goes with them — Ruby, RB, RT, RP, Formula with its MathML, the MathML namespace — to this milestone, and M08 left it vertical fonts (Identity-V, vhea, vmtx). The documents that need them are few but real: a Japanese subsidiary's notices and agreements, set vertically as formal Japanese still often is; names on a Japanese form with their reading in furigana; the annex of a loan agreement that states its interest formula, an actuarial or engineering report. The failures this milestone exists to prevent are specific. A vertical line whose commas and long vowel marks sit in the wrong corner because the vert substitution was not applied. A page extracted left to right across its columns, so that every line reads as fragments of ten others. The digits of a date turned on their side where the reader expects them upright and combined. A second page bound on the wrong side, because a right-to-left page progression was not declared. Furigana extracted merged into its base — 東京とうきょう —, or dropped. A fraction bar off the math axis, a parenthesis drawn at its base size around a tall matrix, an italic h taken from a hole in the Unicode block. A formula that a screen reader can only read as "x 2 plus 1". MathML embedded as a file in a PDF/A-2 document, which forbids it. A template whose mrow nests a hundred thousand deep hanging the render.

Scope​

In:

  • Writing modes — CSS Writing Modes Level 3 (a W3C Recommendation), and the values of Level 4 that Chromium ships: writing-mode: horizontal-tb | vertical-rl | vertical-lr | sideways-rl | sideways-lr (sideways-* shipped in Chrome 132); text-orientation: mixed | upright | sideways, with UAX #50's Vertical_Orientation deciding mixed; the legacy glyph-orientation-vertical values Writing Modes 3 maps; text-combine-upright: none | all; direction and unicode-bidi inside vertical lines; logical properties and logical keywords in every mode (M12.1 mapped them in horizontal-tb only); orthogonal flows — a vertical box in a horizontal flow and the reverse, the rotated table header above all; block, inline, float, positioned, table, flex, grid and multi-column layout in vertical flow; fragmentation along a horizontal block axis; page progression right to left when the root is vertical-rl;
  • vertical glyphs: HarfBuzz's top-to-bottom shaping with the vert substitution, other vertical features (vrt2, vkna, vpal, vkrn) through font-feature-settings; vertical metrics from vhea, vmtx and VORG, synthesized when a face has none; in the PDF, Identity-V fonts with /W2 and /DW2 from M08's writer and sideways runs as horizontal runs under a rotated text matrix;
  • ruby — CSS Ruby Annotation Layout Level 1 (a W3C Working Draft) as far as Chromium implements it: HTML's ruby, rb, rt, rtc and rp; the display values ruby, ruby-base, ruby-text, ruby-base-container, ruby-text-container; pairing of bases and annotations, with the anonymous boxes the specification makes for what is missing; ruby-position: over | under and double-sided ruby (two annotation containers); ruby-align: start | center | space-between | space-around; ruby that breaks across lines (both shipped in Chrome 128); in horizontal and vertical lines;
  • MathML Core (a W3C Candidate Recommendation): math, mrow, mi, mn, mo, mtext, ms, mspace, mstyle, mpadded, mphantom, merror, mfrac, msqrt, mroot, msub, msup, msubsup, mmultiscripts with mprescripts and none, munder, mover, munderover, mtable, mtr, mtd, semantics, annotation, annotation-xml and maction; their attributes; the CSS MathML Core defines — display: math and block math, math-style, math-shift, math-depth, font-size: math, text-transform: math-auto; the operator dictionary; spacing and stretching, vertical and horizontal, through the OpenType MATH table's constants, glyph variants and assemblies; painted as text in a math face — the caller's, or an OFL math face added to M08's set;
  • the structure: Ruby, RB, RT and RP elements with the RubyPosition and RubyAlign attributes, and the WritingMode attribute, in 1.7 and 2.0 output; Formula elements with /Alt; under PDF/UA-2 the MathML carried as an associated file — the default — or as structure elements in the MathML namespace, or both; the pre-write checks each conformance target asks for;
  • the reference documents of the acceptance — vertical Japanese with ruby, a report with formulae —, written by hand, rendered by Chromium and by us, approved;
  • the command-line tool: html2pdf renders all three; --mathml auto|file|structure|both|none.

Out, explicitly:

  • LaTeX, AsciiMath or other math input — the caller converts to MathML; OMML, Word's equations, is mapped to MathML by M31, which lays it out through this milestone;
  • MathML beyond Core — menclose, mfenced, mlabeledtr, the elementary-math elements (mstack, mlongdiv), mglyph, malignmark — laid out as MathML Core lays out an unknown element, as an mrow, and reported; content MathML inside semantics is never laid out, and is carried in the associated file as the author wrote it;
  • speech text generated from MathML (MathSpeak, ClearSpeak) — not planned: the MathML itself is what assistive technology reads, and /Alt is the author's;
  • reading MathML back from a received PDF — not planned (M15's position);
  • text-combine-upright: digits — a Level 4 value no browser implements (MDN's compatibility data); reported, and treated as none;
  • warichu — not planned: no CSS specification lays it out, M13's refusal of a Warichu override stands, and M28 leaves it out too;
  • ruby-merge, ruby-overhang as a property, ruby-position: inter-character (bopomofo) — reported; a later slice when a document asks;
  • text-emphasis, text-spacing-trim, text-autospace, hanging-punctuation and the rest of JLREQ's punctuation compression — reported; a later slice when a document asks;
  • footnotes called from vertical flow — no referee implements GCPM footnotes in vertical writing: the note is placed in the page's bottom footnote area, horizontally, as in horizontal flow, and reported;
  • Mongolian and other scripts set vertical-lr with their own shaping rules — not planned; reported;
  • vertical text outside the HTML engine — M08's simple text path, M16's field appearances, M11's annotations — not planned: a vertical stamp is rendered from an HTML fragment (M12.6);
  • color fonts and emoji in vertical lines — an open question of the roadmap.

Dependencies. The roadmap gives M12 (the engine) and M28 (the 2.0 structure namespace, PDF/UA-2 output and its profile). Also used, all earlier in the chain: M08's font parser and glyph-run path, M13's structure emitter, M14's associated-file writer, M15's extraction (vertical lines, the structure reader), M16's decryption (the vendored vertical form is AES-128 with an empty user password), M25's rasterizer for our own checks — never as the referee.

Design​

Where it lives​

PartWhereWhy
The axis mapping, writing modes in every formatting context, orthogonal flows, vertical fragmentation, page progressionAdCodicem.Pdf.Html, Layout/M12's formatting contexts, parameterized: no second layout
The UAX #50 table, orientation itemization, vertical shaping, text-combine-uprightAdCodicem.Pdf.Html, Text/ADR 43: text analysis ours, shaping HarfBuzz's
Ruby boxes, pairing and layoutAdCodicem.Pdf.Html, Boxes/ and Layout/An extension of the inline formatting context
MathML boxes and layout, the operator dictionary, stretchingAdCodicem.Pdf.Html, Math/ (new)Layout knows nothing of PDF (M12); a formula is a box tree
The MATH, vhea, vmtx and VORG readersCore, Fonts/, internalFont parsing is the core's (M08), bounded like every table; the writer needs the same metrics for /W2
Vertical glyph runs — Identity-V, /W2, /DW2 — and rotated sideways runsCore, Fonts/ and Content/ (M08's glyph-run path)M08 left vertical writing here
Ruby, RB, RT, RP; WritingMode, RubyAlign, RubyPosition; MathML-namespace elements; Formula with /AFCore, Structure/ (M13's and M28's builder)Any caller of the builder may tag them
The HTML-to-structure mapping of ruby, writing modes and formulae; the MathML serializerAdCodicem.Pdf.Html, Tagging/It reads computed style and the box tree
The math faceThe package M08's OFL-set ADR choseADR 11: the first attempt works with no face registered

No new package, no new dependency.

Logical axes​

FlowAxes internal struct: writing mode, direction and text orientation -> the physical axis and sign of
inline-start/end, block-start/end and line-over/under; ToPhysical(LogicalRect),
ToLogical(PhysicalRect); one instance per formatting context, no allocation
  • Layout is logical, painting physical. Every formatting context lays out in inline and block offsets and sizes; a fragment's rectangle is converted once, where it is painted — beside the one conversion from layout's y-down to PDF's y-up that the ADR index records. Slice 1 audits every place M12 read x, width, top or left where it meant inline or block, and replaces each, with the test that covers it.
  • Logical properties map through Writing Modes 3 and Logical Properties 1 in every mode, not by direction alone; text-align: start, float: inline-start, caption-side and the logical keywords follow.
  • Line-relative directions are their own: over and under — ruby, text-underline-position — are the line-over and line-under sides, which in vertical-rl and vertical-lr are the right and the left of the line, and in sideways-lr the reverse; checked against Chromium, never derived from block direction.
  • Orthogonal flows — a box whose writing mode is perpendicular to its container's — are sized as Writing Modes 3's section on orthogonal flows says (§7.3, to verify), the fallback available size being the initial containing block's (the first page area's), and are monolithic under fragmentation (whether CSS Fragmentation 3 requires or permits this: to verify at the slice). The rotated table header is the case that matters.
  • Page progression. A root in vertical-rl progresses right to left: the first page is a left page, so M12.2's :left and :right, which already follow page progression, swap; the catalog's /ViewerPreferences gains /Direction /R2L, so that a two-up viewer binds on the right. Page sizes, page-orientation and the sixteen margin boxes stay physical, as Paged Media names them.
  • Vertical fragmentation. The block axis is horizontal: a page holds as many vertical lines as its width allows; M12's break tokens are logical and resume unchanged; orphans and widows count vertical lines.

Vertical text​

  • Itemization. After UAX #9 and UAX #24 (M12.2), runs split by orientation: under mixed, UAX #50 — U upright; R sideways; Tu and Tr the vertical alternate glyph where the face has one, upright or sideways otherwise —; under upright, every character upright and every bidirectional run set left to right, as Writing Modes 3 says; under sideways, every character rotated. The Vertical_Orientation table is generated as span data from VerticalOrientation.txt of the Unicode version CharUnicodeInfo implements, with M08's version test.
  • Shaping. Upright runs go to HarfBuzz in its top-to-bottom direction, which applies vert (whether it also applies vrt2 by default is to verify; neither is doubled); sideways runs are shaped horizontally and drawn turned 90° clockwise (counter-clockwise in sideways-lr). font-feature-settings passes vrt2, vkna, vpal, vkrn; the shape cache's key gains the direction.
  • Metrics. The vertical advance and origin come from vmtx and vhea, or VORG for CFF outlines, read by M08's parser. A face without them gets metrics synthesized as CSS Writing Modes says (the rule read again at the slice; HarfBuzz's own fallback, from the font's ascender and descender, may differ, and only one is used), and text.vertical-metrics-synthesized once per face.
  • Line breaking is direction-independent: UAX #14 with CSS Text 3's line-break strictness for CJK, as M12.2 runs it; the line's length is the block's physical height. Justification by inter-character space, written as vertical TJ adjustments.
  • text-combine-upright: all sets the run horizontally as one upright unit 1 em wide; a run wider than 1 em is compressed with the face's hwid, twid or qwid alternates when it has them and scaled otherwise, as Writing Modes 3 §9.1 allows (the clause to verify), and reported once (text.combine-compressed). The unit keeps its characters in order, so "12" extracts as "12".
  • In the PDF — M08's glyph-run path, extended in the core:
    • an upright run is drawn with a Type0 font whose encoding is Identity-V — writing mode 1 —, its descendant's /W2 giving each glyph's vertical displacement and position vector and /DW2 the commonest pair (the specification's default, [880 −1000], when absent); the conversion from HarfBuzz's vertical origin to PDF's position vector is written once and tested against MuPDF's and pdf.js's drawing of every glyph of a test face;
    • sideways runs and horizontal text in the same face use its Identity-H font; both Type0 dictionaries share one embedded subset when veraPDF's PDF/A font checks accept a program shared by two encodings (to verify in slice 2), and carry two subsets otherwise;
    • ToUnicode keeps M08's (glyph, text) pairs: a vert alternate maps to the character the source wrote — U+3001, U+30FC —, never to a vertical presentation form (U+FE10 to U+FE19, U+FE30 to U+FE4F);
    • lines are drawn in logical order — each top to bottom, lines right to left in vertical-rl —, one marked-content sequence per line fragment, so that an extractor following content order reads logically, and M13's tags say it explicitly.

Tables, flex, grid and columns in vertical flow​

  • Tables follow the writing mode: rows progress in the block direction — right to left in vertical-rl —, cells in the inline direction; M12.3's column widths become the inline sizes of vertical columns; a table longer than a page breaks between rows, and its header rows repeat at the block-start of each fragment, the right edge. border-collapse resolves logically; caption-side: top is the block-start side.
  • Flex and grid take their main and cross axes from the writing mode, as Flexbox 1 and Grid 1 define, fragmented along the block axis.
  • Multi-column places its columns along the inline axis — top to bottom in vertical-rl, the tiers of Japanese dangumi — and balances them as M12.7 does.
  • A vertical cell in a horizontal table — writing-mode: vertical-rl or sideways-lr on a th — is an orthogonal flow: its inline size is the row's height, found by the table's row-height pass, and its content never fragments.

Ruby​

RubyContainerBox internal: an inline-level box whose content is ruby segments
RubySegment bases and annotation levels paired by position; one column per pair
RubyBaseBox, RubyAnnotationBox, RubyAnnotationContainerBox
  • Boxes. The user-agent stylesheet gains CSS Ruby's and the HTML Standard's rules: ruby is display: ruby, rb ruby-base, rt ruby-text, rtc ruby-text-container, rp display: none, the annotation's font-size 50 %. Missing bases, annotations and containers are made anonymous and white space is dropped between them, as CSS Ruby's box fix-up says; bases and annotations pair by position within each segment; a second annotation container is the second level of a double-sided ruby.
  • Layout. Each pair is a column whose width is the wider of its base and annotation; the narrower is distributed by ruby-align — space-around by default, start, center, space-between —; the annotation level sits on the line-over side (over, the right of a vertical line) or the line-under side (under); an annotation taller than the half-leading grows the line box, as CSS Ruby's line-spacing rules say. Whether an annotation may overhang the text around it is left to the user agent by the specification: Chromium's behavior, measured by the referee, is followed and recorded.
  • Breaking. A ruby container breaks between its pairs, as Chrome 128 does; a pair whose base is itself longer than the line follows Chromium's behavior, recorded where the specification leaves it open.
  • In the PDF. Each pair's base run is drawn before its annotation run, each in its own marked-content sequence, so that content order is base then annotation; the structure (below) names both.

MathML Core​

MathBox internal: base of the math boxes — logical and ink extents, italic correction,
top-accent attachment, the embellished operator it carries
MathTokenBox mi, mn, mo, mtext, ms: shaped text; math-auto italics; an operator's properties
MathRowBox, MathFractionBox, MathRadicalBox, MathScriptsBox, MathUnderOverBox, MathPaddedBox, MathSpaceBox
MathTableBox mtable as a CSS table aligned on the math axis
MathConstants the MATH table's MathConstants, in the face's units, scaled by math-depth
MathGlyphVariants vertical and horizontal variants and assemblies, italic correction, top-accent
attachment, cut-in kerning
OperatorDictionary generated data: (text, form) -> lspace, rspace, stretchy, symmetric, largeop, movablelimits
MathMLSerializer the subtree written as an associated file (below)
  • Parsing. AngleSharp builds <math> and its descendants in the MathML namespace, as the HTML Standard's foreign-content rules require (confirmed by slice 8's first test); M12's box builder receives them as MathML elements, and MathML Core's user-agent stylesheet applies — display: block math for display="block", the math-depth and math-style it sets on scripts and fraction parts, text-transform: math-auto on a single-character mi.
  • Layout follows MathML Core's algorithm for each element, in layout units from the face's units: rows and the stretching of their embellished operators; fractions from FractionNumeratorShiftUp, FractionRuleThickness, AxisHeight and their display-style variants; radicals from RadicalVerticalGap, RadicalRuleThickness and the degree's kerns; scripts from SubscriptShiftDown, SuperscriptShiftUp, SubSuperscriptGapMin, italic correction and cut-ins; under and over scripts with accent, accentunder and movablelimits; mtable as a CSS table centered on the math axis; mpadded and mspace with CSS lengths.
  • Operators: the form inferred from the position in its row (prefix, infix, postfix), the dictionary entry for (text, form), spacing in the dictionary's units; stretchy operators grown to their row's target size, symmetrically about the axis when symmetric; largeop in display style; movablelimits turning under and over scripts into sub- and superscripts in inline style. The dictionary is MathML Core's, generated as data once the W3C document license's terms are read, and attributed in NOTICE.
  • Stretching: the smallest variant in MathVariants at least as large as the target, otherwise an assembly of the face's parts — extenders repeated, connectors overlapping by at least MinConnectorOverlap and at most the parts' own connector lengths —; each part a positioned glyph, their number under a guard (below).
  • Fonts: the element's font-family through M08's registry. A face without a MATH table cannot place operators or scripts by its constants: the whole formula falls back to the OFL math face and math.font-without-math-table says so. Token characters the face lacks go through M08's per-cluster fallback. Whether HarfBuzzSharp exposes HarfBuzz's hb_ot_math_* functions is to verify; the MATH table is read by M08's parser in the core either way, so that the constants are ours to bound.
  • Text: math-auto maps a single-letter mi to the Mathematical Alphanumeric Symbols — with Unicode's holes, italic h being U+210E PLANCK CONSTANT, not a character of U+1D400 to U+1D7FF —; the glyphs extract as those characters, as Chromium's output does, and the associated file keeps what the source wrote (<mi>h</mi>).
  • Painting: token glyph runs through M08; fraction bars and radical overbars as filled rectangles; everything inside the formula's marked content.

Structure​

  • Ruby becomes Ruby with RB and RT children in pair order, and RP only for an rp the author made visible; RubyPosition (Before for over, After for under) and RubyAlign (Start, Center, Justify for space-between, Distribute — its 1:2:1 spacing — for space-around) as layout attributes, from the values ISO 32000-2 §14.8.5.4.4 lists. M13's refusal of -adc-pdf-tag: Ruby overrides lifts for elements that ruby layout draws, and stands for others.
  • Writing modes: a WritingMode layout attribute on each block-level element whose mode differs from its parent's, from the four values ISO 32000-2 §14.8.5.4.2 lists (LrTb, RlTb, TbRl, TbLr): TbRl for vertical-rl and sideways-rl, TbLr for vertical-lr; sideways-lr, whose lines progress bottom to top, has no value — the slice decides between leaving the attribute out and the nearest value, and reports it.
  • Formulae: <math> becomes a Formula — inline inside its paragraph's element, block-level for display="block" — with /Alt from alttext, aria-label or aria-labelledby (M13's resolution), the Layout BBox and Placement M13 writes for figures, and the formula's glyphs and rules as its content.
  • MathML carriage is PdfRenderOptions.MathML: Auto (the default), AssociatedFile, StructureElements, Both, None.
    • As an associated file: one embedded file per formula — /Subtype /application#2Fmathml+xml, /AFRelationship /Supplement, a name derived from the formula's position (formula-0012.mml), no date unless the caller supplies one — referenced from the Formula element's /AF, through M14's associated-file writer. ISO 14289-2 §8.2.5.29.1 asks for exactly that association and relationship (as quoted by Ulrike Fischer, Tagging of math with MathML, TUG 2025).
    • As structure elements: the MathML tree as elements of the MathML namespace, which ISO 32000-2 §14.8.6.3 names (M28's PdfStructureNamespace.MathML) — a math element the only child of the Formula, as ISO 14289-2 §8.2.5.29.1 requires (Fischer, TUG 2025) —, each token element owning the marked content of its own glyphs, MathML attributes written in the MathML namespace through the NSO attribute owner.
    • Why the file by default: viewers differ — Foxit passes the associated file to assistive technology, Acrobat the structure elements (Fischer, TUG 2025) —, and the file is the smaller change to the tree; Both reaches both.
    • The serialized MathML is the source's <math> subtree restricted to MathML Core's elements and attributes, plus alttext, and MathML 4's intent and arg, which assistive technology reads (MathML 4's status to verify); namespace-declared, UTF-8, attributes in a fixed order, no DOCTYPE; anything else left out and reported once per formula (math.mathml-pruned). Content MathML inside semantics is kept as written.
Output and claimsAuto carries the MathML asRefused when the options are builtOtherwise reported
2.0, PDF/UA-2, with PDF/A-4f or 4e or no PDF/A claiman associated file——
2.0, PDF/UA-2 with PDF/A-4structure elements: part 4 embeds only PDF/A files (M28's table)AssociatedFile, Both—
2.0, no claiman associated file——
1.7 with PDF/A-3 (with or without PDF/UA-1)an associated file, which part 3 admitsStructureElements, Both — 1.7 has no namespaces—
1.7 with PDF/A-1 or 2, or with PDF/UA-1 alonenothing: the Formula and its /AltAssociatedFile, StructureElements, Bothmath.mathml-not-carried
1.7, no claimnothing—math.mathml-not-carried; an explicit AssociatedFile is honored without raising the version, since M03's version table gives /AF no row (ISO 19005-3 brought it into 1.7-based files)
  • Pre-write checks. Under PDF/UA-1 a Formula needs /Alt (Matterhorn checkpoint 17-002; the number to verify): a formula without alttext or an ARIA label is a conflict under PdfConformancePolicy, as a figure without alternative text is in M13. Under PDF/UA-2 with the MathML carried, /Alt is written when given and not demanded (to verify against ISO 14289-2's text). Every MathML element resolves to the MathML namespace, which M28's containment table accepts under a Formula only.

Minimum versions (ADR 40)​

Vertical writing, ruby and Formula with /Alt need nothing beyond PDF 1.7. MathML structure elements need 2.0. An associated file on a structure element needs 2.0, or a PDF/A-3 claim in 1.7 output. The table above is the computation.

Visual references and referees​

  • Chromium 141, M12's pinned version, through Playwright in a container, implements all three — vertical writing (with sideways-* since 132), ruby (breakable, with ruby-align, since 128) and MathML Core (since 109) —: it is the visual reference. Its PDF of each fixture and each reference source, and ours, are rasterized by the same MuPDF and compared with Pillow and NumPy in the container, as M12 does; getBoundingClientRect under print emulation gives the geometry of vertical blocks, ruby bases and annotations and MathML elements.
  • Thresholds are M12's, and no new one: a fixture — one vertical line, one ruby pair, one formula — within the regression threshold of Chromium's rendering with the same faces; a whole document by structure — page count, the page each heading and formula lands on, the lines each paragraph is set in, or the difference recorded with its cause — and each page within the regression threshold of its approved image, approved side by side with Chromium's (tests/visual/APPROVALS.md).
  • The same faces: the sources declare @font-face over the OFL files in tests/fonts/, reached by us through an allowed directory and by Chromium through its file access.

Bounds, classified​

As M12 classifies them: a guard is an HtmlRenderLimits property with its limit.* code, thrown under ThrowOnLimit; a constant says here why no valid input reaches it.

BoundKindWhy
MathML nesting, orthogonal flows within orthogonal flowsM12's guard MaxBoxDepthEach element or flow is a box; limit.box-depth
Parts in one stretched assembly — 4,096 proposed, set by measurementGuard, MaxMathAssemblyParts, limit.math-assemblyA valid fence around a table of ten thousand rows needs thousands of extenders; a face whose extender is one unit tall asks for millions. Past the guard, the assembly stops at its length and the operator is reported
mtable extentM12's guard MaxPages; columnspan and rowspan clamped as HTML's colspan (1,000) and rowspan (65,534)MathML Core defines these attributes after HTML's (to verify)
math-depthInternal constant: clamped to a signed 32-bit integer, as CSS lets a user agent clamp; a scaled font size stops at one layout unitEach level multiplies by a scale-down below 1; no size under 1/64 px can be drawn
mspace, mpadded and every math lengthInternal constant: the layout unit's range, as for every CSS length (M12)A 32-bit layout unit is the page's own limit
A text-combine-upright runNone neededIt is compressed into 1 em whatever its length
Ruby nested inside an annotationLaid out as inline content, layout.ruby-nestedNo bound needed: a case neither the reference nor the corpus has, laid out simply and said
MathML serialized per formulaNone neededIt is a copy of the source's subtree, which the source's size bounds

Diagnostics​

In PdfDiagnosticCodes, disjoint from rule identifiers (ADR 36), each with the element's source location as M12's are:

CodeSeverityMeaning
text.vertical-metrics-synthesizedInformationA face without vertical metrics, used vertically with synthesized ones
text.combine-compressedInformationA text-combine-upright run wider than 1 em, compressed or scaled
layout.vertical-footnote-horizontalWarningA footnote called from vertical flow, placed as in horizontal flow
layout.ruby-nestedWarningRuby inside an annotation, laid out as inline content
math.element-unsupportedWarningAn element outside MathML Core, laid out as an mrow
math.attribute-unsupportedInformationAn attribute outside MathML Core, ignored
math.font-without-math-tableWarningThe formula's face has no MATH table; the OFL math face was used
math.mathml-not-carriedWarningThe output version or a claim does not admit the MathML; the Formula carries /Alt only
math.mathml-prunedInformationElements or attributes outside MathML Core left out of the carried MathML
limit.math-assemblyWarningThe guard above; the message names HtmlRenderLimits.MaxMathAssemblyParts

M12's html.element-unsupported stops firing for <math>, <ruby> and vertical writing; css.value-unsupported covers text-combine-upright: digits, ruby-position: inter-character and the other values left out.

Memory and determinism​

  • A page at a time, as M12: a formula is laid out when its line is; the serialized MathML of a page's formulae is written with the page, through M03's forward-only writer; nothing about a formula survives its page but its associated file's object number.
  • Every metric is converted from the face's units to layout units once, in integers; stretching and assemblies are computed in layout units; the same inputs give the same bytes on every platform, as M12 requires.

Slices​

Each slice ends on a green commit, with its codes and properties documented, the declared CSS level's rows moved, its web-platform tests added to the expectations file, and its benchmark, if it has one, recorded in docs/status.md.

  1. The axes, audited. Delivers FlowAxes, M12's formatting contexts parameterized by it, writing-mode computed and inherited (horizontal-tb, vertical-rl, vertical-lr), logical properties in every mode, the audit list in the commit — each horizontal assumption found, with the test now covering it. Proved by an FsCheck property — for any generated tree of blocks, inlines, floats and tables set in a test face whose vertical advances equal its horizontal ones, under text-orientation: upright, the layout in vertical-lr on a transposed page is the transpose of the layout in horizontal-tb, to the layout unit —; unit tests per logical property against Writing Modes' table; the web-platform tests of css/css-writing-modes whose tests and references use the declared level, selected as M12 selects. Leaves glyphs.
  2. Vertical glyphs. Delivers the UAX #50 table, orientation itemization, top-to-bottom shaping, vhea, vmtx and VORG in M08's parser, synthesized metrics, glyph-orientation-vertical, bidirectional runs in vertical lines; in the core, Identity-V fonts with /W2 and /DW2, sideways runs, the shared or separate subset decided with veraPDF. Proved by unit tests — the table against VerticalOrientation.txt whole; /W2 against the face's vmtx; every vert alternate of the test face mapping to its source character —; integration: pdffonts lists Identity-V; PyMuPDF reads our vertical lines as vertical (its wmode, the key to verify) and in order; MuPDF's and pdf.js's rendering of each glyph of the test face on its position vector; single-line fixtures within the regression threshold of Chromium's. Leaves pages.
  3. Vertical blocks, pages and orthogonal flows. Delivers vertical block and inline layout end to end, fragmentation along the horizontal block axis, right-to-left page progression with :left and :right swapped and /Direction /R2L, orthogonal flows, sideways-rl and sideways-lr. Proved by the Chromium layout referee on block fixtures in each mode (border boxes within 0.5 CSS px); the vertical reference's page count and headings' pages equal to Chromium's; pikepdf reading /Direction; a rotated-header fixture; the css/css-writing-modes and css/css-page tests the selection admits. Leaves tables.
  4. Tables, flex, grid and columns in vertical flow. Delivers M12.3, M12.4 and M12.7's contexts in vertical modes, header rows repeated at the right edge, dangumi, the footnote fallback. Proved by the Chromium layout referee on table, flex, grid and multi-column fixtures within 0.5 CSS px; a vertical table spanning three pages read by pdftotext and PyMuPDF row by row. Leaves combined digits.
  5. text-combine-upright. Delivers all, compression by hwid, twid, qwid or scaling, digits reported. Proved by unit tests (which alternate is chosen for two, three and four digits; a run of forty); fixtures — a date 令和8年12月31日, a clause number, a percentage — within the regression threshold of Chromium's rendering; extraction in order. Leaves ruby.
  6. Ruby. Delivers the ruby boxes, pairing, ruby-position, ruby-align, double-sided ruby, line breaking, in both directions. Proved by unit tests over CSS Ruby's box fix-up cases (a missing base, a missing annotation, two rtc, white space between pairs, rp present or absent); the Chromium layout referee on the base and annotation boxes of each fixture within 0.5 CSS px; single-line fixtures within the regression threshold; the css/css-ruby tests the selection admits. Leaves their tags.
  7. Tags for vertical text and ruby. Delivers Ruby, RB, RT, RP, RubyPosition, RubyAlign and WritingMode in the core's builder and M13's emitter, in the 1.7 and 2.0 namespaces. Proved by unit tests over the emitted trees; integration: veraPDF's PDF/UA-1 profile on 1.7 output and its PDF/UA-2 profile on 2.0 output, with no error, on the vertical and ruby fixtures; pdfinfo -struct-text giving each base before its annotation; pikepdf reading the attributes; M15's extraction reading base and annotation from the tags. Leaves MathML.
  8. MathML Core: tokens, rows and the MATH table. Delivers the MATH reader in M08's parser, the operator dictionary as data, the token elements, mrow, mstyle, mpadded, mphantom, mspace, merror, semantics, maction, the MathML CSS properties, font fallback, the math face added to the OFL set by an amendment of M08's ADR with its measured size. Proved by unit tests — each token's geometry against values computed by hand from the test face's constants; form inference and every dictionary entry against MathML Core's table; math-auto over the whole Latin and Greek alphabets, the holes included —; the mathml/ web-platform tests the selection admits; the Chromium layout referee on token fixtures. Leaves the layout schemata.
  9. MathML Core: fractions, radicals, scripts, under and over, tables, stretching. Delivers mfrac, msqrt, mroot, the script elements with mprescripts, munder, mover, munderover, mtable, vertical and horizontal stretching, the assembly guard. Proved by unit tests per element and per constant, an assembly whose connectors overlap at their minimum and at their maximum, the guard reached, raised and thrown; the web-platform tests; each formula fixture within the regression threshold of Chromium's rendering, its elements' boxes within 0.5 CSS px. Leaves their structure.
  10. Formulae in the structure. Delivers Formula with /Alt, the serializer, the associated file through M14's writer, the MathML-namespace elements, PdfRenderOptions.MathML and its table, the pre-write checks, the tool's --mathml. Proved by unit tests over each row of the carriage table and each refusal; integration: veraPDF's PDF/UA-2 profile with no error under AssociatedFile, StructureElements and Both, its PDF/UA-1 profile on 1.7 output with /Alt, its PDF/A-3b and 4f profiles with the associated files; every associated file valid against the W3C MathML schema (the version to verify) in lxml, and equal, canonicalized, to the source's subtree as pruned; pikepdf walking the MathML-namespace tree. Leaves the whole.
  11. The references, accepted. Delivers the reference sources and the vertical-form template, their Chromium renderings and ours committed (the corpus gaps below), the approved images, TypesettingBenchmarks, the documentation. Proved by the acceptance conditions below.

Tests required​

Unit — tests/AdCodicem.Pdf.Tests, under Typesetting/, in new test classes:

  • Axes: the transposition property; logical properties and keywords in each mode; line-relative sides in each mode; orthogonal-flow sizing; page progression, :left and :right in vertical-rl.
  • Vertical text: the UAX #50 table whole; itemization under each text-orientation; synthesized metrics; /W2 and /DW2; vert alternates in ToUnicode; right-to-left runs inside vertical lines; text-combine-upright selection and scaling; line breaking in vertical lines against LineBreakTest.txt, unchanged from M12.
  • Ruby: box fix-up, pairing, ruby-align distribution, breaking inside a container, double-sided ruby, a vertical line's line-over side.
  • MathML: each element against hand-computed geometry; operator form inference; the dictionary; stretching to a target, by variant and by assembly; math-auto with its holes; movablelimits in inline and display style; font fallback with its diagnostic.
  • Structure: the trees for ruby, writing modes and formulae; every row of the carriage table; the pre-write conflict under PDF/UA-1; the associated file's entries; the serializer's canonical form and pruning.
  • Hostile: mrow nested 10⁵ deep; an mtable of 10⁴ rows inside a stretchy fence; a test face whose extender is one unit tall; mspace width="1e9px"; mstyle raising math-depth 10⁶ times; a ruby of 10⁴ annotations; a text-combine-upright run of 10⁴ digits; orthogonal flows alternating 500 deep — each ends in a PDF with its diagnostics, or a typed exception under ThrowOnLimit, within its time and allocation budget. M12's FsCheck generator of HTML and CSS gains writing modes, ruby and MathML, and the property holds: diagnostics or a typed exception, never another exception, a hang or an allocation the input chose. The guard is reached, raised and thrown as ReaderLimitsTests does for the reader's.
  • Determinism: every fixture rendered twice, and under two cultures, gives the same bytes.
  • Benchmarks: TypesettingBenchmarks with MemoryDiagnoser — vertical lines laid out per second and bytes per line once warm, formulae laid out per second, the associated files' cost per formula.

Integration — tests/AdCodicem.Pdf.IntegrationTests, every referee in a container (ADR 27):

  • Chromium 141 through Playwright — geometry under print emulation, and PDF renderings of every fixture and reference source with the same faces;
  • MuPDF — rasterizing both renderings; PyMuPDF — text, line direction and order;
  • poppler — pdftotext (with -raw for content order), pdffonts, pdfinfo -struct-text;
  • veraPDF — PDF/UA-1, PDF/UA-2, PDF/A-3b and PDF/A-4f;
  • pikepdf — /ViewerPreferences, structure attributes, namespaces, associated files and their entries;
  • lxml with the W3C MathML RELAX NG schema — every associated file;
  • qpdf — --check on every document this milestone writes.

External suites: the web-platform tests of css/css-writing-modes, css/css-ruby and mathml/, selected and run as M12 runs its selection, with the same expectations file; the Unicode Character Database's VerticalOrientation.txt at the version CharUnicodeInfo implements.

Acceptance conditions​

"The vertical reference" is a hand-written source in vertical Japanese — headings, justified paragraphs with ruby, dates in text-combine-upright, a table, a rotated table header in a horizontal annex, and dangumi —; "the formula reference" a report with inline and display formulae — fractions, radicals, scripts, a matrix in stretched fences, large operators with limits, an interest formula. Both are not in the corpus (below). "Chromium" is M12's pinned Chromium 141 with the same faces; "the regression threshold" is M12's.

DocumentsBehaviorVerified by
The vertical reference and the formula reference, against Chromium's rendering of the same sourcesChromium's page count; each heading and each display formula on the page where Chromium puts it; each paragraph in the number of vertical lines Chromium sets it in, or the difference recorded with its causeCorpusTypesettingTests.References_paginate_as_chromium_does (new)
The fixture pages — a vertical line in each orientation, combined digits, ruby pairs in both directions, one formula per MathML Core elementEach within the regression threshold of Chromium's rendering; each box within 0.5 CSS px of Chromium's getBoundingClientRectChromiumRenderingRefereeTests.Typesetting_fixtures_render_as_chromium_does (new), ChromiumLayoutRefereeTests.Typesetting_fixtures_lay_out_as_chromium_does (new)
Our renderings of both referencesEvery page within the regression threshold of its approved image, each approval recorded beside Chromium's renderingHtmlVisualRefereeTests.Typesetting_references_match_their_approved_images (new)
Both references in 2.0 output under PDF/UA-2, and in 1.7 output under PDF/UA-1veraPDF's profiles report no error; every formula is a Formula, with its MathML as the carriage table says and /Alt where the source gives it; every ruby a Ruby with its RB and RTVeraPdfTypesettingRefereeTests.References_pass_pdf_ua_1_and_2 (new)
The formula reference under AssociatedFile, StructureElements and Both; under PDF/A-3b in 1.7 and PDF/A-4f and PDF/A-4 in 2.0One associated file per formula, Supplement, application/mathml+xml, valid against the MathML schema, equal to the source's subtree as pruned; the structure elements' tree equal to the same MathML; PDF/A-4 carrying structure elements only; veraPDF upholding each PDF/A claimCorpusMathTests.Formulae_carry_their_mathml_as_their_claims_allow (new)
The vertical reference, and fixtures setting the textContains strings of vendor/us-federal/indesign-irs-pub1-chinese-traditional.pdf and vendor/us-federal/distiller6-cdc-west-nile-chinese-traditional.pdf in vertical-rl — read from the manifest, no file openedpdftotext and PyMuPDF extract every string in logical order, PyMuPDF reading the lines as vertical; pdfinfo -struct-text gives each ruby base before its annotation; M15's extraction agreesCorpusTypesettingTests.Vertical_text_and_ruby_extract_in_logical_order (new)
vendor/jp-nta/indesign-distiller18-nta-gift-tax-vertical.pdf, opened with its empty user password, and a template setting its first page's vertical headings and instructions — not in the corpus (below)M15 reads the form's vertical text top to bottom, columns right to left, as its recorded textContains says (M15's gap); our rendering of the template gives the same strings in the same order through M15 and pdftotext, and Chromium's page structureCorpusTypesettingTests.The_vertical_form_reads_in_order (new)
Our renderings of both references, committed — not in the corpus (below)They pass every earlier milestone's acceptance on committed documents: reading, validation with the findings recorded, round trip, extractionCorpusReadingTests (existing), CorpusValidationTests (existing), and the classes of M03 and M15
The hostile fixtures of the unit suite, rendered through the public APIDiagnostics or a typed exception within the budgets recorded in status.md; the guard's message names its propertyHostileInputTests.Hostile_typesetting_renders_within_its_budget (new, in the existing class)
Every fixture and both referencesByte-identical over two renders and two culturesCorpusTypesettingTests.Typesetting_is_deterministic (new)
A vertical novel-length document generated from the reference's paragraphs, and a report of ten thousand formulaeMemory flat across pages, measured at 10, 100 and 1,000 pages as M12 measures; throughput recordedCorpusTypesettingTests.Long_documents_hold_memory_flat (new), TypesettingBenchmarks (new)
The references through the toolhtml2pdf with --mathml writes what the API writesCorpusToolTests.Html2pdf_typesets_as_the_api_does (new)

Corpus​

What the corpus holds​

  • Vertical writing: one committed document, vendor/jp-nta/indesign-distiller18-nta-gift-tax-vertical.pdf — a two-page Japanese gift-tax return from InDesign and Distiller, vertical CID fonts over Identity-V without ToUnicode, AES-128 with an empty user password (vertical-writing, cjk, no-tounicode) —, and no expected text for it yet.
  • CJK in horizontal writing: vendor/us-federal/indesign-irs-pub1-chinese-traditional.pdf (CID-keyed CFF and TrueType subsets) and vendor/us-federal/distiller6-cdc-west-nile-chinese-traditional.pdf (Arial Unicode MS over Identity-H), each with textContains strings; remote, Chinese font names in GBK bytes.
  • Formulae: remote only, and none carrying MathML — the AbleDocs PDF/UA-1 scan with 480 Formula elements and spoken alternative text (formula-structure-with-alt-text), and TeX's output (remote/opf-format-corpus/jhove-hul-129-latex-distiller705-journal-article.pdf, jhove-hul-80-quartz-latex-dissertation.pdf), untagged.
  • Ruby: nothing.
  • The reference sources of M12 — tests/corpus/sources/invoice-fr.html, report-fr.html, contract-fr.html — in horizontal French only.

What it lacks​

NeedWhyPriorityLikely source
The two reference sources — vertical Japanese with ruby, combined digits, a table, a rotated header and dangumi; a report with formulae — written on fictitious content, in tests/corpus/sources/Every acceptance row renders them; nothing in the corpus sets any of the three1Generated here: written by hand, reviewed by a reader of Japanese, as the invoice's source was written
Chromium's renderings of both sources, committedThe visual reference must be a committed document, not a rendering made at test time1Generated here: build_corpus.py through Chromium 141, the producer's version recorded
Our renderings of both, in 1.7 under PDF/UA-1 and in 2.0 under PDF/UA-2, committed"What we produce must be as readable as what we consume"; M28's own renderings are the same gap for PDF/UA-21Generated here, by this milestone, recorded in build_corpus.py
The vertical form's textContains, recorded from MuPDF over its decrypted twinThe form row compares with what M15 reads; M15 lists the same gap at priority 11Filled by M15 (a recorded qpdf --decrypt); M30 cannot close without it
A template setting the vertical form's first pageThe roadmap's reading of the form needs a rendering of ours to compare with1Generated here from the form's text, under its Public Data License 1.0 (compatible with CC BY 4.0, as the manifest records), attributed
OFL test faces: a CJK face with vhea, vmtx and vert under 2 MB, and a math face with a MATH tableChromium and we must draw with the same faces; M08 keeps test faces in tests/fonts/ under 2 MB each1A subset of Noto Serif CJK JP or Source Han Serif (OFL) — a subset is a Modified Version under the OFL, so the Reserved Font Name is checked first —; STIX Two Math or Noto Sans Math (OFL); test data, not corpus documents
A third-party PDF whose formulae carry MathML, as associated files and as structure elementsOur carriage is judged by veraPDF alone; another producer's shows what readers meet2Generated here: LuaLaTeX with \DocumentMetadata{tagging=on}, unicode-math and luamml — the associated file by default, structure elements with tagging-setup={math/setup=mathml-SE} (Fischer, TUG 2025) — from a LaTeX release that adds MathML (since November 2024, per the same talk), pinned in a container (which TeX Live packaging carries it to verify)
A third-party PDF with furiganaRuby is proven on our output only2A public source: a Japanese government document in plain Japanese with furigana, under the Government of Japan Standard Terms of Use 2.0 (CC BY 4.0 compatible; each document's terms read first)
A second vertical Japanese document from another producer, with ToUnicodeOne vertical document, without ToUnicode, cannot show what extractors do with a normal one2A public source: a ministry publication set vertically, or the official gazette, terms read first; a contribution (W09)
Vertical traditional Chinese, and KoreanW09 still lacks Korean; vertical Chinese has no case3A public source; a contribution (W09)

Traps​

  • A horizontal layout hides its assumption in dozens of places: width where it meant inline size, top where it meant block-start. The audit comes first, and the transposition property is what keeps it honest.
  • Over is not up. In a vertical line the line-over side is the right; ruby and the underline's position follow the line, not the page.
  • Page progression flips :left and :right. In vertical-rl the first page is a left page, and a viewer needs /Direction /R2L to bind it on the right.
  • Vertical metrics are often missing, and HarfBuzz's fallback is not necessarily CSS's: one rule, the specification's, and a diagnostic.
  • PDF's vertical origin is not HarfBuzz's. The position vector of /W2 is measured from the glyph's horizontal origin; convert once, test against two renderers.
  • A vert alternate is the same character. Mapping it to a vertical presentation form in ToUnicode breaks search; the (glyph, text) pair keeps the source's.
  • Extraction and visual order diverge most in vertical text. Content order, marked content and tags must all say top to bottom, right to left; an extractor that sorts by y first reads across columns.
  • text-combine-upright holds its characters in order inside one upright unit; drawn as rotated glyphs, "12" extracts as "21" in some readers.
  • rp is hidden in ruby layout. It is HTML's fallback; drawing it doubles the reading ("東京(とうきょう)") and tagging it as RP when it is not drawn misleads.
  • math-auto changes the characters. An italic x is U+1D465, and italic h is U+210E, outside the block: the mapping has holes. Extraction gives the mathematical characters; the MathML keeps the source's.
  • A face without MATH cannot lay out math. Falling back glyph by glyph mixes two faces' constants in one formula; fall back for the whole formula.
  • An assembly's extenders are counted by the face. A one-unit extender makes a fence of millions of glyphs; the guard is what stops it.
  • PDF/A decides where MathML may go. Parts 1 and 2 embed no such file, part 4 only PDF/A files, 1.7 has no namespaces: the carriage table is not a preference.
  • /Alt on a formula is not optional under PDF/UA-1, and generating it is not ours to do: without the author's text, the claim is the conflict.
  • MathML attributes are in no namespace in XML and in the MathML namespace in PDF, through the NSO owner (Fischer, TUG 2025). Written without the owner, readers take them for PDF layout attributes.
  • Chromium is the reference, not the specification. Where CSS Ruby or MathML Core leaves latitude — overhang, breaking inside a long base — the behavior followed is Chromium's, recorded with its reason, so that a later change of Chromium is a reviewed decision.

Documentation​

  • docs/website/docs/guides/vertical-text-and-ruby.md (new): writing modes, orientation, combined digits, ruby, page progression, the faces to register, what is left out.
  • docs/website/docs/guides/mathml.md (new): MathML Core in templates, the math face, alttext, the carriage options and the table of what each claim admits, what is left out.
  • The declared CSS level generated from the property table (M12.1's css-support.md): writing modes, ruby and the MathML properties moved to Supported, the values left out marked.
  • The accessibility guide M13 wrote and M28 extended: ruby, writing modes and formulae in the structure.
  • docs/website/docs/reference/diagnostics.md and the page of the HTML engine's limits M12 wrote: the codes and the guard above.
  • docs/website/docs/reference/tool/: html2pdf --mathml.
  • docs/website/docs/introduction.md and docs/features/features.json: the typesetting entry brought to its state.
  • docs/architecture.md: Math/ in the HTML engine, the vertical glyph path and the MATH reader in the core.
  • NOTICE: the math face's OFL text, MathML Core's operator dictionary (W3C license), UAX #50's data (Unicode license).
  • The amendment of M08's OFL-set ADR for the math face.
  • docs/corpus.md, docs/corpus-sources.md and docs/corpus-contributions.md: the reference sources, the renderings, the test faces, the new wants.
  • docs/status.md: the budgets, the MaxMathAssemblyParts measurement, the behaviors recorded from Chromium.

Exit criteria​

  • Vertical writing — every mode above, orientation, combined digits, orthogonal flows, right-to-left page progression — is laid out in every formatting context, written with Identity-V fonts and extracted in logical order.
  • Ruby is laid out, tagged and extracted, base before annotation, in both directions.
  • MathML Core is laid out from the MATH table, tagged as Formula, and its MathML carried as the table of claims says; the math face is in the OFL set under its amended ADR.
  • Every value, element and feature left out is reported.
  • MaxMathAssemblyParts is a guard with its code, tests and documentation; every other bound is classified where it is declared.
  • The priority-1 gaps above are filled — the form's text by M15 —; each remaining gap is recorded in docs/corpus-contributions.md.
  • The acceptance conditions above pass on the corpus, in CI, with no document skipped.
  • Unit tests cover each behavior, its degenerate cases and its hostile ones; the transposition and hostile-input properties hold.
  • Integration tests run Chromium, MuPDF, PyMuPDF, poppler, veraPDF, pikepdf, lxml's MathML validation and qpdf, each in a container; the web-platform selection runs with its expectations file.
  • TypesettingBenchmarks runs with MemoryDiagnoser; docs/status.md records the budgets and measurements.
  • The documentation site publishes the pages above, and docs/features/features.json states the feature as it now exists.
  • Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).