Add bitmap safety in applyTiltShiftEffect()

Track all intermediate bitmaps with nullable variables and recycle
them in a finally block. This prevents native memory leaks when an
OOM or other exception occurs mid-processing. Variables are set to
null after recycle or handoff to the caller.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-02-27 15:20:16 +01:00
commit 593f2c5b1f

View file

@ -152,63 +152,81 @@ class ImageCaptureHandler(
/** /**
* Applies tilt-shift blur effect to a bitmap. * Applies tilt-shift blur effect to a bitmap.
* Supports both linear and radial modes. * Supports both linear and radial modes.
*
* All intermediate bitmaps are tracked and recycled in a finally block
* so that an OOM or other exception does not leak native memory.
*/ */
private fun applyTiltShiftEffect(source: Bitmap, params: BlurParameters): Bitmap { private fun applyTiltShiftEffect(source: Bitmap, params: BlurParameters): Bitmap {
val width = source.width val width = source.width
val height = source.height val height = source.height
// Create output bitmap var result: Bitmap? = null
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) var scaled: Bitmap? = null
var blurred: Bitmap? = null
var blurredFullSize: Bitmap? = null
var mask: Bitmap? = null
// For performance, we use a scaled-down version for blur and composite try {
val scaleFactor = 4 // Blur a 1/4 size image for speed result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val blurredWidth = width / scaleFactor
val blurredHeight = height / scaleFactor
// Create scaled bitmap for blur val scaleFactor = 4
val scaled = Bitmap.createScaledBitmap(source, blurredWidth, blurredHeight, true) val blurredWidth = width / scaleFactor
val blurredHeight = height / scaleFactor
// Apply stack blur (fast approximation) scaled = Bitmap.createScaledBitmap(source, blurredWidth, blurredHeight, true)
val blurred = stackBlur(scaled, (params.blurAmount * 25).toInt().coerceIn(1, 25))
scaled.recycle()
// Scale blurred back up blurred = stackBlur(scaled, (params.blurAmount * 25).toInt().coerceIn(1, 25))
val blurredFullSize = Bitmap.createScaledBitmap(blurred, width, height, true) scaled.recycle()
blurred.recycle() scaled = null
// Create gradient mask based on tilt-shift parameters blurredFullSize = Bitmap.createScaledBitmap(blurred, width, height, true)
val mask = createGradientMask(width, height, params) blurred.recycle()
blurred = null
// Composite: blend original with blurred based on mask mask = createGradientMask(width, height, params)
val pixels = IntArray(width * height)
val blurredPixels = IntArray(width * height)
val maskPixels = IntArray(width * height)
source.getPixels(pixels, 0, width, 0, 0, width, height) // Composite: blend original with blurred based on mask
blurredFullSize.getPixels(blurredPixels, 0, width, 0, 0, width, height) val pixels = IntArray(width * height)
mask.getPixels(maskPixels, 0, width, 0, 0, width, height) val blurredPixels = IntArray(width * height)
val maskPixels = IntArray(width * height)
blurredFullSize.recycle() source.getPixels(pixels, 0, width, 0, 0, width, height)
mask.recycle() blurredFullSize.getPixels(blurredPixels, 0, width, 0, 0, width, height)
mask.getPixels(maskPixels, 0, width, 0, 0, width, height)
for (i in pixels.indices) { blurredFullSize.recycle()
val maskAlpha = (maskPixels[i] and 0xFF) / 255f blurredFullSize = null
val origR = (pixels[i] shr 16) and 0xFF mask.recycle()
val origG = (pixels[i] shr 8) and 0xFF mask = null
val origB = pixels[i] and 0xFF
val blurR = (blurredPixels[i] shr 16) and 0xFF
val blurG = (blurredPixels[i] shr 8) and 0xFF
val blurB = blurredPixels[i] and 0xFF
val r = (origR * (1 - maskAlpha) + blurR * maskAlpha).toInt() for (i in pixels.indices) {
val g = (origG * (1 - maskAlpha) + blurG * maskAlpha).toInt() val maskAlpha = (maskPixels[i] and 0xFF) / 255f
val b = (origB * (1 - maskAlpha) + blurB * maskAlpha).toInt() val origR = (pixels[i] shr 16) and 0xFF
val origG = (pixels[i] shr 8) and 0xFF
val origB = pixels[i] and 0xFF
val blurR = (blurredPixels[i] shr 16) and 0xFF
val blurG = (blurredPixels[i] shr 8) and 0xFF
val blurB = blurredPixels[i] and 0xFF
pixels[i] = (0xFF shl 24) or (r shl 16) or (g shl 8) or b val r = (origR * (1 - maskAlpha) + blurR * maskAlpha).toInt()
val g = (origG * (1 - maskAlpha) + blurG * maskAlpha).toInt()
val b = (origB * (1 - maskAlpha) + blurB * maskAlpha).toInt()
pixels[i] = (0xFF shl 24) or (r shl 16) or (g shl 8) or b
}
result.setPixels(pixels, 0, width, 0, 0, width, height)
val output = result
result = null // prevent finally from recycling the returned bitmap
return output
} finally {
result?.recycle()
scaled?.recycle()
blurred?.recycle()
blurredFullSize?.recycle()
mask?.recycle()
} }
result.setPixels(pixels, 0, width, 0, 0, width, height)
return result
} }
/** /**