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.
* 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 {
val width = source.width
val height = source.height
// Create output bitmap
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
var result: Bitmap? = null
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
val scaleFactor = 4 // Blur a 1/4 size image for speed
val blurredWidth = width / scaleFactor
val blurredHeight = height / scaleFactor
try {
result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
// Create scaled bitmap for blur
val scaled = Bitmap.createScaledBitmap(source, blurredWidth, blurredHeight, true)
val scaleFactor = 4
val blurredWidth = width / scaleFactor
val blurredHeight = height / scaleFactor
// Apply stack blur (fast approximation)
val blurred = stackBlur(scaled, (params.blurAmount * 25).toInt().coerceIn(1, 25))
scaled.recycle()
scaled = Bitmap.createScaledBitmap(source, blurredWidth, blurredHeight, true)
// Scale blurred back up
val blurredFullSize = Bitmap.createScaledBitmap(blurred, width, height, true)
blurred.recycle()
blurred = stackBlur(scaled, (params.blurAmount * 25).toInt().coerceIn(1, 25))
scaled.recycle()
scaled = null
// Create gradient mask based on tilt-shift parameters
val mask = createGradientMask(width, height, params)
blurredFullSize = Bitmap.createScaledBitmap(blurred, width, height, true)
blurred.recycle()
blurred = null
// Composite: blend original with blurred based on mask
val pixels = IntArray(width * height)
val blurredPixels = IntArray(width * height)
val maskPixels = IntArray(width * height)
mask = createGradientMask(width, height, params)
source.getPixels(pixels, 0, width, 0, 0, width, height)
blurredFullSize.getPixels(blurredPixels, 0, width, 0, 0, width, height)
mask.getPixels(maskPixels, 0, width, 0, 0, width, height)
// Composite: blend original with blurred based on mask
val pixels = IntArray(width * height)
val blurredPixels = IntArray(width * height)
val maskPixels = IntArray(width * height)
blurredFullSize.recycle()
mask.recycle()
source.getPixels(pixels, 0, width, 0, 0, width, height)
blurredFullSize.getPixels(blurredPixels, 0, width, 0, 0, width, height)
mask.getPixels(maskPixels, 0, width, 0, 0, width, height)
for (i in pixels.indices) {
val maskAlpha = (maskPixels[i] and 0xFF) / 255f
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
blurredFullSize.recycle()
blurredFullSize = null
mask.recycle()
mask = null
val r = (origR * (1 - maskAlpha) + blurR * maskAlpha).toInt()
val g = (origG * (1 - maskAlpha) + blurG * maskAlpha).toInt()
val b = (origB * (1 - maskAlpha) + blurB * maskAlpha).toInt()
for (i in pixels.indices) {
val maskAlpha = (maskPixels[i] and 0xFF) / 255f
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
}
/**