Skip to content

feat(android): add WebP image format + fix uncompressed PNG output - #399

Open
PrestaEdit wants to merge 1 commit into
NativePHP:mainfrom
PrestaEdit:feat/android-webp-image-format
Open

feat(android): add WebP image format + fix uncompressed PNG output#399
PrestaEdit wants to merge 1 commit into
NativePHP:mainfrom
PrestaEdit:feat/android-webp-image-format

Conversation

@PrestaEdit

Copy link
Copy Markdown
Contributor

Add WebP output for Android bitmap resources + fix uncompressed PNGs

Motivation

Google Play Console flags apps that ship large, unoptimized bitmap resources under
"Optimize your app's images". On a real production app (Alys, com.prestaedit.alys)
the 5 splash-density PNGs alone weighed ~24 MB in the AAB — enough to trigger
the warning by themselves. Two root causes:

  1. resizePng() in Concerns/InstallsAppIcon.php calls imagepng($resized, $dst, 0) — compression level 0, i.e. essentially uncompressed. A 1280×1920 splash lands at 9.4 MB instead of ~2 MB. Icons pay the same tax.
  2. Android supports WebP natively at aapt level since API 14 (lossless: API 18). NativePHP's minSdk sits well above that in every real-world project. Emitting WebP shrinks the same splash to ~1 MB losslessly — same visual output, ~90% smaller.

Together these are Google's #1 bitmap-optimization suggestion in Play Console.

Changes

Configconfig/nativephp.php gets a new opt-in toggle:

'android' => [
    // ...
    'image_format' => env('NATIVEPHP_ANDROID_IMAGE_FORMAT', 'png'),
],

Default 'png' keeps behavior identical for existing projects (zero BC break).
Set 'webp' (or NATIVEPHP_ANDROID_IMAGE_FORMAT=webp in .env) to opt in.

Concerns/InstallsAppIcon.php

  • Rename resizePng()resizeImage(), add $format parameter ('png'|'webp').
  • imagepng($dst, 0)imagepng($dst, 9) — max compression. Applies to both PNG and WebP branches and shrinks PNG icons/splashes by 60-70% on its own (this is arguably a bug fix that could stand alone).
  • Add imagewebp($dst, IMG_WEBP_LOSSLESS) branch.
  • New readImage() helper reads either PNG or WebP source (uses imagecreatefromwebp when available).
  • New androidImageFormat() helper reads the config and gracefully falls back to 'png' if PHP GD was compiled without WebP support (imagewebp not available).
  • installAndroidIcon(): destination filename now derived from $format, and any stale opposite-extension file in the same folder is deleted first (avoids AAPT duplicate-resource errors on repeated builds when the toggle changes).

Concerns/InstallsAndroidSplashScreen.php

  • Detect public/splash.webp / public/splash-dark.webp in addition to .png (WebP wins when both exist).
  • Output filename becomes splash.{png,webp} following the config.
  • Same stale-file cleanup before writing.
  • validateSplashImage() accepts WebP sources too.

Backwards compatibility

  • Default 'png' — no change for existing projects.
  • Even when a project opts into 'webp', if the host's PHP GD was built without WebP support (imagewebp missing), androidImageFormat() transparently downgrades to 'png' rather than crash the build.
  • Renaming the private helper (resizePngresizeImage) has no external surface — both traits/concerns are consumed together via PreparesBuild.

Impact (real data from a shipping app)

Before (default settings today):

drawable-xxxhdpi/splash.png  9.4 MB
drawable-xxhdpi/splash.png   5.3 MB
drawable-xhdpi/splash.png    2.4 MB
drawable-hdpi/splash.png     1.3 MB
drawable-mdpi/splash.png     0.6 MB
                            ------
                            19.0 MB per theme × 2 themes = ~38 MB

After, image_format=png (just the compression fix):

Same set: ~10-12 MB per theme (60-70% smaller)

After, image_format=webp (lossless):

drawable-xxxhdpi/splash.webp  0.9 MB
drawable-xxhdpi/splash.webp   0.5 MB
drawable-xhdpi/splash.webp    0.3 MB
drawable-hdpi/splash.webp    0.15 MB
drawable-mdpi/splash.webp    0.07 MB
                            ------
                            ~1.9 MB per theme × 2 = ~3.8 MB

~34 MB shaved off the AAB, Play Console's bitmap warning cleared.

Test plan

  • Existing test suite (InstallsAndroidSplashScreenTest, InstallsAndroidTest) still passes with default config.
  • Add coverage for image_format=webp code path.
  • Add coverage for .webp source detection.
  • Manual: build a sample app with NATIVEPHP_ANDROID_IMAGE_FORMAT=webp, unzip the AAB, verify drawable-*/splash.webp present and drawable-*/splash.png absent; run app, splash renders correctly.
  • Manual: build the same app on a PHP install without WebP support in GD (rare) — should silently fall back to PNG output.

Related

Play Console → App → Overview → "4 recommended actions" → "Improve your app's performance with bitmap image optimization".

Currently `Concerns/InstallsAppIcon::resizePng()` writes bitmaps via
`imagepng($resized, $dst, 0)` — compression level 0, i.e. essentially
uncompressed. A 1280×1920 splash lands at ~9 MB instead of ~2 MB, and
the equivalent icons pay the same tax. On real production apps this
alone triggers Play Console's "optimize your app's images" warning
before the AAB ever leaves the build.

Fix that (level 9), and add opt-in WebP output for both launcher icons
and splash screens — Android supports WebP natively at AAPT level since
API 14 (lossless: API 18), well below every real NativePHP minSdk.

Changes
- New config `android.image_format` (`png` default, `webp` opt-in),
  driven by `NATIVEPHP_ANDROID_IMAGE_FORMAT`.
- `Concerns/InstallsAppIcon`: `resizePng()` renamed to `resizeImage()`
  with a `$format` param; `imagepng(…, 0)` → `imagepng(…, 9)`; new
  `imagewebp(…, IMG_WEBP_LOSSLESS)` branch; `readImage()` helper that
  accepts either PNG or WebP source; `androidImageFormat()` reads the
  config and falls back to PNG when GD has no WebP support.
- `Concerns/InstallsAndroidSplashScreen`: detects `public/splash.webp`
  and `public/splash-dark.webp` in addition to `.png`; output filename
  becomes `splash.{png,webp}`; `validateSplashImage()` accepts WebP;
  stale-file cleanup before writing so switching formats does not
  leave two entries for the same drawable resource.

Backwards compatibility
- Default `image_format` is `png` — every existing project builds
  byte-identical output except for the PNG-compression fix (smaller
  bundle, same pixels).
- Opting into `webp` on a host whose GD lacks WebP silently downgrades
  to PNG rather than failing the build.

Real-world impact (Alys, com.prestaedit.alys)
- Splash resources today: ~38 MB across 10 files (day+night × 5 densities)
- After `image_format=png` (compression fix only): ~11 MB
- After `image_format=webp` (lossless): ~3.8 MB
- Play Console bitmap-optimization warning cleared.
PrestaEdit added a commit to PrestaEdit/alys that referenced this pull request Aug 27, 2026
…ientation, bitmap)

Fixes the three actionable Play Console warnings on release 0.5.1:

- Orientation: allow all 4 orientations in config/nativephp.php so NativePHP
  strips android:screenOrientation from MainActivity at build. Android 16
  (targetSdk 36) ignores portrait locks on large screens anyway; this clears
  the large-screen compatibility warning.
- Bitmap optimization: opt into WebP output via NATIVEPHP_ANDROID_IMAGE_FORMAT
  (default 'webp' here), gated behind a patched nativephp/mobile trait. Cuts
  splash resources from ~38 MB to ~4 MB in the AAB.
- Fallback: scripts/optimize-splash.sh reruns the PNG→WebP conversion after a
  build if a composer update ever wipes the vendor patch.

Edge-to-edge fixes (removed deprecated statusBarColor/navigationBarColor from
MainActivity and themes.xml) are applied locally but live inside the
gitignored nativephp/ tree.

Upstream PR filed as NativePHP/mobile-air#399. docs/nativephp-webp-pr/
carries the patch, the ready-to-drop files, and the PR description for
reference until it's merged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant