Skip to content
This repository was archived by the owner on Jul 8, 2022. It is now read-only.

Commit 98ce0b5

Browse files
Some EXIF & AVIF/HEIC parsing fixes
1 parent 005987c commit 98ce0b5

File tree

9 files changed

+273
-55
lines changed

9 files changed

+273
-55
lines changed

korge-sandbox/src/commonMain/kotlin/Main.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import kotlin.random.*
3232
suspend fun main() = Korge(bgcolor = Colors.DARKCYAN.mix(Colors.BLACK, 0.8), clipBorders = false
3333
//, debugAg = true
3434
) {
35-
mainVectorRendering()
35+
mainExifTest()
36+
//mainVectorRendering()
3637
//mainRenderText()
3738
//mainTextMetrics()
3839
//mainBitmapTexId()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import com.soywiz.korge.view.*
2+
import com.soywiz.korim.format.*
3+
import com.soywiz.korio.file.std.*
4+
5+
suspend fun Stage.mainExifTest() {
6+
//val info = resourcesVfs["IMG_5455.HEIC"].readImageInfo(HEICInfo)
7+
//val info = localVfs("/tmp/IMG_5455.HEIC").readImageInfo(HEICInfo, ImageDecodingProps(debug = true))
8+
val info = localVfs("/tmp/Exif5-2x.avif").readImageInfo(HEICInfo, ImageDecodingProps(debug = true))
9+
println(info)
10+
}
11+
Lines changed: 144 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,165 @@
11
package com.soywiz.korim.format
22

3+
import com.soywiz.kds.*
4+
import com.soywiz.klogger.*
5+
import com.soywiz.kmem.*
6+
import com.soywiz.korio.compression.util.*
7+
import com.soywiz.korio.dynamic.KDynamic.Companion.toInt
8+
import com.soywiz.korio.experimental.*
39
import com.soywiz.korio.lang.*
410
import com.soywiz.korio.stream.*
11+
import com.soywiz.krypto.encoding.*
512

613
// AVIF & HEIC metadata extractor
7-
object AVIFInfo : BaseAVIFInfo("avif")
8-
object HEICInfo : BaseAVIFInfo("heic")
14+
object AVIFInfo : ISOBMFF("avif")
15+
object HEICInfo : ISOBMFF("heic")
916

10-
open class BaseAVIFInfo(vararg exts: String) : ImageFormatSuspend(*exts) {
17+
// ISOBMFF
18+
// https://gpac.github.io/mp4box.js/test/filereader.html
19+
// https://en.wikipedia.org/wiki/ISO/IEC_base_media_file_format
20+
// https://www.w3.org/TR/mse-byte-stream-format-isobmff/
21+
open class ISOBMFF(vararg exts: String) : ImageFormatSuspend(*exts) {
1122
override suspend fun decodeHeaderSuspend(s: AsyncStream, props: ImageDecodingProps): ImageInfo? {
1223
val ss = s.slice(4 until 8).readString(4, LATIN1)
1324
if (ss != "ftyp") return null
14-
return StreamParser(props).also { it.decodeLevel(s, 0) }.info
25+
return StreamParser(props).also { it.decode(s) }.info
1526
}
1627

28+
data class ItemExtent(val offset: Long, val size: Long)
29+
data class ItemInfo(val id: Int) {
30+
var type: String = ""
31+
var extents: ArrayList<ItemExtent> = arrayListOf()
32+
override fun toString(): String = "ItemInfo($id, $type, $extents)"
33+
}
34+
35+
@OptIn(KorioExperimentalApi::class)
1736
class StreamParser(val props: ImageDecodingProps) {
37+
val debug get() = props.debug
1838
var info = ImageInfo()
39+
val items = IntMap<ItemInfo>()
1940

20-
suspend fun decodeLevel(s: AsyncStream, level: Int) {
41+
suspend fun decode(s: AsyncStream) {
42+
decodeLevel(s.sliceHere(), 0)
43+
if (debug) Console.error("ITEMS")
44+
items.fastValueForEach {
45+
if (it.type == "Exif") {
46+
val extent = it.extents.first()
47+
val range = s.sliceWithSize(extent.offset + 4, extent.size - 4).readAllAsFastStream()
48+
EXIF.readExif(range.toAsyncStream(), info, debug = debug)
49+
}
50+
}
51+
}
52+
53+
private suspend fun decodeLevel(s: AsyncStream, level: Int) {
2154
while (!s.eof()) {
2255
val blockSize = s.readS32BE()
2356
val blockType = s.readStringz(4, LATIN1)
2457
//val blockSubtype = s.readStringz(4, LATIN1)
2558
//val blockStream = s.readStream(blockSize - 12)
26-
val blockStream = s.readStream(blockSize - 8)
59+
val blockStream = if (blockSize < 8) {
60+
s.readStream(s.getAvailable())
61+
} else {
62+
s.readStream(blockSize - 8)
63+
}
2764
//if (blockSize)
28-
//Console.error("${" ".repeat(level)}blockSize=$blockSize, blockType=$blockType")
65+
if (debug) {
66+
Console.error("${" ".repeat(level)}blockSize=$blockSize, blockType=$blockType")
67+
}
2968
when (blockType) {
3069
"ftyp" -> Unit
3170
"meta" -> {
3271
blockStream.skip(4)
3372
decodeLevel(blockStream, level + 1)
3473
}
74+
/// See ISO 14496-12:2015 § 8.11.3
75+
// https://github.com/kornelski/avif-parse/blob/2174e0e15647918cbbcb965ba142c285a6ef457f/src/lib.rs#L1136
76+
"iloc" -> {
77+
val version = blockStream.readU8()
78+
val flags = blockStream.readU24BE()
79+
val sizePacked = ByteArrayBitReader(blockStream.readBytesExact(2))
80+
val offsetSize = sizePacked.readIntBits(4)
81+
val lengthSize = sizePacked.readIntBits(4)
82+
val baseOffsetSize = sizePacked.readIntBits(4)
83+
val indexSize = when (version) {
84+
1, 2 -> sizePacked.readIntBits(4)
85+
else -> 0
86+
}
87+
val count: Int = when (version) {
88+
0, 1 -> blockStream.readU16BE()
89+
2 -> blockStream.readS32BE()
90+
else -> TODO()
91+
}
92+
93+
suspend fun AsyncStream.readSize(size: Int): Long {
94+
return when (size) {
95+
0 -> 0L
96+
4 -> blockStream.readS32BE().toLong()
97+
8 -> blockStream.readS64BE()
98+
else -> TODO("size=$size")
99+
}
100+
}
101+
102+
if (debug) {
103+
Console.error("iloc version=$version, flags=$flags, count=$count, offsetSize=$offsetSize, lengthSize=$lengthSize, baseOffsetSize=$baseOffsetSize, indexSize=$indexSize")
104+
}
105+
for (n in 0 until count) {
106+
val itemID = when (version) {
107+
0, 1 -> blockStream.readU16BE()
108+
2 -> blockStream.readS32LE()
109+
else -> TODO()
110+
}
111+
val method = when (version) {
112+
0 -> 0
113+
1, 2 -> blockStream.readU16BE()
114+
else -> TODO()
115+
}
116+
val dataRefIndex = blockStream.readU16BE()
117+
118+
val baseOffset: Long = blockStream.readSize(baseOffsetSize)
119+
val extentCount = blockStream.readU16BE()
120+
if (debug) {
121+
Console.error(" - itemID=$itemID, method=$method, dataRefIndex=$dataRefIndex, baseOffset=$baseOffset, extentCount=$extentCount")
122+
}
123+
val itemInfo = items.getOrPut(itemID) { ItemInfo(itemID) }
124+
for (m in 0 until extentCount) {
125+
val extentIndex = blockStream.readSize(indexSize)
126+
val extentOffset = blockStream.readSize(offsetSize)
127+
val extentLength = blockStream.readSize(lengthSize)
128+
val offset = baseOffset + extentOffset
129+
if (debug) {
130+
Console.error(" - $extentIndex, $offset, $extentLength")
131+
}
132+
itemInfo.extents.add(ItemExtent(offset, extentLength))
133+
}
134+
//Console.error(" - $itemID, $unk2, $offset, $size")
135+
}
136+
}
137+
"iinf" -> {
138+
val version = blockStream.readU8()
139+
val flags = blockStream.readU24BE()
140+
val entryCount = when (version) {
141+
0 -> blockStream.readU16BE()
142+
1 -> blockStream.readS32BE()
143+
else -> TODO()
144+
}
145+
for (n in 0 until entryCount) {
146+
decodeLevel(blockStream, level + 1)
147+
}
148+
}
149+
"infe" -> {
150+
val version = blockStream.readU8()
151+
val flags = blockStream.readU24BE()
152+
val itemID: Int = when (version) {
153+
2 -> blockStream.readU16BE()
154+
3 -> blockStream.readS32BE()
155+
else -> TODO()
156+
}
157+
val protectionIndex = blockStream.readU16BE()
158+
val itemType = blockStream.readStringz(4, LATIN1)
159+
val itemInfo = items.getOrPut(itemID) { ItemInfo(itemID) }
160+
itemInfo.type = itemType
161+
if (debug) Console.error("infe: itemID=$itemID, protectionIndex=$protectionIndex, itemType=${itemType}")
162+
}
35163
"iprp" -> decodeLevel(blockStream, level + 1)
36164
"ipco" -> decodeLevel(blockStream, level + 1)
37165
"ispe" -> {
@@ -41,13 +169,18 @@ open class BaseAVIFInfo(vararg exts: String) : ImageFormatSuspend(*exts) {
41169
}
42170
"mdat" -> {
43171
blockStream.skip(4)
44-
if (blockStream.sliceHere().readStringz(4, LATIN1) == "Exif") {
45-
val exif = EXIF.readExif(blockStream.sliceHere())
46-
info.orientation = exif.orientation
47-
}
48172
}
49173
}
50174
}
51175
}
176+
177+
suspend fun checkExif(blockStream: AsyncStream): Boolean {
178+
if (blockStream.sliceHere().readStringz(4, LATIN1) == "Exif") {
179+
val exif = EXIF.readExif(blockStream.sliceHere())
180+
info.orientation = exif.orientation
181+
return true
182+
}
183+
return false
184+
}
52185
}
53186
}
Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.soywiz.korim.format
22

3-
import com.soywiz.kds.mapDouble
43
import com.soywiz.klogger.*
54
import com.soywiz.kmem.*
65
import com.soywiz.korio.async.*
@@ -10,12 +9,13 @@ import com.soywiz.korio.stream.*
109
import com.soywiz.krypto.encoding.*
1110

1211
// https://zpl.fi/exif-orientation-in-different-formats/
12+
// https://exiftool.org/TagNames/EXIF.html
1313
object EXIF {
14-
suspend fun readExifFromJpeg(s: VfsFile, info: ImageInfo = ImageInfo()): ImageInfo = s.openUse(VfsOpenMode.READ) {
15-
readExifFromJpeg(this, info)
14+
suspend fun readExifFromJpeg(s: VfsFile, info: ImageInfo = ImageInfo(), debug: Boolean = false): ImageInfo = s.openUse(VfsOpenMode.READ) {
15+
readExifFromJpeg(this, info, debug)
1616
}
1717

18-
suspend fun readExifFromJpeg(s: AsyncStream, info: ImageInfo = ImageInfo()): ImageInfo {
18+
suspend fun readExifFromJpeg(s: AsyncStream, info: ImageInfo = ImageInfo(), debug: Boolean = false): ImageInfo {
1919
val jpegHeader = s.readU16BE()
2020
if (jpegHeader != 0xFFD8) error("Not a JPEG file ${jpegHeader.hex}")
2121
while (!s.eof()) {
@@ -26,7 +26,7 @@ object EXIF {
2626
val ss = s.readStream(sectionSize - 2)
2727
when (sectionType) {
2828
0xFFE1 -> { // APP1
29-
readExif(ss.readAllAsFastStream(), info)
29+
readExif(ss.readAllAsFastStream(), info, debug)
3030
}
3131
0xFFC0 -> { // SOF0
3232
val precision = ss.readU8()
@@ -43,43 +43,53 @@ object EXIF {
4343
error("Couldn't find EXIF information")
4444
}
4545

46-
fun readExif(s: FastByteArrayInputStream, info: ImageInfo = ImageInfo()): ImageInfo {
47-
return runBlockingNoSuspensions { readExif(s.toAsyncStream(), info) }
46+
fun readExif(s: FastByteArrayInputStream, info: ImageInfo = ImageInfo(), debug: Boolean = false): ImageInfo {
47+
return runBlockingNoSuspensions { readExif(s.toAsyncStream(), info, debug) }
4848
}
4949

50-
suspend fun readExif(s: AsyncStream, info: ImageInfo = ImageInfo()): ImageInfo {
50+
suspend fun readExif(s: AsyncStream, info: ImageInfo = ImageInfo(), debug: Boolean = false): ImageInfo {
5151
if (s.readString(4, Charsets.LATIN1) != "Exif") error("Not an Exif section")
5252
s.skip(2)
53-
return readExifBase(s, info)
53+
return readExifBase(s, info, debug)
5454
}
5555

56-
suspend fun readExifBase(ss: AsyncStream, info: ImageInfo = ImageInfo()): ImageInfo {
57-
val endian = if (ss.readString(2, Charsets.LATIN1) == "MM") Endian.BIG_ENDIAN else Endian.LITTLE_ENDIAN
56+
// @TODO: We are missing some data from other FIDs, but for orientation it should be on the first FID
57+
suspend fun readExifBase(ss: AsyncStream, info: ImageInfo = ImageInfo(), debug: Boolean = false): ImageInfo {
5858
val s = ss.sliceHere()
59+
val endian = when (val start = s.readString(2, Charsets.LATIN1)) {
60+
"MM" -> Endian.BIG_ENDIAN
61+
"II" -> Endian.LITTLE_ENDIAN
62+
else -> error("Not Exif data (not starting with MM or II but '$start')")
63+
}
5964
val tagMark = s.readU16(endian)
60-
val offsetFirstFID = s.readS32(endian) // @TODO: do we need to use this somehow?
65+
val offsetFirstIFD = s.readS32(endian) // @TODO: do we need to use this somehow?
6166

6267
val nDirEntry = s.readU16(endian)
63-
//Console.error("nDirEntry=$nDirEntry, tagMark=$tagMark, offsetFirstFID=$offsetFirstFID")
68+
if (debug) {
69+
Console.error("nDirEntry=$nDirEntry, tagMark=$tagMark, offsetFirstFID=$offsetFirstIFD")
70+
}
6471
for (n in 0 until nDirEntry) {
72+
val tagPos = s.position.toInt()
6573
val tagNumber = s.readU16(endian)
66-
val dataFormat = s.readU16(endian)
74+
val dataFormat = DataFormat[s.readU16(endian)]
6775
val nComponent = s.readS32(endian)
68-
//Console.error("tagNumber=$tagNumber, dataFormat=$dataFormat, nComponent=$nComponent")
69-
val values = when (dataFormat) {
70-
DataFormat.UBYTE.id, DataFormat.SBYTE.id -> s.readBytesExact(nComponent).mapDouble { it.toDouble() }
71-
DataFormat.STRING.id -> s.readBytesExact(nComponent).mapDouble { it.toDouble() }
72-
DataFormat.UNDEFINED.id -> s.readBytesExact(nComponent).mapDouble { it.toDouble() }
73-
DataFormat.USHORT.id, DataFormat.SSHORT.id -> s.readShortArray(nComponent, endian).mapDouble { it.toDouble() }
74-
DataFormat.ULONG.id, DataFormat.SLONG.id -> s.readIntArray(nComponent, endian).mapDouble { it.toDouble() }
75-
DataFormat.SFLOAT.id -> s.readFloatArray(nComponent, endian).mapDouble { it.toDouble() }
76-
DataFormat.DFLOAT.id -> s.readIntArray(nComponent, endian).mapDouble { it.toDouble() } // These are offsets to the 8-byte structure (DWORD/DWORD)
77-
DataFormat.URATIO.id, DataFormat.SRATIO.id -> s.readIntArray(nComponent, endian).mapDouble { it.toDouble() } // These are offsets to the 8-byte structure (DWORD/DWORD)
78-
else -> error("Invalid data type: ${dataFormat.hex}")
76+
if (debug) {
77+
Console.error("tagPos=${tagPos.hex}, tagNumber=${tagNumber.hex}, dataFormat=$dataFormat, nComponent=$nComponent, size=${dataFormat.indexBytes(nComponent)}")
7978
}
79+
val data: ByteArray = s.readBytesExact(dataFormat.indexBytes(nComponent))
80+
81+
fun readUShort(index: Int): Int = data.readU16(index * 2, little = endian.isLittle)
82+
fun readInt(index: Int): Int = data.readS32(index * 4, little = endian.isLittle)
83+
84+
if (debug) {
85+
if (dataFormat == DataFormat.STRING) {
86+
Console.error(" - '${s.sliceStart(readInt(0).toLong()).readStringz(nComponent)}'")
87+
}
88+
}
89+
8090
when (tagNumber) {
8191
0x112 -> { // Orientation
82-
info.orientation = when (values[0].toInt()) {
92+
info.orientation = when (readUShort(0)) {
8393
1 -> ImageOrientation.ORIGINAL
8494
2 -> ImageOrientation.MIRROR_HORIZONTAL
8595
3 -> ImageOrientation.ROTATE_180
@@ -93,27 +103,33 @@ object EXIF {
93103
}
94104
}
95105
//AsyncStream().skipToAlign()
96-
s.skipToAlign(4)
106+
s.skipToAlign(4, offset = 2)
97107
}
98108
return info
99109
}
100110

101111
enum class DataFormat(
102112
val id: Int,
103-
val nBytes: Int,
104-
val rBytes: Int = nBytes
113+
val indexBytes: (Int) -> Int,
105114
) {
106-
UBYTE(1, 1),
107-
STRING(2, 1),
108-
USHORT(3, 2),
109-
ULONG(4, 3),
110-
URATIO(5, 4, 8),
111-
SBYTE(6, 1),
112-
UNDEFINED(7, 1),
113-
SSHORT(8, 2),
114-
SLONG(9, 4),
115-
SRATIO(10, 4, 8),
116-
SFLOAT(11, 4),
117-
DFLOAT(12, 8);
115+
UBYTE(1, { it }),
116+
STRING(2, { 4 }),
117+
USHORT(3, { it * 2 }),
118+
ULONG(4, { it * 4 }),
119+
URATIO(5, { it * 4 }),
120+
SBYTE(6, { it }),
121+
UNDEFINED(7, { it }),
122+
SSHORT(8, { it * 2 }),
123+
SLONG(9, { it * 4 }),
124+
SRATIO(10, { it * 4 }),
125+
SFLOAT(11, { it * 4 }),
126+
DFLOAT(12, { it * 8 }),
127+
UNKNOWN(-1, { 0 });
128+
129+
companion object {
130+
val BY_ID = values().associateBy { it.id }
131+
operator fun get(index: Int): DataFormat =
132+
BY_ID[index] ?: UNKNOWN
133+
}
118134
}
119135
}

korim/src/commonMain/kotlin/com/soywiz/korim/format/ImageData.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ open class ImageData constructor(
4444

4545
data class ImageDataWithAtlas(val image: ImageData, val atlas: AtlasPacker.Result<ImageFrameLayer>)
4646

47+
var ImageData.info: ImageInfo? by Extra.Property { null }
48+
4749
//fun ImageData.packInAtlas(): ImageDataWithAtlas {
4850
// val frameLayers = frames.flatMap { it.layerData }.filter { it.includeInAtlas }
4951
// val atlasResult = AtlasPacker.pack(frameLayers.map { it to it.slice })

korim/src/commonMain/kotlin/com/soywiz/korim/format/ImageFormat.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ data class ImageDecodingProps(
9090
val premultiplied: Boolean = true,
9191
// Requested but not enforced. Max width and max height
9292
val requestedMaxSize: Int? = null,
93+
val debug: Boolean = false,
9394
override var extra: ExtraType = null
9495
) : Extra {
9596

0 commit comments

Comments
 (0)