pdf-parser.js 4.7.0 → 4.8.0

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/dist/raster.js ADDED
@@ -0,0 +1,815 @@
1
+ import { asArray, asName, asNumber, dictGet } from "./objects.js";
2
+ import { concatBytes } from "./bytes/writer.js";
3
+ import { BEZIER_KAPPA, applyMatrix, multiplyMatrices, scaleMatrix, translationMatrix } from "./matrix.js";
4
+ import { decodeStream } from "./filters.js";
5
+ import { parseSfnt } from "./sfnt.js";
6
+ import { buildCmapLookup } from "./cmap-table.js";
7
+ import { parseHead, parseMaxp } from "./font-tables.js";
8
+ import { readOptionalContent } from "./optional-content.js";
9
+ import { NOOP_DIAGNOSTIC_SINK, PdfParseError } from "./diagnostics.js";
10
+ import { openPdfDocument } from "./document.js";
11
+ import { throwIfAborted } from "./util/abort.js";
12
+ import { createFontResolver } from "./font-read.js";
13
+ import { readImageXObject } from "./images-read.js";
14
+ import { interpretContentStream } from "./interpret.js";
15
+ import { normalizeRotation, pageRotationTransform } from "./read.js";
16
+ import { parseGlyf } from "./glyf.js";
17
+ import { decodeGlyphOutline } from "./glyf-contours.js";
18
+ //#region src/raster.ts
19
+ const FALLBACK_GLYPH_WIDTH_PER_1000 = 500;
20
+ function regionPixels(extentPt, scale) {
21
+ return Math.max(1, Math.ceil(extentPt * scale - 1e-9));
22
+ }
23
+ function renderPdfPage(pdfBytes, pageIndex, options, rasteriser) {
24
+ const sink = options.sink ?? NOOP_DIAGNOSTIC_SINK;
25
+ if (options.scale !== void 0 && options.dpi !== void 0) throw new Error("renderPdfPage accepts at most one of scale and dpi; they name the same factor in different units (dpi = 72 x scale)");
26
+ const scale = options.dpi !== void 0 ? options.dpi / 72 : options.scale;
27
+ if (scale !== void 0 && !(scale > 0)) throw new Error(`renderPdfPage requires a positive scale factor (received scale ${options.scale}, dpi ${options.dpi})`);
28
+ const pixelsPerPt = scale ?? 1;
29
+ if (options.clipPt !== void 0 && (!(options.clipPt.widthPt > 0) || !(options.clipPt.heightPt > 0))) throw new Error("renderPdfPage clipPt must carry positive widthPt and heightPt; a zero-extent region has no pixels to render");
30
+ throwIfAborted(options.signal);
31
+ if (!hasPdfHeader(pdfBytes)) throw new PdfParseError("pdf/no-header", "no \"%PDF-\" header found within the first bytes of the file; this does not look like a PDF at all");
32
+ const doc = openPdfDocument(pdfBytes, sink);
33
+ const pages = doc.pages();
34
+ if (pageIndex < 0 || pageIndex >= pages.length) throw new PdfParseError("pdf/page-index-out-of-range", `page index ${pageIndex} is outside this document's page tree (it declares ${pages.length} page${pages.length === 1 ? "" : "s"})`);
35
+ const page = pages[pageIndex];
36
+ const mediaBox = readMediaBox(page);
37
+ let cropBox = readDeclaredPageBox(page, "CropBox") ?? mediaBox;
38
+ if (cropBox.urx - cropBox.llx <= 0 || cropBox.ury - cropBox.lly <= 0) {
39
+ sink({
40
+ code: "pdf/invalid-crop-box",
41
+ severity: "warning",
42
+ pageIndex,
43
+ message: "page /CropBox is degenerate (zero width or height); falling back to the /MediaBox as the visible region"
44
+ });
45
+ cropBox = mediaBox;
46
+ }
47
+ const rotation = normalizeRotation(asNumber(dictGet(page, "Rotate")));
48
+ const rotationResult = pageRotationTransform(rotation, mediaBox.urx - mediaBox.llx, mediaBox.ury - mediaBox.lly);
49
+ const visibleRect = rotatedRectBounds(cropBox, rotationResult.matrix);
50
+ const pageWidthPt = visibleRect.maxX - visibleRect.minX;
51
+ const pageHeightPt = visibleRect.maxY - visibleRect.minY;
52
+ const pageMatrix = multiplyMatrices(rotationResult.matrix, translationMatrix(-visibleRect.minX, -visibleRect.minY));
53
+ const requested = options.clipPt ?? {
54
+ xPt: 0,
55
+ yPt: 0,
56
+ widthPt: pageWidthPt,
57
+ heightPt: pageHeightPt
58
+ };
59
+ const clipLeft = Math.max(0, requested.xPt);
60
+ const clipBottom = Math.max(0, requested.yPt);
61
+ const clipRight = Math.min(pageWidthPt, requested.xPt + requested.widthPt);
62
+ const clipTop = Math.min(pageHeightPt, requested.yPt + requested.heightPt);
63
+ if (clipRight - clipLeft <= 0 || clipTop - clipBottom <= 0) throw new Error(`renderPdfPage clipPt does not intersect the page's visible region (clip x ${requested.xPt}..${requested.xPt + requested.widthPt}, y ${requested.yPt}..${requested.yPt + requested.heightPt}; page 0..${pageWidthPt} x 0..${pageHeightPt})`);
64
+ const regionWidthPt = clipRight - clipLeft;
65
+ const regionHeightPt = clipTop - clipBottom;
66
+ const widthPx = regionPixels(regionWidthPt, pixelsPerPt);
67
+ const heightPx = regionPixels(regionHeightPt, pixelsPerPt);
68
+ const pageToDeviceMatrix = [
69
+ pixelsPerPt,
70
+ 0,
71
+ 0,
72
+ -pixelsPerPt,
73
+ -clipLeft * pixelsPerPt,
74
+ (clipBottom + regionHeightPt) * pixelsPerPt
75
+ ];
76
+ const interpretToDeviceMatrix = multiplyMatrices(pageMatrix, pageToDeviceMatrix);
77
+ const resources = doc.resolveDict(dictGet(page, "Resources"));
78
+ rasteriser.beginPage({
79
+ widthPx,
80
+ heightPx,
81
+ scale: pixelsPerPt,
82
+ widthPt: regionWidthPt,
83
+ heightPt: regionHeightPt
84
+ });
85
+ if (resources === void 0) {
86
+ sink({
87
+ code: "pdf/object-missing-value",
88
+ severity: "warning",
89
+ message: "page has no /Resources dict; its content stream cannot be interpreted"
90
+ });
91
+ return rasteriser.finish();
92
+ }
93
+ const fontResolver = createFontResolver({
94
+ resolver: doc,
95
+ sink
96
+ });
97
+ const { layers, layerNameOf } = readOptionalContent(doc.catalog, doc, sink);
98
+ const hiddenLayers = new Set(layers.filter((layer) => !layer.visible).map((layer) => layer.name));
99
+ const extracted = interpretContentStream(readPageContentBytes(page, doc, sink), resources, {
100
+ fontMetrics: fontResolver.metrics,
101
+ resolver: doc,
102
+ sink,
103
+ layerNameOf
104
+ });
105
+ const outlineFaces = /* @__PURE__ */ new Map();
106
+ for (const item of extracted) {
107
+ throwIfAborted(options.signal);
108
+ if (item.layerName !== void 0 && hiddenLayers.has(item.layerName)) continue;
109
+ drawExtractedItem(item, doc, fontResolver, outlineFaces, interpretToDeviceMatrix, pixelsPerPt, rasteriser, sink);
110
+ }
111
+ return rasteriser.finish();
112
+ }
113
+ function drawExtractedItem(item, resolver, fontResolver, outlineFaces, interpretToDeviceMatrix, pixelsPerPt, rasteriser, sink) {
114
+ if (item.kind === "text") {
115
+ drawTextRun(item, fontResolver, resolver, outlineFaces, interpretToDeviceMatrix, rasteriser, sink);
116
+ return;
117
+ }
118
+ if (item.kind === "rect") {
119
+ drawRect(item, interpretToDeviceMatrix, pixelsPerPt, rasteriser);
120
+ return;
121
+ }
122
+ if (item.kind === "ellipse") {
123
+ drawEllipse(item, interpretToDeviceMatrix, pixelsPerPt, rasteriser);
124
+ return;
125
+ }
126
+ if (item.kind === "line") {
127
+ drawLine(item, interpretToDeviceMatrix, pixelsPerPt, rasteriser);
128
+ return;
129
+ }
130
+ if (item.kind === "path") {
131
+ drawPath(item, interpretToDeviceMatrix, pixelsPerPt, rasteriser);
132
+ return;
133
+ }
134
+ if (item.kind === "image") {
135
+ drawImageXObjectItem(item, resolver, interpretToDeviceMatrix, rasteriser, sink);
136
+ return;
137
+ }
138
+ drawInlineImageItem(item, resolver, interpretToDeviceMatrix, rasteriser, sink);
139
+ }
140
+ function drawRect(item, matrix, pixelsPerPt, rasteriser) {
141
+ const corners = [
142
+ applyMatrix(matrix, {
143
+ x: item.xPt,
144
+ y: item.yPt
145
+ }),
146
+ applyMatrix(matrix, {
147
+ x: item.xPt + item.widthPt,
148
+ y: item.yPt
149
+ }),
150
+ applyMatrix(matrix, {
151
+ x: item.xPt,
152
+ y: item.yPt + item.heightPt
153
+ }),
154
+ applyMatrix(matrix, {
155
+ x: item.xPt + item.widthPt,
156
+ y: item.yPt + item.heightPt
157
+ })
158
+ ];
159
+ const minX = Math.min(...corners.map((corner) => corner.x));
160
+ const minY = Math.min(...corners.map((corner) => corner.y));
161
+ const maxX = Math.max(...corners.map((corner) => corner.x));
162
+ const maxY = Math.max(...corners.map((corner) => corner.y));
163
+ if (item.fill !== void 0) rasteriser.draw({
164
+ kind: "fillRect",
165
+ xPx: minX,
166
+ yPx: minY,
167
+ widthPx: maxX - minX,
168
+ heightPx: maxY - minY,
169
+ color: item.fill
170
+ });
171
+ if (item.stroke !== void 0) rasteriser.draw({
172
+ kind: "path",
173
+ subpaths: [{
174
+ startXPx: minX,
175
+ startYPx: minY,
176
+ segments: [
177
+ {
178
+ kind: "line",
179
+ xPx: maxX,
180
+ yPx: minY
181
+ },
182
+ {
183
+ kind: "line",
184
+ xPx: maxX,
185
+ yPx: maxY
186
+ },
187
+ {
188
+ kind: "line",
189
+ xPx: minX,
190
+ yPx: maxY
191
+ }
192
+ ],
193
+ closed: true
194
+ }],
195
+ stroke: strokeSpec(item.stroke, void 0, pixelsPerPt)
196
+ });
197
+ }
198
+ function drawEllipse(item, matrix, pixelsPerPt, rasteriser) {
199
+ const cx = item.xPt + item.widthPt / 2;
200
+ const cy = item.yPt + item.heightPt / 2;
201
+ const rx = item.widthPt / 2;
202
+ const ry = item.heightPt / 2;
203
+ const dx = rx * BEZIER_KAPPA;
204
+ const dy = ry * BEZIER_KAPPA;
205
+ const cardinalPoints = [
206
+ [cx + rx, cy],
207
+ [cx, cy + ry],
208
+ [cx - rx, cy],
209
+ [cx, cy - ry]
210
+ ];
211
+ const controls = [
212
+ [cx + rx, cy + dy],
213
+ [cx + dx, cy + ry],
214
+ [cx - dx, cy + ry],
215
+ [cx - rx, cy + dy],
216
+ [cx - rx, cy - dy],
217
+ [cx - dx, cy - ry],
218
+ [cx + dx, cy - ry],
219
+ [cx + rx, cy - dy]
220
+ ];
221
+ const transformed = (point) => applyMatrix(matrix, {
222
+ x: point[0],
223
+ y: point[1]
224
+ });
225
+ const start = transformed(cardinalPoints[0]);
226
+ const segments = [];
227
+ for (let i = 0; i < 4; i++) {
228
+ const end = transformed(cardinalPoints[(i + 1) % 4]);
229
+ const c1 = transformed(controls[i * 2]);
230
+ const c2 = transformed(controls[i * 2 + 1]);
231
+ segments.push({
232
+ kind: "cubic",
233
+ c1xPx: c1.x,
234
+ c1yPx: c1.y,
235
+ c2xPx: c2.x,
236
+ c2yPx: c2.y,
237
+ xPx: end.x,
238
+ yPx: end.y
239
+ });
240
+ }
241
+ rasteriser.draw({
242
+ kind: "path",
243
+ subpaths: [{
244
+ startXPx: start.x,
245
+ startYPx: start.y,
246
+ segments,
247
+ closed: true
248
+ }],
249
+ ...item.fill !== void 0 ? { fill: {
250
+ color: item.fill,
251
+ fillRule: "nonzero"
252
+ } } : {},
253
+ ...item.stroke !== void 0 ? { stroke: strokeSpec(item.stroke, void 0, pixelsPerPt) } : {}
254
+ });
255
+ }
256
+ function drawLine(item, matrix, pixelsPerPt, rasteriser) {
257
+ const p1 = applyMatrix(matrix, {
258
+ x: item.x1Pt,
259
+ y: item.y1Pt
260
+ });
261
+ const p2 = applyMatrix(matrix, {
262
+ x: item.x2Pt,
263
+ y: item.y2Pt
264
+ });
265
+ if (item.style === "dotted") {
266
+ drawDottedSegment(p1, p2, item.widthPt * pixelsPerPt, item.color, rasteriser);
267
+ return;
268
+ }
269
+ rasteriser.draw({
270
+ kind: "path",
271
+ subpaths: [{
272
+ startXPx: p1.x,
273
+ startYPx: p1.y,
274
+ segments: [{
275
+ kind: "line",
276
+ xPx: p2.x,
277
+ yPx: p2.y
278
+ }],
279
+ closed: false
280
+ }],
281
+ stroke: strokeSpec({
282
+ color: item.color,
283
+ widthPt: item.widthPt
284
+ }, item.style, pixelsPerPt)
285
+ });
286
+ }
287
+ function drawPath(item, matrix, pixelsPerPt, rasteriser) {
288
+ if (item.stroke !== void 0 && item.style === "dotted") {
289
+ const widthPx = item.stroke.widthPt * pixelsPerPt;
290
+ for (const subpath of item.subpaths) {
291
+ let prev = applyMatrix(matrix, {
292
+ x: subpath.startXPt,
293
+ y: subpath.startYPt
294
+ });
295
+ const emitTo = (end) => {
296
+ drawDottedSegment(prev, end, widthPx, item.stroke.color, rasteriser);
297
+ prev = end;
298
+ };
299
+ for (const segment of subpath.segments) if (segment.kind === "line") emitTo(applyMatrix(matrix, {
300
+ x: segment.xPt,
301
+ y: segment.yPt
302
+ }));
303
+ else {
304
+ const flattened = flattenCubic(prev, applyMatrix(matrix, {
305
+ x: segment.c1xPt,
306
+ y: segment.c1yPt
307
+ }), applyMatrix(matrix, {
308
+ x: segment.c2xPt,
309
+ y: segment.c2yPt
310
+ }), applyMatrix(matrix, {
311
+ x: segment.xPt,
312
+ y: segment.yPt
313
+ }));
314
+ for (const point of flattened) emitTo(point);
315
+ }
316
+ }
317
+ return;
318
+ }
319
+ const subpaths = item.subpaths.map((subpath) => ({
320
+ startXPx: applyMatrix(matrix, {
321
+ x: subpath.startXPt,
322
+ y: subpath.startYPt
323
+ }).x,
324
+ startYPx: applyMatrix(matrix, {
325
+ x: subpath.startXPt,
326
+ y: subpath.startYPt
327
+ }).y,
328
+ segments: subpath.segments.map((segment) => {
329
+ if (segment.kind === "line") {
330
+ const end = applyMatrix(matrix, {
331
+ x: segment.xPt,
332
+ y: segment.yPt
333
+ });
334
+ return {
335
+ kind: "line",
336
+ xPx: end.x,
337
+ yPx: end.y
338
+ };
339
+ }
340
+ const c1 = applyMatrix(matrix, {
341
+ x: segment.c1xPt,
342
+ y: segment.c1yPt
343
+ });
344
+ const c2 = applyMatrix(matrix, {
345
+ x: segment.c2xPt,
346
+ y: segment.c2yPt
347
+ });
348
+ const end = applyMatrix(matrix, {
349
+ x: segment.xPt,
350
+ y: segment.yPt
351
+ });
352
+ return {
353
+ kind: "cubic",
354
+ c1xPx: c1.x,
355
+ c1yPx: c1.y,
356
+ c2xPx: c2.x,
357
+ c2yPx: c2.y,
358
+ xPx: end.x,
359
+ yPx: end.y
360
+ };
361
+ }),
362
+ closed: subpath.closed
363
+ }));
364
+ rasteriser.draw({
365
+ kind: "path",
366
+ subpaths,
367
+ ...item.fill !== void 0 ? { fill: {
368
+ color: item.fill,
369
+ fillRule: item.fillRule
370
+ } } : {},
371
+ ...item.stroke !== void 0 ? { stroke: strokeSpec(item.stroke, item.style, pixelsPerPt) } : {}
372
+ });
373
+ }
374
+ function drawImageXObjectItem(item, resolver, interpretToDeviceMatrix, rasteriser, sink) {
375
+ const xobjects = resolver.resolveDict(dictGet(item.resources, "XObject"));
376
+ const stream = xobjects !== void 0 ? resolver.resolve(dictGet(xobjects, item.resourceName)) : void 0;
377
+ if (stream?.kind !== "stream") return;
378
+ const image = readImageXObject(stream.dict, stream.raw, resolver, sink);
379
+ if (image === void 0) return;
380
+ drawImage(image, item.matrix, interpretToDeviceMatrix, rasteriser);
381
+ }
382
+ function drawInlineImageItem(item, resolver, interpretToDeviceMatrix, rasteriser, sink) {
383
+ const image = readImageXObject(item.dict, item.data, resolver, sink);
384
+ if (image === void 0) return;
385
+ drawImage(image, item.matrix, interpretToDeviceMatrix, rasteriser);
386
+ }
387
+ function drawImage(image, ctm, interpretToDeviceMatrix, rasteriser) {
388
+ const matrix = multiplyMatrices([
389
+ 1,
390
+ 0,
391
+ 0,
392
+ -1,
393
+ 0,
394
+ 1
395
+ ], multiplyMatrices(ctm, interpretToDeviceMatrix));
396
+ rasteriser.draw({
397
+ kind: "image",
398
+ format: image.format,
399
+ bytes: image.bytes,
400
+ sourceWidthPx: image.widthPx,
401
+ sourceHeightPx: image.heightPx,
402
+ matrix
403
+ });
404
+ }
405
+ function strokeSpec(stroke, style, pixelsPerPt) {
406
+ const widthPx = stroke.widthPt * pixelsPerPt;
407
+ if (style === "dashed") return {
408
+ color: stroke.color,
409
+ widthPx,
410
+ dashPx: [widthPx * 3, widthPx * 3]
411
+ };
412
+ return {
413
+ color: stroke.color,
414
+ widthPx
415
+ };
416
+ }
417
+ function drawDottedSegment(p1, p2, widthPx, color, rasteriser) {
418
+ const length = Math.hypot(p2.x - p1.x, p2.y - p1.y);
419
+ if (length === 0) return;
420
+ const spacing = Math.max(widthPx * 2, 1);
421
+ const dotHalf = widthPx / 2;
422
+ for (let distance = 0; distance <= length; distance += spacing) {
423
+ const t = distance / length;
424
+ rasteriser.draw({
425
+ kind: "fillRect",
426
+ xPx: p1.x + (p2.x - p1.x) * t - dotHalf,
427
+ yPx: p1.y + (p2.y - p1.y) * t - dotHalf,
428
+ widthPx,
429
+ heightPx: widthPx,
430
+ color
431
+ });
432
+ }
433
+ }
434
+ const STROKE_FLATTEN_TOLERANCE_PX = .05;
435
+ const MAX_FLATTEN_DEPTH = 16;
436
+ function flattenCubic(p0, c1, c2, p1) {
437
+ const points = [];
438
+ const flatten = (a, b, c, d, depth) => {
439
+ const chordX = d.x - a.x;
440
+ const chordY = d.y - a.y;
441
+ const chordLength = Math.hypot(chordX, chordY) || 1;
442
+ const dist1 = Math.abs((b.x - a.x) * chordY - (b.y - a.y) * chordX) / chordLength;
443
+ const dist2 = Math.abs((c.x - a.x) * chordY - (c.y - a.y) * chordX) / chordLength;
444
+ if (depth >= MAX_FLATTEN_DEPTH || Math.max(dist1, dist2) <= STROKE_FLATTEN_TOLERANCE_PX) {
445
+ points.push(d);
446
+ return;
447
+ }
448
+ const ab = {
449
+ x: (a.x + b.x) / 2,
450
+ y: (a.y + b.y) / 2
451
+ };
452
+ const bc = {
453
+ x: (b.x + c.x) / 2,
454
+ y: (b.y + c.y) / 2
455
+ };
456
+ const cd = {
457
+ x: (c.x + d.x) / 2,
458
+ y: (c.y + d.y) / 2
459
+ };
460
+ const abc = {
461
+ x: (ab.x + bc.x) / 2,
462
+ y: (ab.y + bc.y) / 2
463
+ };
464
+ const bcd = {
465
+ x: (bc.x + cd.x) / 2,
466
+ y: (bc.y + cd.y) / 2
467
+ };
468
+ const mid = {
469
+ x: (abc.x + bcd.x) / 2,
470
+ y: (abc.y + bcd.y) / 2
471
+ };
472
+ flatten(a, ab, abc, mid, depth + 1);
473
+ flatten(mid, bcd, cd, d, depth + 1);
474
+ };
475
+ flatten(p0, c1, c2, p1, 0);
476
+ return points;
477
+ }
478
+ function openEmbeddedProgram(descriptorOwner, resolver) {
479
+ const descriptor = resolver.resolveDict(dictGet(descriptorOwner, "FontDescriptor"));
480
+ if (descriptor === void 0) return { kind: "absent" };
481
+ for (const key of ["FontFile2", "FontFile3"]) {
482
+ const stream = resolver.resolve(dictGet(descriptor, key));
483
+ if (stream?.kind !== "stream") continue;
484
+ const bytes = decodeStream(stream.raw, stream.dict, NOOP_DIAGNOSTIC_SINK).bytes;
485
+ if (bytes.length >= 3 && bytes[0] === 1 && bytes[1] === 0 && bytes[2] === 4) return { kind: "cff" };
486
+ const sfnt = parseSfnt(bytes);
487
+ if (sfnt === void 0) continue;
488
+ if (sfnt.tables.has("CFF ")) return { kind: "cff" };
489
+ const head = parseHead(sfnt);
490
+ const maxp = parseMaxp(sfnt);
491
+ if (head === void 0 || maxp === void 0) continue;
492
+ const glyf = parseGlyf(sfnt, {
493
+ numGlyphs: maxp.numGlyphs,
494
+ indexToLocFormat: head.indexToLocFormat
495
+ });
496
+ if (glyf === void 0) continue;
497
+ return {
498
+ kind: "glyf",
499
+ sfnt,
500
+ face: {
501
+ composite: false,
502
+ glyf,
503
+ unitsPerEm: head.unitsPerEm,
504
+ glyphIdOf: () => void 0
505
+ }
506
+ };
507
+ }
508
+ return { kind: "absent" };
509
+ }
510
+ function resolveTextOutlineFace(fontResourceName, resources, resolver, fontResolver, outlineFaces, sink) {
511
+ const fontsDict = resolver.resolveDict(dictGet(resources, "Font"));
512
+ const fontDict = fontsDict !== void 0 ? resolver.resolveDict(dictGet(fontsDict, fontResourceName)) : void 0;
513
+ if (fontDict === void 0) return;
514
+ if (outlineFaces.has(fontDict)) return outlineFaces.get(fontDict);
515
+ const face = buildTextOutlineFace(fontDict, resolver, fontResolver, fontResourceName, resources, sink);
516
+ outlineFaces.set(fontDict, face);
517
+ return face;
518
+ }
519
+ function buildTextOutlineFace(fontDict, resolver, fontResolver, fontResourceName, resources, sink) {
520
+ const faceName = asName(dictGet(fontDict, "BaseFont")) ?? asName(dictGet(fontDict, "Subtype")) ?? "font";
521
+ const unavailable = (reason) => {
522
+ sink({
523
+ code: "raster/text-outlines-unavailable",
524
+ severity: "warning",
525
+ message: `font resource /${fontResourceName} (${faceName}) has no drawable outline (${reason}); its text is not rendered`
526
+ });
527
+ };
528
+ const cff = () => {
529
+ sink({
530
+ code: "raster/text-cff-outlines",
531
+ severity: "warning",
532
+ message: `font resource /${fontResourceName} (${faceName}) carries CFF outlines; this raster surface fills sfnt (TrueType/glyf) outlines only, so its text is not rendered rather than approximated`
533
+ });
534
+ };
535
+ const subtype = asName(dictGet(fontDict, "Subtype"));
536
+ if (subtype === "Type0") {
537
+ const encoding = resolver.resolve(dictGet(fontDict, "Encoding"));
538
+ if (encoding?.kind !== "name" || encoding.name !== "Identity-H") {
539
+ unavailable("a Type0 font whose /Encoding is not Identity-H (a predefined or embedded CMap this raster walk does not decode)");
540
+ return;
541
+ }
542
+ const descendants = asArray(dictGet(fontDict, "DescendantFonts"));
543
+ const descendant = descendants !== void 0 ? resolver.resolveDict(descendants[0]) : void 0;
544
+ if (descendant === void 0) {
545
+ unavailable("a Type0 font with no readable /DescendantFonts entry");
546
+ return;
547
+ }
548
+ const descendantSubtype = asName(dictGet(descendant, "Subtype"));
549
+ if (descendantSubtype === "CIDFontType0") {
550
+ cff();
551
+ return;
552
+ }
553
+ if (descendantSubtype !== "CIDFontType2") {
554
+ unavailable(`a descendant font of subtype ${descendantSubtype ?? "(none)"}`);
555
+ return;
556
+ }
557
+ const cidToGidMap = resolver.resolve(dictGet(descendant, "CIDToGIDMap"));
558
+ const program = openEmbeddedProgram(descendant, resolver);
559
+ if (program.kind === "cff") {
560
+ cff();
561
+ return;
562
+ }
563
+ if (program.kind === "absent") {
564
+ unavailable("no readable /FontFile2 (or glyf-bearing /FontFile3) stream");
565
+ return;
566
+ }
567
+ if (cidToGidMap !== void 0 && cidToGidMap.kind !== "name" && cidToGidMap.kind !== "stream") {
568
+ unavailable("a /CIDToGIDMap that is neither /Identity nor a readable stream");
569
+ return;
570
+ }
571
+ if (cidToGidMap?.kind === "stream") {
572
+ const decodedBytes = decodeStream(cidToGidMap.raw, cidToGidMap.dict, NOOP_DIAGNOSTIC_SINK).bytes;
573
+ const entries = [];
574
+ for (let i = 0; i + 1 < decodedBytes.length; i += 2) entries.push(decodedBytes[i] << 8 | decodedBytes[i + 1]);
575
+ return {
576
+ composite: true,
577
+ glyf: program.face.glyf,
578
+ unitsPerEm: program.face.unitsPerEm,
579
+ glyphIdOf: (codes, offset) => {
580
+ const cid = codes[offset] << 8 | codes[offset + 1];
581
+ return cid < entries.length ? entries[cid] : void 0;
582
+ }
583
+ };
584
+ }
585
+ return {
586
+ composite: true,
587
+ glyf: program.face.glyf,
588
+ unitsPerEm: program.face.unitsPerEm,
589
+ glyphIdOf: (codes, offset) => codes[offset] << 8 | codes[offset + 1]
590
+ };
591
+ }
592
+ if (subtype === "TrueType") {
593
+ const program = openEmbeddedProgram(fontDict, resolver);
594
+ if (program.kind === "cff") {
595
+ cff();
596
+ return;
597
+ }
598
+ if (program.kind === "absent") {
599
+ unavailable("no /FontDescriptor or no readable embedded program (a standard-14 or otherwise unembedded face states no outlines at all)");
600
+ return;
601
+ }
602
+ const cmapLookup = buildCmapLookup(program.sfnt);
603
+ if (cmapLookup === void 0) {
604
+ unavailable("an embedded program with no usable Unicode cmap subtable");
605
+ return;
606
+ }
607
+ return {
608
+ composite: false,
609
+ glyf: program.face.glyf,
610
+ unitsPerEm: program.face.unitsPerEm,
611
+ glyphIdOf: (codes, offset) => {
612
+ const codePoint = (fontResolver.resolve(fontResourceName, resources)?.decodeToUnicode(codes.subarray(offset, offset + 1)))?.codePointAt(0);
613
+ return codePoint === void 0 ? void 0 : cmapLookup(codePoint);
614
+ }
615
+ };
616
+ }
617
+ if (subtype === "Type1" || subtype === "MMType1") {
618
+ if (openEmbeddedProgram(fontDict, resolver).kind === "cff") return cff();
619
+ return unavailable("a Type 1 PostScript program, whose outlines are charstrings rather than sfnt glyf data");
620
+ }
621
+ return unavailable(`a font of subtype ${subtype ?? "(none)"} (a Type3 glyph-procedure font has no static outline at all)`);
622
+ }
623
+ function drawTextRun(item, fontResolver, resolver, outlineFaces, interpretToDeviceMatrix, rasteriser, sink) {
624
+ const face = resolveTextOutlineFace(item.fontResourceName, item.resources, resolver, fontResolver, outlineFaces, sink);
625
+ if (face === void 0) return;
626
+ const composite = face.composite;
627
+ const placements = [];
628
+ let offset = 0;
629
+ let cumulative = 0;
630
+ while (offset < item.codes.length) {
631
+ const advance = fontResolver.metrics.glyphAdvance(item.fontResourceName, item.resources, item.codes, offset);
632
+ const widthPer1000 = advance?.widthPer1000 ?? FALLBACK_GLYPH_WIDTH_PER_1000;
633
+ const byteLength = advance?.byteLengthConsumed ?? (composite ? 2 : 1);
634
+ placements.push({
635
+ glyphId: face.glyphIdOf(item.codes, offset),
636
+ advance: cumulative
637
+ });
638
+ cumulative += widthPer1000 / 1e3 * item.sizePt;
639
+ offset += byteLength;
640
+ }
641
+ const startDeviceMatrix = multiplyMatrices(item.startMatrix, interpretToDeviceMatrix);
642
+ const endDeviceMatrix = multiplyMatrices(item.endMatrix, interpretToDeviceMatrix);
643
+ const startPoint = applyMatrix(startDeviceMatrix, {
644
+ x: 0,
645
+ y: 0
646
+ });
647
+ const endPoint = applyMatrix(endDeviceMatrix, {
648
+ x: 0,
649
+ y: 0
650
+ });
651
+ const rawEndPoint = applyMatrix(startDeviceMatrix, {
652
+ x: cumulative,
653
+ y: 0
654
+ });
655
+ const rawExtent = Math.hypot(rawEndPoint.x - startPoint.x, rawEndPoint.y - startPoint.y);
656
+ const actualExtent = Math.hypot(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
657
+ const correction = rawExtent > 1e-9 && cumulative > 0 ? actualExtent / rawExtent : 1;
658
+ const glyphScale = scaleMatrix(1 / face.unitsPerEm, 1 / face.unitsPerEm);
659
+ for (const placement of placements) {
660
+ if (placement.glyphId === void 0) continue;
661
+ const outline = decodeGlyphOutline(face.glyf, placement.glyphId);
662
+ if (outline === void 0 || outline.contours.length === 0) continue;
663
+ const trm = multiplyMatrices(translationMatrix(placement.advance * correction, 0), item.startMatrix);
664
+ const subpaths = glyphOutlineSubpaths(outline, multiplyMatrices(glyphScale, multiplyMatrices(trm, interpretToDeviceMatrix)));
665
+ if (subpaths.length === 0) continue;
666
+ rasteriser.draw({
667
+ kind: "path",
668
+ subpaths,
669
+ fill: {
670
+ color: item.color,
671
+ fillRule: "nonzero"
672
+ }
673
+ });
674
+ }
675
+ }
676
+ function glyphOutlineSubpaths(outline, matrix) {
677
+ const subpaths = [];
678
+ for (const contour of outline.contours) {
679
+ if (contour.length < 3) continue;
680
+ const firstOn = contour.findIndex((point) => point.onCurve);
681
+ const contourPoints = contour.map((point) => ({
682
+ x: point.x,
683
+ y: point.y,
684
+ onCurve: point.onCurve
685
+ }));
686
+ const ordered = firstOn >= 0 ? [...contourPoints.slice(firstOn), ...contourPoints.slice(0, firstOn)] : contourPoints;
687
+ let current = firstOn >= 0 ? {
688
+ x: contourPoints[firstOn].x,
689
+ y: contourPoints[firstOn].y
690
+ } : {
691
+ x: (contourPoints[contourPoints.length - 1].x + contourPoints[0].x) / 2,
692
+ y: (contourPoints[contourPoints.length - 1].y + contourPoints[0].y) / 2
693
+ };
694
+ const start = current;
695
+ const segments = [];
696
+ let pendingOffCurve;
697
+ const emitQuad = (from, control, to) => {
698
+ const p0 = applyMatrix(matrix, from);
699
+ const q = applyMatrix(matrix, control);
700
+ const p1 = applyMatrix(matrix, to);
701
+ segments.push({
702
+ kind: "cubic",
703
+ c1xPx: p0.x + 2 / 3 * (q.x - p0.x),
704
+ c1yPx: p0.y + 2 / 3 * (q.y - p0.y),
705
+ c2xPx: p1.x + 2 / 3 * (q.x - p1.x),
706
+ c2yPx: p1.y + 2 / 3 * (q.y - p1.y),
707
+ xPx: p1.x,
708
+ yPx: p1.y
709
+ });
710
+ };
711
+ const emitLine = (to) => {
712
+ const p1 = applyMatrix(matrix, to);
713
+ segments.push({
714
+ kind: "line",
715
+ xPx: p1.x,
716
+ yPx: p1.y
717
+ });
718
+ };
719
+ for (const point of ordered) if (point.onCurve) {
720
+ if (pendingOffCurve === void 0) emitLine(point);
721
+ else emitQuad(current, pendingOffCurve, point);
722
+ current = point;
723
+ pendingOffCurve = void 0;
724
+ } else {
725
+ if (pendingOffCurve !== void 0) {
726
+ const implied = {
727
+ x: (pendingOffCurve.x + point.x) / 2,
728
+ y: (pendingOffCurve.y + point.y) / 2
729
+ };
730
+ emitQuad(current, pendingOffCurve, implied);
731
+ current = implied;
732
+ }
733
+ pendingOffCurve = point;
734
+ }
735
+ if (pendingOffCurve !== void 0) emitQuad(current, pendingOffCurve, start);
736
+ if (segments.length >= 2) {
737
+ const startPx = applyMatrix(matrix, start);
738
+ subpaths.push({
739
+ startXPx: startPx.x,
740
+ startYPx: startPx.y,
741
+ segments,
742
+ closed: true
743
+ });
744
+ }
745
+ }
746
+ return subpaths;
747
+ }
748
+ const PDF_HEADER_BYTES = new TextEncoder().encode("%PDF-");
749
+ const HEADER_SEARCH_WINDOW = 1024;
750
+ function hasPdfHeader(bytes) {
751
+ const window = bytes.subarray(0, Math.min(HEADER_SEARCH_WINDOW, bytes.length));
752
+ outer: for (let i = 0; i <= window.length - PDF_HEADER_BYTES.length; i++) {
753
+ for (let j = 0; j < PDF_HEADER_BYTES.length; j++) if (window[i + j] !== PDF_HEADER_BYTES[j]) continue outer;
754
+ return true;
755
+ }
756
+ return false;
757
+ }
758
+ function readDeclaredPageBox(page, key) {
759
+ const arr = asArray(dictGet(page, key));
760
+ if (arr === void 0) return;
761
+ const a = asNumber(arr[0]) ?? 0;
762
+ const b = asNumber(arr[1]) ?? 0;
763
+ const c = asNumber(arr[2]) ?? 0;
764
+ const d = asNumber(arr[3]) ?? 0;
765
+ return {
766
+ llx: Math.min(a, c),
767
+ lly: Math.min(b, d),
768
+ urx: Math.max(a, c),
769
+ ury: Math.max(b, d)
770
+ };
771
+ }
772
+ const DEFAULT_PAGE_WIDTH_PT = 612;
773
+ const DEFAULT_PAGE_HEIGHT_PT = 792;
774
+ function readMediaBox(page) {
775
+ return readDeclaredPageBox(page, "MediaBox") ?? {
776
+ llx: 0,
777
+ lly: 0,
778
+ urx: DEFAULT_PAGE_WIDTH_PT,
779
+ ury: DEFAULT_PAGE_HEIGHT_PT
780
+ };
781
+ }
782
+ function rotatedRectBounds(rect, matrix) {
783
+ const transformed = [
784
+ [rect.llx, rect.lly],
785
+ [rect.urx, rect.lly],
786
+ [rect.llx, rect.ury],
787
+ [rect.urx, rect.ury]
788
+ ].map(([x, y]) => applyMatrix(matrix, {
789
+ x,
790
+ y
791
+ }));
792
+ const xs = transformed.map((point) => point.x);
793
+ const ys = transformed.map((point) => point.y);
794
+ return {
795
+ minX: Math.min(...xs),
796
+ minY: Math.min(...ys),
797
+ maxX: Math.max(...xs),
798
+ maxY: Math.max(...ys)
799
+ };
800
+ }
801
+ function readPageContentBytes(page, resolver, sink) {
802
+ const contentsObj = resolver.resolve(dictGet(page, "Contents"));
803
+ if (contentsObj?.kind === "stream") return decodeStream(contentsObj.raw, contentsObj.dict, sink).bytes;
804
+ if (contentsObj?.kind === "array") {
805
+ const chunks = [];
806
+ for (const entry of contentsObj.items) {
807
+ const streamObj = resolver.resolve(entry);
808
+ if (streamObj?.kind === "stream") chunks.push(decodeStream(streamObj.raw, streamObj.dict, sink).bytes, new Uint8Array([10]));
809
+ }
810
+ return concatBytes(chunks);
811
+ }
812
+ return /* @__PURE__ */ new Uint8Array(0);
813
+ }
814
+ //#endregion
815
+ export { renderPdfPage };