pdfrum_common/version.rs
1//! The PDF version a file declares in its header, as a pair of digits.
2//!
3//! `%PDF-1.7` is `PdfVersion { major: 1, minor: 7 }`. The parser reads the
4//! header, the writer emits one, and the facade forwards both — so the type
5//! lives here, at the bottom of the dependency graph, where all three can name
6//! it without any of them depending on either of the others.
7
8use core::fmt;
9
10/// The version digits from a `%PDF-M.N` header.
11///
12/// Two independent digits, not a packed integer. The packed form — `17` for
13/// 1.7 — survives only as a private conversion beside the header parser
14/// (`pdfrum-parser`'s `doc::read_version`), and appears in no public
15/// signature anywhere in the workspace.
16///
17/// Ordering is lexicographic on `(major, minor)`, so `PDF_1_4 < PDF_1_7 <
18/// PDF_2_0` — which is what "at least version X" means, and what a packed
19/// comparison gets wrong the moment a major digit reaches two of its own
20/// (`PdfVersion::new(11, 0)` orders above 1.9; `110 > 19` only by luck of the
21/// digit count).
22///
23/// ```
24/// use pdfrum_common::PdfVersion;
25///
26/// assert_eq!(PdfVersion::PDF_1_7.to_string(), "1.7");
27/// assert!(PdfVersion::PDF_1_4 < PdfVersion::PDF_2_0);
28/// assert_eq!(PdfVersion::new(1, 5), PdfVersion::PDF_1_5);
29/// ```
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub struct PdfVersion {
32 /// The digit before the dot.
33 pub major: u8,
34 /// The digit after it.
35 pub minor: u8,
36}
37
38impl PdfVersion {
39 /// PDF 1.0, the oldest version the writer will emit on request.
40 pub const PDF_1_0: Self = Self { major: 1, minor: 0 };
41 /// PDF 1.4 — the last version before object and cross-reference streams.
42 pub const PDF_1_4: Self = Self { major: 1, minor: 4 };
43 /// PDF 1.5, which introduced cross-reference and object streams.
44 pub const PDF_1_5: Self = Self { major: 1, minor: 5 };
45 /// PDF 1.7, ISO 32000-1, and the writer's fallback for a document that
46 /// declares no version of its own.
47 pub const PDF_1_7: Self = Self { major: 1, minor: 7 };
48 /// PDF 2.0, ISO 32000-2.
49 pub const PDF_2_0: Self = Self { major: 2, minor: 0 };
50
51 /// A version from its two digits.
52 #[must_use]
53 pub const fn new(major: u8, minor: u8) -> Self {
54 Self { major, minor }
55 }
56}
57
58impl fmt::Display for PdfVersion {
59 /// `1.7`, the way the header spells it.
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 write!(f, "{}.{}", self.major, self.minor)
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::PdfVersion;
68
69 #[test]
70 fn display_spells_the_header() {
71 assert_eq!(PdfVersion::PDF_1_0.to_string(), "1.0");
72 assert_eq!(PdfVersion::PDF_1_4.to_string(), "1.4");
73 assert_eq!(PdfVersion::PDF_1_7.to_string(), "1.7");
74 assert_eq!(PdfVersion::PDF_2_0.to_string(), "2.0");
75 }
76
77 // Ordering is on the pair, so a major bump orders above every minor of the
78 // version below it. This is the property a packed `u8` carries only while
79 // the major digit stays single, and the reason `Ord` is derived on the
80 // struct rather than delegated to the packed form.
81 #[test]
82 fn ordering_is_major_then_minor() {
83 assert!(PdfVersion::PDF_1_0 < PdfVersion::PDF_1_4);
84 assert!(PdfVersion::PDF_1_4 < PdfVersion::PDF_1_5);
85 assert!(PdfVersion::PDF_1_5 < PdfVersion::PDF_1_7);
86 assert!(PdfVersion::PDF_1_7 < PdfVersion::PDF_2_0);
87 assert!(PdfVersion::new(1, 9) < PdfVersion::new(2, 0));
88 assert!(PdfVersion::new(1, 9) < PdfVersion::new(11, 0));
89 }
90
91 #[test]
92 fn the_named_constants_are_their_digits() {
93 assert_eq!(PdfVersion::PDF_1_0, PdfVersion::new(1, 0));
94 assert_eq!(PdfVersion::PDF_1_4, PdfVersion::new(1, 4));
95 assert_eq!(PdfVersion::PDF_1_5, PdfVersion::new(1, 5));
96 assert_eq!(PdfVersion::PDF_1_7, PdfVersion::new(1, 7));
97 assert_eq!(PdfVersion::PDF_2_0, PdfVersion::new(2, 0));
98 }
99
100 // Every field is public and `Copy`, so struct-update construction works
101 // the way asks configuration to (this is not a config struct,
102 // but the same freedom applies and a reader will try it).
103 #[test]
104 fn a_minor_can_be_bumped_by_struct_update() {
105 let v = PdfVersion {
106 minor: 5,
107 ..PdfVersion::PDF_1_7
108 };
109 assert_eq!(v, PdfVersion::PDF_1_5);
110 }
111}