Title
Android: no way to bundle a pixel-exact (byte-exact) image resource — res/ always gets re-encoded to lossy, density-scaled WebP
Summary
On Android, every module-resource image reference (<image src="module:stem">, and TS-side lookups via getModuleFileEntryAsBytes/asset-load-observer) resolves through a single hard-coded path: ResourceResolver.resolveResource() → ValdiImage.getImageIdentifier() → an Android resource ID → drawable decode. There is currently no way to get the original, untouched bytes of a bundled image back on Android — the compiler unconditionally re-encodes everything under a module's res/ into per-density WebP drawables, regardless of the source file's extension or content.
This breaks any use case that needs pixel-exact bitmap data bundled with the app (we hit it with small black/white stitch-pattern images used as literal per-pixel data, not decorative UI images — lossy WebP compression plus density upscaling turned hard black/white pixel edges into a gradient of gray artifacts).
Repro
- Add a small (e.g. 60×10) pure black/white PNG to a module's
res/ folder, referenced via the module's own image registry (e.g. preview:my_pattern).
- Build the module's Android resource output in isolation:
bazel build //path/to:module.debug.resource_files.
- Inspect the emitted
res/drawable-*/**.
Observed: the file is emitted as drawable-{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}/<module>_<stem>.webp — five density-bucketed, lossily-recompressed copies, none of which necessarily match the source's actual pixel dimensions (in our repro, the xxxhdpi bucket came out resized to 80×14 from a 60×10 source).
Decoding the shipped xxxhdpi WebP confirms real compression + resize artifacts — a source that only contains pure (0,0,0,255) and (255,255,255,255) pixels decodes back with dozens of distinct intermediate gray values ((1,1,1), (35,35,35), (102,102,102), (222,222,222), (254,254,254), etc.).
What we tried (all editable-repo-side; the compiler toolbox has no local source to patch)
.raw extension passthrough (rename the source to res/<stem>.raw, hoping the compiler would treat an unrecognized extension as an opaque blob and expose it via getModuleFileEntryAsBytes): the compiler still content-sniffs the file as a PNG and produces the exact same WebP drawable set as the .png sibling. getModuleFileEntryAsBytes(module, "<stem>.raw") never resolves to anything — nothing with a .raw extension appears anywhere in the compiled Android resource output.
res/drawable-nodpi/<stem>.png (the standard Android/AAPT2 convention for "never density-scale this"): silently dropped — produced no output at all, in any bucket.
- Explicit per-density variants, per the
core-images.md docs ("If we also had an Android specific variant like drawable-mdpi/my_banner.webp … the compiler will generate any missing variants"):
- Adding one
res/drawable-mdpi/<stem>.png alongside the existing flat res/<stem>.png did not make the compiler prefer ours for mdpi and synthesize the rest — it hard-failed: Got an error during phase 'Verifying final files before saving': Multiple final files writing to the same output URL, listing both the flat file and the explicit drawable-mdpi file as conflicting sources for the same drawable-mdpi/<module>_<stem>.webp output.
- Supplying all five density variants explicitly (removing the flat file to eliminate the obvious ambiguity) made it worse: all five became conflicting sources for the same
xxxhdpi output target simultaneously, same error.
So the documented "supply your own density variant and the compiler fills in the rest" behavior doesn't appear to work as described — it can't disambiguate multiple same-stem source candidates and fails outright rather than preferring an exact-match density folder over synthesizing.
What we'd like
Either of these would unblock us:
- A genuine raw/passthrough resource type for Android module resources — e.g. honoring
res/raw/<name> (Android's own long-standing convention for byte-exact resources via Resources.openRawResource) for files placed there by a module's own res glob, retrievable byte-exact via getModuleFileEntryAsBytes/the asset-load-observer path, with zero WebP re-encoding or density bucketing.
- Failing that, fix explicit per-density-variant resolution so a source placed in
res/drawable-{density}/<stem>.<ext> is used verbatim for that density bucket instead of conflicting with the compiler's own auto-generated variant for the same slot — this would at least let us supply all five buckets ourselves with identical bytes and get a non-lossy (or at least non-resized) result.
Current workaround
We're base64-embedding the affected images directly as JS bundle constants and decoding via drawing/src/BitmapFactory's decodeBitmap(base64string), bypassing the Android resource pipeline entirely. Works, but obviously isn't a generalizable answer for anyone else who wants module-bundled bitmap data.
Environment
- Valdi Bazel toolchain, Android target
- Compiler invoked via
valdi_module/valdi_compiled (bzl/valdi/valdi_compiled.bzl); compiler binary resolved via @valdi//bzl/valdi:valdi_toolchain — no source available locally to patch/debug further.
Title
Android: no way to bundle a pixel-exact (byte-exact) image resource —
res/always gets re-encoded to lossy, density-scaled WebPSummary
On Android, every module-resource image reference (
<image src="module:stem">, and TS-side lookups viagetModuleFileEntryAsBytes/asset-load-observer) resolves through a single hard-coded path:ResourceResolver.resolveResource()→ValdiImage.getImageIdentifier()→ an Android resource ID → drawable decode. There is currently no way to get the original, untouched bytes of a bundled image back on Android — the compiler unconditionally re-encodes everything under a module'sres/into per-density WebP drawables, regardless of the source file's extension or content.This breaks any use case that needs pixel-exact bitmap data bundled with the app (we hit it with small black/white stitch-pattern images used as literal per-pixel data, not decorative UI images — lossy WebP compression plus density upscaling turned hard black/white pixel edges into a gradient of gray artifacts).
Repro
res/folder, referenced via the module's own image registry (e.g.preview:my_pattern).bazel build //path/to:module.debug.resource_files.res/drawable-*/**.Observed: the file is emitted as
drawable-{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}/<module>_<stem>.webp— five density-bucketed, lossily-recompressed copies, none of which necessarily match the source's actual pixel dimensions (in our repro, thexxxhdpibucket came out resized to 80×14 from a 60×10 source).Decoding the shipped
xxxhdpiWebP confirms real compression + resize artifacts — a source that only contains pure(0,0,0,255)and(255,255,255,255)pixels decodes back with dozens of distinct intermediate gray values ((1,1,1),(35,35,35),(102,102,102),(222,222,222),(254,254,254), etc.).What we tried (all editable-repo-side; the compiler toolbox has no local source to patch)
.rawextension passthrough (rename the source tores/<stem>.raw, hoping the compiler would treat an unrecognized extension as an opaque blob and expose it viagetModuleFileEntryAsBytes): the compiler still content-sniffs the file as a PNG and produces the exact same WebP drawable set as the.pngsibling.getModuleFileEntryAsBytes(module, "<stem>.raw")never resolves to anything — nothing with a.rawextension appears anywhere in the compiled Android resource output.res/drawable-nodpi/<stem>.png(the standard Android/AAPT2 convention for "never density-scale this"): silently dropped — produced no output at all, in any bucket.core-images.mddocs ("If we also had an Android specific variant likedrawable-mdpi/my_banner.webp… the compiler will generate any missing variants"):res/drawable-mdpi/<stem>.pngalongside the existing flatres/<stem>.pngdid not make the compiler prefer ours formdpiand synthesize the rest — it hard-failed:Got an error during phase 'Verifying final files before saving': Multiple final files writing to the same output URL, listing both the flat file and the explicitdrawable-mdpifile as conflicting sources for the samedrawable-mdpi/<module>_<stem>.webpoutput.xxxhdpioutput target simultaneously, same error.So the documented "supply your own density variant and the compiler fills in the rest" behavior doesn't appear to work as described — it can't disambiguate multiple same-stem source candidates and fails outright rather than preferring an exact-match density folder over synthesizing.
What we'd like
Either of these would unblock us:
res/raw/<name>(Android's own long-standing convention for byte-exact resources viaResources.openRawResource) for files placed there by a module's ownresglob, retrievable byte-exact viagetModuleFileEntryAsBytes/the asset-load-observer path, with zero WebP re-encoding or density bucketing.res/drawable-{density}/<stem>.<ext>is used verbatim for that density bucket instead of conflicting with the compiler's own auto-generated variant for the same slot — this would at least let us supply all five buckets ourselves with identical bytes and get a non-lossy (or at least non-resized) result.Current workaround
We're base64-embedding the affected images directly as JS bundle constants and decoding via
drawing/src/BitmapFactory'sdecodeBitmap(base64string), bypassing the Android resource pipeline entirely. Works, but obviously isn't a generalizable answer for anyone else who wants module-bundled bitmap data.Environment
valdi_module/valdi_compiled(bzl/valdi/valdi_compiled.bzl); compiler binary resolved via@valdi//bzl/valdi:valdi_toolchain— no source available locally to patch/debug further.