diff --git a/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt b/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt index b7a8d6b..af15e10 100644 --- a/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt +++ b/app/src/main/java/no/naiv/tiltshift/camera/ImageCaptureHandler.kt @@ -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 } /**