Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: TiffReader does not read strip bytes by default #90

Merged
merged 3 commits into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.2")
implementation("com.ashampoo:kim:0.17.3")
```

For the targets `wasmJs` & `js` you also need to specify this:
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.2'
implementation 'com.ashampoo:kim:0.17.3'
}

// Needed to make it work for the Gradle java plugin
Expand Down
3 changes: 1 addition & 2 deletions examples/kim-kotlin-jvm-sample/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,4 @@ bin/
/testphoto_mod2.jpg
/testphoto_mod3.jpg
/geotiff.tif
/geotiff_kotlinxio.tif
/geotiff_lowlevel.tif
/geotiff_kotlinx.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.2")
implementation("com.ashampoo:kim:0.17.3")
}
93 changes: 33 additions & 60 deletions examples/kim-kotlin-jvm-sample/src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ fun main() {
setGeoTiffToJpeg()
setGeoTiffToTiff()
setGeoTiffToTiffUsingKotlinx()
setGeoTiffToTiffUsingKotlinxAndTiffReader()
}

fun printMetadata() {
Expand Down Expand Up @@ -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()

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

Expand Down Expand Up @@ -267,3 +239,4 @@ fun setGeoTiffToTiffUsingKotlinxAndTiffReader() {

outputPath.writeBytes(updatedBytes)
}

Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant

fun ImageMetadata.convertToPhotoMetadata(
includeThumbnail: Boolean = false,
ignoreOrientation: Boolean = false
): PhotoMetadata {

Expand Down Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions src/commonMain/kotlin/com/ashampoo/kim/format/ImageParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -32,6 +33,7 @@ fun interface ImageParser {

companion object {

@JvmStatic
fun forFormat(imageFormat: ImageFormat): ImageParser? =
when (imageFormat) {

Expand Down
27 changes: 24 additions & 3 deletions src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)

Expand All @@ -78,6 +93,7 @@ object TiffReader {
directoryOffset = tiffHeader.offsetToFirstIFD,
directoryType = TiffConstants.TIFF_DIRECTORY_TYPE_IFD0,
visitedOffsets = mutableListOf<Int>(),
readTiffImageBytes = readTiffImageBytes,
addDirectory = {
directories.add(it)
}
Expand All @@ -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")
Expand Down Expand Up @@ -125,6 +141,7 @@ object TiffReader {
directoryOffset: Int,
directoryType: Int,
visitedOffsets: MutableList<Int>,
readTiffImageBytes: Boolean,
addDirectory: (TiffDirectory) -> Unit
): Boolean {

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -228,6 +245,7 @@ object TiffReader {
directoryOffset = subDirOffset,
directoryType = subDirectoryType,
visitedOffsets = visitedOffsets,
readTiffImageBytes = readTiffImageBytes,
addDirectory = addDirectory
)

Expand All @@ -250,6 +268,7 @@ object TiffReader {
directoryOffset = directory.nextDirectoryOffset,
directoryType = directoryType + 1,
visitedOffsets = visitedOffsets,
readTiffImageBytes = readTiffImageBytes,
addDirectory = addDirectory
)

Expand Down Expand Up @@ -516,6 +535,7 @@ object TiffReader {
directoryOffset = makerNoteValueOffset,
directoryType = TiffConstants.TIFF_MAKER_NOTE_CANON,
visitedOffsets = mutableListOf<Int>(),
readTiffImageBytes = false,
addDirectory = addDirectory
)
}
Expand Down Expand Up @@ -547,6 +567,7 @@ object TiffReader {
directoryOffset = makerNoteValueOffset + 18,
directoryType = TiffConstants.TIFF_MAKER_NOTE_NIKON,
visitedOffsets = mutableListOf<Int>(),
readTiffImageBytes = false,
addDirectory = addDirectory
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,16 +463,11 @@ class TiffOutputDirectory(
private fun removeFieldIfPresent(tagInfo: TagInfo) =
findField(tagInfo)?.let { field -> fields.remove(field) }

fun getOutputItems(
outputSummary: TiffOffsetItems
): List<TiffOutputItem> {
fun getOutputItems(tiffOffsetItems: TiffOffsetItems): List<TiffOutputItem> {

/* 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

Expand All @@ -488,7 +483,7 @@ class TiffOutputDirectory(

val lengthValue = FieldTypeLong.writeData(
thumbnailBytes!!.size,
outputSummary.byteOrder
tiffOffsetItems.byteOrder
)

val jpegLengthField = TiffOutputField(
Expand All @@ -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,
Expand All @@ -513,7 +512,7 @@ class TiffOutputDirectory(

val lengthValue = FieldTypeLong.writeData(
tiffImageBytes!!.size,
outputSummary.byteOrder
tiffOffsetItems.byteOrder
)

val stripByteCountsField = TiffOutputField(
Expand All @@ -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(
Expand Down Expand Up @@ -564,7 +563,7 @@ class TiffOutputDirectory(

result.add(item)

outputSummary.addOffsetItem(TiffOffsetItem(item, thumbnailOffsetField!!))
tiffOffsetItems.addOffsetItem(TiffOffsetItem(item, thumbnailOffsetField!!))
}

if (tiffImageBytes != null) {
Expand All @@ -576,7 +575,7 @@ class TiffOutputDirectory(

result.add(item)

outputSummary.addOffsetItem(TiffOffsetItem(item, stripOffsetField!!))
tiffOffsetItems.addOffsetItem(TiffOffsetItem(item, stripOffsetField!!))
}

return result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ class TiffOutputSet(

private val directories = mutableListOf<TiffOutputDirectory>()

fun getOutputItems(outputSummary: TiffOffsetItems): List<TiffOutputItem> {
fun getOutputItems(tiffOffsetItems: TiffOffsetItems): List<TiffOutputItem> {

val outputItems = mutableListOf<TiffOutputItem>()

for (directory in directories)
outputItems.addAll(directory.getOutputItems(outputSummary))
outputItems.addAll(directory.getOutputItems(tiffOffsetItems))

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

Expand Down
Loading
Loading