qpdf-compress 0.2.0 → 0.4.1

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.
@@ -0,0 +1,480 @@
1
+ #include "font_subset.h"
2
+
3
+ #include <algorithm>
4
+ #include <cstring>
5
+ #include <map>
6
+ #include <queue>
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // TrueType binary helpers — big-endian reads
10
+ // ---------------------------------------------------------------------------
11
+
12
+ static uint16_t readU16(const uint8_t *p) {
13
+ return static_cast<uint16_t>((p[0] << 8) | p[1]);
14
+ }
15
+
16
+ static uint32_t readU32(const uint8_t *p) {
17
+ return (static_cast<uint32_t>(p[0]) << 24) |
18
+ (static_cast<uint32_t>(p[1]) << 16) |
19
+ (static_cast<uint32_t>(p[2]) << 8) | p[3];
20
+ }
21
+
22
+ static int16_t readI16(const uint8_t *p) {
23
+ return static_cast<int16_t>(readU16(p));
24
+ }
25
+
26
+ static void writeU16(uint8_t *p, uint16_t v) {
27
+ p[0] = static_cast<uint8_t>(v >> 8);
28
+ p[1] = static_cast<uint8_t>(v);
29
+ }
30
+
31
+ static void writeU32(uint8_t *p, uint32_t v) {
32
+ p[0] = static_cast<uint8_t>(v >> 24);
33
+ p[1] = static_cast<uint8_t>(v >> 16);
34
+ p[2] = static_cast<uint8_t>(v >> 8);
35
+ p[3] = static_cast<uint8_t>(v);
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Table directory parsing
40
+ // ---------------------------------------------------------------------------
41
+
42
+ struct TableRecord {
43
+ uint32_t tag;
44
+ uint32_t checksum;
45
+ uint32_t offset;
46
+ uint32_t length;
47
+ };
48
+
49
+ static uint32_t tag(const char *s) {
50
+ return (static_cast<uint32_t>(s[0]) << 24) |
51
+ (static_cast<uint32_t>(s[1]) << 16) |
52
+ (static_cast<uint32_t>(s[2]) << 8) | s[3];
53
+ }
54
+
55
+ static bool parseTables(const uint8_t *data, size_t size,
56
+ std::map<uint32_t, TableRecord> &tables) {
57
+ if (size < 12)
58
+ return false;
59
+
60
+ uint16_t numTables = readU16(data + 4);
61
+ if (size < 12 + static_cast<size_t>(numTables) * 16)
62
+ return false;
63
+
64
+ for (uint16_t i = 0; i < numTables; ++i) {
65
+ const uint8_t *entry = data + 12 + i * 16;
66
+ TableRecord rec;
67
+ rec.tag = readU32(entry);
68
+ rec.checksum = readU32(entry + 4);
69
+ rec.offset = readU32(entry + 8);
70
+ rec.length = readU32(entry + 12);
71
+
72
+ if (rec.offset + rec.length > size)
73
+ return false;
74
+
75
+ tables[rec.tag] = rec;
76
+ }
77
+ return true;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // cmap parsing — extract character code → glyph ID mapping
82
+ // ---------------------------------------------------------------------------
83
+
84
+ std::set<uint16_t> mapCodesToGlyphIds(const uint8_t *data, size_t size,
85
+ const std::set<uint16_t> &charCodes) {
86
+ std::set<uint16_t> glyphIds;
87
+ glyphIds.insert(0); // always keep .notdef
88
+
89
+ std::map<uint32_t, TableRecord> tables;
90
+ if (!parseTables(data, size, tables))
91
+ return glyphIds;
92
+
93
+ auto it = tables.find(tag("cmap"));
94
+ if (it == tables.end())
95
+ return glyphIds;
96
+
97
+ const uint8_t *cmap = data + it->second.offset;
98
+ size_t cmapLen = it->second.length;
99
+ if (cmapLen < 4)
100
+ return glyphIds;
101
+
102
+ uint16_t numSubtables = readU16(cmap + 2);
103
+
104
+ // find a suitable subtable (prefer format 4 — the most common)
105
+ const uint8_t *subtable = nullptr;
106
+ for (uint16_t i = 0; i < numSubtables; ++i) {
107
+ if (4 + i * 8 + 8 > cmapLen)
108
+ break;
109
+ const uint8_t *entry = cmap + 4 + i * 8;
110
+ uint32_t subtableOffset = readU32(entry + 4);
111
+ if (subtableOffset + 2 > cmapLen)
112
+ continue;
113
+
114
+ uint16_t format = readU16(cmap + subtableOffset);
115
+ if (format == 4) {
116
+ subtable = cmap + subtableOffset;
117
+ break;
118
+ }
119
+ }
120
+
121
+ // fallback: use first subtable
122
+ if (!subtable && numSubtables > 0) {
123
+ uint32_t subtableOffset = readU32(cmap + 4 + 4);
124
+ if (subtableOffset + 2 <= cmapLen)
125
+ subtable = cmap + subtableOffset;
126
+ }
127
+
128
+ if (!subtable)
129
+ return glyphIds;
130
+
131
+ uint16_t format = readU16(subtable);
132
+
133
+ if (format == 4) {
134
+ // format 4: segment mapping to delta values
135
+ if (subtable + 14 > cmap + cmapLen)
136
+ return glyphIds;
137
+
138
+ uint16_t segCount = readU16(subtable + 6) / 2;
139
+ const uint8_t *endCodes = subtable + 14;
140
+ const uint8_t *startCodes =
141
+ endCodes + segCount * 2 + 2; // +2 for reservedPad
142
+ const uint8_t *idDeltas = startCodes + segCount * 2;
143
+ const uint8_t *idRangeOffsets = idDeltas + segCount * 2;
144
+
145
+ // bounds check
146
+ if (idRangeOffsets + segCount * 2 > cmap + cmapLen)
147
+ return glyphIds;
148
+
149
+ for (uint16_t code : charCodes) {
150
+ for (uint16_t seg = 0; seg < segCount; ++seg) {
151
+ uint16_t endCode = readU16(endCodes + seg * 2);
152
+ uint16_t startCode = readU16(startCodes + seg * 2);
153
+
154
+ if (code < startCode || code > endCode)
155
+ continue;
156
+
157
+ uint16_t rangeOffset = readU16(idRangeOffsets + seg * 2);
158
+ uint16_t delta = readU16(idDeltas + seg * 2);
159
+
160
+ uint16_t glyphId;
161
+ if (rangeOffset == 0) {
162
+ glyphId = static_cast<uint16_t>((code + delta) & 0xFFFF);
163
+ } else {
164
+ const uint8_t *glyphIdAddr =
165
+ idRangeOffsets + seg * 2 + rangeOffset +
166
+ 2 * static_cast<uint16_t>(code - startCode);
167
+ if (glyphIdAddr + 2 > cmap + cmapLen)
168
+ break;
169
+ glyphId = readU16(glyphIdAddr);
170
+ if (glyphId != 0)
171
+ glyphId = static_cast<uint16_t>((glyphId + delta) & 0xFFFF);
172
+ }
173
+
174
+ if (glyphId != 0)
175
+ glyphIds.insert(glyphId);
176
+ break;
177
+ }
178
+ }
179
+ } else if (format == 0) {
180
+ // format 0: byte encoding table
181
+ if (subtable + 6 + 256 > cmap + cmapLen)
182
+ return glyphIds;
183
+ for (uint16_t code : charCodes) {
184
+ if (code < 256) {
185
+ uint8_t gid = subtable[6 + code];
186
+ if (gid != 0)
187
+ glyphIds.insert(gid);
188
+ }
189
+ }
190
+ }
191
+
192
+ return glyphIds;
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Collect composite glyph dependencies
197
+ // ---------------------------------------------------------------------------
198
+
199
+ static void collectCompositeGlyphs(const uint8_t *glyfData, size_t glyfLen,
200
+ const uint8_t *locaData, bool locaLong,
201
+ uint16_t numGlyphs,
202
+ std::set<uint16_t> &glyphIds) {
203
+ std::queue<uint16_t> toProcess;
204
+ for (uint16_t gid : glyphIds)
205
+ toProcess.push(gid);
206
+
207
+ while (!toProcess.empty()) {
208
+ uint16_t gid = toProcess.front();
209
+ toProcess.pop();
210
+
211
+ if (gid >= numGlyphs)
212
+ continue;
213
+
214
+ uint32_t offset, nextOffset;
215
+ if (locaLong) {
216
+ offset = readU32(locaData + gid * 4);
217
+ nextOffset = readU32(locaData + (gid + 1) * 4);
218
+ } else {
219
+ offset = static_cast<uint32_t>(readU16(locaData + gid * 2)) * 2;
220
+ nextOffset = static_cast<uint32_t>(readU16(locaData + (gid + 1) * 2)) * 2;
221
+ }
222
+
223
+ if (offset >= nextOffset || offset >= glyfLen)
224
+ continue;
225
+
226
+ const uint8_t *glyph = glyfData + offset;
227
+ size_t glyphLen = nextOffset - offset;
228
+ if (glyphLen < 10)
229
+ continue;
230
+
231
+ int16_t numContours = readI16(glyph);
232
+ if (numContours >= 0)
233
+ continue; // simple glyph, no dependencies
234
+
235
+ // composite glyph — parse component records
236
+ size_t pos = 10; // skip header
237
+ while (pos + 4 <= glyphLen) {
238
+ uint16_t flags = readU16(glyph + pos);
239
+ uint16_t componentGid = readU16(glyph + pos + 2);
240
+ pos += 4;
241
+
242
+ if (componentGid < numGlyphs &&
243
+ glyphIds.find(componentGid) == glyphIds.end()) {
244
+ glyphIds.insert(componentGid);
245
+ toProcess.push(componentGid);
246
+ }
247
+
248
+ // skip arguments based on flags
249
+ if (flags & 0x0001) // ARG_1_AND_2_ARE_WORDS
250
+ pos += 4;
251
+ else
252
+ pos += 2;
253
+
254
+ if (flags & 0x0008) // WE_HAVE_A_SCALE
255
+ pos += 2;
256
+ else if (flags & 0x0040) // WE_HAVE_AN_X_AND_Y_SCALE
257
+ pos += 4;
258
+ else if (flags & 0x0080) // WE_HAVE_A_TWO_BY_TWO
259
+ pos += 8;
260
+
261
+ if (!(flags & 0x0020)) // MORE_COMPONENTS
262
+ break;
263
+ }
264
+ }
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // TrueType font subsetting — keep only used glyphs
269
+ // ---------------------------------------------------------------------------
270
+
271
+ static uint32_t calcChecksum(const uint8_t *data, size_t length) {
272
+ uint32_t sum = 0;
273
+ size_t nLongs = (length + 3) / 4;
274
+ for (size_t i = 0; i < nLongs; ++i) {
275
+ uint32_t val = 0;
276
+ for (size_t j = 0; j < 4; ++j) {
277
+ size_t idx = i * 4 + j;
278
+ val = (val << 8) | (idx < length ? data[idx] : 0);
279
+ }
280
+ sum += val;
281
+ }
282
+ return sum;
283
+ }
284
+
285
+ bool subsetTrueTypeFont(const uint8_t *data, size_t size,
286
+ const std::set<uint16_t> &usedGlyphIds,
287
+ std::vector<uint8_t> &output) {
288
+ std::map<uint32_t, TableRecord> tables;
289
+ if (!parseTables(data, size, tables))
290
+ return false;
291
+
292
+ // required tables
293
+ auto headIt = tables.find(tag("head"));
294
+ auto maxpIt = tables.find(tag("maxp"));
295
+ auto locaIt = tables.find(tag("loca"));
296
+ auto glyfIt = tables.find(tag("glyf"));
297
+
298
+ if (headIt == tables.end() || maxpIt == tables.end() ||
299
+ locaIt == tables.end() || glyfIt == tables.end())
300
+ return false;
301
+
302
+ const uint8_t *headData = data + headIt->second.offset;
303
+ if (headIt->second.length < 54)
304
+ return false;
305
+
306
+ bool locaLong = readI16(headData + 50) == 1;
307
+
308
+ const uint8_t *maxpData = data + maxpIt->second.offset;
309
+ if (maxpIt->second.length < 6)
310
+ return false;
311
+ uint16_t numGlyphs = readU16(maxpData + 4);
312
+
313
+ const uint8_t *locaData = data + locaIt->second.offset;
314
+ const uint8_t *glyfData = data + glyfIt->second.offset;
315
+ size_t glyfLen = glyfIt->second.length;
316
+
317
+ // collect all needed glyphs including composite dependencies
318
+ auto allGlyphs = usedGlyphIds;
319
+ allGlyphs.insert(0); // always keep .notdef
320
+ collectCompositeGlyphs(glyfData, glyfLen, locaData, locaLong, numGlyphs,
321
+ allGlyphs);
322
+
323
+ // build new glyf table — map old glyph IDs to new glyph data
324
+ // we preserve old glyph IDs (don't remap) to keep cmap valid —
325
+ // instead we zero out unused glyph slots
326
+ std::vector<uint8_t> newGlyf;
327
+ std::vector<uint32_t> newLoca(numGlyphs + 1);
328
+
329
+ for (uint16_t gid = 0; gid < numGlyphs; ++gid) {
330
+ newLoca[gid] = static_cast<uint32_t>(newGlyf.size());
331
+
332
+ if (allGlyphs.find(gid) == allGlyphs.end())
333
+ continue; // empty glyph — loca points to same offset as next
334
+
335
+ uint32_t offset, nextOffset;
336
+ if (locaLong) {
337
+ if ((gid + 1) * 4 + 4 > locaIt->second.length)
338
+ continue;
339
+ offset = readU32(locaData + gid * 4);
340
+ nextOffset = readU32(locaData + (gid + 1) * 4);
341
+ } else {
342
+ if ((gid + 1) * 2 + 2 > locaIt->second.length)
343
+ continue;
344
+ offset = static_cast<uint32_t>(readU16(locaData + gid * 2)) * 2;
345
+ nextOffset = static_cast<uint32_t>(readU16(locaData + (gid + 1) * 2)) * 2;
346
+ }
347
+
348
+ if (offset >= nextOffset || offset >= glyfLen)
349
+ continue;
350
+
351
+ uint32_t len =
352
+ std::min(nextOffset - offset, static_cast<uint32_t>(glyfLen - offset));
353
+ newGlyf.insert(newGlyf.end(), glyfData + offset, glyfData + offset + len);
354
+
355
+ // pad to 2-byte boundary (for short loca) or 4-byte boundary (long loca)
356
+ size_t align = locaLong ? 4 : 2;
357
+ while (newGlyf.size() % align != 0)
358
+ newGlyf.push_back(0);
359
+ }
360
+ newLoca[numGlyphs] = static_cast<uint32_t>(newGlyf.size());
361
+
362
+ // build new loca table
363
+ std::vector<uint8_t> newLocaData;
364
+ if (locaLong) {
365
+ newLocaData.resize((numGlyphs + 1) * 4);
366
+ for (uint16_t i = 0; i <= numGlyphs; ++i)
367
+ writeU32(newLocaData.data() + i * 4, newLoca[i]);
368
+ } else {
369
+ newLocaData.resize((numGlyphs + 1) * 2);
370
+ for (uint16_t i = 0; i <= numGlyphs; ++i)
371
+ writeU16(newLocaData.data() + i * 2,
372
+ static_cast<uint16_t>(newLoca[i] / 2));
373
+ }
374
+
375
+ // subset hmtx: zero widths for unused glyphs
376
+ auto hheaIt = tables.find(tag("hhea"));
377
+ auto hmtxIt = tables.find(tag("hmtx"));
378
+ std::vector<uint8_t> newHmtx;
379
+
380
+ if (hheaIt != tables.end() && hmtxIt != tables.end() &&
381
+ hheaIt->second.length >= 36) {
382
+ uint16_t numHMetrics = readU16(data + hheaIt->second.offset + 34);
383
+ const uint8_t *hmtxData = data + hmtxIt->second.offset;
384
+ size_t hmtxLen = hmtxIt->second.length;
385
+
386
+ newHmtx.assign(hmtxData, hmtxData + hmtxLen);
387
+
388
+ for (uint16_t gid = 0; gid < numGlyphs; ++gid) {
389
+ if (allGlyphs.find(gid) != allGlyphs.end())
390
+ continue;
391
+
392
+ if (gid < numHMetrics) {
393
+ // long metric entry: advanceWidth(2) + lsb(2)
394
+ size_t off = static_cast<size_t>(gid) * 4;
395
+ if (off + 4 <= newHmtx.size()) {
396
+ writeU16(newHmtx.data() + off, 0);
397
+ writeU16(newHmtx.data() + off + 2, 0);
398
+ }
399
+ } else {
400
+ // short metric: just lsb(2) after the long entries
401
+ size_t off = static_cast<size_t>(numHMetrics) * 4 +
402
+ static_cast<size_t>(gid - numHMetrics) * 2;
403
+ if (off + 2 <= newHmtx.size())
404
+ writeU16(newHmtx.data() + off, 0);
405
+ }
406
+ }
407
+ }
408
+
409
+ // collect tables to write (all original tables, replacing glyf/loca/hmtx)
410
+ struct OutputTable {
411
+ uint32_t tag;
412
+ std::vector<uint8_t> data;
413
+ };
414
+ std::vector<OutputTable> outTables;
415
+
416
+ for (auto &[tableTag, rec] : tables) {
417
+ OutputTable ot;
418
+ ot.tag = tableTag;
419
+
420
+ if (tableTag == tag("glyf")) {
421
+ ot.data = newGlyf;
422
+ } else if (tableTag == tag("loca")) {
423
+ ot.data = newLocaData;
424
+ } else if (tableTag == tag("hmtx") && !newHmtx.empty()) {
425
+ ot.data = newHmtx;
426
+ } else {
427
+ ot.data.assign(data + rec.offset, data + rec.offset + rec.length);
428
+ }
429
+
430
+ outTables.push_back(std::move(ot));
431
+ }
432
+
433
+ // sort by tag (recommended for efficient access)
434
+ std::sort(
435
+ outTables.begin(), outTables.end(),
436
+ [](const OutputTable &a, const OutputTable &b) { return a.tag < b.tag; });
437
+
438
+ // calculate output size
439
+ uint16_t numOutTables = static_cast<uint16_t>(outTables.size());
440
+ size_t headerSize = 12 + numOutTables * 16;
441
+ size_t totalSize = headerSize;
442
+ for (auto &ot : outTables)
443
+ totalSize += ((ot.data.size() + 3) / 4) * 4; // pad to 4 bytes
444
+
445
+ output.resize(totalSize, 0);
446
+ uint8_t *out = output.data();
447
+
448
+ // write offset table
449
+ writeU32(out, readU32(data)); // sfVersion (same as original)
450
+ writeU16(out + 4, numOutTables);
451
+
452
+ // searchRange, entrySelector, rangeShift
453
+ uint16_t searchRange = 1;
454
+ uint16_t entrySelector = 0;
455
+ while (searchRange * 2 <= numOutTables) {
456
+ searchRange *= 2;
457
+ ++entrySelector;
458
+ }
459
+ searchRange *= 16;
460
+ writeU16(out + 6, searchRange);
461
+ writeU16(out + 8, entrySelector);
462
+ writeU16(out + 10, numOutTables * 16 - searchRange);
463
+
464
+ // write table records and data
465
+ size_t dataOffset = headerSize;
466
+ for (uint16_t i = 0; i < numOutTables; ++i) {
467
+ auto &ot = outTables[i];
468
+ uint8_t *rec = out + 12 + i * 16;
469
+
470
+ writeU32(rec, ot.tag);
471
+ writeU32(rec + 4, calcChecksum(ot.data.data(), ot.data.size()));
472
+ writeU32(rec + 8, static_cast<uint32_t>(dataOffset));
473
+ writeU32(rec + 12, static_cast<uint32_t>(ot.data.size()));
474
+
475
+ memcpy(out + dataOffset, ot.data.data(), ot.data.size());
476
+ dataOffset += ((ot.data.size() + 3) / 4) * 4;
477
+ }
478
+
479
+ return true;
480
+ }
@@ -0,0 +1,17 @@
1
+ #pragma once
2
+
3
+ #include <cstddef>
4
+ #include <cstdint>
5
+ #include <set>
6
+ #include <vector>
7
+
8
+ // subset a TrueType font binary (TTF), keeping only the specified glyph IDs.
9
+ // glyph 0 (.notdef) is always retained. returns true on success.
10
+ bool subsetTrueTypeFont(const uint8_t *data, size_t size,
11
+ const std::set<uint16_t> &usedGlyphIds,
12
+ std::vector<uint8_t> &output);
13
+
14
+ // map character codes to glyph IDs using the font's cmap table.
15
+ // returns a set of glyph IDs that correspond to the given character codes.
16
+ std::set<uint16_t> mapCodesToGlyphIds(const uint8_t *data, size_t size,
17
+ const std::set<uint16_t> &charCodes);