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 @@ - + - + - + - + - + - + - + - + - + - + diff --git a/src/commonTest/resources/com/ashampoo/xmp/sample_39_formatted_compact.xmp b/src/commonTest/resources/com/ashampoo/xmp/sample_39_formatted_compact.xmp index 32a1b62..bc8a6b6 100644 --- a/src/commonTest/resources/com/ashampoo/xmp/sample_39_formatted_compact.xmp +++ b/src/commonTest/resources/com/ashampoo/xmp/sample_39_formatted_compact.xmp @@ -1,5 +1,5 @@ - + - + - + - + - + - + diff --git a/src/commonTest/resources/com/ashampoo/xmp/sample_41_formatted_compact.xmp b/src/commonTest/resources/com/ashampoo/xmp/sample_41_formatted_compact.xmp index 32a1b62..bc8a6b6 100644 --- a/src/commonTest/resources/com/ashampoo/xmp/sample_41_formatted_compact.xmp +++ b/src/commonTest/resources/com/ashampoo/xmp/sample_41_formatted_compact.xmp @@ -1,5 +1,5 @@ - + - + - + - + diff --git a/src/commonTest/resources/com/ashampoo/xmp/sample_43_formatted_compact.xmp b/src/commonTest/resources/com/ashampoo/xmp/sample_43_formatted_compact.xmp index cc23945..4b1bc68 100644 --- a/src/commonTest/resources/com/ashampoo/xmp/sample_43_formatted_compact.xmp +++ b/src/commonTest/resources/com/ashampoo/xmp/sample_43_formatted_compact.xmp @@ -1,5 +1,5 @@ - + - + diff --git a/src/commonTest/resources/com/ashampoo/xmp/sample_44_formatted_compact.xmp b/src/commonTest/resources/com/ashampoo/xmp/sample_44_formatted_compact.xmp index 58500c7..e81b567 100644 --- a/src/commonTest/resources/com/ashampoo/xmp/sample_44_formatted_compact.xmp +++ b/src/commonTest/resources/com/ashampoo/xmp/sample_44_formatted_compact.xmp @@ -1,5 +1,5 @@ - + - + - + - + - + - + - + - + - + - + diff --git a/src/commonTest/resources/com/ashampoo/xmp/sample_49_formatted_compact.xmp b/src/commonTest/resources/com/ashampoo/xmp/sample_49_formatted_compact.xmp index 139b7bb..925873b 100644 --- a/src/commonTest/resources/com/ashampoo/xmp/sample_49_formatted_compact.xmp +++ b/src/commonTest/resources/com/ashampoo/xmp/sample_49_formatted_compact.xmp @@ -1,5 +1,5 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + narrative:Tagged="True" + xmp:Rating="3" + xmpDM:pick="1"> + + + America trip + My wedding + + bird