Skip to content

Commit

Permalink
Refactorings & MakerNote support preparations (#64)
Browse files Browse the repository at this point in the history
* Refactored APIs for more streamlined code
* Improved RAW support
* Prepared MakerNote support
  • Loading branch information
StefanOltmann authored Jan 26, 2024
1 parent 16c03b8 commit adf017b
Show file tree
Hide file tree
Showing 158 changed files with 2,478 additions and 6,506 deletions.
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ It's part of [Ashampoo Photos](https://ashampoo.com/photos).
* WebP: Read EXIF & XMP
* HEIC / AVIF: Read EXIF & XMP
* JPEG XL: Read EXIF & XMP of uncompressed files
* TIFF / DNG / RAW: Read EXIF & XMP
+ Good support for Canon CR2, Fujifilm RAF & Adobe DNG
+ Experimental support for NEF, ARW, RW2 & ORF with known issues
+ API for preview image extraction of DNG, CR2, RAF, NEF, ARW & RW2 (but not ORF)
* TIFF / RAW: Read EXIF & XMP
+ Full support for Adobe DNG, Canon CR2 & Fujifilm RAF
+ Support for Nikon NEF, Sony ARW & Olympus ORF without lens info
+ Support for Panasonic RW2 without lens info and image size
+ API for preview image extraction of DNG, CR2, RAF, NEF, ARW & RW2
* Handling of XMP content through
[XMP Core for Kotlin Multiplatform](https://github.com/Ashampoo/xmpcore)
* Convenient `Kim.update()` API to perform updates to the relevant places
Expand All @@ -36,7 +37,7 @@ of Ashampoo Photos, which, in turn, is driven by user community feedback.
## Installation

```
implementation("com.ashampoo:kim:0.11")
implementation("com.ashampoo:kim:0.12")
```

## Sample usages
Expand Down Expand Up @@ -141,8 +142,12 @@ val newBytes = Kim.updateThumbnail(
## Limitations

* Inability to update EXIF, IPTC and XMP in JPG files simultaneously.
* Does not read the image size and orientation for HEIC, AVIF & JPEG XL
* Does not read brotli compressed metadata of JPEG XL due to missing brotli KMP libs
* Does not read the image size and orientation for HEIC, AVIF & JPEG XL.
* Does not read brotli compressed metadata of JPEG XL due to missing brotli KMP libs.
* MakerNote support is experimental and limited.
+ Can't extract preview image of ORF as offsets are burried into MakerNote.
+ Can't identify lens info of NEF, ARW, RW2 & ORF because this is constructed from MakerNote fields.
+ Missing image size for RW2 as this is also burried in MakerNotes.

### Regarding HEIC & AVIF metadata

Expand Down
16 changes: 8 additions & 8 deletions src/commonMain/kotlin/com/ashampoo/kim/common/ByteConversions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -187,19 +187,19 @@ fun RationalNumber.toBytes(byteOrder: ByteOrder): ByteArray {
return result
}

fun Array<out RationalNumber>.toBytes(byteOrder: ByteOrder): ByteArray =
this.toBytes(0, size, byteOrder)
fun RationalNumbers.toBytes(byteOrder: ByteOrder): ByteArray =
this.toBytes(0, values.size, byteOrder)

private fun Array<out RationalNumber>.toBytes(
private fun RationalNumbers.toBytes(
offset: Int,
length: Int,
byteOrder: ByteOrder
): ByteArray {

val result = ByteArray(length * 8)

for (i in 0 until length)
this[offset + i].toBytes(byteOrder, result, i * 8)
for (index in 0 until length)
values[offset + index].toBytes(byteOrder, result, index * 8)

return result
}
Expand Down Expand Up @@ -402,7 +402,7 @@ private fun ByteArray.toRational(
fun ByteArray.toRationals(
byteOrder: ByteOrder,
unsignedType: Boolean
): Array<RationalNumber> =
): RationalNumbers =
toRationals(this, 0, size, byteOrder, unsignedType)

private fun toRationals(
Expand All @@ -411,15 +411,15 @@ private fun toRationals(
length: Int,
byteOrder: ByteOrder,
unsignedType: Boolean
): Array<RationalNumber> {
): RationalNumbers {

val result = arrayOfNulls<RationalNumber>(length / 8)

repeat(result.size) { i ->
result[i] = bytes.toRational(offset + 8 * i, byteOrder, unsignedType)
}

return result as Array<RationalNumber>
return RationalNumbers(result as Array<RationalNumber>)
}

fun Int.quadsToByteArray(): ByteArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ fun ImageMetadata.convertToPhotoMetadata(
val lensMake = findStringValue(ExifTag.EXIF_TAG_LENS_MAKE)
val lensModel = findStringValue(ExifTag.EXIF_TAG_LENS_MODEL)

/* Look for ISO at the standard place and fall back to test RW2 logic. */
val iso = findShortValue(ExifTag.EXIF_TAG_ISO)
?: findShortValue(ExifTag.EXIF_TAG_ISO_PANASONIC)

val exposureTime = findDoubleValue(ExifTag.EXIF_TAG_EXPOSURE_TIME)
val fNumber = findDoubleValue(ExifTag.EXIF_TAG_FNUMBER)
val focalLength = findDoubleValue(ExifTag.EXIF_TAG_FOCAL_LENGTH)
Expand Down
44 changes: 44 additions & 0 deletions src/commonMain/kotlin/com/ashampoo/kim/common/RationalNumbers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2024 Ashampoo GmbH & Co. KG
*
* 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.common

/**
* To avoid rather unsafe "Array<*>" in instance checking we have this
* extra class to represent a collection of rational numbers.
*/
class RationalNumbers(
val values: Array<RationalNumber>
) {

override fun toString(): String =
values.contentToString()

override fun hashCode(): Int =
values.contentHashCode()

override fun equals(other: Any?): Boolean {

if (this === other)
return true

if (other == null || this::class != other::class)
return false

other as RationalNumbers

return values.contentEquals(other.values)
}
}
29 changes: 9 additions & 20 deletions src/commonMain/kotlin/com/ashampoo/kim/format/ImageMetadata.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ data class ImageMetadata(
val xmp: String?
) {

fun getExifThumbnailBytes(): ByteArray? =
exif?.directories?.asSequence()
?.mapNotNull { it.jpegImageDataElement?.bytes }
?.firstOrNull()

fun findStringValue(tagInfo: TagInfo): String? {

val strings = findTiffField(tagInfo)?.value as? List<String>
Expand All @@ -49,25 +44,19 @@ data class ImageMetadata(
}

fun findShortValue(tagInfo: TagInfo): Short? =
findTiffField(tagInfo)?.value as? Short
findTiffField(tagInfo)?.toShort()

fun findDoubleValue(tagInfo: TagInfo): Double? =
findTiffField(tagInfo)?.toDouble()

/*
* Note: Keep in sync with TiffTags.getTag()
*/
@Suppress("UnnecessaryParentheses")
fun findTiffField(tagInfo: TagInfo): TiffField? {
return exif?.directories?.firstOrNull { directory ->
directory.type == tagInfo.directoryType?.directoryType ||
(tagInfo.directoryType?.isImageDirectory == true && directory.type >= 0) ||
(tagInfo.directoryType?.isImageDirectory == false && directory.type < 0)
}?.findField(tagInfo)
}
fun findTiffField(tagInfo: TagInfo): TiffField? =
exif?.findTiffField(tagInfo)

fun findTiffDirectory(directoryType: Int): TiffDirectory? =
exif?.directories?.find { it.type == directoryType }
exif?.findTiffDirectory(directoryType)

fun getExifThumbnailBytes(): ByteArray? =
exif?.getExifThumbnailBytes()

override fun toString(): String {

Expand All @@ -76,15 +65,15 @@ data class ImageMetadata(
sb.appendLine("Resolution : $imageSize")

if (exif != null)
sb.appendLine(exif)
sb.append(exif)

if (iptc != null)
sb.appendLine(iptc)

if (xmp != null) {

sb.appendLine("---- XMP ----")
sb.appendLine(xmp)
sb.append(xmp)
}

return sb.toString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import com.ashampoo.kim.common.ImageReadException
import com.ashampoo.kim.common.tryWithImageReadException
import com.ashampoo.kim.format.TiffPreviewExtractor
import com.ashampoo.kim.format.tiff.TiffContents
import com.ashampoo.kim.format.tiff.constant.TiffTag
import com.ashampoo.kim.format.tiff.constant.ExifTag
import com.ashampoo.kim.input.RandomAccessByteReader

object Cr2PreviewExtractor : TiffPreviewExtractor {
Expand All @@ -33,10 +33,10 @@ object Cr2PreviewExtractor : TiffPreviewExtractor {
val ifd0 = tiffContents.directories.first()

val previewImageStart =
ifd0.getFieldValue(TiffTag.TIFF_TAG_STRIP_OFFSETS) ?: return null
ifd0.getFieldValue(ExifTag.EXIF_TAG_PREVIEW_IMAGE_START_IFD0) ?: return null

val previewLength =
ifd0.getFieldValue(TiffTag.TIFF_TAG_STRIP_BYTE_COUNTS) ?: return null
ifd0.getFieldValue(ExifTag.EXIF_TAG_PREVIEW_IMAGE_LENGTH_IFD0) ?: return null

if (previewLength == 0)
return null
Expand Down
20 changes: 10 additions & 10 deletions src/commonMain/kotlin/com/ashampoo/kim/format/tiff/GPSInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package com.ashampoo.kim.format.tiff
import com.ashampoo.kim.common.GpsUtil.MINUTES_PER_HOUR
import com.ashampoo.kim.common.GpsUtil.SECONDS_PER_HOUR
import com.ashampoo.kim.common.ImageReadException
import com.ashampoo.kim.common.RationalNumber
import com.ashampoo.kim.common.RationalNumbers
import com.ashampoo.kim.format.tiff.constant.GpsTag

data class GPSInfo private constructor(
Expand Down Expand Up @@ -84,21 +84,21 @@ data class GPSInfo private constructor(
?: return null

// all of these values are strings.
val latitude = latitudeField.value as Array<RationalNumber>
val longitude = longitudeField.value as Array<RationalNumber>
val latitude = latitudeField.value as RationalNumbers
val longitude = longitudeField.value as RationalNumbers

if (latitude.size != 3 || longitude.size != 3)
if (latitude.values.size != 3 || longitude.values.size != 3)
throw ImageReadException("Expected three values for latitude and longitude.")

return GPSInfo(
latitudeRef = latitudeRef,
longitudeRef = longitudeRef,
latitudeDegrees = latitude[0].doubleValue(),
latitudeMinutes = latitude[1].doubleValue(),
latitudeSeconds = latitude[2].doubleValue(),
longitudeDegrees = longitude[0].doubleValue(),
longitudeMinutes = longitude[1].doubleValue(),
longitudeSeconds = longitude[2].doubleValue()
latitudeDegrees = latitude.values[0].doubleValue(),
latitudeMinutes = latitude.values[1].doubleValue(),
latitudeSeconds = latitude.values[2].doubleValue(),
longitudeDegrees = longitude.values[0].doubleValue(),
longitudeMinutes = longitude.values[1].doubleValue(),
longitudeSeconds = longitude.values[2].doubleValue()
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,27 @@
*/
package com.ashampoo.kim.format.tiff

import com.ashampoo.kim.format.tiff.taginfo.TagInfo
import com.ashampoo.kim.format.tiff.write.TiffOutputSet

data class TiffContents(
val header: TiffHeader,
val directories: List<TiffDirectory>
val directories: List<TiffDirectory>,
/** Artifical MakerNote directory */
val makerNoteDirectory: TiffDirectory?
) {

fun findTiffField(tagInfo: TagInfo): TiffField? =
TiffDirectory.findTiffField(directories, tagInfo)

fun findTiffDirectory(directoryType: Int): TiffDirectory? =
directories.find { it.type == directoryType }

fun getExifThumbnailBytes(): ByteArray? =
directories.asSequence()
.mapNotNull { it.jpegImageDataElement?.bytes }
.firstOrNull()

fun createOutputSet(): TiffOutputSet {

val result = TiffOutputSet(header.byteOrder)
Expand Down Expand Up @@ -54,6 +68,10 @@ data class TiffContents(
for (directory in directories)
sb.appendLine(directory)

makerNoteDirectory?.let {
sb.appendLine(makerNoteDirectory)
}

return sb.toString()
}
}
Loading

0 comments on commit adf017b

Please sign in to comment.