pdfboss_core/
linearization.rs1use crate::object::Dict;
6use crate::parser::{NoResolve, Parser};
7
8pub const LINEARIZATION_WINDOW: usize = 1024;
12
13#[derive(Debug, Clone, PartialEq)]
18pub struct Linearization {
19 pub version: f64,
21 pub file_length: u64,
25 pub hint_streams: Vec<(u64, u64)>,
28 pub first_page_object: u32,
30 pub first_page_end: u64,
32 pub page_count: u32,
34 pub main_xref_offset: u64,
37 pub first_page: u32,
39}
40
41impl Linearization {
42 pub fn is_current(&self, file_length: u64) -> bool {
48 self.file_length == file_length
49 }
50}
51
52pub fn linearization_dictionary(head: &[u8]) -> Option<Linearization> {
61 let window = &head[..head.len().min(LINEARIZATION_WINDOW)];
62 let (_, object) = Parser::new(window).parse_indirect(&NoResolve).ok()?;
63 let dict = object.as_dict()?;
64 let version = dict.get("Linearized")?.as_f64()?;
65 let offset = |key: &str| u64::try_from(dict.get_int(key)?).ok();
66 let count = |key: &str| u32::try_from(dict.get_int(key)?).ok();
67 Some(Linearization {
68 version,
69 file_length: offset("L")?,
70 hint_streams: hint_streams(dict)?,
71 first_page_object: count("O")?,
72 first_page_end: offset("E")?,
73 page_count: count("N")?,
74 main_xref_offset: offset("T")?,
75 first_page: match dict.get_int("P") {
76 Some(page) => u32::try_from(page).ok()?,
77 None => 0,
78 },
79 })
80}
81
82fn hint_streams(dict: &Dict) -> Option<Vec<(u64, u64)>> {
85 let values: Vec<u64> = dict
86 .get_array("H")?
87 .iter()
88 .map(|value| u64::try_from(value.as_int()?).ok())
89 .collect::<Option<_>>()?;
90 let (pairs, rest) = values.as_chunks::<2>();
91 if pairs.is_empty() || !rest.is_empty() {
92 return None;
93 }
94 Some(
95 pairs
96 .iter()
97 .map(|&[offset, length]| (offset, length))
98 .collect(),
99 )
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::document::Document;
106 use pdfboss_testkit::PdfBuilder;
107
108 const HEAD: &[u8] = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n43 0 obj\n<< /Linearized 1 /L 12345 /H [ 500 200 ] /O 45 /E 3000 /N 3 /T 11000 /P 2 >>\nendobj\n44 0 obj\n<< /Type /Catalog >>\nendobj\n";
109
110 fn linearized_file(length: &str) -> Vec<u8> {
113 let mut b = PdfBuilder::new();
114 b.object(
115 43,
116 &format!("<< /Linearized 1 /L {length} /H [ 0 0 ] /O 46 /E 0 /N 1 /T 0 >>"),
117 );
118 b.object(44, "<< /Type /Catalog /Pages 45 0 R >>");
119 b.object(45, "<< /Type /Pages /Kids [46 0 R] /Count 1 >>");
120 b.object(46, "<< /Type /Page /Parent 45 0 R /MediaBox [0 0 10 10] >>");
121 b.build(44)
122 }
123
124 fn current_linearized_file() -> Vec<u8> {
127 let mut data = linearized_file("0000000000");
128 let length = format!("{:010}", data.len());
129 let at = data.windows(10).position(|w| w == b"0000000000").unwrap();
130 data[at..at + 10].copy_from_slice(length.as_bytes());
131 data
132 }
133
134 #[test]
136 fn reads_every_entry_of_the_linearization_parameter_dictionary() {
137 let record = linearization_dictionary(HEAD).unwrap();
138 assert_eq!(
139 record,
140 Linearization {
141 version: 1.0,
142 file_length: 12345,
143 hint_streams: vec![(500, 200)],
144 first_page_object: 45,
145 first_page_end: 3000,
146 page_count: 3,
147 main_xref_offset: 11000,
148 first_page: 2,
149 }
150 );
151 }
152
153 #[test]
155 fn the_first_page_number_defaults_to_zero_and_an_overflow_hint_stream_is_read() {
156 let head = b"%PDF-1.5\n7 0 obj\n<< /Linearized 1.0 /L 99 /H [ 500 200 8000 100 ] /O 9 /E 30 /N 1 /T 90 >>\nendobj\n";
157 let record = linearization_dictionary(head).unwrap();
158 assert_eq!(record.hint_streams, vec![(500, 200), (8000, 100)]);
159 assert_eq!(record.first_page, 0);
160 assert_eq!(record.version, 1.0);
161 }
162
163 #[test]
167 fn a_file_without_the_dictionary_as_its_first_object_is_not_linearized() {
168 assert_eq!(linearization_dictionary(b""), None);
169 assert_eq!(
170 linearization_dictionary(b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\n"),
171 None
172 );
173 let missing_page_count =
174 b"%PDF-1.4\n43 0 obj\n<< /Linearized 1 /L 12345 /H [ 500 200 ] /O 45 /E 3000 /T 11000 >>\nendobj\n";
175 assert_eq!(linearization_dictionary(missing_page_count), None);
176 let odd_hint_array =
177 b"%PDF-1.4\n43 0 obj\n<< /Linearized 1 /L 12345 /H [ 500 ] /O 45 /E 3000 /N 3 /T 11000 >>\nendobj\n";
178 assert_eq!(linearization_dictionary(odd_hint_array), None);
179 let mut late = b"%PDF-1.4\n%".to_vec();
180 late.extend(std::iter::repeat_n(b'x', 1024));
181 late.extend_from_slice(&HEAD[9..]);
182 assert_eq!(linearization_dictionary(&late), None);
183 }
184
185 #[test]
190 fn a_document_is_linearized_only_while_the_declared_length_matches() {
191 let doc = Document::load(current_linearized_file()).unwrap();
192 let record = doc.linearization().unwrap();
193 assert_eq!(record.first_page_object, 46);
194 assert_eq!(record.page_count, 1);
195 assert!(doc.is_linearized());
196
197 let mut updated = current_linearized_file();
198 updated.extend_from_slice(b"%appended update\n");
199 let doc = Document::load(updated).unwrap();
200 assert!(doc.linearization().is_some());
201 assert!(!doc.is_linearized());
202
203 let doc = Document::load(pdfboss_testkit::simple_doc("plain")).unwrap();
204 assert_eq!(doc.linearization(), None);
205 assert!(!doc.is_linearized());
206 }
207}