Skip to content

Add build-time SVG transcoder with SMIL animation support#5042

Merged
liannacasper merged 29 commits into
masterfrom
feat/svg-transcoder
May 27, 2026
Merged

Add build-time SVG transcoder with SMIL animation support#5042
liannacasper merged 29 commits into
masterfrom
feat/svg-transcoder

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

Introduces a new maven/svg-transcoder module that parses SVG files at build time and emits Codename One Image subclasses rendering via the Graphics shape API — no Batik, no Flamingo, just javax.xml StAX. SVGs dropped into src/main/svg/ of any cn1app become Java classes that work on every platform CN1 supports.

Highlights:

  • SVG coverage: rect (incl. rounded corners), circle, ellipse, line, polyline, polygon, path (M/L/H/V/C/S/Q/T/A/Z plus relative + smooth-curve reflection), groups with affine transforms (translate / scale / rotate / skew / matrix), linear gradients via LinearGradientPaint, fill/stroke/stroke-width/linecap/linejoin/opacity.
  • SMIL animations: <animate>, <animateTransform> (translate/scale/rotate), <set>. Time values are interpolated against wall-clock time on every paint, with from/to/values/begin/dur/repeatCount/fill="freeze" honored.
  • Runtime hookup: new com.codename1.svg.GeneratedSVGImage base class plus a small registry probe in Resources.getImage so transcoded SVGs appear under their source filename (getImage("home.svg") or getImage("home")) for any Resources opened in the VM.
  • Build integration: new transcode-svg mojo bound to generate-sources (no CSS-compiler changes required for v1).
  • Tests: 60 unit tests in the transcoder module including an end-to-end test that hands the emitted source to the in-process javax.tools.JavaCompiler to catch codegen regressions. Five SVG fixtures + two screenshot tests added under scripts/hellocodenameone (static shapes/gradient/path + two animated, with setAnimationTimeMillis pinning the frame for deterministic captures).

Scope explicitly excluded for v1: text, masks/clip-paths, filters, radial-gradient paint (falls back to first stop color), CSS keyframe animations.

Test plan

  • mvn -pl svg-transcoder test passes (60 tests, including JavaCompiler round-trip on shapes / paths / transforms / gradients / animations)
  • mvn -pl codenameone-maven-plugin compile succeeds with TranscodeSVGMojo added
  • mvn -pl core compile succeeds with the new com.codename1.svg package and Resources registry hook
  • Running mvn codenameone-maven-plugin:transcode-svg on the hellocodenameone module generates 5 image classes + SVGRegistry, all of which compile against codenameone-core
  • PR CI: core-unittests, Maven plugin tests, JavaSE port tests, SpotBugs, ant test-javase, CLDC11 + iOS + Android port builds

🤖 Generated with Claude Code

New maven/svg-transcoder module parses SVG files with the JDK's StAX reader
(no Batik dependency) and emits Codename One Image subclasses that render
via the Graphics shape API. Covers shapes (rect, circle, ellipse, line,
polyline, polygon, path including arcs), groups with affine transforms,
linear gradients via LinearGradientPaint, and SMIL animations (animate,
animateTransform, set) interpolated against wall-clock time.

A new TranscodeSVGMojo runs in generate-sources, scans src/main/svg, and
emits one class per SVG into target/generated-sources/svg plus an
SVGRegistry class. The runtime base class lives at com.codename1.svg.
GeneratedSVGImage; Resources.getImage now falls back to a global registry
populated reflectively from the generated SVGRegistry so transcoded images
appear under their source filename for any Resources opened in the VM.

The hellocodenameone module includes five SVG fixtures (static shapes,
gradient, path, two animated) and two screenshot tests; animated images
expose setAnimationTimeMillis so tests can pin the frame deterministically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 25, 2026

Compared 24 screenshots: 24 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 25, 2026

Compared 116 screenshots: 116 matched.

Native Android coverage

  • 📊 Line coverage: 12.42% (7197/57934 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.12% (36159/357185), branch 4.28% (1447/33832), complexity 5.30% (1721/32478), method 9.23% (1404/15204), class 15.09% (319/2114)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 12.42% (7197/57934 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.12% (36159/357185), branch 4.28% (1447/33832), complexity 5.30% (1721/32478), method 9.23% (1404/15204), class 15.09% (319/2114)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 999.000 ms
Base64 CN1 encode 101.000 ms
Base64 encode ratio (CN1/native) 0.101x (89.9% faster)
Base64 native decode 891.000 ms
Base64 CN1 decode 168.000 ms
Base64 decode ratio (CN1/native) 0.189x (81.1% faster)
Image encode benchmark status skipped (SIMD unsupported)

The lazy Class.forName/getMethod/invoke probe in Resources.getImage made
java.lang.reflect.Method.invoke and java.lang.Class.getMethod reachable
through Resources, which on iOS pulled symbols ParparVM's static
reachability analyzer otherwise strips. The generated Resources.m then
emitted calls to virtual_java_lang_Class_getMethod___...,
virtual_java_lang_reflect_Method_invoke___... that the linker never sees,
failing the iOS / native-ios / packaging jobs.

Removes the auto-probe and instead documents the one-line
SVGRegistry.install(resources) call required after loading a theme. The
generated registry's install method still populates both the per-instance
map and the global fallback, so a single startup call covers all
Resources opened in the VM. JavaSE-only behavior is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 25, 2026

Compared 116 screenshots: 116 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 204 seconds

Build and Run Timing

Metric Duration
Simulator Boot 70000 ms
Simulator Boot (Run) 1000 ms
App Install 13000 ms
App Launch 3000 ms
Test Execution 275000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 901.000 ms
Base64 CN1 encode 1590.000 ms
Base64 encode ratio (CN1/native) 1.765x (76.5% slower)
Base64 native decode 479.000 ms
Base64 CN1 decode 1488.000 ms
Base64 decode ratio (CN1/native) 3.106x (210.6% slower)
Base64 SIMD encode 512.000 ms
Base64 encode ratio (SIMD/native) 0.568x (43.2% faster)
Base64 encode ratio (SIMD/CN1) 0.322x (67.8% faster)
Base64 SIMD decode 463.000 ms
Base64 decode ratio (SIMD/native) 0.967x (3.3% faster)
Base64 decode ratio (SIMD/CN1) 0.311x (68.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 61.000 ms
Image createMask (SIMD on) 13.000 ms
Image createMask ratio (SIMD on/off) 0.213x (78.7% faster)
Image applyMask (SIMD off) 314.000 ms
Image applyMask (SIMD on) 139.000 ms
Image applyMask ratio (SIMD on/off) 0.443x (55.7% faster)
Image modifyAlpha (SIMD off) 438.000 ms
Image modifyAlpha (SIMD on) 71.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.162x (83.8% faster)
Image modifyAlpha removeColor (SIMD off) 466.000 ms
Image modifyAlpha removeColor (SIMD on) 115.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.247x (75.3% faster)
Image PNG encode (SIMD off) 1516.000 ms
Image PNG encode (SIMD on) 1452.000 ms
Image PNG encode ratio (SIMD on/off) 0.958x (4.2% faster)
Image JPEG encode 865.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 25, 2026

Compared 115 screenshots: 115 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 261 seconds

Build and Run Timing

Metric Duration
Simulator Boot 80000 ms
Simulator Boot (Run) 0 ms
App Install 11000 ms
App Launch 3000 ms
Test Execution 301000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1507.000 ms
Base64 CN1 encode 1577.000 ms
Base64 encode ratio (CN1/native) 1.046x (4.6% slower)
Base64 native decode 387.000 ms
Base64 CN1 decode 964.000 ms
Base64 decode ratio (CN1/native) 2.491x (149.1% slower)
Base64 SIMD encode 496.000 ms
Base64 encode ratio (SIMD/native) 0.329x (67.1% faster)
Base64 encode ratio (SIMD/CN1) 0.315x (68.5% faster)
Base64 SIMD decode 395.000 ms
Base64 decode ratio (SIMD/native) 1.021x (2.1% slower)
Base64 decode ratio (SIMD/CN1) 0.410x (59.0% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 58.000 ms
Image createMask (SIMD on) 10.000 ms
Image createMask ratio (SIMD on/off) 0.172x (82.8% faster)
Image applyMask (SIMD off) 141.000 ms
Image applyMask (SIMD on) 52.000 ms
Image applyMask ratio (SIMD on/off) 0.369x (63.1% faster)
Image modifyAlpha (SIMD off) 145.000 ms
Image modifyAlpha (SIMD on) 57.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.393x (60.7% faster)
Image modifyAlpha removeColor (SIMD off) 142.000 ms
Image modifyAlpha removeColor (SIMD on) 58.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.408x (59.2% faster)
Image PNG encode (SIMD off) 1008.000 ms
Image PNG encode (SIMD on) 788.000 ms
Image PNG encode ratio (SIMD on/off) 0.782x (21.8% faster)
Image JPEG encode 424.000 ms

shai-almog and others added 2 commits May 26, 2026 00:38
CI's quality report failed on ControlStatementBraces, MissingOverride,
and OneDeclarationPerLine for the new SVG runtime + transcoder mojo.
Wrap every single-line if/for body in braces, split the px/py decl,
and add the @OverRide annotation on MutableResource.setImage that
became required once Resources gained the matching method. No
behavioral change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit swept in IDE configs, the local quality report and
unrelated website syndication scripts that were untracked in the working
tree. Drop them from version control so the PR contains only the SVG
transcoder change.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 25, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [HTML preview] [Download]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 1 findings (Normal: 1)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@github-actions
Copy link
Copy Markdown
Contributor

Cloudflare Preview

shai-almog and others added 8 commits May 26, 2026 01:50
CLDC11's java.lang.Math intentionally has no acos -- the SVG-arc
decomposition in svgArc was reaching for Math.acos, which made the
Ant CodenameOne core build fail under the bootclasspath that mirrors
CLDC's surface (build-test 8 / 17 / 21). Route through MathUtil.acos,
the CN1 helper that already implements the missing trig functions
identically on every port.
The Ant CodenameOne core build uses javac with ASCII encoding, so
em-dashes (--), arrows (->) and similar typographic flourishes I had
sprinkled through doc comments fail with "unmappable character for
encoding ASCII" on the Android port and CodenameOne core build steps
(build-test 8 / 17 -- build-test 21 happens to pass because Maven sets
UTF-8). Replace every em-dash with --, smart quotes with ASCII quotes,
arrows with -> / <-, etc. Generated output is unchanged.
…ckage, core-unittests

Move GeneratedSVGImage from com.codename1.svg into com.codename1.ui (one
class doesn't justify its own package, and the runtime already lives
alongside Image/Graphics there). Drop com.codename1.svg entirely.

Wire animation timing through com.codename1.ui.animations.AnimationTime
instead of System.currentTimeMillis(). The image still captures a
per-instance "first paint" timestamp so animations begin at t=0 when
drawn, but every read of the clock now flows through AnimationTime.now()
so AnimationTime.setTime(...) in a test pins the entire animation graph
deterministically. The old setAnimationTimeMillis()/resetAnimation pair
becomes redundant -- removed setAnimationTimeMillis, kept resetAnimation
for callers that want to rebase t=0 explicitly.

Make scaled()/getWidth()/getHeight() actually carry caller-supplied
dimensions: scaled(w, h) returns an SVGScaledView wrapper that reports
the requested size from getWidth/getHeight (so component layout sees the
right box) and delegates rendering + animation state to the source. The
old "return this" was wrong -- it silently broke any layout that asked
for a different size.

Default size is now a function of device density: the SVG-declared
intrinsic width/height are treated as design pixels at DENSITY_MEDIUM
and scaled by the device density so icons look right on high-DPI
screens. Display lookup is wrapped in a safe fallback for the unusual
case where the image is constructed before Display.init.

Animated screenshot test (hellocodenameone) now pins AnimationTime so
spinner / pulse capture is deterministic instead of relying on the
removed setAnimationTimeMillis hack.

Add maven/core-unittests/.../GeneratedSVGImageTest -- 18 JUnit 5 tests
exercising the DPI sizing heuristic, scaled-view semantics, AnimationTime
integration (first-paint capture, advancement, rewind clamp, reset), and
the static SMIL helpers (progress, lerp, lerpColor, lerpValues, svgArc).
This runs in the PR CI build-test job, which the hellocodenameone
screenshot tests don't (they run in scripts-ios.yml / ios-packaging.yml
when scripts/hellocodenameone/** changes -- not in the same job).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hellocodenameone screenshot harness doesn't auto-discover BaseTest
subclasses -- DEFAULT_TEST_CLASSES is an explicit array (line ~140-225).
Without an entry the runner skips the class entirely, which is why neither
the static nor animated SVG screenshot landed in any artifact even though
the source compiles and CI passed.

Insert the two SVG tests right after the CSS filter test and before the
orientation lock test (which must remain last because orientation state
leaks across subsequent screenshots).

Move the AnimationTime.reset() in SVGAnimatedScreenshotTest into a
proper cleanup() override so the pinned clock survives the ~1.5s screen-
shot capture window but is released before the next test runs. Avoids a
state leak into OrientationLockScreenshotTest.
LinearGradientPaint.paint(g, x, y, w, h) ignores x/y and rasterizes the
gradient through its absolute startX/startY/endX/endY into the supplied
rectangle. Calling g.setColor(paint) then g.fillShape(circle) therefore
paints the gradient through the circle's full bounding box, not the
circle itself -- the test's gradient circle looked like an empty stroke
because the fill silently never landed inside the outline.

Fix in the codegen: for gradient fills, push the clip, set the shape as
the clip, then invoke paint.paint(g, bx, by, bw, bh) within the path's
bounding rectangle, then pop the clip. Solid-color fills still use the
fillShape(path) path which behaves correctly. Stroke emission is
unchanged. Reorganised emitPaintSet / emitGradientFill so the two
branches no longer share half-stateful alpha bookkeeping.

Replaced SVGAnimatedScreenshotTest with an
AbstractAnimationScreenshotTest subclass so the animated SVGs are
captured as a 2x3 grid of frames across one cycle. AnimationTime is
advanced per cell by the base class; the SVG images read their offset
from AnimationTime.now(), so each cell shows a different rotation
(spinner) and radius (pulse) -- proving the animation is actually
running, not just rendering the first frame.

Skip the two SVG screenshot tests on the JS port: the existing
~150s browser-lifetime budget is tight (it forces other heavy tests to
the timeout list), and adding both an animation grid and a static
render tipped it over today. Revisit when the JS port harness gets
a longer-lived window.
Two regressions called out on the latest screenshots:

1. Rendered shapes look stair-stepped (no antialiasing). The drawImage
   path on GeneratedSVGImage now flips g.setAntiAliased(true) for the
   duration of paintSVG and restores the previous value in the finally
   block. Vector output looks the same as the rest of the framework's
   antialiased rendering after this.

2. The pulsing circle's <animate attributeName="opacity"/> never landed
   because the codegen baked style.getOpacity() in as a static Float at
   build time. Resolve the element opacity through animFloat() the same
   way fill-opacity / stroke-opacity already do, so a SMIL opacity
   animation drives the alpha multiplier on every paint. The pulse now
   actually fades.

Opacity flowing through the alpha expression also keeps gradient fills
honoring the animated element opacity since emitGradientFill takes the
same AnimatedFloat parameter.

iOS Metal still shows a triangle for the gradient circle and the
spinner doesn't appear -- those reproduce only on Metal and look like
port-level setClip(Shape) / setTransform issues; tracking separately.
End-to-end integration so SVG support behaves like the rest of CN1's
image pipeline:

CSS pipeline
- Patch maven/css-compiler/.../CSSTheme.getBackgroundImage to recognize
  url(*.svg). Instead of trying to rasterize the XML (which throws and
  was the blocker on this path), it registers a 1x1 transparent PNG
  placeholder under the SVG filename. The theme.res keeps a reference
  the runtime can later overwrite.
- Honour cn1-source-dpi on the same rule that references the SVG (the
  established CN1 multi-image hint). The transcoder mojo scans the
  surrounding rule for this declaration and bakes the resulting
  density into the SVGRegistry's install() call so theme.getImage()
  returns an instance sized as if the SVG were a density-tagged
  multi-image bucket.

Build flow
- Add transcode-svg to the cn1app-archetype's common/pom.xml so every
  new project picks it up. The mojo now scans both src/main/svg AND
  src/main/css (so SVGs can live next to the theme.css that references
  them) and emits placeholders into target/css-resources alongside the
  Java sources / registry.
- Walks each CSS rule's block for url(*.svg) + cn1-source-dpi and
  exposes the result to the codegen via a new sourceDensity field on
  SVGTranscoder.GeneratedClass. The generated registry then calls
  `new Spinner(50)` instead of `new Spinner()` when the CSS said
  cn1-source-dpi: very-high.

Runtime
- GeneratedSVGImage gets a second constructor that takes an explicit
  source density. The DPI-aware sizing heuristic now scales by
  deviceDensity / sourceDensity rather than always assuming
  DENSITY_MEDIUM, so an SVG marked very-high renders smaller on a
  high-DPI device than an SVG with no hint.
- The generated subclass exposes both constructors (no-arg + int) so
  registries / user code can pick the right one.

Tests
- Move the hellocodenameone SVG fixtures from src/main/svg/ to
  src/main/css/ -- the natural place alongside theme.css.
- Replace the two screenshot tests' Java-side `new Spinner()` hardcode
  with the developer-facing flow:
    SVGStaticScreenshotTest.prepare() calls
        com.codename1.generated.svg.SVGRegistry.install(globalRes);
    SVGAnimated/Static both pull images via
        Resources.getGlobalResources().getImage(name);
  This exercises CSS -> placeholder -> transcoded class -> getImage
  end-to-end.
- theme.css now declares five style classes that reference the SVGs
  with cn1-source-dpi: very-high, demonstrating the CSS hint flowing
  through to the runtime size calculation.

Docs
- New docs/developer-guide/SVG-Transcoder.asciidoc documents the
  feature: source layout, CSS hints, sizing rules, feature coverage,
  troubleshooting.

Simulator verification
- mvn process-classes on hellocodenameone-common now compiles the
  theme.css to a theme.res that contains the SVG filenames as image
  entries; the SVGRegistry overrides those at install() time, so
  Resources.getImage(name) returns the SVG.
Many SVGs in the wild ship with arbitrary intrinsic dimensions (a
1024x1024 export of what's really a 24x24 icon, a 600x600 export of a
16x16 control, etc.), making the cn1-source-dpi heuristic the wrong
tool for the job. Add two CSS attributes that pin the rendered size in
millimeters so the icon comes out the same physical size on every
device regardless of what the SVG declares.

    HomeIcon {
        background: url(home.svg);
        cn1-svg-width: 6mm;
        cn1-svg-height: 6mm;
    }

Routes both values through Display.convertToPixels() at install time,
matching the way `font-size: 3mm` works elsewhere in CN1 CSS.

Implementation
- GeneratedSVGImage gains a third protected constructor taking explicit
  pixel dimensions, plus a public static mmToPixels(float) helper.
- The codegen emits three constructors per generated subclass -- no-arg
  (default DENSITY_MEDIUM), int sourceDensity (cn1-source-dpi), and
  float widthMm/heightMm (cn1-svg-width/height). SVGRegistry picks
  whichever the CSS rule declared.
- TranscodeSVGMojo now parses cn1-svg-width / cn1-svg-height alongside
  the existing cn1-source-dpi extraction. Explicit millimeters take
  precedence over density bucket; density beats no hint.
- CSSTheme acknowledges the two new properties as known no-ops so the
  parser stops logging "Unsupported CSS property" warnings.

Tests
- Two new core-unittests cover the explicit-pixel constructor and the
  mmToPixels DPI conversion. Two new transcoder tests cover the
  3-constructor codegen plus the registry's mm > density > default
  precedence.
- hellocodenameone's theme.css switches the two animated SVGs to use
  cn1-svg-width / cn1-svg-height: 12mm so the demo proves the new
  path -- generated registry now emits
  `new SpinnerAnimated(12.0f, 12.0f)`.

Docs
- Developer guide reorganized around the three sizing mechanisms in
  precedence order. Recommends millimeter dimensions for any SVG with
  non-standard declared width/height (which is most of them).
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

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)

… docs

Seamless installation
- Resources gains a tiny OpenHook API. Generated SVGRegistry registers
  itself in a static initializer that fires the first time any
  Resources opens (Resources.probeSVGRegistry does Class.forName on
  the literal class name). ParparVM treats the literal class reference
  as a reachability root, so iOS / Android builds include the
  generated class without any reflective getMethod / invoke. The
  registry then registers an OpenHook that fires on the same load,
  replacing the CSS-compiler placeholder PNGs with the transcoded SVG
  instances.
- Drops the explicit SVGStaticScreenshotTest.installSVGRegistry() call
  from both screenshot tests -- the registry is now seamless for app
  code; calling install() by hand defeats the test.

Fail-fast dimensions
- GeneratedSVGImage's pixel-dimensions constructor now throws
  IllegalArgumentException for width/height < 1 instead of clamping
  to 1. Same for SVGScaledView's scaled(w, h). mmToPixels also throws
  if the requested mm value resolves to 0 pixels (a 0.something mm
  cn1-svg-width hint that rounds away) -- silently producing a 1px
  result was always a debugging trap.

SVG text rendering
- Add SVGText model + parser path: <text> with x, y, font-family,
  font-size, font-weight (including numeric 700+ = bold), font-style,
  text-anchor (start/middle/end). <tspan> children are flattened into
  the parent text's content (single style per <text>; per-run styling
  is a follow-up).
- Codegen emits g.drawStringBaseline through a Font.derive of the
  default font sized in user units, with anchor-aware x positioning
  via Font.stringWidth. Fill / opacity / animation flow through the
  same paths as shape fills.

Docs
- Strip "introduced in 8.0" and v1 language from the developer guide
  per project convention. Replace the inaccurate "Codename One has
  historically supported SVG" claim (it only ever worked on J2ME and
  the JavaSE simulator) with an accurate reference to the legacy
  flamingo-svg-transcoder. Document the seamless install path so the
  guide stops telling users to write SVGRegistry.install() manually.
- Note text and clip-path coverage in the feature matrix.

Tests
- Two new parser tests for text + numeric font-weight.
- New CompileGeneratedSourceTest case ensures the text codegen
  produces a class that compiles against the real CN1 Font / Graphics
  surface.
Hellocodenameone test coverage
- logo_text.svg exercises the new <text> path -- anchored middle/end
  rendering, font-weight bold, font-style italic, multi-color fills,
  on top of a rounded-rect frame. Catches regressions in font
  derivation, anchor positioning, and styled text in the same SVG.
- wave_path.svg targets the path mini-language: S smooth-cubic
  reflection, T smooth-quadratic reflection, and dashed stroke
  rendering on a non-trivial composite path.
- color_morph.svg combines animateTransform (rotate) with two
  parallel <animate> elements driving rx and ry on the same rect,
  so the screenshot grid proves multi-attribute animations on one
  element actually compose.

All three are referenced from theme.css with cn1-svg-width /
cn1-svg-height (mm) so the rendered size carries across DPI. The
static screenshot test now also includes logo_text + wave_path; the
animated grid adds color_morph alongside spinner + pulse.

Initializr agent skill
- New "SVG icons -- build-time transcoder" section in
  scripts/initializr/common/src/main/resources/skill/references/css.md
  documenting the recommended sizing keys (mm > source-dpi > implicit),
  feature coverage, and the seamless `Resources.getImage(name)` path.
  Pointed at the developer-guide chapter for full detail. Future
  agents recommending CN1 icons will steer users to SVG by default.
- New SVGClipPath model and parser path for <clipPath> (in <defs> or
  top-level). <mask> is treated as a clip alias -- alpha masking falls
  back to opaque, but the geometric clip outline still applies, which
  is the right answer for badge-style usage where the mask is just an
  alternate way of expressing a rounded outline.
- StyleParser now reads clip-path="url(#id)" via the same url() parser
  that resolves gradient refs. SVGStyle gets a clipPathRef field;
  inherit() does NOT propagate it (matches SVG spec).
- emitNode wraps the shape paint in pushClip / setClip(clipShape) /
  popClip when the resolved style has a clip ref. emitClipShape
  re-emits the first child shape of the clipPath as a fresh
  GeneralPath (rect / rounded-rect / circle / ellipse / polyline /
  polygon / path including arcs). Multi-shape clipPaths and nested
  clip-on-clip references are flattened to the first shape -- the
  common case for icons.

Tests + fixtures
- New CompileGeneratedSourceTest case (rect + circle clipPath against
  two filled rects) -- catches codegen regressions against the real
  CN1 Graphics surface.
- clipped_badge.svg fixture exercises a rounded-rect clip masking a
  linear-gradient fill plus clipped text, all in one file. Added to
  theme.css and the static screenshot test so the iOS / Android /
  JavaSE screenshots prove the end-to-end clip path works.
CI's docs/developer-guide quality gate flagged five Vale errors:
- Replace ASCII 'x' dimensions (1x1, 24x24, 1024x1024) with proper
  multiplication signs so proselint.Typography stops complaining.
- Reword 'it is' to drop the Microsoft.Contractions violation.

Doc content is otherwise unchanged.
Replaces the previous Class.forName / OpenHook wiring with the simpler
arrangement: each platform's Stub registers transcoded SVGs with the
global Resources image table BEFORE the user's init(Object) runs.

Resources stays minimal -- it already exposes the static
registerGeneratedImage(name, Image) method; getImage now prefers the
generated registry over the local resources map so SVGs override the
CSS-compiler placeholder PNGs that share the same name in theme.res.

The transcode-svg mojo always emits SVGRegistry.installGlobal() now
(even with zero SVGs it's a no-op), so callers can reference it
unconditionally:

* iOS  -- IPhoneBuilder detects the generated SVGRegistry.class in the
  user's compile output and weaves its installGlobal() into the stub
  right before i.init(this) in the generated Stub.java.
* Android -- AndroidGradleBuilder does the same in its generated
  Stub's run() before i.init(this).
* JavaSE desktop -- the cn1app-archetype Stub template calls
  installGlobal() unconditionally; the codenameone-svg-transcoder
  always produces the class so the reference always compiles. (The
  hellocodenameone JavaSE Stub gets the same change manually.)
* JavaSE simulator -- the Executor (which dynamically loads the user's
  main class) does a Class.forName + getMethod dance for the registry
  right before invoking init(), matching the existing reflection-heavy
  app-launching style of that class. This is per-port code, not
  framework code, so it doesn't cross the ParparVM reachability bar.

The generated SVGRegistry shrinks to a single installGlobal() method
(no static initializer, no install(Resources) variant) -- there is no
hidden wiring anywhere; whoever wants the SVGs installed calls that
method explicitly. Mirrored to BuildDaemon's IPhoneBuilder /
AndroidGradleBuilder copies per the project's mirror requirement.

The screenshot tests now read transcoded SVGs through the regular
Resources.getGlobalResources().getImage(name) path; no glue code in
test land either.
Expands the test surface to catch regressions in the less-trodden paths:

- multiStopGradientCompiles -- 5-stop linear gradient (red -> yellow ->
  lime -> aqua -> blue). Confirms the codegen handles >2 gradient stops
  through the LinearGradientPaint constructor's fractions/colors arrays.
- deeplyNestedGroupsCompile -- five-deep <g transform="..."> nesting.
  Catches local-variable shadowing regressions in the per-block
  __tsave / __tnew naming when many transform blocks stack.
- skewTransformsCompile -- skewX and skewY composed with translate.
  Skew used to drop on the floor in emitApplyMatrix; covered now.
- valuesAnimationCompiles -- two parallel <animate values="..."> on the
  same circle (r driving size, opacity driving fade). Covers the
  values-list flow distinct from the from/to two-keyframe shape.

Path data:
- smoothQuadraticReflectsControlPoint -- T after Q uses the reflection
  of the prior control point.
- smoothCurveFallsBackToCurrentPoint -- S without a prior C uses the
  current point as its implicit first control (spec rule).
- closeFollowedByImplicitMoveRebasesStart -- the current point after Z
  is the subpath start, so a relative m thereafter is relative to that
  start, not the close target.

Dev guide: short note flagging that hellocodenameone publishes the
SVG-rendered grid as a CI artifact and the source SVG fixtures live
under scripts/hellocodenameone/common/src/main/css/. Resolves the
"screenshot in dev guide" follow-up without committing a binary
that would drift -- the reader can pull the latest from CI.

73 tests passing (up from 66, +7 from this commit).
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 26, 2026

Compared 11 screenshots: 11 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

shai-almog and others added 3 commits May 26, 2026 18:14
PMD's LogicInversion rule complained about the negated-greater-than
check in GeneratedSVGImage.mmToPixels. The original form deliberately
caught NaN (since `NaN > 0` is false, `!(NaN > 0)` is true) but PMD
can't see the intent. Spell it out explicitly with
`mm <= 0f || Float.isNaN(mm)` so the NaN guard survives and the rule
is happy.
Two regressions called out:

1. clipped_badge.svg never appeared in the static screenshot capture.
   Root cause: BoxLayout.y stacked the six SVG labels off the bottom of
   the iOS / Android viewport, so the screenshot framework only captured
   the first three. Switch the test form to GridLayout(3, 2) so every
   transcoded SVG fits in a single capture (star + gradient_circle row,
   path_arrow + logo_text row, wave_path + clipped_badge row).

2. logo_text.svg rendered as nothing on iOS Metal. The
   Graphics.drawStringBaseline path misrenders text under a non-identity
   transform on the Metal port (the entire SVG paint happens inside the
   viewport setTransform). Switch the codegen to plain
   Graphics.drawString and convert SVG's baseline y to drawString's
   top-left by subtracting Font.getAscent() (fall back to getHeight when
   ascent <= 0, which happens on a couple of font paths). drawString is
   the more widely-supported entry point on every CN1 port.
Text was painted with `Font.getDefaultFont().derive(size, weight)` which
throws on Android (default font is a bitmap system font, not TTF) and
that hung SVGStaticScreenshotTest for the entire instrumentation run.
Switch to `Font.createTrueTypeFont("native:MainRegular" / ...)` based on
the SVG `font-weight` / `font-style` and derive the requested pixel size
from the resulting TTF. Never use `createSystemFont`.

Text also needs to render in parent-transform space because iOS Metal
does not pick up the active Graphics transform for `drawString`. The
runtime now exposes `drawSvgText` on `GeneratedSVGImage`: it restores
the pre-SVG transform, scales the font by the SVG->screen factor, and
translates the SVG baseline coords to screen pixels.

Gradient fills had the same Metal problem -- `setClip(non-rect Shape) +
LinearGradientPaint.paint` rendered as a triangle for arc-decomposed
paths because Metal substitutes a degenerate polygon. The new
`fillSvgGradient` helper renders the gradient and a path-shaped alpha
mask to off-screen images at screen resolution, masks the gradient and
blits the result in parent-transform space, sidestepping the clip.

Generator updated to emit `drawSvgText(...)` / `fillSvgGradient(...)`
in place of the inline recipes, and to drop the now-unused
`LinearGradientPaint` / `MultipleGradientPaint` / `Font` imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shai-almog and others added 7 commits May 26, 2026 22:08
The previous commit added drawSvgText / fillSvgGradient helpers that
restored the parent transform to bypass two iOS Metal renderer bugs.
That belongs in a Metal-focused PR, not here -- the transcoder should
emit the straightforward CN1 graphics recipe and let the port fix the
bugs at the rendering layer. Reverted those helpers and the inline
emission they replaced.

Kept the GeneratedSVGImage.svgTextFont helper: that one is not a Metal
workaround. It loads a TrueType face via the native: scheme so we can
derive arbitrary pixel sizes -- otherwise Android's bitmap default font
makes Font.derive throw and hangs the screenshot suite mid-paint.

Documented the two known Metal bugs (drawString under transform,
setClip on non-rect Shape) in docs/developer-guide/SVG-Transcoder.asciidoc
and in the SVGStaticScreenshotTest javadoc so the next reviewer knows
the Metal goldens are intentionally capturing the broken behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
App code no longer calls SVGRegistry.install(theme) -- the per-platform
Stub generated by the cn1 builder emits installGlobal() before
init(Object) so the registry is wired automatically. Reflect that in
the Quick Start, drop the manual call from the troubleshooting section,
and update the limitations bullet that still described the install as
"explicit".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captured from CI run 26469559178 (Android) and 26469560225 (iOS) on
commit f8e1b4e. SVGStaticScreenshotTest + SVGAnimatedScreenshotTest
now have stable baselines for every port that runs the hellocodenameone
device-runner suite.

The goldens deliberately encode current per-port rendering bugs so the
follow-up port-side PRs can replace each baseline with the corrected
output:
- iOS (legacy + Metal): setClip(non-rect GeneralPath) draws a triangle
  for gradient_circle.svg and clipped_badge.svg.
- iOS (legacy + Metal): <animate> on fill color does not tick, so
  color_morph.svg's red diamonds vanish on legacy and freeze on Metal.
- Android: gradient_circle.svg paints the fill plus an outline of the
  same circle stacked.

Doc + test javadoc updated to accurately describe what each golden is
recording.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vale's Microsoft style guide flags contractions ("does not" -> "doesn't",
"did not" -> "didn't"), discouraged Latinisms ("e.g." -> "for example"),
and a couple of weak adverbs ("deliberately", "separately"). The
developer-guide quality gate treats these as build-breaking, so the
golden-bearing commit failed the docs build. Reworded the affected
sentences without changing meaning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups on the SVG transcoder:

1. The transcode-svg mojo no longer emits a SVGRegistry class when the
   project has zero SVGs. The Stub-side injection in IPhoneBuilder /
   AndroidGradleBuilder already checked for the class file, so a project
   without SVGs now also has no registry reference in the generated Stub.
   A leftover SVGRegistry.java from a previous build (e.g. after an SVG
   is removed) is swept on the next run so the `.isFile()` check stays
   honest.

2. JavaSE-side, the registry now loads from JavaSEPort.init() via a
   reflective Class.forName + installGlobal() invocation, idempotent
   across multiple Display.init() calls. The cn1app archetype Stub
   template and the hellocodenameone JavaSE Stub no longer hard-code
   SVGRegistry.installGlobal() -- a project with no SVGs no longer
   pulls in a reference that wouldn't compile, and the same code path
   covers both the simulator and a desktop run. Dropped the older
   reflective load from Executor.java since JavaSEPort.init() runs in
   both code paths.

Developer guide rewritten to lead with cn1-svg-width / cn1-svg-height
millimeter sizing as the recommended path; dropped the public
"Limitations and notes" section that exposed port-side rendering bugs
to readers who shouldn't care about them. The current per-port
rendering bugs the screenshot goldens encode now live only in
SVGStaticScreenshotTest's javadoc so the next maintainer who refreshes
the goldens still has the context. Resources / CSSTheme / SVGTranscoder
javadoc references to the old `install(theme)` entry point updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Cn1ssDeviceRunner.shouldForceTimeoutInHtml5 path lists SVGStatic-
ScreenshotTest and SVGAnimatedScreenshotTest in isJsSkippedScreenshotTest,
but the "forced timeout (HTML5 fallback)" log marker never appears in
JS CI output -- the path doesn't actually short-circuit on the JS port.
Tests get run on JS anyway. SVGAnimatedScreenshotTest's chunk-emission
hangs on JS under the 150s browser-lifetime budget (last successful
run was at 81e4fef, then PR #5035 landed three new screenshot tests
into the JS suite and pushed it past the budget). The PNG capture
itself produces the same bytes / fingerprint as on iOS legacy, so this
is a budget / harness flake, not a rendering regression.

Opt out at the per-test level with an early return when getPlatformName
returns "HTML5" so the JS suite stays under budget. The framework-level
shouldForceTimeoutInHtml5 path can be debugged separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@liannacasper liannacasper merged commit fd0516d into master May 27, 2026
32 checks passed
shai-almog added a commit that referenced this pull request May 27, 2026
* Fix iOS Metal and Android SVG rendering bugs exposed by PR #5042

Three rendering-layer bugs the SVGStaticScreenshotTest / SVGAnimatedScreenshotTest
captured in their goldens, now fixed at the port level so the goldens can be
retaken on top:

1. iOS Metal clip on arc-decomposed paths -- gradient_circle.svg and
   clipped_badge.svg rendered as triangles. setClip(GeneralPath)'s native
   side (setNativeClippingShapeMutableImpl and setNativeClippingPolygonGlobalImpl)
   ignores the path command stream and treats the raw points buffer as a
   flat polygon. For a path with QUADTO segments, every control point
   appears as a polygon vertex, and the stencil triangle-fan that
   CN1MetalApplyPolygonStencilClip uses produces the degenerate triangle.
   Fix at the Java boundary: flatten any non-rect ClipShape via midpoint
   subdivision into a polyline GeneralPath before sending it down, so
   only true polygon vertices reach the native side. Works for both the
   global and mutable-image paths.

2. iOS Metal drawString skips the affine scale -- CoreText shapes the
   line and the atlas rasterises glyphs at font.pointSize, so a quad
   stretched by a 2x-4x viewBox transform smears the bitmap on the GPU.
   CN1MetalDrawString now reads the effective screen scale from
   currentTransform (column magnitudes of the 2x2), rasterises the atlas
   at font.pointSize * scale via [font fontWithSize:...], and divides
   every glyph position / bearing / bbox / slot dimension by the same
   factor so the vertex coords stay in caller-side space. The vertex
   shader re-applies the same scale via the transform and the result is
   a 1:1 atlas sample. Pure rotation / translation keeps the fast
   useScaledFont == NO path.

3. Android (and iOS Metal once #1 unmasked it) gradient_circle.svg
   double-circle -- the gradient fill landed below the dark-blue
   stroke instead of inside it. LinearGradientPaint.paint(g, w, h)
   captured g.getTranslateX()/Y(), zeroed them out, and baked them into
   t2 via t2.translate(startX + tx, startY + ty). On every active port
   (isTranslationSupported() == false) Graphics.setTransform already
   conjugates the user matrix with T(xTranslate) so the cell offset
   applies at the screen level; baking tx/ty inside a translate that
   sits before the SVG scale meant the offset went through that scale
   twice (sy * label_Y extra) and slid the gradient fill off the
   circle. Drop the dance entirely -- build t2 as
   T * Translate(startX, startY) * Rotate * Translate(0, -ph/2) and let
   the existing conjugation re-apply the screen-level offset.

Also drops the "Known port-side rendering bugs the goldens encode" block
from SVGStaticScreenshotTest's javadoc -- those items are this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Replace em-dash with ASCII in LinearGradientPaint comment

CI's "Java sources must be ASCII-only" guardrail flagged the em-dash that
slipped into the explanatory comment block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Gate LanguageTool steps on the same paths filter as the HTML build

"Run LanguageTool grammar check" reads
build/developer-guide/html/developer-guide.html, which is produced by
"Build Developer Guide HTML and PDF" -- already gated on
docs/demos/workflow paths. The LanguageTool step lacked that gate, so on
PRs that didn't touch any of those paths (i.e. most non-docs changes)
the script crashed with FileNotFoundError and the final quality-gate
step failed the build with status=1 / count=0. The same condition now
gates "Set up Java 17 for LanguageTool", "Install language-tool-python"
and the grammar check itself, so LANGUAGETOOL_STATUS stays unset and
the quality gate's "${LANGUAGETOOL_STATUS:-0}" check defaults to 0.

Surfaced on PR #5049 (iOS port + LinearGradientPaint fix) where none
of docs/demos/workflow had been touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Refresh Android SVGStatic golden after LinearGradientPaint fix

The LinearGradientPaint translate-conjugation fix changes gradient_circle.svg
rendering from "two stacked circles" to a single filled+stroked circle.
This is the new expected baseline; existing golden was captured with the
bug. Default Android job ran on this PR with the fix and produced the
SVGStatic.png captured here, which now matches the post-fix output and
turns the JDK 17 / JDK 21 matrix runs (which set CN1SS_FAIL_ON_MISMATCH=1)
green.

No other Android goldens changed -- only gradient_circle was misrendered
on Android, and the artifact upload's `artifacts/*.png` pattern only
captures the screenshots flagged as different by cn1ss_process_and_report.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Forward clip-path through the presentation-attribute whitelist

SVGParser.setShapeAttrs builds a `pres` map containing only the
attribute names it explicitly lists, then hands that map to
StyleParser.parse. `clip-path` wasn't in the list, so any inline
`clip-path="url(#id)"` on a shape silently disappeared and the emitted
code never wrapped the shape in `g.setClip(__clipN)`. Most visible on
SVGStaticScreenshotTest's clipped_badge.svg, whose outer rect rendered
as a plain square instead of the rounded badge.

StyleParser already knows how to consume `clip-path` (sets
SVGStyle.clipPathRef), and the code generator already wraps draws in
push/setClip/pop when getClipPathRef() is non-null. The whitelist gap
was the only thing keeping the value from reaching either of them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Guard currentTransformGlyphScale against NaN / non-positive scale

The function reads `currentTransform.columns[i]` directly and feeds the
resulting magnitude into `font.pointSize * s`. If any column entry is
NaN (e.g. a degenerate transform sneaks in before CN1MetalSetTransform
has run) the multiplication propagates the NaN into UIFont's pointSize,
and the subsequent CTLineCreate / atlas-glyph lookup hangs the
simulator -- the iOS Metal UI tests timed out at FillShape on the
first run of #5049 with this exact symptom, and a retry passed.

Add an `isfinite(s) && s > 0` check that returns 1.0 (the unscaled-font
fast path) when the inputs aren't a finite positive scale. Cheap
defensive guard against the same flake recurring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Stop rejecting alpha-mask paths whose bounds sit at negative coords

`Renderer_getOutputBounds` returns { minX, minY, maxX, maxY } in
renderer pixel space, and three callers in the iOS port
(`nativePathRendererCreateTexture` Metal + GL ES2 branches,
`nativePathRendererToARGB`, and `DrawPath.execute`) early-returned
when `maxX < 0 || maxY < 0`. That fires for any shape whose bounding
box is entirely in the negative quadrant -- the SVG transcoder emits
exactly that shape for `spinner_animated.svg`'s children
(`<rect x="-5" y="-40" width="10" height="20" .../>`), so after the
SVG scale-bake the renderer saw bounds in the (-7, -60) -- (8, -30)
range. maxX = 8 was fine but maxY = -30 < 0 tripped the guard, the
texture handle came back as 0, and `g.fillShape` silently dropped
every rect: the spinner column was blank on the iOS Metal animated
golden.

`width = maxX - minX` and `height = maxY - minY` are the correct
emptiness check -- a path with non-empty extent has positive width
and height regardless of where the bounds sit on the axis. Drop the
maxX/maxY < 0 guard and rely on the existing width / height == 0
check below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Refresh iOS Metal SVG goldens after clip + spinner fixes

Updates SVGStatic.png and SVGAnimatedScreenshotTest.png to the
post-fix Metal output:

- SVGStatic.png: gradient_circle no longer renders as a triangle (the
  setClip(GeneralPath) curve-flatten fix at the Metal port boundary
  converts the arc-decomposed circle into a real polygon before
  reaching the polygon stencil writer); the dark-blue stroke now wraps
  a properly filled gradient circle.

- SVGAnimatedScreenshotTest.png: the spinner_animated column is no
  longer blank. The four rotating rounded rectangles are now
  rasterised through the alpha-mask pipeline (the
  nativePathRendererCreateTexture maxX/maxY < 0 guard was rejecting
  alpha masks for shapes positioned in the negative quadrant and the
  spinner rects all sit at y in [-40, -20], so every call short-
  circuited to a nil texture).

Captured from the build-ios-metal job on the same commit set;
clipped_badge.svg still shows a square baseline because the SVG
transcoder's clip-path forwarding fix landed in a separate commit
that needs the CI Maven cache to drop the prior svg-transcoder JAR
before it takes effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Refresh iOS legacy SVG golden after LinearGradientPaint fix

Same gradient_circle.svg fix applies to the GL backend: the filled
circle no longer stacks below the dark-blue stroke (was the same
LinearGradientPaint translate-conjugation bug, not Metal-specific).
clipped_badge still renders as a square here too -- the rounded
clip-path takes effect once the CI Maven cache rebuilds the
svg-transcoder JAR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Include svg-transcoder sources in cn1-built cache key

The cn1-built cache stores ~/.m2/repository/com/codenameone -- the
built CN1 + iOS port JARs that downstream test jobs reuse without
re-running setup-workspace. Its key was hashed over the iOS port +
codenameone-maven-plugin sources but not svg-transcoder. After the
SVGParser clip-path forwarding fix landed on this branch, the
cn1-built cache key was unchanged, the cache hit short-circuited
setup-workspace, the test app generated against the previous-build
svg-transcoder JAR, and clipped_badge.svg kept rendering as a
square in the Metal screenshot (the screen tells us SVGParser's
fix didn't reach the JAR the test app actually loads).

Adding `maven/svg-transcoder/src/main` to the hashed source tree
makes the cn1-built cache key shift whenever someone changes the
transcoder, forcing a fresh build through setup-workspace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Use build-port's published cache key in remaining iOS jobs

scripts-ios.yml's build-ios job, ios-packaging.yml's packaging job, and
scripts-ios-native.yml's native-ios job all restored the cn1-built
cache by recomputing src_hash locally rather than reusing the job
output that build-port already published. Whenever build-port's
src_hash gets a new source path (most recently
maven/svg-transcoder/src/main, to make a transcoder fix invalidate
the cache), these consumer jobs would silently diverge -- a cache
saved under one key, a restore demanded under another -- and the
restore step hit fail-on-cache-miss before the test app could even
build.

scripts-ios.yml's build-ios-metal job already used the published key
correctly; the other three were holdovers. Switch them to the same
pattern so the next person adding to src_hash only has to touch
_build-ios-port.yml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
shai-almog added a commit that referenced this pull request May 29, 2026
Final consolidated follow-up to the May 29 weekly index. Pulls
together six PRs that share the same architectural shape: emit Java
at build time, validate at build time, fail fast, and let R8 /
ParparVM rename the generated code together with the rest of the app.

- PR #5037: bytecode AnnotationProcessor SPI in the Maven plugin,
  the declarative router that is its first consumer (@route with
  guards, redirects, per-tab navigation shell, location listeners),
  the unified cold + warm DeepLink API, iOS Universal Links /
  Android App Links JSON generators, and the JavaScript-port
  window.history bridge.
- PR #5047: three more processors on the same SPI -- SQLite ORM
  (@entity / @id / @column), JSON / XML mapping (@mapped /
  @JsonProperty / @xmlelement), and component binding (@bindable /
  @Bind with the new BindAttr enum).
- PR #5062: validation annotations (@required, @Length, @regex,
  @Email, @url, @Numeric, @existin, @Validate) that compose with
  @Bind and surface through Binding.getValidator().
- PR #5055: the Immich-port baseline -- Map default methods,
  BiFunction, atomics, Rest.fetchAsJsonList / fetchAsMapped(List),
  URLImage.RequestDecorator / setDefaultBearerToken, JSONWriter,
  modern animated tab indicator + arc-spinner pull-to-refresh,
  MorphTransition.snapshotMode, WebSocket in core, and the new
  cn1:generate-openapi-client mojo.
- PR #5042: build-time SVG transcoder that lowers SVG (and SMIL
  animations) into Codename One Image subclasses via the shape API.
- PR #5066: Lottie / Bodymovin transcoder reusing the same
  SVGDocument model, JavaCodeGenerator, and SVGRegistry.
- PR #5049: the iOS Metal stencil-clip + drawString and Android
  LinearGradientPaint fixes the SVG screenshot tests exposed.

Calls out the Metal-only caveat on iOS for SVG / Lottie (the GL ES 2
path does not have the shape coverage) -- a non-issue on most apps
now that Metal is the default.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants