Skip to content

Commit

Permalink
Add XmpMeta::localized_text accessor
Browse files Browse the repository at this point in the history
  • Loading branch information
scouten-adobe committed Oct 22, 2022
1 parent 8a55d32 commit c8e1fd1
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 0 deletions.
36 changes: 36 additions & 0 deletions src/ffi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
121 changes: 121 additions & 0 deletions src/tests/xmp_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<rdf:Description rdf:about="">
<dc:title>
<rdf:Alt>
<rdf:li xml:lang="x-default">
XMP - Extensible Metadata Platform
</rdf:li>
<rdf:li xml:lang="en-us">
XMP - Extensible Metadata Platform (US English)
</rdf:li>
<rdf:li xml:lang="fr">
XMP - Une Platforme Extensible pour les Métadonnées
</rdf:li>
</rdf:Alt>
</dc:title>
</rdf:Description>
</rdf:RDF>"#;

#[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
);
}
}
121 changes: 121 additions & 0 deletions src/xmp_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>, 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>, 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 {
Expand Down

0 comments on commit c8e1fd1

Please sign in to comment.