Skip to content

Commit

Permalink
Some tag infos added (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanOltmann authored Dec 21, 2023
1 parent 4553e9b commit 35c988d
Show file tree
Hide file tree
Showing 49 changed files with 1,196 additions and 79 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ of Ashampoo Photos, which, in turn, is driven by user community feedback.
## Installation

```
implementation("com.ashampoo:kim:0.8.1")
implementation("com.ashampoo:kim:0.8.2")
```

## Sample usages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ object JpegConstants {
const val JPEG_APP0 = 0xE0
const val JPEG_APP0_MARKER = 0xFF00 or JPEG_APP0
const val JPEG_APP1_MARKER = 0xFF00 or JPEG_APP0 + 1
const val JPEG_APP2_MARKER = 0xFF00 or JPEG_APP0 + 2
const val JPEG_APP13_MARKER = 0xFF00 or JPEG_APP0 + 13
const val JPEG_APP15_MARKER = 0xFF00 or JPEG_APP0 + 15

Expand All @@ -114,7 +115,7 @@ object JpegConstants {
const val SOF15_MARKER = 0xFFC0 + 0xF

// marker for restart intervals
const val DRI_MARKER = 0xFFdd
const val DRI_MARKER = 0xFFDD
const val RST0_MARKER = 0xFFD0
const val RST1_MARKER = 0xFFD0 + 0x1
const val RST2_MARKER = 0xFFD0 + 0x2
Expand All @@ -124,11 +125,14 @@ object JpegConstants {
const val RST6_MARKER = 0xFFD0 + 0x6
const val RST7_MARKER = 0xFFD0 + 0x7

const val SOI_MARKER = 0xFFD8
const val EOI_MARKER = 0xFFD9
const val SOS_MARKER = 0xFFDA
const val DQT_MARKER = 0xFFDB
const val DNL_MARKER = 0xFFDC
const val COM_MARKER = 0xFFFE

const val COM_MARKER_1 = 0xFFFE
const val COM_MARKER_2 = 0xFFEE

val SOFN_MARKERS = listOf(
JpegConstants.SOF0_MARKER,
Expand All @@ -146,6 +150,27 @@ object JpegConstants {
JpegConstants.SOF15_MARKER
)

@OptIn(ExperimentalStdlibApi::class)
fun markerDescription(marker: Int): String =
when (marker) {
COM_MARKER_1 -> "COM (Comment)"
COM_MARKER_2 -> "COM (Comment)"
DHT_MARKER -> "DHT (Define Huffman Table)"
DQT_MARKER -> "DQT (Define Quantization Table)"
DRI_MARKER -> "DRI (Define Restart Interval)"
EOI_MARKER -> "EOI (End of Image)"
JPEG_APP0_MARKER -> "APP0 JFIF"
JPEG_APP1_MARKER -> "APP1"
JPEG_APP2_MARKER -> "APP2"
JPEG_APP13_MARKER -> "APP13 IPTC"
JPEG_APP15_MARKER -> "APP15"
SOF0_MARKER -> "SOF0 (Start of Frame, Baseline DCT)"
SOF2_MARKER -> "SOF2 (Start of Frame, Progressive DCT)"
SOI_MARKER -> "SOI (Start of Image)"
SOS_MARKER -> "SOS (Start of Scan)"
else -> marker.toShort().toHexString(HexFormat.UpperCase)
}

// val MARKERS = listOf(
// JPEG_APP0, JPEG_APP0_MARKER,
// JPEG_APP1_MARKER, JPEG_APP2_MARKER, JPEG_APP13_MARKER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import com.ashampoo.kim.common.toSingleNumberHexes
import com.ashampoo.kim.common.tryWithImageReadException
import com.ashampoo.kim.format.ImageFormatMagicNumbers
import com.ashampoo.kim.format.jpeg.JpegConstants.JPEG_BYTE_ORDER
import com.ashampoo.kim.format.jpeg.JpegMetadataExtractor.MARKER_END_OF_IMAGE
import com.ashampoo.kim.format.jpeg.JpegMetadataExtractor.SEGMENT_IDENTIFIER
import com.ashampoo.kim.format.jpeg.JpegMetadataExtractor.SEGMENT_START_OF_SCAN
import com.ashampoo.kim.format.tiff.TiffReader
import com.ashampoo.kim.format.tiff.constants.TiffConstants.TIFF_ENTRY_LENGTH
import com.ashampoo.kim.format.tiff.constants.TiffConstants.TIFF_HEADER_SIZE
Expand All @@ -33,9 +36,6 @@ import com.ashampoo.kim.input.ByteReader
*/
object JpegOrientationOffsetFinder {

const val SEGMENT_IDENTIFIER = 0xFF.toByte()
const val SEGMENT_START_OF_SCAN = 0xDA.toByte()
const val MARKER_END_OF_IMAGE = 0xD9.toByte()
const val APP1_MARKER = 0xE1.toByte()

@OptIn(ExperimentalStdlibApi::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright 2023 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.jpeg

import com.ashampoo.kim.common.ImageReadException
import com.ashampoo.kim.common.toSingleNumberHexes
import com.ashampoo.kim.common.toUInt16
import com.ashampoo.kim.common.tryWithImageReadException
import com.ashampoo.kim.format.ImageFormatMagicNumbers
import com.ashampoo.kim.format.jpeg.JpegConstants.EOI_MARKER
import com.ashampoo.kim.format.jpeg.JpegConstants.JPEG_APP1_MARKER
import com.ashampoo.kim.format.jpeg.JpegConstants.JPEG_BYTE_ORDER
import com.ashampoo.kim.format.jpeg.JpegConstants.SOI_MARKER
import com.ashampoo.kim.format.jpeg.JpegConstants.markerDescription
import com.ashampoo.kim.format.jpeg.JpegMetadataExtractor.SEGMENT_IDENTIFIER
import com.ashampoo.kim.format.jpeg.JpegMetadataExtractor.SEGMENT_START_OF_SCAN
import com.ashampoo.kim.input.ByteReader

/**
* Algorithm to find segment offsets, types and lengths
*/
object JpegSegmentAnalyzer {

@OptIn(ExperimentalStdlibApi::class)
@Throws(ImageReadException::class)
@Suppress("ComplexMethod")
fun findSegmentInfos(
byteReader: ByteReader
): List<JpegSegmentInfo> = tryWithImageReadException {

val soiMarker = byteReader.read2BytesAsInt("SOI", JPEG_BYTE_ORDER)

require(soiMarker == SOI_MARKER) {
"JPEG magic number mismatch: ${soiMarker.toHexString()}"
}

val segmentInfos = mutableListOf<JpegSegmentInfo>()

segmentInfos.add(
JpegSegmentInfo(
offset = 0,
marker = SOI_MARKER,
length = 2
)
)

var positionCounter: Long = ImageFormatMagicNumbers.jpeg.size.toLong()

@Suppress("LoopWithTooManyJumpStatements")
do {

var segmentIdentifier = byteReader.readByte() ?: break
var segmentType = byteReader.readByte() ?: break

positionCounter += 2

/*
* Find the segment marker. Markers are zero or more 0xFF bytes, followed by
* a 0xFF and then a byte not equal to 0x00 or 0xFF.
*/
while (
segmentIdentifier != SEGMENT_IDENTIFIER ||
segmentType == SEGMENT_IDENTIFIER ||
segmentType.toInt() == 0
) {

segmentIdentifier = segmentType

val nextSegmentType = byteReader.readByte() ?: break

positionCounter++

segmentType = nextSegmentType
}

if (segmentType == SEGMENT_START_OF_SCAN) {

val remainingBytesCount = byteReader.contentLength - positionCounter

segmentInfos.add(
JpegSegmentInfo(
offset = positionCounter - 2,
marker = byteArrayOf(segmentIdentifier, segmentType).toUInt16(JPEG_BYTE_ORDER),
length = remainingBytesCount.toInt()
)
)

byteReader.skipBytes("image bytes", remainingBytesCount - 2)

positionCounter += remainingBytesCount

val eoiMarker = byteReader.read2BytesAsInt("EOI", JPEG_BYTE_ORDER)

if (eoiMarker == EOI_MARKER) {

/* Write the EOI marker if it's really there. */

segmentInfos.add(
JpegSegmentInfo(
offset = positionCounter - 2,
marker = EOI_MARKER,
length = 2
)
)
}

break
}

/* Note: Segment length includes size bytes */
val remainingSegmentLength =
byteReader.read2BytesAsInt("segmentLength", JPEG_BYTE_ORDER) - 2

segmentInfos.add(
JpegSegmentInfo(
offset = positionCounter - 2,
marker = byteArrayOf(segmentIdentifier, segmentType).toUInt16(JPEG_BYTE_ORDER),
length = remainingSegmentLength + 4
)
)

positionCounter += 2

if (remainingSegmentLength <= 0)
throw ImageReadException("Illegal JPEG segment length: $remainingSegmentLength")

byteReader.skipBytes("skip segment", remainingSegmentLength.toLong())

positionCounter += remainingSegmentLength

} while (true)

return segmentInfos
}

data class JpegSegmentInfo(
val offset: Long,
val marker: Int,
val length: Int
) {

override fun toString(): String =
"$offset = ${markerDescription(marker)} [$length bytes]"
}
}
11 changes: 10 additions & 1 deletion src/commonMain/kotlin/com/ashampoo/kim/format/tiff/TiffField.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.ashampoo.kim.format.tiff

import com.ashampoo.kim.common.ByteOrder
import com.ashampoo.kim.common.HEX_RADIX
import com.ashampoo.kim.common.ImageReadException
import com.ashampoo.kim.common.RationalNumber
import com.ashampoo.kim.common.head
Expand All @@ -40,6 +41,10 @@ class TiffField(
val sortHint: Int
) {

/** Return a proper Tag ID like 0x0100 */
val tagFormatted: String =
"0x" + tag.toString(HEX_RADIX).padStart(4, '0')

val tagInfo: TagInfo = getTag(directoryType, tag)

val isLocalValue: Boolean = count * fieldType.size <= TiffConstants.TIFF_ENTRY_MAX_VALUE_LENGTH
Expand Down Expand Up @@ -142,8 +147,12 @@ class TiffField(
else
(value as Number).toDouble()

/*
* Note that we need to show the local 'tagFormatted', because
* 'tagInfo' might be an Unknown tag and show a placeholder.
*/
override fun toString(): String =
"${tagInfo.description} = $valueDescription"
"$tagFormatted ${tagInfo.name} = $valueDescription"

fun createOversizeValueElement(): TiffElement? =
if (isLocalValue) null else OversizeValueElement(offset.toInt(), valueBytes.size)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ object ExifTag {
TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD
)

val EXIF_TAG_INTEROPERABILITY_RELATED_IMAGE_WIDTH = TagInfoShort(
"RelatedImageWidth", 0x1001,
TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD
)

val EXIF_TAG_INTEROPERABILITY_RELATED_IMAGE_HEIGHT = TagInfoShort(
"RelatedImageHeight", 0x1002,
TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD
)

val EXIF_TAG_PROCESSING_SOFTWARE = TagInfoAscii(
"ProcessingSoftware", 0x000b, -1,
TIFF_DIRECTORY_IFD0
Expand Down Expand Up @@ -501,6 +511,11 @@ object ExifTag {
TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD
)

val EXIF_TAG_OFFSET_TIME = TagInfoAscii(
"OffsetTime", 0x9010, -1,
TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD
)

val EXIF_TAG_OFFSET_TIME_ORIGINAL = TagInfoAscii(
"OffsetTimeOriginal", 0x9011, -1,
TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD
Expand Down Expand Up @@ -883,8 +898,37 @@ object ExifTag {
TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD
)

val EXIF_TAG_ICC_PROFILE_OFFSET = TagInfoUndefined(
"ICC_Profile", 0x8773,
TIFF_DIRECTORY_IFD0
)

/* Affinity Photo creates it's own tag with custom data. */
val EXIF_TAG_AFFINITY_PHOTO_OFFSET = TagInfoUndefined(
"AffinityPhoto", 0xC7E0,
TIFF_DIRECTORY_IFD0
)

/*
* Page 18 of the XMPSpecificationPart1.pdf:
* When XMP is embedded within digital files, including white-space padding
* is sometimes helpful. Doing so facilitates modification of the XMP packet
* in-place. The rest of the file is unaffected, which could eliminate a need
* to rewrite the entire file if the XMP changes in size. Appropriate padding
* is SPACE characters placed anywhere white space is allowed by the general
* XML syntax and XMP serialization rules, with a linefeed (U+000A) every
* 100 characters or so to improve human display. The amount of padding is
* workflow-dependent; around 2000 bytes is often a reasonable amount.
*/
val EXIF_TAG_PADDING = TagInfoUndefined(
"Padding", 0xEA1C,
TIFF_DIRECTORY_IFD0
)

val ALL_EXIF_TAGS = listOf(
EXIF_TAG_INTEROPERABILITY_INDEX, EXIF_TAG_INTEROPERABILITY_VERSION,
EXIF_TAG_INTEROPERABILITY_RELATED_IMAGE_WIDTH,
EXIF_TAG_INTEROPERABILITY_RELATED_IMAGE_HEIGHT,
EXIF_TAG_PROCESSING_SOFTWARE,
EXIF_TAG_SOFTWARE,
EXIF_TAG_PREVIEW_IMAGE_START_IFD0,
Expand Down Expand Up @@ -938,7 +982,7 @@ object ExifTag {
EXIF_TAG_SUBJECT_AREA,
EXIF_TAG_STO_NITS, EXIF_TAG_SUB_SEC_TIME,
EXIF_TAG_SUB_SEC_TIME_ORIGINAL, EXIF_TAG_SUB_SEC_TIME_DIGITIZED,
EXIF_TAG_OFFSET_TIME_ORIGINAL,
EXIF_TAG_OFFSET_TIME, EXIF_TAG_OFFSET_TIME_ORIGINAL,
EXIF_TAG_FLASHPIX_VERSION,
EXIF_TAG_EXIF_IMAGE_WIDTH, EXIF_TAG_EXIF_IMAGE_LENGTH,
EXIF_TAG_RELATED_SOUND_FILE, EXIF_TAG_INTEROP_OFFSET,
Expand Down Expand Up @@ -968,6 +1012,8 @@ object ExifTag {
EXIF_TAG_MOIRE_FILTER, EXIF_TAG_USER_COMMENT,
EXIF_TAG_MAKER_NOTE, EXIF_TAG_RATING, EXIF_TAG_RATING_PERCENT,
EXIF_TAG_SUB_IFDS_OFFSET, EXIF_TAG_MODIFY_DATE, EXIF_TAG_SENSITIVITY_TYPE,
EXIF_TAG_RECOMMENDED_EXPOSURE_INDEX, EXIF_TAG_COLOR_SPACE
EXIF_TAG_RECOMMENDED_EXPOSURE_INDEX, EXIF_TAG_COLOR_SPACE,
EXIF_TAG_ICC_PROFILE_OFFSET, EXIF_TAG_AFFINITY_PHOTO_OFFSET,
EXIF_TAG_PADDING
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ open class TagInfo(
/**
* @param entry the TIFF field whose value to return
* @return the value of the TIFF field
*
* Implementation detail: This indirection exists because
* [TagInfoGpsText] has some special logic to interpret the value.
*/
open fun getValue(entry: TiffField): Any =
entry.fieldType.getValue(entry)
Expand Down
Loading

0 comments on commit 35c988d

Please sign in to comment.