Guide figures render with a bundled font instead of the host's - #5668

Merged
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness
Sep 2, 2026
Merged

Guide figures render with a bundled font instead of the host's#5668
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Stacked on #5664. Review that one first; this diff is only the last commit.

The defect

None of the 24 generated figures can be reproduced outside CI. Running the generator on a Mac reproduces 0 of 24 byte-for-byte, with 3–20% of pixels differing.

Two consecutive local runs are byte-identical, so the generator is deterministic on one host. The variable is the font:

  • PreAdvancedThemingScreenshots styled with Font.createSystemFont, which resolves through JavaSEPort.fontFaceSystem"Arial" on macOS and Linux alike (JavaSEPort.java:765).
  • Arial exists on a developer's Mac and not on a stock CI runner, so AWT silently substitutes a different face.
  • The differences are exactly that. For flow-layout.png every differing pixel sits in y 10–130 — the title and label rows — while the coloured blocks below match to the pixel.

So the byte-exact gate in developer-guide-docs.yml has only ever been green because CI both generates and verifies. A developer regenerating locally could not match it, and the guide has ~260 more app screenshots that want the same treatment.

The fix

The figures load native:MainRegular, which JavaSEPort.loadTrueTypeFont (:13551) reads from /com/codename1/impl/javase/Roboto-Medium.ttf on the classpath rather than from an installed-font lookup. That cannot depend on what the machine has installed.

This is also what the project's own font rule requires everywhere — never createSystemFont, always the native: scheme — so the violation and the non-determinism were the same bug.

A null return refuses loudly rather than falling back to a host font: a silent fallback would restore precisely the non-determinism this removes.

How this PR proves itself

The committed baselines are regenerated from a Mac, deliberately. CI byte-compares them on Linux. So the existing screenshot check either passes — demonstrating host independence rather than asserting it — or fails and says so immediately.

Composition, dimensions and colours are unchanged; only the typeface moves.

Scope note

These 24 are schematic layout diagrams — green labelled blocks (North/South/East/West/Center) on white under a blue title bar — whose job is to show where a layout manager puts things. They are deliberately not app screenshots, so they keep their hand-styling; rendering them under Material 3 or Liquid Glass would bury what they teach. The native-theme and light/dark machinery belongs with the app-UI figures in the Components, io and graphics chapters, and lands with those.

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T15:06:10.352436Z815afcfNew commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 506b447 to 8a91850CompareSeptember 2, 2026 07:11
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:8a91850b97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:de62de4b98

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread.github/workflows/developer-guide-docs.yml

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2989351749

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threaddocs/developer-guide/img/layered-layout.tolerance

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:365153a366

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/compare-screenshots.py Outdated
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 691841e to 2e7dd10CompareSeptember 2, 2026 09:56

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2e7dd10fe6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/check-guide-structure.py Outdated
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 2e7dd10 to f02771eCompareSeptember 2, 2026 10:06
@shai-almog
shai-almog changed the base branch from devguide-structure-and-guards to devguide-guardsSeptember 2, 2026 10:12
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 2 times, most recently from 6832347 to 1bc8e6aCompareSeptember 2, 2026 11:29
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 1bc8e6a to ae956d3CompareSeptember 2, 2026 11:39
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from ae956d3 to eecf05dCompareSeptember 2, 2026 11:58
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from 335a54a to f915089CompareSeptember 2, 2026 13:06
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from facd9ed to 15e0c1eCompareSeptember 2, 2026 14:42
shai-almogand others added 4 commits September 2, 2026 18:02
None of the 24 generated figures could be reproduced outside CI. Running the
generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels
differing. Two consecutive local runs are byte-identical, so the generator is
deterministic on one host; the variable is the font.
`PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which
resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux
alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT
silently substitutes and every glyph changes. The differences are exactly that:
for `flow-layout.png` every differing pixel sits in y 10-130, the title and
label rows, while the colored blocks below match to the pixel.
So the byte-exact gate has only ever been green because CI both generates and
verifies. A developer regenerating locally could not match it, and at the scale
the guide's remaining ~260 app screenshots would need, that is untenable.
The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont`
reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath
rather than from an installed-font lookup. This is also what the project's font
rule requires everywhere: never `createSystemFont`, always the `native:` scheme.
A null return refuses loudly rather than falling back to a host font, because a
silent fallback would restore the exact non-determinism this removes.
The committed baselines are regenerated from a Mac. CI byte-compares them on
Linux, so the check either passes -- proving host independence rather than
asserting it -- or fails and says so immediately.
Composition, dimensions and colours are unchanged; only the typeface moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ours
CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only
one containing a `TextField`. The other 23 matched Linux byte for byte, so the
bundled-font change worked -- it just did not reach far enough.
`applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the
theme's default font, which resolves through the host, so the text field and its
hint were still host-dependent. Enumerating the types that carry text would have
left the next one added broken the same way, so the walk now pins the face on
every component it visits, plus the hint label, which is painted by a `Label`
that is not in the component tree and so is never reached by the walk.
The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px
against the theme default's 13px the field grew and squeezed "Submit" down to a
clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the
composition is unchanged and only the typeface moves.
Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed
at a family that does not exist -- which is what a machine without Arial looks
like to the port. All 24 come out byte-identical, so nothing in these figures
reads an installed font any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Committed screenshot is stale: <name>" names the file and nothing else, so
there is no way to tell a real regression from an environment difference
without adding a debugging round trip to CI. The generated directory is now
uploaded as an artifact when the step fails.
Needed immediately: one figure still differs between a Mac and the runner after
the font fix, and host fonts, JDK version and working directory have each been
ruled out locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… edge
The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.
`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.
`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.
So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.
Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almogand others added 4 commits September 2, 2026 18:02
Two review findings on this PR.
The first says `native:MainRegular` does not give host-independent output,
because `loadTrueTypeFont` resolves native fonts to the first installed SF or
Helvetica family before reaching the bundled Roboto. The branch is real, but it
is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose
systemFontFamily contains "helvetica" -- and this generator never loads a skin.
The measurement agrees: figures rendered on a Mac match the Linux runner byte
for byte, which could not happen if one side were resolving Helvetica Neue and
the other Roboto.
So the conclusion does not hold today, but the risk is real for tomorrow: a
change that loads a skin here would put host fonts back into the output with no
other symptom. The generator now refuses to run under an iOS platform, and says
why, rather than leaving that to a comment nobody reads.
The second finding is straightforwardly right and is the more serious of the
two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the
`Determine changed components` filter named only two scripts, so a pull request
touching any other script here started the workflow with `docs` false and
skipped the steps that script governs. A change to compare-screenshots.py could
have merged without the screenshot check ever running it -- a gate that skips
itself. The filter now covers the whole directory, which also removes the
two-copies-drift the surrounding comment already warns about.
Verified the guard changes no output: all 24 figures are unchanged, and the
comparer still passes against the runner's own bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator incremented its mismatch counter only for pixels whose channel
delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a
sidecar written the way this one was -- a large delta bound paired with a small
area bound -- that leaves an unbounded hole, and review gave the exploit in the
figures' own palette: recolouring the green #06a806 to #a608a6 moves every
channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and
a dramatically different image reports zero mismatches.
Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the
image, at a worst delta of exactly 160 -- and passed.
The two bounds are now independent. maxMismatchPercent limits how much of the
image may change at all, counting every differing pixel; maxChannelDelta caps how
far any single pixel may move. The measured legitimate noise -- 173 pixels,
0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a
five-pixel solid overwrite fails on delta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit changed the comparator so every differing pixel counts
toward the area budget and the channel delta is a separate ceiling. The sidecar
still explained the old behaviour -- that the area bound did the work and the
delta was meaningless on its own -- which is now wrong in a file whose whole
purpose is to justify the numbers beside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator converted both images to RGB before counting differences. The
committed figures are genuine RGBA PNGs -- four channels, currently opaque
everywhere -- so that dropped a real channel, and any change confined to alpha
was invisible to the tolerance path. A regression that turned the whole figure
transparent while leaving every colour channel intact reported zero changed
pixels and passed.
Byte equality, which every figure without a sidecar is still held to, always
caught this. Only a figure carrying a sidecar could reach the weakened path, so
today the exposure was one image -- but that image is exactly the one whose
comparison is relaxed.
Verified: making layered-layout.png fully transparent with its RGB channels
untouched now reports 100% of pixels changed, the real runner output still
passes, and the recolour and untoleranced-difference probes still fail as they
did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 15e0c1e to 815afcfCompareSeptember 2, 2026 15:02
@shai-almog
shai-almog merged commit 21923bf into devguide-guardsSep 2, 2026
4 checks passed
@shai-almog
shai-almog deleted the devguide-figure-harness branch September 2, 2026 15:21
Sign up for freeto 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.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Guide figures render with a bundled font instead of the host's - #5668

Merged
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness
Sep 2, 2026
Merged

Guide figures render with a bundled font instead of the host's#5668
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Stacked on #5664. Review that one first; this diff is only the last commit.

The defect

None of the 24 generated figures can be reproduced outside CI. Running the generator on a Mac reproduces 0 of 24 byte-for-byte, with 3–20% of pixels differing.

Two consecutive local runs are byte-identical, so the generator is deterministic on one host. The variable is the font:

  • PreAdvancedThemingScreenshots styled with Font.createSystemFont, which resolves through JavaSEPort.fontFaceSystem"Arial" on macOS and Linux alike (JavaSEPort.java:765).
  • Arial exists on a developer's Mac and not on a stock CI runner, so AWT silently substitutes a different face.
  • The differences are exactly that. For flow-layout.png every differing pixel sits in y 10–130 — the title and label rows — while the coloured blocks below match to the pixel.

So the byte-exact gate in developer-guide-docs.yml has only ever been green because CI both generates and verifies. A developer regenerating locally could not match it, and the guide has ~260 more app screenshots that want the same treatment.

The fix

The figures load native:MainRegular, which JavaSEPort.loadTrueTypeFont (:13551) reads from /com/codename1/impl/javase/Roboto-Medium.ttf on the classpath rather than from an installed-font lookup. That cannot depend on what the machine has installed.

This is also what the project's own font rule requires everywhere — never createSystemFont, always the native: scheme — so the violation and the non-determinism were the same bug.

A null return refuses loudly rather than falling back to a host font: a silent fallback would restore precisely the non-determinism this removes.

How this PR proves itself

The committed baselines are regenerated from a Mac, deliberately. CI byte-compares them on Linux. So the existing screenshot check either passes — demonstrating host independence rather than asserting it — or fails and says so immediately.

Composition, dimensions and colours are unchanged; only the typeface moves.

Scope note

These 24 are schematic layout diagrams — green labelled blocks (North/South/East/West/Center) on white under a blue title bar — whose job is to show where a layout manager puts things. They are deliberately not app screenshots, so they keep their hand-styling; rendering them under Material 3 or Liquid Glass would bury what they teach. The native-theme and light/dark machinery belongs with the app-UI figures in the Components, io and graphics chapters, and lands with those.

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T15:06:10.352436Z815afcfNew commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 506b447 to 8a91850CompareSeptember 2, 2026 07:11
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:8a91850b97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:de62de4b98

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread.github/workflows/developer-guide-docs.yml

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2989351749

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threaddocs/developer-guide/img/layered-layout.tolerance

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:365153a366

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/compare-screenshots.py Outdated
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 691841e to 2e7dd10CompareSeptember 2, 2026 09:56

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2e7dd10fe6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/check-guide-structure.py Outdated
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 2e7dd10 to f02771eCompareSeptember 2, 2026 10:06
@shai-almog
shai-almog changed the base branch from devguide-structure-and-guards to devguide-guardsSeptember 2, 2026 10:12
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 2 times, most recently from 6832347 to 1bc8e6aCompareSeptember 2, 2026 11:29
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 1bc8e6a to ae956d3CompareSeptember 2, 2026 11:39
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from ae956d3 to eecf05dCompareSeptember 2, 2026 11:58
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from 335a54a to f915089CompareSeptember 2, 2026 13:06
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from facd9ed to 15e0c1eCompareSeptember 2, 2026 14:42
shai-almogand others added 4 commits September 2, 2026 18:02
None of the 24 generated figures could be reproduced outside CI. Running the
generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels
differing. Two consecutive local runs are byte-identical, so the generator is
deterministic on one host; the variable is the font.
`PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which
resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux
alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT
silently substitutes and every glyph changes. The differences are exactly that:
for `flow-layout.png` every differing pixel sits in y 10-130, the title and
label rows, while the colored blocks below match to the pixel.
So the byte-exact gate has only ever been green because CI both generates and
verifies. A developer regenerating locally could not match it, and at the scale
the guide's remaining ~260 app screenshots would need, that is untenable.
The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont`
reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath
rather than from an installed-font lookup. This is also what the project's font
rule requires everywhere: never `createSystemFont`, always the `native:` scheme.
A null return refuses loudly rather than falling back to a host font, because a
silent fallback would restore the exact non-determinism this removes.
The committed baselines are regenerated from a Mac. CI byte-compares them on
Linux, so the check either passes -- proving host independence rather than
asserting it -- or fails and says so immediately.
Composition, dimensions and colours are unchanged; only the typeface moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ours
CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only
one containing a `TextField`. The other 23 matched Linux byte for byte, so the
bundled-font change worked -- it just did not reach far enough.
`applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the
theme's default font, which resolves through the host, so the text field and its
hint were still host-dependent. Enumerating the types that carry text would have
left the next one added broken the same way, so the walk now pins the face on
every component it visits, plus the hint label, which is painted by a `Label`
that is not in the component tree and so is never reached by the walk.
The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px
against the theme default's 13px the field grew and squeezed "Submit" down to a
clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the
composition is unchanged and only the typeface moves.
Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed
at a family that does not exist -- which is what a machine without Arial looks
like to the port. All 24 come out byte-identical, so nothing in these figures
reads an installed font any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Committed screenshot is stale: <name>" names the file and nothing else, so
there is no way to tell a real regression from an environment difference
without adding a debugging round trip to CI. The generated directory is now
uploaded as an artifact when the step fails.
Needed immediately: one figure still differs between a Mac and the runner after
the font fix, and host fonts, JDK version and working directory have each been
ruled out locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… edge
The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.
`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.
`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.
So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.
Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almogand others added 4 commits September 2, 2026 18:02
Two review findings on this PR.
The first says `native:MainRegular` does not give host-independent output,
because `loadTrueTypeFont` resolves native fonts to the first installed SF or
Helvetica family before reaching the bundled Roboto. The branch is real, but it
is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose
systemFontFamily contains "helvetica" -- and this generator never loads a skin.
The measurement agrees: figures rendered on a Mac match the Linux runner byte
for byte, which could not happen if one side were resolving Helvetica Neue and
the other Roboto.
So the conclusion does not hold today, but the risk is real for tomorrow: a
change that loads a skin here would put host fonts back into the output with no
other symptom. The generator now refuses to run under an iOS platform, and says
why, rather than leaving that to a comment nobody reads.
The second finding is straightforwardly right and is the more serious of the
two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the
`Determine changed components` filter named only two scripts, so a pull request
touching any other script here started the workflow with `docs` false and
skipped the steps that script governs. A change to compare-screenshots.py could
have merged without the screenshot check ever running it -- a gate that skips
itself. The filter now covers the whole directory, which also removes the
two-copies-drift the surrounding comment already warns about.
Verified the guard changes no output: all 24 figures are unchanged, and the
comparer still passes against the runner's own bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator incremented its mismatch counter only for pixels whose channel
delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a
sidecar written the way this one was -- a large delta bound paired with a small
area bound -- that leaves an unbounded hole, and review gave the exploit in the
figures' own palette: recolouring the green #06a806 to #a608a6 moves every
channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and
a dramatically different image reports zero mismatches.
Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the
image, at a worst delta of exactly 160 -- and passed.
The two bounds are now independent. maxMismatchPercent limits how much of the
image may change at all, counting every differing pixel; maxChannelDelta caps how
far any single pixel may move. The measured legitimate noise -- 173 pixels,
0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a
five-pixel solid overwrite fails on delta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit changed the comparator so every differing pixel counts
toward the area budget and the channel delta is a separate ceiling. The sidecar
still explained the old behaviour -- that the area bound did the work and the
delta was meaningless on its own -- which is now wrong in a file whose whole
purpose is to justify the numbers beside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator converted both images to RGB before counting differences. The
committed figures are genuine RGBA PNGs -- four channels, currently opaque
everywhere -- so that dropped a real channel, and any change confined to alpha
was invisible to the tolerance path. A regression that turned the whole figure
transparent while leaving every colour channel intact reported zero changed
pixels and passed.
Byte equality, which every figure without a sidecar is still held to, always
caught this. Only a figure carrying a sidecar could reach the weakened path, so
today the exposure was one image -- but that image is exactly the one whose
comparison is relaxed.
Verified: making layered-layout.png fully transparent with its RGB channels
untouched now reports 100% of pixels changed, the real runner output still
passes, and the recolour and untoleranced-difference probes still fail as they
did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 15e0c1e to 815afcfCompareSeptember 2, 2026 15:02
@shai-almog
shai-almog merged commit 21923bf into devguide-guardsSep 2, 2026
4 checks passed
@shai-almog
shai-almog deleted the devguide-figure-harness branch September 2, 2026 15:21
Sign up for freeto 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.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Guide figures render with a bundled font instead of the host's - #5668

Merged
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness
Sep 2, 2026
Merged

Guide figures render with a bundled font instead of the host's#5668
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Stacked on #5664. Review that one first; this diff is only the last commit.

The defect

None of the 24 generated figures can be reproduced outside CI. Running the generator on a Mac reproduces 0 of 24 byte-for-byte, with 3–20% of pixels differing.

Two consecutive local runs are byte-identical, so the generator is deterministic on one host. The variable is the font:

  • PreAdvancedThemingScreenshots styled with Font.createSystemFont, which resolves through JavaSEPort.fontFaceSystem"Arial" on macOS and Linux alike (JavaSEPort.java:765).
  • Arial exists on a developer's Mac and not on a stock CI runner, so AWT silently substitutes a different face.
  • The differences are exactly that. For flow-layout.png every differing pixel sits in y 10–130 — the title and label rows — while the coloured blocks below match to the pixel.

So the byte-exact gate in developer-guide-docs.yml has only ever been green because CI both generates and verifies. A developer regenerating locally could not match it, and the guide has ~260 more app screenshots that want the same treatment.

The fix

The figures load native:MainRegular, which JavaSEPort.loadTrueTypeFont (:13551) reads from /com/codename1/impl/javase/Roboto-Medium.ttf on the classpath rather than from an installed-font lookup. That cannot depend on what the machine has installed.

This is also what the project's own font rule requires everywhere — never createSystemFont, always the native: scheme — so the violation and the non-determinism were the same bug.

A null return refuses loudly rather than falling back to a host font: a silent fallback would restore precisely the non-determinism this removes.

How this PR proves itself

The committed baselines are regenerated from a Mac, deliberately. CI byte-compares them on Linux. So the existing screenshot check either passes — demonstrating host independence rather than asserting it — or fails and says so immediately.

Composition, dimensions and colours are unchanged; only the typeface moves.

Scope note

These 24 are schematic layout diagrams — green labelled blocks (North/South/East/West/Center) on white under a blue title bar — whose job is to show where a layout manager puts things. They are deliberately not app screenshots, so they keep their hand-styling; rendering them under Material 3 or Liquid Glass would bury what they teach. The native-theme and light/dark machinery belongs with the app-UI figures in the Components, io and graphics chapters, and lands with those.

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T15:06:10.352436Z815afcfNew commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 506b447 to 8a91850CompareSeptember 2, 2026 07:11
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:8a91850b97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:de62de4b98

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread.github/workflows/developer-guide-docs.yml

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2989351749

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threaddocs/developer-guide/img/layered-layout.tolerance

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:365153a366

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/compare-screenshots.py Outdated
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 691841e to 2e7dd10CompareSeptember 2, 2026 09:56

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2e7dd10fe6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/check-guide-structure.py Outdated
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 2e7dd10 to f02771eCompareSeptember 2, 2026 10:06
@shai-almog
shai-almog changed the base branch from devguide-structure-and-guards to devguide-guardsSeptember 2, 2026 10:12
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 2 times, most recently from 6832347 to 1bc8e6aCompareSeptember 2, 2026 11:29
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 1bc8e6a to ae956d3CompareSeptember 2, 2026 11:39
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from ae956d3 to eecf05dCompareSeptember 2, 2026 11:58
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from 335a54a to f915089CompareSeptember 2, 2026 13:06
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from facd9ed to 15e0c1eCompareSeptember 2, 2026 14:42
shai-almogand others added 4 commits September 2, 2026 18:02
None of the 24 generated figures could be reproduced outside CI. Running the
generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels
differing. Two consecutive local runs are byte-identical, so the generator is
deterministic on one host; the variable is the font.
`PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which
resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux
alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT
silently substitutes and every glyph changes. The differences are exactly that:
for `flow-layout.png` every differing pixel sits in y 10-130, the title and
label rows, while the colored blocks below match to the pixel.
So the byte-exact gate has only ever been green because CI both generates and
verifies. A developer regenerating locally could not match it, and at the scale
the guide's remaining ~260 app screenshots would need, that is untenable.
The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont`
reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath
rather than from an installed-font lookup. This is also what the project's font
rule requires everywhere: never `createSystemFont`, always the `native:` scheme.
A null return refuses loudly rather than falling back to a host font, because a
silent fallback would restore the exact non-determinism this removes.
The committed baselines are regenerated from a Mac. CI byte-compares them on
Linux, so the check either passes -- proving host independence rather than
asserting it -- or fails and says so immediately.
Composition, dimensions and colours are unchanged; only the typeface moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ours
CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only
one containing a `TextField`. The other 23 matched Linux byte for byte, so the
bundled-font change worked -- it just did not reach far enough.
`applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the
theme's default font, which resolves through the host, so the text field and its
hint were still host-dependent. Enumerating the types that carry text would have
left the next one added broken the same way, so the walk now pins the face on
every component it visits, plus the hint label, which is painted by a `Label`
that is not in the component tree and so is never reached by the walk.
The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px
against the theme default's 13px the field grew and squeezed "Submit" down to a
clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the
composition is unchanged and only the typeface moves.
Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed
at a family that does not exist -- which is what a machine without Arial looks
like to the port. All 24 come out byte-identical, so nothing in these figures
reads an installed font any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Committed screenshot is stale: <name>" names the file and nothing else, so
there is no way to tell a real regression from an environment difference
without adding a debugging round trip to CI. The generated directory is now
uploaded as an artifact when the step fails.
Needed immediately: one figure still differs between a Mac and the runner after
the font fix, and host fonts, JDK version and working directory have each been
ruled out locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… edge
The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.
`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.
`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.
So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.
Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almogand others added 4 commits September 2, 2026 18:02
Two review findings on this PR.
The first says `native:MainRegular` does not give host-independent output,
because `loadTrueTypeFont` resolves native fonts to the first installed SF or
Helvetica family before reaching the bundled Roboto. The branch is real, but it
is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose
systemFontFamily contains "helvetica" -- and this generator never loads a skin.
The measurement agrees: figures rendered on a Mac match the Linux runner byte
for byte, which could not happen if one side were resolving Helvetica Neue and
the other Roboto.
So the conclusion does not hold today, but the risk is real for tomorrow: a
change that loads a skin here would put host fonts back into the output with no
other symptom. The generator now refuses to run under an iOS platform, and says
why, rather than leaving that to a comment nobody reads.
The second finding is straightforwardly right and is the more serious of the
two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the
`Determine changed components` filter named only two scripts, so a pull request
touching any other script here started the workflow with `docs` false and
skipped the steps that script governs. A change to compare-screenshots.py could
have merged without the screenshot check ever running it -- a gate that skips
itself. The filter now covers the whole directory, which also removes the
two-copies-drift the surrounding comment already warns about.
Verified the guard changes no output: all 24 figures are unchanged, and the
comparer still passes against the runner's own bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator incremented its mismatch counter only for pixels whose channel
delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a
sidecar written the way this one was -- a large delta bound paired with a small
area bound -- that leaves an unbounded hole, and review gave the exploit in the
figures' own palette: recolouring the green #06a806 to #a608a6 moves every
channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and
a dramatically different image reports zero mismatches.
Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the
image, at a worst delta of exactly 160 -- and passed.
The two bounds are now independent. maxMismatchPercent limits how much of the
image may change at all, counting every differing pixel; maxChannelDelta caps how
far any single pixel may move. The measured legitimate noise -- 173 pixels,
0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a
five-pixel solid overwrite fails on delta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit changed the comparator so every differing pixel counts
toward the area budget and the channel delta is a separate ceiling. The sidecar
still explained the old behaviour -- that the area bound did the work and the
delta was meaningless on its own -- which is now wrong in a file whose whole
purpose is to justify the numbers beside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator converted both images to RGB before counting differences. The
committed figures are genuine RGBA PNGs -- four channels, currently opaque
everywhere -- so that dropped a real channel, and any change confined to alpha
was invisible to the tolerance path. A regression that turned the whole figure
transparent while leaving every colour channel intact reported zero changed
pixels and passed.
Byte equality, which every figure without a sidecar is still held to, always
caught this. Only a figure carrying a sidecar could reach the weakened path, so
today the exposure was one image -- but that image is exactly the one whose
comparison is relaxed.
Verified: making layered-layout.png fully transparent with its RGB channels
untouched now reports 100% of pixels changed, the real runner output still
passes, and the recolour and untoleranced-difference probes still fail as they
did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 15e0c1e to 815afcfCompareSeptember 2, 2026 15:02
@shai-almog
shai-almog merged commit 21923bf into devguide-guardsSep 2, 2026
4 checks passed
@shai-almog
shai-almog deleted the devguide-figure-harness branch September 2, 2026 15:21
Sign up for freeto 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.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Guide figures render with a bundled font instead of the host's - #5668

Merged
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness
Sep 2, 2026
Merged

Guide figures render with a bundled font instead of the host's#5668
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Stacked on #5664. Review that one first; this diff is only the last commit.

The defect

None of the 24 generated figures can be reproduced outside CI. Running the generator on a Mac reproduces 0 of 24 byte-for-byte, with 3–20% of pixels differing.

Two consecutive local runs are byte-identical, so the generator is deterministic on one host. The variable is the font:

  • PreAdvancedThemingScreenshots styled with Font.createSystemFont, which resolves through JavaSEPort.fontFaceSystem"Arial" on macOS and Linux alike (JavaSEPort.java:765).
  • Arial exists on a developer's Mac and not on a stock CI runner, so AWT silently substitutes a different face.
  • The differences are exactly that. For flow-layout.png every differing pixel sits in y 10–130 — the title and label rows — while the coloured blocks below match to the pixel.

So the byte-exact gate in developer-guide-docs.yml has only ever been green because CI both generates and verifies. A developer regenerating locally could not match it, and the guide has ~260 more app screenshots that want the same treatment.

The fix

The figures load native:MainRegular, which JavaSEPort.loadTrueTypeFont (:13551) reads from /com/codename1/impl/javase/Roboto-Medium.ttf on the classpath rather than from an installed-font lookup. That cannot depend on what the machine has installed.

This is also what the project's own font rule requires everywhere — never createSystemFont, always the native: scheme — so the violation and the non-determinism were the same bug.

A null return refuses loudly rather than falling back to a host font: a silent fallback would restore precisely the non-determinism this removes.

How this PR proves itself

The committed baselines are regenerated from a Mac, deliberately. CI byte-compares them on Linux. So the existing screenshot check either passes — demonstrating host independence rather than asserting it — or fails and says so immediately.

Composition, dimensions and colours are unchanged; only the typeface moves.

Scope note

These 24 are schematic layout diagrams — green labelled blocks (North/South/East/West/Center) on white under a blue title bar — whose job is to show where a layout manager puts things. They are deliberately not app screenshots, so they keep their hand-styling; rendering them under Material 3 or Liquid Glass would bury what they teach. The native-theme and light/dark machinery belongs with the app-UI figures in the Components, io and graphics chapters, and lands with those.

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T15:06:10.352436Z815afcfNew commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 506b447 to 8a91850CompareSeptember 2, 2026 07:11
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:8a91850b97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:de62de4b98

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread.github/workflows/developer-guide-docs.yml

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2989351749

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threaddocs/developer-guide/img/layered-layout.tolerance

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:365153a366

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/compare-screenshots.py Outdated
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 691841e to 2e7dd10CompareSeptember 2, 2026 09:56

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2e7dd10fe6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/check-guide-structure.py Outdated
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 2e7dd10 to f02771eCompareSeptember 2, 2026 10:06
@shai-almog
shai-almog changed the base branch from devguide-structure-and-guards to devguide-guardsSeptember 2, 2026 10:12
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 2 times, most recently from 6832347 to 1bc8e6aCompareSeptember 2, 2026 11:29
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 1bc8e6a to ae956d3CompareSeptember 2, 2026 11:39
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from ae956d3 to eecf05dCompareSeptember 2, 2026 11:58
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from 335a54a to f915089CompareSeptember 2, 2026 13:06
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from facd9ed to 15e0c1eCompareSeptember 2, 2026 14:42
shai-almogand others added 4 commits September 2, 2026 18:02
None of the 24 generated figures could be reproduced outside CI. Running the
generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels
differing. Two consecutive local runs are byte-identical, so the generator is
deterministic on one host; the variable is the font.
`PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which
resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux
alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT
silently substitutes and every glyph changes. The differences are exactly that:
for `flow-layout.png` every differing pixel sits in y 10-130, the title and
label rows, while the colored blocks below match to the pixel.
So the byte-exact gate has only ever been green because CI both generates and
verifies. A developer regenerating locally could not match it, and at the scale
the guide's remaining ~260 app screenshots would need, that is untenable.
The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont`
reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath
rather than from an installed-font lookup. This is also what the project's font
rule requires everywhere: never `createSystemFont`, always the `native:` scheme.
A null return refuses loudly rather than falling back to a host font, because a
silent fallback would restore the exact non-determinism this removes.
The committed baselines are regenerated from a Mac. CI byte-compares them on
Linux, so the check either passes -- proving host independence rather than
asserting it -- or fails and says so immediately.
Composition, dimensions and colours are unchanged; only the typeface moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ours
CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only
one containing a `TextField`. The other 23 matched Linux byte for byte, so the
bundled-font change worked -- it just did not reach far enough.
`applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the
theme's default font, which resolves through the host, so the text field and its
hint were still host-dependent. Enumerating the types that carry text would have
left the next one added broken the same way, so the walk now pins the face on
every component it visits, plus the hint label, which is painted by a `Label`
that is not in the component tree and so is never reached by the walk.
The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px
against the theme default's 13px the field grew and squeezed "Submit" down to a
clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the
composition is unchanged and only the typeface moves.
Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed
at a family that does not exist -- which is what a machine without Arial looks
like to the port. All 24 come out byte-identical, so nothing in these figures
reads an installed font any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Committed screenshot is stale: <name>" names the file and nothing else, so
there is no way to tell a real regression from an environment difference
without adding a debugging round trip to CI. The generated directory is now
uploaded as an artifact when the step fails.
Needed immediately: one figure still differs between a Mac and the runner after
the font fix, and host fonts, JDK version and working directory have each been
ruled out locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… edge
The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.
`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.
`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.
So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.
Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almogand others added 4 commits September 2, 2026 18:02
Two review findings on this PR.
The first says `native:MainRegular` does not give host-independent output,
because `loadTrueTypeFont` resolves native fonts to the first installed SF or
Helvetica family before reaching the bundled Roboto. The branch is real, but it
is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose
systemFontFamily contains "helvetica" -- and this generator never loads a skin.
The measurement agrees: figures rendered on a Mac match the Linux runner byte
for byte, which could not happen if one side were resolving Helvetica Neue and
the other Roboto.
So the conclusion does not hold today, but the risk is real for tomorrow: a
change that loads a skin here would put host fonts back into the output with no
other symptom. The generator now refuses to run under an iOS platform, and says
why, rather than leaving that to a comment nobody reads.
The second finding is straightforwardly right and is the more serious of the
two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the
`Determine changed components` filter named only two scripts, so a pull request
touching any other script here started the workflow with `docs` false and
skipped the steps that script governs. A change to compare-screenshots.py could
have merged without the screenshot check ever running it -- a gate that skips
itself. The filter now covers the whole directory, which also removes the
two-copies-drift the surrounding comment already warns about.
Verified the guard changes no output: all 24 figures are unchanged, and the
comparer still passes against the runner's own bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator incremented its mismatch counter only for pixels whose channel
delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a
sidecar written the way this one was -- a large delta bound paired with a small
area bound -- that leaves an unbounded hole, and review gave the exploit in the
figures' own palette: recolouring the green #06a806 to #a608a6 moves every
channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and
a dramatically different image reports zero mismatches.
Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the
image, at a worst delta of exactly 160 -- and passed.
The two bounds are now independent. maxMismatchPercent limits how much of the
image may change at all, counting every differing pixel; maxChannelDelta caps how
far any single pixel may move. The measured legitimate noise -- 173 pixels,
0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a
five-pixel solid overwrite fails on delta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit changed the comparator so every differing pixel counts
toward the area budget and the channel delta is a separate ceiling. The sidecar
still explained the old behaviour -- that the area bound did the work and the
delta was meaningless on its own -- which is now wrong in a file whose whole
purpose is to justify the numbers beside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator converted both images to RGB before counting differences. The
committed figures are genuine RGBA PNGs -- four channels, currently opaque
everywhere -- so that dropped a real channel, and any change confined to alpha
was invisible to the tolerance path. A regression that turned the whole figure
transparent while leaving every colour channel intact reported zero changed
pixels and passed.
Byte equality, which every figure without a sidecar is still held to, always
caught this. Only a figure carrying a sidecar could reach the weakened path, so
today the exposure was one image -- but that image is exactly the one whose
comparison is relaxed.
Verified: making layered-layout.png fully transparent with its RGB channels
untouched now reports 100% of pixels changed, the real runner output still
passes, and the recolour and untoleranced-difference probes still fail as they
did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 15e0c1e to 815afcfCompareSeptember 2, 2026 15:02
@shai-almog
shai-almog merged commit 21923bf into devguide-guardsSep 2, 2026
4 checks passed
@shai-almog
shai-almog deleted the devguide-figure-harness branch September 2, 2026 15:21
Sign up for freeto 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.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Guide figures render with a bundled font instead of the host's - #5668

Merged
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness
Sep 2, 2026
Merged

Guide figures render with a bundled font instead of the host's#5668
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Stacked on #5664. Review that one first; this diff is only the last commit.

The defect

None of the 24 generated figures can be reproduced outside CI. Running the generator on a Mac reproduces 0 of 24 byte-for-byte, with 3–20% of pixels differing.

Two consecutive local runs are byte-identical, so the generator is deterministic on one host. The variable is the font:

  • PreAdvancedThemingScreenshots styled with Font.createSystemFont, which resolves through JavaSEPort.fontFaceSystem"Arial" on macOS and Linux alike (JavaSEPort.java:765).
  • Arial exists on a developer's Mac and not on a stock CI runner, so AWT silently substitutes a different face.
  • The differences are exactly that. For flow-layout.png every differing pixel sits in y 10–130 — the title and label rows — while the coloured blocks below match to the pixel.

So the byte-exact gate in developer-guide-docs.yml has only ever been green because CI both generates and verifies. A developer regenerating locally could not match it, and the guide has ~260 more app screenshots that want the same treatment.

The fix

The figures load native:MainRegular, which JavaSEPort.loadTrueTypeFont (:13551) reads from /com/codename1/impl/javase/Roboto-Medium.ttf on the classpath rather than from an installed-font lookup. That cannot depend on what the machine has installed.

This is also what the project's own font rule requires everywhere — never createSystemFont, always the native: scheme — so the violation and the non-determinism were the same bug.

A null return refuses loudly rather than falling back to a host font: a silent fallback would restore precisely the non-determinism this removes.

How this PR proves itself

The committed baselines are regenerated from a Mac, deliberately. CI byte-compares them on Linux. So the existing screenshot check either passes — demonstrating host independence rather than asserting it — or fails and says so immediately.

Composition, dimensions and colours are unchanged; only the typeface moves.

Scope note

These 24 are schematic layout diagrams — green labelled blocks (North/South/East/West/Center) on white under a blue title bar — whose job is to show where a layout manager puts things. They are deliberately not app screenshots, so they keep their hand-styling; rendering them under Material 3 or Liquid Glass would bury what they teach. The native-theme and light/dark machinery belongs with the app-UI figures in the Components, io and graphics chapters, and lands with those.

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T15:06:10.352436Z815afcfNew commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 506b447 to 8a91850CompareSeptember 2, 2026 07:11
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:8a91850b97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:de62de4b98

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread.github/workflows/developer-guide-docs.yml

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2989351749

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threaddocs/developer-guide/img/layered-layout.tolerance

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:365153a366

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/compare-screenshots.py Outdated
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 691841e to 2e7dd10CompareSeptember 2, 2026 09:56

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2e7dd10fe6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/check-guide-structure.py Outdated
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 2e7dd10 to f02771eCompareSeptember 2, 2026 10:06
@shai-almog
shai-almog changed the base branch from devguide-structure-and-guards to devguide-guardsSeptember 2, 2026 10:12
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 2 times, most recently from 6832347 to 1bc8e6aCompareSeptember 2, 2026 11:29
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 1bc8e6a to ae956d3CompareSeptember 2, 2026 11:39
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from ae956d3 to eecf05dCompareSeptember 2, 2026 11:58
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from 335a54a to f915089CompareSeptember 2, 2026 13:06
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from facd9ed to 15e0c1eCompareSeptember 2, 2026 14:42
shai-almogand others added 4 commits September 2, 2026 18:02
None of the 24 generated figures could be reproduced outside CI. Running the
generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels
differing. Two consecutive local runs are byte-identical, so the generator is
deterministic on one host; the variable is the font.
`PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which
resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux
alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT
silently substitutes and every glyph changes. The differences are exactly that:
for `flow-layout.png` every differing pixel sits in y 10-130, the title and
label rows, while the colored blocks below match to the pixel.
So the byte-exact gate has only ever been green because CI both generates and
verifies. A developer regenerating locally could not match it, and at the scale
the guide's remaining ~260 app screenshots would need, that is untenable.
The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont`
reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath
rather than from an installed-font lookup. This is also what the project's font
rule requires everywhere: never `createSystemFont`, always the `native:` scheme.
A null return refuses loudly rather than falling back to a host font, because a
silent fallback would restore the exact non-determinism this removes.
The committed baselines are regenerated from a Mac. CI byte-compares them on
Linux, so the check either passes -- proving host independence rather than
asserting it -- or fails and says so immediately.
Composition, dimensions and colours are unchanged; only the typeface moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ours
CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only
one containing a `TextField`. The other 23 matched Linux byte for byte, so the
bundled-font change worked -- it just did not reach far enough.
`applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the
theme's default font, which resolves through the host, so the text field and its
hint were still host-dependent. Enumerating the types that carry text would have
left the next one added broken the same way, so the walk now pins the face on
every component it visits, plus the hint label, which is painted by a `Label`
that is not in the component tree and so is never reached by the walk.
The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px
against the theme default's 13px the field grew and squeezed "Submit" down to a
clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the
composition is unchanged and only the typeface moves.
Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed
at a family that does not exist -- which is what a machine without Arial looks
like to the port. All 24 come out byte-identical, so nothing in these figures
reads an installed font any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Committed screenshot is stale: <name>" names the file and nothing else, so
there is no way to tell a real regression from an environment difference
without adding a debugging round trip to CI. The generated directory is now
uploaded as an artifact when the step fails.
Needed immediately: one figure still differs between a Mac and the runner after
the font fix, and host fonts, JDK version and working directory have each been
ruled out locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… edge
The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.
`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.
`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.
So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.
Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almogand others added 4 commits September 2, 2026 18:02
Two review findings on this PR.
The first says `native:MainRegular` does not give host-independent output,
because `loadTrueTypeFont` resolves native fonts to the first installed SF or
Helvetica family before reaching the bundled Roboto. The branch is real, but it
is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose
systemFontFamily contains "helvetica" -- and this generator never loads a skin.
The measurement agrees: figures rendered on a Mac match the Linux runner byte
for byte, which could not happen if one side were resolving Helvetica Neue and
the other Roboto.
So the conclusion does not hold today, but the risk is real for tomorrow: a
change that loads a skin here would put host fonts back into the output with no
other symptom. The generator now refuses to run under an iOS platform, and says
why, rather than leaving that to a comment nobody reads.
The second finding is straightforwardly right and is the more serious of the
two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the
`Determine changed components` filter named only two scripts, so a pull request
touching any other script here started the workflow with `docs` false and
skipped the steps that script governs. A change to compare-screenshots.py could
have merged without the screenshot check ever running it -- a gate that skips
itself. The filter now covers the whole directory, which also removes the
two-copies-drift the surrounding comment already warns about.
Verified the guard changes no output: all 24 figures are unchanged, and the
comparer still passes against the runner's own bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator incremented its mismatch counter only for pixels whose channel
delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a
sidecar written the way this one was -- a large delta bound paired with a small
area bound -- that leaves an unbounded hole, and review gave the exploit in the
figures' own palette: recolouring the green #06a806 to #a608a6 moves every
channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and
a dramatically different image reports zero mismatches.
Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the
image, at a worst delta of exactly 160 -- and passed.
The two bounds are now independent. maxMismatchPercent limits how much of the
image may change at all, counting every differing pixel; maxChannelDelta caps how
far any single pixel may move. The measured legitimate noise -- 173 pixels,
0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a
five-pixel solid overwrite fails on delta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit changed the comparator so every differing pixel counts
toward the area budget and the channel delta is a separate ceiling. The sidecar
still explained the old behaviour -- that the area bound did the work and the
delta was meaningless on its own -- which is now wrong in a file whose whole
purpose is to justify the numbers beside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator converted both images to RGB before counting differences. The
committed figures are genuine RGBA PNGs -- four channels, currently opaque
everywhere -- so that dropped a real channel, and any change confined to alpha
was invisible to the tolerance path. A regression that turned the whole figure
transparent while leaving every colour channel intact reported zero changed
pixels and passed.
Byte equality, which every figure without a sidecar is still held to, always
caught this. Only a figure carrying a sidecar could reach the weakened path, so
today the exposure was one image -- but that image is exactly the one whose
comparison is relaxed.
Verified: making layered-layout.png fully transparent with its RGB channels
untouched now reports 100% of pixels changed, the real runner output still
passes, and the recolour and untoleranced-difference probes still fail as they
did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 15e0c1e to 815afcfCompareSeptember 2, 2026 15:02
@shai-almog
shai-almog merged commit 21923bf into devguide-guardsSep 2, 2026
4 checks passed
@shai-almog
shai-almog deleted the devguide-figure-harness branch September 2, 2026 15:21
Sign up for freeto 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.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Guide figures render with a bundled font instead of the host's - #5668

Merged
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness
Sep 2, 2026
Merged

Guide figures render with a bundled font instead of the host's#5668
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Stacked on #5664. Review that one first; this diff is only the last commit.

The defect

None of the 24 generated figures can be reproduced outside CI. Running the generator on a Mac reproduces 0 of 24 byte-for-byte, with 3–20% of pixels differing.

Two consecutive local runs are byte-identical, so the generator is deterministic on one host. The variable is the font:

  • PreAdvancedThemingScreenshots styled with Font.createSystemFont, which resolves through JavaSEPort.fontFaceSystem"Arial" on macOS and Linux alike (JavaSEPort.java:765).
  • Arial exists on a developer's Mac and not on a stock CI runner, so AWT silently substitutes a different face.
  • The differences are exactly that. For flow-layout.png every differing pixel sits in y 10–130 — the title and label rows — while the coloured blocks below match to the pixel.

So the byte-exact gate in developer-guide-docs.yml has only ever been green because CI both generates and verifies. A developer regenerating locally could not match it, and the guide has ~260 more app screenshots that want the same treatment.

The fix

The figures load native:MainRegular, which JavaSEPort.loadTrueTypeFont (:13551) reads from /com/codename1/impl/javase/Roboto-Medium.ttf on the classpath rather than from an installed-font lookup. That cannot depend on what the machine has installed.

This is also what the project's own font rule requires everywhere — never createSystemFont, always the native: scheme — so the violation and the non-determinism were the same bug.

A null return refuses loudly rather than falling back to a host font: a silent fallback would restore precisely the non-determinism this removes.

How this PR proves itself

The committed baselines are regenerated from a Mac, deliberately. CI byte-compares them on Linux. So the existing screenshot check either passes — demonstrating host independence rather than asserting it — or fails and says so immediately.

Composition, dimensions and colours are unchanged; only the typeface moves.

Scope note

These 24 are schematic layout diagrams — green labelled blocks (North/South/East/West/Center) on white under a blue title bar — whose job is to show where a layout manager puts things. They are deliberately not app screenshots, so they keep their hand-styling; rendering them under Material 3 or Liquid Glass would bury what they teach. The native-theme and light/dark machinery belongs with the app-UI figures in the Components, io and graphics chapters, and lands with those.

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T15:06:10.352436Z815afcfNew commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 506b447 to 8a91850CompareSeptember 2, 2026 07:11
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:8a91850b97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:de62de4b98

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread.github/workflows/developer-guide-docs.yml

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2989351749

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threaddocs/developer-guide/img/layered-layout.tolerance

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:365153a366

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/compare-screenshots.py Outdated
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 691841e to 2e7dd10CompareSeptember 2, 2026 09:56

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2e7dd10fe6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/check-guide-structure.py Outdated
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 2e7dd10 to f02771eCompareSeptember 2, 2026 10:06
@shai-almog
shai-almog changed the base branch from devguide-structure-and-guards to devguide-guardsSeptember 2, 2026 10:12
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 2 times, most recently from 6832347 to 1bc8e6aCompareSeptember 2, 2026 11:29
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 1bc8e6a to ae956d3CompareSeptember 2, 2026 11:39
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from ae956d3 to eecf05dCompareSeptember 2, 2026 11:58
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from 335a54a to f915089CompareSeptember 2, 2026 13:06
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from facd9ed to 15e0c1eCompareSeptember 2, 2026 14:42
shai-almogand others added 4 commits September 2, 2026 18:02
None of the 24 generated figures could be reproduced outside CI. Running the
generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels
differing. Two consecutive local runs are byte-identical, so the generator is
deterministic on one host; the variable is the font.
`PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which
resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux
alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT
silently substitutes and every glyph changes. The differences are exactly that:
for `flow-layout.png` every differing pixel sits in y 10-130, the title and
label rows, while the colored blocks below match to the pixel.
So the byte-exact gate has only ever been green because CI both generates and
verifies. A developer regenerating locally could not match it, and at the scale
the guide's remaining ~260 app screenshots would need, that is untenable.
The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont`
reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath
rather than from an installed-font lookup. This is also what the project's font
rule requires everywhere: never `createSystemFont`, always the `native:` scheme.
A null return refuses loudly rather than falling back to a host font, because a
silent fallback would restore the exact non-determinism this removes.
The committed baselines are regenerated from a Mac. CI byte-compares them on
Linux, so the check either passes -- proving host independence rather than
asserting it -- or fails and says so immediately.
Composition, dimensions and colours are unchanged; only the typeface moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ours
CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only
one containing a `TextField`. The other 23 matched Linux byte for byte, so the
bundled-font change worked -- it just did not reach far enough.
`applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the
theme's default font, which resolves through the host, so the text field and its
hint were still host-dependent. Enumerating the types that carry text would have
left the next one added broken the same way, so the walk now pins the face on
every component it visits, plus the hint label, which is painted by a `Label`
that is not in the component tree and so is never reached by the walk.
The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px
against the theme default's 13px the field grew and squeezed "Submit" down to a
clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the
composition is unchanged and only the typeface moves.
Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed
at a family that does not exist -- which is what a machine without Arial looks
like to the port. All 24 come out byte-identical, so nothing in these figures
reads an installed font any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Committed screenshot is stale: <name>" names the file and nothing else, so
there is no way to tell a real regression from an environment difference
without adding a debugging round trip to CI. The generated directory is now
uploaded as an artifact when the step fails.
Needed immediately: one figure still differs between a Mac and the runner after
the font fix, and host fonts, JDK version and working directory have each been
ruled out locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… edge
The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.
`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.
`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.
So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.
Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almogand others added 4 commits September 2, 2026 18:02
Two review findings on this PR.
The first says `native:MainRegular` does not give host-independent output,
because `loadTrueTypeFont` resolves native fonts to the first installed SF or
Helvetica family before reaching the bundled Roboto. The branch is real, but it
is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose
systemFontFamily contains "helvetica" -- and this generator never loads a skin.
The measurement agrees: figures rendered on a Mac match the Linux runner byte
for byte, which could not happen if one side were resolving Helvetica Neue and
the other Roboto.
So the conclusion does not hold today, but the risk is real for tomorrow: a
change that loads a skin here would put host fonts back into the output with no
other symptom. The generator now refuses to run under an iOS platform, and says
why, rather than leaving that to a comment nobody reads.
The second finding is straightforwardly right and is the more serious of the
two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the
`Determine changed components` filter named only two scripts, so a pull request
touching any other script here started the workflow with `docs` false and
skipped the steps that script governs. A change to compare-screenshots.py could
have merged without the screenshot check ever running it -- a gate that skips
itself. The filter now covers the whole directory, which also removes the
two-copies-drift the surrounding comment already warns about.
Verified the guard changes no output: all 24 figures are unchanged, and the
comparer still passes against the runner's own bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator incremented its mismatch counter only for pixels whose channel
delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a
sidecar written the way this one was -- a large delta bound paired with a small
area bound -- that leaves an unbounded hole, and review gave the exploit in the
figures' own palette: recolouring the green #06a806 to #a608a6 moves every
channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and
a dramatically different image reports zero mismatches.
Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the
image, at a worst delta of exactly 160 -- and passed.
The two bounds are now independent. maxMismatchPercent limits how much of the
image may change at all, counting every differing pixel; maxChannelDelta caps how
far any single pixel may move. The measured legitimate noise -- 173 pixels,
0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a
five-pixel solid overwrite fails on delta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit changed the comparator so every differing pixel counts
toward the area budget and the channel delta is a separate ceiling. The sidecar
still explained the old behaviour -- that the area bound did the work and the
delta was meaningless on its own -- which is now wrong in a file whose whole
purpose is to justify the numbers beside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator converted both images to RGB before counting differences. The
committed figures are genuine RGBA PNGs -- four channels, currently opaque
everywhere -- so that dropped a real channel, and any change confined to alpha
was invisible to the tolerance path. A regression that turned the whole figure
transparent while leaving every colour channel intact reported zero changed
pixels and passed.
Byte equality, which every figure without a sidecar is still held to, always
caught this. Only a figure carrying a sidecar could reach the weakened path, so
today the exposure was one image -- but that image is exactly the one whose
comparison is relaxed.
Verified: making layered-layout.png fully transparent with its RGB channels
untouched now reports 100% of pixels changed, the real runner output still
passes, and the recolour and untoleranced-difference probes still fail as they
did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 15e0c1e to 815afcfCompareSeptember 2, 2026 15:02
@shai-almog
shai-almog merged commit 21923bf into devguide-guardsSep 2, 2026
4 checks passed
@shai-almog
shai-almog deleted the devguide-figure-harness branch September 2, 2026 15:21
Sign up for freeto 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.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Guide figures render with a bundled font instead of the host's - #5668

Merged
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness
Sep 2, 2026
Merged

Guide figures render with a bundled font instead of the host's#5668
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Stacked on #5664. Review that one first; this diff is only the last commit.

The defect

None of the 24 generated figures can be reproduced outside CI. Running the generator on a Mac reproduces 0 of 24 byte-for-byte, with 3–20% of pixels differing.

Two consecutive local runs are byte-identical, so the generator is deterministic on one host. The variable is the font:

  • PreAdvancedThemingScreenshots styled with Font.createSystemFont, which resolves through JavaSEPort.fontFaceSystem"Arial" on macOS and Linux alike (JavaSEPort.java:765).
  • Arial exists on a developer's Mac and not on a stock CI runner, so AWT silently substitutes a different face.
  • The differences are exactly that. For flow-layout.png every differing pixel sits in y 10–130 — the title and label rows — while the coloured blocks below match to the pixel.

So the byte-exact gate in developer-guide-docs.yml has only ever been green because CI both generates and verifies. A developer regenerating locally could not match it, and the guide has ~260 more app screenshots that want the same treatment.

The fix

The figures load native:MainRegular, which JavaSEPort.loadTrueTypeFont (:13551) reads from /com/codename1/impl/javase/Roboto-Medium.ttf on the classpath rather than from an installed-font lookup. That cannot depend on what the machine has installed.

This is also what the project's own font rule requires everywhere — never createSystemFont, always the native: scheme — so the violation and the non-determinism were the same bug.

A null return refuses loudly rather than falling back to a host font: a silent fallback would restore precisely the non-determinism this removes.

How this PR proves itself

The committed baselines are regenerated from a Mac, deliberately. CI byte-compares them on Linux. So the existing screenshot check either passes — demonstrating host independence rather than asserting it — or fails and says so immediately.

Composition, dimensions and colours are unchanged; only the typeface moves.

Scope note

These 24 are schematic layout diagrams — green labelled blocks (North/South/East/West/Center) on white under a blue title bar — whose job is to show where a layout manager puts things. They are deliberately not app screenshots, so they keep their hand-styling; rendering them under Material 3 or Liquid Glass would bury what they teach. The native-theme and light/dark machinery belongs with the app-UI figures in the Components, io and graphics chapters, and lands with those.

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T15:06:10.352436Z815afcfNew commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 506b447 to 8a91850CompareSeptember 2, 2026 07:11
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:8a91850b97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:de62de4b98

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread.github/workflows/developer-guide-docs.yml

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2989351749

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threaddocs/developer-guide/img/layered-layout.tolerance

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:365153a366

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/compare-screenshots.py Outdated
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 691841e to 2e7dd10CompareSeptember 2, 2026 09:56

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2e7dd10fe6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/check-guide-structure.py Outdated
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 2e7dd10 to f02771eCompareSeptember 2, 2026 10:06
@shai-almog
shai-almog changed the base branch from devguide-structure-and-guards to devguide-guardsSeptember 2, 2026 10:12
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 2 times, most recently from 6832347 to 1bc8e6aCompareSeptember 2, 2026 11:29
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 1bc8e6a to ae956d3CompareSeptember 2, 2026 11:39
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from ae956d3 to eecf05dCompareSeptember 2, 2026 11:58
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from 335a54a to f915089CompareSeptember 2, 2026 13:06
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from facd9ed to 15e0c1eCompareSeptember 2, 2026 14:42
shai-almogand others added 4 commits September 2, 2026 18:02
None of the 24 generated figures could be reproduced outside CI. Running the
generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels
differing. Two consecutive local runs are byte-identical, so the generator is
deterministic on one host; the variable is the font.
`PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which
resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux
alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT
silently substitutes and every glyph changes. The differences are exactly that:
for `flow-layout.png` every differing pixel sits in y 10-130, the title and
label rows, while the colored blocks below match to the pixel.
So the byte-exact gate has only ever been green because CI both generates and
verifies. A developer regenerating locally could not match it, and at the scale
the guide's remaining ~260 app screenshots would need, that is untenable.
The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont`
reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath
rather than from an installed-font lookup. This is also what the project's font
rule requires everywhere: never `createSystemFont`, always the `native:` scheme.
A null return refuses loudly rather than falling back to a host font, because a
silent fallback would restore the exact non-determinism this removes.
The committed baselines are regenerated from a Mac. CI byte-compares them on
Linux, so the check either passes -- proving host independence rather than
asserting it -- or fails and says so immediately.
Composition, dimensions and colours are unchanged; only the typeface moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ours
CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only
one containing a `TextField`. The other 23 matched Linux byte for byte, so the
bundled-font change worked -- it just did not reach far enough.
`applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the
theme's default font, which resolves through the host, so the text field and its
hint were still host-dependent. Enumerating the types that carry text would have
left the next one added broken the same way, so the walk now pins the face on
every component it visits, plus the hint label, which is painted by a `Label`
that is not in the component tree and so is never reached by the walk.
The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px
against the theme default's 13px the field grew and squeezed "Submit" down to a
clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the
composition is unchanged and only the typeface moves.
Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed
at a family that does not exist -- which is what a machine without Arial looks
like to the port. All 24 come out byte-identical, so nothing in these figures
reads an installed font any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Committed screenshot is stale: <name>" names the file and nothing else, so
there is no way to tell a real regression from an environment difference
without adding a debugging round trip to CI. The generated directory is now
uploaded as an artifact when the step fails.
Needed immediately: one figure still differs between a Mac and the runner after
the font fix, and host fonts, JDK version and working directory have each been
ruled out locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… edge
The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.
`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.
`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.
So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.
Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almogand others added 4 commits September 2, 2026 18:02
Two review findings on this PR.
The first says `native:MainRegular` does not give host-independent output,
because `loadTrueTypeFont` resolves native fonts to the first installed SF or
Helvetica family before reaching the bundled Roboto. The branch is real, but it
is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose
systemFontFamily contains "helvetica" -- and this generator never loads a skin.
The measurement agrees: figures rendered on a Mac match the Linux runner byte
for byte, which could not happen if one side were resolving Helvetica Neue and
the other Roboto.
So the conclusion does not hold today, but the risk is real for tomorrow: a
change that loads a skin here would put host fonts back into the output with no
other symptom. The generator now refuses to run under an iOS platform, and says
why, rather than leaving that to a comment nobody reads.
The second finding is straightforwardly right and is the more serious of the
two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the
`Determine changed components` filter named only two scripts, so a pull request
touching any other script here started the workflow with `docs` false and
skipped the steps that script governs. A change to compare-screenshots.py could
have merged without the screenshot check ever running it -- a gate that skips
itself. The filter now covers the whole directory, which also removes the
two-copies-drift the surrounding comment already warns about.
Verified the guard changes no output: all 24 figures are unchanged, and the
comparer still passes against the runner's own bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator incremented its mismatch counter only for pixels whose channel
delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a
sidecar written the way this one was -- a large delta bound paired with a small
area bound -- that leaves an unbounded hole, and review gave the exploit in the
figures' own palette: recolouring the green #06a806 to #a608a6 moves every
channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and
a dramatically different image reports zero mismatches.
Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the
image, at a worst delta of exactly 160 -- and passed.
The two bounds are now independent. maxMismatchPercent limits how much of the
image may change at all, counting every differing pixel; maxChannelDelta caps how
far any single pixel may move. The measured legitimate noise -- 173 pixels,
0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a
five-pixel solid overwrite fails on delta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit changed the comparator so every differing pixel counts
toward the area budget and the channel delta is a separate ceiling. The sidecar
still explained the old behaviour -- that the area bound did the work and the
delta was meaningless on its own -- which is now wrong in a file whose whole
purpose is to justify the numbers beside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator converted both images to RGB before counting differences. The
committed figures are genuine RGBA PNGs -- four channels, currently opaque
everywhere -- so that dropped a real channel, and any change confined to alpha
was invisible to the tolerance path. A regression that turned the whole figure
transparent while leaving every colour channel intact reported zero changed
pixels and passed.
Byte equality, which every figure without a sidecar is still held to, always
caught this. Only a figure carrying a sidecar could reach the weakened path, so
today the exposure was one image -- but that image is exactly the one whose
comparison is relaxed.
Verified: making layered-layout.png fully transparent with its RGB channels
untouched now reports 100% of pixels changed, the real runner output still
passes, and the recolour and untoleranced-difference probes still fail as they
did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 15e0c1e to 815afcfCompareSeptember 2, 2026 15:02
@shai-almog
shai-almog merged commit 21923bf into devguide-guardsSep 2, 2026
4 checks passed
@shai-almog
shai-almog deleted the devguide-figure-harness branch September 2, 2026 15:21
Sign up for freeto 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.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Guide figures render with a bundled font instead of the host's - #5668

Merged
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness
Sep 2, 2026
Merged

Guide figures render with a bundled font instead of the host's#5668
shai-almog merged 8 commits into
devguide-guardsfrom
devguide-figure-harness

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Stacked on #5664. Review that one first; this diff is only the last commit.

The defect

None of the 24 generated figures can be reproduced outside CI. Running the generator on a Mac reproduces 0 of 24 byte-for-byte, with 3–20% of pixels differing.

Two consecutive local runs are byte-identical, so the generator is deterministic on one host. The variable is the font:

  • PreAdvancedThemingScreenshots styled with Font.createSystemFont, which resolves through JavaSEPort.fontFaceSystem"Arial" on macOS and Linux alike (JavaSEPort.java:765).
  • Arial exists on a developer's Mac and not on a stock CI runner, so AWT silently substitutes a different face.
  • The differences are exactly that. For flow-layout.png every differing pixel sits in y 10–130 — the title and label rows — while the coloured blocks below match to the pixel.

So the byte-exact gate in developer-guide-docs.yml has only ever been green because CI both generates and verifies. A developer regenerating locally could not match it, and the guide has ~260 more app screenshots that want the same treatment.

The fix

The figures load native:MainRegular, which JavaSEPort.loadTrueTypeFont (:13551) reads from /com/codename1/impl/javase/Roboto-Medium.ttf on the classpath rather than from an installed-font lookup. That cannot depend on what the machine has installed.

This is also what the project's own font rule requires everywhere — never createSystemFont, always the native: scheme — so the violation and the non-determinism were the same bug.

A null return refuses loudly rather than falling back to a host font: a silent fallback would restore precisely the non-determinism this removes.

How this PR proves itself

The committed baselines are regenerated from a Mac, deliberately. CI byte-compares them on Linux. So the existing screenshot check either passes — demonstrating host independence rather than asserting it — or fails and says so immediately.

Composition, dimensions and colours are unchanged; only the typeface moves.

Scope note

These 24 are schematic layout diagrams — green labelled blocks (North/South/East/West/Center) on white under a blue title bar — whose job is to show where a layout manager puts things. They are deliberately not app screenshots, so they keep their hand-styling; rendering them under Material 3 or Liquid Glass would bury what they teach. The native-theme and light/dark machinery belongs with the app-UI figures in the Components, io and graphics chapters, and lands with those.

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T15:06:10.352436Z815afcfNew commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 506b447 to 8a91850CompareSeptember 2, 2026 07:11
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:8a91850b97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:de62de4b98

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread.github/workflows/developer-guide-docs.yml

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2989351749

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threaddocs/developer-guide/img/layered-layout.tolerance

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:365153a366

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/compare-screenshots.py Outdated
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 691841e to 2e7dd10CompareSeptember 2, 2026 09:56

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:2e7dd10fe6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadscripts/developer-guide/check-guide-structure.py Outdated
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 2e7dd10 to f02771eCompareSeptember 2, 2026 10:06
@shai-almog
shai-almog changed the base branch from devguide-structure-and-guards to devguide-guardsSeptember 2, 2026 10:12
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 2 times, most recently from 6832347 to 1bc8e6aCompareSeptember 2, 2026 11:29
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 1bc8e6a to ae956d3CompareSeptember 2, 2026 11:39
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from ae956d3 to eecf05dCompareSeptember 2, 2026 11:58
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from 335a54a to f915089CompareSeptember 2, 2026 13:06
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch 3 times, most recently from facd9ed to 15e0c1eCompareSeptember 2, 2026 14:42
shai-almogand others added 4 commits September 2, 2026 18:02
None of the 24 generated figures could be reproduced outside CI. Running the
generator on a Mac reproduces 0 of 24 byte-for-byte, with 3-20% of pixels
differing. Two consecutive local runs are byte-identical, so the generator is
deterministic on one host; the variable is the font.
`PreAdvancedThemingScreenshots` styled with `Font.createSystemFont`, which
resolves through `JavaSEPort.fontFaceSystem` -- "Arial" on macOS and Linux
alike. Arial exists on a developer's Mac and not on a stock CI runner, so AWT
silently substitutes and every glyph changes. The differences are exactly that:
for `flow-layout.png` every differing pixel sits in y 10-130, the title and
label rows, while the colored blocks below match to the pixel.
So the byte-exact gate has only ever been green because CI both generates and
verifies. A developer regenerating locally could not match it, and at the scale
the guide's remaining ~260 app screenshots would need, that is untenable.
The figures now load `native:MainRegular`, which `JavaSEPort.loadTrueTypeFont`
reads from `/com/codename1/impl/javase/Roboto-Medium.ttf` on the classpath
rather than from an installed-font lookup. This is also what the project's font
rule requires everywhere: never `createSystemFont`, always the `native:` scheme.
A null return refuses loudly rather than falling back to a host font, because a
silent fallback would restore the exact non-determinism this removes.
The committed baselines are regenerated from a Mac. CI byte-compares them on
Linux, so the check either passes -- proving host independence rather than
asserting it -- or fails and says so immediately.
Composition, dimensions and colours are unchanged; only the typeface moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ours
CI reported one stale figure out of 24: `guibuilder-2-insets-3.png`, the only
one containing a `TextField`. The other 23 matched Linux byte for byte, so the
bundled-font change worked -- it just did not reach far enough.
`applyBlockStyleToContent` styled `Label` and `Button`. Everything else kept the
theme's default font, which resolves through the host, so the text field and its
hint were still host-dependent. Enumerating the types that carry text would have
left the next one added broken the same way, so the walk now pins the face on
every component it visits, plus the hint label, which is painted by a `Label`
that is not in the component tree and so is never reached by the walk.
The first attempt used `BLOCK_FONT` for this and regressed the figure: at 29px
against the theme default's 13px the field grew and squeezed "Submit" down to a
clipped sliver. `FIELD_FONT` is sized to reproduce the original height, so the
composition is unchanged and only the typeface moves.
Verified locally by rendering twice, once with `JavaSEPort.setFontFaces` pointed
at a family that does not exist -- which is what a machine without Arial looks
like to the port. All 24 come out byte-identical, so nothing in these figures
reads an installed font any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Committed screenshot is stale: <name>" names the file and nothing else, so
there is no way to tell a real regression from an environment difference
without adding a debugging round trip to CI. The generated directory is now
uploaded as an artifact when the step fails.
Needed immediately: one figure still differs between a Mac and the runner after
the font fix, and host fonts, JDK version and working directory have each been
ruled out locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… edge
The artifact step paid for itself immediately. Downloading what the runner
rendered showed two figures differing, not the one the gate reported -- it exits
on the first mismatch -- and the two had nothing in common.
`mig-layout.png` differed by 12.8% of its pixels, a real layout shift.
MigLayout takes its default gaps from `PlatformDefaults`, which reads
`System.getProperty("os.name")` and answers MAC_OSX, GNOME or WINDOWS_XP, each
with different spacing. The figure was rendering with macOS gaps on a Mac and
GNOME gaps on the runner. Pinning the platform fixes it: with the pin, 23 of the
24 figures now match the runner's own output byte for byte.
`layered-layout.png` differed by 173 pixels, 0.113%, inside a 25x25 box. That
one is not fixable. Measured against the runner's bytes, the material glyph
lands at exactly the same size and the same origin -- a 55x49 bounding box --
and differs only in antialiased edge coverage, 946 fully-white pixels against
916. Java2D rasterizes the same glyph, from the same bundled font, at the same
size, slightly differently on the two platforms. Demanding byte equality there
would mean deleting legitimate content from the figure or carrying a
permanently red check.
So the comparison moves from `cmp -s` to a comparer that still requires byte
equality by default and accepts a bounded difference only where a figure carries
a `.tolerance` sidecar explaining itself, in the same key=value shape the CN1SS
suites already use. The area bound does the work: a per-pixel delta that large is
meaningless on its own, since a glyph edge flips between white and the block
behind it, but a regression that changed the icon would move far more than 0.3%
of the image.
Verified by running the comparer with the runner's own output against the
committed figures -- which is exactly what CI will do -- and by four probes: an
untoleranced figure that differs fails, the toleranced figure fails when changed
beyond its budget, a missing figure fails the count, and a clean run passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shai-almogand others added 4 commits September 2, 2026 18:02
Two review findings on this PR.
The first says `native:MainRegular` does not give host-independent output,
because `loadTrueTypeFont` resolves native fonts to the first installed SF or
Helvetica family before reaching the bundled Roboto. The branch is real, but it
is reached only when `isIOS` is set, which `loadSkinFile` does for a skin whose
systemFontFamily contains "helvetica" -- and this generator never loads a skin.
The measurement agrees: figures rendered on a Mac match the Linux runner byte
for byte, which could not happen if one side were resolving Helvetica Neue and
the other Roboto.
So the conclusion does not hold today, but the risk is real for tomorrow: a
change that loads a skin here would put host fonts back into the output with no
other symptom. The generator now refuses to run under an iOS platform, and says
why, rather than leaving that to a comment nobody reads.
The second finding is straightforwardly right and is the more serious of the
two. `on.pull_request.paths` triggers on `scripts/developer-guide/**`, but the
`Determine changed components` filter named only two scripts, so a pull request
touching any other script here started the workflow with `docs` false and
skipped the steps that script governs. A change to compare-screenshots.py could
have merged without the screenshot check ever running it -- a gate that skips
itself. The filter now covers the whole directory, which also removes the
two-copies-drift the surrounding comment already warns about.
Verified the guard changes no output: all 24 figures are unchanged, and the
comparer still passes against the runner's own bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator incremented its mismatch counter only for pixels whose channel
delta EXCEEDED maxChannelDelta, which is what the CN1SS comparator does. With a
sidecar written the way this one was -- a large delta bound paired with a small
area bound -- that leaves an unbounded hole, and review gave the exploit in the
figures' own palette: recolouring the green #06a806 to #a608a6 moves every
channel by exactly 160, so with maxChannelDelta=160 not one pixel is counted and
a dramatically different image reports zero mismatches.
Reproduced it before fixing: the recolour changes 4191 pixels, 2.73% of the
image, at a worst delta of exactly 160 -- and passed.
The two bounds are now independent. maxMismatchPercent limits how much of the
image may change at all, counting every differing pixel; maxChannelDelta caps how
far any single pixel may move. The measured legitimate noise -- 173 pixels,
0.113%, worst delta 141 -- still passes, the recolour now fails on area, and a
five-pixel solid overwrite fails on delta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit changed the comparator so every differing pixel counts
toward the area budget and the channel delta is a separate ceiling. The sidecar
still explained the old behaviour -- that the area bound did the work and the
delta was meaningless on its own -- which is now wrong in a file whose whole
purpose is to justify the numbers beside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comparator converted both images to RGB before counting differences. The
committed figures are genuine RGBA PNGs -- four channels, currently opaque
everywhere -- so that dropped a real channel, and any change confined to alpha
was invisible to the tolerance path. A regression that turned the whole figure
transparent while leaving every colour channel intact reported zero changed
pixels and passed.
Byte equality, which every figure without a sidecar is still held to, always
caught this. Only a figure carrying a sidecar could reach the weakened path, so
today the exposure was one image -- but that image is exactly the one whose
comparison is relaxed.
Verified: making layered-layout.png fully transparent with its RGB channels
untouched now reports 100% of pixels changed, the real runner output still
passes, and the recolour and untoleranced-difference probes still fail as they
did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almogforce-pushed the devguide-figure-harness branch from 15e0c1e to 815afcfCompareSeptember 2, 2026 15:02
@shai-almog
shai-almog merged commit 21923bf into devguide-guardsSep 2, 2026
4 checks passed
@shai-almog
shai-almog deleted the devguide-figure-harness branch September 2, 2026 15:21
Sign up for freeto 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.

1 participant

@shai-almog