1use std::{
2 fmt::{Debug, Write},
3 iter,
4};
5
6use crate::XmpWriter;
7
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
10#[allow(missing_docs)]
11#[non_exhaustive]
12pub enum Namespace<'a> {
13 Rdf,
14 DublinCore,
15 Xmp,
16 XmpRights,
17 XmpResourceRef,
18 XmpResourceEvent,
19 XmpVersion,
20 XmpJob,
21 XmpJobManagement,
22 XmpColorant,
23 XmpFont,
24 XmpDimensions,
25 XmpMedia,
26 XmpPaged,
27 XmpDynamicMedia,
28 XmpImage,
29 XmpIdq,
30 AdobePdf,
31 #[cfg(feature = "pdfa")]
32 PdfAId,
33 PdfUAId,
34 PdfXId,
35 #[cfg(feature = "pdfa")]
36 PdfAExtension,
37 #[cfg(feature = "pdfa")]
38 PdfASchema,
39 #[cfg(feature = "pdfa")]
40 PdfAProperty,
41 #[cfg(feature = "pdfa")]
42 PdfAType,
43 #[cfg(feature = "pdfa")]
44 PdfAField,
45 Custom(Box<CustomNamespace<'a>>),
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
50pub struct CustomNamespace<'a> {
51 name: &'a str,
52 namespace: &'a str,
53 url: &'a str,
54}
55
56impl<'a> CustomNamespace<'a> {
57 pub fn new(name: &'a str, namespace: &'a str, url: &'a str) -> Self {
59 Self { name, namespace, url }
60 }
61}
62
63impl<'a> Namespace<'a> {
64 pub const fn name(&self) -> &'a str {
66 match self {
67 Self::Rdf => "RDF",
68 Self::DublinCore => "Dublin Core",
69 Self::Xmp => "XMP",
70 Self::XmpRights => "XMP Rights",
71 Self::XmpResourceRef => "XMP Resource Reference",
72 Self::XmpResourceEvent => "XMP Resource Event",
73 Self::XmpVersion => "XMP Version",
74 Self::XmpJob => "XMP Job Management",
75 Self::XmpColorant => "XMP Colorant",
76 Self::XmpFont => "XMP Font",
77 Self::XmpDimensions => "XMP Dimensions",
78 Self::XmpMedia => "XMP Media Management",
79 Self::XmpJobManagement => "XMP Job Management",
80 Self::XmpPaged => "XMP Paged Text",
81 Self::XmpDynamicMedia => "XMP Dynamic Media",
82 Self::XmpImage => "XMP Image",
83 Self::AdobePdf => "Adobe PDF",
84 Self::XmpIdq => "XMP Identifier Qualifier",
85 #[cfg(feature = "pdfa")]
86 Self::PdfAId => "PDF/A Identification",
87 Self::PdfUAId => "PDF/UA Identification",
88 Self::PdfXId => "PDF/X Identification",
89 #[cfg(feature = "pdfa")]
90 Self::PdfAExtension => "PDF/A Extension schema container",
91 #[cfg(feature = "pdfa")]
92 Self::PdfASchema => "PDF/A Schema container",
93 #[cfg(feature = "pdfa")]
94 Self::PdfAProperty => "PDF/A Property",
95 #[cfg(feature = "pdfa")]
96 Self::PdfAType => "PDF/A Type",
97 #[cfg(feature = "pdfa")]
98 Self::PdfAField => "PDF/A Field",
99 Self::Custom(custom) => custom.name,
100 }
101 }
102
103 pub fn url(&self) -> &'a str {
105 match self {
106 Self::Rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
107 Self::DublinCore => "http://purl.org/dc/elements/1.1/",
108 Self::Xmp => "http://ns.adobe.com/xap/1.0/",
109 Self::XmpRights => "http://ns.adobe.com/xap/1.0/rights/",
110 Self::XmpResourceRef => "http://ns.adobe.com/xap/1.0/sType/ResourceRef#",
111 Self::XmpResourceEvent => "http://ns.adobe.com/xap/1.0/sType/ResourceEvent#",
112 Self::XmpVersion => "http://ns.adobe.com/xap/1.0/sType/Version#",
113 Self::XmpJob => "http://ns.adobe.com/xap/1.0/sType/Job#",
114 Self::XmpColorant => "http://ns.adobe.com/xap/1.0/g/",
115 Self::XmpFont => "http://ns.adobe.com/xap/1.0/sType/Font#",
116 Self::XmpDimensions => "http://ns.adobe.com/xap/1.0/sType/Dimensions#",
117 Self::XmpMedia => "http://ns.adobe.com/xap/1.0/mm/",
118 Self::XmpJobManagement => "http://ns.adobe.com/xap/1.0/bj/",
119 Self::XmpPaged => "http://ns.adobe.com/xap/1.0/t/pg/",
120 Self::XmpDynamicMedia => "http://ns.adobe.com/xap/1.0/DynamicMedia/",
121 Self::XmpImage => "http://ns.adobe.com/xap/1.0/g/img/",
122 Self::AdobePdf => "http://ns.adobe.com/pdf/1.3/",
123 Self::XmpIdq => "http://ns.adobe.com/xmp/Identifier/qual/1.0/",
124 #[cfg(feature = "pdfa")]
125 Self::PdfAId => "http://www.aiim.org/pdfa/ns/id/",
126 Self::PdfUAId => "http://www.aiim.org/pdfua/ns/id/",
127 Self::PdfXId => "http://www.npes.org/pdfx/ns/id/",
128 #[cfg(feature = "pdfa")]
129 Self::PdfAExtension => "http://www.aiim.org/pdfa/ns/extension/",
130 #[cfg(feature = "pdfa")]
131 Self::PdfASchema => "http://www.aiim.org/pdfa/ns/schema#",
132 #[cfg(feature = "pdfa")]
133 Self::PdfAProperty => "http://www.aiim.org/pdfa/ns/property#",
134 #[cfg(feature = "pdfa")]
135 Self::PdfAType => "http://www.aiim.org/pdfa/ns/type#",
136 #[cfg(feature = "pdfa")]
137 Self::PdfAField => "http://www.aiim.org/pdfa/ns/field#",
138 Self::Custom(custom) => custom.url,
139 }
140 }
141
142 pub fn prefix(&self) -> &'a str {
144 match self {
145 Self::Rdf => "rdf",
146 Self::DublinCore => "dc",
147 Self::Xmp => "xmp",
148 Self::XmpRights => "xmpRights",
149 Self::XmpResourceRef => "stRef",
150 Self::XmpResourceEvent => "stEvt",
151 Self::XmpVersion => "stVer",
152 Self::XmpJob => "stJob",
153 Self::XmpColorant => "xmpG",
154 Self::XmpFont => "stFnt",
155 Self::XmpDimensions => "stDim",
156 Self::XmpMedia => "xmpMM",
157 Self::XmpJobManagement => "xmpBJ",
158 Self::XmpPaged => "xmpTPg",
159 Self::XmpDynamicMedia => "xmpDM",
160 Self::XmpImage => "xmpGImg",
161 Self::AdobePdf => "pdf",
162 Self::XmpIdq => "xmpidq",
163 #[cfg(feature = "pdfa")]
164 Self::PdfAId => "pdfaid",
165 Self::PdfUAId => "pdfuaid",
166 Self::PdfXId => "pdfxid",
167 #[cfg(feature = "pdfa")]
168 Self::PdfAExtension => "pdfaExtension",
169 #[cfg(feature = "pdfa")]
170 Self::PdfASchema => "pdfaSchema",
171 #[cfg(feature = "pdfa")]
172 Self::PdfAProperty => "pdfaProperty",
173 #[cfg(feature = "pdfa")]
174 Self::PdfAType => "pdfaType",
175 #[cfg(feature = "pdfa")]
176 Self::PdfAField => "pdfaField",
177 Self::Custom(custom) => custom.namespace,
178 }
179 }
180}
181
182pub struct Element<'a, 'n: 'a> {
188 writer: &'a mut XmpWriter<'n>,
189 name: &'a str,
190 namespace: Namespace<'n>,
191}
192
193impl<'a, 'n: 'a> Element<'a, 'n> {
194 pub(crate) fn start(
195 writer: &'a mut XmpWriter<'n>,
196 name: &'a str,
197 namespace: Namespace<'n>,
198 ) -> Self {
199 Self::with_attrs(writer, name, namespace, iter::empty())
200 }
201
202 fn with_attrs<'b>(
203 writer: &'a mut XmpWriter<'n>,
204 name: &'a str,
205 namespace: Namespace<'n>,
206 attrs: impl IntoIterator<Item = (&'b str, &'b str)>,
207 ) -> Self {
208 write!(writer.buf, "<{}:{}", namespace.prefix(), name).unwrap();
209
210 for (key, value) in attrs {
211 write!(writer.buf, " {}=\"{}\"", key, value).unwrap();
212 }
213
214 writer.namespaces.insert(namespace.clone());
215 Element { writer, name, namespace }
216 }
217
218 pub fn value(self, val: impl XmpType) {
220 self.writer.buf.push('>');
221 val.write(&mut self.writer.buf);
222 self.close();
223 }
224
225 pub fn obj(self) -> Struct<'a, 'n> {
227 self.writer.namespaces.insert(Namespace::Rdf);
228 self.writer.buf.push_str(" rdf:parseType=\"Resource\">");
229 Struct::start(self.writer, self.name, self.namespace)
230 }
231
232 pub fn array(self, kind: RdfCollectionType) -> Array<'a, 'n> {
234 self.writer.buf.push('>');
235 Array::start(self.writer, kind, self.name, self.namespace)
236 }
237
238 fn close(self) {
239 write!(self.writer.buf, "</{}:{}>", self.namespace.prefix(), self.name).unwrap();
240 }
241
242 pub fn language_alternative<'b>(
244 self,
245 items: impl IntoIterator<Item = (Option<LangId<'b>>, &'b str)>,
246 ) {
247 let mut array = self.array(RdfCollectionType::Alt);
248 for (lang, value) in items {
249 array
250 .element_with_attrs(iter::once(("xml:lang", lang.unwrap_or_default().0)))
251 .value(value);
252 }
253 drop(array);
254 }
255
256 pub fn unordered_array(self, items: impl IntoIterator<Item = impl XmpType>) {
258 let mut array = self.array(RdfCollectionType::Bag);
259 for item in items {
260 array.element().value(item);
261 }
262 }
263
264 pub fn ordered_array(self, items: impl IntoIterator<Item = impl XmpType>) {
266 let mut array = self.array(RdfCollectionType::Seq);
267 for item in items {
268 array.element().value(item);
269 }
270 }
271
272 pub fn alternative_array(self, items: impl IntoIterator<Item = impl XmpType>) {
274 let mut array = self.array(RdfCollectionType::Alt);
275 for item in items {
276 array.element().value(item);
277 }
278 }
279}
280
281pub struct Array<'a, 'n: 'a> {
285 writer: &'a mut XmpWriter<'n>,
286 kind: RdfCollectionType,
287 name: &'a str,
288 namespace: Namespace<'a>,
289}
290
291impl<'a, 'n: 'a> Array<'a, 'n> {
292 fn start(
293 writer: &'a mut XmpWriter<'n>,
294 kind: RdfCollectionType,
295 name: &'a str,
296 namespace: Namespace<'n>,
297 ) -> Self {
298 writer.namespaces.insert(Namespace::Rdf);
299 write!(writer.buf, "<rdf:{}>", kind.rdf_type()).unwrap();
300 Self { writer, kind, name, namespace }
301 }
302
303 pub fn element(&mut self) -> Element<'_, 'n> {
305 self.element_with_attrs(iter::empty())
306 }
307
308 pub fn element_with_attrs(
310 &mut self,
311 attrs: impl IntoIterator<Item = (&'a str, &'a str)>,
312 ) -> Element<'_, 'n> {
313 Element::with_attrs(self.writer, "li", Namespace::Rdf, attrs)
314 }
315}
316
317impl Drop for Array<'_, '_> {
318 fn drop(&mut self) {
319 write!(
320 self.writer.buf,
321 "</rdf:{}></{}:{}>",
322 self.kind.rdf_type(),
323 self.namespace.prefix(),
324 self.name
325 )
326 .unwrap();
327 }
328}
329
330pub struct Struct<'a, 'n: 'a> {
334 writer: &'a mut XmpWriter<'n>,
335 name: &'a str,
336 namespace: Namespace<'a>,
337}
338
339impl<'a, 'n: 'a> Struct<'a, 'n> {
340 fn start(
341 writer: &'a mut XmpWriter<'n>,
342 name: &'a str,
343 namespace: Namespace<'n>,
344 ) -> Self {
345 Self { writer, name, namespace }
346 }
347
348 pub fn element(
350 &mut self,
351 name: &'a str,
352 namespace: Namespace<'n>,
353 ) -> Element<'_, 'n> {
354 self.element_with_attrs(name, namespace, iter::empty())
355 }
356
357 pub fn element_with_attrs<'b>(
359 &mut self,
360 name: &'a str,
361 namespace: Namespace<'n>,
362 attrs: impl IntoIterator<Item = (&'b str, &'b str)>,
363 ) -> Element<'_, 'n> {
364 Element::with_attrs(self.writer, name, namespace, attrs)
365 }
366}
367
368impl Drop for Struct<'_, '_> {
369 fn drop(&mut self) {
370 write!(self.writer.buf, "</{}:{}>", self.namespace.prefix(), self.name).unwrap();
371 }
372}
373
374pub trait XmpType {
376 fn write(&self, buf: &mut String);
378}
379
380impl XmpType for bool {
381 fn write(&self, buf: &mut String) {
382 if *self {
383 buf.push_str("True");
384 } else {
385 buf.push_str("False");
386 }
387 }
388}
389
390impl XmpType for i32 {
391 fn write(&self, buf: &mut String) {
392 write!(buf, "{}", self).unwrap();
393 }
394}
395
396impl XmpType for i64 {
397 fn write(&self, buf: &mut String) {
398 write!(buf, "{}", self).unwrap();
399 }
400}
401
402impl XmpType for f32 {
403 fn write(&self, buf: &mut String) {
404 write!(buf, "{}", self).unwrap();
405 }
406}
407
408impl XmpType for f64 {
409 fn write(&self, buf: &mut String) {
410 write!(buf, "{}", self).unwrap();
411 }
412}
413
414impl XmpType for &str {
415 fn write(&self, buf: &mut String) {
416 for c in self.chars() {
417 match c {
418 '<' => buf.push_str("<"),
419 '>' => buf.push_str(">"),
420 '&' => buf.push_str("&"),
421 '\'' => buf.push_str("'"),
422 '"' => buf.push_str("""),
423 _ => buf.push(c),
424 }
425 }
426 }
427}
428
429pub enum RdfCollectionType {
431 Seq,
433 Bag,
435 Alt,
437}
438
439impl RdfCollectionType {
440 pub fn rdf_type(&self) -> &'static str {
442 match self {
443 RdfCollectionType::Seq => "Seq",
444 RdfCollectionType::Bag => "Bag",
445 RdfCollectionType::Alt => "Alt",
446 }
447 }
448}
449
450#[derive(Debug, Clone, PartialEq)]
453pub struct LangId<'a>(pub &'a str);
454
455impl XmpType for LangId<'_> {
456 fn write(&self, buf: &mut String) {
457 buf.push_str(self.0);
458 }
459}
460
461impl Default for LangId<'_> {
462 fn default() -> Self {
463 Self("x-default")
464 }
465}
466
467#[derive(Debug, Copy, Clone, PartialEq)]
469#[allow(missing_docs)]
470pub struct DateTime {
471 pub year: u16,
472 pub month: Option<u8>,
473 pub day: Option<u8>,
474 pub hour: Option<u8>,
475 pub minute: Option<u8>,
476 pub second: Option<u8>,
477 pub timezone: Option<Timezone>,
480}
481
482#[derive(Debug, Copy, Clone, PartialEq)]
484pub enum Timezone {
485 Utc,
487 Local {
489 hour: i8,
491 minute: i8,
493 },
494}
495
496impl DateTime {
497 #[allow(clippy::too_many_arguments)]
499 pub fn new(
500 year: u16,
501 month: u8,
502 day: u8,
503 hour: u8,
504 minute: u8,
505 second: u8,
506 timezone: Timezone,
507 ) -> Self {
508 Self {
509 year,
510 month: Some(month),
511 day: Some(day),
512 hour: Some(hour),
513 minute: Some(minute),
514 second: Some(second),
515 timezone: Some(timezone),
516 }
517 }
518
519 pub fn year(year: u16) -> Self {
521 Self {
522 year,
523 month: None,
524 day: None,
525 hour: None,
526 minute: None,
527 second: None,
528 timezone: None,
529 }
530 }
531
532 pub fn date(year: u16, month: u8, day: u8) -> Self {
534 Self {
535 year,
536 month: Some(month),
537 day: Some(day),
538 hour: None,
539 minute: None,
540 second: None,
541 timezone: None,
542 }
543 }
544
545 pub fn local_time(
547 year: u16,
548 month: u8,
549 day: u8,
550 hour: u8,
551 minute: u8,
552 second: u8,
553 ) -> Self {
554 Self {
555 year,
556 month: Some(month),
557 day: Some(day),
558 hour: Some(hour),
559 minute: Some(minute),
560 second: Some(second),
561 timezone: None,
562 }
563 }
564}
565
566impl XmpType for DateTime {
567 fn write(&self, buf: &mut String) {
568 (|| {
569 write!(buf, "{:04}", self.year).unwrap();
570 write!(buf, "-{:02}", self.month?).unwrap();
571 write!(buf, "-{:02}", self.day?).unwrap();
572 write!(buf, "T{:02}:{:02}", self.hour?, self.minute?).unwrap();
573 write!(buf, ":{:02}", self.second?).unwrap();
574 match self.timezone? {
575 Timezone::Utc => buf.push('Z'),
576 Timezone::Local { hour, minute } => {
577 write!(buf, "{:+03}:{:02}", hour, minute).unwrap();
578 }
579 }
580 Some(())
581 })();
582 }
583}
584
585#[derive(Debug, Clone, PartialEq)]
587pub enum RenditionClass<'a> {
588 Default,
590 Draft,
592 LowResolution,
594 Proof,
596 Screen,
598 Thumbnail {
600 format: Option<&'a str>,
602 size: Option<(u32, u32)>,
604 color_space: Option<&'a str>,
606 },
607 Custom(&'a str),
609}
610
611impl XmpType for RenditionClass<'_> {
612 fn write(&self, buf: &mut String) {
613 match self {
614 Self::Default => buf.push_str("default"),
615 Self::Draft => buf.push_str("draft"),
616 Self::LowResolution => buf.push_str("low-res"),
617 Self::Proof => buf.push_str("proof"),
618 Self::Screen => buf.push_str("screen"),
619 Self::Thumbnail { format, size, color_space } => {
620 buf.push_str("thumbnail");
621 if let Some(format) = format {
622 buf.push(':');
623 buf.push_str(format);
624 }
625 if let Some((width, height)) = size {
626 buf.push(':');
627 buf.push_str(&width.to_string());
628 buf.push('x');
629 buf.push_str(&height.to_string());
630 }
631 if let Some(color_space) = color_space {
632 buf.push(':');
633 buf.push_str(color_space);
634 }
635 }
636 Self::Custom(s) => buf.push_str(s),
637 }
638 }
639}
640
641pub enum Rating {
643 Rejected,
645 Unknown,
647 OneStar,
649 TwoStars,
651 ThreeStars,
653 FourStars,
655 FiveStars,
657}
658
659impl Rating {
660 pub fn from_stars(stars: Option<u32>) -> Self {
662 match stars {
663 Some(0) => Self::Unknown,
664 Some(1) => Self::OneStar,
665 Some(2) => Self::TwoStars,
666 Some(3) => Self::ThreeStars,
667 Some(4) => Self::FourStars,
668 Some(5) => Self::FiveStars,
669 Some(stars) => {
670 panic!("Invalid number of stars: {} (must be between 0 and 5)", stars)
671 }
672 None => Self::Unknown,
673 }
674 }
675
676 pub fn to_xmp(self) -> f32 {
678 match self {
679 Self::Rejected => -1.0,
680 Self::Unknown => 0.0,
681 Self::OneStar => 1.0,
682 Self::TwoStars => 2.0,
683 Self::ThreeStars => 3.0,
684 Self::FourStars => 4.0,
685 Self::FiveStars => 5.0,
686 }
687 }
688}
689
690pub enum MaskMarkers {
692 All,
694 None,
696}
697
698impl XmpType for MaskMarkers {
699 fn write(&self, buf: &mut String) {
700 match self {
701 Self::All => buf.push_str("All"),
702 Self::None => buf.push_str("None"),
703 }
704 }
705}
706
707#[allow(missing_docs)]
709pub enum ResourceEventAction<'a> {
710 Converted,
711 Copied,
712 Created,
713 Cropped,
714 Edited,
715 Filtered,
716 Formatted,
717 VersionUpdated,
718 Printed,
719 Published,
720 Managed,
721 Produced,
722 Resized,
723 Saved,
724 Custom(&'a str),
725}
726
727impl XmpType for ResourceEventAction<'_> {
728 fn write(&self, buf: &mut String) {
729 match self {
730 Self::Converted => buf.push_str("converted"),
731 Self::Copied => buf.push_str("copied"),
732 Self::Created => buf.push_str("created"),
733 Self::Cropped => buf.push_str("cropped"),
734 Self::Edited => buf.push_str("edited"),
735 Self::Filtered => buf.push_str("filtered"),
736 Self::Formatted => buf.push_str("formatted"),
737 Self::VersionUpdated => buf.push_str("version_updated"),
738 Self::Printed => buf.push_str("printed"),
739 Self::Published => buf.push_str("published"),
740 Self::Managed => buf.push_str("managed"),
741 Self::Produced => buf.push_str("produced"),
742 Self::Resized => buf.push_str("resized"),
743 Self::Saved => buf.push_str("saved"),
744 Self::Custom(s) => buf.push_str(s),
745 }
746 }
747}
748
749#[allow(missing_docs)]
751pub enum ColorantMode {
752 CMYK,
753 RGB,
754 Lab,
755}
756
757impl XmpType for ColorantMode {
758 fn write(&self, buf: &mut String) {
759 buf.push_str(match self {
760 Self::CMYK => "CMYK",
761 Self::RGB => "RGB",
762 Self::Lab => "Lab",
763 });
764 }
765}
766
767pub enum ColorantType {
769 Process,
771 Spot,
773}
774
775impl XmpType for ColorantType {
776 fn write(&self, buf: &mut String) {
777 buf.push_str(match self {
778 Self::Process => "PROCESS",
779 Self::Spot => "SPOT",
780 });
781 }
782}
783
784#[allow(missing_docs)]
786pub enum DimensionUnit<'a> {
787 Inch,
788 Mm,
789 Pixel,
790 Pica,
791 Point,
792 Custom(&'a str),
793}
794
795impl XmpType for DimensionUnit<'_> {
796 fn write(&self, buf: &mut String) {
797 match self {
798 Self::Inch => buf.push_str("inch"),
799 Self::Mm => buf.push_str("mm"),
800 Self::Pixel => buf.push_str("pixel"),
801 Self::Pica => buf.push_str("pica"),
802 Self::Point => buf.push_str("point"),
803 Self::Custom(s) => buf.push_str(s),
804 }
805 }
806}
807
808#[allow(missing_docs)]
810pub enum FontType<'a> {
811 TrueType,
812 OpenType,
813 Type1,
814 Bitmap,
815 Custom(&'a str),
816}
817
818impl XmpType for FontType<'_> {
819 fn write(&self, buf: &mut String) {
820 match self {
821 Self::TrueType => buf.push_str("TrueType"),
822 Self::OpenType => buf.push_str("OpenType"),
823 Self::Type1 => buf.push_str("Type1"),
824 Self::Bitmap => buf.push_str("Bitmap"),
825 Self::Custom(s) => buf.push_str(s),
826 }
827 }
828}