Skip to content

@capacitor/camera - Android: BitmapFactory.decodeFile without inSampleSize decodes the full-resolution bitmap in IonCameraFlow #2599

Description

@MarcelSchuermann

Bug Report

Plugin(s)

@capacitor/camera 8.2.4 (also 8.2.2), @capacitor/core 8.5.2, Android targetSdk 36

Capacitor Version

$ npx cap doctor
💊   Capacitor Doctor  💊 

Latest Dependencies:

  @capacitor/cli: 8.5.2
  @capacitor/core: 8.5.2
  @capacitor/android: 8.5.2
  @capacitor/ios: 8.5.2

Installed Dependencies:

  @capacitor/cli: 8.5.2
  @capacitor/android: 8.5.2
  @capacitor/core: 8.5.2
  @capacitor/ios: 8.5.2

[success] Android looking great! 👌

Platform(s)

Android

Current Behavior

IonCameraFlow.kt handleMediaResult calls BitmapFactory.decodeFile(mediaResult.uri) (line ~726, and again ~806) with no BitmapFactory.Options. On a 12 MP photo that allocates roughly 48 MB as ARGB_8888. LegacyCameraFlow.java does the same with decodeStream.

Expected Behavior

The decode should not allocate pixel memory it never uses.

In both places the decoded Bitmap is used only as a null check — "is this file an
image?". The very next line, ImageUtils.getExifData(context, bitmap, uri), reads the EXIF
from the uri and ignores the bitmap argument entirely. So a bounds-only decode
(inJustDecodeBounds = true) answers the same question for a few bytes instead of ~48 MB.

Where a bitmap really is needed later, BitmapFactory.Options.inSampleSize should be
computed from the requested width/height before the full decode — which the library
already knows how to do: IONCAMRImageEditorView.decodeSampledBitmapFromUri does exactly
that. These call sites bypass it.

Code Reproduction

No special app is needed — any Capacitor app with the plugin reproduces it:

  1. npm i @capacitor/camera@8.2.4 && npx cap sync android
  2. Call Camera.pickImages({ quality: 90, width: 2400, height: 2400 }) and choose three
    12 MP photos (4000×3000).
  3. Watch the heap in Android Studio's Memory Profiler, or
    adb shell dumpsys meminfo <package> | grep -i "Native Heap" before and during the pick.

Each picked photo shows a transient allocation of roughly width × height × 4 bytes —
about 48 MB per 12 MP image — before any resizing happens. Passing a smaller width/
height does not prevent it: the full decode happens first.

A breakpoint on IonCameraFlow.handleMediaResult shows the bitmap local going unused
after the null check.

Other Technical Details

  • @capacitor/camera 8.2.4 (and 8.2.2 — I unpacked both npm tarballs and compared; the
    decode is identical)
  • io.ionic.libs:ioncamera-android 1.0.2, the newest on Maven Central, which the plugin
    pins
  • Android targetSdk 36, AGP 8.13.0, Gradle 8.14.3
  • Affected lines: IonCameraFlow.kt ~726 (handleMediaResult) and ~806 (the multi-pick
    path); LegacyCameraFlow.java uses BitmapFactory.decodeStream without inSampleSize
    in processPickedImage / processPickedImages

Additional Context

Google Play reports this. The Play Console flags it as a bitmap-downsampling
recommendation with the obfuscated symbols w4.k.c, w4.k.l, w4.m.d, and names these
entry points in the deprecated-API section:

  • com.capacitorjs.plugins.camera.LegacyCameraFlow.processPickedImage / processPickedImages
  • io.ionic.libs.ioncameralib.view.IONCAMRImageEditorView.decodeSampledBitmapFromUri / loadBitmapSafely
  • io.ionic.libs.ioncameralib.helper.IONCAMRImageHelper.getBitmapForInputStream / decodeFile
  • io.ionic.libs.ioncameralib.helper.IONCAMRMediaHelper.getImageResolution

This has a deadline. Google's August 2026 app-quality post makes bitmap memory a
bad-behaviour threshold enforced from February 2027, alongside anonymous RSS + swap.
Apps over the threshold can lose visibility.

Why it bites us. Our app scans multi-page recipes, so a session decodes one full-size
bitmap per page. On a low-RAM device the WebView renderer is killed mid-scan.

Workaround we ship, via patch-package on IonCameraFlow.kt — happy to send this as a PR:

// was: val bitmap = BitmapFactory.decodeFile(mediaResult.uri)
//      if (bitmap == null) { sendError(IONCAMRError.PROCESS_IMAGE_ERROR); return }
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(mediaResult.uri, bounds)
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) {
    sendError(IONCAMRError.PROCESS_IMAGE_ERROR)
    return
}
val exif = ImageUtils.getExifData(context, null, uri)

Verified on an Android emulator: camera capture, gallery pick and a three-page recipe scan
of 4000×3000 JPEGs all behave identically, with no error and no crash.

Note ioncamera-android is shipped as an AAR, so the IONCAMR* call sites above cannot be
patched this way — only an upstream fix reaches them.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions