Skip to content

Commit 42a343f

Browse files
Read & write location shown, title & description (#110)
1 parent c9965bf commit 42a343f

File tree

205 files changed

+4880
-1755
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

205 files changed

+4880
-1755
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
6363
run: |
6464
chmod +x ./gradlew
65-
./gradlew build test koverXmlReport detekt sonar assembleXCFramework -x jsBrowserTest -x jsNodeTest -x wasmJsBrowserTest -x wasmJsNodeTest --parallel
65+
./gradlew build test koverXmlReport detekt sonar assembleXCFramework --parallel
6666
- name: Set RELEASE_VERSION variable
6767
run: |
6868
echo "RELEASE_VERSION=$(cat build/version.txt)" >> $GITHUB_ENV

.idea/runConfigurations/Build___Test.xml

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Kim - Kotlin Image Metadata
22

3-
[![Kotlin](https://img.shields.io/badge/kotlin-2.0.21-blue.svg?logo=kotlin)](httpw://kotlinlang.org)
3+
[![Kotlin](https://img.shields.io/badge/kotlin-2.1.0-blue.svg?logo=kotlin)](httpw://kotlinlang.org)
44
![JVM](https://img.shields.io/badge/-JVM-gray.svg?style=flat)
55
![Android](https://img.shields.io/badge/-Android-gray.svg?style=flat)
66
![iOS](https://img.shields.io/badge/-iOS-gray.svg?style=flat)
@@ -39,7 +39,7 @@ of Ashampoo Photo Organizer, which, in turn, is driven by user community feedbac
3939
## Installation
4040

4141
```
42-
implementation("com.ashampoo:kim:0.20.2")
42+
implementation("com.ashampoo:kim:0.21.0")
4343
```
4444

4545
For the targets `wasmJs` & `js` you also need to specify this:
@@ -80,12 +80,14 @@ It contains the following:
8080

8181
- Image size
8282
- Orientation
83-
- Taken date
83+
- Date taken
8484
- GPS coordinates
8585
- Camera make & model
8686
- Lens make & model
8787
- ISO, Exposure time, F-Number, Focal length
88+
- Image title & description
8889
- Rating
90+
- `XMP:pick` flag
8991
- Keywords
9092
- Faces (XMP-mwg-rs regions, used by Picasa and others)
9193
- Persons in image

build.gradle.kts

+8-27
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
55
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
66

77
plugins {
8-
kotlin("multiplatform") version "2.0.21" // Kotlin 2.1.0 results in compile errors!
8+
kotlin("multiplatform") version "2.1.0"
99
id("com.android.library") version "8.5.0"
1010
id("maven-publish")
1111
id("signing")
@@ -14,7 +14,7 @@ plugins {
1414
id("org.jetbrains.kotlinx.kover") version "0.6.1"
1515
id("com.asarkar.gradle.build-time-tracker") version "4.3.0"
1616
id("me.qoomon.git-versioning") version "6.4.4"
17-
id("com.goncalossilva.resources") version "0.9.0" // 0.10.0 requires Kotlin 2.1.0
17+
id("com.goncalossilva.resources") version "0.10.0"
1818
id("com.github.ben-manes.versions") version "0.51.0"
1919
id("org.jetbrains.dokka") version "1.9.20"
2020
}
@@ -27,7 +27,7 @@ repositories {
2727
val productName: String = "Ashampoo Kim"
2828

2929
val ktorVersion: String = "3.0.3"
30-
val xmpCoreVersion: String = "1.4.2"
30+
val xmpCoreVersion: String = "1.5.0"
3131
val dateTimeVersion: String = "0.6.1"
3232
val kotlinxIoVersion: String = "0.6.0"
3333

@@ -170,32 +170,10 @@ kotlin {
170170
}
171171
}
172172

173-
js(IR) {
174-
175-
moduleName = "kim"
176-
177-
browser {
178-
webpackTask {
179-
mainOutputFileName = "kim.js"
180-
output.library = "kimLib"
181-
}
182-
}
183-
184-
nodejs()
185-
186-
binaries.executable()
187-
}
173+
js()
188174

189175
@OptIn(ExperimentalWasmDsl::class)
190-
wasmJs {
191-
192-
moduleName = "kim-wasm"
193-
194-
browser()
195-
nodejs()
196-
197-
binaries.executable()
198-
}
176+
wasmJs()
199177

200178
// WASI support is planned for kotlinx-datetime v0.7
201179
// @OptIn(ExperimentalWasmDsl::class)
@@ -373,6 +351,9 @@ kotlin {
373351
// wasmWasiMain.dependsOn(this)
374352

375353
dependencies {
354+
355+
implementation("org.jetbrains.kotlinx:kotlinx-browser:0.3")
356+
376357
implementation(npm("pako", "2.1.0"))
377358
}
378359
}

src/commonMain/kotlin/com/ashampoo/kim/common/PhotoMetadataConverter.kt

+95-30
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.ashampoo.kim.format.tiff.constant.TiffTag
2626
import com.ashampoo.kim.format.xmp.XmpReader
2727
import com.ashampoo.kim.input.ByteArrayByteReader
2828
import com.ashampoo.kim.model.GpsCoordinates
29+
import com.ashampoo.kim.model.LocationShown
2930
import com.ashampoo.kim.model.PhotoMetadata
3031
import com.ashampoo.kim.model.TiffOrientation
3132
import kotlinx.datetime.LocalDateTime
@@ -48,19 +49,20 @@ public object PhotoMetadataConverter {
4849
ignoreOrientation: Boolean = false
4950
): PhotoMetadata {
5051

52+
val xmpMetadata: PhotoMetadata? = imageMetadata.xmp?.let {
53+
XmpReader.readMetadata(it)
54+
}
55+
5156
val orientation = if (ignoreOrientation)
5257
TiffOrientation.STANDARD
5358
else
5459
TiffOrientation.of(imageMetadata.findShortValue(TiffTag.TIFF_TAG_ORIENTATION)?.toInt())
5560

56-
val takenDateMillis = extractTakenDateMillis(imageMetadata)
57-
58-
val gpsDirectory = imageMetadata.findTiffDirectory(TiffConstants.TIFF_DIRECTORY_GPS)
61+
val takenDateMillis: Long? = xmpMetadata?.takenDate
62+
?: extractTakenDateMillisFromExif(imageMetadata)
5963

60-
val gps = gpsDirectory?.let(GPSInfo::createFrom)
61-
62-
val latitude = gps?.getLatitudeAsDegreesNorth()
63-
val longitude = gps?.getLongitudeAsDegreesEast()
64+
val gpsCoordinates: GpsCoordinates? = xmpMetadata?.gpsCoordinates
65+
?: extractGpsCoordinatesFromExif(imageMetadata)
6466

6567
val cameraMake = imageMetadata.findStringValue(TiffTag.TIFF_TAG_MAKE)
6668
val cameraModel = imageMetadata.findStringValue(TiffTag.TIFF_TAG_MODEL)
@@ -76,28 +78,22 @@ public object PhotoMetadataConverter {
7678
val fNumber = imageMetadata.findDoubleValue(ExifTag.EXIF_TAG_FNUMBER)
7779
val focalLength = imageMetadata.findDoubleValue(ExifTag.EXIF_TAG_FOCAL_LENGTH)
7880

79-
val keywords = mutableSetOf<String>()
81+
val keywords = xmpMetadata?.keywords?.ifEmpty {
82+
extractKeywordsFromIptc(imageMetadata)
83+
} ?: extractKeywordsFromIptc(imageMetadata)
8084

8185
val iptcRecords = imageMetadata.iptc?.records
8286

83-
iptcRecords?.forEach {
87+
val title = xmpMetadata?.title ?: iptcRecords
88+
?.find { it.iptcType == IptcTypes.OBJECT_NAME }
89+
?.value
8490

85-
if (it.iptcType == IptcTypes.KEYWORDS)
86-
keywords.add(it.value)
87-
}
91+
val description = xmpMetadata?.description ?: iptcRecords
92+
?.find { it.iptcType == IptcTypes.CAPTION_ABSTRACT }
93+
?.value
8894

89-
val gpsCoordinates =
90-
if (latitude != null && longitude != null)
91-
GpsCoordinates(
92-
latitude = latitude,
93-
longitude = longitude
94-
)
95-
else
96-
null
97-
98-
val xmpMetadata: PhotoMetadata? = imageMetadata.xmp?.let {
99-
XmpReader.readMetadata(it)
100-
}
95+
val location = xmpMetadata?.locationShown
96+
?: extractLocationFromIptc(imageMetadata)
10197

10298
val thumbnailBytes = imageMetadata.getExifThumbnailBytes()
10399

@@ -120,9 +116,9 @@ public object PhotoMetadataConverter {
120116
widthPx = imageMetadata.imageSize?.width,
121117
heightPx = imageMetadata.imageSize?.height,
122118
orientation = orientation,
123-
takenDate = xmpMetadata?.takenDate ?: takenDateMillis,
124-
gpsCoordinates = xmpMetadata?.gpsCoordinates ?: gpsCoordinates,
125-
location = xmpMetadata?.location,
119+
takenDate = takenDateMillis,
120+
gpsCoordinates = gpsCoordinates,
121+
locationShown = location,
126122
cameraMake = cameraMake,
127123
cameraModel = cameraModel,
128124
lensMake = lensMake,
@@ -131,9 +127,11 @@ public object PhotoMetadataConverter {
131127
exposureTime = exposureTime,
132128
fNumber = fNumber,
133129
focalLength = focalLength,
130+
title = title,
131+
description = description,
134132
flagged = xmpMetadata?.flagged ?: false,
135133
rating = xmpMetadata?.rating,
136-
keywords = keywords.ifEmpty { xmpMetadata?.keywords ?: emptySet() },
134+
keywords = keywords,
137135
faces = xmpMetadata?.faces ?: emptyMap(),
138136
personsInImage = xmpMetadata?.personsInImage ?: emptySet(),
139137
albums = xmpMetadata?.albums ?: emptySet(),
@@ -143,7 +141,7 @@ public object PhotoMetadataConverter {
143141
}
144142

145143
@JvmStatic
146-
public fun extractTakenDateAsIsoString(metadata: ImageMetadata): String? {
144+
private fun extractTakenDateAsIsoString(metadata: ImageMetadata): String? {
147145

148146
val takenDateField = metadata.findTiffField(ExifTag.EXIF_TAG_DATE_TIME_ORIGINAL)
149147
?: return null
@@ -163,7 +161,9 @@ public object PhotoMetadataConverter {
163161
}
164162

165163
@JvmStatic
166-
public fun extractTakenDateMillis(metadata: ImageMetadata): Long? {
164+
private fun extractTakenDateMillisFromExif(
165+
metadata: ImageMetadata
166+
): Long? {
167167

168168
try {
169169

@@ -203,6 +203,71 @@ public object PhotoMetadataConverter {
203203
}
204204
}
205205

206+
@JvmStatic
207+
private fun extractGpsCoordinatesFromExif(
208+
metadata: ImageMetadata
209+
): GpsCoordinates? {
210+
211+
val gpsDirectory = metadata.findTiffDirectory(TiffConstants.TIFF_DIRECTORY_GPS)
212+
213+
val gps = gpsDirectory?.let(GPSInfo::createFrom)
214+
215+
val latitude = gps?.getLatitudeAsDegreesNorth()
216+
val longitude = gps?.getLongitudeAsDegreesEast()
217+
218+
if (latitude == null || longitude == null)
219+
return null
220+
221+
return GpsCoordinates(
222+
latitude = latitude,
223+
longitude = longitude
224+
)
225+
}
226+
227+
@JvmStatic
228+
private fun extractKeywordsFromIptc(
229+
metadata: ImageMetadata
230+
): Set<String> {
231+
232+
return metadata.iptc?.records
233+
?.filter { it.iptcType == IptcTypes.KEYWORDS }
234+
?.map { it.value }
235+
?.toSet()
236+
?: emptySet()
237+
}
238+
239+
@JvmStatic
240+
private fun extractLocationFromIptc(
241+
metadata: ImageMetadata
242+
): LocationShown? {
243+
244+
val iptcRecords = metadata.iptc?.records
245+
?: return null
246+
247+
val iptcCity = iptcRecords
248+
.find { it.iptcType == IptcTypes.CITY }
249+
?.value
250+
251+
val iptcState = iptcRecords
252+
.find { it.iptcType == IptcTypes.PROVINCE_STATE }
253+
?.value
254+
255+
val iptcCountry = iptcRecords
256+
.find { it.iptcType == IptcTypes.COUNTRY_PRIMARY_LOCATION_NAME }
257+
?.value
258+
259+
/* Don't create an object if everything is NULL */
260+
if (iptcCity.isNullOrBlank() && iptcState.isNullOrBlank() && iptcCountry.isNullOrBlank())
261+
return null
262+
263+
return LocationShown(
264+
name = null,
265+
location = null,
266+
city = iptcCity,
267+
state = iptcState,
268+
country = iptcCountry
269+
)
270+
}
206271
}
207272

208273
public fun ImageMetadata.convertToPhotoMetadata(

0 commit comments

Comments
 (0)