Skip to content

Android: no way to bundle a pixel-exact (byte-exact) image resource — res/ always gets re-encoded to lossy, density-scaled WebP #116

Description

@clholgat

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

  1. 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).
  2. Build the module's Android resource output in isolation: bazel build //path/to:module.debug.resource_files.
  3. 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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions