Skip to content

Commit

Permalink
Basic GeoTiff support (#84)
Browse files Browse the repository at this point in the history
Implemented basic GeoTiff read support. See #26.
  • Loading branch information
StefanOltmann authored Apr 1, 2024
1 parent 1eb257d commit 24c5cc7
Show file tree
Hide file tree
Showing 46 changed files with 862 additions and 3,753 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@
/build/
/local.properties
/.idea/kim.iml
/src/commonTest/resources/com/ashampoo/kim/testdata/full/*.html
3 changes: 2 additions & 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.16.4")
implementation("com.ashampoo:kim:0.17")
```

For the targets `wasmJs` & `js` you also need to specify this:
Expand Down Expand Up @@ -159,6 +159,7 @@ See the [Java example project](examples/kim-java-sample) how to use Kim in Java
+ Can't extract preview image of ORF as offsets are burried into MakerNote.
+ Can't identify lens info of NEF, ARW, RW2 & ORF because this is constructed from MakerNote fields.
+ Missing image size for RW2 as this is also burried in MakerNotes.
* GeoTiff support is limited and supports only reading at this time.

### Regarding HEIC & AVIF metadata

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
*/
package com.ashampoo.kim.format.tiff

import com.ashampoo.kim.format.tiff.geotiff.GeoTiffDirectory
import com.ashampoo.kim.format.tiff.taginfo.TagInfo
import com.ashampoo.kim.format.tiff.write.TiffOutputSet

data class TiffContents(
val header: TiffHeader,
val directories: List<TiffDirectory>,
/** Artifical MakerNote directory */
val makerNoteDirectory: TiffDirectory?
/** Artificial MakerNote directory */
val makerNoteDirectory: TiffDirectory?,
/** Artificial GeoTiff directory */
val geoTiffDirectory: GeoTiffDirectory?
) {

fun findTiffField(tagInfo: TagInfo): TiffField? =
Expand Down Expand Up @@ -72,6 +75,10 @@ data class TiffContents(
sb.appendLine(makerNoteDirectory)
}

geoTiffDirectory?.let {
sb.appendLine(geoTiffDirectory)
}

return sb.toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -260,11 +260,29 @@ class TiffDirectory(
* Note: Keep in sync with TiffTags.getTag()
*/
@Suppress("UnnecessaryParentheses")
fun findTiffField(directories: List<TiffDirectory>, tagInfo: TagInfo): TiffField? =
directories.firstOrNull { directory ->
directory.type == tagInfo.directoryType?.typeId ||
(tagInfo.directoryType?.isImageDirectory == true && directory.type >= 0) ||
(tagInfo.directoryType?.isImageDirectory == false && directory.type < 0)
}?.findField(tagInfo)
fun findTiffField(directories: List<TiffDirectory>, tagInfo: TagInfo): TiffField? {

/*
* TagInfos that specify a directory (like GPS and MakerNotes)
* should be exact matches.
*/
if (tagInfo.directoryType != null) {

directories
.firstOrNull { directory -> directory.type == tagInfo.directoryType.typeId }
?.findField(tagInfo)
}

/*
* All others are matched with all directories.
*/
for (directory in directories) {
directory.findField(tagInfo)?.let {
return it
}
}

return null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ class TiffField(

companion object {

private const val MAX_ARRAY_LENGTH_DISPLAY_SIZE = 10
/**
* Limit to 16 bytes, so that a GeoTiff ModelTransformationTag
* is still displayed in full, but not values greater than that.
*/
private const val MAX_ARRAY_LENGTH_DISPLAY_SIZE = 16
}
}
44 changes: 41 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 @@ -23,13 +23,15 @@ 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.GeoTiffTag
import com.ashampoo.kim.format.tiff.constant.TiffConstants
import com.ashampoo.kim.format.tiff.constant.TiffConstants.EXIF_SUB_IFD1
import com.ashampoo.kim.format.tiff.constant.TiffConstants.EXIF_SUB_IFD2
import com.ashampoo.kim.format.tiff.constant.TiffConstants.EXIF_SUB_IFD3
import com.ashampoo.kim.format.tiff.constant.TiffConstants.TIFF_DIRECTORY_TYPE_IFD1
import com.ashampoo.kim.format.tiff.constant.TiffTag
import com.ashampoo.kim.format.tiff.fieldtype.FieldType.Companion.getFieldType
import com.ashampoo.kim.format.tiff.geotiff.GeoTiffDirectory
import com.ashampoo.kim.format.tiff.taginfo.TagInfoLong
import com.ashampoo.kim.format.tiff.taginfo.TagInfoLongs
import com.ashampoo.kim.input.ByteArrayByteReader
Expand Down Expand Up @@ -80,13 +82,15 @@ object TiffReader {
}
)

if (directories.isEmpty())
throw ImageReadException("Image did not contain any directories.")

val makerNoteDirectory =
tryToParseMakerNote(directories, byteReader, tiffHeader.byteOrder)

if (directories.isEmpty())
throw ImageReadException("Image did not contain any directories.")
val geoTiffDirectory = tryToParseGeoTiff(directories)

return TiffContents(tiffHeader, directories, makerNoteDirectory)
return TiffContents(tiffHeader, directories, makerNoteDirectory, geoTiffDirectory)
}

fun readTiffHeader(byteReader: ByteReader): TiffHeader {
Expand Down Expand Up @@ -500,4 +504,38 @@ object TiffReader {
)
}
}

/**
* Inspect if MakerNotes are present and could be added as
* TiffDirectory. This is true for almost all manufacturers.
*/
private fun tryToParseGeoTiff(
directories: MutableList<TiffDirectory>
): GeoTiffDirectory? {

try {

val geoTiffDirectoryField = TiffDirectory.findTiffField(
directories,
GeoTiffTag.EXIF_TAG_GEO_KEY_DIRECTORY_TAG
) ?: return null

val shorts = geoTiffDirectoryField.value as? ShortArray

if (shorts != null)
return GeoTiffDirectory.parseFrom(shorts)

return null

} catch (ignore: Exception) {

ignore.printStackTrace() // FIXME

/*
* Be silent here as GeoTiff interpretation is not essential.
*/

return null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.ashampoo.kim.format.tiff
import com.ashampoo.kim.format.tiff.constant.CanonTag
import com.ashampoo.kim.format.tiff.constant.ExifTag
import com.ashampoo.kim.format.tiff.constant.ExifTag.EXIF_DIRECTORY_UNKNOWN
import com.ashampoo.kim.format.tiff.constant.GeoTiffTag
import com.ashampoo.kim.format.tiff.constant.GpsTag
import com.ashampoo.kim.format.tiff.constant.NikonTag
import com.ashampoo.kim.format.tiff.constant.TiffConstants
Expand All @@ -27,11 +28,11 @@ import com.ashampoo.kim.format.tiff.taginfo.TagInfo

internal object TiffTags {

/* Ordered to give EXIF tag names priority. */
private val TIFF_AND_EXIF_TAGS = ExifTag.ALL_EXIF_TAGS + TiffTag.ALL_TIFF_TAGS
/* Note: Ordered to give EXIF tag names priority. */
private val TIFF_AND_EXIF_TAGS = ExifTag.ALL + TiffTag.ALL + GeoTiffTag.ALL

private val TIFF_AND_EXIF_TAGS_MAP = TIFF_AND_EXIF_TAGS.groupByTo(mutableMapOf()) { it.tag }
private val GPS_TAGS_MAP = GpsTag.ALL_GPS_TAGS.groupByTo(mutableMapOf()) { it.tag }
private val GPS_TAGS_MAP = GpsTag.ALL.groupByTo(mutableMapOf()) { it.tag }
private val CANON_TAGS_MAP = CanonTag.ALL.groupByTo(mutableMapOf()) { it.tag }
private val NIKON_TAGS_MAP = NikonTag.ALL.groupByTo(mutableMapOf()) { it.tag }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ object ExifTag {
TIFF_DIRECTORY_IFD0
)

val ALL_EXIF_TAGS = listOf(
val ALL = listOf(
EXIF_TAG_INTEROPERABILITY_INDEX, EXIF_TAG_INTEROPERABILITY_VERSION,
EXIF_TAG_INTEROPERABILITY_RELATED_IMAGE_WIDTH,
EXIF_TAG_INTEROPERABILITY_RELATED_IMAGE_HEIGHT,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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.constant

import com.ashampoo.kim.format.tiff.constant.ExifTag.EXIF_DIRECTORY_UNKNOWN
import com.ashampoo.kim.format.tiff.taginfo.TagInfoAscii
import com.ashampoo.kim.format.tiff.taginfo.TagInfoDoubles
import com.ashampoo.kim.format.tiff.taginfo.TagInfoShorts

/**
* See https://exiftool.org/TagNames/GeoTiff.html
*/
@Suppress("MagicNumber")
object GeoTiffTag {

val EXIF_TAG_MODEL_PIXEL_SCALE_TAG: TagInfoDoubles = TagInfoDoubles(
0x830e, "ModelPixelScaleTag", 3,
EXIF_DIRECTORY_UNKNOWN
)

val EXIF_TAG_INTERGRAPH_MATRIX_TAG: TagInfoDoubles = TagInfoDoubles(
0x8480, "IntergraphMatrixTag", -1,
EXIF_DIRECTORY_UNKNOWN
)

val EXIF_TAG_MODEL_TIEPOINT_TAG: TagInfoDoubles = TagInfoDoubles(
0x8482, "ModelTiepointTag", -1,
EXIF_DIRECTORY_UNKNOWN
)

val EXIF_TAG_MODEL_TRANSFORMATION_TAG: TagInfoDoubles = TagInfoDoubles(
0x85d8, "ModelTransformationTag", 16,
EXIF_DIRECTORY_UNKNOWN
)

val EXIF_TAG_GEO_KEY_DIRECTORY_TAG: TagInfoShorts = TagInfoShorts(
0x87af, "GeoKeyDirectoryTag", -1,
EXIF_DIRECTORY_UNKNOWN
)

val EXIF_TAG_GEO_DOUBLE_PARAMS_TAG: TagInfoDoubles = TagInfoDoubles(
0x87b0, "GeoDoubleParamsTag", -1,
EXIF_DIRECTORY_UNKNOWN
)

val EXIF_TAG_GEO_ASCII_PARAMS_TAG: TagInfoAscii = TagInfoAscii(
0x87b1, "GeoAsciiParamsTag", -1,
EXIF_DIRECTORY_UNKNOWN
)

val ALL = listOf(
EXIF_TAG_MODEL_PIXEL_SCALE_TAG,
EXIF_TAG_INTERGRAPH_MATRIX_TAG,
EXIF_TAG_MODEL_TIEPOINT_TAG,
EXIF_TAG_MODEL_TRANSFORMATION_TAG,
EXIF_TAG_GEO_KEY_DIRECTORY_TAG,
EXIF_TAG_GEO_DOUBLE_PARAMS_TAG,
EXIF_TAG_GEO_ASCII_PARAMS_TAG
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ object GpsTag {
const val GPS_TAG_GPS_DIFFERENTIAL_VALUE_NO_CORRECTION = 0
const val GPS_TAG_GPS_DIFFERENTIAL_VALUE_DIFFERENTIAL_CORRECTED = 1

val ALL_GPS_TAGS = listOf(
val ALL = listOf(
GPS_TAG_GPS_VERSION_ID, GPS_TAG_GPS_LATITUDE_REF,
GPS_TAG_GPS_LATITUDE, GPS_TAG_GPS_LONGITUDE_REF,
GPS_TAG_GPS_LONGITUDE, GPS_TAG_GPS_ALTITUDE_REF,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ object TiffTag {
0xC612, "DNGVersion", 4, TIFF_DIRECTORY_IFD0
)

val ALL_TIFF_TAGS = listOf(
val ALL = listOf(
TIFF_TAG_NEW_SUBFILE_TYPE, TIFF_TAG_SUBFILE_TYPE,
TIFF_TAG_IMAGE_WIDTH, TIFF_TAG_IMAGE_LENGTH,
TIFF_TAG_BITS_PER_SAMPLE, TIFF_TAG_COMPRESSION,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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.geotiff

import kotlin.jvm.JvmStatic

/**
* See http://geotiff.maptools.org/spec/geotiff6.html
*/
@Suppress("MagicNumber")
enum class GeoKey(
val keyId: Short
) {

/* 6.2.1 GeoTIFF Configuration Keys */
GTModelTypeGeoKey(1024),
GTRasterTypeGeoKey(1025),
GTCitationGeoKey(1026),

/* 6.2.2 Geographic CS Parameter Keys */
GeographicTypeGeoKey(2048),
GeogCitationGeoKey(2049),
GeogGeodeticDatumGeoKey(2050),
GeogPrimeMeridianGeoKey(2051),
GeogLinearUnitsGeoKey(2052),
GeogLinearUnitSizeGeoKey(2053),
GeogAngularUnitsGeoKey(2054),
GeogAngularUnitSizeGeoKey(2055),
GeogEllipsoidGeoKey(2056),
GeogSemiMajorAxisGeoKey(2057),
GeogSemiMinorAxisGeoKey(2058),
GeogInvFlatteningGeoKey(2059),
GeogAzimuthUnitsGeoKey(2060),
GeogPrimeMeridianLongGeoKey(2061),

/* 6.2.3 Projected CS Parameter Keys */
ProjectedCSTypeGeoKey(3072),
PCSCitationGeoKey(3073),
ProjectionGeoKey(3074),
ProjCoordTransGeoKey(3075),
ProjLinearUnitsGeoKey(3076),
ProjLinearUnitSizeGeoKey(3077),
ProjStdParallel1GeoKey(3078),
ProjStdParallel2GeoKey(3079),
ProjNatOriginLongGeoKey(3080),
ProjNatOriginLatGeoKey(3081),
ProjFalseEastingGeoKey(3082),
ProjFalseNorthingGeoKey(3083),
ProjFalseOriginLongGeoKey(3084),
ProjFalseOriginLatGeoKey(3085),
ProjFalseOriginEastingGeoKey(3086),
ProjFalseOriginNorthingGeoKey(3087),
ProjCenterLongGeoKey(3088),
ProjCenterLatGeoKey(3089),
ProjCenterEastingGeoKey(3090),
ProjCenterNorthingGeoKey(3091),
ProjScaleAtNatOriginGeoKey(3092),
ProjScaleAtCenterGeoKey(3093),
ProjAzimuthAngleGeoKey(3094),
ProjStraightVertPoleLongGeoKey(3095),

/* 6.2.4 Vertical CS Keys */
VerticalCSTypeGeoKey(4096),
VerticalCitationGeoKey(4097),
VerticalDatumGeoKey(4098),
VerticalUnitsGeoKey(4099);

companion object {

@JvmStatic
fun of(keyId: Short): GeoKey? =
GeoKey.entries.firstOrNull { it.keyId == keyId }
}
}
Loading

0 comments on commit 24c5cc7

Please sign in to comment.