diff --git a/.gitattributes b/.gitattributes index f529071c..fb5ef153 100644 --- a/.gitattributes +++ b/.gitattributes @@ -25,4 +25,7 @@ *.pyc binary *.swp binary *.pdf binary -*.exe binary \ No newline at end of file +*.exe binary +*.avif binary +*.heif binary +*.jxl binary \ No newline at end of file diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/storage/ImageUtil.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/storage/ImageUtil.kt index c5a855fd..ed65a253 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/storage/ImageUtil.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/storage/ImageUtil.kt @@ -7,8 +7,11 @@ package suwayomi.tachidesk.manga.impl.util.storage * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil.ImageType.AVIF import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil.ImageType.GIF -import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil.ImageType.JPG +import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil.ImageType.HEIF +import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil.ImageType.JPEG +import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil.ImageType.JXL import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil.ImageType.PNG import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil.ImageType.WEBP import java.io.InputStream @@ -31,7 +34,7 @@ object ImageUtil { fun findImageType(stream: InputStream): ImageType? { try { - val bytes = ByteArray(8) + val bytes = ByteArray(12) val length = if (stream.markSupported()) { stream.mark(bytes.size) @@ -45,7 +48,7 @@ object ImageUtil { } if (bytes.compareWith(charByteArrayOf(0xFF, 0xD8, 0xFF))) { - return JPG + return JPEG } if (bytes.compareWith(charByteArrayOf(0x89, 0x50, 0x4E, 0x47))) { return PNG @@ -56,11 +59,88 @@ object ImageUtil { if (bytes.compareWith("RIFF".toByteArray())) { return WEBP } - } catch (e: Exception) { + if (bytes.copyOfRange(4, 12).compareWith("ftypavif".toByteArray())) { + return AVIF + } + if (isHEIF(bytes)) { + return HEIF + } + if (bytes.compareWith(charByteArrayOf(0xFF, 0x0A))) { + return JXL + } + } catch (_: Exception) { } return null } + private fun isHEIF(bytes: ByteArray): Boolean { + // ftypheic + if (bytes[4] == 0x66.toByte() && + bytes[5] == 0x74.toByte() && + bytes[6] == 0x79.toByte() && + bytes[7] == 0x70.toByte() && + bytes[8] == 0x68.toByte() && + bytes[9] == 0x65.toByte() && + bytes[10] == 0x69.toByte() && + bytes[11] == 0x63.toByte() + ) { + return true + } + + // ftypmif1 + if (bytes[4] == 0x66.toByte() && + bytes[5] == 0x74.toByte() && + bytes[6] == 0x79.toByte() && + bytes[7] == 0x70.toByte() && + bytes[8] == 0x6D.toByte() && + bytes[9] == 0x69.toByte() && + bytes[10] == 0x66.toByte() && + bytes[11] == 0x31.toByte() + ) { + return true + } + + // ftypmsf1 + if (bytes[4] == 0x66.toByte() && + bytes[5] == 0x74.toByte() && + bytes[6] == 0x79.toByte() && + bytes[7] == 0x70.toByte() && + bytes[8] == 0x6D.toByte() && + bytes[9] == 0x73.toByte() && + bytes[10] == 0x66.toByte() && + bytes[11] == 0x31.toByte() + ) { + return true + } + + // ftypheis + if (bytes[4] == 0x66.toByte() && + bytes[5] == 0x74.toByte() && + bytes[6] == 0x79.toByte() && + bytes[7] == 0x70.toByte() && + bytes[8] == 0x68.toByte() && + bytes[9] == 0x65.toByte() && + bytes[10] == 0x69.toByte() && + bytes[11] == 0x73.toByte() + ) { + return true + } + + // ftyphevc + if (bytes[4] == 0x66.toByte() && + bytes[5] == 0x74.toByte() && + bytes[6] == 0x79.toByte() && + bytes[7] == 0x70.toByte() && + bytes[8] == 0x68.toByte() && + bytes[9] == 0x65.toByte() && + bytes[10] == 0x76.toByte() && + bytes[11] == 0x63.toByte() + ) { + return true + } + return false + } + private fun ByteArray.compareWith(magic: ByteArray): Boolean { return magic.indices.none { this[it] != magic[it] } } @@ -73,10 +153,13 @@ object ImageUtil { } } - enum class ImageType(val mime: String) { - JPG("image/jpeg"), - PNG("image/png"), - GIF("image/gif"), - WEBP("image/webp") + enum class ImageType(val mime: String, val extension: String) { + AVIF("image/avif", "avif"), + GIF("image/gif", "gif"), + HEIF("image/heif", "heif"), + JPEG("image/jpeg", "jpg"), + JXL("image/jxl", "jxl"), + PNG("image/png", "png"), + WEBP("image/webp", "webp") } } diff --git a/server/src/test/kotlin/suwayomi/tachidesk/ImageUtilTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/ImageUtilTest.kt new file mode 100644 index 00000000..1cdc3a9c --- /dev/null +++ b/server/src/test/kotlin/suwayomi/tachidesk/ImageUtilTest.kt @@ -0,0 +1,25 @@ +package suwayomi.tachidesk + +import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil +import kotlin.test.Test +import kotlin.test.assertEquals + +class ImageUtilTest { + @Test + fun jxlTest() { + val type = ImageUtil.findImageType(this::class.java.classLoader.getResourceAsStream("dice.jxl")!!) + assertEquals(ImageUtil.ImageType.JXL, type) + } + + @Test + fun avifTest() { + val type = ImageUtil.findImageType(this::class.java.classLoader.getResourceAsStream("fox.profile0.8bpc.yuv420.avif")!!) + assertEquals(ImageUtil.ImageType.AVIF, type) + } + + @Test + fun heifTest() { + val type = ImageUtil.findImageType(this::class.java.classLoader.getResourceAsStream("sample1.heif")!!) + assertEquals(ImageUtil.ImageType.HEIF, type) + } +} diff --git a/server/src/test/resources/dice.jxl b/server/src/test/resources/dice.jxl new file mode 100644 index 00000000..d1a5a7d0 Binary files /dev/null and b/server/src/test/resources/dice.jxl differ diff --git a/server/src/test/resources/fox.profile0.8bpc.yuv420.avif b/server/src/test/resources/fox.profile0.8bpc.yuv420.avif new file mode 100644 index 00000000..2bae4c71 Binary files /dev/null and b/server/src/test/resources/fox.profile0.8bpc.yuv420.avif differ diff --git a/server/src/test/resources/sample1.heif b/server/src/test/resources/sample1.heif new file mode 100644 index 00000000..7a68f35d Binary files /dev/null and b/server/src/test/resources/sample1.heif differ