The build-time vector transcoder lets you author UI icons and illustrations as SVG or Lottie / Bodymovin JSON and have them rendered as native Codename One Image instances on every platform (iOS, Android, JavaSE simulator, JavaScript) without shipping a runtime SVG or Lottie parser.

Both formats share the same pipeline: each source file is lowered into the SVG transcoder’s model, the same JavaCodeGenerator emits a com.codename1.ui.GeneratedSVGImage subclass, and the same SVGRegistry makes the result available via Resources.getImage("name.<ext>"). The rest of this guide therefore covers SVG in detail; Lottie gets its own section at the end that only calls out the parts that differ.

Motivation

Codename One’s older Image.createSVG() API depends on a per-platform native SVG renderer that ships only on the JavaSE simulator; iOS and Android applications fall back to PNG multi-image buckets, giving up the resolution-independence that SVG was designed for. The legacy Flamingo SVG transcoder was a precursor to this work — a build-time tool that emitted Java drawing code from an SVG — but it pre-dated CN1’s affine transform pipeline and was missing a Maven hook, CSS integration, animations, and most of the SVG spec.

This transcoder is its successor and covers each of those gaps. Every SVG you drop into src/main/css/ is parsed by codenameone-svg-transcoder, emitted as a Java class that subclasses com.codename1.ui.GeneratedSVGImage, and rendered through the standard Graphics shape API. The same vector source produces pixel-perfect output at any size on every port.

Quick start

  1. Drop your SVG next to theme.css in src/main/css/:

    src/main/css/
      theme.css
      home.svg
  2. Reference it from CSS and pin its rendered size in millimeters — the recommended sizing knob, since it carries unchanged across every device DPI:

    HomeIcon {
        background: url(home.svg);
        cn1-svg-width: 6mm;
        cn1-svg-height: 6mm;
        cn1-background-type: cn1-image-scaled-fit;
    }
  3. Build the project:

    mvn package

That’s it. App code doesn’t call into the registry. The transcoder emits com.codename1.generated.svg.SVGRegistry only when SVGs are present, and the per-port build wiring (IPhoneBuilder, AndroidGradleBuilder, the JavaSE port) loads it automatically before init(Object). Resources.getGlobalResources().getImage("home.svg") returns the transcoded image; CSS rules that reference the SVG by URL pick it up via the same registry.

Why millimeters

cn1-svg-width / cn1-svg-height are the sizing knob you should reach for first.

  • A 6mm icon is 6mm tall on a 1x desktop, 6mm on a high-DPI handset, and 6mm on a 4K tablet. The transcoder routes both values through Display.convertToPixels() at install time, the same way font-size: 3mm behaves elsewhere in CN1 CSS.

  • SVGs in the wild routinely declare odd width/height attributes (a 1024×1024 export of a 24×24 icon, no dimensions at all, etc.). Pinning the rendered size in millimeters sidesteps that guesswork — the SVG’s declared pixel size and the device DPI become irrelevant.

  • It’s the only sizing mode that survives a re-export of the SVG from the asset pipeline. cn1-source-dpi and the implicit medium-density default both depend on the SVG’s declared pixel size, so a tool that rescales the source on export will change the rendered size without any warning.

StarIcon { background: url(star.svg); cn1-svg-width: 4mm; cn1-svg-height: 4mm; }
PrimaryIcon { background: url(home.svg); cn1-svg-width: 6mm; cn1-svg-height: 6mm; }
LogoBanner { background: url(logo.svg); cn1-svg-width: 32mm; cn1-svg-height: 12mm; }

Use cn1-source-dpi: <bucket> (accepted keywords: low, medium, high, very-high, hd, 560, 2hd, 4k) only when an SVG has sensible declared pixel dimensions for a known density target and you want the rendered size to track Display.getDeviceDensity() instead of millimeters. With no hint at all, the SVG’s declared dimensions are treated as design pixels at DENSITY_MEDIUM.

If both keys appear on the same rule, cn1-svg-width / cn1-svg-height wins.

How the build flow works

The cn1app archetype binds the transcode-svg goal to the generate-sources phase:

<execution>
    <id>transcode-svg</id>
    <phase>generate-sources</phase>
    <goals>
        <goal>transcode-svg</goal>
    </goals>
</execution>

That execution was added to the archetype after the Maven project format itself shipped, so a project generated before it has no transcoder bound. Because nothing fails when the transcoder is missing — the CSS compiler leaves a transparent placeholder in the theme and the app renders nothing where the icon should be — the build repairs the gap rather than waiting to be asked. A module that contains vector assets but binds no transcode-svg execution is transcoded in place during the current build and has the execution written into its POM, with the previous contents kept as pom.xml.bak. A project that already binds the goal, or that has no vector assets, is left untouched.

When Maven runs the mojo:

  1. It scans src/main/css/ and src/main/svg/ for *.svg files. If there are none, the goal is a no-op — no registry class is emitted, no per-port stub injection happens.

  2. For each SVG it emits target/generated-sources/svg/com/codename1/generated/svg/<Name>.java — one class per file — plus a single SVGRegistry class whose installGlobal() registers every transcoded image with the global Resources table.

  3. It scans src/main/css/*/.css for url(*.svg) occurrences and the enclosing rule’s cn1-svg-width / cn1-svg-height / cn1-source-dpi hints. Those values are baked into the registry’s installGlobal() call per image.

  4. It drops a 1×1 transparent PNG placeholder into target/css-resources/ so the standalone CSS compiler can finish even though the file extension on the URL is .svg. The placeholder is overwritten in the theme by the runtime registry call.

The simulator / desktop ports load SVGRegistry reflectively, so a project that adds its first SVG today rebuilds and runs without any code change. The iOS and Android builders detect the generated class in the user’s compile output and weave installGlobal() into the generated Stub right before the first init(Object); a project with no SVGs gets no weaving.

Calling the registry yourself

If you don’t use theme.css but still want a transcoded SVG, construct the generated class directly. It lands in com.codename1.generated.svg and is named after the file, so src/main/svg/logo.svg becomes com.codename1.generated.svg.Logo. The class exists only after a build has run the transcoder, which is why this listing isn’t one of the compiled examples:

import com.codename1.generated.svg.Logo;
import com.codename1.ui.Display;

Logo logo = new Logo();                      // declared size at DENSITY_MEDIUM
Logo dense = new Logo(Display.DENSITY_HIGH); // read as a higher-density design
Logo sized = new Logo(12f, 12f);             // 12mm square, converted per device

myButton.setIcon(sized);

Constructors come in three flavours, matching the three CSS sizing mechanisms above. The two-float constructor takes millimeters; that’s the one to prefer for the reasons in the previous section.

scaled(int, int) returns a lightweight view that reports the requested dimensions from getWidth() / getHeight() and shares the animation clock with its source.

SVG feature coverage

The transcoder targets the SVG 1.1 static-shape vocabulary plus the SMIL animation subset:

FeatureStatus

<rect>, <circle>, <ellipse>, <line>, <polyline>, <polygon>

Full

<path> (M/L/H/V/C/S/Q/T/A/Z, relative + smooth-curve reflection)

Full

<g> with transform="translate | scale | rotate | skew | matrix"

Full

fill, stroke, stroke-width, stroke-linecap/linejoin/miterlimit

Full

opacity, fill-opacity, stroke-opacity

Full (animatable)

<linearGradient>

Full (shape-clipped fill on every port)

<radialGradient>

First-stop fallback

<animate> (numeric attributes)

Full

<animateTransform type="translate | scale | rotate">

Full

<set> (instant value snap)

Full

<text> / <tspan> (single-style fills, transforms)

Supported

<clipPath> referenced via clip-path="url(#id)"

Supported (rect/circle/path); nested clip refs ignored

<mask>

Treated as clip — alpha masking falls back to opaque

SVG filter primitives

Not supported

CSS-keyframe animations

Not supported (SMIL only)

SMIL animations read the current time from com.codename1.ui.animations.AnimationTime, so tests can pin the clock with AnimationTime.setTime(t) to capture a deterministic frame. Animated SVGs return true from Image.isAnimation() — as with Timeline, you must register the image with a Form’s animation manager (or set it as a `Component.setIcon with isAnimation() true) for the repaint loop to tick the SMIL clock.

Lottie animations

Lottie / Bodymovin JSON files are picked up by the same transcode-svg goal. Drop them next to your CSS (or under src/main/lottie/) and the Lottie parser lowers each one into the SVG model — so everything in the previous sections (sizing keys, registry, theme url(…​) lookup, Resources.getImage(…​), the per-port wiring) applies unchanged:

src/main/css/
  theme.css
  spinner.json     <-- Lottie/Bodymovin export
SpinnerStyle {
    background: url(spinner.json);
    cn1-svg-width: 12mm;
    cn1-svg-height: 12mm;
    cn1-background-type: cn1-image-scaled-fit;
}
Image spin = Resources.getGlobalResources().getImage("spinner.json");
// or by stem, like a multi-image:
Image spin2 = Resources.getGlobalResources().getImage("spinner");

.lottie (dotLottie ZIP) files are accepted alongside .json for forward compatibility, but the archive container isn’t yet extracted — export your animation as a plain Bodymovin JSON for now.

Lottie feature coverage

The parser targets the subset of Bodymovin a "spinner" or "pulse" animation typically uses. Anything outside the subset is dropped without warning so a file with mixed coverage still produces a renderable class:

FeatureStatus

Shape layers (ty:4) with grouped rc / el / sh primitives

Full

Solid color layers (ty:1)

Rendered as a filled rect

Shape fills (fl) and strokes (st) — solid colors

Full

Layer transform (anchor, position, scale, rotation, opacity) — static

Full

Animated rotation / position / scale — 2 keyframes

Full (loops indefinitely over the comp duration)

Animated colors / opacity

Collapsed to the first keyframe

Bezier easing on keyframes

Linear interpolation (easing curves ignored)

Multi-keyframe properties (3+)

Collapsed to first vs. last (matches the SVG codegen’s from/to model)

Trim path (tm), repeater (rp), merge (mm), rounded corners (rd)

Ignored

Gradient fills (gf) / gradient strokes (gs)

Ignored

Text layers, image layers, precomp, mattes, expressions

Ignored

For animations that need higher fidelity than the subset above, export the relevant frame as an SVG and use the SVG transcoder path directly — the runtime classes are identical.

Troubleshooting

Resources.getImage("name.svg") returns null or a 1×1 transparent PNG

The transcoder ran with zero source files: the name in the CSS url(…​) doesn’t match any file under src/main/css/ (or src/main/svg/ / src/main/lottie/ for the dedicated directories). The build names each unmatched reference in a warning. A POM that doesn’t bind the transcode-svg goal at all is repaired automatically, so it’s not the likely cause. The same goal handles .svg, .json, and .lottie — one goal, both formats. For arbitrary Resources bundles loaded outside the global slot, call com.codename1.ui.util.Resources.registerGeneratedImage(name, image) yourself with a fresh new com.codename1.generated.svg.YourAsset(widthMm, heightMm).

Lottie animation looks frozen or starts halfway through

The Lottie parser collapses each animated property’s keyframe array to its first and last value, then loops indefinitely over the composition’s duration. Animations with three or more keyframes on the same property therefore play as a straight first-to-last interpolation. Re-export the comp split into two-keyframe segments or use an SVG/SMIL export for that animation if you need exact keyframe playback.

SVG looks the wrong size

Switch the rule to cn1-svg-width / cn1-svg-height in millimeters. The other sizing modes depend on the SVG’s declared pixel size, which is what’s biting you here.

Spinner / pulse renders but doesn’t animate

Check that the image is mounted into a Component whose animation manager ticks (Labels added to a Form do this automatically). For a pure-Graphics-driven capture (screenshot tests) pin AnimationTime.setTime(…​) and re-render.