package no.naiv.tiltshift.camera import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Matrix import android.graphics.Paint import android.graphics.RenderEffect import android.graphics.Shader import android.location.Location import android.os.Build import android.view.Surface import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCaptureException import androidx.camera.core.ImageProxy import androidx.exifinterface.media.ExifInterface import kotlinx.coroutines.suspendCancellableCoroutine import no.naiv.tiltshift.effect.BlurParameters import no.naiv.tiltshift.storage.PhotoSaver import no.naiv.tiltshift.storage.SaveResult import no.naiv.tiltshift.util.OrientationDetector import java.io.File import java.util.concurrent.Executor import kotlin.coroutines.resume import kotlin.math.cos import kotlin.math.sin /** * Handles capturing photos with the tilt-shift effect applied. */ class ImageCaptureHandler( private val context: Context, private val photoSaver: PhotoSaver ) { /** * Captures a photo and applies the tilt-shift effect. */ suspend fun capturePhoto( imageCapture: ImageCapture, executor: Executor, blurParams: BlurParameters, deviceRotation: Int, location: Location?, isFrontCamera: Boolean ): SaveResult = suspendCancellableCoroutine { continuation -> imageCapture.takePicture( executor, object : ImageCapture.OnImageCapturedCallback() { override fun onCaptureSuccess(imageProxy: ImageProxy) { try { // Get rotation from ImageProxy (sensor orientation) val imageRotation = imageProxy.imageInfo.rotationDegrees // Convert ImageProxy to Bitmap var bitmap = imageProxyToBitmap(imageProxy) imageProxy.close() if (bitmap == null) { continuation.resume(SaveResult.Error("Failed to convert image")) return } // Rotate bitmap to correct orientation // Camera sensor is landscape, we need to rotate for portrait bitmap = rotateBitmap(bitmap, imageRotation, isFrontCamera) // Apply tilt-shift effect to the correctly oriented image val processedBitmap = applyTiltShiftEffect(bitmap, blurParams) bitmap.recycle() // Save with EXIF orientation as NORMAL (bitmap is already rotated) kotlinx.coroutines.runBlocking { val result = photoSaver.saveBitmap( processedBitmap, ExifInterface.ORIENTATION_NORMAL, location ) processedBitmap.recycle() continuation.resume(result) } } catch (e: Exception) { continuation.resume(SaveResult.Error("Capture failed: ${e.message}", e)) } } override fun onError(exception: ImageCaptureException) { continuation.resume( SaveResult.Error("Capture failed: ${exception.message}", exception) ) } } ) } /** * Rotates a bitmap to the correct orientation. */ private fun rotateBitmap(bitmap: Bitmap, rotationDegrees: Int, isFrontCamera: Boolean): Bitmap { if (rotationDegrees == 0 && !isFrontCamera) { return bitmap } val matrix = Matrix() // Apply rotation if (rotationDegrees != 0) { matrix.postRotate(rotationDegrees.toFloat()) } // Mirror for front camera if (isFrontCamera) { matrix.postScale(-1f, 1f) } val rotated = Bitmap.createBitmap( bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true ) if (rotated != bitmap) { bitmap.recycle() } return rotated } private fun imageProxyToBitmap(imageProxy: ImageProxy): Bitmap? { val buffer = imageProxy.planes[0].buffer val bytes = ByteArray(buffer.remaining()) buffer.get(bytes) return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) } /** * Applies tilt-shift blur effect to a bitmap. * This is a software fallback - on newer devices we could use RenderScript/RenderEffect. */ 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) val canvas = Canvas(result) // 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 // Create scaled bitmap for blur val scaled = Bitmap.createScaledBitmap(source, blurredWidth, blurredHeight, true) // Apply stack blur (fast approximation) val blurred = stackBlur(scaled, (params.blurAmount * 25).toInt().coerceIn(1, 25)) scaled.recycle() // Scale blurred back up val blurredFullSize = Bitmap.createScaledBitmap(blurred, width, height, true) blurred.recycle() // Create gradient mask based on tilt-shift parameters val mask = createGradientMask(width, height, params) // Composite: blend original with blurred based on mask val paint = Paint(Paint.ANTI_ALIAS_FLAG) val pixels = IntArray(width * height) val blurredPixels = IntArray(width * height) val maskPixels = IntArray(width * height) 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) blurredFullSize.recycle() mask.recycle() 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 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) return result } /** * Creates a gradient mask for the tilt-shift effect. */ private fun createGradientMask(width: Int, height: Int, params: BlurParameters): Bitmap { val mask = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val pixels = IntArray(width * height) val centerY = height * params.position val focusHalfHeight = height * params.size * 0.5f val transitionHeight = focusHalfHeight * 0.5f val cosAngle = cos(params.angle) val sinAngle = sin(params.angle) for (y in 0 until height) { for (x in 0 until width) { // Rotate point around center val dx = x - width / 2f val dy = y - centerY val rotatedY = -dx * sinAngle + dy * cosAngle // Calculate blur amount based on distance from focus line val dist = kotlin.math.abs(rotatedY) val blurAmount = when { dist < focusHalfHeight -> 0f dist < focusHalfHeight + transitionHeight -> { (dist - focusHalfHeight) / transitionHeight } else -> 1f } val gray = (blurAmount * 255).toInt().coerceIn(0, 255) pixels[y * width + x] = (0xFF shl 24) or (gray shl 16) or (gray shl 8) or gray } } mask.setPixels(pixels, 0, width, 0, 0, width, height) return mask } /** * Fast stack blur algorithm. */ private fun stackBlur(bitmap: Bitmap, radius: Int): Bitmap { if (radius < 1) return bitmap.copy(Bitmap.Config.ARGB_8888, true) val w = bitmap.width val h = bitmap.height val pix = IntArray(w * h) bitmap.getPixels(pix, 0, w, 0, 0, w, h) val wm = w - 1 val hm = h - 1 val wh = w * h val div = radius + radius + 1 val r = IntArray(wh) val g = IntArray(wh) val b = IntArray(wh) var rsum: Int var gsum: Int var bsum: Int var x: Int var y: Int var i: Int var p: Int var yp: Int var yi: Int var yw: Int val vmin = IntArray(maxOf(w, h)) var divsum = (div + 1) shr 1 divsum *= divsum val dv = IntArray(256 * divsum) for (i2 in 0 until 256 * divsum) { dv[i2] = (i2 / divsum) } yi = 0 yw = 0 val stack = Array(div) { IntArray(3) } var stackpointer: Int var stackstart: Int var sir: IntArray var rbs: Int val r1 = radius + 1 var routsum: Int var goutsum: Int var boutsum: Int var rinsum: Int var ginsum: Int var binsum: Int for (y2 in 0 until h) { rinsum = 0 ginsum = 0 binsum = 0 routsum = 0 goutsum = 0 boutsum = 0 rsum = 0 gsum = 0 bsum = 0 for (i2 in -radius..radius) { p = pix[yi + minOf(wm, maxOf(i2, 0))] sir = stack[i2 + radius] sir[0] = (p and 0xff0000) shr 16 sir[1] = (p and 0x00ff00) shr 8 sir[2] = (p and 0x0000ff) rbs = r1 - kotlin.math.abs(i2) rsum += sir[0] * rbs gsum += sir[1] * rbs bsum += sir[2] * rbs if (i2 > 0) { rinsum += sir[0] ginsum += sir[1] binsum += sir[2] } else { routsum += sir[0] goutsum += sir[1] boutsum += sir[2] } } stackpointer = radius for (x2 in 0 until w) { r[yi] = dv[rsum] g[yi] = dv[gsum] b[yi] = dv[bsum] rsum -= routsum gsum -= goutsum bsum -= boutsum stackstart = stackpointer - radius + div sir = stack[stackstart % div] routsum -= sir[0] goutsum -= sir[1] boutsum -= sir[2] if (y2 == 0) { vmin[x2] = minOf(x2 + radius + 1, wm) } p = pix[yw + vmin[x2]] sir[0] = (p and 0xff0000) shr 16 sir[1] = (p and 0x00ff00) shr 8 sir[2] = (p and 0x0000ff) rinsum += sir[0] ginsum += sir[1] binsum += sir[2] rsum += rinsum gsum += ginsum bsum += binsum stackpointer = (stackpointer + 1) % div sir = stack[(stackpointer) % div] routsum += sir[0] goutsum += sir[1] boutsum += sir[2] rinsum -= sir[0] ginsum -= sir[1] binsum -= sir[2] yi++ } yw += w } for (x2 in 0 until w) { rinsum = 0 ginsum = 0 binsum = 0 routsum = 0 goutsum = 0 boutsum = 0 rsum = 0 gsum = 0 bsum = 0 yp = -radius * w for (i2 in -radius..radius) { yi = maxOf(0, yp) + x2 sir = stack[i2 + radius] sir[0] = r[yi] sir[1] = g[yi] sir[2] = b[yi] rbs = r1 - kotlin.math.abs(i2) rsum += r[yi] * rbs gsum += g[yi] * rbs bsum += b[yi] * rbs if (i2 > 0) { rinsum += sir[0] ginsum += sir[1] binsum += sir[2] } else { routsum += sir[0] goutsum += sir[1] boutsum += sir[2] } if (i2 < hm) { yp += w } } yi = x2 stackpointer = radius for (y2 in 0 until h) { pix[yi] = (0xff000000.toInt() and pix[yi]) or (dv[rsum] shl 16) or (dv[gsum] shl 8) or dv[bsum] rsum -= routsum gsum -= goutsum bsum -= boutsum stackstart = stackpointer - radius + div sir = stack[stackstart % div] routsum -= sir[0] goutsum -= sir[1] boutsum -= sir[2] if (x2 == 0) { vmin[y2] = minOf(y2 + r1, hm) * w } p = x2 + vmin[y2] sir[0] = r[p] sir[1] = g[p] sir[2] = b[p] rinsum += sir[0] ginsum += sir[1] binsum += sir[2] rsum += rinsum gsum += ginsum bsum += binsum stackpointer = (stackpointer + 1) % div sir = stack[stackpointer] routsum += sir[0] goutsum += sir[1] boutsum += sir[2] rinsum -= sir[0] ginsum -= sir[1] binsum -= sir[2] yi += w } } val result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) result.setPixels(pix, 0, w, 0, 0, w, h) return result } }