Skip to content

Commit

Permalink
Fixes for some problematic JPEG files (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanOltmann authored Feb 16, 2024
1 parent a12f08b commit fbc57be
Show file tree
Hide file tree
Showing 51 changed files with 1,110 additions and 1,582 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ of Ashampoo Photos, which, in turn, is driven by user community feedback.
## Installation

```
implementation("com.ashampoo:kim:0.14")
implementation("com.ashampoo:kim:0.14.1")
```

## Sample usages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ package com.ashampoo.kim.common

import com.ashampoo.kim.Kim.underUnitTesting
import com.ashampoo.kim.format.ImageMetadata
import com.ashampoo.kim.format.jpeg.JpegImageParser
import com.ashampoo.kim.format.jpeg.iptc.IptcTypes
import com.ashampoo.kim.format.tiff.GPSInfo
import com.ashampoo.kim.format.tiff.constant.ExifTag
import com.ashampoo.kim.format.tiff.constant.TiffConstants
import com.ashampoo.kim.format.tiff.constant.TiffTag
import com.ashampoo.kim.format.xmp.XmpReader
import com.ashampoo.kim.input.ByteArrayByteReader
import com.ashampoo.kim.model.GpsCoordinates
import com.ashampoo.kim.model.PhotoMetadata
import com.ashampoo.kim.model.TiffOrientation
Expand Down Expand Up @@ -86,6 +88,17 @@ fun ImageMetadata.convertToPhotoMetadata(
XmpReader.readMetadata(it)
}

val thumbnailBytes = if (includeThumbnail)
getExifThumbnailBytes()
else
null

val thumbnailImageSize = thumbnailBytes?.let {
JpegImageParser.getImageSize(
ByteArrayByteReader(thumbnailBytes)
)
}

/*
* Embedded XMP metadata has higher priority than EXIF or IPTC
* for certain fields because it's the newer format. Some fields
Expand Down Expand Up @@ -113,7 +126,8 @@ fun ImageMetadata.convertToPhotoMetadata(
keywords = keywords.ifEmpty { xmpMetadata?.keywords ?: emptySet() },
faces = xmpMetadata?.faces ?: emptyMap(),
personsInImage = xmpMetadata?.personsInImage ?: emptySet(),
thumbnailBytes = if (includeThumbnail) getExifThumbnailBytes() else null
thumbnailImageSize = thumbnailImageSize,
thumbnailBytes = thumbnailBytes
)
}

Expand Down
30 changes: 23 additions & 7 deletions src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffField.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class TiffField(
if (value.size == 1)
return@lazy value.first().toString()

if (value.size <= MAX_BYTE_ARRAY_DISPLAY_SIZE)
if (value.size <= MAX_ARRAY_LENGTH_DISPLAY_SIZE)
return@lazy "[${value.toSingleNumberHexes()}]"

return@lazy "[${value.size} bytes]"
Expand All @@ -84,39 +84,54 @@ class TiffField(
if (value.size == 1)
return@lazy value.first().toString()

return@lazy value.contentToString()
if (value.size <= MAX_ARRAY_LENGTH_DISPLAY_SIZE)
return@lazy value.contentToString()

return@lazy "[${value.size} ints]"
}

if (value is ShortArray) {

if (value.size == 1)
return@lazy value.first().toString()

return@lazy value.contentToString()
if (value.size <= MAX_ARRAY_LENGTH_DISPLAY_SIZE)
return@lazy value.contentToString()

return@lazy "[${value.size} shorts]"
}

if (value is DoubleArray) {

if (value.size == 1)
return@lazy value.first().toString()

return@lazy value.contentToString()
if (value.size <= MAX_ARRAY_LENGTH_DISPLAY_SIZE)
return@lazy value.contentToString()

return@lazy "[${value.size} doubles]"
}

if (value is FloatArray) {

if (value.size == 1)
return@lazy value.first().toString()

return@lazy value.contentToString()
if (value.size <= MAX_ARRAY_LENGTH_DISPLAY_SIZE)
return@lazy value.contentToString()

return@lazy "[${value.size} floats]"
}

if (value is RationalNumbers) {

if (value.values.size == 1)
return@lazy value.values.first().toString()

return@lazy value.values.contentToString()
if (value.values.size <= MAX_ARRAY_LENGTH_DISPLAY_SIZE)
return@lazy value.values.contentToString()

return@lazy "[${value.values.size} rationals]"
}

value.toString()
Expand Down Expand Up @@ -187,6 +202,7 @@ class TiffField(
is ShortArray -> value.first().toDouble()
is IntArray -> value.first().toDouble()
is FloatArray -> value.first().toDouble()
is DoubleArray -> value.first().toDouble()
else -> (value as Number).toDouble()
}

Expand All @@ -212,6 +228,6 @@ class TiffField(

companion object {

private const val MAX_BYTE_ARRAY_DISPLAY_SIZE = 10
private const val MAX_ARRAY_LENGTH_DISPLAY_SIZE = 10
}
}
27 changes: 22 additions & 5 deletions src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ package com.ashampoo.kim.format.tiff
import com.ashampoo.kim.common.ByteOrder
import com.ashampoo.kim.common.ImageReadException
import com.ashampoo.kim.common.head
import com.ashampoo.kim.common.startsWith
import com.ashampoo.kim.common.toInt
import com.ashampoo.kim.format.ImageFormatMagicNumbers
import com.ashampoo.kim.format.tiff.constant.ExifTag
import com.ashampoo.kim.format.tiff.constant.TiffConstants
import com.ashampoo.kim.format.tiff.constant.TiffConstants.DIRECTORY_TYPE_SUB
Expand Down Expand Up @@ -340,10 +342,15 @@ object TiffReader {
return fields
}

/**
* Reads the thumbnail image if data is valid or returns NULL if a problem was found.
*
* Discarding corrupt thumbnails is not a big issue, so no exceptions will be thrown here.
*/
private fun getJpegRawImageData(
byteReader: RandomAccessByteReader,
directory: TiffDirectory
): JpegImageDataElement {
): JpegImageDataElement? {

val element = directory.getJpegRawImageDataElement()

Expand All @@ -356,10 +363,20 @@ object TiffReader {
if (offset + length > byteReader.contentLength)
length = (byteReader.contentLength - offset).toInt()

val data = byteReader.readBytes(offset.toInt(), length)
if (length <= 0)
return null

val bytes = byteReader.readBytes(offset.toInt(), length)

if (data.size != length)
throw ImageReadException("Unexpected length: Wanted $length, but got ${data.size}")
if (bytes.size != length)
return null

/*
* Ignore it if it's not a JPEG.
* Some files have random garbage bytes here.
*/
if (!bytes.startsWith(ImageFormatMagicNumbers.jpeg))
return null

/*
* Note: Apache Commons Imaging has a validation check here to ensure that
Expand All @@ -369,7 +386,7 @@ object TiffReader {
* there are some random bytes present.
*/

return JpegImageDataElement(offset, length, data)
return JpegImageDataElement(offset, length, bytes)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,27 @@ class ByteArrayByteReader(
}

override fun moveTo(position: Int) {

require(position <= contentLength) {
"Can't move to $position in content of $contentLength bytes."
}

this.currentPosition = position
}

override fun readBytes(offset: Int, length: Int): ByteArray =
bytes.copyOfRange(offset, offset + length)
override fun readBytes(offset: Int, length: Int): ByteArray {

require(offset > 0) { "Offset must be positive: $offset" }
require(length > 0) { "Length must be positive: $length" }

val toIndex = offset + length

require(offset + length <= contentLength) {
"Requested to read to index $toIndex where max index is ${contentLength - 1}"
}

return bytes.copyOfRange(offset, toIndex)
}

override fun close() {
/* Does nothing. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ data class PhotoMetadata(
val personsInImage: Set<String> = emptySet(),

/* EXIF Thumbnail (IFD1) */
val thumbnailImageSize: ImageSize? = null,
val thumbnailBytes: ByteArray? = null

) {
Expand Down Expand Up @@ -136,7 +137,11 @@ data class PhotoMetadata(

/* Persons */
faces = faces.ifEmpty { other.faces },
personsInImage = personsInImage.ifEmpty { other.personsInImage }
personsInImage = personsInImage.ifEmpty { other.personsInImage },

/* EXIF Thumbnail (IFD1) */
thumbnailImageSize = thumbnailImageSize,
thumbnailBytes = thumbnailBytes
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import kotlin.test.fail
class XmpExtractionTest {

val indicesWithoutXmp = setOf(
2, 20, 23, 30, 48,
2, 18, 20, 23, 30, 48,
KimTestData.HEIC_TEST_IMAGE_INDEX,
KimTestData.GIF_TEST_IMAGE_INDEX,
KimTestData.NEF_TEST_IMAGE_INDEX,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ class PhotoMetadataConverterTest {
if (index == KimTestData.HEIC_TEST_IMAGE_INDEX)
return

val photoMetadata = Kim.readMetadata(bytes)?.convertToPhotoMetadata()
val photoMetadata = Kim.readMetadata(bytes)?.convertToPhotoMetadata(
includeThumbnail = true
)

assertNotNull(photoMetadata)

Expand All @@ -76,7 +78,7 @@ class PhotoMetadataConverterTest {
stringBuilder.appendLine(
"name;widthPx;heightPx;orientation;takenDate;latitude;longitude;" +
"cameraMake;cameraModel;lensMake;lensModel;iso;exposureTime;fNumber;" +
"focalLength;rating;keywords"
"focalLength;rating;keywords;thumbnailImageSize;thumbnailBytes.size"
)

for (entry in metadataMap.entries) {
Expand All @@ -91,7 +93,8 @@ class PhotoMetadataConverterTest {
"${metadata.cameraMake};${metadata.cameraModel};${metadata.lensMake};" +
"${metadata.lensModel};${metadata.iso};${metadata.exposureTime};" +
"${metadata.fNumber};${metadata.focalLength};${metadata.rating?.value};" +
"${metadata.keywords}"
"${metadata.keywords};${metadata.thumbnailImageSize};" +
"${metadata.thumbnailBytes?.size}"
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class JpegImageParserTest {
6 to ImageSize(3000, 1688),
7 to ImageSize(6000, 3376),
8 to ImageSize(5472, 3648),
9 to ImageSize(2862, 1905),
9 to ImageSize(5946, 3964),
10 to ImageSize(3320, 2490),
11 to ImageSize(4094, 2699),
12 to ImageSize(3482, 2460),
Expand All @@ -41,8 +41,8 @@ class JpegImageParserTest {
15 to ImageSize(5045, 4000),
16 to ImageSize(2072, 2590),
17 to ImageSize(3136, 3919),
18 to ImageSize(5332, 2973),
19 to ImageSize(3948, 2632),
18 to ImageSize(3456, 2304),
19 to ImageSize(2468, 4051),
20 to ImageSize(250, 250),
21 to ImageSize(4522, 6783),
22 to ImageSize(3024, 4032),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class JpegOrientationOffsetFinderTest {
1 to 72,
2 to 72,
15 to 85,
19 to 54,
20 to 84,
21 to 55,
23 to 43,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,23 +317,6 @@ class JpegSegmentAnalyzerTest {
JpegSegmentAnalyzer.JpegSegmentInfo(46366, 65498, 985753),
JpegSegmentAnalyzer.JpegSegmentInfo(1032119, 65497, 2),
),
19 to listOf(
JpegSegmentAnalyzer.JpegSegmentInfo(0, 65496, 2),
JpegSegmentAnalyzer.JpegSegmentInfo(2, 65505, 6030),
JpegSegmentAnalyzer.JpegSegmentInfo(6032, 65517, 7488),
JpegSegmentAnalyzer.JpegSegmentInfo(13520, 65505, 24959),
JpegSegmentAnalyzer.JpegSegmentInfo(38479, 65506, 578),
JpegSegmentAnalyzer.JpegSegmentInfo(39057, 65518, 16),
JpegSegmentAnalyzer.JpegSegmentInfo(39073, 65499, 69),
JpegSegmentAnalyzer.JpegSegmentInfo(39142, 65499, 69),
JpegSegmentAnalyzer.JpegSegmentInfo(39211, 65472, 19),
JpegSegmentAnalyzer.JpegSegmentInfo(39230, 65476, 30),
JpegSegmentAnalyzer.JpegSegmentInfo(39260, 65476, 75),
JpegSegmentAnalyzer.JpegSegmentInfo(39335, 65476, 27),
JpegSegmentAnalyzer.JpegSegmentInfo(39362, 65476, 49),
JpegSegmentAnalyzer.JpegSegmentInfo(39411, 65498, 1700456),
JpegSegmentAnalyzer.JpegSegmentInfo(1739867, 65497, 2),
),
20 to listOf(
JpegSegmentAnalyzer.JpegSegmentInfo(0, 65496, 2),
JpegSegmentAnalyzer.JpegSegmentInfo(2, 65504, 18),
Expand Down Expand Up @@ -835,6 +818,10 @@ class JpegSegmentAnalyzerTest {

for (index in 1..KimTestData.HIGHEST_JPEG_INDEX) {

// TODO Refresh data
if (index == 9 || index == 18 || index == 19)
continue

val bytes = KimTestData.getBytesOf(index)

val byteReader = ByteArrayByteReader(bytes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ class WebPWriterTest {

val newBytes = byteWriter.toByteArray()

Path("test.webp").writeBytes(newBytes)

assertContentEquals(
expected = bytes,
actual = newBytes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ object KimTestData {

@Suppress("MagicNumber")
val photoIdsWithExifThumbnail: Set<Int> = setOf(
2, 3, 4, 5, 6, 7, 8, 10, 12, 15, 16, 18, 19, 20, 21,
2, 3, 4, 5, 6, 7, 8, 10, 12, 15, 16, 19, 20, 21,
22, 24, 25, 27, 28, 29, 30, 31, 32, 33, 35, 37,
38, 39, 40, 41, 42, 44, 45, 46, 47, 48, 49, 50,
PNG_TEST_IMAGE_INDEX,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Special photos

photo_9.jpg = Corrupted thumbnail data, but intact IFD1
photo_18.jpg = Corrupted IFD1 (thumbnail)
photo_19.jpg = F-Number is stored as double array
photo_20.jpg = EXIF DateTimeOriginal with all zeros
photo_21.jpg = Corrupted IFD1
photo_21.jpg = Corrupted IFD1 (thumbnail)
photo_22.jpg = Non-corrupted version of photo_30.jpg
photo_23.jpg = Nothing Phone OOC-JPEG (50 MP)
photo_30.jpg = Multiple APP1
Expand Down
Binary file modified src/commonTest/resources/com/ashampoo/kim/testdata/full/photo_18.jpg
100755 → 100644
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/commonTest/resources/com/ashampoo/kim/testdata/full/photo_9.jpg
100755 → 100644
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit fbc57be

Please sign in to comment.