qpdf-compress 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/strip.h ADDED
@@ -0,0 +1,9 @@
1
+ #pragma once
2
+
3
+ #include <qpdf/QPDF.hh>
4
+
5
+ void stripMetadata(QPDF &qpdf);
6
+ void stripEmbeddedFiles(QPDF &qpdf);
7
+ void stripJavaScript(QPDF &qpdf);
8
+ void stripDocumentOverhead(QPDF &qpdf);
9
+ void removeUnusedResources(QPDF &qpdf);
@@ -0,0 +1,346 @@
1
+ #include "structure.h"
2
+ #include "hash_utils.h"
3
+ #include "images.h"
4
+
5
+ #include <algorithm>
6
+ #include <cmath>
7
+ #include <cstdint>
8
+ #include <cstdio>
9
+ #include <cstring>
10
+ #include <map>
11
+ #include <set>
12
+ #include <string>
13
+ #include <unordered_map>
14
+ #include <vector>
15
+
16
+ #include <qpdf/QPDFObjectHandle.hh>
17
+ #include <qpdf/QPDFPageDocumentHelper.hh>
18
+ #include <qpdf/QPDFPageObjectHelper.hh>
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Deduplicate identical non-image streams (fonts, ICC profiles, etc.)
22
+ // ---------------------------------------------------------------------------
23
+
24
+ void deduplicateStreams(QPDF &qpdf) {
25
+ // collect all stream objects and their raw data hashes
26
+ struct StreamEntry {
27
+ QPDFObjGen og;
28
+ size_t dataSize;
29
+ QPDFObjectHandle handle;
30
+ };
31
+
32
+ std::unordered_map<uint64_t, std::vector<StreamEntry>> hashGroups;
33
+ std::set<QPDFObjGen> imageObjGens;
34
+
35
+ // collect image object IDs to skip them (already handled by
36
+ // deduplicateImages)
37
+ forEachImage(qpdf, [&](const std::string &, QPDFObjectHandle xobj,
38
+ QPDFObjectHandle, QPDFPageObjectHelper &) {
39
+ imageObjGens.insert(xobj.getObjGen());
40
+ });
41
+
42
+ for (auto &obj : qpdf.getAllObjects()) {
43
+ if (!obj.isStream())
44
+ continue;
45
+
46
+ auto og = obj.getObjGen();
47
+
48
+ // skip images
49
+ if (imageObjGens.count(og))
50
+ continue;
51
+
52
+ try {
53
+ auto rawData = obj.getRawStreamData();
54
+ size_t size = rawData->getSize();
55
+ if (size == 0)
56
+ continue;
57
+
58
+ uint64_t hash = fnv1aHash(rawData->getBuffer(), size);
59
+
60
+ hashGroups[hash].push_back({og, size, obj});
61
+ } catch (...) {
62
+ }
63
+ }
64
+
65
+ // find duplicates via full byte comparison
66
+ std::map<QPDFObjGen, QPDFObjectHandle> replacements;
67
+
68
+ for (auto &[hash, group] : hashGroups) {
69
+ if (group.size() < 2)
70
+ continue;
71
+
72
+ for (size_t i = 0; i < group.size(); ++i) {
73
+ if (replacements.count(group[i].og))
74
+ continue;
75
+
76
+ auto rawI = group[i].handle.getRawStreamData();
77
+ for (size_t j = i + 1; j < group.size(); ++j) {
78
+ if (replacements.count(group[j].og))
79
+ continue;
80
+
81
+ auto rawJ = group[j].handle.getRawStreamData();
82
+ if (rawI->getSize() != rawJ->getSize())
83
+ continue;
84
+
85
+ if (memcmp(rawI->getBuffer(), rawJ->getBuffer(), rawI->getSize()) ==
86
+ 0) {
87
+ // verify stream dictionaries are compatible
88
+ auto dictI = group[i].handle.getDict();
89
+ auto dictJ = group[j].handle.getDict();
90
+
91
+ auto filterI = dictI.getKey("/Filter");
92
+ auto filterJ = dictJ.getKey("/Filter");
93
+ bool filtersMatch = (filterI.isName() && filterJ.isName() &&
94
+ filterI.getName() == filterJ.getName()) ||
95
+ (!filterI.isName() && !filterJ.isName());
96
+
97
+ // also verify DecodeParms match — different predictors on
98
+ // identical raw bytes would produce different decoded content
99
+ auto dpI = dictI.getKey("/DecodeParms");
100
+ auto dpJ = dictJ.getKey("/DecodeParms");
101
+ bool paramsMatch = (dpI.isNull() && dpJ.isNull()) ||
102
+ (!dpI.isNull() && !dpJ.isNull() &&
103
+ dpI.unparse() == dpJ.unparse());
104
+
105
+ if (filtersMatch && paramsMatch)
106
+ replacements[group[j].og] = group[i].handle;
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ if (replacements.empty())
113
+ return;
114
+
115
+ // rewrite references: scan all objects for indirect references to duplicates
116
+ for (auto &obj : qpdf.getAllObjects()) {
117
+ if (!obj.isDictionary() && !obj.isStream())
118
+ continue;
119
+
120
+ auto dict = obj.isStream() ? obj.getDict() : obj;
121
+ for (auto &key : dict.getKeys()) {
122
+ auto val = dict.getKey(key);
123
+ if (val.isIndirect()) {
124
+ auto it = replacements.find(val.getObjGen());
125
+ if (it != replacements.end())
126
+ dict.replaceKey(key, it->second);
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Form flattening — merge interactive form field appearances into page
134
+ // content and remove the /AcroForm dictionary
135
+ // ---------------------------------------------------------------------------
136
+
137
+ void flattenForms(QPDF &qpdf) {
138
+ auto root = qpdf.getRoot();
139
+ if (!root.hasKey("/AcroForm"))
140
+ return;
141
+
142
+ // stamp each widget annotation's appearance into the page content,
143
+ // then remove the annotation
144
+ for (auto &page : QPDFPageDocumentHelper(qpdf).getAllPages()) {
145
+ auto pageObj = page.getObjectHandle();
146
+ if (!pageObj.hasKey("/Annots"))
147
+ continue;
148
+
149
+ auto annots = pageObj.getKey("/Annots");
150
+ if (!annots.isArray())
151
+ continue;
152
+
153
+ std::vector<int> widgetIndices;
154
+ std::string allSnippets;
155
+ for (int i = 0; i < annots.getArrayNItems(); ++i) {
156
+ auto annot = annots.getArrayItem(i);
157
+ if (!annot.isDictionary())
158
+ continue;
159
+
160
+ auto subtype = annot.getKey("/Subtype");
161
+ if (!subtype.isName() || subtype.getName() != "/Widget")
162
+ continue;
163
+
164
+ // skip hidden, invisible, or no-view annotations
165
+ if (annot.hasKey("/F")) {
166
+ auto flags = annot.getKey("/F");
167
+ if (flags.isInteger()) {
168
+ int f = static_cast<int>(flags.getIntValue());
169
+ // bit 1 = Invisible, bit 2 = Hidden, bit 6 (32) = NoView
170
+ if (f & (1 | 2 | 32))
171
+ continue;
172
+ }
173
+ }
174
+
175
+ // check if there's a normal appearance to flatten
176
+ // skip signature fields — their appearance should not be flattened
177
+ if (annot.hasKey("/FT")) {
178
+ auto ft = annot.getKey("/FT");
179
+ if (ft.isName() && ft.getName() == "/Sig")
180
+ continue;
181
+ }
182
+
183
+ auto ap = annot.getKey("/AP");
184
+ if (!ap.isDictionary())
185
+ continue;
186
+ auto nAppearance = ap.getKey("/N");
187
+ if (!nAppearance.isStream())
188
+ continue;
189
+
190
+ // get widget rectangle
191
+ auto rect = annot.getKey("/Rect");
192
+ if (!rect.isArray() || rect.getArrayNItems() < 4)
193
+ continue;
194
+
195
+ try {
196
+ double rx1 = rect.getArrayItem(0).getNumericValue();
197
+ double ry1 = rect.getArrayItem(1).getNumericValue();
198
+ double rx2 = rect.getArrayItem(2).getNumericValue();
199
+ double ry2 = rect.getArrayItem(3).getNumericValue();
200
+
201
+ // normalize rect (some generators produce inverted coordinates)
202
+ if (rx1 > rx2)
203
+ std::swap(rx1, rx2);
204
+ if (ry1 > ry2)
205
+ std::swap(ry1, ry2);
206
+
207
+ double rw = rx2 - rx1;
208
+ double rh = ry2 - ry1;
209
+ if (rw <= 0 || rh <= 0)
210
+ continue;
211
+
212
+ // get appearance stream bounding box and matrix
213
+ auto apDict = nAppearance.getDict();
214
+ double bx1 = 0, by1 = 0, bx2 = 1, by2 = 1;
215
+ if (apDict.hasKey("/BBox")) {
216
+ auto bbox = apDict.getKey("/BBox");
217
+ if (bbox.isArray() && bbox.getArrayNItems() >= 4) {
218
+ bx1 = bbox.getArrayItem(0).getNumericValue();
219
+ by1 = bbox.getArrayItem(1).getNumericValue();
220
+ bx2 = bbox.getArrayItem(2).getNumericValue();
221
+ by2 = bbox.getArrayItem(3).getNumericValue();
222
+ }
223
+ }
224
+
225
+ double bw = bx2 - bx1;
226
+ double bh = by2 - by1;
227
+ if (std::abs(bw) < 1e-10 || std::abs(bh) < 1e-10)
228
+ continue;
229
+
230
+ // BBox→Rect mapping: scale then translate
231
+ double sx = rw / bw;
232
+ double sy = rh / bh;
233
+ double stx = rx1 - bx1 * sx;
234
+ double sty = ry1 - by1 * sy;
235
+ // S = [sx, 0, 0, sy, stx, sty]
236
+
237
+ // read the form's /Matrix (default identity). the Do operator
238
+ // applies this matrix, so our CTM must compensate:
239
+ // rendered = FormMatrix × OurCTM, we want rendered = S,
240
+ // so OurCTM = FormMatrix⁻¹ × S.
241
+ double ma = 1, mb = 0, mc = 0, md = 1, me = 0, mf = 0;
242
+ if (apDict.hasKey("/Matrix")) {
243
+ auto matrix = apDict.getKey("/Matrix");
244
+ if (matrix.isArray() && matrix.getArrayNItems() >= 6) {
245
+ ma = matrix.getArrayItem(0).getNumericValue();
246
+ mb = matrix.getArrayItem(1).getNumericValue();
247
+ mc = matrix.getArrayItem(2).getNumericValue();
248
+ md = matrix.getArrayItem(3).getNumericValue();
249
+ me = matrix.getArrayItem(4).getNumericValue();
250
+ mf = matrix.getArrayItem(5).getNumericValue();
251
+ }
252
+ }
253
+
254
+ // compute CTM = Matrix⁻¹ × S
255
+ // PDF matrix [a b c d e f] represents:
256
+ // | a b 0 |
257
+ // | c d 0 |
258
+ // | e f 1 |
259
+ double ca, cb, cc, cd, ce, cf;
260
+ double det = ma * md - mb * mc;
261
+ if (std::abs(det) < 1e-10) {
262
+ // degenerate matrix — use BBox→Rect directly
263
+ ca = sx;
264
+ cb = 0;
265
+ cc = 0;
266
+ cd = sy;
267
+ ce = stx;
268
+ cf = sty;
269
+ } else {
270
+ // inverse of Matrix
271
+ double ia = md / det;
272
+ double ib = -mb / det;
273
+ double ic = -mc / det;
274
+ double id = ma / det;
275
+ double ie = (mc * mf - md * me) / det;
276
+ double iif = (mb * me - ma * mf) / det;
277
+
278
+ // multiply M⁻¹ × S (M⁻¹ is left, S is right):
279
+ // [ia, ib, ic, id, ie, iif] × [sx, 0, 0, sy, stx, sty]
280
+ ca = ia * sx;
281
+ cb = ib * sy;
282
+ cc = ic * sx;
283
+ cd = id * sy;
284
+ ce = ie * sx + stx;
285
+ cf = iif * sy + sty;
286
+ }
287
+
288
+ // register appearance as a form XObject on the page
289
+ auto resources = pageObj.getKey("/Resources");
290
+ if (!resources.isDictionary()) {
291
+ resources = QPDFObjectHandle::newDictionary();
292
+ pageObj.replaceKey("/Resources", resources);
293
+ }
294
+ auto xobjects = resources.getKey("/XObject");
295
+ if (!xobjects.isDictionary()) {
296
+ xobjects = QPDFObjectHandle::newDictionary();
297
+ resources.replaceKey("/XObject", xobjects);
298
+ }
299
+
300
+ std::string xobjName = "/FlatForm" + std::to_string(i);
301
+ xobjects.replaceKey(xobjName, nAppearance);
302
+
303
+ // ensure the appearance stream has /Type /XObject /Subtype /Form
304
+ if (!apDict.hasKey("/Type"))
305
+ apDict.replaceKey("/Type", QPDFObjectHandle::newName("/XObject"));
306
+ if (!apDict.hasKey("/Subtype"))
307
+ apDict.replaceKey("/Subtype", QPDFObjectHandle::newName("/Form"));
308
+
309
+ // build content stream snippet with the full affine CTM
310
+ // use snprintf for locale-safe decimal formatting
311
+ char buf[384];
312
+ std::snprintf(buf, sizeof(buf),
313
+ "q %.6g %.6g %.6g %.6g %.6g %.6g cm %s Do Q\n", ca, cb,
314
+ cc, cd, ce, cf, xobjName.c_str());
315
+ allSnippets += buf;
316
+
317
+ widgetIndices.push_back(i);
318
+ } catch (...) {
319
+ continue;
320
+ }
321
+ }
322
+
323
+ if (!widgetIndices.empty()) {
324
+ // wrap existing page content in q/Q so the flattened form snippets
325
+ // run with the default (identity) CTM, regardless of any active
326
+ // transforms in the page content (e.g. top-down Y-flip)
327
+ page.addPageContents(QPDFObjectHandle::newStream(&qpdf, "q\n"), true);
328
+ page.addPageContents(
329
+ QPDFObjectHandle::newStream(&qpdf, "Q\n" + allSnippets), false);
330
+
331
+ // remove widget annotations (reverse order to preserve indices)
332
+ for (auto it = widgetIndices.rbegin(); it != widgetIndices.rend(); ++it)
333
+ annots.eraseItem(*it);
334
+ }
335
+ }
336
+
337
+ // remove the /AcroForm dictionary
338
+ root.removeKey("/AcroForm");
339
+ }
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // Page tree flattening — push inherited attributes to pages so QPDFWriter
343
+ // can generate a flat single-level page tree
344
+ // ---------------------------------------------------------------------------
345
+
346
+ void flattenPageTree(QPDF &qpdf) { qpdf.pushInheritedAttributesToPage(); }
@@ -0,0 +1,7 @@
1
+ #pragma once
2
+
3
+ #include <qpdf/QPDF.hh>
4
+
5
+ void deduplicateStreams(QPDF &qpdf);
6
+ void flattenForms(QPDF &qpdf);
7
+ void flattenPageTree(QPDF &qpdf);