use std::{
fmt::{Debug, Write},
iter,
};
use crate::XmpWriter;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum Namespace<'a> {
Rdf,
DublinCore,
Xmp,
XmpRights,
XmpResourceRef,
XmpResourceEvent,
XmpVersion,
XmpJob,
XmpJobManagement,
XmpColorant,
XmpFont,
XmpDimensions,
XmpMedia,
XmpPaged,
XmpDynamicMedia,
XmpImage,
XmpIdq,
AdobePdf,
#[cfg(feature = "pdfa")]
PdfAId,
PdfXId,
#[cfg(feature = "pdfa")]
PdfAExtension,
#[cfg(feature = "pdfa")]
PdfASchema,
#[cfg(feature = "pdfa")]
PdfAProperty,
#[cfg(feature = "pdfa")]
PdfAType,
#[cfg(feature = "pdfa")]
PdfAField,
Custom(Box<CustomNamespace<'a>>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct CustomNamespace<'a> {
name: &'a str,
namespace: &'a str,
url: &'a str,
}
impl<'a> Namespace<'a> {
pub const fn name(&self) -> &'a str {
match self {
Self::Rdf => "RDF",
Self::DublinCore => "Dublin Core",
Self::Xmp => "XMP",
Self::XmpRights => "XMP Rights",
Self::XmpResourceRef => "XMP Resource Reference",
Self::XmpResourceEvent => "XMP Resource Event",
Self::XmpVersion => "XMP Version",
Self::XmpJob => "XMP Job Management",
Self::XmpColorant => "XMP Colorant",
Self::XmpFont => "XMP Font",
Self::XmpDimensions => "XMP Dimensions",
Self::XmpMedia => "XMP Media Management",
Self::XmpJobManagement => "XMP Job Management",
Self::XmpPaged => "XMP Paged Text",
Self::XmpDynamicMedia => "XMP Dynamic Media",
Self::XmpImage => "XMP Image",
Self::AdobePdf => "Adobe PDF",
Self::XmpIdq => "XMP Identifier Qualifier",
#[cfg(feature = "pdfa")]
Self::PdfAId => "PDF/A Identification",
Self::PdfXId => "PDF/X Identification",
#[cfg(feature = "pdfa")]
Self::PdfAExtension => "PDF/A Extension schema container",
#[cfg(feature = "pdfa")]
Self::PdfASchema => "PDF/A Schema container",
#[cfg(feature = "pdfa")]
Self::PdfAProperty => "PDF/A Property",
#[cfg(feature = "pdfa")]
Self::PdfAType => "PDF/A Type",
#[cfg(feature = "pdfa")]
Self::PdfAField => "PDF/A Field",
Self::Custom(custom) => custom.name,
}
}
pub fn url(&self) -> &'a str {
match self {
Self::Rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
Self::DublinCore => "http://purl.org/dc/elements/1.1/",
Self::Xmp => "http://ns.adobe.com/xap/1.0/",
Self::XmpRights => "http://ns.adobe.com/xap/1.0/rights/",
Self::XmpResourceRef => "http://ns.adobe.com/xap/1.0/sType/ResourceRef#",
Self::XmpResourceEvent => "http://ns.adobe.com/xap/1.0/sType/ResourceEvent#",
Self::XmpVersion => "http://ns.adobe.com/xap/1.0/sType/Version#",
Self::XmpJob => "http://ns.adobe.com/xap/1.0/sType/Job#",
Self::XmpColorant => "http://ns.adobe.com/xap/1.0/g/",
Self::XmpFont => "http://ns.adobe.com/xap/1.0/sType/Font#",
Self::XmpDimensions => "http://ns.adobe.com/xap/1.0/sType/Dimensions#",
Self::XmpMedia => "http://ns.adobe.com/xap/1.0/mm/",
Self::XmpJobManagement => "http://ns.adobe.com/xap/1.0/bj/",
Self::XmpPaged => "http://ns.adobe.com/xap/1.0/t/pg/",
Self::XmpDynamicMedia => "http://ns.adobe.com/xap/1.0/DynamicMedia/",
Self::XmpImage => "http://ns.adobe.com/xap/1.0/g/img/",
Self::AdobePdf => "http://ns.adobe.com/pdf/1.3/",
Self::XmpIdq => "http://ns.adobe.com/xmp/Identifier/qual/1.0/",
#[cfg(feature = "pdfa")]
Self::PdfAId => "http://www.aiim.org/pdfa/ns/id/",
Self::PdfXId => "http://www.npes.org/pdfx/ns/id/",
#[cfg(feature = "pdfa")]
Self::PdfAExtension => "http://www.aiim.org/pdfa/ns/extension/",
#[cfg(feature = "pdfa")]
Self::PdfASchema => "http://www.aiim.org/pdfa/ns/schema#",
#[cfg(feature = "pdfa")]
Self::PdfAProperty => "http://www.aiim.org/pdfa/ns/property#",
#[cfg(feature = "pdfa")]
Self::PdfAType => "http://www.aiim.org/pdfa/ns/type#",
#[cfg(feature = "pdfa")]
Self::PdfAField => "http://www.aiim.org/pdfa/ns/field#",
Self::Custom(custom) => custom.url,
}
}
pub fn prefix(&self) -> &'a str {
match self {
Self::Rdf => "rdf",
Self::DublinCore => "dc",
Self::Xmp => "xmp",
Self::XmpRights => "xmpRights",
Self::XmpResourceRef => "stRef",
Self::XmpResourceEvent => "stEvt",
Self::XmpVersion => "stVer",
Self::XmpJob => "stJob",
Self::XmpColorant => "xmpG",
Self::XmpFont => "stFnt",
Self::XmpDimensions => "stDim",
Self::XmpMedia => "xmpMM",
Self::XmpJobManagement => "xmpBJ",
Self::XmpPaged => "xmpTPg",
Self::XmpDynamicMedia => "xmpDM",
Self::XmpImage => "xmpGImg",
Self::AdobePdf => "pdf",
Self::XmpIdq => "xmpidq",
#[cfg(feature = "pdfa")]
Self::PdfAId => "pdfaid",
Self::PdfXId => "pdfxid",
#[cfg(feature = "pdfa")]
Self::PdfAExtension => "pdfaExtension",
#[cfg(feature = "pdfa")]
Self::PdfASchema => "pdfaSchema",
#[cfg(feature = "pdfa")]
Self::PdfAProperty => "pdfaProperty",
#[cfg(feature = "pdfa")]
Self::PdfAType => "pdfaType",
#[cfg(feature = "pdfa")]
Self::PdfAField => "pdfaField",
Self::Custom(custom) => custom.namespace,
}
}
}
pub struct Element<'a, 'n: 'a> {
writer: &'a mut XmpWriter<'n>,
name: &'a str,
namespace: Namespace<'n>,
}
impl<'a, 'n: 'a> Element<'a, 'n> {
pub(crate) fn start(
writer: &'a mut XmpWriter<'n>,
name: &'a str,
namespace: Namespace<'n>,
) -> Self {
Self::with_attrs(writer, name, namespace, iter::empty())
}
fn with_attrs<'b>(
writer: &'a mut XmpWriter<'n>,
name: &'a str,
namespace: Namespace<'n>,
attrs: impl IntoIterator<Item = (&'b str, &'b str)>,
) -> Self {
write!(writer.buf, "<{}:{}", namespace.prefix(), name).unwrap();
for (key, value) in attrs {
write!(writer.buf, " {}=\"{}\"", key, value).unwrap();
}
writer.namespaces.insert(namespace.clone());
Element { writer, name, namespace }
}
pub fn value(self, val: impl XmpType) {
self.writer.buf.push('>');
val.write(&mut self.writer.buf);
self.close();
}
pub fn obj(self) -> Struct<'a, 'n> {
self.writer.namespaces.insert(Namespace::Rdf);
self.writer.buf.push_str(" rdf:parseType=\"Resource\">");
Struct::start(self.writer, self.name, self.namespace)
}
pub fn array(self, kind: RdfCollectionType) -> Array<'a, 'n> {
self.writer.buf.push('>');
Array::start(self.writer, kind, self.name, self.namespace)
}
fn close(self) {
write!(self.writer.buf, "</{}:{}>", self.namespace.prefix(), self.name).unwrap();
}
pub fn language_alternative<'b>(
self,
items: impl IntoIterator<Item = (Option<LangId<'b>>, &'b str)>,
) {
let mut array = self.array(RdfCollectionType::Alt);
for (lang, value) in items {
array
.element_with_attrs(iter::once(("xml:lang", lang.unwrap_or_default().0)))
.value(value);
}
drop(array);
}
pub fn unordered_array(self, items: impl IntoIterator<Item = impl XmpType>) {
let mut array = self.array(RdfCollectionType::Bag);
for item in items {
array.element().value(item);
}
}
pub fn ordered_array(self, items: impl IntoIterator<Item = impl XmpType>) {
let mut array = self.array(RdfCollectionType::Seq);
for item in items {
array.element().value(item);
}
}
pub fn alternative_array(self, items: impl IntoIterator<Item = impl XmpType>) {
let mut array = self.array(RdfCollectionType::Alt);
for item in items {
array.element().value(item);
}
}
}
pub struct Array<'a, 'n: 'a> {
writer: &'a mut XmpWriter<'n>,
kind: RdfCollectionType,
name: &'a str,
namespace: Namespace<'a>,
}
impl<'a, 'n: 'a> Array<'a, 'n> {
fn start(
writer: &'a mut XmpWriter<'n>,
kind: RdfCollectionType,
name: &'a str,
namespace: Namespace<'n>,
) -> Self {
writer.namespaces.insert(Namespace::Rdf);
write!(writer.buf, "<rdf:{}>", kind.rdf_type()).unwrap();
Self { writer, kind, name, namespace }
}
pub fn element(&mut self) -> Element<'_, 'n> {
self.element_with_attrs(iter::empty())
}
pub fn element_with_attrs(
&mut self,
attrs: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> Element<'_, 'n> {
Element::with_attrs(self.writer, "li", Namespace::Rdf, attrs)
}
}
impl Drop for Array<'_, '_> {
fn drop(&mut self) {
write!(
self.writer.buf,
"</rdf:{}></{}:{}>",
self.kind.rdf_type(),
self.namespace.prefix(),
self.name
)
.unwrap();
}
}
pub struct Struct<'a, 'n: 'a> {
writer: &'a mut XmpWriter<'n>,
name: &'a str,
namespace: Namespace<'a>,
}
impl<'a, 'n: 'a> Struct<'a, 'n> {
fn start(
writer: &'a mut XmpWriter<'n>,
name: &'a str,
namespace: Namespace<'n>,
) -> Self {
Self { writer, name, namespace }
}
pub fn element(
&mut self,
name: &'a str,
namespace: Namespace<'n>,
) -> Element<'_, 'n> {
self.element_with_attrs(name, namespace, iter::empty())
}
pub fn element_with_attrs<'b>(
&mut self,
name: &'a str,
namespace: Namespace<'n>,
attrs: impl IntoIterator<Item = (&'b str, &'b str)>,
) -> Element<'_, 'n> {
Element::with_attrs(self.writer, name, namespace, attrs)
}
}
impl Drop for Struct<'_, '_> {
fn drop(&mut self) {
write!(self.writer.buf, "</{}:{}>", self.namespace.prefix(), self.name).unwrap();
}
}
pub trait XmpType {
fn write(&self, buf: &mut String);
}
impl XmpType for bool {
fn write(&self, buf: &mut String) {
if *self {
buf.push_str("True");
} else {
buf.push_str("False");
}
}
}
impl XmpType for i32 {
fn write(&self, buf: &mut String) {
write!(buf, "{}", self).unwrap();
}
}
impl XmpType for i64 {
fn write(&self, buf: &mut String) {
write!(buf, "{}", self).unwrap();
}
}
impl XmpType for f32 {
fn write(&self, buf: &mut String) {
write!(buf, "{}", self).unwrap();
}
}
impl XmpType for f64 {
fn write(&self, buf: &mut String) {
write!(buf, "{}", self).unwrap();
}
}
impl XmpType for &str {
fn write(&self, buf: &mut String) {
for c in self.chars() {
match c {
'<' => buf.push_str("<"),
'>' => buf.push_str(">"),
'&' => buf.push_str("&"),
'\'' => buf.push_str("'"),
'"' => buf.push_str("""),
_ => buf.push(c),
}
}
}
}
pub enum RdfCollectionType {
Seq,
Bag,
Alt,
}
impl RdfCollectionType {
pub fn rdf_type(&self) -> &'static str {
match self {
RdfCollectionType::Seq => "Seq",
RdfCollectionType::Bag => "Bag",
RdfCollectionType::Alt => "Alt",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LangId<'a>(pub &'a str);
impl XmpType for LangId<'_> {
fn write(&self, buf: &mut String) {
buf.push_str(self.0);
}
}
impl Default for LangId<'_> {
fn default() -> Self {
Self("x-default")
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[allow(missing_docs)]
pub struct DateTime {
pub year: u16,
pub month: Option<u8>,
pub day: Option<u8>,
pub hour: Option<u8>,
pub minute: Option<u8>,
pub second: Option<u8>,
pub timezone: Option<Timezone>,
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Timezone {
Utc,
Local {
hour: i8,
minute: i8,
},
}
impl DateTime {
#[allow(clippy::too_many_arguments)]
pub fn new(
year: u16,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
timezone: Timezone,
) -> Self {
Self {
year,
month: Some(month),
day: Some(day),
hour: Some(hour),
minute: Some(minute),
second: Some(second),
timezone: Some(timezone),
}
}
pub fn year(year: u16) -> Self {
Self {
year,
month: None,
day: None,
hour: None,
minute: None,
second: None,
timezone: None,
}
}
pub fn date(year: u16, month: u8, day: u8) -> Self {
Self {
year,
month: Some(month),
day: Some(day),
hour: None,
minute: None,
second: None,
timezone: None,
}
}
pub fn local_time(
year: u16,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
) -> Self {
Self {
year,
month: Some(month),
day: Some(day),
hour: Some(hour),
minute: Some(minute),
second: Some(second),
timezone: None,
}
}
}
impl XmpType for DateTime {
fn write(&self, buf: &mut String) {
(|| {
write!(buf, "{:04}", self.year).unwrap();
write!(buf, "-{:02}", self.month?).unwrap();
write!(buf, "-{:02}", self.day?).unwrap();
write!(buf, "T{:02}:{:02}", self.hour?, self.minute?).unwrap();
write!(buf, ":{:02}", self.second?).unwrap();
match self.timezone? {
Timezone::Utc => buf.push('Z'),
Timezone::Local { hour, minute } => {
write!(buf, "{:+03}:{:02}", hour, minute).unwrap();
}
}
Some(())
})();
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RenditionClass<'a> {
Default,
Draft,
LowResolution,
Proof,
Screen,
Thumbnail {
format: Option<&'a str>,
size: Option<(u32, u32)>,
color_space: Option<&'a str>,
},
Custom(&'a str),
}
impl XmpType for RenditionClass<'_> {
fn write(&self, buf: &mut String) {
match self {
Self::Default => buf.push_str("default"),
Self::Draft => buf.push_str("draft"),
Self::LowResolution => buf.push_str("low-res"),
Self::Proof => buf.push_str("proof"),
Self::Screen => buf.push_str("screen"),
Self::Thumbnail { format, size, color_space } => {
buf.push_str("thumbnail");
if let Some(format) = format {
buf.push(':');
buf.push_str(format);
}
if let Some((width, height)) = size {
buf.push(':');
buf.push_str(&width.to_string());
buf.push('x');
buf.push_str(&height.to_string());
}
if let Some(color_space) = color_space {
buf.push(':');
buf.push_str(color_space);
}
}
Self::Custom(s) => buf.push_str(s),
}
}
}
pub enum Rating {
Rejected,
Unknown,
OneStar,
TwoStars,
ThreeStars,
FourStars,
FiveStars,
}
impl Rating {
pub fn from_stars(stars: Option<u32>) -> Self {
match stars {
Some(0) => Self::Unknown,
Some(1) => Self::OneStar,
Some(2) => Self::TwoStars,
Some(3) => Self::ThreeStars,
Some(4) => Self::FourStars,
Some(5) => Self::FiveStars,
Some(stars) => {
panic!("Invalid number of stars: {} (must be between 0 and 5)", stars)
}
None => Self::Unknown,
}
}
pub fn to_xmp(self) -> f32 {
match self {
Self::Rejected => -1.0,
Self::Unknown => 0.0,
Self::OneStar => 1.0,
Self::TwoStars => 2.0,
Self::ThreeStars => 3.0,
Self::FourStars => 4.0,
Self::FiveStars => 5.0,
}
}
}
pub enum MaskMarkers {
All,
None,
}
impl XmpType for MaskMarkers {
fn write(&self, buf: &mut String) {
match self {
Self::All => buf.push_str("All"),
Self::None => buf.push_str("None"),
}
}
}
#[allow(missing_docs)]
pub enum ResourceEventAction<'a> {
Converted,
Copied,
Created,
Cropped,
Edited,
Filtered,
Formatted,
VersionUpdated,
Printed,
Published,
Managed,
Produced,
Resized,
Saved,
Custom(&'a str),
}
impl XmpType for ResourceEventAction<'_> {
fn write(&self, buf: &mut String) {
match self {
Self::Converted => buf.push_str("converted"),
Self::Copied => buf.push_str("copied"),
Self::Created => buf.push_str("created"),
Self::Cropped => buf.push_str("cropped"),
Self::Edited => buf.push_str("edited"),
Self::Filtered => buf.push_str("filtered"),
Self::Formatted => buf.push_str("formatted"),
Self::VersionUpdated => buf.push_str("version_updated"),
Self::Printed => buf.push_str("printed"),
Self::Published => buf.push_str("published"),
Self::Managed => buf.push_str("managed"),
Self::Produced => buf.push_str("produced"),
Self::Resized => buf.push_str("resized"),
Self::Saved => buf.push_str("saved"),
Self::Custom(s) => buf.push_str(s),
}
}
}
#[allow(missing_docs)]
pub enum ColorantMode {
CMYK,
RGB,
Lab,
}
impl XmpType for ColorantMode {
fn write(&self, buf: &mut String) {
buf.push_str(match self {
Self::CMYK => "CMYK",
Self::RGB => "RGB",
Self::Lab => "Lab",
});
}
}
pub enum ColorantType {
Process,
Spot,
}
impl XmpType for ColorantType {
fn write(&self, buf: &mut String) {
buf.push_str(match self {
Self::Process => "PROCESS",
Self::Spot => "SPOT",
});
}
}
#[allow(missing_docs)]
pub enum DimensionUnit<'a> {
Inch,
Mm,
Pixel,
Pica,
Point,
Custom(&'a str),
}
impl<'a> XmpType for DimensionUnit<'a> {
fn write(&self, buf: &mut String) {
match self {
Self::Inch => buf.push_str("inch"),
Self::Mm => buf.push_str("mm"),
Self::Pixel => buf.push_str("pixel"),
Self::Pica => buf.push_str("pica"),
Self::Point => buf.push_str("point"),
Self::Custom(s) => buf.push_str(s),
}
}
}
#[allow(missing_docs)]
pub enum FontType<'a> {
TrueType,
OpenType,
Type1,
Bitmap,
Custom(&'a str),
}
impl<'a> XmpType for FontType<'a> {
fn write(&self, buf: &mut String) {
match self {
Self::TrueType => buf.push_str("TrueType"),
Self::OpenType => buf.push_str("OpenType"),
Self::Type1 => buf.push_str("Type1"),
Self::Bitmap => buf.push_str("Bitmap"),
Self::Custom(s) => buf.push_str(s),
}
}
}