From e1f8ce8ebe05ca96c52492ce24b59fd3cbbfa478 Mon Sep 17 00:00:00 2001 From: Stefan Oltmann Date: Sat, 6 Apr 2024 11:53:42 +0200 Subject: [PATCH] TiffWriter now preserves existing strip image bytes (#86) Also added a unit test for GeoTiff updates --- README.md | 2 +- build.gradle.kts | 2 + examples/kim-java-sample/build.gradle | 2 +- examples/kim-kotlin-jvm-sample/.gitignore | 3 +- .../kim-kotlin-jvm-sample/build.gradle.kts | 2 +- examples/kim-kotlin-jvm-sample/empty.tif | Bin 0 -> 40262 bytes .../src/main/kotlin/Main.kt | 48 ++++++++- .../kim/format/jpeg/iptc/IptcTypes.kt | 2 +- .../kim/format/png/PngMetadataExtractor.kt | 2 +- .../ashampoo/kim/format/tiff/TiffContents.kt | 2 +- .../ashampoo/kim/format/tiff/TiffDirectory.kt | 32 +++++- .../com/ashampoo/kim/format/tiff/TiffField.kt | 2 +- .../kim/format/tiff/TiffImageDataElement.kt | 30 ++++++ .../ashampoo/kim/format/tiff/TiffReader.kt | 44 ++++++++- .../kim/format/tiff/constant/TiffTag.kt | 11 +++ .../tiff/geotiff/GeoTiffGeographicType.kt | 2 +- .../format/tiff/write/TiffOutputDirectory.kt | 76 +++++++++++---- .../kim/format/tiff/write/TiffOutputSet.kt | 2 +- .../format/tiff/write/TiffWriterLossless.kt | 8 +- .../com/ashampoo/kim/model/ImageFormat.kt | 2 +- .../ashampoo/kim/common/KotlinIoExtensions.kt | 60 ++++++++++++ .../kim/format/tiff/GeoTiffUpdateTest.kt | 91 ++++++++++++++++++ .../com/ashampoo/kim/updates_tif/empty.tif | Bin 0 -> 40262 bytes .../com/ashampoo/kim/updates_tif/geotiff.tif | Bin 0 -> 40404 bytes 24 files changed, 384 insertions(+), 41 deletions(-) create mode 100644 examples/kim-kotlin-jvm-sample/empty.tif create mode 100644 src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffImageDataElement.kt create mode 100644 src/commonTest/kotlin/com/ashampoo/kim/common/KotlinIoExtensions.kt create mode 100644 src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt create mode 100644 src/commonTest/resources/com/ashampoo/kim/updates_tif/empty.tif create mode 100644 src/commonTest/resources/com/ashampoo/kim/updates_tif/geotiff.tif diff --git a/README.md b/README.md index 40de78fa..41b17ab0 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") +implementation("com.ashampoo:kim:0.17.1") ``` For the targets `wasmJs` & `js` you also need to specify this: diff --git a/build.gradle.kts b/build.gradle.kts index 477b0ef2..008bec91 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -202,6 +202,8 @@ kotlin { /* Kotlin Test */ implementation(kotlin("test")) + + implementation("org.jetbrains.kotlinx:kotlinx-io-core:$ioCoreVersion") } } diff --git a/examples/kim-java-sample/build.gradle b/examples/kim-java-sample/build.gradle index 0e3cf6a3..406463eb 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' + implementation 'com.ashampoo:kim:0.17.1' } // 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 eac6890e..a405c66b 100644 --- a/examples/kim-kotlin-jvm-sample/.gitignore +++ b/examples/kim-kotlin-jvm-sample/.gitignore @@ -38,7 +38,8 @@ bin/ ### Mac OS ### .DS_Store -### Test files ### +### Test output files ### /testphoto_mod1.jpg /testphoto_mod2.jpg /testphoto_mod3.jpg +/geotiff.tif diff --git a/examples/kim-kotlin-jvm-sample/build.gradle.kts b/examples/kim-kotlin-jvm-sample/build.gradle.kts index c7b462f5..6a653210 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") + implementation("com.ashampoo:kim:0.17.1") } diff --git a/examples/kim-kotlin-jvm-sample/empty.tif b/examples/kim-kotlin-jvm-sample/empty.tif new file mode 100644 index 0000000000000000000000000000000000000000..32265cc80f13e8f12915a099db5c455b9a354601 GIT binary patch literal 40262 zcmeI(yKWOf6adg!C!iEGK?0-zjVlyUQBqMRElBR%B|+x4oho~^&%?RGa#cRHQ6T0`?SXS3PLTJ!mQ=XHv%ALH?O=XvsTdTg)qQCw#Z zl_&X-kGyE9SDO~>kIk4Is#c}z%7=WYSLFumkMh_YqHf8De5hCD2A;zH$n)Fububt_ zn$P$0u~^8*gXFe84>sKw`El|gA7%G$PoZApl695A~|tfc;S( zn|;-!s(Iz(|H#L`{QZZ|=k=!KqdrDOb!%VsN + + val writer = TiffWriterLossy( + ByteOrder.LITTLE_ENDIAN + ) + + writer.write( + byteWriter = outputStreamByteWriter, + outputSet = outputSet + ) + } +} diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/jpeg/iptc/IptcTypes.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/jpeg/iptc/IptcTypes.kt index 2c175518..55a29cdb 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/jpeg/iptc/IptcTypes.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/jpeg/iptc/IptcTypes.kt @@ -89,7 +89,7 @@ enum class IptcTypes( init { - for (iptcType in IptcTypes.values()) + for (iptcType in IptcTypes.entries) iptcTypeMap[iptcType.type] = iptcType } diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/png/PngMetadataExtractor.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/png/PngMetadataExtractor.kt index 15d4422f..b0b12d91 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/png/PngMetadataExtractor.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/png/PngMetadataExtractor.kt @@ -214,7 +214,7 @@ object PngMetadataExtractor : MetadataExtractor { fun get(bytes: ByteArray): PngChunkType? { - for (type in PngChunkType.values()) + for (type in PngChunkType.entries) if (bytes.contentEquals(type.bytes)) return type diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffContents.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffContents.kt index fd3d6812..0e99226a 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffContents.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffContents.kt @@ -37,7 +37,7 @@ data class TiffContents( fun getExifThumbnailBytes(): ByteArray? = directories.asSequence() - .mapNotNull { it.jpegImageDataElement?.bytes } + .mapNotNull { it.thumbnailImageDataElement?.bytes } .firstOrNull() fun createOutputSet(): TiffOutputSet { diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffDirectory.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffDirectory.kt index f37f4bef..b2090204 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffDirectory.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffDirectory.kt @@ -53,13 +53,17 @@ class TiffDirectory( TiffConstants.TIFF_ENTRY_LENGTH + TiffConstants.TIFF_DIRECTORY_FOOTER_LENGTH ) { - var jpegImageDataElement: JpegImageDataElement? = null + var thumbnailImageDataElement: JpegImageDataElement? = null + var tiffImageDataElement: TiffImageDataElement? = null fun getDirectoryEntries(): List = entries fun hasJpegImageData(): Boolean = null != findField(TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT) + fun hasStripImageData(): Boolean = + null != findField(TiffTag.TIFF_TAG_STRIP_OFFSETS) + fun findField(tag: TagInfo): TiffField? { return findField( tag = tag, @@ -120,15 +124,15 @@ class TiffDirectory( return field.valueBytes.toInts(field.byteOrder) } - fun getJpegRawImageDataElement(): ImageDataElement { + fun getJpegImageDataElement(): ImageDataElement { val jpegInterchangeFormat = findField(TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT) val jpegInterchangeFormatLength = findField(TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH) if (jpegInterchangeFormat != null && jpegInterchangeFormatLength != null) { - val offset = jpegInterchangeFormat.toIntArray()[0] - val byteCount = jpegInterchangeFormatLength.toIntArray()[0] + val offset = jpegInterchangeFormat.toInt() + val byteCount = jpegInterchangeFormatLength.toInt() return ImageDataElement(offset, byteCount) } @@ -136,6 +140,22 @@ class TiffDirectory( throw ImageReadException("Couldn't find image data.") } + fun getStripImageDataElement(): ImageDataElement { + + val offsetField = findField(TiffTag.TIFF_TAG_STRIP_OFFSETS) + val lengthField = findField(TiffTag.TIFF_TAG_STRIP_BYTE_COUNTS) + + if (offsetField != null && lengthField != null) { + + val offset = offsetField.toInt() + val length = lengthField.toInt() + + return ImageDataElement(offset, length) + } + + throw ImageReadException("Couldn't find image data.") + } + fun createOutputDirectory(byteOrder: ByteOrder): TiffOutputDirectory { /* @@ -209,7 +229,9 @@ class TiffDirectory( ) } - outputDirectory.setJpegImageData(jpegImageDataElement) + outputDirectory.setThumbnailImageDataElement(thumbnailImageDataElement) + + outputDirectory.setTiffImageDataElement(tiffImageDataElement) return outputDirectory diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffField.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffField.kt index 70296e01..5271a317 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffField.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffField.kt @@ -165,7 +165,7 @@ class TiffField( return intArrayOf(value.toInt()) if (value is IntArray) - return value.copyOf() + return value if (value is ShortArray) { diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffImageDataElement.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffImageDataElement.kt new file mode 100644 index 00000000..3fd03ae1 --- /dev/null +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffImageDataElement.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Ashampoo GmbH & Co. KG + * Copyright 2007-2023 The Apache Software Foundation + * + * 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.tiff + +class TiffImageDataElement( + offset: Int, + length: Int, + val bytes: ByteArray +) : ImageDataElement( + offset, + length +) { + + override fun toString(): String = "StripImageData offet=$offset, length=$length" + +} 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 d65f2fc1..9c89eabf 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt @@ -183,7 +183,10 @@ object TiffReader { ) if (directory.hasJpegImageData()) - directory.jpegImageDataElement = getJpegRawImageData(byteReader, directory) + directory.thumbnailImageDataElement = getJpegImageDataElement(byteReader, directory) + + if (directory.hasStripImageData()) + directory.tiffImageDataElement = getStripImageDataElement(byteReader, directory) addDirectory(directory) @@ -356,22 +359,25 @@ object TiffReader { * * Discarding corrupt thumbnails is not a big issue, so no exceptions will be thrown here. */ - private fun getJpegRawImageData( + private fun getJpegImageDataElement( byteReader: RandomAccessByteReader, directory: TiffDirectory ): JpegImageDataElement? { - val element = directory.getJpegRawImageDataElement() + val element = directory.getJpegImageDataElement() val offset = element.offset var length = element.length /* - * If the length is not correct (going beyond the file size), we adjust it. + * If the length is not correct (going beyond the file size) we need to adjust it. */ if (offset + length > byteReader.contentLength) length = (byteReader.contentLength - offset).toInt() + /* + * If the new length is 0 or negative, ignore this element. + */ if (length <= 0) return null @@ -398,6 +404,36 @@ object TiffReader { return JpegImageDataElement(offset, length, bytes) } + private fun getStripImageDataElement( + byteReader: RandomAccessByteReader, + directory: TiffDirectory + ): TiffImageDataElement? { + + val element = directory.getStripImageDataElement() + + val offset = element.offset + var length = element.length + + /* + * If the length is not correct (going beyond the file size) we need to adjust it. + */ + if (offset + length > byteReader.contentLength) + length = (byteReader.contentLength - offset).toInt() + + /* + * If the new length is 0 or negative, ignore this element. + */ + if (length <= 0) + return null + + val bytes = byteReader.readBytes(offset.toInt(), length) + + if (bytes.size != length) + return null + + return TiffImageDataElement(offset, length, bytes) + } + /** * Inspect if MakerNotes are present and could be added as * TiffDirectory. This is true for almost all manufacturers. diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/constant/TiffTag.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/constant/TiffTag.kt index e3c90008..eceeed5e 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/constant/TiffTag.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/constant/TiffTag.kt @@ -179,6 +179,12 @@ object TiffTag { TIFF_DIRECTORY_IFD0 ) + val TIFF_TAG_STRIP_OFFSETS = TagInfoLong( + 0x0111, "StripOffsets", + TIFF_DIRECTORY_IFD0, + isOffset = true + ) + val TIFF_TAG_ORIENTATION = TagInfoShort( 0x0112, "Orientation", TIFF_DIRECTORY_IFD0 @@ -208,6 +214,11 @@ object TiffTag { 0x0116, "RowsPerStrip", TIFF_DIRECTORY_IFD0 ) + val TIFF_TAG_STRIP_BYTE_COUNTS = TagInfoLong( + 0x0117, "StripByteCounts", + TIFF_DIRECTORY_IFD0 + ) + val TIFF_TAG_MIN_SAMPLE_VALUE = TagInfoShorts( 0x0118, "MinSampleValue", TagInfo.LENGTH_UNKNOWN, TIFF_DIRECTORY_IFD0 diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/geotiff/GeoTiffGeographicType.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/geotiff/GeoTiffGeographicType.kt index 56f5b8c3..f4783f08 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/geotiff/GeoTiffGeographicType.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/geotiff/GeoTiffGeographicType.kt @@ -206,6 +206,6 @@ enum class GeoTiffGeographicType( @JvmStatic fun of(typeCode: Short): GeoTiffGeographicType? = - GeoTiffGeographicType.values().firstOrNull { it.typeCode == typeCode } + GeoTiffGeographicType.entries.firstOrNull { it.typeCode == typeCode } } } 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 e4fe6d82..dc4a281f 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 @@ -23,7 +23,7 @@ import com.ashampoo.kim.common.RationalNumbers import com.ashampoo.kim.common.toBytes import com.ashampoo.kim.format.tiff.JpegImageDataElement import com.ashampoo.kim.format.tiff.TiffDirectory.Companion.description -import com.ashampoo.kim.format.tiff.constant.ExifTag +import com.ashampoo.kim.format.tiff.TiffImageDataElement import com.ashampoo.kim.format.tiff.constant.TiffConstants.TIFF_DIRECTORY_FOOTER_LENGTH import com.ashampoo.kim.format.tiff.constant.TiffConstants.TIFF_DIRECTORY_HEADER_LENGTH import com.ashampoo.kim.format.tiff.constant.TiffConstants.TIFF_ENTRY_LENGTH @@ -78,7 +78,10 @@ class TiffOutputDirectory( override var offset: Int = UNDEFINED_VALUE - var rawJpegImageDataElement: JpegImageDataElement? = null + var thumbnailImageDataElement: JpegImageDataElement? = null + private set + + var tiffImageDataElement: TiffImageDataElement? = null private set fun setNextDirectory(nextDirectory: TiffOutputDirectory?) { @@ -448,8 +451,12 @@ class TiffOutputDirectory( } /* Internal, because callers should use setThumbnailBytes() */ - internal fun setJpegImageData(rawJpegImageDataElement: JpegImageDataElement?) { - this.rawJpegImageDataElement = rawJpegImageDataElement + internal fun setThumbnailImageDataElement(thumbnailImageDataElement: JpegImageDataElement?) { + this.thumbnailImageDataElement = thumbnailImageDataElement + } + + internal fun setTiffImageDataElement(tiffImageDataElement: TiffImageDataElement?) { + this.tiffImageDataElement = tiffImageDataElement } override fun getItemLength(): Int = @@ -462,22 +469,25 @@ class TiffOutputDirectory( outputSummary: TiffOffsetItems ): List { - /* First validate directory fields. */ + /* 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_STRIP_BYTE_COUNTS) - var jpegOffsetField: TiffOutputField? = null + var thumbnailOffsetField: TiffOutputField? = null - if (rawJpegImageDataElement != null) { + if (thumbnailImageDataElement != null) { - jpegOffsetField = TiffOutputField( + thumbnailOffsetField = TiffOutputField( TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT.tag, - FieldTypeLong, 1, ByteArray(TIFF_ENTRY_MAX_VALUE_LENGTH) + FieldTypeLong, 1, + ByteArray(TIFF_ENTRY_MAX_VALUE_LENGTH) ) - add(jpegOffsetField) + add(thumbnailOffsetField) - val lengthValue = FieldTypeLong.writeData(rawJpegImageDataElement!!.length, outputSummary.byteOrder) + val lengthValue = FieldTypeLong.writeData(thumbnailImageDataElement!!.length, outputSummary.byteOrder) val jpegLengthField = TiffOutputField( TiffTag.TIFF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH.tag, @@ -487,8 +497,28 @@ class TiffOutputDirectory( add(jpegLengthField) } - removeFieldIfPresent(ExifTag.EXIF_TAG_PREVIEW_IMAGE_START_IFD0) - removeFieldIfPresent(ExifTag.EXIF_TAG_PREVIEW_IMAGE_LENGTH_IFD0) + var stripOffsetField: TiffOutputField? = null + + if (tiffImageDataElement != null) { + + stripOffsetField = TiffOutputField( + TiffTag.TIFF_TAG_STRIP_OFFSETS.tag, + FieldTypeLong, 1, + ByteArray(TIFF_ENTRY_MAX_VALUE_LENGTH) + ) + + add(stripOffsetField) + + val lengthValue = FieldTypeLong.writeData(tiffImageDataElement!!.length, outputSummary.byteOrder) + + val stripByteCountsField = TiffOutputField( + TiffTag.TIFF_TAG_STRIP_BYTE_COUNTS.tag, + FieldTypeLong, 1, lengthValue + ) + + add(stripByteCountsField) + } + removeFieldIfPresent(TiffTag.TIFF_TAG_TILE_OFFSETS) removeFieldIfPresent(TiffTag.TIFF_TAG_TILE_BYTE_COUNTS) @@ -507,16 +537,28 @@ class TiffOutputDirectory( result.add(item) } - if (rawJpegImageDataElement != null) { + if (thumbnailImageDataElement != null) { + + val item: TiffOutputItem = TiffOutputValue( + "thumbnailImageDataElement", + thumbnailImageDataElement!!.bytes + ) + + result.add(item) + + outputSummary.addOffsetItem(TiffOffsetItem(item, thumbnailOffsetField!!)) + } + + if (tiffImageDataElement != null) { val item: TiffOutputItem = TiffOutputValue( - "rawJpegImageData", - rawJpegImageDataElement!!.bytes + "tiffImageDataElement", + tiffImageDataElement!!.bytes ) result.add(item) - outputSummary.addOffsetItem(TiffOffsetItem(item, jpegOffsetField!!)) + outputSummary.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 0dee2879..2ee50546 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 @@ -145,7 +145,7 @@ class TiffOutputSet( val thumbnailDirectory = getOrCreateThumbnailDirectory() - thumbnailDirectory.setJpegImageData( + thumbnailDirectory.setThumbnailImageDataElement( JpegImageDataElement( /* Offset will be calculated, but the block should come early in the file. */ offset = -1, diff --git a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffWriterLossless.kt b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffWriterLossless.kt index 10aae415..c6fe4b08 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffWriterLossless.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/format/tiff/write/TiffWriterLossless.kt @@ -65,7 +65,7 @@ class TiffWriterLossless( * Don't write IFD1 directories without image data. If the thumbnail image is broken * on load, we should drop the IFD1 on rewrite entirely. It's just a waste of space. */ - if (directory.type == 1 && directory.jpegImageDataElement == null) + if (directory.type == 1 && directory.thumbnailImageDataElement == null) continue elements.add(directory) @@ -87,7 +87,11 @@ class TiffWriterLossless( } } - directory.jpegImageDataElement?.let { + directory.thumbnailImageDataElement?.let { + elements.add(it) + } + + directory.tiffImageDataElement?.let { elements.add(it) } } diff --git a/src/commonMain/kotlin/com/ashampoo/kim/model/ImageFormat.kt b/src/commonMain/kotlin/com/ashampoo/kim/model/ImageFormat.kt index 1866a4e4..4de52693 100644 --- a/src/commonMain/kotlin/com/ashampoo/kim/model/ImageFormat.kt +++ b/src/commonMain/kotlin/com/ashampoo/kim/model/ImageFormat.kt @@ -57,7 +57,7 @@ enum class ImageFormat( */ const val REQUIRED_HEADER_BYTE_COUNT_FOR_DETECTION: Int = 16 - private val allImageFormats = ImageFormat.values() + private val allImageFormats = ImageFormat.entries private val allFileNameExtensions = getAllFileNameExtensions() diff --git a/src/commonTest/kotlin/com/ashampoo/kim/common/KotlinIoExtensions.kt b/src/commonTest/kotlin/com/ashampoo/kim/common/KotlinIoExtensions.kt new file mode 100644 index 00000000..7219bf45 --- /dev/null +++ b/src/commonTest/kotlin/com/ashampoo/kim/common/KotlinIoExtensions.kt @@ -0,0 +1,60 @@ +/* + * 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 + +import kotlinx.io.buffered +import kotlinx.io.files.Path +import kotlinx.io.files.SystemFileSystem +import kotlinx.io.readByteArray + +/* + * Copied from ktorMain sourceset, which is not available for pure JS! + */ + +@OptIn(ExperimentalStdlibApi::class) +internal fun Path.copyTo(destination: Path) { + + require(exists()) { "$this does not exist." } + + val metadata = SystemFileSystem.metadataOrNull(this) + + requireNotNull(metadata) { "Failed to read metadata of $this" } + require(metadata.isRegularFile) { "Source $this must be a regular file." } + + SystemFileSystem.source(this).buffered().use { rawSource -> + SystemFileSystem.sink(destination).buffered().use { sink -> + sink.write(rawSource, metadata.size) + } + } +} + +@OptIn(ExperimentalStdlibApi::class) +internal fun Path.writeBytes(byteArray: ByteArray) = + SystemFileSystem + .sink(this) + .buffered() + .use { it.write(byteArray) } + +@OptIn(ExperimentalStdlibApi::class) +internal fun Path.readBytes(): ByteArray = + SystemFileSystem + .source(this) + .buffered() + .use { it.readByteArray() } + +@OptIn(ExperimentalStdlibApi::class) +internal fun Path.exists(): Boolean = + SystemFileSystem.exists(this) diff --git a/src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt b/src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt new file mode 100644 index 00000000..bdd73a5a --- /dev/null +++ b/src/commonTest/kotlin/com/ashampoo/kim/format/tiff/GeoTiffUpdateTest.kt @@ -0,0 +1,91 @@ +/* + * 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.format.tiff + +import com.ashampoo.kim.Kim +import com.ashampoo.kim.common.ByteOrder +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.output.ByteArrayByteWriter +import kotlinx.io.files.Path +import kotlin.test.Test +import kotlin.test.fail + +class GeoTiffUpdateTest { + + private val resourcePath: String = "src/commonTest/resources/com/ashampoo/kim/updates_tif" + + private val originalBytes = Path( + getPathForResource("$resourcePath/empty.tif") + ).readBytes() + + private val expectedBytes = Path( + getPathForResource("$resourcePath/geotiff.tif") + ).readBytes() + + @Test + fun testSetGeoTiff() { + + val metadata = Kim.readMetadata(originalBytes) ?: 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 byteWriter = ByteArrayByteWriter() + + val writer = TiffWriterLossy( + ByteOrder.LITTLE_ENDIAN + ) + + writer.write( + byteWriter = byteWriter, + outputSet = outputSet + ) + + val actualBytes = byteWriter.toByteArray() + + val equals = expectedBytes.contentEquals(actualBytes) + + if (!equals) { + + Path("build/geotiff.tif") + .writeBytes(actualBytes) + + fail("geotiff.tif has not the expected bytes!") + } + } + +} diff --git a/src/commonTest/resources/com/ashampoo/kim/updates_tif/empty.tif b/src/commonTest/resources/com/ashampoo/kim/updates_tif/empty.tif new file mode 100644 index 0000000000000000000000000000000000000000..32265cc80f13e8f12915a099db5c455b9a354601 GIT binary patch literal 40262 zcmeI(yKWOf6adg!C!iEGK?0-zjVlyUQBqMRElBR%B|+x4oho~^&%?RGa#cRHQ6T0`?SXS3PLTJ!mQ=XHv%ALH?O=XvsTdTg)qQCw#Z zl_&X-kGyE9SDO~>kIk4Is#c}z%7=WYSLFumkMh_YqHf8De5hCD2A;zH$n)Fububt_ zn$P$0u~^8*gXFe84>sKw`El|gA7%G$PoZApl695A~|tfc;S( zn|;-!s(Iz(|H#L`{QZZ|=k=!KqdrDOb!%VsN<#x5=*N^>(I1ADMjeo#S9e`E0hY^0uGka#X+=POBMSMbm&lA z6_>6~aqS*mLgo^(HB+ENd|q=eTbZ&n1DZtUOm~rPO_; zyp-}J+LWi)5&IHPNSof73fgtQ$sqP6p7JKHj{Pn>?sfEe5XAcU=^*|h@l4P^|5K@1 z@6Oe+X*qT-h_ezu>KJF<^HoNUdnoPCf_6RcyN*8V66<-MN}s!X8I_Xwg~VU?Gip-e z*AoBwmQq=XcO-8ArRBfZ58YEYn(gw(t)2JVAGUY5Hl@A&*!X;UeDrc*@!-{uwUfV3 z&deCL?b7cz{>Ay@{M5h0)wiFP^QNDk$Jgg4r|U_i6X2;oSy`0!Yo%J!g(qu(0SsUO z0~o*n1~7mD3}65Q7{CAqFn|FJU;qOczyJm?fB_6(00S7n00uCC0SsUO0~o*n1~7mD z3}65Q7{CAqFn|F^23lkKtJP{sxZq4;N4>}(CARmKT8K$4>dKFjCdiUpYxxUk_*XvGesF}0TXmrihY&M6U zC+z-FtyYH~$DGqm+mes)Iy0_3kq`1=iWc>%-vZZ1|C)@eR_S@k2l=30*$r@g*wsSoU!y2vuJ@4-@?qZBxO#uZ{Uezv?jP~%M!kw(BiHwcsPV`L`QZLx zH^B8_Uz@19GOB&#gM3i0>;||#>}xZsniMrI`S?Hbah<>a;PZLBDe@7&MqzbpRP~B{ zkdH8Lv?X)Fy0LB;zyJm?fB_6(00S7n00uCC0SsUO0~o*n1~7mD3}65Q7{CAqFn|FJ cU;qOczyJm?fB_6(00S7n00uCCfn)|Q0p>a;bpQYW literal 0 HcmV?d00001