@jasy/pdf 1.0.0-alpha.4 → 1.0.0-alpha.5

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.
Files changed (42) hide show
  1. package/README.md +5 -2
  2. package/dist/api/content.d.ts +3 -0
  3. package/dist/api/content.js +1 -0
  4. package/dist/api/structure.d.ts +19 -0
  5. package/dist/api/structure.js +25 -0
  6. package/dist/api/table.js +13 -5
  7. package/dist/api/text.d.ts +4 -1
  8. package/dist/api/text.js +1 -0
  9. package/dist/elements/image-element.d.ts +5 -1
  10. package/dist/elements/image-element.js +3 -1
  11. package/dist/elements/layout/struct-group.d.ts +28 -0
  12. package/dist/elements/layout/struct-group.js +42 -0
  13. package/dist/elements/pdf-element.d.ts +4 -0
  14. package/dist/elements/pdf-element.js +15 -0
  15. package/dist/elements/text-element.d.ts +7 -1
  16. package/dist/elements/text-element.js +5 -2
  17. package/dist/ir/display-list.d.ts +79 -1
  18. package/dist/layout/fragmentation.js +9 -2
  19. package/dist/renderer/emoji-image.d.ts +9 -0
  20. package/dist/renderer/emoji-image.js +68 -0
  21. package/dist/renderer/image-renderer.d.ts +1 -1
  22. package/dist/renderer/image-renderer.js +10 -2
  23. package/dist/renderer/page-renderer.js +19 -4
  24. package/dist/renderer/pdf-backend.d.ts +6 -2
  25. package/dist/renderer/pdf-backend.js +59 -3
  26. package/dist/renderer/pdf-renderer.js +11 -0
  27. package/dist/renderer/struct-group-renderer.d.ts +10 -0
  28. package/dist/renderer/struct-group-renderer.js +24 -0
  29. package/dist/renderer/text-renderer.d.ts +3 -0
  30. package/dist/renderer/text-renderer.js +182 -2
  31. package/dist/text/emoji-codepoints.d.ts +1 -0
  32. package/dist/text/emoji-codepoints.js +29 -0
  33. package/dist/text/line-breaker.js +6 -3
  34. package/dist/utils/pdf-object-manager.d.ts +20 -1
  35. package/dist/utils/pdf-object-manager.js +0 -0
  36. package/dist/utils/struct-tree.d.ts +44 -0
  37. package/dist/utils/struct-tree.js +130 -0
  38. package/dist/utils/ttf-parser.d.ts +73 -0
  39. package/dist/utils/ttf-parser.js +456 -16
  40. package/dist/utils/ua-xmp.d.ts +9 -0
  41. package/dist/utils/ua-xmp.js +32 -0
  42. package/package.json +1 -1
@@ -1,7 +1,35 @@
1
1
  // Parses the metric tables of a TrueType (.ttf) font - the binary counterpart to AFMParser.
2
2
  // Slice 1: glyph advance widths (hmtx) + the Unicode→glyph map (cmap), enough to compute
3
3
  // text width the same way AFMParser does for the standard-14. Embedding/subsetting come later.
4
- import { i16, latin1FromBytes, u16, u32 } from "./bytes.js";
4
+ import { i16, latin1FromBytes, u8, u16, u32 } from "./bytes.js";
5
+ const IDENTITY = [1, 0, 0, 1, 0, 0];
6
+ function isIdentity(m) {
7
+ return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
8
+ }
9
+ // compose(m, n): the transform that applies n first, then m (the matrix product m . n).
10
+ function compose(m, n) {
11
+ return [
12
+ m[0] * n[0] + m[2] * n[1],
13
+ m[1] * n[0] + m[3] * n[1],
14
+ m[0] * n[2] + m[2] * n[3],
15
+ m[1] * n[2] + m[3] * n[3],
16
+ m[0] * n[4] + m[2] * n[5] + m[4],
17
+ m[1] * n[4] + m[3] * n[5] + m[5],
18
+ ];
19
+ }
20
+ // A transform applied about a center point: translate(c) . t . translate(-c).
21
+ function aroundCenter(t, cx, cy) {
22
+ return compose([1, 0, 0, 1, cx, cy], compose(t, [1, 0, 0, 1, -cx, -cy]));
23
+ }
24
+ // Applies an affine to a point (font units).
25
+ function applyAffine(m, x, y) {
26
+ return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
27
+ }
28
+ // The affine's mean scale factor sqrt(|det|), used to scale a gradient radius (a circle can only
29
+ // stay a circle under uniform scaling; this is the best single-radius approximation otherwise).
30
+ function detScale(m) {
31
+ return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
32
+ }
5
33
  export class TTFParser {
6
34
  constructor(data) {
7
35
  this.data = data;
@@ -12,10 +40,19 @@ export class TTFParser {
12
40
  this.bbox = [0, 0, 0, 0];
13
41
  this.numGlyphs = 0;
14
42
  this.numHMetrics = 0;
43
+ this.indexToLocFormat = 0; // head[50]: 0 = short loca (offsets/2), 1 = long loca
15
44
  this.advanceWidths = []; // per glyph id, in font units
16
45
  this.cmap = new Map(); // codepoint → glyph id (format 4)
17
46
  this.cmapGroups = []; // format 12, searched on lookup
18
47
  this.tables = {};
48
+ // COLR v0: base glyph id → its slice of the layer-record array. Empty when the font has no
49
+ // v0 color layers, so getColorGlyph falls back to v1 or to normal (monochrome) drawing.
50
+ this.colrBase = new Map();
51
+ this.colrLayersOffset = 0; // byte offset of the v0 layer-record array in the COLR table
52
+ this.palette = []; // CPAL palette 0, indexed by palette entry
53
+ // COLR v1: base glyph id → the absolute offset of its Paint table (a recursive paint graph).
54
+ this.colrBaseV1 = new Map();
55
+ this.colrLayerListOffset = 0; // absolute offset of the v1 LayerList (paint-offset array)
19
56
  this.readTableDirectory();
20
57
  const head = this.table("head").offset;
21
58
  this.unitsPerEm = u16(this.data, head + 18);
@@ -30,8 +67,13 @@ export class TTFParser {
30
67
  const hhea = this.table("hhea").offset;
31
68
  this.ascent = this.toGlyphSpace(i16(this.data, hhea + 4));
32
69
  this.descent = this.toGlyphSpace(i16(this.data, hhea + 6));
70
+ this.indexToLocFormat = i16(this.data, head + 50);
33
71
  this.readHmtx();
34
72
  this.readCmap();
73
+ if (this.tables["COLR"] && this.tables["CPAL"]) {
74
+ this.readColr();
75
+ this.readCpal();
76
+ }
35
77
  }
36
78
  // The raw font bytes, for the /FontFile2 stream.
37
79
  getData() {
@@ -120,28 +162,26 @@ export class TTFParser {
120
162
  readCmap() {
121
163
  const base = this.table("cmap").offset;
122
164
  const numTables = u16(this.data, base + 2);
123
- // Prefer the Windows Unicode BMP subtable (3,1); accept full-Unicode (3,10) or any (0,x).
124
- let subtable = -1;
165
+ // Read EVERY Unicode subtable, not just one: fonts commonly ship a (3,1) format-4 for the BMP
166
+ // AND a (3,10) format-12 for astral code points (emoji, CJK-ext). Reading only the first would
167
+ // drop astral coverage. Format-4 fills the direct map; format-12 groups are searched on lookup,
168
+ // and getGlyphIndex checks the map first, so an overlapping BMP entry stays correct.
125
169
  let p = base + 4;
126
170
  for (let i = 0; i < numTables; i++) {
127
171
  const platform = u16(this.data, p);
128
172
  const encoding = u16(this.data, p + 2);
129
173
  const offset = u32(this.data, p + 4);
130
- const unicode = (platform === 3 && (encoding === 1 || encoding === 10)) || platform === 0;
131
- if (unicode) {
132
- subtable = base + offset;
133
- if (platform === 3 && encoding === 1)
134
- break; // best fit, stop looking
135
- }
136
174
  p += 8;
175
+ const unicode = (platform === 3 && (encoding === 1 || encoding === 10)) || platform === 0;
176
+ if (!unicode)
177
+ continue;
178
+ const subtable = base + offset;
179
+ const format = u16(this.data, subtable);
180
+ if (format === 4)
181
+ this.readCmapFormat4(subtable);
182
+ else if (format === 12)
183
+ this.readCmapFormat12(subtable);
137
184
  }
138
- if (subtable < 0)
139
- return;
140
- const format = u16(this.data, subtable);
141
- if (format === 4)
142
- this.readCmapFormat4(subtable);
143
- else if (format === 12)
144
- this.readCmapFormat12(subtable);
145
185
  }
146
186
  // Format 4: segment mapping. Four parallel arrays keyed by segment; the glyph comes either
147
187
  // from idDelta or, when idRangeOffset != 0, from the glyphIdArray it points into.
@@ -184,4 +224,404 @@ export class TTFParser {
184
224
  p += 12;
185
225
  }
186
226
  }
227
+ // True when the font carries glyph outlines we can draw (a `glyf`+`loca` pair). CFF/OTF fonts
228
+ // and bitmap-only fonts return false - color layers need vector outlines.
229
+ hasGlyfOutlines() {
230
+ return this.tables["glyf"] !== undefined && this.tables["loca"] !== undefined;
231
+ }
232
+ // Byte range of glyph `glyphId` inside the glyf table, via the loca offset array. An empty range
233
+ // (start === end) means the glyph has no outline (e.g. the space) - returns null.
234
+ glyfRange(glyphId) {
235
+ const loca = this.table("loca").offset;
236
+ let start;
237
+ let end;
238
+ if (this.indexToLocFormat === 0) {
239
+ // short loca stores offsets divided by two
240
+ start = u16(this.data, loca + glyphId * 2) * 2;
241
+ end = u16(this.data, loca + (glyphId + 1) * 2) * 2;
242
+ }
243
+ else {
244
+ start = u32(this.data, loca + glyphId * 4);
245
+ end = u32(this.data, loca + (glyphId + 1) * 4);
246
+ }
247
+ return end > start ? { start, end } : null;
248
+ }
249
+ // Glyph outline as drawing commands in font units. Empty for glyphs without an outline. Composite
250
+ // glyphs (numberOfContours < 0) are not assembled yet - they return empty (a follow-up sub-step).
251
+ getGlyphPath(glyphId) {
252
+ if (!this.hasGlyfOutlines())
253
+ return [];
254
+ const range = this.glyfRange(glyphId);
255
+ if (!range)
256
+ return [];
257
+ const g = this.table("glyf").offset + range.start;
258
+ const numberOfContours = i16(this.data, g);
259
+ if (numberOfContours < 0)
260
+ return []; // composite: deferred
261
+ return this.parseSimpleGlyph(g, numberOfContours);
262
+ }
263
+ // Simple-glyph body: end-point indices per contour, then flags, then delta-encoded x/y runs.
264
+ parseSimpleGlyph(g, numberOfContours) {
265
+ let p = g + 10; // past numberOfContours + the four bbox int16s
266
+ const endPts = [];
267
+ for (let i = 0; i < numberOfContours; i++) {
268
+ endPts.push(u16(this.data, p));
269
+ p += 2;
270
+ }
271
+ const numPoints = numberOfContours === 0 ? 0 : endPts[endPts.length - 1] + 1;
272
+ const instructionLength = u16(this.data, p);
273
+ p += 2 + instructionLength; // skip hinting instructions
274
+ // Flags, with the REPEAT bit (0x08) run-length-encoding a flag byte.
275
+ const flags = [];
276
+ while (flags.length < numPoints) {
277
+ const flag = u8(this.data, p++);
278
+ flags.push(flag);
279
+ if (flag & 0x08) {
280
+ let repeat = u8(this.data, p++);
281
+ while (repeat-- > 0)
282
+ flags.push(flag);
283
+ }
284
+ }
285
+ // X then Y coordinates, each a delta from the previous. Short (1 byte, sign in a flag bit) or
286
+ // long (int16), or "same as previous" (delta 0) when the short bit is clear but the same bit set.
287
+ const xs = this.readCoordinates(flags, p, 0x02, 0x10);
288
+ p = xs.next;
289
+ const ys = this.readCoordinates(flags, p, 0x04, 0x20);
290
+ const points = flags.map((flag, i) => ({
291
+ x: xs.values[i],
292
+ y: ys.values[i],
293
+ on: (flag & 0x01) !== 0,
294
+ }));
295
+ const commands = [];
296
+ let contourStart = 0;
297
+ for (const endPt of endPts) {
298
+ this.contourToCommands(points.slice(contourStart, endPt + 1), commands);
299
+ contourStart = endPt + 1;
300
+ }
301
+ return commands;
302
+ }
303
+ // Decodes one delta-encoded coordinate axis. `shortBit` = value is a single unsigned byte;
304
+ // `sameBit` = (short) its sign, or (long) repeat the previous value (delta 0).
305
+ readCoordinates(flags, start, shortBit, sameBit) {
306
+ let p = start;
307
+ let value = 0;
308
+ const values = [];
309
+ for (const flag of flags) {
310
+ if (flag & shortBit) {
311
+ const delta = u8(this.data, p++);
312
+ value += flag & sameBit ? delta : -delta;
313
+ }
314
+ else if (!(flag & sameBit)) {
315
+ value += i16(this.data, p);
316
+ p += 2;
317
+ }
318
+ // else: sameBit set without shortBit => delta 0, value unchanged
319
+ values.push(value);
320
+ }
321
+ return { values, next: p };
322
+ }
323
+ // Converts one TrueType contour (mixed on/off-curve points) into path commands. Between two
324
+ // consecutive off-curve control points TrueType implies an on-curve midpoint; we insert those,
325
+ // rotate the contour to begin on an on-curve anchor, then emit quads (off->on) and lines (on->on).
326
+ contourToCommands(raw, out) {
327
+ const n = raw.length;
328
+ if (n === 0)
329
+ return;
330
+ const pts = [];
331
+ for (let i = 0; i < n; i++) {
332
+ const cur = raw[i];
333
+ pts.push(cur);
334
+ const next = raw[(i + 1) % n];
335
+ if (!cur.on && !next.on) {
336
+ pts.push({ x: (cur.x + next.x) / 2, y: (cur.y + next.y) / 2, on: true });
337
+ }
338
+ }
339
+ const firstOn = pts.findIndex((pt) => pt.on);
340
+ if (firstOn < 0)
341
+ return; // degenerate: no anchor even after inserting midpoints
342
+ const loop = [...pts.slice(firstOn), ...pts.slice(0, firstOn)]; // starts on an on-curve anchor
343
+ const start = loop[0];
344
+ out.push({ type: "M", x: start.x, y: start.y });
345
+ let i = 1;
346
+ while (i < loop.length) {
347
+ const cur = loop[i];
348
+ if (cur.on) {
349
+ out.push({ type: "L", x: cur.x, y: cur.y });
350
+ i += 1;
351
+ }
352
+ else {
353
+ // An off-curve control is always followed by an on-curve point (real or implied); at the
354
+ // end of the loop that closing anchor is the start point itself.
355
+ const end = i + 1 < loop.length ? loop[i + 1] : start;
356
+ out.push({ type: "Q", cx: cur.x, cy: cur.y, x: end.x, y: end.y });
357
+ i += 2;
358
+ }
359
+ }
360
+ out.push({ type: "Z" });
361
+ }
362
+ // True when the font has color glyphs we can render: COLR base glyphs (v0 or v1) plus a CPAL
363
+ // palette to color them.
364
+ hasColorGlyphs() {
365
+ return (this.colrBase.size > 0 || this.colrBaseV1.size > 0) && this.palette.length > 0;
366
+ }
367
+ // The color layers of a base glyph, back-to-front (first drawn underneath), or null when the glyph
368
+ // has no COLR entry (draw it as a normal monochrome glyph then). Prefers the richer v1 paint graph
369
+ // when present, else reads the v0 records; both resolve to the same `ColorGlyphLayer` shape.
370
+ getColorGlyph(glyphId) {
371
+ const v1 = this.colrBaseV1.get(glyphId);
372
+ if (v1 !== undefined)
373
+ return this.walkColrLayers(v1, IDENTITY, new Set());
374
+ const rec = this.colrBase.get(glyphId);
375
+ if (!rec)
376
+ return null;
377
+ const layers = [];
378
+ for (let i = 0; i < rec.count; i++) {
379
+ const o = this.colrLayersOffset + (rec.first + i) * 4;
380
+ const paletteIndex = u16(this.data, o + 2);
381
+ layers.push({
382
+ glyphId: u16(this.data, o),
383
+ paint: { type: "solid", color: this.paletteColor(paletteIndex, 1) },
384
+ });
385
+ }
386
+ return layers;
387
+ }
388
+ // Walks a v1 paint (sub)tree into the flat list of glyph-clipped layers it draws, carrying the
389
+ // accumulated affine `m` down the graph. Handles the structural paints: PaintColrLayers (a list),
390
+ // PaintGlyph (a glyph + its fill), PaintColrGlyph (a reference), the transform paints (compose
391
+ // into `m`), and PaintComposite (backdrop under source). Unhandled paints (rotate/skew, the
392
+ // variable variants) are skipped. `seen` guards against cyclic references.
393
+ walkColrLayers(paintOffset, m, seen) {
394
+ const key = paintOffset;
395
+ if (seen.has(key))
396
+ return [];
397
+ seen.add(key);
398
+ const format = u8(this.data, paintOffset);
399
+ const child = () => paintOffset + this.u24(paintOffset + 1);
400
+ const descend = (t) => this.walkColrLayers(child(), compose(m, t), seen);
401
+ switch (format) {
402
+ case 1: {
403
+ // PaintColrLayers: numLayers (u8) starting at firstLayerIndex (u32) into the LayerList.
404
+ const numLayers = u8(this.data, paintOffset + 1);
405
+ const first = u32(this.data, paintOffset + 2);
406
+ const out = [];
407
+ for (let i = 0; i < numLayers; i++) {
408
+ const layerPaint = this.colrLayerListOffset +
409
+ u32(this.data, this.colrLayerListOffset + 4 + (first + i) * 4);
410
+ out.push(...this.walkColrLayers(layerPaint, m, new Set(seen)));
411
+ }
412
+ return out;
413
+ }
414
+ case 10: {
415
+ // PaintGlyph: paint (Offset24 to the fill) + glyphID (u16). The fill paints inside the glyph.
416
+ const fill = this.resolveFill(paintOffset + this.u24(paintOffset + 1), IDENTITY);
417
+ if (!fill)
418
+ return [];
419
+ const layer = { glyphId: u16(this.data, paintOffset + 4), paint: fill };
420
+ if (!isIdentity(m))
421
+ layer.transform = m;
422
+ return [layer];
423
+ }
424
+ case 11: {
425
+ // PaintColrGlyph: draw another base glyph's paint graph under the current transform.
426
+ const ref = this.colrBaseV1.get(u16(this.data, paintOffset + 1));
427
+ return ref !== undefined ? this.walkColrLayers(ref, m, new Set(seen)) : [];
428
+ }
429
+ case 12: // PaintTransform: child + an Affine2x3 (six Fixed 16.16 values).
430
+ return descend(this.readAffine(paintOffset + this.u24(paintOffset + 4)));
431
+ case 14: // PaintTranslate: dx, dy (FWORD).
432
+ return descend([
433
+ 1,
434
+ 0,
435
+ 0,
436
+ 1,
437
+ i16(this.data, paintOffset + 4),
438
+ i16(this.data, paintOffset + 6),
439
+ ]);
440
+ case 16: // PaintScale: scaleX, scaleY (F2Dot14).
441
+ return descend([this.f2dot14(paintOffset + 4), 0, 0, this.f2dot14(paintOffset + 6), 0, 0]);
442
+ case 18: // PaintScaleAroundCenter: scaleX, scaleY, centerX, centerY.
443
+ return descend(aroundCenter([this.f2dot14(paintOffset + 4), 0, 0, this.f2dot14(paintOffset + 6), 0, 0], i16(this.data, paintOffset + 8), i16(this.data, paintOffset + 10)));
444
+ case 20: {
445
+ // PaintScaleUniform: a single scale for both axes.
446
+ const s = this.f2dot14(paintOffset + 4);
447
+ return descend([s, 0, 0, s, 0, 0]);
448
+ }
449
+ case 22: {
450
+ // PaintScaleUniformAroundCenter: uniform scale about a center.
451
+ const s = this.f2dot14(paintOffset + 4);
452
+ return descend(aroundCenter([s, 0, 0, s, 0, 0], i16(this.data, paintOffset + 6), i16(this.data, paintOffset + 8)));
453
+ }
454
+ case 32: {
455
+ // PaintComposite: sourcePaint, mode (u8), backdropPaint. We treat every mode as source-over:
456
+ // draw the backdrop underneath, then the source on top (correct for the common case).
457
+ const source = paintOffset + this.u24(paintOffset + 1);
458
+ const backdrop = paintOffset + this.u24(paintOffset + 5);
459
+ return [
460
+ ...this.walkColrLayers(backdrop, m, new Set(seen)),
461
+ ...this.walkColrLayers(source, m, new Set(seen)),
462
+ ];
463
+ }
464
+ default:
465
+ return []; // a bare fill with no glyph, or a paint format not supported yet
466
+ }
467
+ }
468
+ // Resolves the fill under a PaintGlyph to a solid or gradient `Paint`, descending through any
469
+ // transform paints and baking the accumulated affine `t` into the gradient's coordinates (so the
470
+ // renderer only has to apply the OUTER glyph transform). Returns null for a still-unsupported fill.
471
+ resolveFill(paintOffset, t) {
472
+ const format = u8(this.data, paintOffset);
473
+ const child = () => paintOffset + this.u24(paintOffset + 1);
474
+ const p = (x, y) => applyAffine(t, x, y);
475
+ switch (format) {
476
+ case 2: {
477
+ // PaintSolid: paletteIndex (u16) + alpha (F2Dot14). A solid fill ignores the transform.
478
+ const paletteIndex = u16(this.data, paintOffset + 1);
479
+ return {
480
+ type: "solid",
481
+ color: this.paletteColor(paletteIndex, this.f2dot14(paintOffset + 3)),
482
+ };
483
+ }
484
+ case 4: {
485
+ // PaintLinearGradient: a color line + p0/p1 (a rotation point p2 we treat axially).
486
+ const line = this.readColorLine(paintOffset + this.u24(paintOffset + 1));
487
+ return {
488
+ type: "linearGradient",
489
+ p0: p(i16(this.data, paintOffset + 4), i16(this.data, paintOffset + 6)),
490
+ p1: p(i16(this.data, paintOffset + 8), i16(this.data, paintOffset + 10)),
491
+ stops: line.stops,
492
+ extend: line.extend,
493
+ };
494
+ }
495
+ case 6: {
496
+ // PaintRadialGradient: a color line + two circles. Radii scale by the affine's mean scale.
497
+ const line = this.readColorLine(paintOffset + this.u24(paintOffset + 1));
498
+ const rScale = detScale(t);
499
+ const [c0x, c0y] = p(i16(this.data, paintOffset + 4), i16(this.data, paintOffset + 6));
500
+ const [c1x, c1y] = p(i16(this.data, paintOffset + 10), i16(this.data, paintOffset + 12));
501
+ return {
502
+ type: "radialGradient",
503
+ c0: [c0x, c0y, u16(this.data, paintOffset + 8) * rScale],
504
+ c1: [c1x, c1y, u16(this.data, paintOffset + 14) * rScale],
505
+ stops: line.stops,
506
+ extend: line.extend,
507
+ };
508
+ }
509
+ // Transform paints wrapping the fill: compose into `t` and descend to the actual gradient.
510
+ case 12:
511
+ return this.resolveFill(child(), compose(t, this.readAffine(paintOffset + this.u24(paintOffset + 4))));
512
+ case 14:
513
+ return this.resolveFill(child(), compose(t, [
514
+ 1,
515
+ 0,
516
+ 0,
517
+ 1,
518
+ i16(this.data, paintOffset + 4),
519
+ i16(this.data, paintOffset + 6),
520
+ ]));
521
+ case 16:
522
+ return this.resolveFill(child(), compose(t, [this.f2dot14(paintOffset + 4), 0, 0, this.f2dot14(paintOffset + 6), 0, 0]));
523
+ case 18:
524
+ return this.resolveFill(child(), compose(t, aroundCenter([this.f2dot14(paintOffset + 4), 0, 0, this.f2dot14(paintOffset + 6), 0, 0], i16(this.data, paintOffset + 8), i16(this.data, paintOffset + 10))));
525
+ case 20: {
526
+ const s = this.f2dot14(paintOffset + 4);
527
+ return this.resolveFill(child(), compose(t, [s, 0, 0, s, 0, 0]));
528
+ }
529
+ case 22: {
530
+ const s = this.f2dot14(paintOffset + 4);
531
+ return this.resolveFill(child(), compose(t, aroundCenter([s, 0, 0, s, 0, 0], i16(this.data, paintOffset + 6), i16(this.data, paintOffset + 8))));
532
+ }
533
+ default:
534
+ return null;
535
+ }
536
+ }
537
+ // A ColorLine: an extend mode then color stops (position + palette color).
538
+ readColorLine(o) {
539
+ const extend = ["pad", "repeat", "reflect"][u8(this.data, o)] ?? "pad";
540
+ const numStops = u16(this.data, o + 1);
541
+ const stops = [];
542
+ for (let i = 0; i < numStops; i++) {
543
+ const p = o + 3 + i * 6;
544
+ stops.push({
545
+ offset: this.f2dot14(p),
546
+ color: this.paletteColor(u16(this.data, p + 2), this.f2dot14(p + 4)),
547
+ });
548
+ }
549
+ return { stops, extend };
550
+ }
551
+ // A CPAL palette color scaled by a gradient/solid alpha (0..1). paletteIndex 0xFFFF is the spec's
552
+ // "use the text foreground color" sentinel -> null.
553
+ paletteColor(paletteIndex, alpha) {
554
+ if (paletteIndex === 0xffff)
555
+ return null;
556
+ const c = this.palette[paletteIndex];
557
+ if (!c)
558
+ return null;
559
+ return { r: c.r, g: c.g, b: c.b, a: Math.round(c.a * alpha) };
560
+ }
561
+ // A big-endian 24-bit offset (used for a paint's sub-offsets).
562
+ u24(o) {
563
+ return (u8(this.data, o) << 16) | (u8(this.data, o + 1) << 8) | u8(this.data, o + 2);
564
+ }
565
+ // An F2Dot14 fixed-point number (signed, 2 integer + 14 fraction bits) as a float.
566
+ f2dot14(o) {
567
+ return i16(this.data, o) / 16384;
568
+ }
569
+ // A signed 32-bit big-endian integer.
570
+ i32(o) {
571
+ const v = u32(this.data, o);
572
+ return v >= 0x80000000 ? v - 0x100000000 : v;
573
+ }
574
+ // An Affine2x3 struct: six Fixed (16.16) values xx, yx, xy, yy, dx, dy -> [a, b, c, d, e, f].
575
+ readAffine(o) {
576
+ const fixed = (p) => this.i32(p) / 65536;
577
+ return [fixed(o), fixed(o + 4), fixed(o + 8), fixed(o + 12), fixed(o + 16), fixed(o + 20)];
578
+ }
579
+ // COLR header (v0 and v1). v0: base-glyph + layer records (solid). v1 (version >= 1): additionally
580
+ // a BaseGlyphList of paint graphs + a LayerList they index into.
581
+ readColr() {
582
+ const o = this.table("COLR").offset;
583
+ const version = u16(this.data, o);
584
+ const numBaseGlyphs = u16(this.data, o + 2);
585
+ const baseOffset = o + u32(this.data, o + 4);
586
+ this.colrLayersOffset = o + u32(this.data, o + 8);
587
+ for (let i = 0; i < numBaseGlyphs; i++) {
588
+ const p = baseOffset + i * 6;
589
+ this.colrBase.set(u16(this.data, p), {
590
+ first: u16(this.data, p + 2),
591
+ count: u16(this.data, p + 4),
592
+ });
593
+ }
594
+ if (version < 1)
595
+ return;
596
+ // v1 fields follow the v0 ones: BaseGlyphList (glyphID -> paint offset) + LayerList.
597
+ const baseGlyphListOffset = u32(this.data, o + 14);
598
+ const layerListOffset = u32(this.data, o + 18);
599
+ if (layerListOffset)
600
+ this.colrLayerListOffset = o + layerListOffset;
601
+ if (baseGlyphListOffset) {
602
+ const listStart = o + baseGlyphListOffset;
603
+ const numRecords = u32(this.data, listStart);
604
+ for (let i = 0; i < numRecords; i++) {
605
+ const p = listStart + 4 + i * 6;
606
+ this.colrBaseV1.set(u16(this.data, p), listStart + u32(this.data, p + 2));
607
+ }
608
+ }
609
+ }
610
+ // CPAL: reads palette 0 into `palette`. Color records are BGRA bytes; colorRecordIndices[0] is
611
+ // where palette 0 starts in the shared record array.
612
+ readCpal() {
613
+ const o = this.table("CPAL").offset;
614
+ const numPaletteEntries = u16(this.data, o + 2);
615
+ const recordsOffset = o + u32(this.data, o + 8);
616
+ const firstRecord = u16(this.data, o + 12); // colorRecordIndices[0], the start of palette 0
617
+ for (let i = 0; i < numPaletteEntries; i++) {
618
+ const p = recordsOffset + (firstRecord + i) * 4;
619
+ this.palette.push({
620
+ b: u8(this.data, p),
621
+ g: u8(this.data, p + 1),
622
+ r: u8(this.data, p + 2),
623
+ a: u8(this.data, p + 3),
624
+ });
625
+ }
626
+ }
187
627
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * XMP metadata packet that identifies the document as PDF/UA-1 (accessible). A fully conformant PDF/UA file
3
+ * also needs a displayed title (the /ViewerPreferences /DisplayDocTitle flag + this dc:title) and embedded
4
+ * fonts - the same font rule PDF/A has, so the standard-14 fonts do not qualify. Language is declared once
5
+ * in the catalog `/Lang`, not here (the dc:title uses `x-default`), so this takes only the title.
6
+ */
7
+ export declare function uaXmp(opts?: {
8
+ title?: string;
9
+ }): string;
@@ -0,0 +1,32 @@
1
+ // Escapes text for XML AND keeps the packet ASCII-safe: XML metacharacters become entities, and any
2
+ // non-ASCII / control character becomes a numeric character reference. The PDF writer emits the
3
+ // metadata stream one byte per char (Latin-1), so a raw non-ASCII title like "R\u00E9sum\u00E9" would corrupt
4
+ // the UTF-8 XMP (and misalign /Length); escaping keeps every char <= 0x7e.
5
+ const esc = (s) => s
6
+ .replace(/&/g, "&amp;")
7
+ .replace(/</g, "&lt;")
8
+ .replace(/>/g, "&gt;")
9
+ .replace(/[^ -~]/gu, (c) => `&#${c.codePointAt(0)};`); // any non printable-ASCII char
10
+ /**
11
+ * XMP metadata packet that identifies the document as PDF/UA-1 (accessible). A fully conformant PDF/UA file
12
+ * also needs a displayed title (the /ViewerPreferences /DisplayDocTitle flag + this dc:title) and embedded
13
+ * fonts - the same font rule PDF/A has, so the standard-14 fonts do not qualify. Language is declared once
14
+ * in the catalog `/Lang`, not here (the dc:title uses `x-default`), so this takes only the title.
15
+ */
16
+ export function uaXmp(opts = {}) {
17
+ const title = esc(opts.title ?? "");
18
+ return (
19
+ // The xpacket marker is the 3-byte UTF-8 BOM (EF BB BF) written as Latin-1 chars so the
20
+ // single-byte writer emits it verbatim - a literal U+FEFF would be mangled to "?".
21
+ `<?xpacket begin="\u00EF\u00BB\u00BF" id="W5M0MpCehiHzreSzNTczkc9d"?>` +
22
+ `<x:xmpmeta xmlns:x="adobe:ns:meta/">` +
23
+ `<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">` +
24
+ `<rdf:Description rdf:about="" xmlns:pdfuaid="http://www.aiim.org/pdfua/ns/id/">` +
25
+ `<pdfuaid:part>1</pdfuaid:part>` +
26
+ `</rdf:Description>` +
27
+ `<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">` +
28
+ `<dc:title><rdf:Alt><rdf:li xml:lang="x-default">${title}</rdf:li></rdf:Alt></dc:title>` +
29
+ `</rdf:Description>` +
30
+ `</rdf:RDF></x:xmpmeta>` +
31
+ `<?xpacket end="w"?>`);
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasy/pdf",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.5",
4
4
  "description": "Declarative, component-based PDF generation in pure TypeScript - Flutter-style components, real AFM font metrics, a hand-rolled PDF writer. No headless browser, no Java.",
5
5
  "keywords": [
6
6
  "declarative",