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 {