diff --git a/README.md b/README.md
index c7f91b0..d09a572 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,15 @@
# XMP Core for Kotlin Multiplatform
-[![Kotlin](https://img.shields.io/badge/kotlin-1.9.22-blue.svg?logo=kotlin)](httpw://kotlinlang.org)
+[![Kotlin](https://img.shields.io/badge/kotlin-1.9.23-blue.svg?logo=kotlin)](httpw://kotlinlang.org)
![JVM](https://img.shields.io/badge/-JVM-gray.svg?style=flat)
![Android](https://img.shields.io/badge/-Android-gray.svg?style=flat)
-![macOS](https://img.shields.io/badge/-macOS-gray.svg?style=flat)
![iOS](https://img.shields.io/badge/-iOS-gray.svg?style=flat)
![Windows](https://img.shields.io/badge/-Windows-gray.svg?style=flat)
![Linux](https://img.shields.io/badge/-Linux-gray.svg?style=flat)
+![macOS](https://img.shields.io/badge/-macOS-gray.svg?style=flat)
+![JS](https://img.shields.io/badge/-JS-gray.svg?style=flat)
![WASM](https://img.shields.io/badge/-WASM-gray.svg?style=flat)
+![WASI](https://img.shields.io/badge/-WASI-gray.svg?style=flat)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=xmpcore&metric=coverage)](https://sonarcloud.io/summary/new_code?id=xmpcore)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.ashampoo/xmpcore/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.ashampoo/xmpcore)
@@ -18,7 +20,7 @@ It's part of [Ashampoo Photos](https://ashampoo.com/photos).
## Installation
```
-implementation("com.ashampoo:xmpcore:1.1.0")
+implementation("com.ashampoo:xmpcore:1.2.0")
```
## How to use
diff --git a/build.gradle.kts b/build.gradle.kts
index d3baefa..5e6556f 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
plugins {
- kotlin("multiplatform") version "1.9.22"
+ kotlin("multiplatform") version "1.9.23"
id("com.android.library") version "8.2.2"
id("maven-publish")
id("signing")
@@ -13,8 +13,8 @@ plugins {
id("com.asarkar.gradle.build-time-tracker") version "4.3.0"
id("me.qoomon.git-versioning") version "6.4.3"
id("com.goncalossilva.resources") version "0.4.0"
- id("com.github.ben-manes.versions") version "0.50.0"
- id("org.jetbrains.dokka") version "1.9.10"
+ id("com.github.ben-manes.versions") version "0.51.0"
+ id("org.jetbrains.dokka") version "1.9.20"
}
repositories {
@@ -25,6 +25,7 @@ repositories {
val productName = "Ashampoo XMP Core"
val xmlUtilVersion: String = "0.86.3"
+val kotlinIoVersion: String = "0.3.1"
description = productName
group = "com.ashampoo"
@@ -103,7 +104,7 @@ koverMerged {
}
dependencies {
- detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.4")
+ detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.5")
}
kotlin {
@@ -151,6 +152,8 @@ kotlin {
}
}
+ js()
+
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
// All tests reading from files fail, because kotlinx-io
@@ -180,7 +183,7 @@ kotlin {
implementation(kotlin("test"))
/* Multiplatform file access */
- implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.3.0")
+ implementation("org.jetbrains.kotlinx:kotlinx-io-core:$kotlinIoVersion")
}
}
@@ -351,6 +354,7 @@ afterEvaluate {
val signWinPublication by tasks.getting
val signLinuxX64Publication by tasks.getting
val signLinuxArm64Publication by tasks.getting
+ val signJsPublication by tasks.getting
val signWasmJsPublication by tasks.getting
val signWasmWasiPublication by tasks.getting
val signKotlinMultiplatformPublication by tasks.getting
@@ -365,6 +369,7 @@ afterEvaluate {
val publishWinPublicationToSonatypeRepository by tasks.getting
val publishLinuxX64PublicationToSonatypeRepository by tasks.getting
val publishLinuxArm64PublicationToSonatypeRepository by tasks.getting
+ val publishJsPublicationToSonatypeRepository by tasks.getting
val publishWasmJsPublicationToSonatypeRepository by tasks.getting
val publishWasmWasiPublicationToSonatypeRepository by tasks.getting
val publishKotlinMultiplatformPublicationToSonatypeRepository by tasks.getting
@@ -375,7 +380,8 @@ afterEvaluate {
signIosArm64Publication, signIosX64Publication, signIosSimulatorArm64Publication,
signMacosArm64Publication, signMacosX64Publication, signWinPublication,
signLinuxX64Publication, signLinuxArm64Publication,
- signWasmJsPublication, signWasmWasiPublication, signKotlinMultiplatformPublication
+ signJsPublication, signWasmJsPublication, signWasmWasiPublication,
+ signKotlinMultiplatformPublication
)
val publishTasks = listOf(
@@ -389,6 +395,7 @@ afterEvaluate {
publishWinPublicationToSonatypeRepository,
publishLinuxX64PublicationToSonatypeRepository,
publishLinuxArm64PublicationToSonatypeRepository,
+ publishJsPublicationToSonatypeRepository,
publishWasmJsPublicationToSonatypeRepository,
publishWasmWasiPublicationToSonatypeRepository,
publishKotlinMultiplatformPublicationToSonatypeRepository,
diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPConst.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPConst.kt
index 769bae2..e647de0 100644
--- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPConst.kt
+++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPConst.kt
@@ -50,10 +50,6 @@ object XMPConst {
const val NS_MWG_RS: String = "http://www.metadataworkinggroup.com/schemas/regions/"
- const val NS_ACDSEE: String = "http://ns.acdsee.com/iptc/1.0/"
-
- // Adobe standard namespaces
-
/**
* The XML namespace Adobe XMP Metadata.
*/
@@ -76,24 +72,12 @@ object XMPConst {
*/
const val NS_XMP_MM: String = "http://ns.adobe.com/xap/1.0/mm/"
- /**
- * The XML namespace for the job management schema.
- */
const val NS_XMP_BJ: String = "http://ns.adobe.com/xap/1.0/bj/"
- /**
- * The XML namespace for the job management schema.
- */
const val NS_XMP_NOTE: String = "http://ns.adobe.com/xmp/note/"
- /**
- * The XML namespace for the PDF schema.
- */
const val NS_PDF: String = "http://ns.adobe.com/pdf/1.3/"
- /**
- * The XML namespace for the PDF schema.
- */
const val NS_PDFX: String = "http://ns.adobe.com/pdfx/1.3/"
const val NS_PDFX_ID: String = "http://www.npes.org/pdfx/ns/id/"
@@ -110,11 +94,6 @@ object XMPConst {
const val NS_PDFA_EXTENSION: String = "http://www.aiim.org/pdfa/ns/extension/"
- /**
- * The XML namespace for the Photoshop custom schema.
- */
- const val NS_PHOTOSHOP: String = "http://ns.adobe.com/photoshop/1.0/"
-
/**
* The XML namespace for the Photoshop Album schema.
*/
@@ -183,6 +162,16 @@ object XMPConst {
*/
const val NS_DC_DEPRECATED: String = "http://purl.org/dc/1.1/"
+ // Namespaces of products
+ const val NS_ASHAMPOO: String = "http://ns.ashampoo.com/xmp/1.0/"
+ const val NS_ACDSEE: String = "http://ns.acdsee.com/iptc/1.0/"
+ const val NS_DIGIKAM: String = "http://www.digikam.org/ns/1.0/"
+ const val NS_MYLIO: String = "http://ns.mylollc.com/MyloEdit/"
+ const val NS_NARRATIVE: String = "http://ns.narrative.so/narrative_select/1.0/"
+ const val NS_MICROSOFT_PHOTO: String = "http://ns.microsoft.com/photo/1.0/"
+ const val NS_LIGHTROOM: String = "http://ns.adobe.com/lightroom/1.0/"
+ const val NS_PHOTOSHOP: String = "http://ns.adobe.com/photoshop/1.0/"
+
// XML namespace constants for qualifiers and structured property fields.
/**
@@ -297,5 +286,28 @@ object XMPConst {
const val XMP_IPTC_EXT_PERSON_IN_IMAGE: String = "PersonInImage"
const val XMP_MWG_RS_TYPE_FACE: String = "Face"
-
+ const val XMP_MWG_RS_REGION_LIST: String = "Regions/mwg-rs:RegionList"
+ const val XMP_MWG_RS_APPLIED_TO_DIMENSIONS: String = "Regions/mwg-rs:AppliedToDimensions"
+
+ /* xmpDM:pick="1" or xmpDM:pick="0" */
+ const val FLAGGED_TAG_ADOBE_NAME: String = "pick"
+ const val FLAGGED_TAG_ADOBE_TRUE: String = "1"
+ const val FLAGGED_TAG_ADOBE_FALSE: String = "0"
+
+ /* True or False */
+ const val FLAGGED_TAG_ACDSEE_NAME: String = "tagged"
+ const val FLAGGED_TAG_ACDSEE_TRUE: String = TRUE_STRING
+ const val FLAGGED_TAG_ACDSEE_FALSE: String = FALSE_STRING
+
+ /* MY:flag="true" or MY:flag="false" */
+ const val FLAGGED_TAG_MYLIO_NAME: String = "flag"
+ const val FLAGGED_TAG_MYLIO_TRUE: String = "true"
+ const val FLAGGED_TAG_MYLIO_FALSE: String = "false"
+
+ /* narrative:Tagged="True" or narrative:Tagged="False" */
+ const val FLAGGED_TAG_NARRATIVE_NAME: String = "Tagged"
+ const val FLAGGED_TAG_NARRATIVE_TRUE: String = TRUE_STRING
+ const val FLAGGED_TAG_NARRATIVE_FALSE: String = FALSE_STRING
+
+ const val XMP_ASHAMPOO_ALBUMS: String = "albums"
}
diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt
index bd4f535..5fb84a2 100644
--- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt
+++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMeta.kt
@@ -10,6 +10,8 @@ package com.ashampoo.xmp
import com.ashampoo.xmp.Utils.normalizeLangValue
import com.ashampoo.xmp.XMPConst.NS_MWG_RS
+import com.ashampoo.xmp.XMPConst.XMP_MWG_RS_APPLIED_TO_DIMENSIONS
+import com.ashampoo.xmp.XMPConst.XMP_MWG_RS_REGION_LIST
import com.ashampoo.xmp.XMPNodeUtils.appendLangItem
import com.ashampoo.xmp.XMPNodeUtils.chooseLocalizedText
import com.ashampoo.xmp.XMPNodeUtils.deleteNode
@@ -96,16 +98,9 @@ class XMPMeta {
* if the property does not exist.
*/
fun getProperty(schemaNS: String, propName: String): XMPProperty? =
- getProperty(schemaNS, propName, VALUE_STRING)
+ getProperty(schemaNS, propName, XMPValueType.STRING)
- /**
- * Returns a property, but the result value can be requested. It can be one
- * of [XMPMeta.VALUE_STRING], [XMPMeta.VALUE_BOOLEAN],
- * [XMPMeta.VALUE_INTEGER], [XMPMeta.VALUE_LONG],
- * [XMPMeta.VALUE_DOUBLE], [XMPMeta.VALUE_DATE],
- * [XMPMeta.VALUE_TIME_IN_MILLIS], [XMPMeta.VALUE_BASE64].
- */
- private fun getProperty(schemaNS: String, propName: String, valueType: Int): XMPProperty? {
+ private fun getProperty(schemaNS: String, propName: String, valueType: XMPValueType): XMPProperty? {
if (schemaNS.isEmpty())
throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM)
@@ -120,7 +115,7 @@ class XMPMeta {
leafOptions = null
) ?: return null
- if (valueType != VALUE_STRING && propNode.options.isCompositeProperty())
+ if (valueType != XMPValueType.STRING && propNode.options.isCompositeProperty())
throw XMPException("Property must be simple when a value type is requested", XMPError.BADXPATH)
val value = evaluateNodeValue(valueType, propNode)
@@ -145,36 +140,31 @@ class XMPMeta {
* Evaluates a raw node value to the given value type, apply special
* conversions for defined types in XMP.
*/
- private fun evaluateNodeValue(valueType: Int, propNode: XMPNode): Any? {
-
- val value: Any?
- val rawValue = propNode.value
-
- value = when (valueType) {
-
- VALUE_BOOLEAN -> convertToBoolean(rawValue)
+ private fun evaluateNodeValue(valueType: XMPValueType, propNode: XMPNode): Any? =
+ when (valueType) {
- VALUE_INTEGER -> convertToInteger(rawValue)
+ XMPValueType.BOOLEAN -> convertToBoolean(propNode.value)
- VALUE_LONG -> convertToLong(rawValue)
+ XMPValueType.INTEGER -> convertToInteger(propNode.value)
- VALUE_DOUBLE -> convertToDouble(rawValue)
+ XMPValueType.LONG -> convertToLong(propNode.value)
- VALUE_BASE64 -> decodeBase64(rawValue!!)
+ XMPValueType.DOUBLE -> convertToDouble(propNode.value)
- // leaf values return empty string instead of null
- // for the other cases the converter methods provides a "null" value.
- // a default value can only occur if this method is made public.
- VALUE_STRING ->
- if (rawValue != null || propNode.options.isCompositeProperty()) rawValue else ""
+ XMPValueType.BASE64 -> decodeBase64(propNode.value!!)
- else ->
- if (rawValue != null || propNode.options.isCompositeProperty()) rawValue else ""
+ /*
+ * Leaf values return empty string instead of null
+ * for the other cases the converter methods provides a "null" value.
+ * a default value can only occur if this method is made public.
+ */
+ XMPValueType.STRING ->
+ if (propNode.value != null || propNode.options.isCompositeProperty())
+ propNode.value
+ else
+ ""
}
- return value
- }
-
/**
* Provides access to items within an array. The index is passed as an integer, you need not
* worry about the path string syntax for array items, convert a loop index to a string, etc.
@@ -319,8 +309,8 @@ class XMPMeta {
return getProperty(schemaNS, qualPath)
}
- // ---------------------------------------------------------------------------------------------
- // Functions for setting property values
+// ---------------------------------------------------------------------------------------------
+// Functions for setting property values
/**
* The property value `setters` all take a property specification, their
@@ -698,9 +688,9 @@ class XMPMeta {
setProperty(schemaNS, qualPath, qualValue, options)
}
- // ---------------------------------------------------------------------------------------------
- // Functions for deleting and detecting properties.
- // These should be obvious from the descriptions of the getters and setters.
+// ---------------------------------------------------------------------------------------------
+// Functions for deleting and detecting properties.
+// These should be obvious from the descriptions of the getters and setters.
/**
* Deletes the given XMP subtree rooted at the given property.
@@ -930,8 +920,8 @@ class XMPMeta {
return doesPropertyExist(schemaNS, propName + path)
}
- // ---------------------------------------------------------------------------------------------
- // Specialized Get and Set functions
+// ---------------------------------------------------------------------------------------------
+// Specialized Get and Set functions
/**
* These functions provide convenient support for localized text properties, including a number
@@ -1222,8 +1212,8 @@ class XMPMeta {
appendLangItem(arrayNode, XMPConst.X_DEFAULT, itemValue)
}
- // ---------------------------------------------------------------------------------------------
- // Functions accessing properties as binary values.
+// ---------------------------------------------------------------------------------------------
+// Functions accessing properties as binary values.
/**
* These are very similar to `getProperty()` and `SetProperty()` above,
@@ -1236,7 +1226,7 @@ class XMPMeta {
* @return Returns a `Boolean` value or `null` if the property does not exist.
*/
fun getPropertyBoolean(schemaNS: String, propName: String): Boolean? =
- getPropertyObject(schemaNS, propName, VALUE_BOOLEAN) as? Boolean
+ getPropertyObject(schemaNS, propName, XMPValueType.BOOLEAN) as? Boolean
/**
* Convenience method to retrieve the literal value of a property.
@@ -1246,7 +1236,7 @@ class XMPMeta {
* @return Returns an `Integer` value or `null` if the property does not exist.
*/
fun getPropertyInteger(schemaNS: String, propName: String): Int? =
- getPropertyObject(schemaNS, propName, VALUE_INTEGER) as? Int
+ getPropertyObject(schemaNS, propName, XMPValueType.INTEGER) as? Int
/**
* Convenience method to retrieve the literal value of a property.
@@ -1256,7 +1246,7 @@ class XMPMeta {
* @return Returns a `Long` value or `null` if the property does not exist.
*/
fun getPropertyLong(schemaNS: String, propName: String): Long? =
- getPropertyObject(schemaNS, propName, VALUE_LONG) as? Long
+ getPropertyObject(schemaNS, propName, XMPValueType.LONG) as? Long
/**
* Convenience method to retrieve the literal value of a property.
@@ -1266,7 +1256,7 @@ class XMPMeta {
* @return Returns a `Double` value or `null` if the property does not exist.
*/
fun getPropertyDouble(schemaNS: String, propName: String): Double? =
- getPropertyObject(schemaNS, propName, VALUE_DOUBLE) as? Double
+ getPropertyObject(schemaNS, propName, XMPValueType.DOUBLE) as? Double
/**
* Convenience method to retrieve the literal value of a property.
@@ -1277,7 +1267,7 @@ class XMPMeta {
* not exist.
*/
fun getPropertyBase64(schemaNS: String, propName: String): ByteArray? =
- getPropertyObject(schemaNS, propName, VALUE_BASE64) as? ByteArray
+ getPropertyObject(schemaNS, propName, XMPValueType.BASE64) as? ByteArray
/**
* Convenience method to retrieve the literal value of a property.
@@ -1290,12 +1280,12 @@ class XMPMeta {
* @return Returns a `String` value or `null` if the property does not exist.
*/
fun getPropertyString(schemaNS: String, propName: String): String? =
- getPropertyObject(schemaNS, propName, VALUE_STRING) as? String
+ getPropertyObject(schemaNS, propName, XMPValueType.STRING) as? String
/**
* Returns a property, but the result value can be requested.
*/
- private fun getPropertyObject(schemaNS: String, propName: String, valueType: Int): Any? {
+ private fun getPropertyObject(schemaNS: String, propName: String, valueType: XMPValueType): Any? {
if (schemaNS.isEmpty())
throw XMPException(XMPError.EMPTY_SCHEMA_TEXT, XMPError.BADPARAM)
@@ -1310,7 +1300,7 @@ class XMPMeta {
leafOptions = null
) ?: return null
- if (valueType != VALUE_STRING && propNode.options.isCompositeProperty())
+ if (valueType != XMPValueType.STRING && propNode.options.isCompositeProperty())
throw XMPException("Property must be simple when a value type is requested", XMPError.BADXPATH)
return evaluateNodeValue(valueType, propNode)
@@ -1554,6 +1544,58 @@ class XMPMeta {
deleteProperty(XMPConst.NS_EXIF, "GPSLongitude")
}
+ /**
+ * Check for common used fields if they have a positive flagged value.
+ */
+ fun isFlagged(): Boolean =
+ getPropertyBoolean(XMPConst.NS_DM, XMPConst.FLAGGED_TAG_ADOBE_NAME) == true ||
+ getPropertyBoolean(XMPConst.NS_ACDSEE, XMPConst.FLAGGED_TAG_ACDSEE_NAME) == true ||
+ getPropertyBoolean(XMPConst.NS_MYLIO, XMPConst.FLAGGED_TAG_MYLIO_NAME) == true ||
+ getPropertyBoolean(XMPConst.NS_NARRATIVE, XMPConst.FLAGGED_TAG_NARRATIVE_NAME) == true
+
+ /**
+ * Sets flagged/tagged/picked marker for standard schema and other commonly used fields by popular tools.
+ */
+ fun setFlagged(flagged: Boolean) {
+
+ /* Set the standard schema */
+ setProperty(
+ schemaNS = XMPConst.NS_DM,
+ propName = XMPConst.FLAGGED_TAG_ADOBE_NAME,
+ propValue = if (flagged)
+ XMPConst.FLAGGED_TAG_ADOBE_TRUE
+ else
+ XMPConst.FLAGGED_TAG_ADOBE_FALSE
+ )
+
+ setProperty(
+ schemaNS = XMPConst.NS_ACDSEE,
+ propName = XMPConst.FLAGGED_TAG_ACDSEE_NAME,
+ propValue = if (flagged)
+ XMPConst.FLAGGED_TAG_ACDSEE_TRUE
+ else
+ XMPConst.FLAGGED_TAG_ACDSEE_FALSE
+ )
+
+ setProperty(
+ schemaNS = XMPConst.NS_MYLIO,
+ propName = XMPConst.FLAGGED_TAG_MYLIO_NAME,
+ propValue = if (flagged)
+ XMPConst.FLAGGED_TAG_MYLIO_TRUE
+ else
+ XMPConst.FLAGGED_TAG_MYLIO_FALSE
+ )
+
+ setProperty(
+ schemaNS = XMPConst.NS_NARRATIVE,
+ propName = XMPConst.FLAGGED_TAG_NARRATIVE_NAME,
+ propValue = if (flagged)
+ XMPConst.FLAGGED_TAG_NARRATIVE_TRUE
+ else
+ XMPConst.FLAGGED_TAG_NARRATIVE_FALSE
+ )
+ }
+
/**
* Gets the regular keywords specified by XMP standard.
*/
@@ -1637,18 +1679,19 @@ class XMPMeta {
fun getFaces(): Map {
- val regionListExists = doesPropertyExist(XMPConst.NS_MWG_RS, "Regions/mwg-rs:RegionList")
+ val regionListExists = doesPropertyExist(XMPConst.NS_MWG_RS, XMP_MWG_RS_REGION_LIST)
if (!regionListExists)
return emptyMap()
- val regionCount = countArrayItems(XMPConst.NS_MWG_RS, "Regions/mwg-rs:RegionList")
+ val regionCount = countArrayItems(XMPConst.NS_MWG_RS, XMP_MWG_RS_REGION_LIST)
if (regionCount == 0)
return emptyMap()
val faces = mutableMapOf()
+ @Suppress("LoopWithTooManyJumpStatements")
for (index in 1..regionCount) {
val prefix = "Regions/mwg-rs:RegionList[$index]/mwg-rs"
@@ -1688,19 +1731,19 @@ class XMPMeta {
if (faces.isNotEmpty()) {
setStructField(
- NS_MWG_RS, "Regions/mwg-rs:AppliedToDimensions",
+ NS_MWG_RS, XMP_MWG_RS_APPLIED_TO_DIMENSIONS,
XMPConst.TYPE_DIMENSIONS, "w",
widthPx.toString()
)
setStructField(
- NS_MWG_RS, "Regions/mwg-rs:AppliedToDimensions",
+ NS_MWG_RS, XMP_MWG_RS_APPLIED_TO_DIMENSIONS,
XMPConst.TYPE_DIMENSIONS, "h",
heightPx.toString()
)
setStructField(
- NS_MWG_RS, "Regions/mwg-rs:AppliedToDimensions",
+ NS_MWG_RS, XMP_MWG_RS_APPLIED_TO_DIMENSIONS,
XMPConst.TYPE_DIMENSIONS, "unit", "pixel"
)
@@ -1840,17 +1883,63 @@ class XMPMeta {
)
}
- companion object {
+ /**
+ * Get album names
+ */
+ fun getAlbums(): Set {
+
+ val subjectCount = countArrayItems(XMPConst.NS_ASHAMPOO, XMPConst.XMP_ASHAMPOO_ALBUMS)
+
+ if (subjectCount == 0)
+ return emptySet()
+
+ val keywords = mutableSetOf()
+
+ for (index in 1..subjectCount) {
+
+ val keyword = getPropertyString(
+ XMPConst.NS_ASHAMPOO,
+ "${XMPConst.XMP_ASHAMPOO_ALBUMS}[$index]"
+ ) ?: continue
+
+ keywords.add(keyword)
+ }
+
+ return keywords
+ }
+
+ fun setAlbums(albums: Set) {
+
+ /* Delete existing entries, if any */
+ deleteProperty(XMPConst.NS_ASHAMPOO, XMPConst.XMP_ASHAMPOO_ALBUMS)
+
+ if (albums.isEmpty())
+ return
+
+ /* Create a new array property. */
+ setProperty(
+ XMPConst.NS_ASHAMPOO,
+ XMPConst.XMP_ASHAMPOO_ALBUMS,
+ null,
+ arrayOptions
+ )
+
+ /* Fill the new array with album names. */
+ for (albumName in albums.sorted())
+ appendArrayItem(
+ schemaNS = XMPConst.NS_ASHAMPOO,
+ arrayName = XMPConst.XMP_ASHAMPOO_ALBUMS,
+ itemValue = albumName
+ )
+ }
- /**
- * Property values are Strings by default
- */
+ internal enum class XMPValueType {
- private const val VALUE_STRING = 0
- private const val VALUE_BOOLEAN = 1
- private const val VALUE_INTEGER = 2
- private const val VALUE_LONG = 3
- private const val VALUE_DOUBLE = 4
- private const val VALUE_BASE64 = 7
+ STRING,
+ BOOLEAN,
+ INTEGER,
+ LONG,
+ DOUBLE,
+ BASE64
}
}
diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaParser.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaParser.kt
index 2ba7e6e..eafef81 100644
--- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaParser.kt
+++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPMetaParser.kt
@@ -10,12 +10,12 @@ package com.ashampoo.xmp
import com.ashampoo.xmp.XMPNormalizer.normalize
import com.ashampoo.xmp.options.ParseOptions
-import nl.adaptivity.xmlutil.dom.Comment
import nl.adaptivity.xmlutil.dom.Element
import nl.adaptivity.xmlutil.dom.Node
+import nl.adaptivity.xmlutil.dom.NodeConsts
import nl.adaptivity.xmlutil.dom.ProcessingInstruction
-import nl.adaptivity.xmlutil.dom.Text
import nl.adaptivity.xmlutil.dom.childNodes
+import nl.adaptivity.xmlutil.dom.nodeType
import nl.adaptivity.xmlutil.dom.getData
import nl.adaptivity.xmlutil.dom.getTarget
import nl.adaptivity.xmlutil.dom.length
@@ -53,6 +53,7 @@ internal object XMPMetaParser {
return if (result != null && result[1] === XMP_RDF) {
+ @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
val xmp = XMPRDFParser.parse(result[0] as Node, actualOptions)
xmp.setPacketHeader(result[2] as? String)
@@ -97,6 +98,7 @@ internal object XMPMetaParser {
* * [1] - an object that is either XMP_RDF or XMP_PLAIN (the latter is decrecated)
* * [2] - the body text of the xpacket-instruction.
*/
+ @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
private fun findRootNode(root: Node, xmpmetaRequired: Boolean, result: Array): Array? {
/*
@@ -114,19 +116,18 @@ internal object XMPMetaParser {
when {
- child is ProcessingInstruction && XMPConst.XMP_PI == child.getTarget() -> {
+ child.nodeType == NodeConsts.PROCESSING_INSTRUCTION_NODE &&
+ XMPConst.XMP_PI == (child as ProcessingInstruction).getTarget() -> {
/* Store the processing instructions content */
result[2] = child.getData()
}
- child is Comment -> {
+ /* Ignore comments */
+ child.nodeType == NodeConsts.COMMENT_NODE -> continue
- /* Ignore comments */
- continue
- }
-
- child !is Text && child !is ProcessingInstruction -> {
+ child.nodeType != NodeConsts.TEXT_NODE &&
+ child.nodeType != NodeConsts.PROCESSING_INSTRUCTION_NODE -> {
val childElement = child as Element
diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFParser.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFParser.kt
index 9bddb6c..993fdfc 100644
--- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFParser.kt
+++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPRDFParser.kt
@@ -13,6 +13,7 @@ import com.ashampoo.xmp.options.PropertyOptions
import nl.adaptivity.xmlutil.dom.Attr
import nl.adaptivity.xmlutil.dom.Element
import nl.adaptivity.xmlutil.dom.Node
+import nl.adaptivity.xmlutil.dom.NodeConsts
import nl.adaptivity.xmlutil.dom.Text
import nl.adaptivity.xmlutil.dom.attributes
import nl.adaptivity.xmlutil.dom.childNodes
@@ -116,15 +117,17 @@ internal object XMPRDFParser {
* syntax production and adding the appropriate structure to the XMP tree.
* They simply return for success, failures will throw an exception.
*/
- @Suppress("ThrowsCount")
+ @Suppress("ThrowsCount", "UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
fun parseRdfRoot(xmp: XMPMeta, rdfRdfNode: Node, options: ParseOptions) {
if (rdfRdfNode.nodeName != "rdf:RDF")
throw XMPException("Root node should be of type rdf:RDF", XMPError.BADRDF)
- if (rdfRdfNode !is Element)
+ if (rdfRdfNode.nodeType != NodeConsts.ELEMENT_NODE)
throw XMPException("Root node must be of element type.", XMPError.BADRDF)
+ rdfRdfNode as Element
+
if (rdfRdfNode.attributes.length == 0)
throw XMPException("Illegal: rdf:RDF node has no attributes", XMPError.BADRDF)
@@ -251,8 +254,8 @@ internal object XMPRDFParser {
* @param xmlParent the currently processed XML node
* @param isTopLevel Flag if the node is a top-level node
* @param options ParseOptions to indicate the parse options provided by the client
- *
*/
+ @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
private fun parseRdfPropertyElementList(
xmp: XMPMeta,
xmpParent: XMPNode,
@@ -268,10 +271,10 @@ internal object XMPRDFParser {
if (isWhitespaceNode(currChild))
continue
- if (currChild !is Element)
+ if (currChild.nodeType != NodeConsts.ELEMENT_NODE)
throw XMPException("Expected property element node not found", XMPError.BADRDF)
- parseRdfPropertyElement(xmp, xmpParent, currChild, isTopLevel, options)
+ parseRdfPropertyElement(xmp, xmpParent, currChild as Element, isTopLevel, options)
}
}
@@ -327,8 +330,8 @@ internal object XMPRDFParser {
*
* NOTE: The RDF syntax does not explicitly include the xml:lang attribute although it can
* appear in many of these. We have to allow for it in the attibute counts below.
- *
*/
+ @Suppress("NestedBlockDepth", "UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
private fun parseRdfPropertyElement(
xmp: XMPMeta,
xmpParent: XMPNode,
@@ -422,7 +425,7 @@ internal object XMPRDFParser {
val currentChild = xmlNode.childNodes.item(index)
- if (currentChild !is Text) {
+ if (currentChild?.nodeType != NodeConsts.TEXT_NODE) {
parseRdfResourcePropertyElement(xmp, xmpParent, xmlNode, isTopLevel, options)
@@ -489,7 +492,9 @@ internal object XMPRDFParser {
if (!isWhitespaceNode(currentChild)) {
- if (currentChild is Element && !found) {
+ if (currentChild.nodeType == NodeConsts.ELEMENT_NODE && !found) {
+
+ currentChild as Element
val isRDF = XMPConst.NS_RDF == currentChild.namespaceURI
@@ -591,9 +596,11 @@ internal object XMPRDFParser {
val child = xmlNode.childNodes.item(index)
- if (child !is Text)
+ if (child?.nodeType != NodeConsts.TEXT_NODE)
throw XMPException("Invalid child of literal property element", XMPError.BADRDF)
+ child as Text
+
textValue += child.data
}
@@ -793,10 +800,10 @@ internal object XMPRDFParser {
if (hasValueAttr || hasResourceAttr) {
- val valueNodeValue = when (valueNode) {
- null -> null
- is Attr -> valueNode.value
- else -> throw XMPException("Unknown Node ${xmlNode.nodeType}", XMPError.BADXMP)
+ val valueNodeValue = when {
+ valueNode == null -> null
+ valueNode.nodeType == NodeConsts.ATTRIBUTE_NODE -> (valueNode as Attr).value
+ else -> throw XMPException("Unknown node type ${xmlNode.nodeType}", XMPError.BADXMP)
}
childNode.value = valueNodeValue ?: ""
@@ -853,6 +860,7 @@ internal object XMPRDFParser {
}
}
+ @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
private fun addChildNode(
xmp: XMPMeta,
xmpParent: XMPNode,
@@ -861,10 +869,10 @@ internal object XMPRDFParser {
isTopLevel: Boolean
): XMPNode {
- var namespace = when (xmlNode) {
- is Element -> xmlNode.namespaceURI
- is Attr -> xmlNode.namespaceURI
- else -> throw XMPException("Unknown Node ${xmlNode.nodeType}", XMPError.BADXMP)
+ var namespace = when {
+ xmlNode.nodeType == NodeConsts.ELEMENT_NODE -> (xmlNode as Element).namespaceURI
+ xmlNode.nodeType == NodeConsts.ATTRIBUTE_NODE -> (xmlNode as Attr).namespaceURI
+ else -> throw XMPException("Unknown node type ${xmlNode.nodeType}", XMPError.BADXMP)
}
if (namespace.isNullOrEmpty())
@@ -881,10 +889,10 @@ internal object XMPRDFParser {
if (prefix == null) {
- val xmlNodePrefix = when (xmlNode) {
- is Element -> xmlNode.prefix
- is Attr -> xmlNode.prefix
- else -> throw XMPException("Unknown Node ${xmlNode.nodeType}", XMPError.BADXMP)
+ val xmlNodePrefix = when {
+ xmlNode.nodeType == NodeConsts.ELEMENT_NODE -> (xmlNode as Element).prefix
+ xmlNode.nodeType == NodeConsts.ATTRIBUTE_NODE -> (xmlNode as Attr).prefix
+ else -> throw XMPException("Unknown node type ${xmlNode.nodeType}", XMPError.BADXMP)
}
prefix = if (xmlNodePrefix != null)
@@ -895,10 +903,10 @@ internal object XMPRDFParser {
prefix = XMPSchemaRegistry.registerNamespace(namespace, prefix)
}
- val xmlNodeLocalName = when (xmlNode) {
- is Element -> xmlNode.localName
- is Attr -> xmlNode.localName
- else -> throw XMPException("Unknown Node ${xmlNode.nodeType}", XMPError.BADXMP)
+ val xmlNodeLocalName = when {
+ xmlNode.nodeType == NodeConsts.ELEMENT_NODE -> (xmlNode as Element).localName
+ xmlNode.nodeType == NodeConsts.ATTRIBUTE_NODE -> (xmlNode as Attr).localName
+ else -> throw XMPException("Unknown node type ${xmlNode.nodeType}", XMPError.BADXMP)
}
val childName = prefix + xmlNodeLocalName
@@ -1061,12 +1069,13 @@ internal object XMPRDFParser {
* @param node an XML-node
* @return Returns whether the node is a whitespace node, i.e. a text node that contains only whitespaces.
*/
+ @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
private fun isWhitespaceNode(node: Node): Boolean {
- if (node !is Text)
+ if (node.nodeType != NodeConsts.TEXT_NODE)
return false
- val value = node.data
+ val value = (node as Text).data
for (index in 0 until value.length)
if (!value[index].isWhitespace())
@@ -1115,11 +1124,12 @@ internal object XMPRDFParser {
* @param node an XML node
* @return Returns the term ID.
*/
+ @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
private fun getRDFTermKind(node: Node): Int {
- val namespace = when (node) {
- is Element -> node.namespaceURI
- is Attr -> node.namespaceURI
+ val namespace = when {
+ node.nodeType == NodeConsts.ELEMENT_NODE -> (node as Element).namespaceURI
+ node.nodeType == NodeConsts.ATTRIBUTE_NODE -> (node as Attr).namespaceURI
else -> throw XMPException("Unknown Node ${node.nodeType}", XMPError.BADXMP)
}
@@ -1133,8 +1143,8 @@ internal object XMPRDFParser {
@Suppress("ComplexCondition")
val mustBeRdfNamespace = namespace.isNullOrEmpty() &&
("about" == node.nodeName || "ID" == node.nodeName) &&
- node is Attr &&
- XMPConst.NS_RDF == node.ownerElement?.namespaceURI
+ node.nodeType == NodeConsts.ATTRIBUTE_NODE &&
+ XMPConst.NS_RDF == (node as Attr).ownerElement?.namespaceURI
if (mustBeRdfNamespace || namespace == XMPConst.NS_RDF) {
diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistry.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistry.kt
index fe23f8e..4251264 100644
--- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistry.kt
+++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPSchemaRegistry.kt
@@ -218,7 +218,16 @@ object XMPSchemaRegistry {
// register other common schemas
registerNamespace(XMPConst.NS_MWG_RS, "mwg-rs")
+
+ // register product specific schemas
+ registerNamespace(XMPConst.NS_ASHAMPOO, "ashampoo")
registerNamespace(XMPConst.NS_ACDSEE, "acdsee")
+ registerNamespace(XMPConst.NS_DIGIKAM, "digiKam")
+ registerNamespace(XMPConst.NS_MYLIO, "MY")
+ registerNamespace(XMPConst.NS_NARRATIVE, "narrative")
+ registerNamespace(XMPConst.NS_MICROSOFT_PHOTO, "MicrosoftPhoto")
+ registerNamespace(XMPConst.NS_LIGHTROOM, "lr")
+ registerNamespace(XMPConst.NS_PHOTOSHOP, "photoshop")
// register Adobe standard namespaces
registerNamespace(XMPConst.NS_X, "x")
@@ -237,7 +246,6 @@ object XMPSchemaRegistry {
registerNamespace(XMPConst.NS_PDFA_FIELD, "pdfaField")
registerNamespace(XMPConst.NS_PDFA_ID, "pdfaid")
registerNamespace(XMPConst.NS_PDFA_EXTENSION, "pdfaExtension")
- registerNamespace(XMPConst.NS_PHOTOSHOP, "photoshop")
registerNamespace(XMPConst.NS_PS_ALBUM, "album")
registerNamespace(XMPConst.NS_EXIF, "exif")
registerNamespace(XMPConst.NS_EXIF_CIPA, "exifEX")
diff --git a/src/commonMain/kotlin/com/ashampoo/xmp/XMPVersionInfo.kt b/src/commonMain/kotlin/com/ashampoo/xmp/XMPVersionInfo.kt
index 170f878..5a3d7f8 100644
--- a/src/commonMain/kotlin/com/ashampoo/xmp/XMPVersionInfo.kt
+++ b/src/commonMain/kotlin/com/ashampoo/xmp/XMPVersionInfo.kt
@@ -8,7 +8,7 @@ package com.ashampoo.xmp
object XMPVersionInfo {
const val MAJOR: Int = 1
- const val MINOR: Int = 1
+ const val MINOR: Int = 2
const val PATCH: Int = 0
const val VERSION_MESSAGE = "Ashampoo XMP Core $MAJOR.$MINOR.$PATCH"
diff --git a/src/commonTest/kotlin/com/ashampoo/xmp/ReadXmpTest.kt b/src/commonTest/kotlin/com/ashampoo/xmp/ReadXmpTest.kt
index b54ec47..68a691b 100644
--- a/src/commonTest/kotlin/com/ashampoo/xmp/ReadXmpTest.kt
+++ b/src/commonTest/kotlin/com/ashampoo/xmp/ReadXmpTest.kt
@@ -3,6 +3,7 @@ package com.ashampoo.xmp
import com.ashampoo.xmp.XMPConst.XMP_DC_SUBJECT
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertTrue
/**
* Demonstrates how to use the library to read values.
@@ -15,14 +16,23 @@ class ReadXmpTest {
val testXmp = """
+
+
+ America trip
+ My wedding
+
+
fox
@@ -60,6 +70,8 @@ class ReadXmpTest {
actual = xmpMeta.getRating()
)
+ assertTrue(xmpMeta.isFlagged())
+
assertEquals(
expected = 2,
actual = xmpMeta.countArrayItems(XMPConst.NS_DC, XMP_DC_SUBJECT)
@@ -79,5 +91,10 @@ class ReadXmpTest {
expected = setOf("fox", "swiper"),
actual = xmpMeta.getKeywords()
)
+
+ assertEquals(
+ expected = setOf("My wedding", "America trip"),
+ actual = xmpMeta.getAlbums()
+ )
}
}
diff --git a/src/commonTest/kotlin/com/ashampoo/xmp/WriteXmpTest.kt b/src/commonTest/kotlin/com/ashampoo/xmp/WriteXmpTest.kt
index 0984230..c392d17 100644
--- a/src/commonTest/kotlin/com/ashampoo/xmp/WriteXmpTest.kt
+++ b/src/commonTest/kotlin/com/ashampoo/xmp/WriteXmpTest.kt
@@ -21,9 +21,6 @@ class WriteXmpTest {
.setUseCanonicalFormat(false)
.setSort(true)
- private val arrayOptions =
- PropertyOptions().setArray(true)
-
/**
* Create an empty XMP file with only the required envelope.
*/
@@ -168,6 +165,8 @@ class WriteXmpTest {
xmpMeta.setRating(3)
+ xmpMeta.setFlagged(true)
+
xmpMeta.setDateTimeOriginal("2023-07-07T13:37:42")
xmpMeta.setGpsCoordinates(
@@ -186,6 +185,8 @@ class WriteXmpTest {
widthPx = 1500,
heightPx = 1000
)
+
+ xmpMeta.setAlbums(setOf("My wedding", "America trip"))
}
private fun getXmp(name: String): String =
diff --git a/src/commonTest/resources/com/ashampoo/xmp/empty.xmp b/src/commonTest/resources/com/ashampoo/xmp/empty.xmp
index 3900b8c..ac5299c 100644
--- a/src/commonTest/resources/com/ashampoo/xmp/empty.xmp
+++ b/src/commonTest/resources/com/ashampoo/xmp/empty.xmp
@@ -1,5 +1,5 @@
-
+
diff --git a/src/commonTest/resources/com/ashampoo/xmp/new.xmp b/src/commonTest/resources/com/ashampoo/xmp/new.xmp
index 1038c04..64fd9d2 100644
--- a/src/commonTest/resources/com/ashampoo/xmp/new.xmp
+++ b/src/commonTest/resources/com/ashampoo/xmp/new.xmp
@@ -1,18 +1,33 @@
-
+
+ narrative:Tagged="True"
+ xmp:Rating="3"
+ xmpDM:pick="1">
+
+
+ America trip
+ My wedding
+
+
bird
diff --git a/src/commonTest/resources/com/ashampoo/xmp/rating.xmp b/src/commonTest/resources/com/ashampoo/xmp/rating.xmp
index 89d93bc..18abbbf 100644
--- a/src/commonTest/resources/com/ashampoo/xmp/rating.xmp
+++ b/src/commonTest/resources/com/ashampoo/xmp/rating.xmp
@@ -1,5 +1,5 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/src/commonTest/resources/com/ashampoo/xmp/sample_13_formatted_compact.xmp b/src/commonTest/resources/com/ashampoo/xmp/sample_13_formatted_compact.xmp
index 32a1b62..bc8a6b6 100644
--- a/src/commonTest/resources/com/ashampoo/xmp/sample_13_formatted_compact.xmp
+++ b/src/commonTest/resources/com/ashampoo/xmp/sample_13_formatted_compact.xmp
@@ -1,5 +1,5 @@
-
+
-
+
diff --git a/src/commonTest/resources/com/ashampoo/xmp/sample_14_formatted_compact.xmp b/src/commonTest/resources/com/ashampoo/xmp/sample_14_formatted_compact.xmp
index 139b7bb..925873b 100644
--- a/src/commonTest/resources/com/ashampoo/xmp/sample_14_formatted_compact.xmp
+++ b/src/commonTest/resources/com/ashampoo/xmp/sample_14_formatted_compact.xmp
@@ -1,5 +1,5 @@
-
+
-
+
diff --git a/src/commonTest/resources/com/ashampoo/xmp/sample_15_formatted_compact.xmp b/src/commonTest/resources/com/ashampoo/xmp/sample_15_formatted_compact.xmp
index 6e22b0e..71ebcda 100644
--- a/src/commonTest/resources/com/ashampoo/xmp/sample_15_formatted_compact.xmp
+++ b/src/commonTest/resources/com/ashampoo/xmp/sample_15_formatted_compact.xmp
@@ -1,5 +1,5 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/src/commonTest/resources/com/ashampoo/xmp/sample_31_formatted_compact.xmp b/src/commonTest/resources/com/ashampoo/xmp/sample_31_formatted_compact.xmp
index 139b7bb..925873b 100644
--- a/src/commonTest/resources/com/ashampoo/xmp/sample_31_formatted_compact.xmp
+++ b/src/commonTest/resources/com/ashampoo/xmp/sample_31_formatted_compact.xmp
@@ -1,5 +1,5 @@
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/src/commonTest/resources/com/ashampoo/xmp/sample_34_formatted_compact.xmp b/src/commonTest/resources/com/ashampoo/xmp/sample_34_formatted_compact.xmp
index 139b7bb..925873b 100644
--- a/src/commonTest/resources/com/ashampoo/xmp/sample_34_formatted_compact.xmp
+++ b/src/commonTest/resources/com/ashampoo/xmp/sample_34_formatted_compact.xmp
@@ -1,5 +1,5 @@
-
+
-
+
-
+