/src/qpdf/libqpdf/QPDF_linearization.cc
Line | Count | Source |
1 | | // See doc/linearization. |
2 | | |
3 | | #include <qpdf/QPDF_private.hh> |
4 | | |
5 | | #include <qpdf/BitStream.hh> |
6 | | #include <qpdf/BitWriter.hh> |
7 | | #include <qpdf/InputSource_private.hh> |
8 | | #include <qpdf/Pipeline_private.hh> |
9 | | #include <qpdf/Pl_Buffer.hh> |
10 | | #include <qpdf/Pl_Flate.hh> |
11 | | #include <qpdf/Pl_String.hh> |
12 | | #include <qpdf/QPDFExc.hh> |
13 | | #include <qpdf/QPDFObjectHandle_private.hh> |
14 | | #include <qpdf/QPDFWriter_private.hh> |
15 | | #include <qpdf/QTC.hh> |
16 | | #include <qpdf/QUtil.hh> |
17 | | #include <qpdf/Util.hh> |
18 | | |
19 | | #include <algorithm> |
20 | | #include <cmath> |
21 | | #include <cstring> |
22 | | #include <utility> |
23 | | |
24 | | using namespace qpdf; |
25 | | using namespace std::literals; |
26 | | |
27 | | using Lin = QPDF::Doc::Linearization; |
28 | | |
29 | | template <class T, class int_type> |
30 | | static void |
31 | | load_vector_int( |
32 | | BitStream& bit_stream, int nitems, std::vector<T>& vec, int bits_wanted, int_type T::* field) |
33 | 0 | { |
34 | 0 | bool append = vec.empty(); |
35 | | // nitems times, read bits_wanted from the given bit stream, storing results in the ith vector |
36 | | // entry. |
37 | |
|
38 | 0 | for (size_t i = 0; i < QIntC::to_size(nitems); ++i) { |
39 | 0 | if (append) { |
40 | 0 | vec.push_back(T()); |
41 | 0 | } |
42 | 0 | vec.at(i).*field = bit_stream.getBitsInt(QIntC::to_size(bits_wanted)); |
43 | 0 | } |
44 | 0 | util::assertion( |
45 | 0 | std::cmp_equal(vec.size(), nitems), "vector has wrong size in load_vector_int" // |
46 | 0 | ); |
47 | | // The PDF spec says that each hint table starts at a byte boundary. Each "row" actually must |
48 | | // start on a byte boundary. |
49 | 0 | bit_stream.skipToNextByte(); |
50 | 0 | } Unexecuted instantiation: QPDF_linearization.cc:void load_vector_int<QPDF::Doc::Linearization::HPageOffsetEntry, int>(BitStream&, int, std::__1::vector<QPDF::Doc::Linearization::HPageOffsetEntry, std::__1::allocator<QPDF::Doc::Linearization::HPageOffsetEntry> >&, int, int QPDF::Doc::Linearization::HPageOffsetEntry::*) Unexecuted instantiation: QPDF_linearization.cc:void load_vector_int<QPDF::Doc::Linearization::HPageOffsetEntry, long long>(BitStream&, int, std::__1::vector<QPDF::Doc::Linearization::HPageOffsetEntry, std::__1::allocator<QPDF::Doc::Linearization::HPageOffsetEntry> >&, int, long long QPDF::Doc::Linearization::HPageOffsetEntry::*) Unexecuted instantiation: QPDF_linearization.cc:void load_vector_int<QPDF::Doc::Linearization::HSharedObjectEntry, int>(BitStream&, int, std::__1::vector<QPDF::Doc::Linearization::HSharedObjectEntry, std::__1::allocator<QPDF::Doc::Linearization::HSharedObjectEntry> >&, int, int QPDF::Doc::Linearization::HSharedObjectEntry::*) |
51 | | |
52 | | template <class T> |
53 | | static void |
54 | | load_vector_vector( |
55 | | BitStream& bit_stream, |
56 | | int nitems1, |
57 | | std::vector<T>& vec1, |
58 | | int T::* nitems2, |
59 | | int bits_wanted, |
60 | | std::vector<int> T::* vec2) |
61 | 0 | { |
62 | | // nitems1 times, read nitems2 (from the ith element of vec1) items into the vec2 vector field |
63 | | // of the ith item of vec1. |
64 | 0 | for (size_t i1 = 0; i1 < QIntC::to_size(nitems1); ++i1) { |
65 | 0 | for (int i2 = 0; i2 < vec1.at(i1).*nitems2; ++i2) { |
66 | 0 | (vec1.at(i1).*vec2).push_back(bit_stream.getBitsInt(QIntC::to_size(bits_wanted))); |
67 | 0 | } |
68 | 0 | } |
69 | 0 | bit_stream.skipToNextByte(); |
70 | 0 | } |
71 | | |
72 | | Lin::ObjUser::ObjUser(user_e type) : |
73 | 9.56k | ou_type(type) |
74 | 9.56k | { |
75 | 9.56k | qpdf_expect(type == ou_root); |
76 | 9.56k | } |
77 | | |
78 | | Lin::ObjUser::ObjUser(user_e type, size_t pageno) : |
79 | 35.1k | ou_type(type), |
80 | 35.1k | pageno(pageno) |
81 | 35.1k | { |
82 | 35.1k | qpdf_expect(type == ou_page || type == ou_thumb); |
83 | 35.1k | } |
84 | | |
85 | | Lin::ObjUser::ObjUser(user_e type, std::string const& key) : |
86 | 54.6k | ou_type(type), |
87 | 54.6k | key(key) |
88 | 54.6k | { |
89 | 54.6k | qpdf_expect(type == ou_trailer_key || type == ou_root_key); |
90 | 54.6k | } |
91 | | |
92 | | bool |
93 | | Lin::ObjUser::operator<(ObjUser const& rhs) const |
94 | 8.75M | { |
95 | 8.75M | if (ou_type < rhs.ou_type) { |
96 | 284k | return true; |
97 | 284k | } |
98 | 8.47M | if (ou_type == rhs.ou_type) { |
99 | 8.21M | if (pageno < rhs.pageno) { |
100 | 707k | return true; |
101 | 707k | } |
102 | 7.50M | if (pageno == rhs.pageno) { |
103 | 6.81M | return key < rhs.key; |
104 | 6.81M | } |
105 | 7.50M | } |
106 | 948k | return false; |
107 | 8.47M | } |
108 | | |
109 | | Lin::UpdateObjectMapsFrame::UpdateObjectMapsFrame( |
110 | | ObjUser const& ou, QPDFObjectHandle oh, bool top) : |
111 | 2.09M | ou(ou), |
112 | 2.09M | oh(oh), |
113 | 2.09M | top(top) |
114 | 2.09M | { |
115 | 2.09M | } |
116 | | |
117 | | void |
118 | | QPDF::optimize( |
119 | | std::map<int, int> const& object_stream_data, |
120 | | bool allow_changes, |
121 | | std::function<int(QPDFObjectHandle&)> skip_stream_parameters) |
122 | 0 | { |
123 | 0 | m->lin.optimize_internal(object_stream_data, allow_changes, skip_stream_parameters); |
124 | 0 | } |
125 | | |
126 | | void |
127 | | Lin::optimize( |
128 | | QPDFWriter::ObjTable const& obj, std::function<int(QPDFObjectHandle&)> skip_stream_parameters) |
129 | 9.58k | { |
130 | 9.58k | optimize_internal(obj, true, skip_stream_parameters); |
131 | 9.58k | } |
132 | | |
133 | | template <typename T> |
134 | | void |
135 | | Lin::optimize_internal( |
136 | | T const& object_stream_data, |
137 | | bool allow_changes, |
138 | | std::function<int(QPDFObjectHandle&)> skip_stream_parameters) |
139 | 9.58k | { |
140 | 9.58k | if (!obj_user_to_objects_.empty()) { |
141 | | // already optimized |
142 | 0 | return; |
143 | 0 | } |
144 | | |
145 | | // The PDF specification indicates that /Outlines is supposed to be an indirect reference. Force |
146 | | // it to be so if it exists and is direct. (This has been seen in the wild.) |
147 | 9.58k | QPDFObjectHandle root = qpdf.getRoot(); |
148 | 9.58k | if (root.getKey("/Outlines").isDictionary()) { |
149 | 177 | QPDFObjectHandle outlines = root.getKey("/Outlines"); |
150 | 177 | if (!outlines.isIndirect()) { |
151 | 3 | root.replaceKey("/Outlines", qpdf.makeIndirectObject(outlines)); |
152 | 3 | } |
153 | 177 | } |
154 | | |
155 | | // Traverse pages tree pushing all inherited resources down to the page level. This also |
156 | | // initializes m->all_pages. |
157 | 9.58k | m->pages.pushInheritedAttributesToPage(allow_changes, false); |
158 | | |
159 | | // Traverse pages to create mappings between objects and the objects that use them. This |
160 | | // operation dominates the time spent linearizing. To support more accurate progress reporting, |
161 | | // we report progress through this operation when a callback is registered. Throttle to one |
162 | | // event per integer-percent change to minimize overhead. |
163 | 9.58k | size_t const total = m->pages.size(); |
164 | 9.58k | int last_pct = -1; |
165 | 9.58k | size_t n = 0; |
166 | 13.7k | for (auto const& page: m->pages) { |
167 | 13.7k | updateObjectMaps(ObjUser(ObjUser::ou_page, n), page, skip_stream_parameters); |
168 | 13.7k | ++n; |
169 | 13.7k | if (progress_callback_ && total > 0) { |
170 | 0 | int pct = static_cast<int>((100ULL * n) / total); |
171 | 0 | if (pct != last_pct) { |
172 | 0 | progress_callback_(pct); |
173 | 0 | last_pct = pct; |
174 | 0 | } |
175 | 0 | } |
176 | 13.7k | } |
177 | | |
178 | | // Traverse document-level items |
179 | 32.4k | for (auto const& [key, value]: m->trailer.as_dictionary()) { |
180 | 32.4k | if (key == "/Root") { |
181 | | // handled separately |
182 | 22.8k | } else { |
183 | 22.8k | if (!value.null()) { |
184 | 16.9k | updateObjectMaps( |
185 | 16.9k | ObjUser(ObjUser::ou_trailer_key, key), value, skip_stream_parameters); |
186 | 16.9k | } |
187 | 22.8k | } |
188 | 32.4k | } |
189 | | |
190 | 34.2k | for (auto const& [key, value]: root.as_dictionary()) { |
191 | | // Technically, /I keys from /Thread dictionaries are supposed to be handled separately, but |
192 | | // we are going to disregard that specification for now. There is loads of evidence that |
193 | | // pdlin and Acrobat both disregard things like this from time to time, so this is almost |
194 | | // certain not to cause any problems. |
195 | 34.2k | if (!value.null()) { |
196 | 28.6k | updateObjectMaps(ObjUser(ObjUser::ou_root_key, key), value, skip_stream_parameters); |
197 | 28.6k | } |
198 | 34.2k | } |
199 | | |
200 | 9.58k | ObjUser root_ou = ObjUser(ObjUser::ou_root); |
201 | 9.58k | auto root_og = root.id_gen(); |
202 | 9.58k | obj_user_to_objects_[root_ou].insert(root_og); |
203 | 9.58k | object_to_obj_users_[root_og].insert(root_ou); |
204 | | |
205 | 9.58k | filterCompressedObjects(object_stream_data); |
206 | 9.58k | } Unexecuted instantiation: void QPDF::Doc::Linearization::optimize_internal<std::__1::map<int, int, std::__1::less<int>, std::__1::allocator<std::__1::pair<int const, int> > > >(std::__1::map<int, int, std::__1::less<int>, std::__1::allocator<std::__1::pair<int const, int> > > const&, bool, std::__1::function<int (QPDFObjectHandle&)>) void QPDF::Doc::Linearization::optimize_internal<QPDFWriter::ObjTable>(QPDFWriter::ObjTable const&, bool, std::__1::function<int (QPDFObjectHandle&)>) Line | Count | Source | 139 | 9.58k | { | 140 | 9.58k | if (!obj_user_to_objects_.empty()) { | 141 | | // already optimized | 142 | 0 | return; | 143 | 0 | } | 144 | | | 145 | | // The PDF specification indicates that /Outlines is supposed to be an indirect reference. Force | 146 | | // it to be so if it exists and is direct. (This has been seen in the wild.) | 147 | 9.58k | QPDFObjectHandle root = qpdf.getRoot(); | 148 | 9.58k | if (root.getKey("/Outlines").isDictionary()) { | 149 | 177 | QPDFObjectHandle outlines = root.getKey("/Outlines"); | 150 | 177 | if (!outlines.isIndirect()) { | 151 | 3 | root.replaceKey("/Outlines", qpdf.makeIndirectObject(outlines)); | 152 | 3 | } | 153 | 177 | } | 154 | | | 155 | | // Traverse pages tree pushing all inherited resources down to the page level. This also | 156 | | // initializes m->all_pages. | 157 | 9.58k | m->pages.pushInheritedAttributesToPage(allow_changes, false); | 158 | | | 159 | | // Traverse pages to create mappings between objects and the objects that use them. This | 160 | | // operation dominates the time spent linearizing. To support more accurate progress reporting, | 161 | | // we report progress through this operation when a callback is registered. Throttle to one | 162 | | // event per integer-percent change to minimize overhead. | 163 | 9.58k | size_t const total = m->pages.size(); | 164 | 9.58k | int last_pct = -1; | 165 | 9.58k | size_t n = 0; | 166 | 13.7k | for (auto const& page: m->pages) { | 167 | 13.7k | updateObjectMaps(ObjUser(ObjUser::ou_page, n), page, skip_stream_parameters); | 168 | 13.7k | ++n; | 169 | 13.7k | if (progress_callback_ && total > 0) { | 170 | 0 | int pct = static_cast<int>((100ULL * n) / total); | 171 | 0 | if (pct != last_pct) { | 172 | 0 | progress_callback_(pct); | 173 | 0 | last_pct = pct; | 174 | 0 | } | 175 | 0 | } | 176 | 13.7k | } | 177 | | | 178 | | // Traverse document-level items | 179 | 32.4k | for (auto const& [key, value]: m->trailer.as_dictionary()) { | 180 | 32.4k | if (key == "/Root") { | 181 | | // handled separately | 182 | 22.8k | } else { | 183 | 22.8k | if (!value.null()) { | 184 | 16.9k | updateObjectMaps( | 185 | 16.9k | ObjUser(ObjUser::ou_trailer_key, key), value, skip_stream_parameters); | 186 | 16.9k | } | 187 | 22.8k | } | 188 | 32.4k | } | 189 | | | 190 | 34.2k | for (auto const& [key, value]: root.as_dictionary()) { | 191 | | // Technically, /I keys from /Thread dictionaries are supposed to be handled separately, but | 192 | | // we are going to disregard that specification for now. There is loads of evidence that | 193 | | // pdlin and Acrobat both disregard things like this from time to time, so this is almost | 194 | | // certain not to cause any problems. | 195 | 34.2k | if (!value.null()) { | 196 | 28.6k | updateObjectMaps(ObjUser(ObjUser::ou_root_key, key), value, skip_stream_parameters); | 197 | 28.6k | } | 198 | 34.2k | } | 199 | | | 200 | 9.58k | ObjUser root_ou = ObjUser(ObjUser::ou_root); | 201 | 9.58k | auto root_og = root.id_gen(); | 202 | 9.58k | obj_user_to_objects_[root_ou].insert(root_og); | 203 | 9.58k | object_to_obj_users_[root_og].insert(root_ou); | 204 | | | 205 | 9.58k | filterCompressedObjects(object_stream_data); | 206 | 9.58k | } |
|
207 | | |
208 | | void |
209 | | Lin::updateObjectMaps( |
210 | | ObjUser const& first_ou, |
211 | | QPDFObjectHandle first_oh, |
212 | | std::function<int(QPDFObjectHandle&)> skip_stream_parameters) |
213 | 59.3k | { |
214 | 59.3k | QPDFObjGen::set visited; |
215 | 59.3k | std::vector<UpdateObjectMapsFrame> pending; |
216 | 59.3k | pending.emplace_back(first_ou, first_oh, true); |
217 | | // Traverse the object tree from this point taking care to avoid crossing page boundaries. |
218 | 59.3k | std::unique_ptr<ObjUser> thumb_ou; |
219 | 2.15M | while (!pending.empty()) { |
220 | 2.09M | auto cur = pending.back(); |
221 | 2.09M | pending.pop_back(); |
222 | | |
223 | 2.09M | bool is_page_node = false; |
224 | | |
225 | 2.09M | if (cur.oh.isDictionaryOfType("/Page")) { |
226 | 68.2k | is_page_node = true; |
227 | 68.2k | if (!cur.top) { |
228 | 53.7k | continue; |
229 | 53.7k | } |
230 | 68.2k | } |
231 | | |
232 | 2.03M | if (cur.oh.indirect()) { |
233 | 549k | QPDFObjGen og(cur.oh.getObjGen()); |
234 | 549k | if (!visited.add(og)) { |
235 | 134k | QTC::TC("qpdf", "QPDF opt loop detected"); |
236 | 134k | continue; |
237 | 134k | } |
238 | 414k | obj_user_to_objects_[cur.ou].insert(og); |
239 | 414k | object_to_obj_users_[og].insert(cur.ou); |
240 | 414k | } |
241 | | |
242 | 1.90M | if (cur.oh.isArray()) { |
243 | 1.20M | for (auto const& item: cur.oh.as_array()) { |
244 | 1.20M | pending.emplace_back(cur.ou, item, false); |
245 | 1.20M | } |
246 | 1.78M | } else if (cur.oh.isDictionary() || cur.oh.isStream()) { |
247 | 260k | QPDFObjectHandle dict = cur.oh; |
248 | 260k | bool is_stream = cur.oh.isStream(); |
249 | 260k | int ssp = 0; |
250 | 260k | if (is_stream) { |
251 | 41.4k | dict = cur.oh.getDict(); |
252 | 41.4k | if (skip_stream_parameters) { |
253 | 41.4k | ssp = skip_stream_parameters(cur.oh); |
254 | 41.4k | } |
255 | 41.4k | } |
256 | | |
257 | 1.07M | for (auto& [key, value]: dict.as_dictionary()) { |
258 | 1.07M | if (value.null()) { |
259 | 195k | continue; |
260 | 195k | } |
261 | | |
262 | 879k | if (is_page_node && (key == "/Thumb")) { |
263 | | // Traverse page thumbnail dictionaries as a special case. There can only ever |
264 | | // be one /Thumb key on a page, and we see at most one page node per call. |
265 | 661 | thumb_ou = std::make_unique<ObjUser>(ObjUser::ou_thumb, cur.ou.pageno); |
266 | 661 | pending.emplace_back(*thumb_ou, dict.getKey(key), false); |
267 | 878k | } else if (is_page_node && (key == "/Parent")) { |
268 | | // Don't traverse back up the page tree |
269 | 870k | } else if ( |
270 | 870k | ((ssp >= 1) && (key == "/Length")) || |
271 | 837k | ((ssp >= 2) && ((key == "/Filter") || (key == "/DecodeParms")))) { |
272 | | // Don't traverse into stream parameters that we are not going to write. |
273 | 826k | } else { |
274 | 826k | pending.emplace_back(cur.ou, value, false); |
275 | 826k | } |
276 | 879k | } |
277 | 260k | } |
278 | 1.90M | } |
279 | 59.3k | } |
280 | | |
281 | | void |
282 | | Lin::filterCompressedObjects(std::map<int, int> const& object_stream_data) |
283 | 0 | { |
284 | 0 | if (object_stream_data.empty()) { |
285 | 0 | return; |
286 | 0 | } |
287 | | |
288 | | // Transform object_to_obj_users and obj_user_to_objects so that they refer only to uncompressed |
289 | | // objects. If something is a user of a compressed object, then it is really a user of the |
290 | | // object stream that contains it. |
291 | | |
292 | 0 | std::map<ObjUser, std::set<QPDFObjGen>> t_obj_user_to_objects; |
293 | 0 | std::map<QPDFObjGen, std::set<ObjUser>> t_object_to_obj_users; |
294 | |
|
295 | 0 | for (auto const& [ou, ogs]: obj_user_to_objects_) { |
296 | 0 | for (auto const& og: ogs) { |
297 | 0 | auto i2 = object_stream_data.find(og.getObj()); |
298 | 0 | if (i2 == object_stream_data.end()) { |
299 | 0 | t_obj_user_to_objects[ou].insert(og); |
300 | 0 | } else { |
301 | 0 | t_obj_user_to_objects[ou].insert({i2->second, 0}); |
302 | 0 | } |
303 | 0 | } |
304 | 0 | } |
305 | |
|
306 | 0 | for (auto const& [og, ous]: object_to_obj_users_) { |
307 | 0 | for (auto const& ou: ous) { |
308 | 0 | auto i2 = object_stream_data.find(og.getObj()); |
309 | 0 | if (i2 == object_stream_data.end()) { |
310 | 0 | t_object_to_obj_users[og].insert(ou); |
311 | 0 | } else { |
312 | 0 | t_object_to_obj_users[{i2->second, 0}].insert(ou); |
313 | 0 | } |
314 | 0 | } |
315 | 0 | } |
316 | |
|
317 | 0 | obj_user_to_objects_ = std::move(t_obj_user_to_objects); |
318 | 0 | object_to_obj_users_ = std::move(t_object_to_obj_users); |
319 | 0 | } |
320 | | |
321 | | void |
322 | | Lin::filterCompressedObjects(QPDFWriter::ObjTable const& obj) |
323 | 9.56k | { |
324 | 9.56k | if (obj.getStreamsEmpty()) { |
325 | 8.78k | return; |
326 | 8.78k | } |
327 | | |
328 | | // Transform object_to_obj_users and obj_user_to_objects so that they refer only to uncompressed |
329 | | // objects. If something is a user of a compressed object, then it is really a user of the |
330 | | // object stream that contains it. |
331 | | |
332 | 780 | std::map<ObjUser, std::set<QPDFObjGen>> t_obj_user_to_objects; |
333 | 780 | std::map<QPDFObjGen, std::set<ObjUser>> t_object_to_obj_users; |
334 | | |
335 | 4.95k | for (auto const& [ou, ogs]: obj_user_to_objects_) { |
336 | 239k | for (auto const& og: ogs) { |
337 | 239k | if (obj.contains(og)) { |
338 | 237k | if (auto const& i2 = obj[og].object_stream; i2 <= 0) { |
339 | 57.5k | t_obj_user_to_objects[ou].insert(og); |
340 | 180k | } else { |
341 | 180k | t_obj_user_to_objects[ou].insert(QPDFObjGen(i2, 0)); |
342 | 180k | } |
343 | 237k | } |
344 | 239k | } |
345 | 4.95k | } |
346 | | |
347 | 78.4k | for (auto const& [og, ous]: object_to_obj_users_) { |
348 | 78.4k | if (obj.contains(og)) { |
349 | | // Loop over obj_users. |
350 | 237k | for (auto const& ou: ous) { |
351 | 237k | if (auto i2 = obj[og].object_stream; i2 <= 0) { |
352 | 57.5k | t_object_to_obj_users[og].insert(ou); |
353 | 180k | } else { |
354 | 180k | t_object_to_obj_users[{i2, 0}].insert(ou); |
355 | 180k | } |
356 | 237k | } |
357 | 77.5k | } |
358 | 78.4k | } |
359 | | |
360 | 780 | obj_user_to_objects_ = std::move(t_obj_user_to_objects); |
361 | 780 | object_to_obj_users_ = std::move(t_object_to_obj_users); |
362 | 780 | } |
363 | | |
364 | | void |
365 | | Lin::linearizationWarning(std::string_view msg) |
366 | 0 | { |
367 | 0 | linearization_warnings_ = true; |
368 | 0 | warn(qpdf_e_linearization, "", 0, std::string(msg)); |
369 | 0 | } |
370 | | |
371 | | bool |
372 | | QPDF::checkLinearization() |
373 | 0 | { |
374 | 0 | return m->lin.check(); |
375 | 0 | } |
376 | | |
377 | | bool |
378 | | Lin::check() |
379 | 0 | { |
380 | 0 | try { |
381 | 0 | readLinearizationData(); |
382 | 0 | checkLinearizationInternal(); |
383 | 0 | return !linearization_warnings_; |
384 | 0 | } catch (std::runtime_error& e) { |
385 | 0 | linearizationWarning( |
386 | 0 | "error encountered while checking linearization data: " + std::string(e.what())); |
387 | 0 | return false; |
388 | 0 | } |
389 | 0 | } |
390 | | |
391 | | bool |
392 | | QPDF::isLinearized() |
393 | 0 | { |
394 | 0 | return m->lin.linearized(); |
395 | 0 | } |
396 | | |
397 | | bool |
398 | | Lin::linearized() |
399 | 0 | { |
400 | | // If the first object in the file is a dictionary with a suitable /Linearized key and has an /L |
401 | | // key that accurately indicates the file size, initialize m->lindict and return true. |
402 | | |
403 | | // A linearized PDF spec's first object will be contained within the first 1024 bytes of the |
404 | | // file and will be a dictionary with a valid /Linearized key. This routine looks for that and |
405 | | // does no additional validation. |
406 | | |
407 | | // The PDF spec says the linearization dictionary must be completely contained within the first |
408 | | // 1024 bytes of the file. Add a byte for a null terminator. |
409 | 0 | auto buffer = m->file->read(1024, 0); |
410 | 0 | size_t pos = 0; |
411 | 0 | while (true) { |
412 | | // Find a digit or end of buffer |
413 | 0 | pos = buffer.find_first_of("0123456789"sv, pos); |
414 | 0 | if (pos == std::string::npos) { |
415 | 0 | return false; |
416 | 0 | } |
417 | | // Seek to the digit. Then skip over digits for a potential |
418 | | // next iteration. |
419 | 0 | m->file->seek(toO(pos), SEEK_SET); |
420 | |
|
421 | 0 | auto t1 = m->objects.readToken(*m->file, 20); |
422 | 0 | if (!(t1.isInteger() && m->objects.readToken(*m->file, 6).isInteger() && |
423 | 0 | m->objects.readToken(*m->file, 4).isWord("obj"))) { |
424 | 0 | pos = buffer.find_first_not_of("0123456789"sv, pos); |
425 | 0 | if (pos == std::string::npos) { |
426 | 0 | return false; |
427 | 0 | } |
428 | 0 | continue; |
429 | 0 | } |
430 | | |
431 | 0 | Dictionary candidate = qpdf.getObject(toI(QUtil::string_to_ll(t1.getValue().data())), 0); |
432 | 0 | auto linkey = candidate["/Linearized"]; |
433 | 0 | if (!(linkey.isNumber() && toI(floor(linkey.getNumericValue())) == 1)) { |
434 | 0 | return false; |
435 | 0 | } |
436 | | |
437 | 0 | m->file->seek(0, SEEK_END); |
438 | 0 | Integer L = candidate["/L"]; |
439 | 0 | if (L != m->file->tell()) { |
440 | 0 | return false; |
441 | 0 | } |
442 | 0 | linp_.file_size = L; |
443 | 0 | lindict_ = candidate; |
444 | 0 | return true; |
445 | 0 | } |
446 | 0 | } |
447 | | |
448 | | void |
449 | | Lin::readLinearizationData() |
450 | 0 | { |
451 | 0 | util::assertion( |
452 | 0 | linearized(), "called readLinearizationData for file that is not linearized" // |
453 | 0 | ); |
454 | | |
455 | | // This function throws an exception (which is trapped by checkLinearization()) for any errors |
456 | | // that prevent loading. |
457 | | |
458 | | // /L is read and stored in linp by isLinearized() |
459 | 0 | Array H = lindict_["/H"]; // hint table offset/length for primary and overflow hint tables |
460 | 0 | auto H_size = H.size(); |
461 | 0 | Integer H_0 = H[0]; // hint table offset |
462 | 0 | Integer H_1 = H[1]; // hint table length |
463 | 0 | Integer H_2 = H[2]; // hint table offset for overflow hint table |
464 | 0 | Integer H_3 = H[3]; // hint table length for overflow hint table |
465 | 0 | Integer O = lindict_["/O"]; |
466 | 0 | Integer E = lindict_["/E"]; |
467 | 0 | Integer N = lindict_["/N"]; |
468 | 0 | Integer T = lindict_["/T"]; |
469 | 0 | auto P_oh = lindict_["/P"]; |
470 | 0 | Integer P = P_oh; // first page number |
471 | 0 | QTC::TC("qpdf", "QPDF P absent in lindict", P ? 0 : 1); |
472 | |
|
473 | 0 | no_ci_stop_if( |
474 | 0 | !(H && O && E && N && T && (P || P_oh.null())), |
475 | 0 | "some keys in linearization dictionary are of the wrong type", |
476 | 0 | "linearization dictionary" // |
477 | 0 | ); |
478 | |
|
479 | 0 | no_ci_stop_if( |
480 | 0 | !(H_size == 2 || H_size == 4), |
481 | 0 | "H has the wrong number of items", |
482 | 0 | "linearization dictionary" // |
483 | 0 | ); |
484 | |
|
485 | 0 | no_ci_stop_if( |
486 | 0 | !(H_0 && H_1 && (H_size == 2 || (H_2 && H_3))), |
487 | 0 | "some H items are of the wrong type", |
488 | 0 | "linearization dictionary" // |
489 | 0 | ); |
490 | | |
491 | | // Store linearization parameter data |
492 | | |
493 | | // Various places in the code use linp.npages, which is initialized from N, to pre-allocate |
494 | | // memory, so make sure it's accurate and bail right now if it's not. |
495 | 0 | no_ci_stop_if( |
496 | 0 | N != pages.size(), |
497 | 0 | "/N does not match number of pages", |
498 | 0 | "linearization dictionary" // |
499 | 0 | ); |
500 | | |
501 | | // file_size initialized by isLinearized() |
502 | 0 | linp_.first_page_object = O.value<int>(); |
503 | 0 | linp_.first_page_end = E; |
504 | 0 | linp_.npages = N.value<size_t>(); |
505 | 0 | linp_.xref_zero_offset = T; |
506 | 0 | linp_.first_page = P ? P.value<int>() : 0; |
507 | 0 | linp_.H_offset = H_0; |
508 | 0 | linp_.H_length = H_1; |
509 | | |
510 | | // Read hint streams |
511 | |
|
512 | 0 | Pl_Buffer pb("hint buffer"); |
513 | 0 | auto H0 = readHintStream(pb, H_0, H_1.value<size_t>()); |
514 | 0 | if (H_2) { |
515 | 0 | (void)readHintStream(pb, H_2, H_3.value<size_t>()); |
516 | 0 | } |
517 | | |
518 | | // PDF 1.4 hint tables that we ignore: |
519 | | |
520 | | // /T thumbnail |
521 | | // /A thread information |
522 | | // /E named destination |
523 | | // /V interactive form |
524 | | // /I information dictionary |
525 | | // /C logical structure |
526 | | // /L page label |
527 | | |
528 | | // Individual hint table offsets |
529 | 0 | Integer HS = H0["/S"]; // shared object |
530 | 0 | Integer HO = H0["/O"]; // outline |
531 | |
|
532 | 0 | auto hbp = pb.getBufferSharedPointer(); |
533 | 0 | Buffer* hb = hbp.get(); |
534 | 0 | unsigned char const* h_buf = hb->getBuffer(); |
535 | 0 | size_t h_size = hb->getSize(); |
536 | |
|
537 | 0 | readHPageOffset(BitStream(h_buf, h_size)); |
538 | |
|
539 | 0 | size_t HSi = HS.value<size_t>(); |
540 | 0 | if (HSi < 0 || HSi >= h_size) { |
541 | 0 | throw damagedPDF("linearization hint table", "/S (shared object) offset is out of bounds"); |
542 | 0 | } |
543 | 0 | readHSharedObject(BitStream(h_buf + HSi, h_size - HSi)); |
544 | |
|
545 | 0 | if (HO) { |
546 | 0 | no_ci_stop_if( |
547 | 0 | HO < 0 || HO >= h_size, |
548 | 0 | "/O (outline) offset is out of bounds", |
549 | 0 | "linearization dictionary" // |
550 | 0 | ); |
551 | 0 | size_t HOi = HO.value<size_t>(); |
552 | 0 | readHGeneric(BitStream(h_buf + HO, h_size - HOi), outline_hints_); |
553 | 0 | } |
554 | 0 | } |
555 | | |
556 | | Dictionary |
557 | | Lin::readHintStream(Pipeline& pl, qpdf_offset_t offset, size_t length) |
558 | 0 | { |
559 | 0 | auto H = m->objects.readObjectAtOffset(offset, "linearization hint stream", false); |
560 | 0 | no_ci_stop_if( |
561 | 0 | !H.isStream(), "hint table is not a stream", "linearization dictionary" // |
562 | 0 | ); |
563 | 0 | ObjCache& oc = m->obj_cache[H]; |
564 | 0 | qpdf_offset_t min_end_offset = oc.end_before_space; |
565 | 0 | qpdf_offset_t max_end_offset = oc.end_after_space; |
566 | |
|
567 | 0 | Dictionary Hdict = H.getDict(); |
568 | | |
569 | | // Some versions of Acrobat make /Length indirect and place it immediately after the stream, |
570 | | // increasing length to cover it, even though the specification says all objects in the |
571 | | // linearization parameter dictionary must be direct. We have to get the file position of the |
572 | | // end of length in this case. |
573 | 0 | if (Hdict["/Length"].indirect()) { |
574 | 0 | ObjCache& oc2 = m->obj_cache[Hdict["/Length"]]; |
575 | 0 | min_end_offset = oc2.end_before_space; |
576 | 0 | max_end_offset = oc2.end_after_space; |
577 | 0 | } else { |
578 | 0 | QTC::TC("qpdf", "QPDF hint table length direct"); |
579 | 0 | } |
580 | 0 | qpdf_offset_t computed_end = offset + toO(length); |
581 | 0 | no_ci_stop_if( |
582 | 0 | computed_end < min_end_offset || computed_end > max_end_offset, |
583 | 0 | "hint table length mismatch (expected = " + std::to_string(computed_end) + "; actual = " + |
584 | 0 | std::to_string(min_end_offset) + ".." + std::to_string(max_end_offset) + ")", |
585 | 0 | "linearization dictionary" // |
586 | 0 | ); |
587 | 0 | H.pipeStreamData(&pl, 0, qpdf_dl_specialized); |
588 | 0 | return Hdict; |
589 | 0 | } |
590 | | |
591 | | void |
592 | | Lin::readHPageOffset(BitStream h) |
593 | 0 | { |
594 | | // All comments referring to the PDF spec refer to the spec for version 1.4. |
595 | |
|
596 | 0 | HPageOffset& t = page_offset_hints_; |
597 | |
|
598 | 0 | t.min_nobjects = h.getBitsInt(32); // 1 |
599 | 0 | t.first_page_offset = h.getBitsInt(32); // 2 |
600 | 0 | t.nbits_delta_nobjects = h.getBitsInt(16); // 3 |
601 | 0 | t.min_page_length = h.getBitsInt(32); // 4 |
602 | 0 | t.nbits_delta_page_length = h.getBitsInt(16); // 5 |
603 | 0 | t.min_content_offset = h.getBitsInt(32); // 6 |
604 | 0 | t.nbits_delta_content_offset = h.getBitsInt(16); // 7 |
605 | 0 | t.min_content_length = h.getBitsInt(32); // 8 |
606 | 0 | t.nbits_delta_content_length = h.getBitsInt(16); // 9 |
607 | 0 | t.nbits_nshared_objects = h.getBitsInt(16); // 10 |
608 | 0 | t.nbits_shared_identifier = h.getBitsInt(16); // 11 |
609 | 0 | t.nbits_shared_numerator = h.getBitsInt(16); // 12 |
610 | 0 | t.shared_denominator = h.getBitsInt(16); // 13 |
611 | |
|
612 | 0 | std::vector<HPageOffsetEntry>& entries = t.entries; |
613 | 0 | entries.clear(); |
614 | 0 | int nitems = toI(linp_.npages); |
615 | 0 | load_vector_int(h, nitems, entries, t.nbits_delta_nobjects, &HPageOffsetEntry::delta_nobjects); |
616 | 0 | load_vector_int( |
617 | 0 | h, nitems, entries, t.nbits_delta_page_length, &HPageOffsetEntry::delta_page_length); |
618 | 0 | load_vector_int( |
619 | 0 | h, nitems, entries, t.nbits_nshared_objects, &HPageOffsetEntry::nshared_objects); |
620 | 0 | load_vector_vector( |
621 | 0 | h, |
622 | 0 | nitems, |
623 | 0 | entries, |
624 | 0 | &HPageOffsetEntry::nshared_objects, |
625 | 0 | t.nbits_shared_identifier, |
626 | 0 | &HPageOffsetEntry::shared_identifiers); |
627 | 0 | load_vector_vector( |
628 | 0 | h, |
629 | 0 | nitems, |
630 | 0 | entries, |
631 | 0 | &HPageOffsetEntry::nshared_objects, |
632 | 0 | t.nbits_shared_numerator, |
633 | 0 | &HPageOffsetEntry::shared_numerators); |
634 | 0 | load_vector_int( |
635 | 0 | h, nitems, entries, t.nbits_delta_content_offset, &HPageOffsetEntry::delta_content_offset); |
636 | 0 | load_vector_int( |
637 | 0 | h, nitems, entries, t.nbits_delta_content_length, &HPageOffsetEntry::delta_content_length); |
638 | 0 | } |
639 | | |
640 | | void |
641 | | Lin::readHSharedObject(BitStream h) |
642 | 0 | { |
643 | 0 | HSharedObject& t = shared_object_hints_; |
644 | |
|
645 | 0 | t.first_shared_obj = h.getBitsInt(32); // 1 |
646 | 0 | t.first_shared_offset = h.getBitsInt(32); // 2 |
647 | 0 | t.nshared_first_page = h.getBitsInt(32); // 3 |
648 | 0 | t.nshared_total = h.getBitsInt(32); // 4 |
649 | 0 | t.nbits_nobjects = h.getBitsInt(16); // 5 |
650 | 0 | t.min_group_length = h.getBitsInt(32); // 6 |
651 | 0 | t.nbits_delta_group_length = h.getBitsInt(16); // 7 |
652 | |
|
653 | 0 | QTC::TC( |
654 | 0 | "qpdf", |
655 | 0 | "QPDF lin nshared_total > nshared_first_page", |
656 | 0 | (t.nshared_total > t.nshared_first_page) ? 1 : 0); |
657 | |
|
658 | 0 | std::vector<HSharedObjectEntry>& entries = t.entries; |
659 | 0 | entries.clear(); |
660 | 0 | int nitems = t.nshared_total; |
661 | 0 | load_vector_int( |
662 | 0 | h, nitems, entries, t.nbits_delta_group_length, &HSharedObjectEntry::delta_group_length); |
663 | 0 | load_vector_int(h, nitems, entries, 1, &HSharedObjectEntry::signature_present); |
664 | 0 | for (size_t i = 0; i < toS(nitems); ++i) { |
665 | 0 | if (entries.at(i).signature_present) { |
666 | | // Skip 128-bit MD5 hash. These are not supported by acrobat, so they should probably |
667 | | // never be there. We have no test case for this. |
668 | 0 | for (int j = 0; j < 4; ++j) { |
669 | 0 | (void)h.getBits(32); |
670 | 0 | } |
671 | 0 | } |
672 | 0 | } |
673 | 0 | load_vector_int(h, nitems, entries, t.nbits_nobjects, &HSharedObjectEntry::nobjects_minus_one); |
674 | 0 | } |
675 | | |
676 | | void |
677 | | Lin::readHGeneric(BitStream h, HGeneric& t) |
678 | 0 | { |
679 | 0 | t.first_object = h.getBitsInt(32); // 1 |
680 | 0 | t.first_object_offset = h.getBitsInt(32); // 2 |
681 | 0 | t.nobjects = h.getBitsInt(32); // 3 |
682 | 0 | t.group_length = h.getBitsInt(32); // 4 |
683 | 0 | } |
684 | | |
685 | | void |
686 | | Lin::checkLinearizationInternal() |
687 | 0 | { |
688 | | // All comments referring to the PDF spec refer to the spec for version 1.4. |
689 | | |
690 | | // Check all values in linearization parameter dictionary |
691 | |
|
692 | 0 | LinParameters& p = linp_; |
693 | | |
694 | | // L: file size in bytes -- checked by isLinearized |
695 | | |
696 | | // O: object number of first page |
697 | 0 | auto const& all_pages = pages.all(); |
698 | 0 | if (p.first_page_object != all_pages.at(0).getObjectID()) { |
699 | 0 | linearizationWarning("first page object (/O) mismatch"); |
700 | 0 | } |
701 | | |
702 | | // N: number of pages |
703 | 0 | size_t npages = all_pages.size(); |
704 | 0 | if (std::cmp_not_equal(p.npages, npages)) { |
705 | | // Not tested in the test suite |
706 | 0 | linearizationWarning("page count (/N) mismatch"); |
707 | 0 | } |
708 | |
|
709 | 0 | int i = 0; |
710 | 0 | for (auto const& page: all_pages) { |
711 | 0 | if (m->xref_table[page].getType() == 2) { |
712 | 0 | linearizationWarning( |
713 | 0 | "page dictionary for page " + std::to_string(i) + " is compressed"); |
714 | 0 | } |
715 | 0 | ++i; |
716 | 0 | } |
717 | | |
718 | | // T: offset of whitespace character preceding xref entry for object 0 |
719 | 0 | m->file->seek(p.xref_zero_offset, SEEK_SET); |
720 | 0 | while (true) { |
721 | 0 | char ch; |
722 | 0 | m->file->read(&ch, 1); |
723 | 0 | if (!(ch == ' ' || ch == '\r' || ch == '\n')) { |
724 | 0 | m->file->seek(-1, SEEK_CUR); |
725 | 0 | break; |
726 | 0 | } |
727 | 0 | } |
728 | 0 | if (m->file->tell() != objects.first_xref_item_offset()) { |
729 | 0 | linearizationWarning( |
730 | 0 | "space before first xref item (/T) mismatch (computed = " + |
731 | 0 | std::to_string(objects.first_xref_item_offset()) + |
732 | 0 | "; file = " + std::to_string(m->file->tell())); |
733 | 0 | } |
734 | | |
735 | | // P: first page number -- Implementation note 124 says Acrobat ignores this value, so we will |
736 | | // too. |
737 | | |
738 | | // Check numbering of compressed objects in each xref section. For linearized files, all |
739 | | // compressed objects are supposed to be at the end of the containing xref section if any object |
740 | | // streams are in use. |
741 | |
|
742 | 0 | if (objects.uncompressed_after_compressed()) { |
743 | 0 | linearizationWarning( |
744 | 0 | "linearized file contains an uncompressed object after a compressed " |
745 | 0 | "one in a cross-reference stream"); |
746 | 0 | } |
747 | | |
748 | | // Further checking requires optimization and order calculation. Don't allow optimization to |
749 | | // make changes. If it has to, then the file is not properly linearized. We use the xref table |
750 | | // to figure out which objects are compressed and which are uncompressed. |
751 | 0 | { // local scope |
752 | 0 | std::map<int, int> object_stream_data; |
753 | 0 | for (auto const& [og, entry]: m->xref_table) { |
754 | 0 | if (entry.getType() == 2) { |
755 | 0 | object_stream_data[og.getObj()] = entry.getObjStreamNumber(); |
756 | 0 | } |
757 | 0 | } |
758 | 0 | optimize_internal(object_stream_data, false, nullptr); |
759 | 0 | calculateLinearizationData(object_stream_data); |
760 | 0 | } |
761 | | |
762 | | // E: offset of end of first page -- Implementation note 123 says Acrobat includes on extra |
763 | | // object here by mistake. pdlin fails to place thumbnail images in section 9, so when |
764 | | // thumbnails are present, it also gets the wrong value for /E. It also doesn't count outlines |
765 | | // here when it should even though it places them in part 6. This code fails to put thread |
766 | | // information dictionaries in part 9, so it actually gets the wrong value for E when threads |
767 | | // are present. In that case, it would probably agree with pdlin. As of this writing, the test |
768 | | // suite doesn't contain any files with threads. |
769 | |
|
770 | 0 | no_ci_stop_if( |
771 | 0 | part6_.empty(), "linearization part 6 unexpectedly empty" // |
772 | 0 | ); |
773 | 0 | qpdf_offset_t min_E = -1; |
774 | 0 | qpdf_offset_t max_E = -1; |
775 | 0 | for (auto const& oh: part6_) { |
776 | 0 | QPDFObjGen og(oh.getObjGen()); |
777 | | // All objects have to have been dereferenced to be classified. |
778 | 0 | util::assertion(m->obj_cache.contains(og), "linearization part6 object not in cache"); |
779 | 0 | ObjCache const& oc = m->obj_cache[og]; |
780 | 0 | min_E = std::max(min_E, oc.end_before_space); |
781 | 0 | max_E = std::max(max_E, oc.end_after_space); |
782 | 0 | } |
783 | 0 | if (p.first_page_end < min_E || p.first_page_end > max_E) { |
784 | 0 | linearizationWarning( |
785 | 0 | "end of first page section (/E) mismatch: /E = " + std::to_string(p.first_page_end) + |
786 | 0 | "; computed = " + std::to_string(min_E) + ".." + std::to_string(max_E)); |
787 | 0 | } |
788 | | |
789 | | // Check hint tables |
790 | |
|
791 | 0 | std::map<int, int> shared_idx_to_obj; |
792 | 0 | checkHSharedObject(all_pages, shared_idx_to_obj); |
793 | 0 | checkHPageOffset(all_pages, shared_idx_to_obj); |
794 | 0 | checkHOutlines(); |
795 | 0 | } |
796 | | |
797 | | qpdf_offset_t |
798 | | Lin::maxEnd(ObjUser const& ou) |
799 | 0 | { |
800 | 0 | no_ci_stop_if( |
801 | 0 | !obj_user_to_objects_.contains(ou), |
802 | 0 | "no entry in object user table for requested object user" // |
803 | 0 | ); |
804 | |
|
805 | 0 | qpdf_offset_t end = 0; |
806 | 0 | for (auto const& og: obj_user_to_objects_[ou]) { |
807 | 0 | no_ci_stop_if( |
808 | 0 | !m->obj_cache.contains(og), "unknown object referenced in object user table" // |
809 | 0 | ); |
810 | 0 | end = std::max(end, m->obj_cache[og].end_after_space); |
811 | 0 | } |
812 | 0 | return end; |
813 | 0 | } |
814 | | |
815 | | qpdf_offset_t |
816 | | Lin::getLinearizationOffset(QPDFObjGen og, bool require_type_1) |
817 | 0 | { |
818 | 0 | QPDFXRefEntry const& entry = m->xref_table[og]; |
819 | 0 | auto typ = entry.getType(); |
820 | 0 | if (typ == 1) { |
821 | 0 | return entry.getOffset(); |
822 | 0 | } |
823 | 0 | no_ci_stop_if(typ != 2, "getLinearizationOffset called for xref entry not of type 1 or 2"); |
824 | 0 | if (require_type_1) { |
825 | 0 | stopOnError( |
826 | 0 | "the object stream containing an object is itself contained in an object stream"); |
827 | 0 | } |
828 | | |
829 | | // For compressed objects, return the offset of the object stream that contains them. |
830 | 0 | return getLinearizationOffset({entry.getObjStreamNumber(), 0}, true); |
831 | 0 | } |
832 | | |
833 | | QPDFObjectHandle |
834 | | Lin::getUncompressedObject(QPDFObjectHandle& obj, std::map<int, int> const& object_stream_data) |
835 | 0 | { |
836 | 0 | if (obj.null() || !object_stream_data.contains(obj.getObjectID())) { |
837 | 0 | return obj; |
838 | 0 | } |
839 | 0 | return qpdf.getObject((*(object_stream_data.find(obj.getObjectID()))).second, 0); |
840 | 0 | } |
841 | | |
842 | | QPDFObjectHandle |
843 | | Lin::getUncompressedObject(QPDFObjectHandle& oh, QPDFWriter::ObjTable const& obj) |
844 | 26.8k | { |
845 | 26.8k | if (obj.contains(oh)) { |
846 | 26.6k | if (auto id = obj[oh].object_stream; id > 0) { |
847 | 89 | return oh.null() ? oh : qpdf.getObject(id, 0); |
848 | 89 | } |
849 | 26.6k | } |
850 | 26.7k | return oh; |
851 | 26.8k | } |
852 | | |
853 | | int |
854 | | Lin::lengthNextN(int first_object, int n) |
855 | 0 | { |
856 | 0 | int length = 0; |
857 | 0 | for (int i = 0; i < n; ++i) { |
858 | 0 | QPDFObjGen og(first_object + i, 0); |
859 | 0 | if (m->xref_table.contains(og)) { |
860 | 0 | no_ci_stop_if( |
861 | 0 | !m->obj_cache.contains(og), |
862 | 0 | "found unknown object while calculating length for linearization data" // |
863 | 0 | ); |
864 | |
|
865 | 0 | length += toI(m->obj_cache[og].end_after_space - getLinearizationOffset(og)); |
866 | 0 | } else { |
867 | 0 | linearizationWarning( |
868 | 0 | "no xref table entry for " + std::to_string(first_object + i) + " 0"); |
869 | 0 | } |
870 | 0 | } |
871 | 0 | return length; |
872 | 0 | } |
873 | | |
874 | | void |
875 | | Lin::checkHPageOffset( |
876 | | std::vector<QPDFObjectHandle> const& pages, std::map<int, int>& shared_idx_to_obj) |
877 | 0 | { |
878 | | // Implementation note 126 says Acrobat always sets delta_content_offset and |
879 | | // delta_content_length in the page offset header dictionary to 0. It also states that |
880 | | // min_content_offset in the per-page information is always 0, which is an incorrect value. |
881 | | |
882 | | // Implementation note 127 explains that Acrobat always sets item 8 (min_content_length) to |
883 | | // zero, item 9 (nbits_delta_content_length) to the value of item 5 (nbits_delta_page_length), |
884 | | // and item 7 of each per-page hint table (delta_content_length) to item 2 (delta_page_length) |
885 | | // of that entry. Acrobat ignores these values when reading files. |
886 | | |
887 | | // Empirically, it also seems that Acrobat sometimes puts items under a page's /Resources |
888 | | // dictionary in with shared objects even when they are private. |
889 | |
|
890 | 0 | size_t npages = pages.size(); |
891 | 0 | qpdf_offset_t table_offset = adjusted_offset(page_offset_hints_.first_page_offset); |
892 | 0 | QPDFObjGen first_page_og(pages.at(0).getObjGen()); |
893 | 0 | if (!m->xref_table.contains(first_page_og)) { |
894 | 0 | stopOnError("supposed first page object is not known"); |
895 | 0 | } |
896 | 0 | qpdf_offset_t offset = getLinearizationOffset(first_page_og); |
897 | 0 | if (table_offset != offset) { |
898 | 0 | linearizationWarning("first page object offset mismatch"); |
899 | 0 | } |
900 | |
|
901 | 0 | for (size_t pageno = 0; pageno < npages; ++pageno) { |
902 | 0 | QPDFObjGen page_og(pages.at(pageno).getObjGen()); |
903 | 0 | int first_object = page_og.getObj(); |
904 | 0 | if (!m->xref_table.contains(page_og)) { |
905 | 0 | stopOnError("unknown object in page offset hint table"); |
906 | 0 | } |
907 | 0 | offset = getLinearizationOffset(page_og); |
908 | |
|
909 | 0 | HPageOffsetEntry& he = page_offset_hints_.entries.at(pageno); |
910 | 0 | CHPageOffsetEntry& ce = c_page_offset_data_.entries.at(pageno); |
911 | 0 | int h_nobjects = he.delta_nobjects + page_offset_hints_.min_nobjects; |
912 | 0 | if (h_nobjects != ce.nobjects) { |
913 | | // This happens with pdlin when there are thumbnails. |
914 | 0 | linearizationWarning( |
915 | 0 | "object count mismatch for page " + std::to_string(pageno) + ": hint table = " + |
916 | 0 | std::to_string(h_nobjects) + "; computed = " + std::to_string(ce.nobjects)); |
917 | 0 | } |
918 | | |
919 | | // Use value for number of objects in hint table rather than computed value if there is a |
920 | | // discrepancy. |
921 | 0 | int length = lengthNextN(first_object, h_nobjects); |
922 | 0 | int h_length = toI(he.delta_page_length + page_offset_hints_.min_page_length); |
923 | 0 | if (length != h_length) { |
924 | | // This condition almost certainly indicates a bad hint table or a bug in this code. |
925 | 0 | linearizationWarning( |
926 | 0 | "page length mismatch for page " + std::to_string(pageno) + ": hint table = " + |
927 | 0 | std::to_string(h_length) + "; computed length = " + std::to_string(length) + |
928 | 0 | " (offset = " + std::to_string(offset) + ")"); |
929 | 0 | } |
930 | |
|
931 | 0 | offset += h_length; |
932 | | |
933 | | // Translate shared object indexes to object numbers. |
934 | 0 | std::set<int> hint_shared; |
935 | 0 | std::set<int> computed_shared; |
936 | |
|
937 | 0 | if (pageno == 0 && he.nshared_objects > 0) { |
938 | | // pdlin and Acrobat both do this even though the spec states clearly and unambiguously |
939 | | // that they should not. |
940 | 0 | linearizationWarning("page 0 has shared identifier entries"); |
941 | 0 | } |
942 | |
|
943 | 0 | for (size_t i = 0; i < toS(he.nshared_objects); ++i) { |
944 | 0 | int idx = he.shared_identifiers.at(i); |
945 | 0 | no_ci_stop_if( |
946 | 0 | !shared_idx_to_obj.contains(idx), |
947 | 0 | "unable to get object for item in shared objects hint table"); |
948 | |
|
949 | 0 | hint_shared.insert(shared_idx_to_obj[idx]); |
950 | 0 | } |
951 | |
|
952 | 0 | for (size_t i = 0; i < toS(ce.nshared_objects); ++i) { |
953 | 0 | int idx = ce.shared_identifiers.at(i); |
954 | 0 | no_ci_stop_if( |
955 | 0 | idx >= c_shared_object_data_.nshared_total, |
956 | 0 | "index out of bounds for shared object hint table" // |
957 | 0 | ); |
958 | |
|
959 | 0 | int obj = c_shared_object_data_.entries.at(toS(idx)).object; |
960 | 0 | computed_shared.insert(obj); |
961 | 0 | } |
962 | |
|
963 | 0 | for (int iter: hint_shared) { |
964 | 0 | if (!computed_shared.contains(iter)) { |
965 | | // pdlin puts thumbnails here even though it shouldn't |
966 | 0 | linearizationWarning( |
967 | 0 | "page " + std::to_string(pageno) + ": shared object " + std::to_string(iter) + |
968 | 0 | ": in hint table but not computed list"); |
969 | 0 | } |
970 | 0 | } |
971 | |
|
972 | 0 | for (int iter: computed_shared) { |
973 | 0 | if (!hint_shared.contains(iter)) { |
974 | | // Acrobat does not put some things including at least built-in fonts and procsets |
975 | | // here, at least in some cases. |
976 | 0 | linearizationWarning( |
977 | 0 | ("page " + std::to_string(pageno) + ": shared object " + std::to_string(iter) + |
978 | 0 | ": in computed list but not hint table")); |
979 | 0 | } |
980 | 0 | } |
981 | 0 | } |
982 | 0 | } |
983 | | |
984 | | void |
985 | | Lin::checkHSharedObject(std::vector<QPDFObjectHandle> const& pages, std::map<int, int>& idx_to_obj) |
986 | 0 | { |
987 | | // Implementation note 125 says shared object groups always contain only one object. |
988 | | // Implementation note 128 says that Acrobat always nbits_nobjects to zero. Implementation note |
989 | | // 130 says that Acrobat does not support more than one shared object per group. These are all |
990 | | // consistent. |
991 | | |
992 | | // Implementation note 129 states that MD5 signatures are not implemented in Acrobat, so |
993 | | // signature_present must always be zero. |
994 | | |
995 | | // Implementation note 131 states that first_shared_obj and first_shared_offset have meaningless |
996 | | // values for single-page files. |
997 | | |
998 | | // Empirically, Acrobat and pdlin generate incorrect values for these whenever there are no |
999 | | // shared objects not referenced by the first page (i.e., nshared_total == nshared_first_page). |
1000 | |
|
1001 | 0 | HSharedObject& so = shared_object_hints_; |
1002 | 0 | if (so.nshared_total < so.nshared_first_page) { |
1003 | 0 | linearizationWarning("shared object hint table: ntotal < nfirst_page"); |
1004 | 0 | } else { |
1005 | | // The first nshared_first_page objects are consecutive objects starting with the first page |
1006 | | // object. The rest are consecutive starting from the first_shared_obj object. |
1007 | 0 | int cur_object = pages.at(0).getObjectID(); |
1008 | 0 | for (int i = 0; i < so.nshared_total; ++i) { |
1009 | 0 | if (i == so.nshared_first_page) { |
1010 | 0 | QTC::TC("qpdf", "QPDF lin check shared past first page"); |
1011 | 0 | if (part8_.empty()) { |
1012 | 0 | linearizationWarning("part 8 is empty but nshared_total > nshared_first_page"); |
1013 | 0 | } else { |
1014 | 0 | int obj = part8_.at(0).getObjectID(); |
1015 | 0 | if (obj != so.first_shared_obj) { |
1016 | 0 | linearizationWarning( |
1017 | 0 | "first shared object number mismatch: hint table = " + |
1018 | 0 | std::to_string(so.first_shared_obj) + |
1019 | 0 | "; computed = " + std::to_string(obj)); |
1020 | 0 | } |
1021 | 0 | } |
1022 | |
|
1023 | 0 | cur_object = so.first_shared_obj; |
1024 | |
|
1025 | 0 | QPDFObjGen og(cur_object, 0); |
1026 | 0 | if (!m->xref_table.contains(og)) { |
1027 | 0 | stopOnError("unknown object in shared object hint table"); |
1028 | 0 | } |
1029 | 0 | qpdf_offset_t offset = getLinearizationOffset(og); |
1030 | 0 | qpdf_offset_t h_offset = adjusted_offset(so.first_shared_offset); |
1031 | 0 | if (offset != h_offset) { |
1032 | 0 | linearizationWarning( |
1033 | 0 | "first shared object offset mismatch: hint table = " + |
1034 | 0 | std::to_string(h_offset) + "; computed = " + std::to_string(offset)); |
1035 | 0 | } |
1036 | 0 | } |
1037 | |
|
1038 | 0 | idx_to_obj[i] = cur_object; |
1039 | 0 | HSharedObjectEntry& se = so.entries.at(toS(i)); |
1040 | 0 | int nobjects = se.nobjects_minus_one + 1; |
1041 | 0 | int length = lengthNextN(cur_object, nobjects); |
1042 | 0 | int h_length = so.min_group_length + se.delta_group_length; |
1043 | 0 | if (length != h_length) { |
1044 | 0 | linearizationWarning( |
1045 | 0 | "shared object " + std::to_string(i) + " length mismatch: hint table = " + |
1046 | 0 | std::to_string(h_length) + "; computed = " + std::to_string(length)); |
1047 | 0 | } |
1048 | 0 | cur_object += nobjects; |
1049 | 0 | } |
1050 | 0 | } |
1051 | 0 | } |
1052 | | |
1053 | | void |
1054 | | Lin::checkHOutlines() |
1055 | 0 | { |
1056 | | // Empirically, Acrobat generates the correct value for the object number but incorrectly stores |
1057 | | // the next object number's offset as the offset, at least when outlines appear in part 6. It |
1058 | | // also generates an incorrect value for length (specifically, the length that would cover the |
1059 | | // correct number of objects from the wrong starting place). pdlin appears to generate correct |
1060 | | // values in those cases. |
1061 | |
|
1062 | 0 | if (c_outline_data_.nobjects == outline_hints_.nobjects) { |
1063 | 0 | if (c_outline_data_.nobjects == 0) { |
1064 | 0 | return; |
1065 | 0 | } |
1066 | | |
1067 | 0 | if (c_outline_data_.first_object == outline_hints_.first_object) { |
1068 | | // Check length and offset. Acrobat gets these wrong. |
1069 | 0 | QPDFObjectHandle outlines = qpdf.getRoot().getKey("/Outlines"); |
1070 | 0 | if (!outlines.isIndirect()) { |
1071 | | // This case is not exercised in test suite since not permitted by the spec, but if |
1072 | | // this does occur, the code below would fail. |
1073 | 0 | linearizationWarning("/Outlines key of root dictionary is not indirect"); |
1074 | 0 | return; |
1075 | 0 | } |
1076 | 0 | QPDFObjGen og(outlines.getObjGen()); |
1077 | 0 | no_ci_stop_if( |
1078 | 0 | !m->xref_table.contains(og), "unknown object in outlines hint table" // |
1079 | 0 | ); |
1080 | 0 | qpdf_offset_t offset = getLinearizationOffset(og); |
1081 | 0 | ObjUser ou(ObjUser::ou_root_key, "/Outlines"); |
1082 | 0 | int length = toI(maxEnd(ou) - offset); |
1083 | 0 | qpdf_offset_t table_offset = adjusted_offset(outline_hints_.first_object_offset); |
1084 | 0 | if (offset != table_offset) { |
1085 | 0 | linearizationWarning( |
1086 | 0 | "incorrect offset in outlines table: hint table = " + |
1087 | 0 | std::to_string(table_offset) + "; computed = " + std::to_string(offset)); |
1088 | 0 | } |
1089 | 0 | int table_length = outline_hints_.group_length; |
1090 | 0 | if (length != table_length) { |
1091 | 0 | linearizationWarning( |
1092 | 0 | "incorrect length in outlines table: hint table = " + |
1093 | 0 | std::to_string(table_length) + "; computed = " + std::to_string(length)); |
1094 | 0 | } |
1095 | 0 | } else { |
1096 | 0 | linearizationWarning("incorrect first object number in outline hints table."); |
1097 | 0 | } |
1098 | 0 | } else { |
1099 | 0 | linearizationWarning("incorrect object count in outline hint table"); |
1100 | 0 | } |
1101 | 0 | } |
1102 | | |
1103 | | void |
1104 | | QPDF::showLinearizationData() |
1105 | 0 | { |
1106 | 0 | m->lin.show_data(); |
1107 | 0 | } |
1108 | | |
1109 | | void |
1110 | | Lin::show_data() |
1111 | 0 | { |
1112 | 0 | try { |
1113 | 0 | readLinearizationData(); |
1114 | 0 | checkLinearizationInternal(); |
1115 | 0 | } catch (QPDFExc const& e) { |
1116 | 0 | linearizationWarning(e.what()); |
1117 | 0 | } |
1118 | 0 | try { |
1119 | 0 | dumpLinearizationDataInternal(); |
1120 | 0 | } catch (QPDFExc const& e) { |
1121 | 0 | linearizationWarning(e.what()); |
1122 | 0 | } |
1123 | 0 | } |
1124 | | |
1125 | | void |
1126 | | Lin::dumpLinearizationDataInternal() |
1127 | 0 | { |
1128 | 0 | auto& info = *cf.log()->getInfo(); |
1129 | |
|
1130 | 0 | info << m->file->getName() << ": linearization data:\n\n"; |
1131 | |
|
1132 | 0 | info << "file_size: " << linp_.file_size << "\n" |
1133 | 0 | << "first_page_object: " << linp_.first_page_object << "\n" |
1134 | 0 | << "first_page_end: " << linp_.first_page_end << "\n" |
1135 | 0 | << "npages: " << linp_.npages << "\n" |
1136 | 0 | << "xref_zero_offset: " << linp_.xref_zero_offset << "\n" |
1137 | 0 | << "first_page: " << linp_.first_page << "\n" |
1138 | 0 | << "H_offset: " << linp_.H_offset << "\n" |
1139 | 0 | << "H_length: " << linp_.H_length << "\n" |
1140 | 0 | << "\n"; |
1141 | |
|
1142 | 0 | info << "Page Offsets Hint Table\n\n"; |
1143 | 0 | dumpHPageOffset(); |
1144 | 0 | info << "\nShared Objects Hint Table\n\n"; |
1145 | 0 | dumpHSharedObject(); |
1146 | |
|
1147 | 0 | if (outline_hints_.nobjects > 0) { |
1148 | 0 | info << "\nOutlines Hint Table\n\n"; |
1149 | 0 | dumpHGeneric(outline_hints_); |
1150 | 0 | } |
1151 | 0 | } |
1152 | | |
1153 | | qpdf_offset_t |
1154 | | Lin::adjusted_offset(qpdf_offset_t offset) |
1155 | 0 | { |
1156 | | // All offsets >= H_offset have to be increased by H_length since all hint table location values |
1157 | | // disregard the hint table itself. |
1158 | 0 | if (offset >= linp_.H_offset) { |
1159 | 0 | return offset + linp_.H_length; |
1160 | 0 | } |
1161 | 0 | return offset; |
1162 | 0 | } |
1163 | | |
1164 | | void |
1165 | | Lin::dumpHPageOffset() |
1166 | 0 | { |
1167 | 0 | auto& info = *cf.log()->getInfo(); |
1168 | 0 | HPageOffset& t = page_offset_hints_; |
1169 | 0 | info << "min_nobjects: " << t.min_nobjects << "\n" |
1170 | 0 | << "first_page_offset: " << adjusted_offset(t.first_page_offset) << "\n" |
1171 | 0 | << "nbits_delta_nobjects: " << t.nbits_delta_nobjects << "\n" |
1172 | 0 | << "min_page_length: " << t.min_page_length << "\n" |
1173 | 0 | << "nbits_delta_page_length: " << t.nbits_delta_page_length << "\n" |
1174 | 0 | << "min_content_offset: " << t.min_content_offset << "\n" |
1175 | 0 | << "nbits_delta_content_offset: " << t.nbits_delta_content_offset << "\n" |
1176 | 0 | << "min_content_length: " << t.min_content_length << "\n" |
1177 | 0 | << "nbits_delta_content_length: " << t.nbits_delta_content_length << "\n" |
1178 | 0 | << "nbits_nshared_objects: " << t.nbits_nshared_objects << "\n" |
1179 | 0 | << "nbits_shared_identifier: " << t.nbits_shared_identifier << "\n" |
1180 | 0 | << "nbits_shared_numerator: " << t.nbits_shared_numerator << "\n" |
1181 | 0 | << "shared_denominator: " << t.shared_denominator << "\n"; |
1182 | |
|
1183 | 0 | for (size_t i1 = 0; i1 < linp_.npages; ++i1) { |
1184 | 0 | HPageOffsetEntry& pe = t.entries.at(i1); |
1185 | 0 | info << "Page " << i1 << ":\n" |
1186 | 0 | << " nobjects: " << pe.delta_nobjects + t.min_nobjects << "\n" |
1187 | 0 | << " length: " << pe.delta_page_length + t.min_page_length |
1188 | 0 | << "\n" |
1189 | | // content offset is relative to page, not file |
1190 | 0 | << " content_offset: " << pe.delta_content_offset + t.min_content_offset << "\n" |
1191 | 0 | << " content_length: " << pe.delta_content_length + t.min_content_length << "\n" |
1192 | 0 | << " nshared_objects: " << pe.nshared_objects << "\n"; |
1193 | 0 | for (size_t i2 = 0; i2 < toS(pe.nshared_objects); ++i2) { |
1194 | 0 | info << " identifier " << i2 << ": " << pe.shared_identifiers.at(i2) << "\n"; |
1195 | 0 | info << " numerator " << i2 << ": " << pe.shared_numerators.at(i2) << "\n"; |
1196 | 0 | } |
1197 | 0 | } |
1198 | 0 | } |
1199 | | |
1200 | | void |
1201 | | Lin::dumpHSharedObject() |
1202 | 0 | { |
1203 | 0 | auto& info = *cf.log()->getInfo(); |
1204 | 0 | HSharedObject& t = shared_object_hints_; |
1205 | 0 | info << "first_shared_obj: " << t.first_shared_obj << "\n" |
1206 | 0 | << "first_shared_offset: " << adjusted_offset(t.first_shared_offset) << "\n" |
1207 | 0 | << "nshared_first_page: " << t.nshared_first_page << "\n" |
1208 | 0 | << "nshared_total: " << t.nshared_total << "\n" |
1209 | 0 | << "nbits_nobjects: " << t.nbits_nobjects << "\n" |
1210 | 0 | << "min_group_length: " << t.min_group_length << "\n" |
1211 | 0 | << "nbits_delta_group_length: " << t.nbits_delta_group_length << "\n"; |
1212 | |
|
1213 | 0 | for (size_t i = 0; i < toS(t.nshared_total); ++i) { |
1214 | 0 | HSharedObjectEntry& se = t.entries.at(i); |
1215 | 0 | info << "Shared Object " << i << ":\n" |
1216 | 0 | << " group length: " << se.delta_group_length + t.min_group_length << "\n"; |
1217 | | // PDF spec says signature present nobjects_minus_one are always 0, so print them only if |
1218 | | // they have a non-zero value. |
1219 | 0 | if (se.signature_present) { |
1220 | 0 | info << " signature present\n"; |
1221 | 0 | } |
1222 | 0 | if (se.nobjects_minus_one != 0) { |
1223 | 0 | info << " nobjects: " << se.nobjects_minus_one + 1 << "\n"; |
1224 | 0 | } |
1225 | 0 | } |
1226 | 0 | } |
1227 | | |
1228 | | void |
1229 | | Lin::dumpHGeneric(HGeneric& t) |
1230 | 0 | { |
1231 | 0 | *cf.log()->getInfo() << "first_object: " << t.first_object << "\n" |
1232 | 0 | << "first_object_offset: " << adjusted_offset(t.first_object_offset) |
1233 | 0 | << "\n" |
1234 | 0 | << "nobjects: " << t.nobjects << "\n" |
1235 | 0 | << "group_length: " << t.group_length << "\n"; |
1236 | 0 | } |
1237 | | |
1238 | | template <typename T> |
1239 | | void |
1240 | | Lin::calculateLinearizationData(T const& object_stream_data) |
1241 | 9.56k | { |
1242 | | // This function calculates the ordering of objects, divides them into the appropriate parts, |
1243 | | // and computes some values for the linearization parameter dictionary and hint tables. The |
1244 | | // file must be optimized (via calling optimize()) prior to calling this function. Note that |
1245 | | // actual offsets and lengths are not computed here, but anything related to object ordering is. |
1246 | | |
1247 | 9.56k | util::assertion( |
1248 | 9.56k | !object_to_obj_users_.empty(), |
1249 | 9.56k | "INTERNAL ERROR: QPDF::calculateLinearizationData called before optimize()" // |
1250 | 9.56k | ); |
1251 | | // Note that we can't call optimize here because we don't know whether it should be called |
1252 | | // with or without allow changes. |
1253 | | |
1254 | | // Separate objects into the categories sufficient for us to determine which part of the |
1255 | | // linearized file should contain the object. This categorization is useful for other purposes |
1256 | | // as well. Part numbers refer to version 1.4 of the PDF spec. |
1257 | | |
1258 | | // Parts 1, 3, 5, 10, and 11 don't contain any objects from the original file (except the |
1259 | | // trailer dictionary in part 11). |
1260 | | |
1261 | | // Part 4 is the document catalog (root) and the following root keys: /ViewerPreferences, |
1262 | | // /PageMode, /Threads, /OpenAction, /AcroForm, /Encrypt. Note that Thread information |
1263 | | // dictionaries are supposed to appear in part 9, but we are disregarding that recommendation |
1264 | | // for now. |
1265 | | |
1266 | | // Part 6 is the first page section. It includes all remaining objects referenced by the first |
1267 | | // page including shared objects but not including thumbnails. Additionally, if /PageMode is |
1268 | | // /Outlines, then information from /Outlines also appears here. |
1269 | | |
1270 | | // Part 7 contains remaining objects private to pages other than the first page. |
1271 | | |
1272 | | // Part 8 contains all remaining shared objects except those that are shared only within |
1273 | | // thumbnails. |
1274 | | |
1275 | | // Part 9 contains all remaining objects. |
1276 | | |
1277 | | // We sort objects into the following categories: |
1278 | | |
1279 | | // * open_document: part 4 |
1280 | | |
1281 | | // * first_page_private: part 6 |
1282 | | |
1283 | | // * first_page_shared: part 6 |
1284 | | |
1285 | | // * other_page_private: part 7 |
1286 | | |
1287 | | // * other_page_shared: part 8 |
1288 | | |
1289 | | // * thumbnail_private: part 9 |
1290 | | |
1291 | | // * thumbnail_shared: part 9 |
1292 | | |
1293 | | // * other: part 9 |
1294 | | |
1295 | | // * outlines: part 6 or 9 |
1296 | | |
1297 | 9.56k | part4_.clear(); |
1298 | 9.56k | part6_.clear(); |
1299 | 9.56k | part7_.clear(); |
1300 | 9.56k | part8_.clear(); |
1301 | 9.56k | part9_.clear(); |
1302 | 9.56k | c_linp_ = LinParameters(); |
1303 | 9.56k | c_page_offset_data_ = CHPageOffset(); |
1304 | 9.56k | c_shared_object_data_ = CHSharedObject(); |
1305 | 9.56k | c_outline_data_ = HGeneric(); |
1306 | | |
1307 | 9.56k | QPDFObjectHandle root = qpdf.getRoot(); |
1308 | 9.56k | bool outlines_in_first_page = false; |
1309 | 9.56k | QPDFObjectHandle pagemode = root.getKey("/PageMode"); |
1310 | 9.56k | QTC::TC("qpdf", "QPDF categorize pagemode present", pagemode.isName() ? 1 : 0); |
1311 | 9.56k | if (pagemode.isName()) { |
1312 | 183 | if (pagemode.getName() == "/UseOutlines") { |
1313 | 132 | if (root.hasKey("/Outlines")) { |
1314 | 47 | outlines_in_first_page = true; |
1315 | 85 | } else { |
1316 | 85 | QTC::TC("qpdf", "QPDF UseOutlines but no Outlines"); |
1317 | 85 | } |
1318 | 132 | } |
1319 | 183 | QTC::TC("qpdf", "QPDF categorize pagemode outlines", outlines_in_first_page ? 1 : 0); |
1320 | 183 | } |
1321 | | |
1322 | 9.56k | std::set<std::string> open_document_keys; |
1323 | 9.56k | open_document_keys.insert("/ViewerPreferences"); |
1324 | 9.56k | open_document_keys.insert("/PageMode"); |
1325 | 9.56k | open_document_keys.insert("/Threads"); |
1326 | 9.56k | open_document_keys.insert("/OpenAction"); |
1327 | 9.56k | open_document_keys.insert("/AcroForm"); |
1328 | | |
1329 | 9.56k | std::set<QPDFObjGen> lc_open_document; |
1330 | 9.56k | std::set<QPDFObjGen> lc_first_page_private; |
1331 | 9.56k | std::set<QPDFObjGen> lc_first_page_shared; |
1332 | 9.56k | std::set<QPDFObjGen> lc_other_page_private; |
1333 | 9.56k | std::set<QPDFObjGen> lc_other_page_shared; |
1334 | 9.56k | std::set<QPDFObjGen> lc_thumbnail_private; |
1335 | 9.56k | std::set<QPDFObjGen> lc_thumbnail_shared; |
1336 | 9.56k | std::set<QPDFObjGen> lc_other; |
1337 | 9.56k | std::set<QPDFObjGen> lc_outlines; |
1338 | 9.56k | std::set<QPDFObjGen> lc_root; |
1339 | | |
1340 | 130k | for (auto& [og, ous]: object_to_obj_users_) { |
1341 | 130k | bool in_open_document = false; |
1342 | 130k | bool in_first_page = false; |
1343 | 130k | int other_pages = 0; |
1344 | 130k | int thumbs = 0; |
1345 | 130k | int others = 0; |
1346 | 130k | bool in_outlines = false; |
1347 | 130k | bool is_root = false; |
1348 | | |
1349 | 252k | for (auto const& ou: ous) { |
1350 | 252k | switch (ou.ou_type) { |
1351 | 24.2k | case ObjUser::ou_trailer_key: |
1352 | 24.2k | if (ou.key == "/Encrypt") { |
1353 | 935 | in_open_document = true; |
1354 | 23.2k | } else { |
1355 | 23.2k | ++others; |
1356 | 23.2k | } |
1357 | 24.2k | break; |
1358 | | |
1359 | 4.92k | case ObjUser::ou_thumb: |
1360 | 4.92k | ++thumbs; |
1361 | 4.92k | break; |
1362 | | |
1363 | 67.1k | case ObjUser::ou_root_key: |
1364 | 67.1k | if (open_document_keys.contains(ou.key)) { |
1365 | 12.6k | in_open_document = true; |
1366 | 54.5k | } else if (ou.key == "/Outlines") { |
1367 | 1.75k | in_outlines = true; |
1368 | 52.7k | } else { |
1369 | 52.7k | ++others; |
1370 | 52.7k | } |
1371 | 67.1k | break; |
1372 | | |
1373 | 147k | case ObjUser::ou_page: |
1374 | 147k | if (ou.pageno == 0) { |
1375 | 63.6k | in_first_page = true; |
1376 | 83.4k | } else { |
1377 | 83.4k | ++other_pages; |
1378 | 83.4k | } |
1379 | 147k | break; |
1380 | | |
1381 | 9.56k | case ObjUser::ou_root: |
1382 | 9.56k | is_root = true; |
1383 | 9.56k | break; |
1384 | 252k | } |
1385 | 252k | } |
1386 | | |
1387 | 130k | if (is_root) { |
1388 | 9.56k | lc_root.insert(og); |
1389 | 121k | } else if (in_outlines) { |
1390 | 1.74k | lc_outlines.insert(og); |
1391 | 119k | } else if (in_open_document) { |
1392 | 13.4k | lc_open_document.insert(og); |
1393 | 106k | } else if ((in_first_page) && (others == 0) && (other_pages == 0) && (thumbs == 0)) { |
1394 | 48.4k | lc_first_page_private.insert(og); |
1395 | 57.6k | } else if (in_first_page) { |
1396 | 11.2k | lc_first_page_shared.insert(og); |
1397 | 46.4k | } else if ((other_pages == 1) && (others == 0) && (thumbs == 0)) { |
1398 | 12.7k | lc_other_page_private.insert(og); |
1399 | 33.7k | } else if (other_pages > 1) { |
1400 | 3.29k | lc_other_page_shared.insert(og); |
1401 | 30.4k | } else if ((thumbs == 1) && (others == 0)) { |
1402 | 1.49k | lc_thumbnail_private.insert(og); |
1403 | 28.9k | } else if (thumbs > 1) { |
1404 | 801 | lc_thumbnail_shared.insert(og); |
1405 | 28.1k | } else { |
1406 | 28.1k | lc_other.insert(og); |
1407 | 28.1k | } |
1408 | 130k | } |
1409 | | |
1410 | | // Generate ordering for objects in the output file. Sometimes we just dump right from a set |
1411 | | // into a vector. Rather than optimizing this by going straight into the vector, we'll leave |
1412 | | // these phases separate for now. That way, this section can be concerned only with ordering, |
1413 | | // and the above section can be considered only with categorization. Note that sets of |
1414 | | // QPDFObjGens are sorted by QPDFObjGen. In a linearized file, objects appear in sequence with |
1415 | | // the possible exception of hints tables which we won't see here anyway. That means that |
1416 | | // running calculateLinearizationData() on a linearized file should give results identical to |
1417 | | // the original file ordering. |
1418 | | |
1419 | | // We seem to traverse the page tree a lot in this code, but we can address this for a future |
1420 | | // code optimization if necessary. Premature optimization is the root of all evil. |
1421 | 9.56k | std::vector<QPDFObjectHandle> uc_pages; |
1422 | 9.56k | { // local scope |
1423 | | // Map all page objects to the containing object stream. This should be a no-op in a |
1424 | | // properly linearized file. |
1425 | 13.7k | for (auto oh: pages) { |
1426 | 13.7k | uc_pages.emplace_back(getUncompressedObject(oh, object_stream_data)); |
1427 | 13.7k | } |
1428 | 9.56k | } |
1429 | 9.56k | size_t npages = pages.size(); |
1430 | | |
1431 | | // We will be initializing some values of the computed hint tables. Specifically, we can |
1432 | | // initialize any items that deal with object numbers or counts but not any items that deal with |
1433 | | // lengths or offsets. The code that writes linearized files will have to fill in these values |
1434 | | // during the first pass. The validation code can compute them relatively easily given the rest |
1435 | | // of the information. |
1436 | | |
1437 | | // npages is the size of the existing pages vector, which has been created by traversing the |
1438 | | // pages tree, and as such is a reasonable size. |
1439 | 9.56k | c_linp_.npages = npages; |
1440 | 9.56k | c_page_offset_data_.entries = std::vector<CHPageOffsetEntry>(npages); |
1441 | | |
1442 | | // Part 4: open document objects. We don't care about the order. |
1443 | | |
1444 | 9.56k | no_ci_stop_if( |
1445 | 9.56k | lc_root.size() != 1, "found other than one root while calculating linearization data" // |
1446 | 9.56k | ); |
1447 | | |
1448 | 9.56k | part4_.emplace_back(qpdf.getObject(*(lc_root.begin()))); |
1449 | 13.4k | for (auto const& og: lc_open_document) { |
1450 | 13.4k | part4_.emplace_back(qpdf.getObject(og)); |
1451 | 13.4k | } |
1452 | | |
1453 | | // Part 6: first page objects. Note: implementation note 124 states that Acrobat always treats |
1454 | | // page 0 as the first page for linearization regardless of /OpenAction. pdlin doesn't provide |
1455 | | // any option to set this and also disregards /OpenAction. We will do the same. |
1456 | | |
1457 | | // First, place the actual first page object itself. |
1458 | 9.56k | no_ci_stop_if( |
1459 | 9.56k | pages.empty(), "no pages found while calculating linearization data" // |
1460 | 9.56k | ); |
1461 | 9.56k | QPDFObjGen first_page_og(uc_pages.at(0).getObjGen()); |
1462 | 9.56k | no_ci_stop_if( |
1463 | 9.56k | !lc_first_page_private.erase(first_page_og), "unable to linearize first page" // |
1464 | 9.56k | ); |
1465 | 9.56k | c_linp_.first_page_object = uc_pages.at(0).getObjectID(); |
1466 | 9.56k | part6_.emplace_back(uc_pages.at(0)); |
1467 | | |
1468 | | // The PDF spec "recommends" an order for the rest of the objects, but we are going to disregard |
1469 | | // it except to the extent that it groups private and shared objects contiguously for the sake |
1470 | | // of hint tables. |
1471 | | |
1472 | 39.3k | for (auto const& og: lc_first_page_private) { |
1473 | 39.3k | part6_.emplace_back(qpdf.getObject(og)); |
1474 | 39.3k | } |
1475 | | |
1476 | 9.56k | for (auto const& og: lc_first_page_shared) { |
1477 | 8.74k | part6_.emplace_back(qpdf.getObject(og)); |
1478 | 8.74k | } |
1479 | | |
1480 | | // Place the outline dictionary if it goes in the first page section. |
1481 | 9.56k | if (outlines_in_first_page) { |
1482 | 46 | pushOutlinesToPart(part6_, lc_outlines, object_stream_data); |
1483 | 46 | } |
1484 | | |
1485 | | // Fill in page offset hint table information for the first page. The PDF spec says that |
1486 | | // nshared_objects should be zero for the first page. pdlin does not appear to obey this, but |
1487 | | // it fills in garbage values for all the shared object identifiers on the first page. |
1488 | | |
1489 | 9.56k | c_page_offset_data_.entries.at(0).nobjects = toI(part6_.size()); |
1490 | | |
1491 | | // Part 7: other pages' private objects |
1492 | | |
1493 | | // For each page in order: |
1494 | 13.5k | for (size_t i = 1; i < npages; ++i) { |
1495 | | // Place this page's page object |
1496 | | |
1497 | 4.03k | QPDFObjGen page_og(uc_pages.at(i).getObjGen()); |
1498 | 4.03k | no_ci_stop_if( |
1499 | 4.03k | !lc_other_page_private.erase(page_og), |
1500 | 4.03k | "unable to linearize page " + std::to_string(i) // |
1501 | 4.03k | ); |
1502 | | |
1503 | 4.03k | part7_.emplace_back(uc_pages.at(i)); |
1504 | | |
1505 | | // Place all non-shared objects referenced by this page, updating the page object count for |
1506 | | // the hint table. |
1507 | | |
1508 | 4.03k | c_page_offset_data_.entries.at(i).nobjects = 1; |
1509 | | |
1510 | 4.03k | ObjUser ou(ObjUser::ou_page, i); |
1511 | 4.03k | no_ci_stop_if( |
1512 | 4.03k | !obj_user_to_objects_.contains(ou), |
1513 | 4.03k | "found unreferenced page while calculating linearization data" // |
1514 | 4.03k | ); |
1515 | | |
1516 | 82.5k | for (auto const& og: obj_user_to_objects_[ou]) { |
1517 | 82.5k | if (lc_other_page_private.erase(og)) { |
1518 | 8.31k | part7_.emplace_back(qpdf.getObject(og)); |
1519 | 8.31k | ++c_page_offset_data_.entries.at(i).nobjects; |
1520 | 8.31k | } |
1521 | 82.5k | } |
1522 | 4.03k | } |
1523 | | // That should have covered all part7 objects. |
1524 | 9.56k | util::assertion( |
1525 | 9.56k | lc_other_page_private.empty(), |
1526 | 9.56k | "INTERNAL ERROR: QPDF::calculateLinearizationData: lc_other_page_private is not empty " |
1527 | 9.56k | "after generation of part7" // |
1528 | 9.56k | ); |
1529 | | |
1530 | | // Part 8: other pages' shared objects |
1531 | | |
1532 | | // Order is unimportant. |
1533 | 9.56k | for (auto const& og: lc_other_page_shared) { |
1534 | 3.29k | part8_.emplace_back(qpdf.getObject(og)); |
1535 | 3.29k | } |
1536 | | |
1537 | | // Part 9: other objects |
1538 | | |
1539 | | // The PDF specification makes recommendations on ordering here. We follow them only to a |
1540 | | // limited extent. Specifically, we put the pages tree first, then private thumbnail objects in |
1541 | | // page order, then shared thumbnail objects, and then outlines (unless in part 6). After that, |
1542 | | // we throw all remaining objects in arbitrary order. |
1543 | | |
1544 | | // Place the pages tree. |
1545 | 9.56k | auto& pages_ogs = obj_user_to_objects_[{ObjUser::ou_root_key, "/Pages"}]; |
1546 | 9.56k | no_ci_stop_if( |
1547 | 9.56k | pages_ogs.empty(), "found empty pages tree while calculating linearization data" // |
1548 | 9.56k | ); |
1549 | 13.6k | for (auto const& og: pages_ogs) { |
1550 | 13.6k | if (lc_other.erase(og)) { |
1551 | 10.8k | part9_.emplace_back(qpdf.getObject(og)); |
1552 | 10.8k | } |
1553 | 13.6k | } |
1554 | | |
1555 | | // Place private thumbnail images in page order. Slightly more information would be required if |
1556 | | // we were going to bother with thumbnail hint tables. |
1557 | 22.4k | for (size_t i = 0; i < npages; ++i) { |
1558 | 12.9k | QPDFObjectHandle thumb = uc_pages.at(i).getKey("/Thumb"); |
1559 | 12.9k | thumb = getUncompressedObject(thumb, object_stream_data); |
1560 | 12.9k | QPDFObjGen thumb_og(thumb.getObjGen()); |
1561 | | // Output the thumbnail itself |
1562 | 12.9k | if (lc_thumbnail_private.erase(thumb_og) && !thumb.null()) { |
1563 | 276 | part9_.emplace_back(thumb); |
1564 | 12.6k | } else { |
1565 | | // No internal error this time...there's nothing to stop this object from having |
1566 | | // been referred to somewhere else outside of a page's /Thumb, and if it had been, |
1567 | | // there's nothing to prevent it from having been in some set other than |
1568 | | // lc_thumbnail_private. |
1569 | 12.6k | } |
1570 | 12.9k | for (auto const& og: obj_user_to_objects_[{ObjUser::ou_thumb, i}]) { |
1571 | 4.27k | if (lc_thumbnail_private.erase(og)) { |
1572 | 1.04k | part9_.emplace_back(qpdf.getObject(og)); |
1573 | 1.04k | } |
1574 | 4.27k | } |
1575 | 12.9k | } |
1576 | 9.56k | util::assertion( |
1577 | 9.56k | lc_thumbnail_private.empty(), |
1578 | 9.56k | "INTERNAL ERROR: QPDF::calculateLinearizationData: lc_thumbnail_private not " |
1579 | 9.56k | "empty after placing thumbnails" // |
1580 | 9.56k | ); |
1581 | | |
1582 | | // Place shared thumbnail objects |
1583 | 9.56k | for (auto const& og: lc_thumbnail_shared) { |
1584 | 766 | part9_.emplace_back(qpdf.getObject(og)); |
1585 | 766 | } |
1586 | | |
1587 | | // Place outlines unless in first page |
1588 | 9.56k | if (!outlines_in_first_page) { |
1589 | 9.03k | pushOutlinesToPart(part9_, lc_outlines, object_stream_data); |
1590 | 9.03k | } |
1591 | | |
1592 | | // Place all remaining objects |
1593 | 16.6k | for (auto const& og: lc_other) { |
1594 | 16.6k | part9_.emplace_back(qpdf.getObject(og)); |
1595 | 16.6k | } |
1596 | | |
1597 | | // Make sure we got everything exactly once. |
1598 | | |
1599 | 9.56k | size_t num_placed = |
1600 | 9.56k | part4_.size() + part6_.size() + part7_.size() + part8_.size() + part9_.size(); |
1601 | 9.56k | size_t num_wanted = object_to_obj_users_.size(); |
1602 | 9.56k | no_ci_stop_if( |
1603 | | // This can happen with damaged files, e.g. if the root is part of the the pages tree. |
1604 | 9.56k | num_placed != num_wanted, |
1605 | 9.56k | "QPDF::calculateLinearizationData: wrong number of objects placed (num_placed = " + |
1606 | 9.56k | std::to_string(num_placed) + "; number of objects: " + std::to_string(num_wanted) + |
1607 | 9.56k | "\nIf the file did not generate any other warnings please report this as a bug." // |
1608 | 9.56k | ); |
1609 | | |
1610 | | // Calculate shared object hint table information including references to shared objects from |
1611 | | // page offset hint data. |
1612 | | |
1613 | | // The shared object hint table consists of all part 6 (whether shared or not) in order followed |
1614 | | // by all part 8 objects in order. Add the objects to shared object data keeping a map of |
1615 | | // object number to index. Then populate the shared object information for the pages. |
1616 | | |
1617 | | // Note that two objects never have the same object number, so we can map from object number |
1618 | | // only without regards to generation. |
1619 | 9.56k | std::map<int, int> obj_to_index; |
1620 | | |
1621 | 9.56k | c_shared_object_data_.nshared_first_page = toI(part6_.size()); |
1622 | 9.56k | c_shared_object_data_.nshared_total = |
1623 | 9.56k | c_shared_object_data_.nshared_first_page + toI(part8_.size()); |
1624 | | |
1625 | 9.56k | std::vector<CHSharedObjectEntry>& shared = c_shared_object_data_.entries; |
1626 | 57.5k | for (auto& oh: part6_) { |
1627 | 57.5k | int obj = oh.getObjectID(); |
1628 | 57.5k | obj_to_index[obj] = toI(shared.size()); |
1629 | 57.5k | shared.emplace_back(obj); |
1630 | 57.5k | } |
1631 | 9.56k | QTC::TC("qpdf", "QPDF lin part 8 empty", part8_.empty() ? 1 : 0); |
1632 | 9.56k | if (!part8_.empty()) { |
1633 | 101 | c_shared_object_data_.first_shared_obj = part8_.at(0).getObjectID(); |
1634 | 3.29k | for (auto& oh: part8_) { |
1635 | 3.29k | int obj = oh.getObjectID(); |
1636 | 3.29k | obj_to_index[obj] = toI(shared.size()); |
1637 | 3.29k | shared.emplace_back(obj); |
1638 | 3.29k | } |
1639 | 101 | } |
1640 | 9.56k | no_ci_stop_if( |
1641 | 9.56k | std::cmp_not_equal( |
1642 | 9.56k | c_shared_object_data_.nshared_total, c_shared_object_data_.entries.size()), |
1643 | 9.56k | "shared object hint table has wrong number of entries" // |
1644 | 9.56k | ); |
1645 | | |
1646 | | // Now compute the list of shared objects for each page after the first page. |
1647 | | |
1648 | 13.3k | for (size_t i = 1; i < npages; ++i) { |
1649 | 3.82k | CHPageOffsetEntry& pe = c_page_offset_data_.entries.at(i); |
1650 | 3.82k | ObjUser ou(ObjUser::ou_page, i); |
1651 | 3.82k | no_ci_stop_if( |
1652 | 3.82k | !obj_user_to_objects_.contains(ou), |
1653 | 3.82k | "found unreferenced page while calculating linearization data" // |
1654 | 3.82k | ); |
1655 | | |
1656 | 82.0k | for (auto const& og: obj_user_to_objects_[ou]) { |
1657 | 82.0k | if (object_to_obj_users_[og].size() > 1 && obj_to_index.contains(og.getObj())) { |
1658 | 67.9k | int idx = obj_to_index[og.getObj()]; |
1659 | 67.9k | ++pe.nshared_objects; |
1660 | 67.9k | pe.shared_identifiers.push_back(idx); |
1661 | 67.9k | } |
1662 | 82.0k | } |
1663 | 3.82k | } |
1664 | 9.56k | } Unexecuted instantiation: void QPDF::Doc::Linearization::calculateLinearizationData<std::__1::map<int, int, std::__1::less<int>, std::__1::allocator<std::__1::pair<int const, int> > > >(std::__1::map<int, int, std::__1::less<int>, std::__1::allocator<std::__1::pair<int const, int> > > const&) void QPDF::Doc::Linearization::calculateLinearizationData<QPDFWriter::ObjTable>(QPDFWriter::ObjTable const&) Line | Count | Source | 1241 | 9.56k | { | 1242 | | // This function calculates the ordering of objects, divides them into the appropriate parts, | 1243 | | // and computes some values for the linearization parameter dictionary and hint tables. The | 1244 | | // file must be optimized (via calling optimize()) prior to calling this function. Note that | 1245 | | // actual offsets and lengths are not computed here, but anything related to object ordering is. | 1246 | | | 1247 | 9.56k | util::assertion( | 1248 | 9.56k | !object_to_obj_users_.empty(), | 1249 | 9.56k | "INTERNAL ERROR: QPDF::calculateLinearizationData called before optimize()" // | 1250 | 9.56k | ); | 1251 | | // Note that we can't call optimize here because we don't know whether it should be called | 1252 | | // with or without allow changes. | 1253 | | | 1254 | | // Separate objects into the categories sufficient for us to determine which part of the | 1255 | | // linearized file should contain the object. This categorization is useful for other purposes | 1256 | | // as well. Part numbers refer to version 1.4 of the PDF spec. | 1257 | | | 1258 | | // Parts 1, 3, 5, 10, and 11 don't contain any objects from the original file (except the | 1259 | | // trailer dictionary in part 11). | 1260 | | | 1261 | | // Part 4 is the document catalog (root) and the following root keys: /ViewerPreferences, | 1262 | | // /PageMode, /Threads, /OpenAction, /AcroForm, /Encrypt. Note that Thread information | 1263 | | // dictionaries are supposed to appear in part 9, but we are disregarding that recommendation | 1264 | | // for now. | 1265 | | | 1266 | | // Part 6 is the first page section. It includes all remaining objects referenced by the first | 1267 | | // page including shared objects but not including thumbnails. Additionally, if /PageMode is | 1268 | | // /Outlines, then information from /Outlines also appears here. | 1269 | | | 1270 | | // Part 7 contains remaining objects private to pages other than the first page. | 1271 | | | 1272 | | // Part 8 contains all remaining shared objects except those that are shared only within | 1273 | | // thumbnails. | 1274 | | | 1275 | | // Part 9 contains all remaining objects. | 1276 | | | 1277 | | // We sort objects into the following categories: | 1278 | | | 1279 | | // * open_document: part 4 | 1280 | | | 1281 | | // * first_page_private: part 6 | 1282 | | | 1283 | | // * first_page_shared: part 6 | 1284 | | | 1285 | | // * other_page_private: part 7 | 1286 | | | 1287 | | // * other_page_shared: part 8 | 1288 | | | 1289 | | // * thumbnail_private: part 9 | 1290 | | | 1291 | | // * thumbnail_shared: part 9 | 1292 | | | 1293 | | // * other: part 9 | 1294 | | | 1295 | | // * outlines: part 6 or 9 | 1296 | | | 1297 | 9.56k | part4_.clear(); | 1298 | 9.56k | part6_.clear(); | 1299 | 9.56k | part7_.clear(); | 1300 | 9.56k | part8_.clear(); | 1301 | 9.56k | part9_.clear(); | 1302 | 9.56k | c_linp_ = LinParameters(); | 1303 | 9.56k | c_page_offset_data_ = CHPageOffset(); | 1304 | 9.56k | c_shared_object_data_ = CHSharedObject(); | 1305 | 9.56k | c_outline_data_ = HGeneric(); | 1306 | | | 1307 | 9.56k | QPDFObjectHandle root = qpdf.getRoot(); | 1308 | 9.56k | bool outlines_in_first_page = false; | 1309 | 9.56k | QPDFObjectHandle pagemode = root.getKey("/PageMode"); | 1310 | 9.56k | QTC::TC("qpdf", "QPDF categorize pagemode present", pagemode.isName() ? 1 : 0); | 1311 | 9.56k | if (pagemode.isName()) { | 1312 | 183 | if (pagemode.getName() == "/UseOutlines") { | 1313 | 132 | if (root.hasKey("/Outlines")) { | 1314 | 47 | outlines_in_first_page = true; | 1315 | 85 | } else { | 1316 | 85 | QTC::TC("qpdf", "QPDF UseOutlines but no Outlines"); | 1317 | 85 | } | 1318 | 132 | } | 1319 | 183 | QTC::TC("qpdf", "QPDF categorize pagemode outlines", outlines_in_first_page ? 1 : 0); | 1320 | 183 | } | 1321 | | | 1322 | 9.56k | std::set<std::string> open_document_keys; | 1323 | 9.56k | open_document_keys.insert("/ViewerPreferences"); | 1324 | 9.56k | open_document_keys.insert("/PageMode"); | 1325 | 9.56k | open_document_keys.insert("/Threads"); | 1326 | 9.56k | open_document_keys.insert("/OpenAction"); | 1327 | 9.56k | open_document_keys.insert("/AcroForm"); | 1328 | | | 1329 | 9.56k | std::set<QPDFObjGen> lc_open_document; | 1330 | 9.56k | std::set<QPDFObjGen> lc_first_page_private; | 1331 | 9.56k | std::set<QPDFObjGen> lc_first_page_shared; | 1332 | 9.56k | std::set<QPDFObjGen> lc_other_page_private; | 1333 | 9.56k | std::set<QPDFObjGen> lc_other_page_shared; | 1334 | 9.56k | std::set<QPDFObjGen> lc_thumbnail_private; | 1335 | 9.56k | std::set<QPDFObjGen> lc_thumbnail_shared; | 1336 | 9.56k | std::set<QPDFObjGen> lc_other; | 1337 | 9.56k | std::set<QPDFObjGen> lc_outlines; | 1338 | 9.56k | std::set<QPDFObjGen> lc_root; | 1339 | | | 1340 | 130k | for (auto& [og, ous]: object_to_obj_users_) { | 1341 | 130k | bool in_open_document = false; | 1342 | 130k | bool in_first_page = false; | 1343 | 130k | int other_pages = 0; | 1344 | 130k | int thumbs = 0; | 1345 | 130k | int others = 0; | 1346 | 130k | bool in_outlines = false; | 1347 | 130k | bool is_root = false; | 1348 | | | 1349 | 252k | for (auto const& ou: ous) { | 1350 | 252k | switch (ou.ou_type) { | 1351 | 24.2k | case ObjUser::ou_trailer_key: | 1352 | 24.2k | if (ou.key == "/Encrypt") { | 1353 | 935 | in_open_document = true; | 1354 | 23.2k | } else { | 1355 | 23.2k | ++others; | 1356 | 23.2k | } | 1357 | 24.2k | break; | 1358 | | | 1359 | 4.92k | case ObjUser::ou_thumb: | 1360 | 4.92k | ++thumbs; | 1361 | 4.92k | break; | 1362 | | | 1363 | 67.1k | case ObjUser::ou_root_key: | 1364 | 67.1k | if (open_document_keys.contains(ou.key)) { | 1365 | 12.6k | in_open_document = true; | 1366 | 54.5k | } else if (ou.key == "/Outlines") { | 1367 | 1.75k | in_outlines = true; | 1368 | 52.7k | } else { | 1369 | 52.7k | ++others; | 1370 | 52.7k | } | 1371 | 67.1k | break; | 1372 | | | 1373 | 147k | case ObjUser::ou_page: | 1374 | 147k | if (ou.pageno == 0) { | 1375 | 63.6k | in_first_page = true; | 1376 | 83.4k | } else { | 1377 | 83.4k | ++other_pages; | 1378 | 83.4k | } | 1379 | 147k | break; | 1380 | | | 1381 | 9.56k | case ObjUser::ou_root: | 1382 | 9.56k | is_root = true; | 1383 | 9.56k | break; | 1384 | 252k | } | 1385 | 252k | } | 1386 | | | 1387 | 130k | if (is_root) { | 1388 | 9.56k | lc_root.insert(og); | 1389 | 121k | } else if (in_outlines) { | 1390 | 1.74k | lc_outlines.insert(og); | 1391 | 119k | } else if (in_open_document) { | 1392 | 13.4k | lc_open_document.insert(og); | 1393 | 106k | } else if ((in_first_page) && (others == 0) && (other_pages == 0) && (thumbs == 0)) { | 1394 | 48.4k | lc_first_page_private.insert(og); | 1395 | 57.6k | } else if (in_first_page) { | 1396 | 11.2k | lc_first_page_shared.insert(og); | 1397 | 46.4k | } else if ((other_pages == 1) && (others == 0) && (thumbs == 0)) { | 1398 | 12.7k | lc_other_page_private.insert(og); | 1399 | 33.7k | } else if (other_pages > 1) { | 1400 | 3.29k | lc_other_page_shared.insert(og); | 1401 | 30.4k | } else if ((thumbs == 1) && (others == 0)) { | 1402 | 1.49k | lc_thumbnail_private.insert(og); | 1403 | 28.9k | } else if (thumbs > 1) { | 1404 | 801 | lc_thumbnail_shared.insert(og); | 1405 | 28.1k | } else { | 1406 | 28.1k | lc_other.insert(og); | 1407 | 28.1k | } | 1408 | 130k | } | 1409 | | | 1410 | | // Generate ordering for objects in the output file. Sometimes we just dump right from a set | 1411 | | // into a vector. Rather than optimizing this by going straight into the vector, we'll leave | 1412 | | // these phases separate for now. That way, this section can be concerned only with ordering, | 1413 | | // and the above section can be considered only with categorization. Note that sets of | 1414 | | // QPDFObjGens are sorted by QPDFObjGen. In a linearized file, objects appear in sequence with | 1415 | | // the possible exception of hints tables which we won't see here anyway. That means that | 1416 | | // running calculateLinearizationData() on a linearized file should give results identical to | 1417 | | // the original file ordering. | 1418 | | | 1419 | | // We seem to traverse the page tree a lot in this code, but we can address this for a future | 1420 | | // code optimization if necessary. Premature optimization is the root of all evil. | 1421 | 9.56k | std::vector<QPDFObjectHandle> uc_pages; | 1422 | 9.56k | { // local scope | 1423 | | // Map all page objects to the containing object stream. This should be a no-op in a | 1424 | | // properly linearized file. | 1425 | 13.7k | for (auto oh: pages) { | 1426 | 13.7k | uc_pages.emplace_back(getUncompressedObject(oh, object_stream_data)); | 1427 | 13.7k | } | 1428 | 9.56k | } | 1429 | 9.56k | size_t npages = pages.size(); | 1430 | | | 1431 | | // We will be initializing some values of the computed hint tables. Specifically, we can | 1432 | | // initialize any items that deal with object numbers or counts but not any items that deal with | 1433 | | // lengths or offsets. The code that writes linearized files will have to fill in these values | 1434 | | // during the first pass. The validation code can compute them relatively easily given the rest | 1435 | | // of the information. | 1436 | | | 1437 | | // npages is the size of the existing pages vector, which has been created by traversing the | 1438 | | // pages tree, and as such is a reasonable size. | 1439 | 9.56k | c_linp_.npages = npages; | 1440 | 9.56k | c_page_offset_data_.entries = std::vector<CHPageOffsetEntry>(npages); | 1441 | | | 1442 | | // Part 4: open document objects. We don't care about the order. | 1443 | | | 1444 | 9.56k | no_ci_stop_if( | 1445 | 9.56k | lc_root.size() != 1, "found other than one root while calculating linearization data" // | 1446 | 9.56k | ); | 1447 | | | 1448 | 9.56k | part4_.emplace_back(qpdf.getObject(*(lc_root.begin()))); | 1449 | 13.4k | for (auto const& og: lc_open_document) { | 1450 | 13.4k | part4_.emplace_back(qpdf.getObject(og)); | 1451 | 13.4k | } | 1452 | | | 1453 | | // Part 6: first page objects. Note: implementation note 124 states that Acrobat always treats | 1454 | | // page 0 as the first page for linearization regardless of /OpenAction. pdlin doesn't provide | 1455 | | // any option to set this and also disregards /OpenAction. We will do the same. | 1456 | | | 1457 | | // First, place the actual first page object itself. | 1458 | 9.56k | no_ci_stop_if( | 1459 | 9.56k | pages.empty(), "no pages found while calculating linearization data" // | 1460 | 9.56k | ); | 1461 | 9.56k | QPDFObjGen first_page_og(uc_pages.at(0).getObjGen()); | 1462 | 9.56k | no_ci_stop_if( | 1463 | 9.56k | !lc_first_page_private.erase(first_page_og), "unable to linearize first page" // | 1464 | 9.56k | ); | 1465 | 9.56k | c_linp_.first_page_object = uc_pages.at(0).getObjectID(); | 1466 | 9.56k | part6_.emplace_back(uc_pages.at(0)); | 1467 | | | 1468 | | // The PDF spec "recommends" an order for the rest of the objects, but we are going to disregard | 1469 | | // it except to the extent that it groups private and shared objects contiguously for the sake | 1470 | | // of hint tables. | 1471 | | | 1472 | 39.3k | for (auto const& og: lc_first_page_private) { | 1473 | 39.3k | part6_.emplace_back(qpdf.getObject(og)); | 1474 | 39.3k | } | 1475 | | | 1476 | 9.56k | for (auto const& og: lc_first_page_shared) { | 1477 | 8.74k | part6_.emplace_back(qpdf.getObject(og)); | 1478 | 8.74k | } | 1479 | | | 1480 | | // Place the outline dictionary if it goes in the first page section. | 1481 | 9.56k | if (outlines_in_first_page) { | 1482 | 46 | pushOutlinesToPart(part6_, lc_outlines, object_stream_data); | 1483 | 46 | } | 1484 | | | 1485 | | // Fill in page offset hint table information for the first page. The PDF spec says that | 1486 | | // nshared_objects should be zero for the first page. pdlin does not appear to obey this, but | 1487 | | // it fills in garbage values for all the shared object identifiers on the first page. | 1488 | | | 1489 | 9.56k | c_page_offset_data_.entries.at(0).nobjects = toI(part6_.size()); | 1490 | | | 1491 | | // Part 7: other pages' private objects | 1492 | | | 1493 | | // For each page in order: | 1494 | 13.5k | for (size_t i = 1; i < npages; ++i) { | 1495 | | // Place this page's page object | 1496 | | | 1497 | 4.03k | QPDFObjGen page_og(uc_pages.at(i).getObjGen()); | 1498 | 4.03k | no_ci_stop_if( | 1499 | 4.03k | !lc_other_page_private.erase(page_og), | 1500 | 4.03k | "unable to linearize page " + std::to_string(i) // | 1501 | 4.03k | ); | 1502 | | | 1503 | 4.03k | part7_.emplace_back(uc_pages.at(i)); | 1504 | | | 1505 | | // Place all non-shared objects referenced by this page, updating the page object count for | 1506 | | // the hint table. | 1507 | | | 1508 | 4.03k | c_page_offset_data_.entries.at(i).nobjects = 1; | 1509 | | | 1510 | 4.03k | ObjUser ou(ObjUser::ou_page, i); | 1511 | 4.03k | no_ci_stop_if( | 1512 | 4.03k | !obj_user_to_objects_.contains(ou), | 1513 | 4.03k | "found unreferenced page while calculating linearization data" // | 1514 | 4.03k | ); | 1515 | | | 1516 | 82.5k | for (auto const& og: obj_user_to_objects_[ou]) { | 1517 | 82.5k | if (lc_other_page_private.erase(og)) { | 1518 | 8.31k | part7_.emplace_back(qpdf.getObject(og)); | 1519 | 8.31k | ++c_page_offset_data_.entries.at(i).nobjects; | 1520 | 8.31k | } | 1521 | 82.5k | } | 1522 | 4.03k | } | 1523 | | // That should have covered all part7 objects. | 1524 | 9.56k | util::assertion( | 1525 | 9.56k | lc_other_page_private.empty(), | 1526 | 9.56k | "INTERNAL ERROR: QPDF::calculateLinearizationData: lc_other_page_private is not empty " | 1527 | 9.56k | "after generation of part7" // | 1528 | 9.56k | ); | 1529 | | | 1530 | | // Part 8: other pages' shared objects | 1531 | | | 1532 | | // Order is unimportant. | 1533 | 9.56k | for (auto const& og: lc_other_page_shared) { | 1534 | 3.29k | part8_.emplace_back(qpdf.getObject(og)); | 1535 | 3.29k | } | 1536 | | | 1537 | | // Part 9: other objects | 1538 | | | 1539 | | // The PDF specification makes recommendations on ordering here. We follow them only to a | 1540 | | // limited extent. Specifically, we put the pages tree first, then private thumbnail objects in | 1541 | | // page order, then shared thumbnail objects, and then outlines (unless in part 6). After that, | 1542 | | // we throw all remaining objects in arbitrary order. | 1543 | | | 1544 | | // Place the pages tree. | 1545 | 9.56k | auto& pages_ogs = obj_user_to_objects_[{ObjUser::ou_root_key, "/Pages"}]; | 1546 | 9.56k | no_ci_stop_if( | 1547 | 9.56k | pages_ogs.empty(), "found empty pages tree while calculating linearization data" // | 1548 | 9.56k | ); | 1549 | 13.6k | for (auto const& og: pages_ogs) { | 1550 | 13.6k | if (lc_other.erase(og)) { | 1551 | 10.8k | part9_.emplace_back(qpdf.getObject(og)); | 1552 | 10.8k | } | 1553 | 13.6k | } | 1554 | | | 1555 | | // Place private thumbnail images in page order. Slightly more information would be required if | 1556 | | // we were going to bother with thumbnail hint tables. | 1557 | 22.4k | for (size_t i = 0; i < npages; ++i) { | 1558 | 12.9k | QPDFObjectHandle thumb = uc_pages.at(i).getKey("/Thumb"); | 1559 | 12.9k | thumb = getUncompressedObject(thumb, object_stream_data); | 1560 | 12.9k | QPDFObjGen thumb_og(thumb.getObjGen()); | 1561 | | // Output the thumbnail itself | 1562 | 12.9k | if (lc_thumbnail_private.erase(thumb_og) && !thumb.null()) { | 1563 | 276 | part9_.emplace_back(thumb); | 1564 | 12.6k | } else { | 1565 | | // No internal error this time...there's nothing to stop this object from having | 1566 | | // been referred to somewhere else outside of a page's /Thumb, and if it had been, | 1567 | | // there's nothing to prevent it from having been in some set other than | 1568 | | // lc_thumbnail_private. | 1569 | 12.6k | } | 1570 | 12.9k | for (auto const& og: obj_user_to_objects_[{ObjUser::ou_thumb, i}]) { | 1571 | 4.27k | if (lc_thumbnail_private.erase(og)) { | 1572 | 1.04k | part9_.emplace_back(qpdf.getObject(og)); | 1573 | 1.04k | } | 1574 | 4.27k | } | 1575 | 12.9k | } | 1576 | 9.56k | util::assertion( | 1577 | 9.56k | lc_thumbnail_private.empty(), | 1578 | 9.56k | "INTERNAL ERROR: QPDF::calculateLinearizationData: lc_thumbnail_private not " | 1579 | 9.56k | "empty after placing thumbnails" // | 1580 | 9.56k | ); | 1581 | | | 1582 | | // Place shared thumbnail objects | 1583 | 9.56k | for (auto const& og: lc_thumbnail_shared) { | 1584 | 766 | part9_.emplace_back(qpdf.getObject(og)); | 1585 | 766 | } | 1586 | | | 1587 | | // Place outlines unless in first page | 1588 | 9.56k | if (!outlines_in_first_page) { | 1589 | 9.03k | pushOutlinesToPart(part9_, lc_outlines, object_stream_data); | 1590 | 9.03k | } | 1591 | | | 1592 | | // Place all remaining objects | 1593 | 16.6k | for (auto const& og: lc_other) { | 1594 | 16.6k | part9_.emplace_back(qpdf.getObject(og)); | 1595 | 16.6k | } | 1596 | | | 1597 | | // Make sure we got everything exactly once. | 1598 | | | 1599 | 9.56k | size_t num_placed = | 1600 | 9.56k | part4_.size() + part6_.size() + part7_.size() + part8_.size() + part9_.size(); | 1601 | 9.56k | size_t num_wanted = object_to_obj_users_.size(); | 1602 | 9.56k | no_ci_stop_if( | 1603 | | // This can happen with damaged files, e.g. if the root is part of the the pages tree. | 1604 | 9.56k | num_placed != num_wanted, | 1605 | 9.56k | "QPDF::calculateLinearizationData: wrong number of objects placed (num_placed = " + | 1606 | 9.56k | std::to_string(num_placed) + "; number of objects: " + std::to_string(num_wanted) + | 1607 | 9.56k | "\nIf the file did not generate any other warnings please report this as a bug." // | 1608 | 9.56k | ); | 1609 | | | 1610 | | // Calculate shared object hint table information including references to shared objects from | 1611 | | // page offset hint data. | 1612 | | | 1613 | | // The shared object hint table consists of all part 6 (whether shared or not) in order followed | 1614 | | // by all part 8 objects in order. Add the objects to shared object data keeping a map of | 1615 | | // object number to index. Then populate the shared object information for the pages. | 1616 | | | 1617 | | // Note that two objects never have the same object number, so we can map from object number | 1618 | | // only without regards to generation. | 1619 | 9.56k | std::map<int, int> obj_to_index; | 1620 | | | 1621 | 9.56k | c_shared_object_data_.nshared_first_page = toI(part6_.size()); | 1622 | 9.56k | c_shared_object_data_.nshared_total = | 1623 | 9.56k | c_shared_object_data_.nshared_first_page + toI(part8_.size()); | 1624 | | | 1625 | 9.56k | std::vector<CHSharedObjectEntry>& shared = c_shared_object_data_.entries; | 1626 | 57.5k | for (auto& oh: part6_) { | 1627 | 57.5k | int obj = oh.getObjectID(); | 1628 | 57.5k | obj_to_index[obj] = toI(shared.size()); | 1629 | 57.5k | shared.emplace_back(obj); | 1630 | 57.5k | } | 1631 | 9.56k | QTC::TC("qpdf", "QPDF lin part 8 empty", part8_.empty() ? 1 : 0); | 1632 | 9.56k | if (!part8_.empty()) { | 1633 | 101 | c_shared_object_data_.first_shared_obj = part8_.at(0).getObjectID(); | 1634 | 3.29k | for (auto& oh: part8_) { | 1635 | 3.29k | int obj = oh.getObjectID(); | 1636 | 3.29k | obj_to_index[obj] = toI(shared.size()); | 1637 | 3.29k | shared.emplace_back(obj); | 1638 | 3.29k | } | 1639 | 101 | } | 1640 | 9.56k | no_ci_stop_if( | 1641 | 9.56k | std::cmp_not_equal( | 1642 | 9.56k | c_shared_object_data_.nshared_total, c_shared_object_data_.entries.size()), | 1643 | 9.56k | "shared object hint table has wrong number of entries" // | 1644 | 9.56k | ); | 1645 | | | 1646 | | // Now compute the list of shared objects for each page after the first page. | 1647 | | | 1648 | 13.3k | for (size_t i = 1; i < npages; ++i) { | 1649 | 3.82k | CHPageOffsetEntry& pe = c_page_offset_data_.entries.at(i); | 1650 | 3.82k | ObjUser ou(ObjUser::ou_page, i); | 1651 | 3.82k | no_ci_stop_if( | 1652 | 3.82k | !obj_user_to_objects_.contains(ou), | 1653 | 3.82k | "found unreferenced page while calculating linearization data" // | 1654 | 3.82k | ); | 1655 | | | 1656 | 82.0k | for (auto const& og: obj_user_to_objects_[ou]) { | 1657 | 82.0k | if (object_to_obj_users_[og].size() > 1 && obj_to_index.contains(og.getObj())) { | 1658 | 67.9k | int idx = obj_to_index[og.getObj()]; | 1659 | 67.9k | ++pe.nshared_objects; | 1660 | 67.9k | pe.shared_identifiers.push_back(idx); | 1661 | 67.9k | } | 1662 | 82.0k | } | 1663 | 3.82k | } | 1664 | 9.56k | } |
|
1665 | | |
1666 | | template <typename T> |
1667 | | void |
1668 | | Lin::pushOutlinesToPart( |
1669 | | std::vector<QPDFObjectHandle>& part, |
1670 | | std::set<QPDFObjGen>& lc_outlines, |
1671 | | T const& object_stream_data) |
1672 | 9.08k | { |
1673 | 9.08k | QPDFObjectHandle root = qpdf.getRoot(); |
1674 | 9.08k | QPDFObjectHandle outlines = root.getKey("/Outlines"); |
1675 | 9.08k | if (outlines.null()) { |
1676 | 8.85k | return; |
1677 | 8.85k | } |
1678 | 222 | outlines = getUncompressedObject(outlines, object_stream_data); |
1679 | 222 | QPDFObjGen outlines_og(outlines.getObjGen()); |
1680 | 222 | QTC::TC( |
1681 | 222 | "qpdf", |
1682 | 222 | "QPDF lin outlines in part", |
1683 | 222 | &part == &part6_ ? 0 |
1684 | 222 | : (&part == &part9_) ? 1 |
1685 | 176 | : 9999); // can't happen |
1686 | 222 | if (lc_outlines.erase(outlines_og)) { |
1687 | | // Make sure outlines is in lc_outlines in case the file is damaged. in which case it may be |
1688 | | // included in an earlier part. |
1689 | 196 | part.emplace_back(outlines); |
1690 | 196 | c_outline_data_.first_object = outlines_og.getObj(); |
1691 | 196 | c_outline_data_.nobjects = 1; |
1692 | 196 | } |
1693 | 1.44k | for (auto const& og: lc_outlines) { |
1694 | 1.44k | if (!c_outline_data_.first_object) { |
1695 | 10 | c_outline_data_.first_object = og.getObj(); |
1696 | 10 | } |
1697 | 1.44k | part.emplace_back(qpdf.getObject(og)); |
1698 | 1.44k | ++c_outline_data_.nobjects; |
1699 | 1.44k | } |
1700 | 222 | } Unexecuted instantiation: void QPDF::Doc::Linearization::pushOutlinesToPart<std::__1::map<int, int, std::__1::less<int>, std::__1::allocator<std::__1::pair<int const, int> > > >(std::__1::vector<QPDFObjectHandle, std::__1::allocator<QPDFObjectHandle> >&, std::__1::set<QPDFObjGen, std::__1::less<QPDFObjGen>, std::__1::allocator<QPDFObjGen> >&, std::__1::map<int, int, std::__1::less<int>, std::__1::allocator<std::__1::pair<int const, int> > > const&) void QPDF::Doc::Linearization::pushOutlinesToPart<QPDFWriter::ObjTable>(std::__1::vector<QPDFObjectHandle, std::__1::allocator<QPDFObjectHandle> >&, std::__1::set<QPDFObjGen, std::__1::less<QPDFObjGen>, std::__1::allocator<QPDFObjGen> >&, QPDFWriter::ObjTable const&) Line | Count | Source | 1672 | 9.08k | { | 1673 | 9.08k | QPDFObjectHandle root = qpdf.getRoot(); | 1674 | 9.08k | QPDFObjectHandle outlines = root.getKey("/Outlines"); | 1675 | 9.08k | if (outlines.null()) { | 1676 | 8.85k | return; | 1677 | 8.85k | } | 1678 | 222 | outlines = getUncompressedObject(outlines, object_stream_data); | 1679 | 222 | QPDFObjGen outlines_og(outlines.getObjGen()); | 1680 | 222 | QTC::TC( | 1681 | 222 | "qpdf", | 1682 | 222 | "QPDF lin outlines in part", | 1683 | 222 | &part == &part6_ ? 0 | 1684 | 222 | : (&part == &part9_) ? 1 | 1685 | 176 | : 9999); // can't happen | 1686 | 222 | if (lc_outlines.erase(outlines_og)) { | 1687 | | // Make sure outlines is in lc_outlines in case the file is damaged. in which case it may be | 1688 | | // included in an earlier part. | 1689 | 196 | part.emplace_back(outlines); | 1690 | 196 | c_outline_data_.first_object = outlines_og.getObj(); | 1691 | 196 | c_outline_data_.nobjects = 1; | 1692 | 196 | } | 1693 | 1.44k | for (auto const& og: lc_outlines) { | 1694 | 1.44k | if (!c_outline_data_.first_object) { | 1695 | 10 | c_outline_data_.first_object = og.getObj(); | 1696 | 10 | } | 1697 | 1.44k | part.emplace_back(qpdf.getObject(og)); | 1698 | 1.44k | ++c_outline_data_.nobjects; | 1699 | 1.44k | } | 1700 | 222 | } |
|
1701 | | |
1702 | | void |
1703 | | Lin::parts( |
1704 | | QPDFWriter::ObjTable const& obj, |
1705 | | std::vector<QPDFObjectHandle>& part4, |
1706 | | std::vector<QPDFObjectHandle>& part6, |
1707 | | std::vector<QPDFObjectHandle>& part7, |
1708 | | std::vector<QPDFObjectHandle>& part8, |
1709 | | std::vector<QPDFObjectHandle>& part9) |
1710 | 9.56k | { |
1711 | 9.56k | calculateLinearizationData(obj); |
1712 | 9.56k | part4 = part4_; |
1713 | 9.56k | part6 = part6_; |
1714 | 9.56k | part7 = part7_; |
1715 | 9.56k | part8 = part8_; |
1716 | 9.56k | part9 = part9_; |
1717 | 9.56k | } |
1718 | | |
1719 | | static inline int |
1720 | | nbits(int val) |
1721 | 125k | { |
1722 | 125k | return (val == 0 ? 0 : (1 + nbits(val >> 1))); |
1723 | 125k | } |
1724 | | |
1725 | | int |
1726 | | Lin::outputLengthNextN( |
1727 | | int in_object, int n, QPDFWriter::NewObjTable const& new_obj, QPDFWriter::ObjTable const& obj) |
1728 | 71.5k | { |
1729 | | // Figure out the length of a series of n consecutive objects in the output file starting with |
1730 | | // whatever object in_object from the input file mapped to. |
1731 | | |
1732 | 71.5k | int first = obj[in_object].renumber; |
1733 | 71.5k | int last = first + n; |
1734 | 71.5k | no_ci_stop_if( |
1735 | 71.5k | first <= 0, "found object that is not renumbered while writing linearization data"); |
1736 | 71.5k | qpdf_offset_t length = 0; |
1737 | 192k | for (int i = first; i < last; ++i) { |
1738 | 121k | auto l = new_obj[i].length; |
1739 | 121k | no_ci_stop_if( |
1740 | 121k | l == 0, "found item with unknown length while writing linearization data" // |
1741 | 121k | ); |
1742 | 121k | length += l; |
1743 | 121k | } |
1744 | 71.5k | return toI(length); |
1745 | 71.5k | } |
1746 | | |
1747 | | void |
1748 | | Lin::calculateHPageOffset(QPDFWriter::NewObjTable const& new_obj, QPDFWriter::ObjTable const& obj) |
1749 | 8.05k | { |
1750 | | // Page Offset Hint Table |
1751 | | |
1752 | | // We are purposely leaving some values set to their initial zero values. |
1753 | | |
1754 | 8.05k | auto const& all_pages = pages.all(); |
1755 | 8.05k | size_t npages = all_pages.size(); |
1756 | 8.05k | CHPageOffset& cph = c_page_offset_data_; |
1757 | 8.05k | std::vector<CHPageOffsetEntry>& cphe = cph.entries; |
1758 | | |
1759 | | // Calculate minimum and maximum values for number of objects per page and page length. |
1760 | | |
1761 | 8.05k | int min_nobjects = std::numeric_limits<int>::max(); |
1762 | 8.05k | int max_nobjects = 0; |
1763 | 8.05k | int min_length = std::numeric_limits<int>::max(); |
1764 | 8.05k | int max_length = 0; |
1765 | 8.05k | int max_shared = 0; |
1766 | | |
1767 | 8.05k | HPageOffset& ph = page_offset_hints_; |
1768 | 8.05k | std::vector<HPageOffsetEntry>& phe = ph.entries; |
1769 | | // npages is the size of the existing pages array. |
1770 | 8.05k | phe = std::vector<HPageOffsetEntry>(npages); |
1771 | | |
1772 | 8.05k | size_t i = 0; |
1773 | 11.7k | for (auto& phe_i: phe) { |
1774 | | // Calculate values for each page, assigning full values to the delta items. They will be |
1775 | | // adjusted later. |
1776 | | |
1777 | | // Repeat calculations for page 0 so we can assign to phe[i] without duplicating those |
1778 | | // assignments. |
1779 | | |
1780 | 11.7k | int nobjects = cphe.at(i).nobjects; |
1781 | 11.7k | int length = outputLengthNextN(all_pages.at(i).getObjectID(), nobjects, new_obj, obj); |
1782 | 11.7k | int nshared = cphe.at(i).nshared_objects; |
1783 | | |
1784 | 11.7k | min_nobjects = std::min(min_nobjects, nobjects); |
1785 | 11.7k | max_nobjects = std::max(max_nobjects, nobjects); |
1786 | 11.7k | min_length = std::min(min_length, length); |
1787 | 11.7k | max_length = std::max(max_length, length); |
1788 | 11.7k | max_shared = std::max(max_shared, nshared); |
1789 | | |
1790 | 11.7k | phe_i.delta_nobjects = nobjects; |
1791 | 11.7k | phe_i.delta_page_length = length; |
1792 | 11.7k | phe_i.nshared_objects = nshared; |
1793 | 11.7k | ++i; |
1794 | 11.7k | } |
1795 | | |
1796 | 8.05k | ph.min_nobjects = min_nobjects; |
1797 | 8.05k | ph.first_page_offset = new_obj[obj[all_pages.at(0)].renumber].xref.getOffset(); |
1798 | 8.05k | ph.nbits_delta_nobjects = nbits(max_nobjects - min_nobjects); |
1799 | 8.05k | ph.min_page_length = min_length; |
1800 | 8.05k | ph.nbits_delta_page_length = nbits(max_length - min_length); |
1801 | 8.05k | ph.nbits_nshared_objects = nbits(max_shared); |
1802 | 8.05k | ph.nbits_shared_identifier = nbits(c_shared_object_data_.nshared_total); |
1803 | 8.05k | ph.shared_denominator = 4; // doesn't matter |
1804 | | |
1805 | | // It isn't clear how to compute content offset and content length. Since we are not |
1806 | | // interleaving page objects with the content stream, we'll use the same values for content |
1807 | | // length as page length. We will use 0 as content offset because this is what Adobe does |
1808 | | // (implementation note 127) and pdlin as well. |
1809 | 8.05k | ph.nbits_delta_content_length = ph.nbits_delta_page_length; |
1810 | 8.05k | ph.min_content_length = ph.min_page_length; |
1811 | | |
1812 | 8.05k | i = 0; |
1813 | 11.7k | for (auto& phe_i: phe) { |
1814 | | // Adjust delta entries |
1815 | 11.7k | if (phe_i.delta_nobjects < min_nobjects || phe_i.delta_page_length < min_length) { |
1816 | 0 | stopOnError( |
1817 | 0 | "found too small delta nobjects or delta page length while writing " |
1818 | 0 | "linearization data"); |
1819 | 0 | } |
1820 | 11.7k | phe_i.delta_nobjects -= min_nobjects; |
1821 | 11.7k | phe_i.delta_page_length -= min_length; |
1822 | 11.7k | phe_i.delta_content_length = phe_i.delta_page_length; |
1823 | | |
1824 | 11.7k | auto& si = cphe.at(i).shared_identifiers; |
1825 | 11.7k | phe_i.shared_identifiers.insert(phe_i.shared_identifiers.end(), si.begin(), si.end()); |
1826 | 11.7k | phe_i.shared_numerators.insert(phe_i.shared_numerators.end(), si.size(), 0); |
1827 | 11.7k | ++i; |
1828 | 11.7k | } |
1829 | 8.05k | } |
1830 | | |
1831 | | void |
1832 | | Lin::calculateHSharedObject(QPDFWriter::NewObjTable const& new_obj, QPDFWriter::ObjTable const& obj) |
1833 | 8.05k | { |
1834 | 8.05k | CHSharedObject& cso = c_shared_object_data_; |
1835 | 8.05k | std::vector<CHSharedObjectEntry>& csoe = cso.entries; |
1836 | 8.05k | HSharedObject& so = shared_object_hints_; |
1837 | 8.05k | std::vector<HSharedObjectEntry>& soe = so.entries; |
1838 | 8.05k | soe.clear(); |
1839 | | |
1840 | 8.05k | int min_length = outputLengthNextN(csoe.at(0).object, 1, new_obj, obj); |
1841 | 8.05k | int max_length = min_length; |
1842 | | |
1843 | 59.5k | for (size_t i = 0; i < toS(cso.nshared_total); ++i) { |
1844 | | // Assign absolute numbers to deltas; adjust later |
1845 | 51.5k | int length = outputLengthNextN(csoe.at(i).object, 1, new_obj, obj); |
1846 | 51.5k | min_length = std::min(min_length, length); |
1847 | 51.5k | max_length = std::max(max_length, length); |
1848 | 51.5k | soe.emplace_back(); |
1849 | 51.5k | soe.at(i).delta_group_length = length; |
1850 | 51.5k | } |
1851 | 8.05k | no_ci_stop_if( |
1852 | 8.05k | soe.size() != toS(cso.nshared_total), "soe has wrong size after initialization" // |
1853 | 8.05k | ); |
1854 | | |
1855 | 8.05k | so.nshared_total = cso.nshared_total; |
1856 | 8.05k | so.nshared_first_page = cso.nshared_first_page; |
1857 | 8.05k | if (so.nshared_total > so.nshared_first_page) { |
1858 | 87 | so.first_shared_obj = obj[cso.first_shared_obj].renumber; |
1859 | 87 | so.min_group_length = min_length; |
1860 | 87 | so.first_shared_offset = new_obj[so.first_shared_obj].xref.getOffset(); |
1861 | 87 | } |
1862 | 8.05k | so.min_group_length = min_length; |
1863 | 8.05k | so.nbits_delta_group_length = nbits(max_length - min_length); |
1864 | | |
1865 | 59.5k | for (size_t i = 0; i < toS(cso.nshared_total); ++i) { |
1866 | | // Adjust deltas |
1867 | 51.5k | no_ci_stop_if( |
1868 | 51.5k | soe.at(i).delta_group_length < min_length, |
1869 | 51.5k | "found too small group length while writing linearization data" // |
1870 | 51.5k | ); |
1871 | | |
1872 | 51.5k | soe.at(i).delta_group_length -= min_length; |
1873 | 51.5k | } |
1874 | 8.05k | } |
1875 | | |
1876 | | void |
1877 | | Lin::calculateHOutline(QPDFWriter::NewObjTable const& new_obj, QPDFWriter::ObjTable const& obj) |
1878 | 8.05k | { |
1879 | 8.05k | HGeneric& cho = c_outline_data_; |
1880 | | |
1881 | 8.05k | if (cho.nobjects == 0) { |
1882 | 7.86k | return; |
1883 | 7.86k | } |
1884 | | |
1885 | 198 | HGeneric& ho = outline_hints_; |
1886 | | |
1887 | 198 | ho.first_object = obj[cho.first_object].renumber; |
1888 | 198 | ho.first_object_offset = new_obj[ho.first_object].xref.getOffset(); |
1889 | 198 | ho.nobjects = cho.nobjects; |
1890 | 198 | ho.group_length = outputLengthNextN(cho.first_object, ho.nobjects, new_obj, obj); |
1891 | 198 | } |
1892 | | |
1893 | | template <class T, class int_type> |
1894 | | static void |
1895 | | write_vector_int(BitWriter& w, int nitems, std::vector<T>& vec, int bits, int_type T::* field) |
1896 | 64.4k | { |
1897 | | // nitems times, write bits bits from the given field of the ith vector to the given bit writer. |
1898 | | |
1899 | 277k | for (size_t i = 0; i < QIntC::to_size(nitems); ++i) { |
1900 | 213k | w.writeBits(QIntC::to_ulonglong(vec.at(i).*field), QIntC::to_size(bits)); |
1901 | 213k | } |
1902 | | // The PDF spec says that each hint table starts at a byte boundary. Each "row" actually must |
1903 | | // start on a byte boundary. |
1904 | 64.4k | w.flush(); |
1905 | 64.4k | } QPDF_linearization.cc:void write_vector_int<QPDF::Doc::Linearization::HPageOffsetEntry, int>(BitWriter&, int, std::__1::vector<QPDF::Doc::Linearization::HPageOffsetEntry, std::__1::allocator<QPDF::Doc::Linearization::HPageOffsetEntry> >&, int, int QPDF::Doc::Linearization::HPageOffsetEntry::*) Line | Count | Source | 1896 | 16.1k | { | 1897 | | // nitems times, write bits bits from the given field of the ith vector to the given bit writer. | 1898 | | | 1899 | 39.5k | for (size_t i = 0; i < QIntC::to_size(nitems); ++i) { | 1900 | 23.4k | w.writeBits(QIntC::to_ulonglong(vec.at(i).*field), QIntC::to_size(bits)); | 1901 | 23.4k | } | 1902 | | // The PDF spec says that each hint table starts at a byte boundary. Each "row" actually must | 1903 | | // start on a byte boundary. | 1904 | 16.1k | w.flush(); | 1905 | 16.1k | } |
QPDF_linearization.cc:void write_vector_int<QPDF::Doc::Linearization::HPageOffsetEntry, long long>(BitWriter&, int, std::__1::vector<QPDF::Doc::Linearization::HPageOffsetEntry, std::__1::allocator<QPDF::Doc::Linearization::HPageOffsetEntry> >&, int, long long QPDF::Doc::Linearization::HPageOffsetEntry::*) Line | Count | Source | 1896 | 24.1k | { | 1897 | | // nitems times, write bits bits from the given field of the ith vector to the given bit writer. | 1898 | | | 1899 | 59.3k | for (size_t i = 0; i < QIntC::to_size(nitems); ++i) { | 1900 | 35.1k | w.writeBits(QIntC::to_ulonglong(vec.at(i).*field), QIntC::to_size(bits)); | 1901 | 35.1k | } | 1902 | | // The PDF spec says that each hint table starts at a byte boundary. Each "row" actually must | 1903 | | // start on a byte boundary. | 1904 | 24.1k | w.flush(); | 1905 | 24.1k | } |
QPDF_linearization.cc:void write_vector_int<QPDF::Doc::Linearization::HSharedObjectEntry, int>(BitWriter&, int, std::__1::vector<QPDF::Doc::Linearization::HSharedObjectEntry, std::__1::allocator<QPDF::Doc::Linearization::HSharedObjectEntry> >&, int, int QPDF::Doc::Linearization::HSharedObjectEntry::*) Line | Count | Source | 1896 | 24.1k | { | 1897 | | // nitems times, write bits bits from the given field of the ith vector to the given bit writer. | 1898 | | | 1899 | 178k | for (size_t i = 0; i < QIntC::to_size(nitems); ++i) { | 1900 | 154k | w.writeBits(QIntC::to_ulonglong(vec.at(i).*field), QIntC::to_size(bits)); | 1901 | 154k | } | 1902 | | // The PDF spec says that each hint table starts at a byte boundary. Each "row" actually must | 1903 | | // start on a byte boundary. | 1904 | 24.1k | w.flush(); | 1905 | 24.1k | } |
|
1906 | | |
1907 | | template <class T> |
1908 | | static void |
1909 | | write_vector_vector( |
1910 | | BitWriter& w, |
1911 | | int nitems1, |
1912 | | std::vector<T>& vec1, |
1913 | | int T::* nitems2, |
1914 | | int bits, |
1915 | | std::vector<int> T::* vec2) |
1916 | 16.1k | { |
1917 | | // nitems1 times, write nitems2 (from the ith element of vec1) items from the vec2 vector field |
1918 | | // of the ith item of vec1. |
1919 | 39.5k | for (size_t i1 = 0; i1 < QIntC::to_size(nitems1); ++i1) { |
1920 | 156k | for (size_t i2 = 0; i2 < QIntC::to_size(vec1.at(i1).*nitems2); ++i2) { |
1921 | 132k | w.writeBits(QIntC::to_ulonglong((vec1.at(i1).*vec2).at(i2)), QIntC::to_size(bits)); |
1922 | 132k | } |
1923 | 23.4k | } |
1924 | 16.1k | w.flush(); |
1925 | 16.1k | } |
1926 | | |
1927 | | void |
1928 | | Lin::writeHPageOffset(BitWriter& w) |
1929 | 8.05k | { |
1930 | 8.05k | HPageOffset& t = page_offset_hints_; |
1931 | | |
1932 | 8.05k | w.writeBitsInt(t.min_nobjects, 32); // 1 |
1933 | 8.05k | w.writeBits(toULL(t.first_page_offset), 32); // 2 |
1934 | 8.05k | w.writeBitsInt(t.nbits_delta_nobjects, 16); // 3 |
1935 | 8.05k | w.writeBitsInt(t.min_page_length, 32); // 4 |
1936 | 8.05k | w.writeBitsInt(t.nbits_delta_page_length, 16); // 5 |
1937 | 8.05k | w.writeBits(toULL(t.min_content_offset), 32); // 6 |
1938 | 8.05k | w.writeBitsInt(t.nbits_delta_content_offset, 16); // 7 |
1939 | 8.05k | w.writeBitsInt(t.min_content_length, 32); // 8 |
1940 | 8.05k | w.writeBitsInt(t.nbits_delta_content_length, 16); // 9 |
1941 | 8.05k | w.writeBitsInt(t.nbits_nshared_objects, 16); // 10 |
1942 | 8.05k | w.writeBitsInt(t.nbits_shared_identifier, 16); // 11 |
1943 | 8.05k | w.writeBitsInt(t.nbits_shared_numerator, 16); // 12 |
1944 | 8.05k | w.writeBitsInt(t.shared_denominator, 16); // 13 |
1945 | | |
1946 | 8.05k | int nitems = toI(pages.size()); |
1947 | 8.05k | std::vector<HPageOffsetEntry>& entries = t.entries; |
1948 | | |
1949 | 8.05k | write_vector_int(w, nitems, entries, t.nbits_delta_nobjects, &HPageOffsetEntry::delta_nobjects); |
1950 | 8.05k | write_vector_int( |
1951 | 8.05k | w, nitems, entries, t.nbits_delta_page_length, &HPageOffsetEntry::delta_page_length); |
1952 | 8.05k | write_vector_int( |
1953 | 8.05k | w, nitems, entries, t.nbits_nshared_objects, &HPageOffsetEntry::nshared_objects); |
1954 | 8.05k | write_vector_vector( |
1955 | 8.05k | w, |
1956 | 8.05k | nitems, |
1957 | 8.05k | entries, |
1958 | 8.05k | &HPageOffsetEntry::nshared_objects, |
1959 | 8.05k | t.nbits_shared_identifier, |
1960 | 8.05k | &HPageOffsetEntry::shared_identifiers); |
1961 | 8.05k | write_vector_vector( |
1962 | 8.05k | w, |
1963 | 8.05k | nitems, |
1964 | 8.05k | entries, |
1965 | 8.05k | &HPageOffsetEntry::nshared_objects, |
1966 | 8.05k | t.nbits_shared_numerator, |
1967 | 8.05k | &HPageOffsetEntry::shared_numerators); |
1968 | 8.05k | write_vector_int( |
1969 | 8.05k | w, nitems, entries, t.nbits_delta_content_offset, &HPageOffsetEntry::delta_content_offset); |
1970 | 8.05k | write_vector_int( |
1971 | 8.05k | w, nitems, entries, t.nbits_delta_content_length, &HPageOffsetEntry::delta_content_length); |
1972 | 8.05k | } |
1973 | | |
1974 | | void |
1975 | | Lin::writeHSharedObject(BitWriter& w) |
1976 | 8.05k | { |
1977 | 8.05k | HSharedObject& t = shared_object_hints_; |
1978 | | |
1979 | 8.05k | w.writeBitsInt(t.first_shared_obj, 32); // 1 |
1980 | 8.05k | w.writeBits(toULL(t.first_shared_offset), 32); // 2 |
1981 | 8.05k | w.writeBitsInt(t.nshared_first_page, 32); // 3 |
1982 | 8.05k | w.writeBitsInt(t.nshared_total, 32); // 4 |
1983 | 8.05k | w.writeBitsInt(t.nbits_nobjects, 16); // 5 |
1984 | 8.05k | w.writeBitsInt(t.min_group_length, 32); // 6 |
1985 | 8.05k | w.writeBitsInt(t.nbits_delta_group_length, 16); // 7 |
1986 | | |
1987 | 8.05k | QTC::TC( |
1988 | 8.05k | "qpdf", |
1989 | 8.05k | "QPDF lin write nshared_total > nshared_first_page", |
1990 | 8.05k | (t.nshared_total > t.nshared_first_page) ? 1 : 0); |
1991 | | |
1992 | 8.05k | int nitems = t.nshared_total; |
1993 | 8.05k | std::vector<HSharedObjectEntry>& entries = t.entries; |
1994 | | |
1995 | 8.05k | write_vector_int( |
1996 | 8.05k | w, nitems, entries, t.nbits_delta_group_length, &HSharedObjectEntry::delta_group_length); |
1997 | 8.05k | write_vector_int(w, nitems, entries, 1, &HSharedObjectEntry::signature_present); |
1998 | 59.5k | for (size_t i = 0; i < toS(nitems); ++i) { |
1999 | | // If signature were present, we'd have to write a 128-bit hash. |
2000 | 51.5k | if (entries.at(i).signature_present != 0) { |
2001 | 0 | stopOnError("found unexpected signature present while writing linearization data"); |
2002 | 0 | } |
2003 | 51.5k | } |
2004 | 8.05k | write_vector_int(w, nitems, entries, t.nbits_nobjects, &HSharedObjectEntry::nobjects_minus_one); |
2005 | 8.05k | } |
2006 | | |
2007 | | void |
2008 | | Lin::writeHGeneric(BitWriter& w, HGeneric& t) |
2009 | 198 | { |
2010 | 198 | w.writeBitsInt(t.first_object, 32); // 1 |
2011 | 198 | w.writeBits(toULL(t.first_object_offset), 32); // 2 |
2012 | 198 | w.writeBitsInt(t.nobjects, 32); // 3 |
2013 | 198 | w.writeBitsInt(t.group_length, 32); // 4 |
2014 | 198 | } |
2015 | | |
2016 | | void |
2017 | | Lin::generateHintStream( |
2018 | | QPDFWriter::NewObjTable const& new_obj, |
2019 | | QPDFWriter::ObjTable const& obj, |
2020 | | std::string& hint_buffer, |
2021 | | int& S, |
2022 | | int& O, |
2023 | | bool compressed) |
2024 | 8.05k | { |
2025 | | // Populate actual hint table values |
2026 | 8.05k | calculateHPageOffset(new_obj, obj); |
2027 | 8.05k | calculateHSharedObject(new_obj, obj); |
2028 | 8.05k | calculateHOutline(new_obj, obj); |
2029 | | |
2030 | | // Write the hint stream itself into a compressed memory buffer. Write through a counter so we |
2031 | | // can get offsets. |
2032 | 8.05k | pl::Count c(0, hint_buffer); |
2033 | 8.05k | BitWriter w(&c); |
2034 | | |
2035 | 8.05k | writeHPageOffset(w); |
2036 | 8.05k | S = toI(c.getCount()); |
2037 | 8.05k | writeHSharedObject(w); |
2038 | 8.05k | O = 0; |
2039 | 8.05k | if (outline_hints_.nobjects > 0) { |
2040 | 198 | O = toI(c.getCount()); |
2041 | 198 | writeHGeneric(w, outline_hints_); |
2042 | 198 | } |
2043 | 8.05k | if (compressed) { |
2044 | 8.05k | hint_buffer = pl::pipe<Pl_Flate>(hint_buffer, Pl_Flate::a_deflate); |
2045 | 8.05k | } |
2046 | 8.05k | } |