Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ internal fun getOriginalSize(bytes: ByteArray): Pair<Int, Int> { // (w,h)
val jpegSize = getJpegSizeOrNull(bytes)
if (jpegSize != null) return jpegSize

// Fallback for WebP and others
val webpSize = getWebpSizeOrNull(bytes)
if (webpSize != null) return webpSize

// Fallback for others
val image = Image.makeFromEncoded(bytes)
return image.width to image.height
}
Expand Down Expand Up @@ -94,10 +97,60 @@ internal fun getJpegSizeOrNull(bytes: ByteArray): Pair<Int, Int>? {
return null
}

internal fun getWebpSizeOrNull(bytes: ByteArray): Pair<Int, Int>? {
if (bytes.size < 30) return null

// check "RIFF" and "WEBP" signatures
if (
int32(bytes[0], bytes[1], bytes[2], bytes[3]) != 0x5249_4646u ||
int32(bytes[8], bytes[9], bytes[10], bytes[11]) != 0x5745_4250u
) {
return null
}

val chunkType = int32(bytes[12], bytes[13], bytes[14], bytes[15])

return when (chunkType) {
0x5650_3858u -> { // "VP8X" (Extended WebP)
val w = int24LE(bytes[24], bytes[25], bytes[26]).toInt() + 1
val h = int24LE(bytes[27], bytes[28], bytes[29]).toInt() + 1
w to h
}
0x5650_3820u -> { // "VP8" (Lossy WebP)
// Check Sync Code "0x9D 0x01 0x2A"
if (bytes[23].asInt() != 0x9Du || bytes[24].asInt() != 0x01u || bytes[25].asInt() != 0x2Au) return null
val w = (int16LE(bytes[26], bytes[27]) and 0x3FFFu).toInt()
val h = (int16LE(bytes[28], bytes[29]) and 0x3FFFu).toInt()
w to h
}
0x5650_384Cu -> { // "VP8L" (Lossless WebP)
// Check Lossless
if (bytes[20].asInt() != 0x2Fu) return null
val bits = int32LE(bytes[21], bytes[22], bytes[23], bytes[24])
val w = (bits and 0x3FFFu).toInt() + 1
val h = ((bits shr 14) and 0x3FFFu).toInt() + 1
w to h
}
else -> null
}
}

private fun int16(b1: Byte, b2: Byte): UInt =
(b1.asInt() shl 8) or b2.asInt()

private fun int24(b1: Byte, b2: Byte, b3: Byte): UInt =
(b1.asInt() shl 16) or (b2.asInt() shl 8) or b3.asInt()

private fun int32(b1: Byte, b2: Byte, b3: Byte, b4: Byte): UInt =
(int16(b1, b2) shl 16) or int16(b3, b4)

private fun int16LE(b1: Byte, b2: Byte): UInt =
int16(b2, b1)

private fun int24LE(b1: Byte, b2: Byte, b3: Byte): UInt =
int24(b3, b2, b1)

private fun int32LE(b1: Byte, b2: Byte, b3: Byte, b4: Byte): UInt =
int32(b4, b3, b2, b1)

private fun Byte.asInt() = this.toUInt() and 0xFFu
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,82 @@ class ImageSizeExtractionTest {
"r3YsZ4X/1vOP/VLJh8T8lz82YxIyGV47BcfOmEaVCS1fRi4yFYwxUOO1HZ89ljeH9gTrmUf/FaTKevz1" +
"/G/8YFv+Dfdfb0/Jwsv/hyn3h7/R9ttfrBnO/3I985yfjf+MC2qOZeXV+Tcq/+CCAAA="
).decodeBase64()!!.toByteArray()
private val webp_VP8L = (
"UklGRswAAABXRUJQVlA4TL8AAAAvCAACAE/BNpIkJ/NaWwIN+QeIy5EG20aSFPUxPpP9+uw++/OJUSRJ" +
"iqqXQcL697e/4+v5DyAICAJgYAiAIIAwJcAIqAamgCTRn/zZtAuUCGT6pJG41LBDeCgnb7+oUvV3sr35" +
"nodOn+ffzvqK0+T4beXQjyKC/CzNRlkCDCRJMpzttfds7vki+h/FNNY2+hirZe0QfQjlvA0iUMi87RtN" +
"CVeZgXQEhDIIjv04E4UUON7vjkFKnN//xCAVSfdzAQA="
).decodeBase64()!!.toByteArray()
private val webp_VP8X = (
"UklGRiQDAABXRUJQVlA4WAoAAAAgAAAABwAABwAASUNDUKACAAAAAAKgbGNtcwRAAABtbnRyUkdCIFhZ" +
"WiAH6gADAAMAFQA7ACBhY3NwQVBQTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWxj" +
"bXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1kZXNjAAABIAAA" +
"AEBjcHJ0AAABYAAAADZ3dHB0AAABmAAAABRjaGFkAAABrAAAACxyWFlaAAAB2AAAABRiWFlaAAAB7AAA" +
"ABRnWFlaAAACAAAAABRyVFJDAAACFAAAACBnVFJDAAACFAAAACBiVFJDAAACFAAAACBjaHJtAAACNAAA" +
"ACRkbW5kAAACWAAAACRkbWRkAAACfAAAACRtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACQAAAAcAEcASQBN" +
"AFAAIABiAHUAaQBsAHQALQBpAG4AIABzAFIARwBCbWx1YwAAAAAAAAABAAAADGVuVVMAAAAaAAAAHABQ" +
"AHUAYgBsAGkAYwAgAEQAbwBtAGEAaQBuAABYWVogAAAAAAAA9tYAAQAAAADTLXNmMzIAAAAAAAEMQgAA" +
"Bd7///MlAAAHkwAA/ZD///uh///9ogAAA9wAAMBuWFlaIAAAAAAAAG+gAAA49QAAA5BYWVogAAAAAAAA" +
"JJ8AAA+EAAC2xFhZWiAAAAAAAABilwAAt4cAABjZcGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAA" +
"CltjaHJtAAAAAAADAAAAAKPXAABUfAAATM0AAJmaAAAmZwAAD1xtbHVjAAAAAAAAAAEAAAAMZW5VUwAA" +
"AAgAAAAcAEcASQBNAFBtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJWUDggXgAAADAC" +
"AJ0BKggACAAAwBIlsAJ0ugH4AALPcx3gAP7/uxe17up9/+aTPv/y6r/41ak0v5gXaD6ln/ZB83KC/1/p" +
"vpt+76Zy/Bl2/uXv+ZqHfL4BJ6v8lOMxG+DLt/bsAAA="
).decodeBase64()!!.toByteArray()
private val bmp = (
"Qk1uAgAAAAAAAIoAAAB8AAAACwAAAAsAAAABACAAAwAAAOQBAAATCwAAEwsAAAAAAAAAAAAAAAD/AAD/" +
"AAD/AAAAAAAA/0JHUnMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
"AAACAAAAAAAAAAAAAAAAAAAAAAIA/wABAP8AAAL/AAAC/wIAAf8BAAD/AEQF/wJ5Ev8Fkhn/A4wT/wB0" +
"A/8AAgD/AAEA/wAAAv8AAAL/AQAB/wAAAP8FTQ3/GY8o/yWxN/8cqC7/A4wT/wEAAP8AAAD/AAAC/wAA" +
"Av8AAAH/AAAA/whQEP8ekyv/K7g8/yWxN/8Fkhn/AgAA/wAAAP8AAAL/AAAC/wAAAf8AAAD/B0MO/xZ4" +
"Iv8ekyv/GY8o/wJ5Ef8CAAH/AAAB/wAAAP8AAQD/AQAB/wEAAf8CJQX/B0MO/whQEP8FTQ3/AEMF/wEA" +
"C/8AAAX/AAAB/wAAAP8BAAP/AgEE/wEAAf8AAAD/AAAA/wEAAP8CAAD/AQEl/wkKQv8HCVf/BgtW/wMF" +
"Nv8BAAT/AgAB/wEAAP8BAAD/AgAB/wIAAf8CAjn/ExVq/xMXkP8RF4z/BwxZ/wAAAv8BAAD/AgAA/wIA" +
"AP8AAAH/AAAC/wICPv8VFXX/GRyi/xUYm/8ICmH/AAAB/wACAP8BAQD/AgAA/wAAAP8AAAH/AwI8/xoX" +
"bv8YFoz/FBWG/wkKVf8AAAH/AAIA/wECAP8CAQD/AAEA/wABAP8BADf/CAJV/wQCbf8CAmj/AQFB/wAB" +
"Bv8AAgD/AQIA/wIBAP8AAgD/AAIA/w=="
).decodeBase64()!!.toByteArray()

@Test
fun testPngSizeExtraction() {
assertNull(getPngSizeOrNull(jpeg))
assertNull(getPngSizeOrNull(webp))
assertNull(getPngSizeOrNull(webp_VP8X))
assertNull(getPngSizeOrNull(webp_VP8L))
assertNull(getPngSizeOrNull(bmp))
assertEquals(10 to 10, getPngSizeOrNull(png))
}

@Test
fun testJpegSizeExtraction() {
assertNull(getJpegSizeOrNull(png))
assertNull(getJpegSizeOrNull(webp))
assertNull(getPngSizeOrNull(webp_VP8X))
assertNull(getPngSizeOrNull(webp_VP8L))
assertNull(getPngSizeOrNull(bmp))
assertEquals(10 to 10, getJpegSizeOrNull(jpeg))
}

@Test
fun testWebpSizeExtraction() {
assertNull(getWebpSizeOrNull(png))
assertNull(getWebpSizeOrNull(jpeg))
assertNull(getWebpSizeOrNull(bmp))
assertEquals(10 to 10, getWebpSizeOrNull(webp))
assertEquals(9 to 9, getWebpSizeOrNull(webp_VP8L))
assertEquals(8 to 8, getWebpSizeOrNull(webp_VP8X))
}

@OptIn(ExperimentalWasmJsInterop::class)
@Test
fun testSizeExtraction() = runTest {
assertEquals(10 to 10, getOriginalSize(jpeg))
assertEquals(10 to 10, getOriginalSize(png))
assertEquals(10 to 10, getOriginalSize(webp))
assertEquals(9 to 9, getOriginalSize(webp_VP8L))
assertEquals(8 to 8, getOriginalSize(webp_VP8X))

awaitSkiko()
assertEquals(10 to 10, getOriginalSize(webp))
assertEquals(11 to 11, getOriginalSize(bmp))
}
}