Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BMFF refactoring: Removed PositionTrackingByteReader #67

Merged
merged 8 commits into from
Jan 28, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ object BMFFConstants {

val BMFF_BYTE_ORDER = ByteOrder.BIG_ENDIAN

/* BoxType must be always 4 bytes */
/** BoxType must be always 4 bytes */
const val TPYE_LENGTH = 4

/* 4 length bytes + 4 type bytes */
const val BOX_HEADER_LENGTH = 8
/** The size is presented as unsinged integer */
const val SIZE_LENGTH = 4

/** 4 size bytes + 4 type bytes */
const val BOX_HEADER_LENGTH = TPYE_LENGTH + SIZE_LENGTH

const val TIFF_HEADER_OFFSET_BYTE_COUNT = 4

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ import com.ashampoo.kim.format.bmff.BMFFConstants.BMFF_BYTE_ORDER
import com.ashampoo.kim.format.bmff.BMFFConstants.TIFF_HEADER_OFFSET_BYTE_COUNT
import com.ashampoo.kim.format.bmff.box.FileTypeBox
import com.ashampoo.kim.format.bmff.box.MetaBox
import com.ashampoo.kim.format.jxl.JxlReader
import com.ashampoo.kim.format.tiff.TiffReader
import com.ashampoo.kim.input.ByteArrayByteReader
import com.ashampoo.kim.input.ByteReader
import com.ashampoo.kim.input.PositionTrackingByteReader
import com.ashampoo.kim.input.PositionTrackingByteReaderDecorator

/**
* Reads containers that follow the ISO base media file format
Expand All @@ -39,17 +38,18 @@ import com.ashampoo.kim.input.PositionTrackingByteReaderDecorator
*/
object BaseMediaFileFormatImageParser : ImageParser {

override fun parseMetadata(byteReader: ByteReader): ImageMetadata =
parseMetadata(PositionTrackingByteReaderDecorator(byteReader))

private fun parseMetadata(byteReader: PositionTrackingByteReader): ImageMetadata {
override fun parseMetadata(byteReader: ByteReader): ImageMetadata {

val copyByteReader = CopyByteReader(byteReader)

var position: Long = 0

val allBoxes = BoxReader.readBoxes(
byteReader = copyByteReader,
stopAfterMetadataRead = true,
offsetShift = 0
positionOffset = 0,
offsetShift = 0,
updatePosition = { position = it }
)

if (allBoxes.isEmpty())
Expand All @@ -66,7 +66,7 @@ object BaseMediaFileFormatImageParser : ImageParser {
* This format has EXIF & XMP neatly in dedicated boxes, so we can just extract these.
*/
if (fileTypeBox.majorBrand == FileTypeBox.JXL_BRAND)
return JxlHandler.createMetadata(allBoxes)
return JxlReader.createMetadata(allBoxes)

val metaBox = allBoxes.filterIsInstance<MetaBox>().firstOrNull()

Expand Down Expand Up @@ -98,26 +98,35 @@ object BaseMediaFileFormatImageParser : ImageParser {
* in buffer and input everything we read so far in again.
* FIXME There must be a better solution. Find it.
*/
val byteReaderToUse = if (byteReader.position <= minOffset)
val onPositionBeforeMinimumOffset = position <= minOffset

val byteReaderToUse = if (onPositionBeforeMinimumOffset)
byteReader
else
ByteArrayByteReader(copyByteReader.getBytes())

if (!onPositionBeforeMinimumOffset)
position = 0

var exifBytes: ByteArray? = null
var xmp: String? = null

for (offset in metadataOffsets) {

when (offset.type) {

MetadataType.EXIF ->
exifBytes = readExifBytes(byteReaderToUse, offset)
MetadataType.EXIF -> {
exifBytes = readExifBytes(byteReaderToUse, position, offset)
position = offset.offset + offset.length
}

MetadataType.IPTC ->
continue // Unsupported

MetadataType.XMP ->
xmp = readXmpString(byteReaderToUse, offset)
MetadataType.XMP -> {
xmp = readXmpString(byteReaderToUse, position, offset)
position = offset.offset + offset.length
}
}
}

Expand All @@ -134,11 +143,16 @@ object BaseMediaFileFormatImageParser : ImageParser {
}

private fun readExifBytes(
byteReader: PositionTrackingByteReader,
byteReader: ByteReader,
position: Long,
offset: MetadataOffset
): ByteArray {

val bytesToSkip = offset.offset - byteReader.position
val bytesToSkip = offset.offset - position

check(bytesToSkip >= 0) {
"Position must be before extent offset: position=$position offset=$offset"
}

byteReader.skipBytes("offset to EXIF extent", bytesToSkip.toInt())

Expand All @@ -148,17 +162,23 @@ object BaseMediaFileFormatImageParser : ImageParser {
/* Usualy there are 6 bytes skipped, which are the EXIF header. ("Exif.."). */
byteReader.skipBytes("offset to TIFF header", tiffHeaderOffset)

return byteReader.readBytes(
val exifBytesLength =
offset.length.toInt() - TIFF_HEADER_OFFSET_BYTE_COUNT - tiffHeaderOffset
)

return byteReader.readBytes(exifBytesLength)
}

private fun readXmpString(
byteReader: PositionTrackingByteReader,
byteReader: ByteReader,
position: Long,
offset: MetadataOffset
): String {

val bytesToSkip = offset.offset - byteReader.position
val bytesToSkip = offset.offset - position

check(bytesToSkip >= 0) {
"Position must be before extent offset: position=$position offset=$offset"
}

byteReader.skipBytes("offset to MIME extent", bytesToSkip.toInt())

Expand Down
74 changes: 48 additions & 26 deletions src/commonMain/kotlin/com/ashampoo/kim/format/bmff/BoxReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ package com.ashampoo.kim.format.bmff

import com.ashampoo.kim.format.bmff.BMFFConstants.BMFF_BYTE_ORDER
import com.ashampoo.kim.format.bmff.box.Box
import com.ashampoo.kim.format.bmff.box.ExifBox
import com.ashampoo.kim.format.bmff.box.FileTypeBox
import com.ashampoo.kim.format.bmff.box.HandlerReferenceBox
import com.ashampoo.kim.format.bmff.box.ItemInfoEntryBox
import com.ashampoo.kim.format.bmff.box.ItemInformationBox
import com.ashampoo.kim.format.bmff.box.ItemLocationBox
import com.ashampoo.kim.format.bmff.box.JxlParticalCodestreamBox
import com.ashampoo.kim.format.bmff.box.MediaDataBox
import com.ashampoo.kim.format.bmff.box.MetaBox
import com.ashampoo.kim.format.bmff.box.PrimaryItemBox
import com.ashampoo.kim.format.bmff.box.XmlBox
import com.ashampoo.kim.input.PositionTrackingByteReader
import com.ashampoo.kim.format.jxl.box.ExifBox
import com.ashampoo.kim.format.jxl.box.JxlParticalCodestreamBox
import com.ashampoo.kim.format.jxl.box.XmlBox
import com.ashampoo.kim.input.ByteReader

/**
* Reads ISOBMFF boxes
Expand All @@ -43,74 +43,94 @@ object BoxReader {
* For iPhone HEIC this is possible, but Samsung HEIC has "meta" coming after "mdat"
*/
fun readBoxes(
byteReader: PositionTrackingByteReader,
byteReader: ByteReader,
stopAfterMetadataRead: Boolean = false,
offsetShift: Long = 0
positionOffset: Long = 0,
offsetShift: Long = 0,
updatePosition: ((Long) -> Unit)? = null
): List<Box> {

var haveSeenJxlHeaderBox: Boolean = false

val boxes = mutableListOf<Box>()

var position: Long = positionOffset

while (true) {

val available = byteReader.contentLength - position

/*
* Check if there are enough bytes for another box.
* If so, we at least need the 8 header bytes.
*/
if (byteReader.available < BMFFConstants.BOX_HEADER_LENGTH)
if (available < BMFFConstants.BOX_HEADER_LENGTH)
break

val offset: Long = byteReader.position.toLong()
val offset: Long = position

/* Note: The length includes the 8 header bytes. */
val length: Long =
val size: Long =
byteReader.read4BytesAsInt("length", BMFF_BYTE_ORDER).toLong()

val type = BoxType.of(
byteReader.readBytes("type", BMFFConstants.TPYE_LENGTH)
)

position += BMFFConstants.BOX_HEADER_LENGTH

/*
* If we read an JXL file and we already have seen the header,
* all reamining JXLP boxes are image data that we can skip.
*/
if (stopAfterMetadataRead && type == BoxType.JXLP && haveSeenJxlHeaderBox)
break

val actualLength: Long = when (length) {
var largeSize: Long? = null

val actualLength: Long = when (size) {

/* A vaule of zero indicates that it's the last box. */
0L -> byteReader.available
0L -> available

/* A length of 1 indicates that we should read the next 8 bytes to get a long value. */
1L -> byteReader.read8BytesAsLong("length", BMFF_BYTE_ORDER)
1L -> {
largeSize = byteReader.read8BytesAsLong("length", BMFF_BYTE_ORDER)
largeSize
}

/* Keep the length we already read. */
else -> length
else -> size
}

val nextBoxOffset = offset + actualLength

val remainingBytesToReadInThisBox = (nextBoxOffset - byteReader.position).toInt()
@Suppress("MagicNumber")
if (size == 1L)
position += 8

val remainingBytesToReadInThisBox = (nextBoxOffset - position).toInt()

val bytes = byteReader.readBytes("data", remainingBytesToReadInThisBox)

position += remainingBytesToReadInThisBox

val globalOffset = offset + offsetShift

val box = when (type) {
BoxType.FTYP -> FileTypeBox(globalOffset, actualLength, bytes)
BoxType.META -> MetaBox(globalOffset, actualLength, bytes)
BoxType.HDLR -> HandlerReferenceBox(globalOffset, actualLength, bytes)
BoxType.IINF -> ItemInformationBox(globalOffset, actualLength, bytes)
BoxType.INFE -> ItemInfoEntryBox(globalOffset, actualLength, bytes)
BoxType.ILOC -> ItemLocationBox(globalOffset, actualLength, bytes)
BoxType.PITM -> PrimaryItemBox(globalOffset, actualLength, bytes)
BoxType.MDAT -> MediaDataBox(globalOffset, actualLength, bytes)
BoxType.EXIF -> ExifBox(globalOffset, actualLength, bytes)
BoxType.XML -> XmlBox(globalOffset, actualLength, bytes)
BoxType.JXLP -> JxlParticalCodestreamBox(globalOffset, actualLength, bytes)
else -> Box(type, globalOffset, actualLength, bytes)
BoxType.FTYP -> FileTypeBox(globalOffset, size, largeSize, bytes)
BoxType.META -> MetaBox(globalOffset, size, largeSize, bytes)
BoxType.HDLR -> HandlerReferenceBox(globalOffset, size, largeSize, bytes)
BoxType.IINF -> ItemInformationBox(globalOffset, size, largeSize, bytes)
BoxType.INFE -> ItemInfoEntryBox(globalOffset, size, largeSize, bytes)
BoxType.ILOC -> ItemLocationBox(globalOffset, size, largeSize, bytes)
BoxType.PITM -> PrimaryItemBox(globalOffset, size, largeSize, bytes)
BoxType.MDAT -> MediaDataBox(globalOffset, size, largeSize, bytes)
/* JXL boxes */
BoxType.EXIF -> ExifBox(globalOffset, size, largeSize, bytes)
BoxType.XML -> XmlBox(globalOffset, size, largeSize, bytes)
BoxType.JXLP -> JxlParticalCodestreamBox(globalOffset, size, largeSize, bytes)
else -> Box(type, globalOffset, size, largeSize, bytes)
}

boxes.add(box)
Expand All @@ -135,6 +155,8 @@ object BoxReader {
}
}

updatePosition?.let { it(position) }

return boxes
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,18 @@
*/
package com.ashampoo.kim.format.bmff

import com.ashampoo.kim.input.PositionTrackingByteReader
import com.ashampoo.kim.input.ByteReader
import com.ashampoo.kim.output.ByteArrayByteWriter

internal class CopyByteReader(
val byteReader: PositionTrackingByteReader
) : PositionTrackingByteReader {
val byteReader: ByteReader
) : ByteReader {

private val byteWriter = ByteArrayByteWriter()

override val contentLength: Long =
byteReader.contentLength

override val position: Int
get() = byteReader.position

override val available: Long
get() = byteReader.available

fun getBytes(): ByteArray =
byteWriter.toByteArray()

Expand Down
22 changes: 19 additions & 3 deletions src/commonMain/kotlin/com/ashampoo/kim/format/bmff/box/Box.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,32 @@
*/
package com.ashampoo.kim.format.bmff.box

import com.ashampoo.kim.format.bmff.BMFFConstants.BOX_HEADER_LENGTH
import com.ashampoo.kim.format.bmff.BoxType

open class Box(
val type: BoxType,
val offset: Long,
val length: Long,
/* Payload bytes, not including type & length bytes */
val size: Long,
val largeSize: Long?,
/** Payload bytes, not including type & length bytes */
val payload: ByteArray
) {

/*
* "size" is an integer that specifies the number of bytes in this box,
* including all its fields and contained boxes; if size is 1 then the
* actual size is in the field largesize; if size is 0, then this
* box is the last one in the file, and its contents extend to the
* end of the file (normally only used for a Media Data Box)
*/
val actualLength: Long =
when (size) {
0L -> BOX_HEADER_LENGTH.toLong() + payload.size
1L -> largeSize!!
else -> size
}

override fun toString(): String =
"Box '$type' @ $offset ($length bytes)"
"Box '$type' @ $offset ($actualLength bytes)"
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.ashampoo.kim.format.bmff.box
import com.ashampoo.kim.common.toFourCCTypeString
import com.ashampoo.kim.format.bmff.BMFFConstants
import com.ashampoo.kim.format.bmff.BMFFConstants.BMFF_BYTE_ORDER
import com.ashampoo.kim.format.bmff.BMFFConstants.BOX_HEADER_LENGTH
import com.ashampoo.kim.format.bmff.BoxType
import com.ashampoo.kim.input.ByteArrayByteReader

Expand All @@ -27,9 +28,10 @@ import com.ashampoo.kim.input.ByteArrayByteReader
*/
class FileTypeBox(
offset: Long,
length: Long,
size: Long,
largeSize: Long?,
payload: ByteArray
) : Box(BoxType.FTYP, offset, length, payload) {
) : Box(BoxType.FTYP, offset, size, largeSize, payload) {

val majorBrand: String

Expand All @@ -49,7 +51,7 @@ class FileTypeBox(
.read4BytesAsInt("minorBrand", BMFF_BYTE_ORDER)
.toFourCCTypeString()

val brandCount: Int = (length.toInt() - 8 - 8) / 4
val brandCount: Int = (actualLength.toInt() - BOX_HEADER_LENGTH - 8 - 8) / 4

val brands = mutableListOf<String>()

Expand Down
Loading
Loading