package com.tt.honeyDue.util import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Color import android.graphics.Matrix import androidx.exifinterface.media.ExifInterface import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import kotlin.math.max /** * Unit tests for [ImageCompression] on Android. * * Mirrors iOS `ImageCompression.swift` semantics: * - JPEG quality 0.7 * - Long edge downscaled to max 1920px (aspect preserved) * - EXIF orientation applied into pixels, result has normalized orientation * * Uses Robolectric so real [Bitmap] / [BitmapFactory] / [ExifInterface] * plumbing is available under JVM unit tests. */ @RunWith(RobolectricTestRunner::class) class ImageCompressionAndroidTest { // ---- helpers ------------------------------------------------------------ /** Create a solid-color [Bitmap] of the requested size. */ private fun makeBitmap(width: Int, height: Int, color: Int = Color.RED): Bitmap { val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas = Canvas(bmp) canvas.drawColor(color) return bmp } /** Encode a bitmap to a JPEG [ByteArray] at max quality (100). */ private fun toJpegBytes(bmp: Bitmap, quality: Int = 100): ByteArray { val baos = ByteArrayOutputStream() bmp.compress(Bitmap.CompressFormat.JPEG, quality, baos) return baos.toByteArray() } /** Decode bytes to get final (width, height) of the encoded JPEG. */ private fun dimensionsOf(bytes: ByteArray): Pair { val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opts) return opts.outWidth to opts.outHeight } /** Read EXIF orientation tag from encoded bytes. */ private fun orientationOf(bytes: ByteArray): Int { val exif = ExifInterface(ByteArrayInputStream(bytes)) return exif.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL ) } // ---- tests -------------------------------------------------------------- @Test fun compress_largeImage_returnsSmallerByteArray() = runTest { // Start with a reasonably large (and thus reasonably compressible) image. val src = toJpegBytes(makeBitmap(2400, 1600, Color.BLUE), quality = 100) val out = ImageCompression.compress(src) assertTrue( "Expected compressed output to be strictly smaller than input " + "(src=${src.size}, out=${out.size})", out.size < src.size ) } @Test fun compress_downscalesLongEdge_to1920_byDefault() = runTest { val src = toJpegBytes(makeBitmap(3000, 1500)) val out = ImageCompression.compress(src) val (w, h) = dimensionsOf(out) assertTrue( "Long edge must be <= 1920 (got ${max(w, h)})", max(w, h) <= 1920 ) // Aspect preserved: 3000x1500 → 2:1 → 1920x960. assertEquals("Width should match downscaled target", 1920, w) assertEquals("Height should preserve 2:1 aspect", 960, h) } @Test fun compress_respectsCustomMaxEdgePx() = runTest { val src = toJpegBytes(makeBitmap(1200, 800)) val out = ImageCompression.compress(src, maxEdgePx = 500) val (w, h) = dimensionsOf(out) assertTrue( "Long edge must be <= 500 (got w=$w, h=$h)", max(w, h) <= 500 ) } @Test fun compress_smallImage_isStillRecompressed_atLowerQuality() = runTest { // Tiny bitmap, encoded at MAX quality so JPEG is relatively fat. val src = toJpegBytes(makeBitmap(400, 300), quality = 100) val out = ImageCompression.compress(src, maxEdgePx = 1920, quality = 0.7f) // Dimensions should NOT be upscaled. val (w, h) = dimensionsOf(out) assertEquals(400, w) assertEquals(300, h) // Re-encoded at quality 0.7 → bytes should be smaller than the // quality-100 input for a non-trivial bitmap. assertTrue( "Expected re-compressed (q=0.7) output to be smaller than src " + "(src=${src.size}, out=${out.size})", out.size < src.size ) } @Test fun compress_normalizesExifOrientation() = runTest { // Build an image and tag it with EXIF Orientation=6 (rotate 90° CW). val src = toJpegBytes(makeBitmap(1000, 500)) val tagged = run { val baos = ByteArrayOutputStream() baos.write(src) val bytes = baos.toByteArray() // Write EXIF into the JPEG via a temp file-backed approach: // easiest = write to a temp file, set attribute, read back. val tmp = java.io.File.createTempFile("exif_", ".jpg") tmp.writeBytes(bytes) val exif = ExifInterface(tmp.absolutePath) exif.setAttribute( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_ROTATE_90.toString() ) exif.saveAttributes() val result = tmp.readBytes() tmp.delete() result } // Sanity: tagged input actually carries orientation=6. assertEquals( ExifInterface.ORIENTATION_ROTATE_90, orientationOf(tagged) ) val out = ImageCompression.compress(tagged) // After compression, orientation should be normalized // (applied into pixels), so the tag should be NORMAL (1) or missing. val outOrientation = orientationOf(out) assertTrue( "Expected normalized orientation (NORMAL or UNDEFINED), got $outOrientation", outOrientation == ExifInterface.ORIENTATION_NORMAL || outOrientation == ExifInterface.ORIENTATION_UNDEFINED ) } @Test fun compress_preservesImageUsability() = runTest { val src = toJpegBytes(makeBitmap(800, 600, Color.GREEN)) val out = ImageCompression.compress(src) // Result must be decodable back into a Bitmap. val decoded = BitmapFactory.decodeByteArray(out, 0, out.size) assertNotNull("Compressed output must be a valid JPEG", decoded) assertTrue(decoded!!.width > 0) assertTrue(decoded.height > 0) } }