Bundled image assets
A PAPK can carry an ASST (“asset”) section of pre-decoded PNG images as LVGL-native RGB565 structures (the section arrived with format v1.1 and is unchanged in today’s v2). The framework builds the asset at PAPK-pack time, embeds it directly in the file, and the firmware maps it from XIP flash at runtime — ImageView.setImageSource("foo.png") becomes a name-keyed lookup with no PNG decoder on the device.
This guide covers the manifest format, the build pipeline, and the runtime API.
For images referenced by id — R.drawable.logo, android:src="@drawable/logo" — put them under
res/drawable/ instead; see resources. They are stored in the same ASSETS
section, under the name res/drawable/<file>.
Manifest format
In your app’s directory (e.g. examples/imagedemo/), declare the assets next to the Java sources:
examples/imagedemo/ PicodroidManifest.xml build.gradle.kts assets/ logo.png java/imagedemo/ImageDemoApp.javapapk-pack discovers anything under assets/ and emits one entry per file into the PAPK ASST section.
PNG decoding happens at pack time on the host. The on-device firmware sees only RGB565 framebuffers + dimensions. Today papk-pack bundles PNG only — the file scan is case-insensitive on the .png extension and any other file in assets/ is silently skipped (the host packer is built with the PNG codec alone). Alpha is discarded during the RGB565 conversion, so transparency is not preserved.
API: ImageView.setImageSource(String)
import picodroid.widget.ImageView;
ImageView img = new ImageView();img.setImageSource("logo.png"); // matches assets/logo.png in the app directoryThe string is the asset’s file name — setImageSource("logo.png") matches assets/logo.png. The packer scans assets/ flat (non-recursive), so the key is just the base file name including its extension. Each setImageSource is a small linear scan of the registry.
If the asset name doesn’t match anything, the call does nothing — no warning, no exception, no change to the widget. The lookup is silently best-effort, so double-check the spelling against papk-info (below) if an image doesn’t appear.
App icon
The manifest’s <application icon="…"> names one of the files under assets/
as the app’s icon — what a launcher shows beside the app’s label. The file
must exist (the Gradle build fails at configuration time when it does not) and
should be a small square: 48×48 is the convention, 64×64 the point past which
papk-pack warns, because every icon costs width × height × 2 bytes of flash
and a launcher scales anything larger. See the
manifest reference.
<application application="myapp/MyApp" label="My App" icon="icon.png" />Scale, tint, and aspect
The full Tier C ImageView surface lives in Graphics & UI → ImageView:
img.setScaleType(ImageView.SCALE_FIT_CENTER);img.setScale(384); // ImageView.SCALE_1X (256) = 1.0×, so 384 = 1.5×img.setTint(Color.RED); // the colour's alpha is the blend strengthFor scaled rendering to be anti-aliased, LV_DRAW_SW_SUPPORT_RGB565A8 must be enabled in lv_conf.h (it is, by default). Without it scaled images render aliased.
Step by step: from PNG to screen
-
Size the PNG for the screen. Each bundled image costs
width × height × 2bytes on flash (RGB565, 2 bytes per pixel — there is no compression on device). Budget against the app region every installed app shares —app_region_kbin the board’sboard.toml: 1536 KiB ontestbench_rp2350andpico_enviro_mon, 1280 KiB on the RP2350 WiFi boards, 10 MiB onpico_touch_kit, and 768 KiB ontestbench_rp2040(see limits):Image Bytes On flash 32×32 32 × 32 × 2 2 KiB 64×64 64 × 64 × 2 8 KiB 128×128 128 × 128 × 2 32 KiB 240×240 (full screen) 240 × 240 × 2 112.5 KiB Keep a full-screen background to one image — two of them already eat a quarter of a megabyte. As a rule of thumb keep the
assets/total well under ~256 KiB so code and bytecode still fit; the only hard error the packer raises is for a single image larger than 65535 px on a side. -
Drop it in
assets/. Create the directory next to your Java sources and add the.png(no manifest entry needed — the build auto-detectsassets/):examples/myapp/PicodroidManifest.xmlbuild.gradle.ktsassets/logo.pngjava/myapp/MyApp.java -
Build the PAPK.
Terminal window ./scripts/build-apk.sh --app myapp -
Confirm the asset landed with
papk-info— it prints the ASST table with each image’s dimensions, color format, and size:Terminal window ./scripts/papk-info.sh build/apks/myapp.papkAssets (8220 bytes @ 0x608) tag "ASST"┌──────────┬───────┬───────────────┬─────────┐│ Asset │ Dim │ Format │ Size │├──────────┼───────┼───────────────┼─────────┤│ logo.png │ 64x64 │ RGB565 (0x12) │ 8.0 KiB │└──────────┴───────┴───────────────┴─────────┘If your image isn’t in the table, the file name didn’t end in
.pngor wasn’t directly underassets/. -
Show it. Reference the file by name:
ImageView img = new ImageView();img.setImageSource("logo.png");
PAPK compatibility
Bundled assets land outside the framework class set, so they don’t change the shrink map, and --shrink / --shrink-app never rename an asset: the name passed to setImageSource is a string literal, which the shrinker leaves alone.
The container itself is at format v3: classes are linked when the app is packed, and pdb install and the firmware read only that version (PAPK file is not a valid PAPK: PAPK format version is not the one this build reads). Sections are 2-byte aligned, so an image’s pixels never land at an odd address; the firmware skips, rather than draws, an image whose pixels do and logs [assets] <name> skipped: pixel data is not 2-byte aligned.
The papk-info tool prints both the manifest and the asset table:
./scripts/papk-info.sh build/apks/imagedemo.papkInternals (for the curious)
- The ASST section is a
[u32 count]followed by one record per asset:[u16 name_len][name bytes][u16 width][u16 height][u8 cf][u8 reserved0][u16 stride (0 = derive from width + cf)][u32 data_len], each record padded to a 4-byte boundary before and after its pixel data. Every PAPK section also starts on a 4-byte boundary in the file, so pixel data sits at an aligned flash address — LVGL reads it in place as 16-bit words, and a Cortex-M0+ (RP2040) hard-faults on an unaligned halfword read. - The firmware-side resolver lives in
crates/picodroid-core/src/graphics/assets.rsand registers each entry with LVGL’s image cache aslv_img_dsc_tpointers into XIP flash. - There is no asset-section byte cap in the packer — the only enforced image limit is a per-axis maximum of 65535 px. The real ceiling is the app region the PAPK is installed into, shared with its classes and manifest and with every other installed app, so keep
assets/modest (a few hundred KiB at most) to leave room for code. - Another app’s icon reaches your app as a
BitmapDrawablefromPackageManager.getApplicationIcon, read out of that app’s installed image; show it withImageView.setImageDrawable. That is how the launcher draws its rows.
See examples/imagedemo/ for a worked example.