diff --git a/src/ffi.cpp b/src/ffi.cpp index 09a5884..d83fad2 100644 --- a/src/ffi.cpp +++ b/src/ffi.cpp @@ -632,6 +632,42 @@ extern "C" { #endif } + const char* CXmpMetaGetLocalizedText(CXmpMeta* m, + CXmpError* outError, + const char* schemaNS, + const char* altTextName, + const char* genericLang, + const char* specificLang, + const char** actualLang, + AdobeXMPCommon::uint32* outOptions) { + *outOptions = 0; + + #ifndef NOOP_FFI + try { + std::string propValue; + std::string outActualLang; + if (m->m.GetLocalizedText(schemaNS, + altTextName, + genericLang, + specificLang, + &outActualLang, + &propValue, + outOptions)) { + *actualLang = copyStringForResult(outActualLang); + return copyStringForResult(propValue); + } + } + catch (XMP_Error& e) { + copyErrorForResult(e, outError); + } + catch (...) { + signalUnknownError(outError); + } + #endif + + return NULL; + } + // --- CXmpDateTime --- void CXmpDateTimeCurrent(XMP_DateTime* dt, CXmpError* outError) { diff --git a/src/ffi.rs b/src/ffi.rs index 376d27f..e5fb80e 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -214,6 +214,17 @@ extern "C" { out_options: *mut u32, ) -> *mut c_char; + pub(crate) fn CXmpMetaGetLocalizedText( + meta: *mut 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, + out_actual_lang: *mut *const c_char, + out_options: *mut u32, + ) -> *mut c_char; + // --- CXmpDateTime --- pub(crate) fn CXmpDateTimeCurrent(dt: *mut CXmpDateTime, out_error: *mut CXmpError); diff --git a/src/tests/xmp_meta.rs b/src/tests/xmp_meta.rs index 9b3405a..fa84a12 100644 --- a/src/tests/xmp_meta.rs +++ b/src/tests/xmp_meta.rs @@ -1019,3 +1019,124 @@ mod set_property_date { ); } } + +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 + + + + + "#; + + #[test] + fn happy_path() { + let m = XmpMeta::from_str(LOCALIZED_TEXT_EXAMPLE).unwrap(); + + let (value, actual_lang) = m + .localized_text(xmp_ns::DC, "title", None, "x-default") + .unwrap(); + + assert_eq!(value.value.trim(), "XMP - Extensible Metadata Platform"); + assert_eq!(value.options, xmp_prop::HAS_LANG | xmp_prop::HAS_QUALIFIERS); + assert_eq!(actual_lang, "x-default"); + + let (value, actual_lang) = m + .localized_text(xmp_ns::DC, "title", Some("x-default"), "x-default") + .unwrap(); + + assert_eq!(value.value.trim(), "XMP - Extensible Metadata Platform"); + assert_eq!(value.options, xmp_prop::HAS_LANG | xmp_prop::HAS_QUALIFIERS); + assert_eq!(actual_lang, "x-default"); + + let (value, actual_lang) = m + .localized_text(xmp_ns::DC, "title", Some("en"), "en-US") + .unwrap(); + + assert_eq!( + value.value.trim(), + "XMP - Extensible Metadata Platform (US English)" + ); + assert_eq!(value.options, xmp_prop::HAS_LANG | xmp_prop::HAS_QUALIFIERS); + assert_eq!(actual_lang, "en-US"); + + let (value, actual_lang) = m + .localized_text(xmp_ns::DC, "title", Some("en-us"), "en-uk") + .unwrap(); + + assert_eq!(value.value.trim(), "XMP - Extensible Metadata Platform"); + assert_eq!(value.options, xmp_prop::HAS_LANG | xmp_prop::HAS_QUALIFIERS); + assert_eq!(actual_lang, "x-default"); + + let (value, actual_lang) = m + .localized_text(xmp_ns::DC, "title", Some("fr"), "fr") + .unwrap(); + + assert_eq!( + value.value.trim(), + "XMP - Une Platforme Extensible pour les Métadonnées" + ); + assert_eq!(value.options, xmp_prop::HAS_LANG | xmp_prop::HAS_QUALIFIERS); + assert_eq!(actual_lang, "fr"); + } + + #[test] + fn empty_namespace() { + let m = XmpMeta::from_str(LOCALIZED_TEXT_EXAMPLE).unwrap(); + assert_eq!(m.localized_text("", "CreatorTool", None, "x-default"), None); + } + + #[test] + fn empty_prop_name() { + let m = XmpMeta::from_str(LOCALIZED_TEXT_EXAMPLE).unwrap(); + assert_eq!(m.localized_text(xmp_ns::XMP, "", None, "x-default"), None); + } + + #[test] + fn invalid_namespace() { + let m = XmpMeta::from_str(LOCALIZED_TEXT_EXAMPLE).unwrap(); + assert_eq!( + m.localized_text("\0", "CreatorTool", None, "x-default"), + None, + ); + } + + #[test] + fn invalid_prop_name() { + let m = XmpMeta::from_str(LOCALIZED_TEXT_EXAMPLE).unwrap(); + assert_eq!(m.localized_text(xmp_ns::XMP, "\0", None, "x-default"), None); + } + + #[test] + fn invalid_generic_lang() { + let m = XmpMeta::from_str(LOCALIZED_TEXT_EXAMPLE).unwrap(); + assert_eq!( + m.localized_text(xmp_ns::XMP, "title", Some("no-such-lang"), "x-default"), + None + ); + } + + #[test] + fn invalid_specific_lang() { + let m = XmpMeta::from_str(LOCALIZED_TEXT_EXAMPLE).unwrap(); + assert_eq!( + m.localized_text(xmp_ns::XMP, "title", Some("x-default"), "no-such-lang"), + None + ); + } +} diff --git a/src/xmp_meta.rs b/src/xmp_meta.rs index fb0d63e..4e09502 100644 --- a/src/xmp_meta.rs +++ b/src/xmp_meta.rs @@ -592,6 +592,127 @@ impl XmpMeta { XmpError::raise_from_c(&err) } + + /// Retrieves information about a selected item from an alt-text array. + /// + /// Localized text properties are stored in alt-text arrays. They allow + /// multiple concurrent localizations of a property value, for example a + /// document title or copyright in several languages. These functions + /// provide convenient support for localized text properties, including a + /// number of special and obscure aspects. The most important aspect of + /// these functions is that they select an appropriate array item based on + /// one or two RFC 3066 language tags. One of these languages, the + /// "specific" language, is preferred and selected if there is an exact + /// match. For many languages it is also possible to define a "generic" + /// language that can be used if there is no specific language match. The + /// generic language must be a valid RFC 3066 primary subtag, or the empty + /// string. + /// + /// For example, a specific language of `en-US` should be used in the US, + /// and a specific language of `en-UK` should be used in England. It is also + /// appropriate to use `en` as the generic language in each case. If a US + /// document goes to England, the `en-US` title is selected by using the + /// `en` generic language and the `en-UK` specific language. + /// + /// It is considered poor practice, but allowed, to pass a specific language + /// that is just an RFC 3066 primary tag. For example `en` is not a good + /// specific language, it should only be used as a generic language. Passing + /// `i` or `x` as the generic language is also considered poor practice but + /// allowed. + /// + /// Advice from the W3C about the use of RFC 3066 language tags can be found at http://www.w3.org/International/articles/language-tags/. + /// + /// **Note:** RFC 3066 language tags must be treated in a case insensitive + /// manner. The XMP toolkit does this by normalizing their capitalization: + /// + /// * The primary subtag is lower case, the suggested practice of ISO 639. + /// * All 2-letter secondary subtags are upper case, the suggested practice + /// of ISO 3166. + /// * All other subtags are lower case. The XMP specification defines an + /// 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: + /// * 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. + /// + /// ## Arguments + /// + /// * `namespace` and `path`: See [Accessing + /// properties](#accessing-properties). + /// * `generic_lang`: The name of the generic language as an RFC 3066 + /// primary subtag. Can be `None` or the empty string if no generic + /// language is wanted. + /// * `specific_lang`: The name of the specific language as an RFC 3066 tag, + /// or `x-default`. Must not be an empty string. + /// + /// ## Return value + /// + /// If a suitable match is found, returns `Some(XmpValue, String)` + /// where the second string is the actual language that was matched. + /// + /// ## Error handling + /// + /// Any errors (for instance, empty or invalid namespace or property name) + /// are ignored; the function will return `false` in such cases. + pub fn localized_text( + &self, + namespace: &str, + path: &str, + generic_lang: Option<&str>, + specific_lang: &str, + ) -> Option<(XmpValue, String)> { + 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 mut options: u32 = 0; + let mut err = ffi::CXmpError::default(); + + unsafe { + let mut c_actual_lang: *const i8 = std::ptr::null_mut(); + + let c_result = ffi::CXmpMetaGetLocalizedText( + self.m, + &mut err, + c_ns.as_ptr(), + c_name.as_ptr(), + match c_generic_lang { + Some(p) => p.as_ptr(), + None => std::ptr::null(), + }, + c_specific_lang.as_ptr(), + &mut c_actual_lang, + &mut options, + ); + + if c_result.is_null() { + None + } else { + Some(( + XmpValue { + value: CStr::from_ptr(c_result).to_string_lossy().into_owned(), + options, + }, + CStr::from_ptr(c_actual_lang).to_string_lossy().into_owned(), + )) + } + } + } } impl FromStr for XmpMeta {