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
Drop your SVG next to
theme.cssinsrc/main/css/:src/main/css/ theme.css home.svgReference 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; }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 wayfont-size: 3mmbehaves elsewhere in CN1 CSS.SVGs in the wild routinely declare odd
width/heightattributes (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-dpiand 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:
It scans
src/main/css/andsrc/main/svg/for*.svgfiles. If there are none, the goal is a no-op — no registry class is emitted, no per-port stub injection happens.For each SVG it emits
target/generated-sources/svg/com/codename1/generated/svg/<Name>.java— one class per file — plus a singleSVGRegistryclass whoseinstallGlobal()registers every transcoded image with the globalResourcestable.It scans
src/main/css/*/.cssforurl(*.svg)occurrences and the enclosing rule’scn1-svg-width/cn1-svg-height/cn1-source-dpihints. Those values are baked into the registry’sinstallGlobal()call per image.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:
| Feature | Status |
|---|---|
| Full |
| Full |
| Full |
| Full |
| Full (animatable) |
| Full (shape-clipped fill on every port) |
| First-stop fallback |
| Full |
| Full |
| Full |
| Supported |
| Supported (rect/circle/path); nested clip refs ignored |
| Treated as clip — alpha masking falls back to opaque |
SVG | 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:
| Feature | Status |
|---|---|
Shape layers ( | Full |
Solid color layers ( | Rendered as a filled rect |
Shape fills ( | 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 |
Trim path ( | Ignored |
Gradient fills ( | 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 PNGThe transcoder ran with zero source files: the name in the CSS
url(…)doesn’t match any file undersrc/main/css/(orsrc/main/svg//src/main/lottie/for the dedicated directories). The build names each unmatched reference in a warning. A POM that doesn’t bind thetranscode-svggoal 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, callcom.codename1.ui.util.Resources.registerGeneratedImage(name, image)yourself with a freshnew 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-heightin 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
Componentwhose animation manager ticks (Labelsadded to aFormdo this automatically). For a pure-Graphics-driven capture (screenshot tests) pinAnimationTime.setTime(…)and re-render.