Your Android image compressor might crash on Android 15 — so I built a pure-JVM one
DEV Community Grade 10 4d ago

Your Android image compressor might crash on Android 15 — so I built a pure-JVM one

Android 15 quietly changed the rules for native code. Since November 2025, apps targeting API 35 on Google Play must support 16 KB memory pages , and every bundled .so has to be 16 KB-aligned — including the ones inside your third-party libraries. An unaligned native lib doesn't warn; it crashes on 16 KB-page devices. That sent me looking at how my apps compress images. The most popular option, Luban 2 , is excellent — but it ships libjpeg-turbo , a native encoder. The original Luban and its fork AdvancedLuban were pure-JVM… and abandoned. So I revived that lineage as PixelDiet : a pure-JVM, zero-native Android image compressor. What "pure-JVM" buys you No .so → no 16 KB page-size risk. Nothing to align, no NDK, no per-ABI builds. Tiny footprint. The release AAR is ~44 KB. Honest guarantee. A CI step unzips the AAR and fails the build if it ever contains a .so . The claim is enforced, not promised. The trade-off, stated plainly: you give up libjpeg-turbo's raw encode speed. In exchange you get a dependency that can't break on a platform page-size change and won't bloat your APK. What I kept, and what I modernized The valuable part of Luban is its WeChat-Moments-style "gear" sizing strategy — I ported that math faithfully (and unit-tested it). Around it, everything is new: Kotlin coroutines core: suspend get() , getFirst() , and asFlow() for progress. Java-friendly OnCompressListener path backed by Dispatchers.IO — no RxJava . Scoped-storage safe inputs: File , content:// Uri , InputStream , Bitmap . androidx ExifInterface orientation handling read from streams. WebP lossy/lossless output (smaller than JPEG), plus JPEG/PNG. val out = PixelDiet . with ( context ) . load ( uri ) . format ( OutputFormat . WEBP_LOSSY ) . getFirst () The feature Luban 2 removed Luban 2 dropped setMaxSize . If your code relied on "compress to under N KB," upgrading silently breaks it. PixelDiet brings it back as hardCap(kb) — and makes it a real guarantee with a two-phase quality-loop + resize-fallback , so the output is actually ≤ your target on any gear: PixelDiet . with ( context ). load ( uri ). hardCap ( 200 ). getFirst () // ≤ 200 KB, guaranteed Does it actually compress? From the sample app: an 863 KB photo → 78 KB (−91%) in ~223 ms (Custom gear → PNG). With Smart gear + WebP + a 300 KB cap: 863 KB → 250.8 KB (−71%). The part nobody writes about: licensing PixelDiet is a fork , and I'm explicit about that. Luban and AdvancedLuban are Apache-2.0, which permits forking, renaming, and redistribution — but it has conditions: keep the original copyright headers, ship LICENSE + a NOTICE stating your changes, and don't imply endorsement. The compression strategy is Curzibn's and shaohui's work; my contribution is the modern API, the scoped-storage/EXIF fixes, the native-free guarantee, and ongoing maintenance. Presenting that honestly is both the legal requirement and the more credible story. Try it JitPack: implementation ( "com.github.basheerpaliyathu.PixelDiet:pixeldiet:0.1.0" ) Code, sample app, and the full comparison table: https://github.com/basheerpaliyathu/PixelDiet If you maintain an Android app targeting API 35, it's worth auditing your dependencies for bundled .so files this week. You might be one transitive dependency away from a 16 KB crash.

Android 15 quietly changed the rules for native code. Since November 2025, apps targeting API 35 on Google Play must support 16 KB memory pages, and every bundled .so has to be 16 KB-aligned — including the ones inside your third-party libraries. An unaligned native lib doesn't warn; it crashes on 16 KB-page devices. That sent me looking at how my apps compress images. The most popular option, Luban 2, is excellent — but it ships libjpeg-turbo, a native encoder. The original Luban and its fork AdvancedLuban were pure-JVM… and abandoned. So I revived that lineage as PixelDiet: a pure-JVM, zero-native Android image compressor. What "pure-JVM" buys you - No .so → no 16 KB page-size risk. Nothing to align, no NDK, no per-ABI builds. - Tiny footprint. The release AAR is ~44 KB. - Honest guarantee. A CI step unzips the AAR and fails the build if it ever contains a .so . The claim is enforced, not promised. The trade-off, stated plainly: you give up libjpeg-turbo's raw encode speed. In exchange you get a dependency that can't break on a platform page-size change and won't bloat your APK. What I kept, and what I modernized The valuable part of Luban is its WeChat-Moments-style "gear" sizing strategy — I ported that math faithfully (and unit-tested it). Around it, everything is new: - Kotlin coroutines core: suspend get() ,getFirst() , andasFlow() for progress. - Java-friendly OnCompressListener path backed byDispatchers.IO — no RxJava. - Scoped-storage safe inputs: File ,content:// Uri ,InputStream ,Bitmap . - androidx ExifInterface orientation handling read from streams. - WebP lossy/lossless output (smaller than JPEG), plus JPEG/PNG. val out = PixelDiet.with(context) .load(uri) .format(OutputFormat.WEBP_LOSSY) .getFirst() The feature Luban 2 removed Luban 2 dropped setMaxSize . If your code relied on "compress to under N KB," upgrading silently breaks it. PixelDiet brings it back as hardCap(kb) — and makes it a real guarantee with a two-phase quality-loop + resize-fallback, so the output is actually ≤ your target on any gear: PixelDiet.with(context).load(uri).hardCap(200).getFirst() // ≤ 200 KB, guaranteed Does it actually compress? From the sample app: an 863 KB photo → 78 KB (−91%) in ~223 ms (Custom gear → PNG). With Smart gear + WebP + a 300 KB cap: 863 KB → 250.8 KB (−71%). The part nobody writes about: licensing PixelDiet is a fork, and I'm explicit about that. Luban and AdvancedLuban are Apache-2.0, which permits forking, renaming, and redistribution — but it has conditions: keep the original copyright headers, ship LICENSE + a NOTICE stating your changes, and don't imply endorsement. The compression strategy is Curzibn's and shaohui's work; my contribution is the modern API, the scoped-storage/EXIF fixes, the native-free guarantee, and ongoing maintenance. Presenting that honestly is both the legal requirement and the more credible story. Try it JitPack: implementation("com.github.basheerpaliyathu.PixelDiet:pixeldiet:0.1.0") Code, sample app, and the full comparison table: https://github.com/basheerpaliyathu/PixelDiet If you maintain an Android app targeting API 35, it's worth auditing your dependencies for bundled .so files this week. You might be one transitive dependency away from a 16 KB crash. Top comments (0)

Comments

No comments yet. Start the discussion.