diff --git a/src/ffi.cpp b/src/ffi.cpp index a4543d4..65c92d1 100644 --- a/src/ffi.cpp +++ b/src/ffi.cpp @@ -1056,6 +1056,32 @@ extern "C" { return NULL; } + void CXmpMetaSetLocalizedText(CXmpMeta* m, + CXmpError* outError, + const char* schemaNS, + const char* altTextName, + const char* genericLang, + const char* specificLang, + const char* itemValue, const char** actualLang, + AdobeXMPCommon::uint32 options) { + #ifndef NOOP_FFI + try { + m->m.SetLocalizedText(schemaNS, + altTextName, + genericLang, + specificLang, + itemValue, + options); + } + catch (XMP_Error& e) { + copyErrorForResult(e, outError); + } + catch (...) { + signalUnknownError(outError); + } + #endif + } + const char* CXmpMetaGetObjectName(CXmpMeta* m, CXmpError* outError) { #ifndef NOOP_FFI try { diff --git a/src/ffi.rs b/src/ffi.rs index a35f308..e09d28a 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -451,6 +451,17 @@ extern "C" { out_options: *mut u32, ) -> *const c_char; + pub(crate) fn CXmpMetaSetLocalizedText( + meta: *const CXmpMeta, + out_error: *mut CXmpError, + schema_ns: *const c_char, + alt_text_name: *const c_char, + generic_lang: *const c_char, + specific_lang: *const c_char, + item_value: *const c_char, + options: u32, + ); + pub(crate) fn CXmpMetaGetObjectName( meta: *mut CXmpMeta, out_error: *mut CXmpError, diff --git a/src/tests/fixtures/mod.rs b/src/tests/fixtures/mod.rs index 7b1bf02..676340a 100644 --- a/src/tests/fixtures/mod.rs +++ b/src/tests/fixtures/mod.rs @@ -170,3 +170,16 @@ pub(crate) const QUAL_EXAMPLE: &str = r#" "#; + +pub(crate) const LOCALIZED_TEXT_EXAMPLE: &str = r#" + + + + XMP - Extensible Metadata Platform + XMP - Extensible Metadata Platform (US English) + XMP - Une Platforme Extensible pour les Métadonnées + + + + "#; diff --git a/src/tests/xmp_meta.rs b/src/tests/xmp_meta.rs index 412ed4d..cad42b0 100644 --- a/src/tests/xmp_meta.rs +++ b/src/tests/xmp_meta.rs @@ -2709,26 +2709,7 @@ mod delete_qualifier { mod localized_text { use std::str::FromStr; - use crate::{xmp_ns, xmp_value::xmp_prop, XmpMeta}; - - const LOCALIZED_TEXT_EXAMPLE: &str = r#" - - - - - XMP - Extensible Metadata Platform - - - XMP - Extensible Metadata Platform (US English) - - - XMP - Une Platforme Extensible pour les Métadonnées - - - - - "#; + use crate::{tests::fixtures::LOCALIZED_TEXT_EXAMPLE, xmp_ns, xmp_value::xmp_prop, XmpMeta}; #[test] fn happy_path() { @@ -2837,6 +2818,104 @@ mod localized_text { } } +mod set_localized_text { + use std::str::FromStr; + + use crate::{ + tests::fixtures::LOCALIZED_TEXT_EXAMPLE, xmp_ns, xmp_value::xmp_prop, XmpError, + XmpErrorType, XmpMeta, XmpValue, + }; + + #[test] + fn happy_path() { + let mut m = XmpMeta::from_str(LOCALIZED_TEXT_EXAMPLE).unwrap(); + + assert_eq!( + m.localized_text(xmp_ns::DC, "title", None, "en-us") + .unwrap(), + ( + XmpValue { + value: "XMP - Extensible Metadata Platform (US English)".to_owned(), + options: xmp_prop::HAS_LANG | xmp_prop::HAS_QUALIFIERS + }, + "en-US".to_owned() + ) + ); + + m.set_localized_text(xmp_ns::DC, "title", None, "en-us", "XMP in Rust") + .unwrap(); + + assert_eq!( + m.localized_text(xmp_ns::DC, "title", None, "en-us") + .unwrap(), + ( + XmpValue { + value: "XMP in Rust".to_owned(), + options: xmp_prop::HAS_LANG | xmp_prop::HAS_QUALIFIERS + }, + "en-US".to_owned() + ) + ); + } + + #[test] + fn generic_lang() { + let mut m = XmpMeta::default(); + + const NS1: &str = "ns:test1/"; + + m.set_localized_text(NS1, "AltText", None, "x-default", "default value") + .unwrap(); + + m.set_localized_text(NS1, "AltText", Some("en"), "en-us", "en-us value") + .unwrap(); + + m.set_localized_text(NS1, "AltText", Some("en"), "en-uk", "en-uk value") + .unwrap(); + + assert_eq!(m.to_string(), " en-us value en-us value en-uk value "); + } + + #[test] + fn init_fail() { + let mut m = XmpMeta::new_fail(); + + assert_eq!( + m.set_localized_text(xmp_ns::DC, "title", None, "en-us", "XMP in Rust"), + Err(XmpError { + error_type: XmpErrorType::NoCppToolkit, + debug_message: "C++ XMP Toolkit not available".to_owned() + }) + ); + } + + #[test] + fn error_empty_struct_name() { + let mut m = XmpMeta::default(); + + assert_eq!( + m.set_localized_text(xmp_ns::XMP, "", None, "CiAdrPcode", "95110",), + Err(XmpError { + error_type: XmpErrorType::BadXPath, + debug_message: "Empty array name".to_owned() + }) + ); + } + + #[test] + fn error_nul_in_name() { + let mut m = XmpMeta::default(); + + assert_eq!( + m.set_localized_text(xmp_ns::XMP, "x\0x", None, "en-US", "95110",), + Err(XmpError { + error_type: XmpErrorType::BadXPath, + debug_message: "Empty array name".to_owned() + }) + ); + } +} + mod name { use crate::{XmpErrorType, XmpMeta}; diff --git a/src/xmp_meta.rs b/src/xmp_meta.rs index 8f2327c..34c25a7 100644 --- a/src/xmp_meta.rs +++ b/src/xmp_meta.rs @@ -1349,9 +1349,9 @@ impl XmpMeta { /// artificial language, `x-default`, that is used to explicitly denote a /// default item in an alt-text array. The XMP toolkit normalizes alt-text /// arrays such that the x-default item is the first item. The - /// `set_localized_text` function has several special features related to - /// the `x-default` item. See its description for details. The array item - /// is selected according to these rules: + /// [`XmpMeta::set_localized_text()`] function has several special + /// features related to the `x-default` item. See its description for + /// details. The array item is selected according to these rules: /// * Look for an exact match with the specific language. /// * If a generic language is given, look for a partial match. /// * Look for an `x-default` item. @@ -1429,6 +1429,89 @@ impl XmpMeta { } } + /// Modifies the value of a selected item in an alt-text array using a + /// string object. + /// + /// Creates an appropriate array item if necessary, and handles special + /// cases for the `x-default` item. + /// + /// The array item is selected according to these rules: + /// + /// * Look for an exact match with the specific language. + /// * If a generic language is given, look for a partial match. + /// * Look for an `x-default` item. + /// * Choose the first item. + /// + /// A partial match with the generic language is where the start of the + /// item's language matches the generic string and the next character is + /// `-`. An exact match is also recognized as a degenerate case. + /// + /// You can pass `x-default` as the specific language. In this case, + /// selection of an `x-default` item is an exact match by the first rule, + /// not a selection by the 3rd rule. The last 2 rules are fallbacks used + /// when the specific and generic languages fail to produce a match. + /// + /// Item values are modified according to these rules: + /// + /// * If the selected item is from a match with the specific language, the + /// value of that item is modified. If the existing value of that item + /// matches the existing value of the `x-default` item, the `x-default` + /// item is also modified. If the array only has 1 existing item (which is + /// not `x-default`), an `x-default` item is added with the given value. + /// * If the selected item is from a match with the generic language and + /// there are no other generic matches, the value of that item is + /// modified. If the existing value of that item matches the existing + /// value of the `x-default` item, the `x-default` item is also modified. + /// If the array only has 1 existing item (which is not `x-default`), an + /// `x-default` item is added with the given value. + /// * If the selected item is from a partial match with the generic language + /// and there are other partial matches, a new item is created for the + /// specific language. The `x-default` item is not modified. + /// * If the selected item is from the last 2 rules then a new item is + /// created for the specific language. If the array only had an + /// `x-default` item, the `x-default` item is also modified. If the array + /// was empty, items are created for the specific language and + /// `x-default`. + pub fn set_localized_text( + &mut self, + namespace: &str, + path: &str, + generic_lang: Option<&str>, + specific_lang: &str, + item_value: &str, + ) -> XmpResult<()> { + if let Some(m) = self.m { + let c_ns = CString::new(namespace).unwrap_or_default(); + let c_name = CString::new(path).unwrap_or_default(); + let c_generic_lang = generic_lang.map(|s| CString::new(s).unwrap_or_default()); + let c_specific_lang = CString::new(specific_lang).unwrap_or_default(); + let c_item_value = CString::new(item_value).unwrap_or_default(); + + let mut err = ffi::CXmpError::default(); + + unsafe { + ffi::CXmpMetaSetLocalizedText( + m, + &mut err, + c_ns.as_ptr(), + c_name.as_ptr(), + match c_generic_lang { + Some(lang) => lang.as_ptr(), + None => std::ptr::null(), + }, + c_specific_lang.as_ptr(), + c_item_value.as_ptr(), + 0, + ); + }; + + XmpError::raise_from_c(&err)?; + Ok(()) + } else { + Err(no_cpp_toolkit()) + } + } + /// Composes the path expression for an item in an array. /// /// ## Arguments