1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! The linearization parameter dictionary (ISO 32000-1 Annex F.3): the
//! first object of a linearized file, read as data. Nothing here uses the
//! hint streams or the first-page section to load pages early.
use crate::object::Dict;
use crate::parser::{NoResolve, Parser};
/// The leading bytes a linearization parameter dictionary must lie within
/// (ISO 32000-1 Annex F.3.3), so a reader decides whether a file is
/// linearized from one small read.
pub const LINEARIZATION_WINDOW: usize = 1024;
/// The linearization parameter dictionary (ISO 32000-1 Annex F.3, Table
/// F.1): the first object of a linearized file, whose entries locate the
/// first page and the hint streams so a reader can show the first page
/// before the whole file has arrived.
#[derive(Debug, Clone, PartialEq)]
pub struct Linearization {
/// `/Linearized`: the version of the linearized format.
pub version: f64,
/// `/L`: the length of the file the dictionary was written for. When
/// it differs from the actual length the file is ordinary PDF; see
/// [`Linearization::is_current`].
pub file_length: u64,
/// `/H`: the offset and length of the primary hint stream and, when
/// present, of the overflow hint stream, as written.
pub hint_streams: Vec<(u64, u64)>,
/// `/O`: the object number of the first page's page object.
pub first_page_object: u32,
/// `/E`: the offset of the end of the first page.
pub first_page_end: u64,
/// `/N`: the number of pages in the document.
pub page_count: u32,
/// `/T`: the offset of the main cross-reference table's first entry
/// (of the main cross-reference stream in a file that uses streams).
pub main_xref_offset: u64,
/// `/P`: the page number of the first page, 0 when absent.
pub first_page: u32,
}
impl Linearization {
/// Whether `/L` equals the file's actual `file_length`. Table F.1 makes
/// a mismatch mean the file is not linearized and shall be read as
/// ordinary PDF, the usual cause being an appended update.
///
/// Covers ISO 32000-1 Annex F.3.
pub fn is_current(&self, file_length: u64) -> bool {
self.file_length == file_length
}
}
/// Reads the linearization parameter dictionary from `head`, the first
/// bytes of a file: the first indirect object of the body, which the annex
/// requires to lie entirely within the first 1024 bytes. `None` when that
/// object is not a dictionary carrying `/Linearized` or lacks one of Table
/// F.1's required entries, so a file whose first object is anything else
/// is simply not linearized.
///
/// Covers ISO 32000-1 Annex F.3.
pub fn linearization_dictionary(head: &[u8]) -> Option<Linearization> {
let window = &head[..head.len().min(LINEARIZATION_WINDOW)];
let (_, object) = Parser::new(window).parse_indirect(&NoResolve).ok()?;
let dict = object.as_dict()?;
let version = dict.get("Linearized")?.as_f64()?;
let offset = |key: &str| u64::try_from(dict.get_int(key)?).ok();
let count = |key: &str| u32::try_from(dict.get_int(key)?).ok();
Some(Linearization {
version,
file_length: offset("L")?,
hint_streams: hint_streams(dict)?,
first_page_object: count("O")?,
first_page_end: offset("E")?,
page_count: count("N")?,
main_xref_offset: offset("T")?,
first_page: match dict.get_int("P") {
Some(page) => u32::try_from(page).ok()?,
None => 0,
},
})
}
/// The `/H` array as (offset, length) pairs: two or four integers, so an
/// odd count or a non-integer reads as a malformed dictionary.
fn hint_streams(dict: &Dict) -> Option<Vec<(u64, u64)>> {
let values: Vec<u64> = dict
.get_array("H")?
.iter()
.map(|value| u64::try_from(value.as_int()?).ok())
.collect::<Option<_>>()?;
let (pairs, rest) = values.as_chunks::<2>();
if pairs.is_empty() || !rest.is_empty() {
return None;
}
Some(
pairs
.iter()
.map(|&[offset, length]| (offset, length))
.collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::Document;
use pdfboss_testkit::PdfBuilder;
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";
/// A one-page file whose first object is a linearization parameter
/// dictionary declaring `length` as the file length.
fn linearized_file(length: &str) -> Vec<u8> {
let mut b = PdfBuilder::new();
b.object(
43,
&format!("<< /Linearized 1 /L {length} /H [ 0 0 ] /O 46 /E 0 /N 1 /T 0 >>"),
);
b.object(44, "<< /Type /Catalog /Pages 45 0 R >>");
b.object(45, "<< /Type /Pages /Kids [46 0 R] /Count 1 >>");
b.object(46, "<< /Type /Page /Parent 45 0 R /MediaBox [0 0 10 10] >>");
b.build(44)
}
/// The same file with `/L` patched to its actual length; the
/// placeholder keeps the width, so the length does not move.
fn current_linearized_file() -> Vec<u8> {
let mut data = linearized_file("0000000000");
let length = format!("{:010}", data.len());
let at = data.windows(10).position(|w| w == b"0000000000").unwrap();
data[at..at + 10].copy_from_slice(length.as_bytes());
data
}
// Covers ISO 32000-1 Annex F.3.
#[test]
fn reads_every_entry_of_the_linearization_parameter_dictionary() {
let record = linearization_dictionary(HEAD).unwrap();
assert_eq!(
record,
Linearization {
version: 1.0,
file_length: 12345,
hint_streams: vec![(500, 200)],
first_page_object: 45,
first_page_end: 3000,
page_count: 3,
main_xref_offset: 11000,
first_page: 2,
}
);
}
// Covers ISO 32000-1 Annex F.3.
#[test]
fn the_first_page_number_defaults_to_zero_and_an_overflow_hint_stream_is_read() {
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";
let record = linearization_dictionary(head).unwrap();
assert_eq!(record.hint_streams, vec![(500, 200), (8000, 100)]);
assert_eq!(record.first_page, 0);
assert_eq!(record.version, 1.0);
}
/// Only the first object counts, it must sit within the first 1024
/// bytes, and every required entry of Table F.1 must be present.
// Covers ISO 32000-1 Annex F.3.
#[test]
fn a_file_without_the_dictionary_as_its_first_object_is_not_linearized() {
assert_eq!(linearization_dictionary(b""), None);
assert_eq!(
linearization_dictionary(b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\n"),
None
);
let missing_page_count =
b"%PDF-1.4\n43 0 obj\n<< /Linearized 1 /L 12345 /H [ 500 200 ] /O 45 /E 3000 /T 11000 >>\nendobj\n";
assert_eq!(linearization_dictionary(missing_page_count), None);
let odd_hint_array =
b"%PDF-1.4\n43 0 obj\n<< /Linearized 1 /L 12345 /H [ 500 ] /O 45 /E 3000 /N 3 /T 11000 >>\nendobj\n";
assert_eq!(linearization_dictionary(odd_hint_array), None);
let mut late = b"%PDF-1.4\n%".to_vec();
late.extend(std::iter::repeat_n(b'x', 1024));
late.extend_from_slice(&HEAD[9..]);
assert_eq!(linearization_dictionary(&late), None);
}
/// `/L` must equal the file's length for the linearization information
/// to apply; after an appended update the dictionary is still readable
/// but the file counts as ordinary PDF.
// Covers ISO 32000-1 Annex F.3.
#[test]
fn a_document_is_linearized_only_while_the_declared_length_matches() {
let doc = Document::load(current_linearized_file()).unwrap();
let record = doc.linearization().unwrap();
assert_eq!(record.first_page_object, 46);
assert_eq!(record.page_count, 1);
assert!(doc.is_linearized());
let mut updated = current_linearized_file();
updated.extend_from_slice(b"%appended update\n");
let doc = Document::load(updated).unwrap();
assert!(doc.linearization().is_some());
assert!(!doc.is_linearized());
let doc = Document::load(pdfboss_testkit::simple_doc("plain")).unwrap();
assert_eq!(doc.linearization(), None);
assert!(!doc.is_linearized());
}
}