@jasy/pdf 1.0.0-alpha.8 → 1.0.0-beta.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.
Files changed (167) hide show
  1. package/README.md +29 -10
  2. package/dist/api/color.js +69 -0
  3. package/dist/api/content.d.ts +45 -5
  4. package/dist/api/content.js +96 -7
  5. package/dist/api/descriptor.js +6 -1
  6. package/dist/api/dimension.d.ts +57 -0
  7. package/dist/api/dimension.js +27 -0
  8. package/dist/api/font-url.d.ts +28 -0
  9. package/dist/api/font-url.js +135 -0
  10. package/dist/api/gradient.d.ts +53 -0
  11. package/dist/api/gradient.js +97 -0
  12. package/dist/api/index.d.ts +2 -0
  13. package/dist/api/index.js +2 -0
  14. package/dist/api/insets.d.ts +19 -12
  15. package/dist/api/insets.js +18 -7
  16. package/dist/api/layout.d.ts +20 -6
  17. package/dist/api/layout.js +31 -6
  18. package/dist/api/page-builder.d.ts +8 -0
  19. package/dist/api/page-builder.js +12 -0
  20. package/dist/api/structure.d.ts +12 -0
  21. package/dist/api/structure.js +54 -7
  22. package/dist/api/text.d.ts +65 -8
  23. package/dist/api/text.js +38 -5
  24. package/dist/canvas/painter.d.ts +102 -0
  25. package/dist/canvas/painter.js +183 -0
  26. package/dist/edit/document.d.ts +14 -4
  27. package/dist/edit/document.js +15 -22
  28. package/dist/edit/fill.d.ts +19 -2
  29. package/dist/edit/fill.js +63 -34
  30. package/dist/edit/flatten.d.ts +13 -1
  31. package/dist/edit/flatten.js +38 -13
  32. package/dist/edit/index.d.ts +2 -0
  33. package/dist/edit/index.js +3 -1
  34. package/dist/edit/objects.d.ts +5 -0
  35. package/dist/edit/objects.js +27 -0
  36. package/dist/edit/writer.d.ts +7 -0
  37. package/dist/edit/writer.js +23 -1
  38. package/dist/elements/canvas-element.d.ts +38 -0
  39. package/dist/elements/canvas-element.js +47 -0
  40. package/dist/elements/container-element.d.ts +23 -1
  41. package/dist/elements/container-element.js +31 -10
  42. package/dist/elements/image-element.d.ts +18 -4
  43. package/dist/elements/image-element.js +27 -12
  44. package/dist/elements/index.d.ts +2 -0
  45. package/dist/elements/index.js +2 -0
  46. package/dist/elements/layout/expanded-element.d.ts +5 -1
  47. package/dist/elements/layout/expanded-element.js +16 -3
  48. package/dist/elements/layout/keep-together-element.d.ts +2 -0
  49. package/dist/elements/layout/keep-together-element.js +4 -0
  50. package/dist/elements/layout/padding-element.d.ts +9 -2
  51. package/dist/elements/layout/padding-element.js +12 -2
  52. package/dist/elements/layout/page-builder-element.js +2 -0
  53. package/dist/elements/layout/struct-group.d.ts +2 -0
  54. package/dist/elements/layout/struct-group.js +4 -0
  55. package/dist/elements/pdf-element.d.ts +75 -1
  56. package/dist/elements/pdf-element.js +80 -1
  57. package/dist/elements/rectangle-element.d.ts +22 -6
  58. package/dist/elements/rectangle-element.js +31 -12
  59. package/dist/elements/row-element.d.ts +26 -1
  60. package/dist/elements/row-element.js +31 -10
  61. package/dist/elements/svg-element.d.ts +41 -0
  62. package/dist/elements/svg-element.js +93 -0
  63. package/dist/elements/text-element.d.ts +92 -1
  64. package/dist/elements/text-element.js +208 -33
  65. package/dist/ir/display-list.d.ts +67 -11
  66. package/dist/ir/display-list.js +8 -1
  67. package/dist/layout/alignment.d.ts +10 -0
  68. package/dist/layout/alignment.js +6 -0
  69. package/dist/layout/box-constraints.d.ts +53 -0
  70. package/dist/layout/box-constraints.js +72 -0
  71. package/dist/layout/insets.d.ts +28 -0
  72. package/dist/layout/insets.js +20 -0
  73. package/dist/layout/min-intrinsic.d.ts +29 -0
  74. package/dist/layout/min-intrinsic.js +37 -0
  75. package/dist/renderer/anchor-renderer.js +1 -1
  76. package/dist/renderer/bookmark-renderer.js +1 -1
  77. package/dist/renderer/canvas-renderer.d.ts +14 -0
  78. package/dist/renderer/canvas-renderer.js +36 -0
  79. package/dist/renderer/container-renderer.js +1 -3
  80. package/dist/renderer/default-text-style-renderer.js +1 -1
  81. package/dist/renderer/deferred-renderer.js +1 -1
  82. package/dist/renderer/expanded-renderer.js +1 -1
  83. package/dist/renderer/image-renderer.js +2 -1
  84. package/dist/renderer/index.d.ts +2 -0
  85. package/dist/renderer/index.js +2 -0
  86. package/dist/renderer/keep-together-renderer.js +1 -1
  87. package/dist/renderer/link-renderer.js +1 -1
  88. package/dist/renderer/padding-renderer.js +1 -1
  89. package/dist/renderer/page-builder-renderer.js +1 -1
  90. package/dist/renderer/page-renderer.js +1 -3
  91. package/dist/renderer/pdf-backend.d.ts +21 -4
  92. package/dist/renderer/pdf-backend.js +198 -61
  93. package/dist/renderer/pdf-document-renderer.js +8 -1
  94. package/dist/renderer/pdf-renderer.js +5 -1
  95. package/dist/renderer/positioned-renderer.js +1 -1
  96. package/dist/renderer/rectangle-renderer.js +9 -5
  97. package/dist/renderer/repeating-header-renderer.js +1 -2
  98. package/dist/renderer/rotated-renderer.js +1 -1
  99. package/dist/renderer/row-renderer.js +1 -3
  100. package/dist/renderer/struct-group-renderer.js +1 -1
  101. package/dist/renderer/svg-renderer.d.ts +11 -0
  102. package/dist/renderer/svg-renderer.js +27 -0
  103. package/dist/renderer/text-renderer.d.ts +2 -2
  104. package/dist/renderer/text-renderer.js +176 -57
  105. package/dist/svg/css.d.ts +38 -0
  106. package/dist/svg/css.js +115 -0
  107. package/dist/svg/errors.d.ts +24 -0
  108. package/dist/svg/errors.js +30 -0
  109. package/dist/svg/gradients.d.ts +34 -0
  110. package/dist/svg/gradients.js +156 -0
  111. package/dist/svg/index.d.ts +22 -0
  112. package/dist/svg/index.js +331 -0
  113. package/dist/svg/shapes.d.ts +6 -0
  114. package/dist/svg/shapes.js +177 -0
  115. package/dist/svg/style.d.ts +61 -0
  116. package/dist/svg/style.js +144 -0
  117. package/dist/svg/transform.d.ts +13 -0
  118. package/dist/svg/transform.js +68 -0
  119. package/dist/svg/units.d.ts +11 -0
  120. package/dist/svg/units.js +73 -0
  121. package/dist/svg/xml.d.ts +24 -0
  122. package/dist/svg/xml.js +158 -0
  123. package/dist/text/advance.d.ts +3 -0
  124. package/dist/text/advance.js +8 -4
  125. package/dist/text/arabic.d.ts +20 -0
  126. package/dist/text/arabic.js +101 -0
  127. package/dist/text/bidi.d.ts +33 -0
  128. package/dist/text/bidi.js +94 -0
  129. package/dist/text/font-fallback.d.ts +25 -0
  130. package/dist/text/font-fallback.js +38 -0
  131. package/dist/text/glyph-coverage.d.ts +15 -0
  132. package/dist/text/glyph-coverage.js +77 -0
  133. package/dist/text/line-breaker.d.ts +43 -1
  134. package/dist/text/line-breaker.js +181 -45
  135. package/dist/text/orphans-widows.d.ts +27 -0
  136. package/dist/text/orphans-widows.js +53 -0
  137. package/dist/text/shape.d.ts +29 -0
  138. package/dist/text/shape.js +91 -0
  139. package/dist/text/text-style.d.ts +38 -0
  140. package/dist/text/text-style.js +50 -1
  141. package/dist/text/whitespace.d.ts +4 -0
  142. package/dist/text/whitespace.js +43 -0
  143. package/dist/text/word-splitting.d.ts +42 -0
  144. package/dist/text/word-splitting.js +72 -0
  145. package/dist/utils/flex-layout.d.ts +24 -4
  146. package/dist/utils/flex-layout.js +218 -7
  147. package/dist/utils/font-metrics.d.ts +13 -2
  148. package/dist/utils/gsub.d.ts +36 -0
  149. package/dist/utils/gsub.js +178 -0
  150. package/dist/utils/inflate.d.ts +16 -0
  151. package/dist/utils/inflate.js +52 -0
  152. package/dist/utils/pdf-object-manager.d.ts +44 -3
  153. package/dist/utils/pdf-object-manager.js +176 -14
  154. package/dist/utils/renderer-registry.d.ts +13 -1
  155. package/dist/utils/renderer-registry.js +22 -1
  156. package/dist/utils/ttf-parser.d.ts +6 -0
  157. package/dist/utils/ttf-parser.js +19 -0
  158. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  159. package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
  160. package/dist/utils/woff.d.ts +10 -0
  161. package/dist/utils/woff.js +116 -0
  162. package/dist/utils/woff2.d.ts +6 -0
  163. package/dist/utils/woff2.js +651 -0
  164. package/dist/validators/element-validator.js +9 -2
  165. package/dist/vector/path.d.ts +33 -0
  166. package/dist/vector/path.js +81 -0
  167. package/package.json +6 -2
@@ -1,3 +1,20 @@
1
+ /**
2
+ * Thrown when an element reaches the render pass with no registered renderer.
3
+ *
4
+ * This used to return undefined and every call site skipped the element silently, so a whole document
5
+ * could serialize to an empty content stream and still be a valid PDF - a blank page, no warning
6
+ * (ISSUE-11). The registry is keyed on the CONSTRUCTOR, which makes it the one place in the engine that
7
+ * two loaded copies of the library break: elements built by copy A are invisible to copy B.
8
+ */
9
+ export class MissingRendererError extends Error {
10
+ constructor(elementName) {
11
+ super(`@jasy/pdf: no renderer registered for "${elementName}". Either the element was never registered ` +
12
+ `in PDFRenderer.render(), or two copies of @jasy/pdf are loaded - the registry is keyed on the ` +
13
+ `element's constructor, so elements built by one copy cannot be rendered by the other. ` +
14
+ `Check the dependency tree for a duplicate (e.g. \`pnpm why @jasy/pdf\`).`);
15
+ this.elementName = elementName;
16
+ }
17
+ }
1
18
  export class RendererRegistry {
2
19
  static register(elementClass, renderer) {
3
20
  if (!RendererRegistry.renderers.has(elementClass)) {
@@ -5,8 +22,12 @@ export class RendererRegistry {
5
22
  }
6
23
  }
7
24
  // Keyed on the element's constructor, so it only needs an object - not `any`.
25
+ // Throws rather than returning undefined: a missing renderer is always a bug, and skipping it drew nothing.
8
26
  static getRenderer(element) {
9
- return RendererRegistry.renderers.get(element.constructor);
27
+ const renderer = RendererRegistry.renderers.get(element.constructor);
28
+ if (!renderer)
29
+ throw new MissingRendererError(element.constructor.name);
30
+ return renderer;
10
31
  }
11
32
  static isRendererAsync(renderer) {
12
33
  return renderer.constructor.name === "AsyncFunction";
@@ -1,4 +1,5 @@
1
1
  import type { FontDecoration } from "../text/text-decoration.js";
2
+ import { GsubTable } from "./gsub.js";
2
3
  export type GlyphPathCommand = {
3
4
  type: "M";
4
5
  x: number;
@@ -77,14 +78,19 @@ export declare class TTFParser {
77
78
  private kernPairs;
78
79
  private hasKerning;
79
80
  private gposPairPos;
81
+ private gsubTable?;
80
82
  constructor(data: Uint8Array);
81
83
  getData(): Uint8Array;
82
84
  glyphCount(): number;
83
85
  glyphWidths(): number[];
84
86
  reverseCmap(): Map<number, number>;
85
87
  private toGlyphSpace;
88
+ /** The font's `GSUB`, or undefined when it has none. Parsed once, on demand. */
89
+ gsub(): GsubTable | undefined;
86
90
  getGlyphIndex(codePoint: number): number;
87
91
  getAdvanceWidth(glyphIndex: number): number;
92
+ /** Font units -> points at `fontSize`; the one scaling rule for shaped and plain advances alike. */
93
+ unitsToPoints(units: number, fontSize: number): number;
88
94
  getCharWidth(char: string, fontSize: number): number;
89
95
  getStringWidth(text: string, fontSize: number): number;
90
96
  private readDecorationMetrics;
@@ -2,6 +2,7 @@
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
4
  import { i16, latin1FromBytes, u8, u16, u32 } from "./bytes.js";
5
+ import { GsubTable } from "./gsub.js";
5
6
  const IDENTITY = [1, 0, 0, 1, 0, 0];
6
7
  /** Sorts x-intervals and unions the overlapping ones. */
7
8
  export function mergeSpans(spans) {
@@ -151,6 +152,20 @@ export class TTFParser {
151
152
  toGlyphSpace(v) {
152
153
  return Math.round((v * 1000) / this.unitsPerEm);
153
154
  }
155
+ /** The font's `GSUB`, or undefined when it has none. Parsed once, on demand. */
156
+ gsub() {
157
+ if (this.gsubTable === undefined) {
158
+ const record = this.tables["GSUB"];
159
+ // Degrade to "no shaping" rather than break font loading - same rule as kern.
160
+ try {
161
+ this.gsubTable = record ? new GsubTable(this.data, record.offset) : null;
162
+ }
163
+ catch {
164
+ this.gsubTable = null;
165
+ }
166
+ }
167
+ return this.gsubTable ?? undefined;
168
+ }
154
169
  // Codepoint → glyph id (0 = .notdef when the font has no glyph for it).
155
170
  getGlyphIndex(codePoint) {
156
171
  const g = this.cmap.get(codePoint);
@@ -169,6 +184,10 @@ export class TTFParser {
169
184
  return this.advanceWidths[i] ?? 0;
170
185
  }
171
186
  // Char width at fontSize, scaled from font units (em = unitsPerEm) to points.
187
+ /** Font units -> points at `fontSize`; the one scaling rule for shaped and plain advances alike. */
188
+ unitsToPoints(units, fontSize) {
189
+ return (units / this.unitsPerEm) * fontSize;
190
+ }
172
191
  getCharWidth(char, fontSize) {
173
192
  const units = this.getAdvanceWidth(this.getGlyphIndex(char.codePointAt(0)));
174
193
  return (units / this.unitsPerEm) * fontSize;
@@ -1,2 +1,8 @@
1
+ /**
2
+ * Whether Windows-1252 can carry this code point - which is exactly what a standard-14 font can draw,
3
+ * since that is the encoding its `/Widths` array is indexed by. Latin-1 passes straight through,
4
+ * except the C1 range, which the table above fills with printable punctuation instead.
5
+ */
6
+ export declare function isWindows1252(codePoint: number): boolean;
1
7
  /** Encodes a JavaScript string to a Windows-1252 byte buffer (the PDF text encoding). */
2
8
  export declare function getArrayBuffer(data: string): ArrayBuffer;
@@ -33,6 +33,16 @@ const CP1252_FROM_UNICODE = {
33
33
  0x017e: 0x9e, // ž z caron
34
34
  0x0178: 0x9f, // Ÿ Y diaeresis
35
35
  };
36
+ /**
37
+ * Whether Windows-1252 can carry this code point - which is exactly what a standard-14 font can draw,
38
+ * since that is the encoding its `/Widths` array is indexed by. Latin-1 passes straight through,
39
+ * except the C1 range, which the table above fills with printable punctuation instead.
40
+ */
41
+ export function isWindows1252(codePoint) {
42
+ if (codePoint in CP1252_FROM_UNICODE)
43
+ return true;
44
+ return codePoint <= 0xff && !(codePoint >= 0x80 && codePoint <= 0x9f);
45
+ }
36
46
  /** Encodes a JavaScript string to a Windows-1252 byte buffer (the PDF text encoding). */
37
47
  export function getArrayBuffer(data) {
38
48
  const u8 = new Uint8Array(data.length);
@@ -0,0 +1,10 @@
1
+ export declare class WoffError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ /** Whether these bytes are a WOFF 1.0 container. Cheap enough to call on every font. */
5
+ export declare const isWoff: (bytes: Uint8Array) => boolean;
6
+ /**
7
+ * Unpack a WOFF into the sfnt it wraps. The result is byte-for-byte what a `.ttf` of the same font
8
+ * would be, apart from table padding, so nothing downstream can tell the difference.
9
+ */
10
+ export declare function woffToSfnt(woff: Uint8Array): Uint8Array;
@@ -0,0 +1,116 @@
1
+ import { inflateBounded } from "./inflate.js";
2
+ /**
3
+ * WOFF 1.0 -> plain sfnt, so the existing `TTFParser` never learns a second container format.
4
+ *
5
+ * A WOFF is not a different FONT, it is a different wrapper: the same sfnt tables, each optionally
6
+ * zlib-compressed, behind a small header. Unpacking it back into an sfnt is therefore the whole job -
7
+ * every metric, glyph and cmap path downstream stays exactly as it was.
8
+ *
9
+ * WOFF2 is deliberately NOT here: it compresses with Brotli, which `fflate` does not do, and it also
10
+ * TRANSFORMS the glyf/loca tables rather than merely deflating them. That is a different piece of work,
11
+ * not a bigger version of this one.
12
+ */
13
+ const SIG_WOFF = 0x774f4646; // "wOFF"
14
+ const HEADER = 44;
15
+ const DIR_ENTRY = 20;
16
+ /** One table may not inflate past this. A single sfnt table above 64 MB is not a real font. */
17
+ const MAX_TABLE_BYTES = 64 * 1024 * 1024;
18
+ /**
19
+ * And all of them together may not either. The per-table ceiling alone is not a bound: `numTables` is a
20
+ * uint16, so 65535 directory entries may all point at the SAME small compressed blob, and each would be
21
+ * inflated and kept. Generous enough for a full CJK face, which is the largest real font there is.
22
+ */
23
+ const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
24
+ export class WoffError extends Error {
25
+ constructor(message) {
26
+ super(`@jasy/pdf: ${message}`);
27
+ }
28
+ }
29
+ const be32 = (b, at) => ((b[at] << 24) | (b[at + 1] << 16) | (b[at + 2] << 8) | b[at + 3]) >>> 0;
30
+ const be16 = (b, at) => (b[at] << 8) | b[at + 1];
31
+ /** Whether these bytes are a WOFF 1.0 container. Cheap enough to call on every font. */
32
+ export const isWoff = (bytes) => bytes.length >= HEADER && be32(bytes, 0) === SIG_WOFF;
33
+ /**
34
+ * Unpack a WOFF into the sfnt it wraps. The result is byte-for-byte what a `.ttf` of the same font
35
+ * would be, apart from table padding, so nothing downstream can tell the difference.
36
+ */
37
+ export function woffToSfnt(woff) {
38
+ if (!isWoff(woff))
39
+ throw new WoffError("these bytes are not a WOFF container");
40
+ const flavor = be32(woff, 4);
41
+ const numTables = be16(woff, 12);
42
+ if (numTables === 0)
43
+ throw new WoffError("this WOFF declares no tables");
44
+ if (woff.length < HEADER + numTables * DIR_ENTRY) {
45
+ throw new WoffError("this WOFF is truncated: its table directory does not fit in the file");
46
+ }
47
+ // Read the directory first, then the tables, so a malformed offset is caught before any allocation.
48
+ const entries = Array.from({ length: numTables }, (_, i) => {
49
+ const at = HEADER + i * DIR_ENTRY;
50
+ return {
51
+ tag: be32(woff, at),
52
+ offset: be32(woff, at + 4),
53
+ compLength: be32(woff, at + 8),
54
+ origLength: be32(woff, at + 12),
55
+ checksum: be32(woff, at + 16),
56
+ };
57
+ });
58
+ // Tracked across ALL tables, and checked BEFORE each inflate so a hostile directory costs neither the
59
+ // CPU of unpacking nor the memory of keeping the result.
60
+ let unpacked = 0;
61
+ const claim = (bytes) => {
62
+ unpacked += bytes;
63
+ if (unpacked > MAX_TOTAL_BYTES) {
64
+ throw new WoffError(`this WOFF unpacks to more than ${MAX_TOTAL_BYTES} bytes in total; no real font is that large`);
65
+ }
66
+ };
67
+ const tables = entries.map((e) => {
68
+ if (e.offset + e.compLength > woff.length) {
69
+ throw new WoffError(`this WOFF is truncated: a table runs past the end of the file`);
70
+ }
71
+ claim(e.origLength); // what it DECLARES, before any work is done on it
72
+ const raw = woff.subarray(e.offset, e.offset + e.compLength);
73
+ // Equal lengths mean the table was stored as-is; anything else is zlib, per the spec.
74
+ if (e.compLength === e.origLength)
75
+ return raw;
76
+ let out;
77
+ try {
78
+ out = inflateBounded(raw, MAX_TABLE_BYTES);
79
+ }
80
+ catch (err) {
81
+ throw new WoffError(`a table in this WOFF could not be unpacked: ${String(err?.message ?? err)}`);
82
+ }
83
+ if (out.length !== e.origLength) {
84
+ throw new WoffError(`a table in this WOFF unpacked to ${out.length} bytes where it declared ${e.origLength}`);
85
+ }
86
+ return out;
87
+ });
88
+ // Reassemble: offset table, directory, then the tables each padded to a 4-byte boundary.
89
+ const padded = (n) => (n + 3) & ~3;
90
+ const dirSize = numTables * 16;
91
+ const total = 12 + dirSize + tables.reduce((n, t) => n + padded(t.length), 0);
92
+ const out = new Uint8Array(total);
93
+ const view = new DataView(out.buffer);
94
+ // The binary-search hints an sfnt offset table carries. Readers may ignore them; a correct file has
95
+ // them right, and getting them wrong is the classic way a "valid" font is rejected by a strict one.
96
+ const entrySelector = Math.floor(Math.log2(numTables));
97
+ const searchRange = 2 ** entrySelector * 16;
98
+ view.setUint32(0, flavor);
99
+ view.setUint16(4, numTables);
100
+ view.setUint16(6, searchRange);
101
+ view.setUint16(8, entrySelector);
102
+ view.setUint16(10, numTables * 16 - searchRange);
103
+ let dir = 12;
104
+ let body = 12 + dirSize;
105
+ entries.forEach((e, i) => {
106
+ const data = tables[i];
107
+ view.setUint32(dir, e.tag);
108
+ view.setUint32(dir + 4, e.checksum);
109
+ view.setUint32(dir + 8, body);
110
+ view.setUint32(dir + 12, data.length); // the DECLARED length excludes the padding
111
+ out.set(data, body);
112
+ dir += 16;
113
+ body += padded(data.length);
114
+ });
115
+ return out;
116
+ }
@@ -0,0 +1,6 @@
1
+ export declare const isWoff2: (bytes: Uint8Array) => boolean;
2
+ /**
3
+ * The whole container: header, directory, one Brotli stream holding every table back to back, then
4
+ * `glyf`/`loca` put back together and the sfnt reassembled around them.
5
+ */
6
+ export declare function woff2ToSfnt(woff2: Uint8Array): Promise<Uint8Array>;