package com.margelo.nitro.image import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Matrix import android.graphics.Rect import android.graphics.RectF import android.os.Build import androidx.annotation.Keep import androidx.core.graphics.createBitmap import androidx.core.graphics.scale import com.facebook.proguard.annotations.DoNotStrip import com.madebyevan.thumbhash.ThumbHash import com.margelo.nitro.core.ArrayBuffer import com.margelo.nitro.core.Promise import com.margelo.nitro.image.extensions.compressInMemory import com.margelo.nitro.image.extensions.isGPU import com.margelo.nitro.image.extensions.isRawPixelDataAccessible import com.margelo.nitro.image.extensions.pixelFormat import com.margelo.nitro.image.extensions.saveToFile import com.margelo.nitro.image.extensions.toFilePath import com.margelo.nitro.image.extensions.toByteBuffer import com.margelo.nitro.image.extensions.toCpuAccessible import com.margelo.nitro.image.extensions.toMutable import java.io.File import java.nio.ByteBuffer import kotlin.math.ceil @Suppress("ConvertSecondaryConstructorToPrimary") @Keep @DoNotStrip class HybridImage: HybridImageSpec { val bitmap: Bitmap override val width: Double get() = bitmap.width.toDouble() override val height: Double get() = bitmap.height.toDouble() override val memorySize: Long get() = bitmap.allocationByteCount.toLong() constructor(bitmap: Bitmap) { this.bitmap = bitmap } override fun dispose() { bitmap.recycle() } override fun toRawPixelData(allowGpu: Boolean?): RawPixelData { val allowGpu = allowGpu ?: false if (allowGpu && bitmap.isGPU && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Wrap the existing GPU buffer (HardwareBuffer) val arrayBuffer = ArrayBuffer.wrap(bitmap.hardwareBuffer) return RawPixelData(arrayBuffer, width, height, bitmap.pixelFormat) } else { // Copy the data into a CPU buffer (ByteBuffer) var bitmap = bitmap if (!bitmap.isRawPixelDataAccessible) { // If this is a GPU-based or otherwise unsupported Bitmap config, normalize it first. bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false) } val buffer = bitmap.toByteBuffer() val arrayBuffer = ArrayBuffer.wrap(buffer) return RawPixelData(arrayBuffer, bitmap.width.toDouble(), bitmap.height.toDouble(), bitmap.pixelFormat) } } override fun toRawPixelDataAsync(allowGpu: Boolean?): Promise { return Promise.async { toRawPixelData(allowGpu) } } override fun toEncodedImageData(format: ImageFormat, quality: Double?): EncodedImageData { val quality = quality ?: 100.0 val byteBuffer = bitmap.compressInMemory(format, quality.toInt()) val arrayBuffer = ArrayBuffer.copy(byteBuffer) return EncodedImageData(arrayBuffer, width, height, format) } override fun toEncodedImageDataAsync( format: ImageFormat, quality: Double? ): Promise { return Promise.async { toEncodedImageData(format, quality) } } override fun rotate(degrees: Double, allowFastFlagRotation: Boolean?): HybridImageSpec { // 1. Make sure the Bitmap we want to draw is drawable (HARDWARE isn't) val source = bitmap.toCpuAccessible() // 2. Create a rotation Matrix val matrix = Matrix() matrix.setRotate(degrees.toFloat(), source.width / 2f, source.height / 2f) // Rotating around the center can move the image into negative coordinates. // Map the original bounds through the rotation to measure the full output size, // then translate the matrix back so the rotated image starts at (0, 0). val bounds = RectF(0f, 0f, source.width.toFloat(), source.height.toFloat()) matrix.mapRect(bounds) matrix.postTranslate(-bounds.left, -bounds.top) // 3. Create a new blank Bitmap as our output val destinationWidth = ceil(bounds.width()).toInt().coerceAtLeast(1) val destinationHeight = ceil(bounds.height()).toInt().coerceAtLeast(1) val destination = createBitmap(destinationWidth, destinationHeight) // 4. Draw the Bitmap to our destination Canvas(destination).apply { drawBitmap(source, matrix, null) } // 5. Return it! return HybridImage(destination) } override fun rotateAsync(degrees: Double, allowFastFlagRotation: Boolean?): Promise { return Promise.async { rotate(degrees, allowFastFlagRotation) } } override fun resize(width: Double, height: Double): HybridImageSpec { if (width < 0) { throw Error("Width cannot be less than 0! (width: $width)") } if (height < 0) { throw Error("Height cannot be less than 0! (height: $height)") } val resizedBitmap = bitmap.scale(width.toInt(), height.toInt(), true) return HybridImage(resizedBitmap) } override fun resizeAsync(width: Double, height: Double): Promise { return Promise.async { resize(width, height) } } override fun crop(startX: Double, startY: Double, endX: Double, endY: Double): HybridImageSpec { val width = endX - startX val height = endY - startY if (width < 0) { throw Error("Width cannot be less than 0! (startX: $startX - endX: $endX = $width)") } if (height < 0) { throw Error("Height cannot be less than 0! (startY: $startY - endY: $endY = $height)") } val croppedBitmap = Bitmap.createBitmap( bitmap, startX.toInt(), startY.toInt(), width.toInt(), height.toInt() ) return HybridImage(croppedBitmap) } override fun cropAsync( startX: Double, startY: Double, endX: Double, endY: Double ): Promise { return Promise.async { crop(startX, startY, endX, endY) } } override fun mirrorHorizontally(): HybridImageSpec { val matrix = Matrix().apply { preScale(-1f, 1f) } val mirrored = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false) return HybridImage(mirrored) } override fun mirrorHorizontallyAsync(): Promise { return Promise.async { mirrorHorizontally() } } override fun saveToFileAsync( path: String, format: ImageFormat, quality: Double? ): Promise { val quality = quality ?: 100.0 return Promise.async { bitmap.saveToFile(path.toFilePath(), format, quality.toInt()) } } override fun saveToTemporaryFileAsync(format: ImageFormat, quality: Double?): Promise { val quality = quality ?: 100.0 return Promise.async { val tempFile = File.createTempFile("nitro_image_", ".${format.name.lowercase()}") bitmap.saveToFile(tempFile.path, format, quality.toInt()) return@async tempFile.path } } override fun toThumbHash(): ArrayBuffer { if (width > 100 || height > 100) { throw Error("Cannot encode an Image larger than 100x100 to a ThumbHash. " + "Resize the image to <100 pixels in width and height first, then try again!") } val bitmapBuffer = bitmap.toByteBuffer() val thumbHash = ThumbHash.rgbaToThumbHash(bitmap.width, bitmap.height, bitmapBuffer.array()) val buffer = ByteBuffer.wrap(thumbHash) return ArrayBuffer.copy(buffer) } override fun toThumbHashAsync(): Promise { return Promise.async { toThumbHash() } } override fun renderInto(image: HybridImageSpec, x: Double, y: Double, width: Double, height: Double): HybridImageSpec { val newImage = image as? HybridImage ?: throw Error("The image ($image) is not a `HybridImage`!") // 1. Copy this Bitmap into a new Bitmap val copy = bitmap.toMutable(true) // 2. Create a Canvas to start drawing Canvas(copy).also { canvas -> // 3. Prepare the Bitmap we want to draw into our Canvas val rect = Rect(x.toInt(), y.toInt(), width.toInt(), height.toInt()) // 4. Make sure we can draw the Bitmap (HARDWARE isn't CPU accessible) val drawable = newImage.bitmap.toCpuAccessible() // 5. Now draw! canvas.drawBitmap(drawable, null, rect, null) } // 6. Wrap the new Bitmap as a HybridImage and return return HybridImage(copy) } override fun renderIntoAsync(image: HybridImageSpec, x: Double, y: Double, width: Double, height: Double): Promise { return Promise.async { renderInto(image, x, y, width, height) } } }