P6 Stream V: ImageCompression (expect/actual) + CameraPicker polish
Android: BitmapFactory + ExifInterface + JPEG quality 0.7 + 1920px downscale. iOS: UIImage.jpegData. JVM/JS/WASM: no-op. CameraPicker uses TakePicture ActivityResult + permission rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package com.tt.honeyDue.util
|
||||
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.useContents
|
||||
import kotlinx.cinterop.usePinned
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import platform.CoreGraphics.CGRectMake
|
||||
import platform.CoreGraphics.CGSizeMake
|
||||
import platform.Foundation.NSData
|
||||
import platform.Foundation.create
|
||||
import platform.UIKit.UIGraphicsBeginImageContextWithOptions
|
||||
import platform.UIKit.UIGraphicsEndImageContext
|
||||
import platform.UIKit.UIGraphicsGetImageFromCurrentImageContext
|
||||
import platform.UIKit.UIImage
|
||||
import platform.UIKit.UIImageJPEGRepresentation
|
||||
import platform.posix.memcpy
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* iOS implementation of [ImageCompression].
|
||||
*
|
||||
* Matches the iOS reference helper at
|
||||
* `iosApp/iosApp/Helpers/ImageCompression.swift`:
|
||||
* - decode bytes into a [UIImage]
|
||||
* - optionally downscale so the long edge ≤ `maxEdgePx`, aspect preserved
|
||||
* - JPEG-encode at `quality` via [UIImageJPEGRepresentation]
|
||||
*
|
||||
* `UIImage(data:)` preserves EXIF via `imageOrientation`; drawing the
|
||||
* image into a graphics context — or calling `UIImageJPEGRepresentation`
|
||||
* directly — bakes that orientation into the output pixels so the result
|
||||
* is always upright (no orientation tag round-tripped).
|
||||
*/
|
||||
@OptIn(ExperimentalForeignApi::class, kotlinx.cinterop.BetaInteropApi::class)
|
||||
actual object ImageCompression {
|
||||
|
||||
actual suspend fun compress(
|
||||
input: ByteArray,
|
||||
maxEdgePx: Int,
|
||||
quality: Float
|
||||
): ByteArray = withContext(Dispatchers.Default) {
|
||||
// --- wrap input in NSData -----------------------------------------
|
||||
val nsData: NSData = input.usePinned { pinned ->
|
||||
NSData.create(
|
||||
bytes = pinned.addressOf(0),
|
||||
length = input.size.toULong()
|
||||
)
|
||||
}
|
||||
|
||||
// --- decode UIImage -----------------------------------------------
|
||||
val decoded = UIImage.imageWithData(nsData) ?: return@withContext input
|
||||
|
||||
// --- downscale if needed ------------------------------------------
|
||||
val resized = downscaleIfNeeded(decoded, maxEdgePx)
|
||||
|
||||
// --- encode JPEG (orientation is baked into pixels) ---------------
|
||||
val clampedQuality = quality.coerceIn(0f, 1f).toDouble()
|
||||
val jpeg = UIImageJPEGRepresentation(resized, clampedQuality)
|
||||
?: return@withContext input
|
||||
|
||||
jpeg.toByteArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Downscale a [UIImage] so its long edge is at most [maxEdgePx].
|
||||
* Aspect ratio is preserved. Returns the original image if it already
|
||||
* fits. Drawing through `UIGraphicsBeginImageContextWithOptions` also
|
||||
* normalizes `imageOrientation` into the pixel data.
|
||||
*/
|
||||
private fun downscaleIfNeeded(image: UIImage, maxEdgePx: Int): UIImage {
|
||||
if (maxEdgePx <= 0) return image
|
||||
|
||||
val (srcW, srcH) = image.size.useContents { width to height }
|
||||
val longEdge = max(srcW, srcH)
|
||||
if (longEdge <= maxEdgePx) return image
|
||||
|
||||
val scale = maxEdgePx.toDouble() / longEdge
|
||||
val targetW = (srcW * scale).coerceAtLeast(1.0)
|
||||
val targetH = (srcH * scale).coerceAtLeast(1.0)
|
||||
|
||||
// `scale = 1.0` keeps the output at target pixel dimensions
|
||||
// regardless of screen scale (important in unit tests / server uploads).
|
||||
UIGraphicsBeginImageContextWithOptions(
|
||||
size = CGSizeMake(targetW, targetH),
|
||||
opaque = false,
|
||||
scale = 1.0
|
||||
)
|
||||
image.drawInRect(CGRectMake(0.0, 0.0, targetW, targetH))
|
||||
val output = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return output ?: image
|
||||
}
|
||||
|
||||
/** Copy an [NSData] buffer into a Kotlin [ByteArray]. */
|
||||
private fun NSData.toByteArray(): ByteArray {
|
||||
val len = length.toInt()
|
||||
if (len == 0) return ByteArray(0)
|
||||
return ByteArray(len).apply {
|
||||
usePinned { pinned ->
|
||||
memcpy(pinned.addressOf(0), this@toByteArray.bytes, this@toByteArray.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user