Skip to content

Commit

Permalink
TiffWriter now preserves existing strip image bytes (#86)
Browse files Browse the repository at this point in the history
Also added a unit test for GeoTiff updates
  • Loading branch information
StefanOltmann authored Apr 6, 2024
1 parent 01a8ffb commit e1f8ce8
Show file tree
Hide file tree
Showing 24 changed files with 384 additions and 41 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ kotlin {

/* Kotlin Test */
implementation(kotlin("test"))

implementation("org.jetbrains.kotlinx:kotlinx-io-core:$ioCoreVersion")
}
}

Expand Down
2 changes: 1 addition & 1 deletion examples/kim-java-sample/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion examples/kim-kotlin-jvm-sample/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion examples/kim-kotlin-jvm-sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ repositories {
}

dependencies {
implementation("com.ashampoo:kim:0.17")
implementation("com.ashampoo:kim:0.17.1")
}
Binary file added examples/kim-kotlin-jvm-sample/empty.tif
Binary file not shown.
48 changes: 46 additions & 2 deletions examples/kim-kotlin-jvm-sample/src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import com.ashampoo.kim.Kim
import com.ashampoo.kim.common.ByteOrder
import com.ashampoo.kim.format.jpeg.JpegRewriter
import com.ashampoo.kim.format.tiff.constant.ExifTag
import com.ashampoo.kim.format.tiff.constant.GeoTiffTag
import com.ashampoo.kim.format.tiff.write.TiffOutputSet
import com.ashampoo.kim.format.tiff.write.TiffWriterLossless
import com.ashampoo.kim.format.tiff.write.TiffWriterLossy
import com.ashampoo.kim.input.JvmInputStreamByteReader
import com.ashampoo.kim.input.use
import com.ashampoo.kim.model.MetadataUpdate
Expand All @@ -18,7 +21,9 @@ fun main() {

updateTakenDateLowLevelApi()

setGeoTiff()
setGeoTiffToJpeg()

setGeoTiffToTif()
}

fun printMetadata() {
Expand Down Expand Up @@ -76,7 +81,7 @@ fun updateTakenDateLowLevelApi() {
}
}

fun setGeoTiff() {
fun setGeoTiffToJpeg() {

val inputFile = File("testphoto.jpg")
val outputFile = File("testphoto_mod3.jpg")
Expand Down Expand Up @@ -111,3 +116,42 @@ fun setGeoTiff() {
)
}
}

fun setGeoTiffToTif() {

val inputFile = File("empty.tif")
val outputFile = File("geotiff.tif")

val metadata = Kim.readMetadata(inputFile) ?: 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)
)

OutputStreamByteWriter(outputFile.outputStream()).use { outputStreamByteWriter ->

val writer = TiffWriterLossy(
ByteOrder.LITTLE_ENDIAN
)

writer.write(
byteWriter = outputStreamByteWriter,
outputSet = outputSet
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ enum class IptcTypes(

init {

for (iptcType in IptcTypes.values())
for (iptcType in IptcTypes.entries)
iptcTypeMap[iptcType.type] = iptcType
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ data class TiffContents(

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

fun createOutputSet(): TiffOutputSet {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TiffField> = 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,
Expand Down Expand Up @@ -120,22 +124,38 @@ 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)
}

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 {

/*
Expand Down Expand Up @@ -209,7 +229,9 @@ class TiffDirectory(
)
}

outputDirectory.setJpegImageData(jpegImageDataElement)
outputDirectory.setThumbnailImageDataElement(thumbnailImageDataElement)

outputDirectory.setTiffImageDataElement(tiffImageDataElement)

return outputDirectory

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ class TiffField(
return intArrayOf(value.toInt())

if (value is IntArray)
return value.copyOf()
return value

if (value is ShortArray) {

Expand Down
Original file line number Diff line number Diff line change
@@ -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"

}
44 changes: 40 additions & 4 deletions src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
}
Loading

0 comments on commit e1f8ce8

Please sign in to comment.