diff --git a/README.md b/README.md index 53101881..79582765 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ of Ashampoo Photos, which, in turn, is driven by user community feedback. ## Installation ``` -implementation("com.ashampoo:kim:0.17.2") +implementation("com.ashampoo:kim:0.17.3") ``` For the targets `wasmJs` & `js` you also need to specify this: diff --git a/examples/kim-java-sample/build.gradle b/examples/kim-java-sample/build.gradle index 3654bd0d..410ade11 100644 --- a/examples/kim-java-sample/build.gradle +++ b/examples/kim-java-sample/build.gradle @@ -10,7 +10,7 @@ repositories { } dependencies { - implementation 'com.ashampoo:kim:0.17.2' + implementation 'com.ashampoo:kim:0.17.3' } // Needed to make it work for the Gradle java plugin diff --git a/examples/kim-kotlin-jvm-sample/.gitignore b/examples/kim-kotlin-jvm-sample/.gitignore index d307775b..715a47ba 100644 --- a/examples/kim-kotlin-jvm-sample/.gitignore +++ b/examples/kim-kotlin-jvm-sample/.gitignore @@ -43,5 +43,4 @@ bin/ /testphoto_mod2.jpg /testphoto_mod3.jpg /geotiff.tif -/geotiff_kotlinxio.tif -/geotiff_lowlevel.tif +/geotiff_kotlinx.tif diff --git a/examples/kim-kotlin-jvm-sample/build.gradle.kts b/examples/kim-kotlin-jvm-sample/build.gradle.kts index 9f897903..8177c2d0 100644 --- a/examples/kim-kotlin-jvm-sample/build.gradle.kts +++ b/examples/kim-kotlin-jvm-sample/build.gradle.kts @@ -10,5 +10,5 @@ repositories { } dependencies { - implementation("com.ashampoo:kim:0.17.2") + implementation("com.ashampoo:kim:0.17.3") } diff --git a/examples/kim-kotlin-jvm-sample/src/main/kotlin/Main.kt b/examples/kim-kotlin-jvm-sample/src/main/kotlin/Main.kt index af9dda77..1e8f8d1a 100644 --- a/examples/kim-kotlin-jvm-sample/src/main/kotlin/Main.kt +++ b/examples/kim-kotlin-jvm-sample/src/main/kotlin/Main.kt @@ -30,7 +30,6 @@ fun main() { setGeoTiffToJpeg() setGeoTiffToTiff() setGeoTiffToTiffUsingKotlinx() - setGeoTiffToTiffUsingKotlinxAndTiffReader() } fun printMetadata() { @@ -132,9 +131,26 @@ fun setGeoTiffToTiff() { val inputFile = File("empty.tif") val outputFile = File("geotiff.tif") - val metadata = Kim.readMetadata(inputFile) ?: return + val tiffContents: TiffContents = + JvmInputStreamByteReader(inputFile.inputStream(), inputFile.length()).use { byteReader -> + byteReader.let { + + TiffReader.read( + /* + * TIFF files can be extremely large. + * It is not advisable to load them entirely into a ByteArray. + */ + byteReader = DefaultRandomAccessByteReader(byteReader), + /* + * Per default the strip bytes are not read. + * For GeoTiff writing to a TIFF this needs to be turned on. + */ + readTiffImageBytes = true + ) + } + } - val outputSet: TiffOutputSet = metadata.exif?.createOutputSet() ?: TiffOutputSet() + val outputSet: TiffOutputSet = tiffContents.createOutputSet() val rootDirectory = outputSet.getOrCreateRootDirectory() @@ -170,68 +186,24 @@ fun setGeoTiffToTiff() { fun setGeoTiffToTiffUsingKotlinx() { val inputPath = kotlinx.io.files.Path("empty.tif") - val outputPath = kotlinx.io.files.Path("geotiff_kotlinxio.tif") - - /* - * Kim.readMetadata(inputPath) (extension function) is also possible, - * but if IDEA yields errors this approach works better. - */ - val metadata = KimKotlinx.readMetadata(inputPath) ?: return - - val outputSet: TiffOutputSet = metadata.exif?.createOutputSet() ?: TiffOutputSet() - - val rootDirectory = outputSet.getOrCreateRootDirectory() - - rootDirectory.add( - GeoTiffTag.EXIF_TAG_MODEL_PIXEL_SCALE_TAG, - doubleArrayOf(0.0002303616678184751, -0.0001521606816798535, 0.0) - ) - - rootDirectory.add( - GeoTiffTag.EXIF_TAG_MODEL_TIEPOINT_TAG, - doubleArrayOf(0.0, 0.0, 0.0, 8.915687629578438, 48.92432542097789, 0.0) - ) - - rootDirectory.add( - GeoTiffTag.EXIF_TAG_GEO_KEY_DIRECTORY_TAG, - shortArrayOf(1, 0, 2, 3, 1024, 0, 1, 2, 2048, 0, 1, 4326, 1025, 0, 1, 2) - ) - - val byteArrayByteWriter = ByteArrayByteWriter() - - val tiffWriter = TiffWriterLossy(outputSet.byteOrder) - - tiffWriter.write( - byteWriter = byteArrayByteWriter, - outputSet = outputSet - ) - - val updatedBytes = byteArrayByteWriter.toByteArray() - - outputPath.writeBytes(updatedBytes) -} - -/** - * Shows how to update set GeoTiff to a TIF file using kotlinx-io - * using low level API. Only use this if you now for sure it's a TIFF file, - * because this skips the file format detection in Kim.readMetadata() - */ -fun setGeoTiffToTiffUsingKotlinxAndTiffReader() { - - val inputPath = kotlinx.io.files.Path("empty.tif") - val outputPath = kotlinx.io.files.Path("geotiff_lowlevel.tif") + val outputPath = kotlinx.io.files.Path("geotiff_kotlinx.tif") val tiffContents: TiffContents? = KotlinIoSourceByteReader.read(inputPath) { byteReader -> byteReader?.let { - /* - * TIFF files can be extremely large. - * It is not advisable to load them entirely into a ByteArray. - */ - val randomAccessByteReader = DefaultRandomAccessByteReader(byteReader) - - TiffReader.read(randomAccessByteReader) + TiffReader.read( + /* + * TIFF files can be extremely large. + * It is not advisable to load them entirely into a ByteArray. + */ + byteReader = DefaultRandomAccessByteReader(byteReader), + /* + * Per default the strip bytes are not read. + * For GeoTiff writing to a TIFF this needs to be turned on. + */ + readTiffImageBytes = true + ) } } @@ -267,3 +239,4 @@ fun setGeoTiffToTiffUsingKotlinxAndTiffReader() { outputPath.writeBytes(updatedBytes) } + diff --git a/src/commonMain/kotlin/com/ashampoo/kim/common/PhotoMetadataConverter.kt b/src/commonMain/kotlin/com/ashampoo/kim/common/PhotoMetadataConverter.kt index 6cbd72d5..6dd848b4 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/common/PhotoMetadataConverter.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/common/PhotoMetadataConverter.kt @@ -33,7 +33,6 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant fun ImageMetadata.convertToPhotoMetadata( - includeThumbnail: Boolean = false, ignoreOrientation: Boolean = false ): PhotoMetadata { @@ -88,10 +87,7 @@ fun ImageMetadata.convertToPhotoMetadata( XmpReader.readMetadata(it) } - val thumbnailBytes = if (includeThumbnail) - getExifThumbnailBytes() - else - null + val thumbnailBytes = getExifThumbnailBytes() val thumbnailImageSize = thumbnailBytes?.let { JpegImageParser.getImageSize( diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/ImageParser.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/ImageParser.kt index 35deb273..cd34b637 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/ImageParser.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/ImageParser.kt @@ -24,6 +24,7 @@ import com.ashampoo.kim.format.tiff.TiffImageParser import com.ashampoo.kim.format.webp.WebPImageParser import com.ashampoo.kim.input.ByteReader import com.ashampoo.kim.model.ImageFormat +import kotlin.jvm.JvmStatic fun interface ImageParser { @@ -32,6 +33,7 @@ fun interface ImageParser { companion object { + @JvmStatic fun forFormat(imageFormat: ImageFormat): ImageParser? = when (imageFormat) { diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt index a5df6f51..552fd6c0 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt @@ -38,7 +38,9 @@ import com.ashampoo.kim.input.ByteArrayByteReader import com.ashampoo.kim.input.ByteReader import com.ashampoo.kim.input.RandomAccessByteReader import com.ashampoo.kim.output.ByteArrayByteWriter +import kotlin.jvm.JvmStatic +@Suppress("TooManyFunctions") object TiffReader { const val NIKON_MAKER_NOTE_SIGNATURE = "Nikon\u0000" @@ -61,10 +63,23 @@ object TiffReader { * Convenience method for calls with short byte array like * the EXIF bytes in JPG, which are limited to 64 KB. */ + @JvmStatic fun read(exifBytes: ByteArray): TiffContents = read(ByteArrayByteReader(exifBytes)) - fun read(byteReader: RandomAccessByteReader): TiffContents { + /** + * Reads the TIFF file. + * + * @param byteReader The bytes source + * @param readTiffImageBytes Flag to include strip bytes. + * This should only set if a rewrite of the file is intended. + * For normal reading of RAW metadata this consumes a lot of memory. + */ + @JvmStatic + fun read( + byteReader: RandomAccessByteReader, + readTiffImageBytes: Boolean = false + ): TiffContents { val tiffHeader = readTiffHeader(byteReader) @@ -78,6 +93,7 @@ object TiffReader { directoryOffset = tiffHeader.offsetToFirstIFD, directoryType = TiffConstants.TIFF_DIRECTORY_TYPE_IFD0, visitedOffsets = mutableListOf(), + readTiffImageBytes = readTiffImageBytes, addDirectory = { directories.add(it) } @@ -94,7 +110,7 @@ object TiffReader { return TiffContents(tiffHeader, directories, makerNoteDirectory, geoTiffDirectory) } - fun readTiffHeader(byteReader: ByteReader): TiffHeader { + internal fun readTiffHeader(byteReader: ByteReader): TiffHeader { val byteOrder1 = byteReader.readByte("Byte order: First byte") val byteOrder2 = byteReader.readByte("Byte Order: Second byte") @@ -125,6 +141,7 @@ object TiffReader { directoryOffset: Int, directoryType: Int, visitedOffsets: MutableList, + readTiffImageBytes: Boolean, addDirectory: (TiffDirectory) -> Unit ): Boolean { @@ -186,7 +203,7 @@ object TiffReader { if (directory.hasJpegImageData()) directory.thumbnailBytes = readThumbnailBytes(byteReader, directory) - if (directory.hasStripImageData()) + if (readTiffImageBytes && directory.hasStripImageData()) directory.tiffImageBytes = readTiffImageBytes(byteReader, directory) addDirectory(directory) @@ -228,6 +245,7 @@ object TiffReader { directoryOffset = subDirOffset, directoryType = subDirectoryType, visitedOffsets = visitedOffsets, + readTiffImageBytes = readTiffImageBytes, addDirectory = addDirectory ) @@ -250,6 +268,7 @@ object TiffReader { directoryOffset = directory.nextDirectoryOffset, directoryType = directoryType + 1, visitedOffsets = visitedOffsets, + readTiffImageBytes = readTiffImageBytes, addDirectory = addDirectory ) @@ -516,6 +535,7 @@ object TiffReader { directoryOffset = makerNoteValueOffset, directoryType = TiffConstants.TIFF_MAKER_NOTE_CANON, visitedOffsets = mutableListOf(), + readTiffImageBytes = false, addDirectory = addDirectory ) } @@ -547,6 +567,7 @@ object TiffReader { directoryOffset = makerNoteValueOffset + 18, directoryType = TiffConstants.TIFF_MAKER_NOTE_NIKON, visitedOffsets = mutableListOf(), + readTiffImageBytes = false, addDirectory = addDirectory ) } diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputDirectory.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputDirectory.kt index d1d019f2..3af71c88 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputDirectory.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputDirectory.kt @@ -463,16 +463,11 @@ class TiffOutputDirectory( private fun removeFieldIfPresent(tagInfo: TagInfo) = findField(tagInfo)?.let { field -> fields.remove(field) } - fun getOutputItems( - outputSummary: TiffOffsetItems - ): List { + fun getOutputItems(tiffOffsetItems: TiffOffsetItems): List { /* First remove old fields */ removeFieldIfPresent(TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT) removeFieldIfPresent(TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH) - removeFieldIfPresent(TiffTag.TIFF_TAG_STRIP_OFFSETS) - removeFieldIfPresent(TiffTag.TIFF_TAG_ROWS_PER_STRIP) - removeFieldIfPresent(TiffTag.TIFF_TAG_STRIP_BYTE_COUNTS) var thumbnailOffsetField: TiffOutputField? = null @@ -488,7 +483,7 @@ class TiffOutputDirectory( val lengthValue = FieldTypeLong.writeData( thumbnailBytes!!.size, - outputSummary.byteOrder + tiffOffsetItems.byteOrder ) val jpegLengthField = TiffOutputField( @@ -503,6 +498,10 @@ class TiffOutputDirectory( if (tiffImageBytes != null) { + removeFieldIfPresent(TiffTag.TIFF_TAG_STRIP_OFFSETS) + removeFieldIfPresent(TiffTag.TIFF_TAG_ROWS_PER_STRIP) + removeFieldIfPresent(TiffTag.TIFF_TAG_STRIP_BYTE_COUNTS) + stripOffsetField = TiffOutputField( TiffTag.TIFF_TAG_STRIP_OFFSETS.tag, FieldTypeLong, 1, @@ -513,7 +512,7 @@ class TiffOutputDirectory( val lengthValue = FieldTypeLong.writeData( tiffImageBytes!!.size, - outputSummary.byteOrder + tiffOffsetItems.byteOrder ) val stripByteCountsField = TiffOutputField( @@ -526,7 +525,7 @@ class TiffOutputDirectory( /* Set to MAX value. We combine all strips into one block. */ val rowsPerStripValue = FieldTypeLong.writeData( Int.MAX_VALUE, - outputSummary.byteOrder + tiffOffsetItems.byteOrder ) val rowsPerStripField = TiffOutputField( @@ -564,7 +563,7 @@ class TiffOutputDirectory( result.add(item) - outputSummary.addOffsetItem(TiffOffsetItem(item, thumbnailOffsetField!!)) + tiffOffsetItems.addOffsetItem(TiffOffsetItem(item, thumbnailOffsetField!!)) } if (tiffImageBytes != null) { @@ -576,7 +575,7 @@ class TiffOutputDirectory( result.add(item) - outputSummary.addOffsetItem(TiffOffsetItem(item, stripOffsetField!!)) + tiffOffsetItems.addOffsetItem(TiffOffsetItem(item, stripOffsetField!!)) } return result diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputSet.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputSet.kt index d9b5ed7d..bafd5a3d 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputSet.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffOutputSet.kt @@ -42,12 +42,12 @@ class TiffOutputSet( private val directories = mutableListOf() - fun getOutputItems(outputSummary: TiffOffsetItems): List { + fun getOutputItems(tiffOffsetItems: TiffOffsetItems): List { val outputItems = mutableListOf() for (directory in directories) - outputItems.addAll(directory.getOutputItems(outputSummary)) + outputItems.addAll(directory.getOutputItems(tiffOffsetItems)) return outputItems } diff --git a/src/commonTest/kotlin/com/ashampoo/kim/common/PhotoMetadataConverterTest.kt b/src/commonTest/kotlin/com/ashampoo/kim/common/PhotoMetadataConverterTest.kt index fff08b0b..293699c0 100644 --- a/src/commonTest/kotlin/com/ashampoo/kim/common/PhotoMetadataConverterTest.kt +++ b/src/commonTest/kotlin/com/ashampoo/kim/common/PhotoMetadataConverterTest.kt @@ -62,9 +62,7 @@ class PhotoMetadataConverterTest { if (index == KimTestData.HEIC_TEST_IMAGE_INDEX) return - val photoMetadata = Kim.readMetadata(bytes)?.convertToPhotoMetadata( - includeThumbnail = true - ) + val photoMetadata = Kim.readMetadata(bytes)?.convertToPhotoMetadata() assertNotNull(photoMetadata) diff --git a/src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt b/src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt index 685d0052..d5fae53a 100644 --- a/src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt +++ b/src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt @@ -15,13 +15,13 @@ */ package com.ashampoo.kim.format.tiff -import com.ashampoo.kim.Kim import com.ashampoo.kim.common.readBytes import com.ashampoo.kim.common.writeBytes import com.ashampoo.kim.format.tiff.constant.GeoTiffTag import com.ashampoo.kim.format.tiff.write.TiffOutputSet import com.ashampoo.kim.format.tiff.write.TiffWriterLossy import com.ashampoo.kim.getPathForResource +import com.ashampoo.kim.input.ByteArrayByteReader import com.ashampoo.kim.output.ByteArrayByteWriter import kotlinx.io.files.Path import kotlin.test.Test @@ -42,9 +42,12 @@ class GeoTiffUpdateTest { @Test fun testSetGeoTiff() { - val metadata = Kim.readMetadata(originalBytes) ?: return + val tiffContents = TiffReader.read( + byteReader = ByteArrayByteReader(originalBytes), + readTiffImageBytes = true + ) - val outputSet: TiffOutputSet = metadata.exif?.createOutputSet() ?: TiffOutputSet() + val outputSet: TiffOutputSet = tiffContents.createOutputSet() val rootDirectory = outputSet.getOrCreateRootDirectory() @@ -84,5 +87,4 @@ class GeoTiffUpdateTest { fail("geotiff.tif has not the expected bytes!") } } - }