Skip to main content

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}