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