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.
Comments
No comments yet. Start the discussion.