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'sVertical_Orientationdecidingmixed; the legacyglyph-orientation-verticalvalues Writing Modes 3 maps;text-combine-upright: none | all;directionandunicode-bidiinside vertical lines; logical properties and logical keywords in every mode (M12.1 mapped them inhorizontal-tbonly); 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 isvertical-rl; - vertical glyphs: HarfBuzz's top-to-bottom shaping with the
vertsubstitution, other vertical features (vrt2,vkna,vpal,vkrn) throughfont-feature-settings; vertical metrics fromvhea,vmtxandVORG, synthesized when a face has none; in the PDF,Identity-Vfonts with/W2and/DW2from 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,rtcandrp; thedisplayvaluesruby,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 | underand 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,mmultiscriptswithmprescriptsandnone,munder,mover,munderover,mtable,mtr,mtd,semantics,annotation,annotation-xmlandmaction; their attributes; the CSS MathML Core defines —display: mathandblock 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 OpenTypeMATHtable'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,RTandRPelements with theRubyPositionandRubyAlignattributes, and theWritingModeattribute, in 1.7 and 2.0 output;Formulaelements 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:
html2pdfrenders 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 anmrow, and reported; content MathML insidesemanticsis 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
/Altis 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 asnone;- warichu — not planned: no CSS specification lays it out, M13's refusal of a
Warichuoverride stands, and M28 leaves it out too; ruby-merge,ruby-overhangas a property,ruby-position: inter-character(bopomofo) — reported; a later slice when a document asks;text-emphasis,text-spacing-trim,text-autospace,hanging-punctuationand 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-lrwith 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
| Part | Where | Why |
|---|---|---|
| The axis mapping, writing modes in every formatting context, orthogonal flows, vertical fragmentation, page progression | AdCodicem.Pdf.Html, Layout/ | M12's formatting contexts, parameterized: no second layout |
The UAX #50 table, orientation itemization, vertical shaping, text-combine-upright | AdCodicem.Pdf.Html, Text/ | ADR 43: text analysis ours, shaping HarfBuzz's |
| Ruby boxes, pairing and layout | AdCodicem.Pdf.Html, Boxes/ and Layout/ | An extension of the inline formatting context |
| MathML boxes and layout, the operator dictionary, stretching | AdCodicem.Pdf.Html, Math/ (new) | Layout knows nothing of PDF (M12); a formula is a box tree |
The MATH, vhea, vmtx and VORG readers | Core, Fonts/, internal | Font 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 runs | Core, 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 /AF | Core, 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 serializer | AdCodicem.Pdf.Html, Tagging/ | It reads computed style and the box tree |
| The math face | The package M08's OFL-set ADR chose | ADR 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,toporleftwhere 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
directionalone;text-align: start,float: inline-start,caption-sideand the logical keywords follow. - Line-relative directions are their own:
overandunder— ruby,text-underline-position— are the line-over and line-under sides, which invertical-rlandvertical-lrare the right and the left of the line, and insideways-lrthe 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-rlprogresses right to left: the first page is a left page, so M12.2's:leftand:right, which already follow page progression, swap; the catalog's/ViewerPreferencesgains/Direction /R2L, so that a two-up viewer binds on the right. Page sizes,page-orientationand 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;
orphansandwidowscount vertical lines.
Vertical text
- Itemization. After UAX #9 and UAX #24 (M12.2), runs split by orientation: under
mixed, UAX #50 —Uupright;Rsideways;TuandTrthe vertical alternate glyph where the face has one, upright or sideways otherwise —; underupright, every character upright and every bidirectional run set left to right, as Writing Modes 3 says; undersideways, every character rotated. TheVertical_Orientationtable is generated as span data fromVerticalOrientation.txtof the Unicode versionCharUnicodeInfoimplements, with M08's version test. - Shaping. Upright runs go to HarfBuzz in its top-to-bottom direction, which applies
vert(whether it also appliesvrt2by default is to verify; neither is doubled); sideways runs are shaped horizontally and drawn turned 90° clockwise (counter-clockwise insideways-lr).font-feature-settingspassesvrt2,vkna,vpal,vkrn; the shape cache's key gains the direction. - Metrics. The vertical advance and origin come from
vmtxandvhea, orVORGfor 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), andtext.vertical-metrics-synthesizedonce per face. - Line breaking is direction-independent: UAX #14 with CSS Text 3's
line-breakstrictness for CJK, as M12.2 runs it; the line's length is the block's physical height. Justification by inter-character space, written as verticalTJadjustments. text-combine-upright: allsets the run horizontally as one upright unit 1 em wide; a run wider than 1 em is compressed with the face'shwid,twidorqwidalternates 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
Type0font whose encoding isIdentity-V— writing mode 1 —, its descendant's/W2giving each glyph's vertical displacement and position vector and/DW2the 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-Hfont; bothType0dictionaries 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; ToUnicodekeeps M08's (glyph, text) pairs: avertalternate 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.
- an upright run is drawn with a
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-collapseresolves logically;caption-side: topis 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-rlorsideways-lron ath— 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:
rubyisdisplay: ruby,rbruby-base,rtruby-text,rtcruby-text-container,rpdisplay: none, the annotation'sfont-size50 %. 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-aroundby 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 mathfordisplay="block", themath-depthandmath-styleit sets on scripts and fraction parts,text-transform: math-autoon a single-charactermi. - 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,AxisHeightand their display-style variants; radicals fromRadicalVerticalGap,RadicalRuleThicknessand the degree's kerns; scripts fromSubscriptShiftDown,SuperscriptShiftUp,SubSuperscriptGapMin, italic correction and cut-ins; under and over scripts withaccent,accentunderandmovablelimits;mtableas a CSS table centered on the math axis;mpaddedandmspacewith 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;
stretchyoperators grown to their row's target size, symmetrically about the axis whensymmetric;largeopin display style;movablelimitsturning 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 inNOTICE. - Stretching: the smallest variant in
MathVariantsat least as large as the target, otherwise an assembly of the face's parts — extenders repeated, connectors overlapping by at leastMinConnectorOverlapand at most the parts' own connector lengths —; each part a positioned glyph, their number under a guard (below). - Fonts: the element's
font-familythrough M08's registry. A face without aMATHtable cannot place operators or scripts by its constants: the whole formula falls back to the OFL math face andmath.font-without-math-tablesays so. Token characters the face lacks go through M08's per-cluster fallback. Whether HarfBuzzSharp exposes HarfBuzz'shb_ot_math_*functions is to verify; theMATHtable is read by M08's parser in the core either way, so that the constants are ours to bound. - Text:
math-automaps a single-lettermito 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
RubywithRBandRTchildren in pair order, andRPonly for anrpthe author made visible;RubyPosition(Beforeforover,Afterforunder) andRubyAlign(Start,Center,Justifyforspace-between,Distribute— its 1:2:1 spacing — forspace-around) as layout attributes, from the values ISO 32000-2 §14.8.5.4.4 lists. M13's refusal of-adc-pdf-tag: Rubyoverrides lifts for elements that ruby layout draws, and stands for others. - Writing modes: a
WritingModelayout 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):TbRlforvertical-rlandsideways-rl,TbLrforvertical-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 aFormula— inline inside its paragraph's element, block-level fordisplay="block"— with/Altfromalttext,aria-labeloraria-labelledby(M13's resolution), theLayoutBBoxandPlacementM13 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 theFormulaelement'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) — amathelement the only child of theFormula, 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 theNSOattribute 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;
Bothreaches both. - The serialized MathML is the source's
<math>subtree restricted to MathML Core's elements and attributes, plusalttext, and MathML 4'sintentandarg, 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 insidesemanticsis kept as written.
- As an associated file: one embedded file per formula —
| Output and claims | Auto carries the MathML as | Refused when the options are built | Otherwise reported |
|---|---|---|---|
| 2.0, PDF/UA-2, with PDF/A-4f or 4e or no PDF/A claim | an associated file | — | — |
| 2.0, PDF/UA-2 with PDF/A-4 | structure elements: part 4 embeds only PDF/A files (M28's table) | AssociatedFile, Both | — |
| 2.0, no claim | an associated file | — | — |
| 1.7 with PDF/A-3 (with or without PDF/UA-1) | an associated file, which part 3 admits | StructureElements, Both — 1.7 has no namespaces | — |
| 1.7 with PDF/A-1 or 2, or with PDF/UA-1 alone | nothing: the Formula and its /Alt | AssociatedFile, StructureElements, Both | math.mathml-not-carried |
| 1.7, no claim | nothing | — | 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
Formulaneeds/Alt(Matterhorn checkpoint 17-002; the number to verify): a formula withoutalttextor an ARIA label is a conflict underPdfConformancePolicy, as a figure without alternative text is in M13. Under PDF/UA-2 with the MathML carried,/Altis 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 aFormulaonly.
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, withruby-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;getBoundingClientRectunder 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-faceover the OFL files intests/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.
| Bound | Kind | Why |
|---|---|---|
| MathML nesting, orthogonal flows within orthogonal flows | M12's guard MaxBoxDepth | Each element or flow is a box; limit.box-depth |
| Parts in one stretched assembly — 4,096 proposed, set by measurement | Guard, MaxMathAssemblyParts, limit.math-assembly | A 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 extent | M12'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-depth | Internal constant: clamped to a signed 32-bit integer, as CSS lets a user agent clamp; a scaled font size stops at one layout unit | Each level multiplies by a scale-down below 1; no size under 1/64 px can be drawn |
mspace, mpadded and every math length | Internal 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 run | None needed | It is compressed into 1 em whatever its length |
| Ruby nested inside an annotation | Laid out as inline content, layout.ruby-nested | No bound needed: a case neither the reference nor the corpus has, laid out simply and said |
| MathML serialized per formula | None needed | It 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:
| Code | Severity | Meaning |
|---|---|---|
text.vertical-metrics-synthesized | Information | A face without vertical metrics, used vertically with synthesized ones |
text.combine-compressed | Information | A text-combine-upright run wider than 1 em, compressed or scaled |
layout.vertical-footnote-horizontal | Warning | A footnote called from vertical flow, placed as in horizontal flow |
layout.ruby-nested | Warning | Ruby inside an annotation, laid out as inline content |
math.element-unsupported | Warning | An element outside MathML Core, laid out as an mrow |
math.attribute-unsupported | Information | An attribute outside MathML Core, ignored |
math.font-without-math-table | Warning | The formula's face has no MATH table; the OFL math face was used |
math.mathml-not-carried | Warning | The output version or a claim does not admit the MathML; the Formula carries /Alt only |
math.mathml-pruned | Information | Elements or attributes outside MathML Core left out of the carried MathML |
limit.math-assembly | Warning | The 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.
- The axes, audited. Delivers
FlowAxes, M12's formatting contexts parameterized by it,writing-modecomputed 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, undertext-orientation: upright, the layout invertical-lron a transposed page is the transpose of the layout inhorizontal-tb, to the layout unit —; unit tests per logical property against Writing Modes' table; the web-platform tests ofcss/css-writing-modeswhose tests and references use the declared level, selected as M12 selects. Leaves glyphs. - Vertical glyphs. Delivers the UAX #50 table, orientation itemization, top-to-bottom shaping,
vhea,vmtxandVORGin M08's parser, synthesized metrics,glyph-orientation-vertical, bidirectional runs in vertical lines; in the core,Identity-Vfonts with/W2and/DW2, sideways runs, the shared or separate subset decided with veraPDF. Proved by unit tests — the table againstVerticalOrientation.txtwhole;/W2against the face'svmtx; everyvertalternate of the test face mapping to its source character —; integration:pdffontslistsIdentity-V; PyMuPDF reads our vertical lines as vertical (itswmode, 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. - 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
:leftand:rightswapped and/Direction /R2L, orthogonal flows,sideways-rlandsideways-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; thecss/css-writing-modesandcss/css-pagetests the selection admits. Leaves tables. - 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.
text-combine-upright. Deliversall, compression byhwid,twid,qwidor scaling,digitsreported. 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.- 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, twortc, white space between pairs,rppresent 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; thecss/css-rubytests the selection admits. Leaves their tags. - Tags for vertical text and ruby. Delivers
Ruby,RB,RT,RP,RubyPosition,RubyAlignandWritingModein 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-textgiving each base before its annotation; pikepdf reading the attributes; M15's extraction reading base and annotation from the tags. Leaves MathML. - MathML Core: tokens, rows and the
MATHtable. Delivers theMATHreader 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-autoover the whole Latin and Greek alphabets, the holes included —; themathml/web-platform tests the selection admits; the Chromium layout referee on token fixtures. Leaves the layout schemata. - MathML Core: fractions, radicals, scripts, under and over, tables, stretching. Delivers
mfrac,msqrt,mroot, the script elements withmprescripts,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. - Formulae in the structure. Delivers
Formulawith/Alt, the serializer, the associated file through M14's writer, the MathML-namespace elements,PdfRenderOptions.MathMLand 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 underAssociatedFile,StructureElementsandBoth, 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. - 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,
:leftand:rightinvertical-rl. - Vertical text: the UAX #50 table whole; itemization under each
text-orientation; synthesized metrics;/W2and/DW2;vertalternates inToUnicode; right-to-left runs inside vertical lines;text-combine-uprightselection and scaling; line breaking in vertical lines againstLineBreakTest.txt, unchanged from M12. - Ruby: box fix-up, pairing,
ruby-aligndistribution, 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-autowith its holes;movablelimitsin 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:
mrownested 10⁵ deep; anmtableof 10⁴ rows inside a stretchy fence; a test face whose extender is one unit tall;mspace width="1e9px";mstyleraisingmath-depth10⁶ times; a ruby of 10⁴ annotations; atext-combine-uprightrun of 10⁴ digits; orthogonal flows alternating 500 deep — each ends in a PDF with its diagnostics, or a typed exception underThrowOnLimit, 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 asReaderLimitsTestsdoes for the reader's. - Determinism: every fixture rendered twice, and under two cultures, gives the same bytes.
- Benchmarks:
TypesettingBenchmarkswithMemoryDiagnoser— 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-rawfor 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 —
--checkon 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.
| Documents | Behavior | Verified by |
|---|---|---|
| The vertical reference and the formula reference, against Chromium's rendering of the same sources | Chromium'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 cause | CorpusTypesettingTests.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 element | Each within the regression threshold of Chromium's rendering; each box within 0.5 CSS px of Chromium's getBoundingClientRect | ChromiumRenderingRefereeTests.Typesetting_fixtures_render_as_chromium_does (new), ChromiumLayoutRefereeTests.Typesetting_fixtures_lay_out_as_chromium_does (new) |
| Our renderings of both references | Every page within the regression threshold of its approved image, each approval recorded beside Chromium's rendering | HtmlVisualRefereeTests.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-1 | veraPDF'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 RT | VeraPdfTypesettingRefereeTests.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.0 | One 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 claim | CorpusMathTests.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 opened | pdftotext 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 agrees | CorpusTypesettingTests.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 structure | CorpusTypesettingTests.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, extraction | CorpusReadingTests (existing), CorpusValidationTests (existing), and the classes of M03 and M15 |
| The hostile fixtures of the unit suite, rendered through the public API | Diagnostics or a typed exception within the budgets recorded in status.md; the guard's message names its property | HostileInputTests.Hostile_typesetting_renders_within_its_budget (new, in the existing class) |
| Every fixture and both references | Byte-identical over two renders and two cultures | CorpusTypesettingTests.Typesetting_is_deterministic (new) |
| A vertical novel-length document generated from the reference's paragraphs, and a report of ten thousand formulae | Memory flat across pages, measured at 10, 100 and 1,000 pages as M12 measures; throughput recorded | CorpusTypesettingTests.Long_documents_hold_memory_flat (new), TypesettingBenchmarks (new) |
| The references through the tool | html2pdf with --mathml writes what the API writes | CorpusToolTests.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 overIdentity-VwithoutToUnicode, 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) andvendor/us-federal/distiller6-cdc-west-nile-chinese-traditional.pdf(Arial Unicode MS overIdentity-H), each withtextContainsstrings; remote, Chinese font names in GBK bytes. - Formulae: remote only, and none carrying MathML — the AbleDocs PDF/UA-1 scan with 480
Formulaelements 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
| Need | Why | Priority | Likely 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 three | 1 | Generated here: written by hand, reviewed by a reader of Japanese, as the invoice's source was written |
| Chromium's renderings of both sources, committed | The visual reference must be a committed document, not a rendering made at test time | 1 | Generated 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-2 | 1 | Generated here, by this milestone, recorded in build_corpus.py |
The vertical form's textContains, recorded from MuPDF over its decrypted twin | The form row compares with what M15 reads; M15 lists the same gap at priority 1 | 1 | Filled by M15 (a recorded qpdf --decrypt); M30 cannot close without it |
| A template setting the vertical form's first page | The roadmap's reading of the form needs a rendering of ours to compare with | 1 | Generated 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 table | Chromium and we must draw with the same faces; M08 keeps test faces in tests/fonts/ under 2 MB each | 1 | A 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 elements | Our carriage is judged by veraPDF alone; another producer's shows what readers meet | 2 | Generated 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 furigana | Ruby is proven on our output only | 2 | A 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 ToUnicode | One vertical document, without ToUnicode, cannot show what extractors do with a normal one | 2 | A public source: a ministry publication set vertically, or the official gazette, terms read first; a contribution (W09) |
| Vertical traditional Chinese, and Korean | W09 still lacks Korean; vertical Chinese has no case | 3 | A public source; a contribution (W09) |
Traps
- A horizontal layout hides its assumption in dozens of places:
widthwhere it meant inline size,topwhere 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
:leftand:right. Invertical-rlthe first page is a left page, and a viewer needs/Direction /R2Lto 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
/W2is measured from the glyph's horizontal origin; convert once, test against two renderers. - A
vertalternate is the same character. Mapping it to a vertical presentation form inToUnicodebreaks 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-uprightholds its characters in order inside one upright unit; drawn as rotated glyphs, "12" extracts as "21" in some readers.rpis hidden in ruby layout. It is HTML's fallback; drawing it doubles the reading ("東京(とうきょう)") and tagging it asRPwhen it is not drawn misleads.math-autochanges 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
MATHcannot 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.
/Alton 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
NSOowner (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.mdand 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.mdanddocs/features/features.json: thetypesettingentry brought to its state.docs/architecture.md:Math/in the HTML engine, the vertical glyph path and theMATHreader 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.mdanddocs/corpus-contributions.md: the reference sources, the renderings, the test faces, the new wants.docs/status.md: the budgets, theMaxMathAssemblyPartsmeasurement, 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-Vfonts 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
MATHtable, tagged asFormula, 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.
-
MaxMathAssemblyPartsis 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.
-
TypesettingBenchmarksruns withMemoryDiagnoser;docs/status.mdrecords the budgets and measurements. - The documentation site publishes the pages above, and
docs/features/features.jsonstates the feature as it now exists. - Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).