Skip to content

Backport to Java 17 - #9

Closed
jottinger wants to merge 1 commit into
DemchaAV:mainfrom
jottinger:8/fix/migrate-to-java-17
Closed

Backport to Java 17#9
jottinger wants to merge 1 commit into
DemchaAV:mainfrom
jottinger:8/fix/migrate-to-java-17

Conversation

@jottinger

Copy link
Copy Markdown
Contributor

Fixes #8

Note that the src/test/resources/layout-snapshots/document/nested_list_three_levels.json is changed slightly due to rendering differences based on platform; it may be that a straight merge needs a slight fix to address this, or a standardization. This is probably locale-specific. It may ALSO be that a good idea is to MAKE it locale-specific, so different locales can load different values. I did not do this. I am lazy.

Fixes DemchaAV#8

Note that the src/test/resources/layout-snapshots/document/nested_list_three_levels.json is changed slightly due to rendering differences based on platform; it may be that a straight merge needs a slight fix to address this, or a standardization. This is probably locale-specific. It may ALSO be that a good idea is to MAKE it locale-specific, so different locales can load different values. I did not do this. I am lazy.
@DemchaAV

DemchaAV commented May 8, 2026

Copy link
Copy Markdown
Owner

Thanks for the PR — I really appreciate it.

I’ll review it properly from desktop and run the tests locally before merging, especially because this touches quite a few files and includes a layout snapshot update.

The Java 17 compatibility direction looks very useful, but I want to check the snapshot change carefully so we don’t accidentally make the tests platform- or locale-dependent.

I’ll approve the checks and take a proper look.

@jottinger

Copy link
Copy Markdown
Contributor Author

Okay, some thoughts: this is NOT a "good PR." The sanitization code here collapses multiple nonbreaking spaces into a single breaking space, that's where the layout changes are coming from; that's easy to fix BUT it doesn't fix the font inconsistencies across installations. It's still a change that needs to be part of this PR but this PR is not right.

@DemchaAV

DemchaAV commented May 8, 2026

Copy link
Copy Markdown
Owner

Yeah, no worries - thanks for catching it before merge.

I agree, let’s not merge #9 as-is. I think the cleanest path is to close it and open a new PR with only the Java 17 migration: compiler release, CI JDK, and the Java 21 API replacements.

The font sanitization / unsupported glyph thing was on my mind too, but I think it should be a separate PR. Fonts can miss all kinds of characters, so we probably need a proper fallback strategy there — maybe a square for unknown glyphs, or mappings for common symbols like arrows to "->", so the output keeps at least some meaning. But that’s more of a behavior/design change than a Java 17 compatibility fix.

Also, the NBSP/list indentation issue is exactly why I don’t want to bless the snapshot change in this PR. Better to keep the Java 17 PR clean first, then handle glyph fallback with focused tests.

@DemchaAV DemchaAV closed this May 8, 2026
DemchaAV added a commit that referenced this pull request Aug 11, 2026
The zero-width non-joiner and joiner are how Unicode lets an author
override contextual joining — U+200C forbids a join the letters would
otherwise make, U+200D forces one — and both were deleted before the
shaper, the only thing that reads them, ever ran. They are category C
like any other control, so the sanitizer that runs first removed them
along with the rest, and a document could not express the distinction
at all.

The shaper needed almost nothing for the non-joiner: it is an opaque
non-joining character, so the lookups that walk letters already came to
the right answer once it survived. The joiner is the one that had to be
taught, since it stands in for a letter that is not there. Both are
consumed during shaping, because past that point they are code points no
font can encode — measured as one thing and drawn as a substitution
mark. Text with no Arabic in it never reaches the shaper, so the glyph
seam drops them there instead.

The joining controls get their own predicate rather than joining the
bidirectional ones: these say nothing about direction, only about
whether two letters connect.

Also:

- Auto-size measured a highlight run unshaped while the token it builds
  from that run is shaped, so a chip was sized against a string the
  layout never uses. Both paths shape now. This is not currently visible
  end to end, because auto-size does not shrink a paragraph holding a
  chip at all for any script, which is its own defect.
- The degraded-shaping warning is keyed on the font rather than on
  (font, code point). A font that carries no presentation forms carries
  none of them, so the second letter said nothing the first had not, and
  a document turned one fact about the font into a warning per letter.
- The changelog claimed mirroring per UAX #9 L4 without the scope the
  implementation has always documented: the punctuation that occurs in
  documents, not the whole mirroring table.
DemchaAV added a commit that referenced this pull request Aug 11, 2026
Which way a paragraph runs was worked out twice from the same text. The
builder scanned for the first character of strong directionality to
decide which edge an unaligned paragraph sits at; the layout asked the
bidirectional algorithm. Two readings of one rule, and they did not
agree.

They part company on isolates. UAX #9 rule P2 skips everything between
an isolate initiator and its matching PDI when looking for the first
strong character — that is what an isolate is for — and a plain scan
reads straight into it. A paragraph opening with an isolated Hebrew
quotation therefore aligned to the right edge and laid out from the
left. This is the release that started carrying isolates through control
sanitizing, so the disagreement was reachable by exactly the author who
took the documentation's advice on steering a neutral stretch of text.

ParagraphDirection is now the single answer, and it lives in the layout
package because that is the sanctioned bridge from the canonical surface
to the engine: the builder may call there, and only there does the call
reach the resolver.

Word is the third caller. AUTO used to reach the DOCX export unresolved,
on the reasoning that it had already become a concrete alignment — but
alignment is not direction, and what Word actually received was a
paragraph with no base direction at all, left to infer one. That is the
thing w:bidi exists to prevent, and it came out the other way round for
a line opening with a digit or a parenthesis. The export is now marked
from the same answer the page used, and the test that pinned the old
behaviour asserts the new one in three cases instead.
DemchaAV added a commit that referenced this pull request Aug 11, 2026
Shaping runs before measurement — on the logical lines of the plain and
markdown paths, at token construction on the inline path — because the
contextual forms have their own advance widths and the engine's contract is
that what is measured is what is drawn. Text without an Arabic letter passes
through as the same instances.

Each backend again does only what its medium requires. The PDF draws the
forms, since its content stream will never run the font's own shaping.
PowerPoint gets the base letters back at its seam: it shapes Arabic itself,
and frozen presentation forms would end up in a file users search and copy
from. Word was never handed forms at all. Mirroring lives with reversal at
the PDF seam and nowhere else, for the same reason reversal does — both
PowerPoint and Word apply UAX #9 L4 themselves, and a mirror baked into the
span would come out double-mirrored there.

A font that carries the Arabic letters but not the forms — the GSUB-only
families — now degrades at the glyph seam to unjoined base letters instead
of '?', which costs the joining rather than the text; a lam-alef ligature
decomposes back into its two letters.

The proofs read the content stream: every Arabic glyph on the page is a
presentation form and none a base letter, a parenthesis before Hebrew is
drawn facing what it encloses, and the slide text carries base letters with
no form leaked into it.
DemchaAV added a commit that referenced this pull request Aug 11, 2026
The zero-width non-joiner and joiner are how Unicode lets an author
override contextual joining — U+200C forbids a join the letters would
otherwise make, U+200D forces one — and both were deleted before the
shaper, the only thing that reads them, ever ran. They are category C
like any other control, so the sanitizer that runs first removed them
along with the rest, and a document could not express the distinction
at all.

The shaper needed almost nothing for the non-joiner: it is an opaque
non-joining character, so the lookups that walk letters already came to
the right answer once it survived. The joiner is the one that had to be
taught, since it stands in for a letter that is not there. Both are
consumed during shaping, because past that point they are code points no
font can encode — measured as one thing and drawn as a substitution
mark. Text with no Arabic in it never reaches the shaper, so the glyph
seam drops them there instead.

The joining controls get their own predicate rather than joining the
bidirectional ones: these say nothing about direction, only about
whether two letters connect.

Also:

- Auto-size measured a highlight run unshaped while the token it builds
  from that run is shaped, so a chip was sized against a string the
  layout never uses. Both paths shape now. This is not currently visible
  end to end, because auto-size does not shrink a paragraph holding a
  chip at all for any script, which is its own defect.
- The degraded-shaping warning is keyed on the font rather than on
  (font, code point). A font that carries no presentation forms carries
  none of them, so the second letter said nothing the first had not, and
  a document turned one fact about the font into a warning per letter.
- The changelog claimed mirroring per UAX #9 L4 without the scope the
  implementation has always documented: the punctuation that occurs in
  documents, not the whole mirroring table.
DemchaAV added a commit that referenced this pull request Aug 11, 2026
Shaping runs before measurement — on the logical lines of the plain and
markdown paths, at token construction on the inline path — because the
contextual forms have their own advance widths and the engine's contract is
that what is measured is what is drawn. Text without an Arabic letter passes
through as the same instances.

Each backend again does only what its medium requires. The PDF draws the
forms, since its content stream will never run the font's own shaping.
PowerPoint gets the base letters back at its seam: it shapes Arabic itself,
and frozen presentation forms would end up in a file users search and copy
from. Word was never handed forms at all. Mirroring lives with reversal at
the PDF seam and nowhere else, for the same reason reversal does — both
PowerPoint and Word apply UAX #9 L4 themselves, and a mirror baked into the
span would come out double-mirrored there.

A font that carries the Arabic letters but not the forms — the GSUB-only
families — now degrades at the glyph seam to unjoined base letters instead
of '?', which costs the joining rather than the text; a lam-alef ligature
decomposes back into its two letters.

The proofs read the content stream: every Arabic glyph on the page is a
presentation form and none a base letter, a parenthesis before Hebrew is
drawn facing what it encloses, and the slide text carries base letters with
no form leaked into it.
DemchaAV added a commit that referenced this pull request Aug 11, 2026
The zero-width non-joiner and joiner are how Unicode lets an author
override contextual joining — U+200C forbids a join the letters would
otherwise make, U+200D forces one — and both were deleted before the
shaper, the only thing that reads them, ever ran. They are category C
like any other control, so the sanitizer that runs first removed them
along with the rest, and a document could not express the distinction
at all.

The shaper needed almost nothing for the non-joiner: it is an opaque
non-joining character, so the lookups that walk letters already came to
the right answer once it survived. The joiner is the one that had to be
taught, since it stands in for a letter that is not there. Both are
consumed during shaping, because past that point they are code points no
font can encode — measured as one thing and drawn as a substitution
mark. Text with no Arabic in it never reaches the shaper, so the glyph
seam drops them there instead.

The joining controls get their own predicate rather than joining the
bidirectional ones: these say nothing about direction, only about
whether two letters connect.

Also:

- Auto-size measured a highlight run unshaped while the token it builds
  from that run is shaped, so a chip was sized against a string the
  layout never uses. Both paths shape now. This is not currently visible
  end to end, because auto-size does not shrink a paragraph holding a
  chip at all for any script, which is its own defect.
- The degraded-shaping warning is keyed on the font rather than on
  (font, code point). A font that carries no presentation forms carries
  none of them, so the second letter said nothing the first had not, and
  a document turned one fact about the font into a warning per letter.
- The changelog claimed mirroring per UAX #9 L4 without the scope the
  implementation has always documented: the punctuation that occurs in
  documents, not the whole mirroring table.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Backmigrate to Java 17

2 participants