Skip to content

Commit

Permalink
Updated XMP Core & fixed offset reported in BMFF boxes (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanOltmann authored Jan 12, 2024
1 parent 5c0150c commit d1f8b68
Show file tree
Hide file tree
Showing 37 changed files with 348 additions and 163 deletions.
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ of Ashampoo Photos, which, in turn, is driven by user community feedback.
## Installation

```
implementation("com.ashampoo:kim:0.9.1")
implementation("com.ashampoo:kim:0.9.2")
```

## Sample usages
Expand Down Expand Up @@ -142,17 +142,14 @@ val newBytes = Kim.updateThumbnail(
* Inability to update EXIF, IPTC and XMP in JPG files simultaneously.
* HEIC files are only tested for iPhone SE 3 & Samsung Galaxy S21.

## Regarding HEIC
### Regarding missing HEIC metadata

When handling ISO base media file format (ISOBMFF) files like HEIC,
our adherence to the EIC/ISO 14496-12 specification.

For example we intentionally omit certain features specified in the HEIC
standard, such as image size ("ispe") and rotation ("irot"), to steer clear
of potential legal complications.

In future updates, we plan to integrate special values from other
patent-free ISOBMFF-based formats as we broaden support for additional file types.
In the processing of HEIC files, we handle them as standard ISO base
media file format (ISOBMFF) files, adhering rigorously to the
EIC/ISO 14496-12 specification. To preempt potential legal issues,
we intentionally omit certain features outlined in the HEIC specification,
notably the image size ("ispe") and image rotation ("irot") boxes.
This precautionary approach extends to AVIF images, as they repurpose these same boxes.

## Contributions

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ repositories {
val productName = "Ashampoo Kim"

val ktorVersion: String = "2.3.7"
val xmpCoreVersion: String = "0.3"
val xmpCoreVersion: String = "1.0.0"
val dateTimeVersion: String = "0.5.0"
val testRessourcesVersion: String = "0.4.0"
val ioCoreVersion: String = "0.3.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ object ImageFormatMagicNumbers {
null, null, null, null
).plus("ftyphevx".encodeToByteArray().toList())

val jxlCodeStream: List<Byte> = byteListOf(
0xFF, 0x0A
)

val jxlContainer: List<Byte> = byteListOf(
0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A
)

private fun byteListOf(vararg ints: Int?): List<Byte?> =
ints.map { it?.toByte() }

Expand Down
5 changes: 3 additions & 2 deletions src/commonMain/kotlin/com/ashampoo/kim/format/ImageParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package com.ashampoo.kim.format

import com.ashampoo.kim.common.ImageReadException
import com.ashampoo.kim.format.isobmff.ISOBMFFImageParser
import com.ashampoo.kim.format.bmff.BaseMediaFileFormatImageParser
import com.ashampoo.kim.format.jpeg.JpegImageParser
import com.ashampoo.kim.format.png.PngImageParser
import com.ashampoo.kim.format.raf.RafImageParser
Expand Down Expand Up @@ -47,7 +47,8 @@ fun interface ImageParser {

ImageFormat.RAF -> RafImageParser

ImageFormat.HEIC -> ISOBMFFImageParser
ImageFormat.HEIC -> BaseMediaFileFormatImageParser
// ImageFormat.JXL -> BaseMediaFileFormatImageParser

else -> null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ashampoo.kim.format.isobmff
package com.ashampoo.kim.format.bmff

import com.ashampoo.kim.common.ByteOrder

object ISOBMFFConstants {
object BMFFConstants {

val BMFF_BYTE_ORDER = ByteOrder.BIG_ENDIAN

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ashampoo.kim.format.isobmff
package com.ashampoo.kim.format.bmff

import com.ashampoo.kim.common.ImageReadException
import com.ashampoo.kim.common.MetadataOffset
import com.ashampoo.kim.common.MetadataType
import com.ashampoo.kim.format.ImageMetadata
import com.ashampoo.kim.format.ImageParser
import com.ashampoo.kim.format.isobmff.ISOBMFFConstants.BMFF_BYTE_ORDER
import com.ashampoo.kim.format.isobmff.ISOBMFFConstants.TIFF_HEADER_OFFSET_BYTE_COUNT
import com.ashampoo.kim.format.isobmff.boxes.MetaBox
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.boxes.MetaBox
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
* as defined in ISO/IEC 14496-12. Examples for these are MP4, HEIC & JPEG XL.
*
* https://en.wikipedia.org/wiki/ISO_base_media_file_format
*/
object ISOBMFFImageParser : ImageParser {
object BaseMediaFileFormatImageParser : ImageParser {

override fun parseMetadata(byteReader: ByteReader): ImageMetadata =
parseMetadata(PositionTrackingByteReaderDecorator(byteReader))
Expand All @@ -47,7 +46,8 @@ object ISOBMFFImageParser : ImageParser {

val allBoxes = BoxReader.readBoxes(
byteReader = copyByteReader,
stopAfterMetaBox = true
stopAfterMetaBox = true,
offsetShift = 0
)

val metaBox = allBoxes.find { it.type == BoxType.META } as? MetaBox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ashampoo.kim.format.isobmff

import com.ashampoo.kim.format.isobmff.ISOBMFFConstants.BMFF_BYTE_ORDER
import com.ashampoo.kim.format.isobmff.boxes.Box
import com.ashampoo.kim.format.isobmff.boxes.FileTypeBox
import com.ashampoo.kim.format.isobmff.boxes.HandlerReferenceBox
import com.ashampoo.kim.format.isobmff.boxes.ItemInfoEntryBox
import com.ashampoo.kim.format.isobmff.boxes.ItemInformationBox
import com.ashampoo.kim.format.isobmff.boxes.ItemLocationBox
import com.ashampoo.kim.format.isobmff.boxes.MediaDataBox
import com.ashampoo.kim.format.isobmff.boxes.MetaBox
import com.ashampoo.kim.format.isobmff.boxes.PrimaryItemBox
package com.ashampoo.kim.format.bmff

import com.ashampoo.kim.format.bmff.BMFFConstants.BMFF_BYTE_ORDER
import com.ashampoo.kim.format.bmff.boxes.Box
import com.ashampoo.kim.format.bmff.boxes.FileTypeBox
import com.ashampoo.kim.format.bmff.boxes.HandlerReferenceBox
import com.ashampoo.kim.format.bmff.boxes.ItemInfoEntryBox
import com.ashampoo.kim.format.bmff.boxes.ItemInformationBox
import com.ashampoo.kim.format.bmff.boxes.ItemLocationBox
import com.ashampoo.kim.format.bmff.boxes.MediaDataBox
import com.ashampoo.kim.format.bmff.boxes.MetaBox
import com.ashampoo.kim.format.bmff.boxes.PrimaryItemBox
import com.ashampoo.kim.input.PositionTrackingByteReader

/**
Expand All @@ -41,7 +41,8 @@ object BoxReader {
*/
fun readBoxes(
byteReader: PositionTrackingByteReader,
stopAfterMetaBox: Boolean = false
stopAfterMetaBox: Boolean = false,
offsetShift: Long = 0
): List<Box> {

val boxes = mutableListOf<Box>()
Expand All @@ -52,7 +53,7 @@ object BoxReader {
* Check if there are enough bytes for another box.
* If so, we at least need the 8 header bytes.
*/
if (byteReader.available < ISOBMFFConstants.BOX_HEADER_LENGTH)
if (byteReader.available < BMFFConstants.BOX_HEADER_LENGTH)
break

val offset: Long = byteReader.position.toLong()
Expand All @@ -62,7 +63,7 @@ object BoxReader {
byteReader.read4BytesAsInt("length", BMFF_BYTE_ORDER).toLong()

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

val actualLength: Long = when (length) {
Expand All @@ -83,16 +84,18 @@ object BoxReader {

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

val globalOffset = offset + offsetShift

val box = when (type) {
BoxType.FTYP -> FileTypeBox(offset, actualLength, bytes)
BoxType.META -> MetaBox(offset, actualLength, bytes)
BoxType.HDLR -> HandlerReferenceBox(offset, actualLength, bytes)
BoxType.IINF -> ItemInformationBox(offset, actualLength, bytes)
BoxType.INFE -> ItemInfoEntryBox(offset, actualLength, bytes)
BoxType.ILOC -> ItemLocationBox(offset, actualLength, bytes)
BoxType.PITM -> PrimaryItemBox(offset, actualLength, bytes)
BoxType.MDAT -> MediaDataBox(offset, actualLength, bytes)
else -> Box(offset, type, actualLength, bytes)
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)
else -> Box(globalOffset, type, actualLength, bytes)
}

boxes.add(box)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ashampoo.kim.format.isobmff
package com.ashampoo.kim.format.bmff

import com.ashampoo.kim.common.toFourCCTypeString

Expand Down Expand Up @@ -96,7 +96,7 @@ data class BoxType internal constructor(
@Suppress("MagicNumber")
fun of(typeBytes: ByteArray): BoxType {

require(typeBytes.size == ISOBMFFConstants.TPYE_LENGTH) {
require(typeBytes.size == BMFFConstants.TPYE_LENGTH) {
"BoxType must be always 4 bytes!"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ashampoo.kim.format.isobmff
package com.ashampoo.kim.format.bmff

import com.ashampoo.kim.input.PositionTrackingByteReader
import com.ashampoo.kim.output.ByteArrayByteWriter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ashampoo.kim.format.isobmff
package com.ashampoo.kim.format.bmff

data class Extent(
val itemId: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ashampoo.kim.format.isobmff.boxes
package com.ashampoo.kim.format.bmff.boxes

import com.ashampoo.kim.format.isobmff.BoxType
import com.ashampoo.kim.format.bmff.BoxType

open class Box(
val offset: Long,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2024 Ashampoo GmbH & Co. KG
* Copyright 2002-2023 Drew Noakes and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ashampoo.kim.format.bmff.boxes

interface BoxContainer {

val boxes: List<Box>

companion object {

fun findAllBoxesRecursive(boxes: List<Box>): List<Box> {

val allBoxes = mutableListOf<Box>()

for (box in boxes) {

allBoxes.add(box)

if (box is BoxContainer)
allBoxes.addAll(findAllBoxesRecursive(box.boxes))
}

return allBoxes
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ashampoo.kim.format.isobmff.boxes
package com.ashampoo.kim.format.bmff.boxes

import com.ashampoo.kim.common.toFourCCTypeString
import com.ashampoo.kim.format.isobmff.BoxType
import com.ashampoo.kim.format.isobmff.ISOBMFFConstants
import com.ashampoo.kim.format.isobmff.ISOBMFFConstants.BMFF_BYTE_ORDER
import com.ashampoo.kim.format.bmff.BMFFConstants
import com.ashampoo.kim.format.bmff.BMFFConstants.BMFF_BYTE_ORDER
import com.ashampoo.kim.format.bmff.BoxType
import com.ashampoo.kim.input.ByteArrayByteReader

class FileTypeBox(
Expand All @@ -34,9 +34,6 @@ class FileTypeBox(

val compatibleBrands: List<String>

override fun toString(): String =
"$type major=$majorBrand minor=$minorBrand compatible=$compatibleBrands"

init {

val byteReader = ByteArrayByteReader(payload)
Expand All @@ -56,11 +53,14 @@ class FileTypeBox(
repeat(brandCount) {
brands.add(
byteReader
.read4BytesAsInt("brand $it", ISOBMFFConstants.BMFF_BYTE_ORDER)
.read4BytesAsInt("brand $it", BMFFConstants.BMFF_BYTE_ORDER)
.toFourCCTypeString()
)
}

compatibleBrands = brands
}

override fun toString(): String =
"$type major=$majorBrand minor=$minorBrand compatible=$compatibleBrands"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ashampoo.kim.format.isobmff.boxes
package com.ashampoo.kim.format.bmff.boxes

import com.ashampoo.kim.common.toHex
import com.ashampoo.kim.format.isobmff.BoxType
import com.ashampoo.kim.format.bmff.BoxType
import com.ashampoo.kim.input.ByteArrayByteReader

class HandlerReferenceBox(
Expand All @@ -34,13 +34,6 @@ class HandlerReferenceBox(

val name: String

override fun toString(): String =
"$type " +
"version=$version " +
"flags=${flags.toHex()} " +
"handlerType=$handlerType " +
"name=$name"

init {

val byteReader = ByteArrayByteReader(payload)
Expand All @@ -57,4 +50,11 @@ class HandlerReferenceBox(

name = byteReader.readNullTerminatedString("name")
}

override fun toString(): String =
"$type " +
"version=$version " +
"flags=${flags.toHex()} " +
"handlerType=$handlerType " +
"name=$name"
}
Loading

0 comments on commit d1f8b68

Please sign in to comment.