feat(api): let a table cell say which way its text runs - #558
Merged
Conversation
A cell written as a plain string reached the page through the table's own layout rather than the text pipeline, so it received neither of the two things that make Hebrew and Arabic correct: the same string drew reversed in a cell while drawing properly in a paragraph, and Arabic came out in isolated letters instead of joined. The Hebrew invoice example builds its line items from rows rather than a table for exactly that reason. DocumentTableStyle.direction(...) takes the same LTR / RTL / AUTO a paragraph does and inherits down the cascade a cell style already follows, so one call turns a whole table round. The cell is the unit AUTO reads: two cells under one declared direction answer it separately, and a cell's second line does not run the other way from its first because it opens on Latin. Direction decides the edge only when no anchor was asked for. The column is measured on the joined forms, so an auto column is sized to the text the page draws rather than to a wider form that never appears. Each backend does what it must. The PDF is painted, so the engine shapes and reorders the line and marks it with the text as written. Word is told w:bidi and receives the text untouched. PowerPoint is told the direction on the cell's frame and likewise receives the text as written — which is the opposite of what a paragraph does, and for a reason: a paragraph arrives as one frame per span, so no frame holds a bracket pair for PowerPoint to resolve and the mirroring has to be done first, while a cell is one frame holding a whole line, the input PowerPoint's own algorithm is complete for. Pre-mirroring that line swaps the brackets twice, and (2026) closing an Arabic cell was drawn as )2026(. Word needed a second property. w:bidi settles which edge a line starts from and nothing else; how Word resolves the characters inside a run comes from w:rtl, and a run without it is handled as Latin, with paired punctuation left unmirrored. Every right-to-left run now carries it, in a paragraph as well as a cell — the paragraph path had the same gap since direction shipped, invisible because it needs Arabic, where digits after a letter resolve as an Arabic number rather than a European one. A cell that declares no direction is untouched, so every table laid out before this keeps its geometry and its export. Tests: 20 new. PDF reads the glyph positions off the rendered page for reversal, per-cell AUTO, the whole-cell base direction and both anchor rules; DOCX and PPTX assert the properties each format is owed and that a cell with no direction still writes the file it always did; a qa scenario pins the coordinates and the pixels. Full reactor gate green at 1958 tests, japicmp green against the 2.0.0 baseline.
… run Both table backends sanitised a cell's line with the plain control sanitizer, which removes all of Unicode category C and puts a space in its place. That pass sits above the shaper and above the bidirectional algorithm, so it deleted instructions neither had read yet — and did worse than delete them: a ZWNJ between two Arabic letters became a word break, and the letters the author had asked to keep apart joined up anyway. The direction marks went the same way, leaving the PDF to run the algorithm over a different string than the one layout had resolved the cell's direction from. Both now use TextControlSanitizer.removeExceptFormattingControls, the call the paragraph pipeline already makes. The controls are dropped at the glyph seam instead, where a zero-width character has nothing to draw and measurement and drawing pass through one string. PowerPoint needed a seam of its own. sanitizeForRender is for a backend that draws glyphs; sanitizeForTextExport keeps the joining controls for one that shapes for itself. A table cell is handed over as a whole logical line for PowerPoint to order as well, so the direction marks are part of its input too — sanitizeForLogicalTextExport keeps both classes, with the glyph fallback still applied to everything that draws. The three policies are an enum rather than a second boolean, because they are three answers to one question: which controls has the consumer still to read. Tests: the PPTX cell holds a ZWNJ and an LRI/PDI pair through the export. The PDF measures what it drew — the ZWNJ pair is wider than the naturally joined one and the ZWJ pair is exactly as wide, asserted both ways because the defect collapsed all three to the same spaced-apart width.
…cell "A cell that declares no direction is untouched" was too strong a claim. It holds for Latin and not for the scripts this is about: a declaration settles the direction a line is embedded in, while Hebrew and Arabic run the way they run inside it whatever the base. So an existing cell that declared nothing is now shaped, ordered, and measured on the joined forms — it moves, and it moves because what it drew before was the word backwards. Both halves are now tests rather than a sentence: the Latin case keeps its name with the direction spelled out, and a Hebrew cell declaring nothing asserts the reversal it now gets. The bundled scripts also get a scenario in a table cell, held to the coordinates and to the pixels. The sibling scenario runs all five through paragraphs; the cell path measures and draws on its own, so a font swap, a shaping regression, or a column sized on the wrong form shows up here instead of in whichever document happens to hold that script in a table. Arabic and Hebrew declare a direction there; Georgian, Armenian and Korean do not, and the baseline holds them to not acquiring one.
…rection # Conflicts: # CHANGELOG.md # README.md
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
2.2.0 gave a paragraph a writing direction. A table cell written as a plain string —
table.row("…"),DocumentTableCell.text(…)— never got one, because that path ismeasured and drawn by the table's own layout rather than by the text pipeline. It
therefore received neither half of what makes Hebrew and Arabic correct: the same string
drew reversed in a cell while drawing properly in a paragraph, and Arabic came out in
isolated letters instead of joined. The Hebrew invoice example builds its line items from
rows rather than a table for exactly that reason, and said so in its Javadoc.
What changed
DocumentTableStyle.direction(...)takes the sameLTR/RTL/AUTOa paragraphdoes and inherits down the cascade a cell style already follows — table default, column,
row, cell — so one call turns a whole table round.
AUTOreads.TableLayoutSupport.answerDirectionresolves itagainst that cell's own lines, so two cells under one declared direction answer it
separately, and a cell's second line does not run the other way from its first because it
opens on Latin. The fixed-layout backends consume that answer from the resolved
TableCellLayoutStylerather than re-deriving it; the semantic DOCX backend does not seea
LayoutGraphat all, so it resolves the same public cascade through the sameParagraphDirection— one rule, reached two ways, rather than two rules.already follows for alignment. The author's style cascade is now merged on its own before
the built-in defaults go underneath it, because
TableCellLayoutStyle.DEFAULTfills theanchor for every cell and would answer "somebody set it" every time. The fold is
property-wise last-non-null-wins, so the two orders are equivalent; every existing layout
snapshot and pixel baseline is unchanged.
cellNaturalWidthshapes beforemeasuring). An unjoined Arabic string is wider than the one the page draws, so the column
was being sized to text that never appears. Reordering moves no glyph and changes no
advance, so it has nothing to say to measurement.
ActualTextmarked-content section, so a reader copies out the letters that were typed.mirroring nothing. This is the opposite of what the paragraph handler does, for a reason:
a paragraph arrives as one frame per span, so no frame holds a bracket pair for PowerPoint
to resolve and the swap has to be made first; a cell is one frame holding a whole line,
which is the input PowerPoint's own algorithm is complete for. Pre-mirroring that line
swaps the brackets a second time —
(2026)closing an Arabic cell drew as)2026(.w:bidisettles which edge a line starts from and nothing else; how Word resolves the characters
inside a run comes from
w:rtl, and a run without it is handled as Latin, with pairedpunctuation left unmirrored. Every right-to-left run now carries it, in a paragraph as
well as a cell. The paragraph path has had this gap since direction shipped: it is
invisible in Hebrew and needs Arabic, where digits following a letter resolve as an Arabic
number rather than a European one. Measured in Word one property at a time;
w:csandw:langwere tried and are not required.backends sanitised a cell's line with the plain control sanitizer, which removes all of
Unicode category C — and substitutes a space. That ran above the shaper and the
algorithm: a
ZWNJbetween two Arabic letters did not merely go missing, it became aword break, and the letters the author had separated joined up anyway. Direction marks
went the same way, so the PDF ran the algorithm over a different string than the one
layout resolved the cell's direction from. Both now use
TextControlSanitizer.removeExceptFormattingControls, as the paragraph pipeline does, andthe controls are dropped at the glyph seam once their readers have run.
sanitizeForRenderis for a backend thatdraws glyphs;
sanitizeForTextExportkeeps the joining controls for one that shapes. Atable cell is handed over as a whole logical line for PowerPoint to order as well, so
the direction marks are part of its input too —
PdfFont.sanitizeForLogicalTextExportkeeps both classes, with the glyph fallback still applied to everything that draws.
Arabic text is not, and deliberately: what a declaration settles is the direction a line
is embedded in, while a script runs the way it runs inside that whatever the base. So a
cell that declared nothing is now shaped and ordered too, and its auto column measured on
the joined forms — such a table moves, because what it drew before was the word backwards.
Verification
./mvnw -B -ntp clean verify→ BUILD SUCCESS, 1963 tests, 0 failures (from 1938)../mvnw -DskipTests -P japicmp verify -pl :graph-compose-core→ BUILD SUCCESS againstthe 2.0.0 baseline; the new record component on
TableCellLayoutStylekeeps a delegatingconstructor at the old arity.
+25 tests.
RtlTableCellTest(PDF, 8) reads the glyph positions off the rendered page: Hebrew drawnreversed,
AUTOanswered per cell, one base direction for a whole multi-line cell, thedirection-derived right edge, an explicit anchor winning over it, a Latin cell declaring
nothing left exactly as it was, and a Hebrew cell declaring nothing reversed all the same.
DocxTableCellDirectionTest(6) and the two newDocxRightToLeftTestcases assertw:bidion the paragraph andw:rtlon the runs, that the text itself is handed overuntouched, and that a left-to-right cell or paragraph writes the runs it always did.
PptxTableCellDirectionTest(7) asserts the declared frame direction, logical text, thata bracket is left for PowerPoint to mirror rather than pre-swapped, and that a
ZWNJandan
LRI/PDIpair both survive the hand-off.RtlTableCellTest.theJoiningControlsReachTheShapermeasures what the page drew: theZWNJpair is wider than the naturally joined one, and theZWJpair is exactly as wide— asserted in both directions, because the defect made all three collapse to the same
spaced-apart width.
RtlScenariosVisualTestgains two scenarios, each pinned to both the coordinates and thepixels: a page holding an
RTLtable, anAUTOtable and an undeclared control, and allfive bundled scripts run through the cell path — Arabic and Hebrew declaring a direction,
Georgian, Armenian and Korean held to not acquiring one.
All 105 examples regenerate;
CommittedAssetDriftTestis green, withtext-direction.pdfand
arabic-article.pdfrefreshed — the first gains a table row showing the new call, thesecond because its closing paragraph described the old limitation in Arabic prose.
Notes
textAnchoris dropped there; the direction-derived edge arrives because Word's owndefault is the start of the flow. Column order in a right-to-left table is not mirrored
(
w:tblPr/w:bidiVisualunwritten). Both are recorded in the capability matrix.writeListLinewrites neitherproperty. Unchanged here.
path cannot promise, since it mirrors before the hand-off.
Lane: canonical (
document.table) + shared-engine (document.layout, three renderbackends) — one public builder method, resolved once in layout, honoured per backend.