@jasy/pdf 1.0.0-beta.2 → 1.0.0-beta.4

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 (210) hide show
  1. package/dist/api/content.d.ts +5 -5
  2. package/dist/api/content.js +12 -6
  3. package/dist/api/font-url.d.ts +1 -2
  4. package/dist/api/font-url.js +8 -74
  5. package/dist/api/forms.d.ts +3 -0
  6. package/dist/api/forms.js +3 -0
  7. package/dist/api/index.d.ts +2 -0
  8. package/dist/api/index.js +1 -0
  9. package/dist/api/layout.d.ts +6 -0
  10. package/dist/api/layout.js +7 -2
  11. package/dist/api/page-builder.d.ts +4 -0
  12. package/dist/api/page-builder.js +4 -0
  13. package/dist/api/structure.d.ts +10 -2
  14. package/dist/api/structure.js +9 -6
  15. package/dist/api/table.d.ts +5 -3
  16. package/dist/api/table.js +99 -22
  17. package/dist/common/diagnostics.d.ts +53 -0
  18. package/dist/common/diagnostics.js +77 -0
  19. package/dist/edit/acroform-reader.js +7 -7
  20. package/dist/edit/appearance.js +3 -3
  21. package/dist/edit/attachments.d.ts +52 -0
  22. package/dist/edit/attachments.js +178 -0
  23. package/dist/edit/document.d.ts +38 -8
  24. package/dist/edit/document.js +208 -51
  25. package/dist/edit/fill.js +8 -23
  26. package/dist/edit/filters.d.ts +15 -0
  27. package/dist/edit/filters.js +157 -0
  28. package/dist/edit/index.d.ts +4 -1
  29. package/dist/edit/index.js +3 -1
  30. package/dist/edit/objects.d.ts +3 -4
  31. package/dist/edit/objects.js +5 -18
  32. package/dist/elements/canvas-element.d.ts +3 -0
  33. package/dist/elements/canvas-element.js +14 -3
  34. package/dist/elements/container-element.d.ts +5 -1
  35. package/dist/elements/container-element.js +35 -15
  36. package/dist/elements/forms/checkbox-element.d.ts +2 -0
  37. package/dist/elements/forms/checkbox-element.js +12 -3
  38. package/dist/elements/forms/choice-element.d.ts +6 -1
  39. package/dist/elements/forms/choice-element.js +17 -6
  40. package/dist/elements/forms/field-font.d.ts +8 -0
  41. package/dist/elements/forms/field-font.js +5 -0
  42. package/dist/elements/forms/push-button-element.d.ts +6 -1
  43. package/dist/elements/forms/push-button-element.js +16 -4
  44. package/dist/elements/forms/radio-element.d.ts +2 -0
  45. package/dist/elements/forms/radio-element.js +12 -3
  46. package/dist/elements/forms/signature-element.d.ts +6 -1
  47. package/dist/elements/forms/signature-element.js +16 -4
  48. package/dist/elements/forms/text-field-element.d.ts +6 -1
  49. package/dist/elements/forms/text-field-element.js +16 -4
  50. package/dist/elements/image-element.d.ts +39 -1
  51. package/dist/elements/image-element.js +95 -12
  52. package/dist/elements/layout/anchor-element.d.ts +8 -10
  53. package/dist/elements/layout/anchor-element.js +10 -18
  54. package/dist/elements/layout/bookmark-element.d.ts +9 -11
  55. package/dist/elements/layout/bookmark-element.js +11 -20
  56. package/dist/elements/layout/default-text-style-element.d.ts +11 -13
  57. package/dist/elements/layout/default-text-style-element.js +14 -31
  58. package/dist/elements/layout/deferred-element.d.ts +12 -4
  59. package/dist/elements/layout/deferred-element.js +24 -7
  60. package/dist/elements/layout/expanded-element.d.ts +7 -0
  61. package/dist/elements/layout/expanded-element.js +40 -18
  62. package/dist/elements/layout/keep-together-element.d.ts +8 -18
  63. package/dist/elements/layout/keep-together-element.js +21 -45
  64. package/dist/elements/layout/link-element.d.ts +8 -13
  65. package/dist/elements/layout/link-element.js +9 -19
  66. package/dist/elements/layout/padding-element.d.ts +15 -1
  67. package/dist/elements/layout/padding-element.js +55 -27
  68. package/dist/elements/layout/page-break-element.d.ts +4 -1
  69. package/dist/elements/layout/page-break-element.js +6 -1
  70. package/dist/elements/layout/page-builder-element.d.ts +5 -0
  71. package/dist/elements/layout/page-builder-element.js +11 -0
  72. package/dist/elements/layout/positioned-element.d.ts +14 -2
  73. package/dist/elements/layout/positioned-element.js +36 -10
  74. package/dist/elements/layout/proxy-element.d.ts +43 -0
  75. package/dist/elements/layout/proxy-element.js +94 -0
  76. package/dist/elements/layout/repeating-header-element.d.ts +6 -0
  77. package/dist/elements/layout/repeating-header-element.js +40 -13
  78. package/dist/elements/layout/rotated-box-element.d.ts +6 -0
  79. package/dist/elements/layout/rotated-box-element.js +27 -14
  80. package/dist/elements/layout/rotated-element.d.ts +7 -0
  81. package/dist/elements/layout/rotated-element.js +25 -2
  82. package/dist/elements/layout/struct-group.d.ts +9 -17
  83. package/dist/elements/layout/struct-group.js +10 -35
  84. package/dist/elements/line-element.d.ts +1 -0
  85. package/dist/elements/line-element.js +6 -0
  86. package/dist/elements/page-element.d.ts +10 -3
  87. package/dist/elements/page-element.js +39 -31
  88. package/dist/elements/pdf-document-element.d.ts +3 -0
  89. package/dist/elements/pdf-document-element.js +7 -0
  90. package/dist/elements/pdf-element.d.ts +75 -15
  91. package/dist/elements/pdf-element.js +68 -18
  92. package/dist/elements/rectangle-element.d.ts +14 -0
  93. package/dist/elements/rectangle-element.js +97 -52
  94. package/dist/elements/row-element.d.ts +11 -0
  95. package/dist/elements/row-element.js +58 -8
  96. package/dist/elements/svg-element.d.ts +30 -6
  97. package/dist/elements/svg-element.js +88 -17
  98. package/dist/elements/text-element.d.ts +40 -13
  99. package/dist/elements/text-element.js +144 -99
  100. package/dist/forms/acroform.d.ts +9 -1
  101. package/dist/forms/acroform.js +83 -34
  102. package/dist/forms/appearance.d.ts +17 -6
  103. package/dist/forms/appearance.js +18 -16
  104. package/dist/ir/display-list.d.ts +12 -2
  105. package/dist/layout/box-constraints.d.ts +5 -0
  106. package/dist/layout/box-constraints.js +14 -0
  107. package/dist/layout/collect-images.d.ts +6 -4
  108. package/dist/layout/collect-images.js +6 -4
  109. package/dist/layout/flex-item.d.ts +9 -0
  110. package/dist/layout/flex-item.js +39 -0
  111. package/dist/layout/fragmentation.d.ts +12 -4
  112. package/dist/layout/fragmentation.js +37 -14
  113. package/dist/layout/layout-cache.d.ts +53 -0
  114. package/dist/layout/layout-cache.js +107 -0
  115. package/dist/renderer/anchor-renderer.d.ts +2 -1
  116. package/dist/renderer/anchor-renderer.js +5 -4
  117. package/dist/renderer/bookmark-renderer.d.ts +2 -1
  118. package/dist/renderer/bookmark-renderer.js +5 -4
  119. package/dist/renderer/canvas-renderer.d.ts +2 -1
  120. package/dist/renderer/canvas-renderer.js +5 -2
  121. package/dist/renderer/container-renderer.d.ts +2 -1
  122. package/dist/renderer/container-renderer.js +6 -5
  123. package/dist/renderer/default-text-style-renderer.d.ts +2 -1
  124. package/dist/renderer/default-text-style-renderer.js +2 -3
  125. package/dist/renderer/deferred-renderer.d.ts +2 -1
  126. package/dist/renderer/deferred-renderer.js +2 -3
  127. package/dist/renderer/expanded-renderer.d.ts +2 -1
  128. package/dist/renderer/expanded-renderer.js +2 -3
  129. package/dist/renderer/form-field-renderer.d.ts +2 -1
  130. package/dist/renderer/form-field-renderer.js +16 -3
  131. package/dist/renderer/image-renderer.d.ts +2 -1
  132. package/dist/renderer/image-renderer.js +5 -2
  133. package/dist/renderer/keep-together-renderer.d.ts +2 -1
  134. package/dist/renderer/keep-together-renderer.js +2 -3
  135. package/dist/renderer/line-renderer.d.ts +2 -1
  136. package/dist/renderer/line-renderer.js +7 -2
  137. package/dist/renderer/link-renderer.d.ts +2 -1
  138. package/dist/renderer/link-renderer.js +36 -5
  139. package/dist/renderer/padding-renderer.d.ts +2 -1
  140. package/dist/renderer/padding-renderer.js +5 -5
  141. package/dist/renderer/page-break-renderer.d.ts +6 -5
  142. package/dist/renderer/page-break-renderer.js +14 -7
  143. package/dist/renderer/page-builder-renderer.d.ts +2 -1
  144. package/dist/renderer/page-builder-renderer.js +2 -3
  145. package/dist/renderer/page-renderer.js +28 -7
  146. package/dist/renderer/pdf-backend.d.ts +4 -3
  147. package/dist/renderer/pdf-backend.js +5 -5
  148. package/dist/renderer/pdf-document-renderer.d.ts +1 -8
  149. package/dist/renderer/pdf-document-renderer.js +15 -16
  150. package/dist/renderer/pdf-renderer.js +11 -11
  151. package/dist/renderer/positioned-renderer.d.ts +6 -1
  152. package/dist/renderer/positioned-renderer.js +6 -5
  153. package/dist/renderer/rectangle-renderer.d.ts +2 -1
  154. package/dist/renderer/rectangle-renderer.js +9 -4
  155. package/dist/renderer/repeating-header-renderer.d.ts +2 -1
  156. package/dist/renderer/repeating-header-renderer.js +6 -5
  157. package/dist/renderer/rotated-renderer.d.ts +2 -1
  158. package/dist/renderer/rotated-renderer.js +8 -5
  159. package/dist/renderer/row-renderer.d.ts +2 -1
  160. package/dist/renderer/row-renderer.js +6 -5
  161. package/dist/renderer/struct-group-renderer.d.ts +2 -1
  162. package/dist/renderer/struct-group-renderer.js +2 -3
  163. package/dist/renderer/svg-renderer.d.ts +2 -1
  164. package/dist/renderer/svg-renderer.js +8 -2
  165. package/dist/renderer/text-renderer.d.ts +4 -6
  166. package/dist/renderer/text-renderer.js +101 -111
  167. package/dist/svg/gradients.js +5 -0
  168. package/dist/svg/index.js +10 -1
  169. package/dist/svg/style.d.ts +2 -0
  170. package/dist/svg/style.js +11 -0
  171. package/dist/text/advance.d.ts +4 -0
  172. package/dist/text/advance.js +49 -4
  173. package/dist/text/bidi.d.ts +0 -2
  174. package/dist/text/bidi.js +0 -10
  175. package/dist/text/glyph-coverage.d.ts +1 -1
  176. package/dist/text/glyph-coverage.js +7 -3
  177. package/dist/text/hyphen-breaks.d.ts +17 -0
  178. package/dist/text/hyphen-breaks.js +54 -0
  179. package/dist/text/line-breaker.d.ts +34 -31
  180. package/dist/text/line-breaker.js +364 -176
  181. package/dist/text/line-metrics.d.ts +6 -3
  182. package/dist/text/line-metrics.js +7 -2
  183. package/dist/text/pdf-text-string.d.ts +21 -0
  184. package/dist/text/pdf-text-string.js +114 -0
  185. package/dist/text/text-decoration.d.ts +5 -5
  186. package/dist/text/text-decoration.js +5 -5
  187. package/dist/text/word-splitting.d.ts +15 -2
  188. package/dist/text/word-splitting.js +34 -8
  189. package/dist/utils/afm-parser.d.ts +3 -0
  190. package/dist/utils/afm-parser.js +19 -1
  191. package/dist/utils/dest-registry.d.ts +1 -1
  192. package/dist/utils/dest-registry.js +3 -2
  193. package/dist/utils/fetch-bytes.d.ts +19 -0
  194. package/dist/utils/fetch-bytes.js +79 -0
  195. package/dist/utils/flex-layout.d.ts +13 -13
  196. package/dist/utils/flex-layout.js +318 -216
  197. package/dist/utils/font-metrics.d.ts +4 -3
  198. package/dist/utils/image-helper.js +2 -1
  199. package/dist/utils/pdf-object-manager.d.ts +11 -3
  200. package/dist/utils/pdf-object-manager.js +41 -15
  201. package/dist/utils/renderer-registry.d.ts +43 -2
  202. package/dist/utils/renderer-registry.js +40 -0
  203. package/dist/utils/struct-tree.d.ts +24 -0
  204. package/dist/utils/struct-tree.js +61 -9
  205. package/dist/utils/ttf-parser.d.ts +4 -0
  206. package/dist/utils/ttf-parser.js +27 -4
  207. package/dist/validators/element-validator.js +2 -1
  208. package/package.json +1 -1
  209. package/dist/elements/layout/sized-container-element.d.ts +0 -14
  210. package/dist/elements/layout/sized-container-element.js +0 -32
@@ -12,6 +12,7 @@ import { DestRegistry } from "./dest-registry.js";
12
12
  import { AcroFormCollector } from "../forms/acroform.js";
13
13
  import type { PDFConfig } from "../renderer/pdf-document-class.js";
14
14
  import type { FontMetrics } from "./font-metrics.js";
15
+ import { Diagnostics } from "../common/diagnostics.js";
15
16
  interface FontIndexes {
16
17
  fontIndex: number;
17
18
  resourceIndex: number;
@@ -44,8 +45,8 @@ export declare class PDFObjectManager implements FontMetrics {
44
45
  /** Shaped runs, cached face -> feature set -> text. Nested, not one map under a composed key: that
45
46
  * key would carry the whole run's text, so every lookup would copy it into a fresh string. */
46
47
  private shapeCache;
47
- /** Bumped by every font registration; `runAdvance` watches it to drop memoised widths, since a
48
- * family name can mean a different face afterwards. */
48
+ /** Bumped when a face is really ADDED (a repeat registration returns early, untouched); every
49
+ * memoised measurement watches it, since a family name can mean a different face afterwards. */
49
50
  fontEpoch: number;
50
51
  private shapedOrigins;
51
52
  private attachments;
@@ -78,7 +79,9 @@ export declare class PDFObjectManager implements FontMetrics {
78
79
  * character went missing instead of a server log nobody reads.
79
80
  */
80
81
  private readonly missingGlyphs;
81
- reportMissingGlyph(codePoints: number[]): void;
82
+ reportMissingGlyph(codePoints: number[], about?: object, page?: number): void;
83
+ /** The render's diagnostics channel (`onDiagnostic`); see `common/diagnostics.ts`. */
84
+ readonly diagnostics: Diagnostics;
82
85
  /** The dropped code points, in the order first seen. Empty for a document that hit none. */
83
86
  getMissingGlyphs(): number[];
84
87
  setOverflowPolicy(policy: OverflowPolicy): void;
@@ -97,6 +100,11 @@ export declare class PDFObjectManager implements FontMetrics {
97
100
  * A string INSIDE a content stream needs none of this - the stream is enciphered as a whole.
98
101
  */
99
102
  pdfString(text: string): string;
103
+ /**
104
+ * A URI: 7-bit ASCII by definition, so anything else is percent-encoded as UTF-8 - the way a browser
105
+ * writes the link - rather than squeezed into a text encoding.
106
+ */
107
+ pdfUri(uri: string): string;
100
108
  private streamPayload;
101
109
  private stream;
102
110
  addContentStream(content: string): number;
@@ -3,7 +3,7 @@ import { STANDARD_AFM } from "../assets/font-data.js";
3
3
  import { md5, md5Hex } from "./md5.js";
4
4
  import { zlibSync } from "fflate";
5
5
  import { bytesFromLatin1, latin1FromBytes } from "./bytes.js";
6
- import { AFMParser } from "./afm-parser.js";
6
+ import { standardParser } from "./afm-parser.js";
7
7
  import { mergeSpans, TTFParser } from "./ttf-parser.js";
8
8
  import { WoffError, isWoff, woffToSfnt } from "./woff.js";
9
9
  import { isWoff2 } from "./woff2.js";
@@ -26,6 +26,7 @@ export function featureKey(f) {
26
26
  }
27
27
  const features = (ligatures) => ligatures ? LATIN_FEATURES : NO_FEATURES;
28
28
  import { getArrayBuffer, isWindows1252 } from "./utf8-to-windows1252-encoder.js";
29
+ import { encodePdfText, isPlainPdfText } from "../text/pdf-text-string.js";
29
30
  import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
30
31
  import { StructTree } from "./struct-tree.js";
31
32
  import { OutlineBuilder } from "./outline.js";
@@ -34,6 +35,7 @@ import { AcroFormCollector } from "../forms/acroform.js";
34
35
  // Enums come from the leaf config module (never in a cycle); the config type is
35
36
  // erased at runtime so it can come from the cyclic module safely.
36
37
  import { ColorMode, Orientation } from "../renderer/pdf-config.js";
38
+ import { Diagnostics } from "../common/diagnostics.js";
37
39
  export var FontStyle;
38
40
  (function (FontStyle) {
39
41
  FontStyle["Normal"] = "normal";
@@ -184,8 +186,8 @@ export class PDFObjectManager {
184
186
  /** Shaped runs, cached face -> feature set -> text. Nested, not one map under a composed key: that
185
187
  * key would carry the whole run's text, so every lookup would copy it into a fresh string. */
186
188
  this.shapeCache = new Map();
187
- /** Bumped by every font registration; `runAdvance` watches it to drop memoised widths, since a
188
- * family name can mean a different face afterwards. */
189
+ /** Bumped when a face is really ADDED (a repeat registration returns early, untouched); every
190
+ * memoised measurement watches it, since a family name can mean a different face afterwards. */
189
191
  this.fontEpoch = 0;
190
192
  // Per font, glyph -> the code points it stands for. A shaped glyph is often absent from the cmap (a
191
193
  // ligature) or maps back to a presentation form, so `ToUnicode` cannot come from the cmap alone.
@@ -220,10 +222,11 @@ export class PDFObjectManager {
220
222
  * character went missing instead of a server log nobody reads.
221
223
  */
222
224
  this.missingGlyphs = new Set();
225
+ /** The render's diagnostics channel (`onDiagnostic`); see `common/diagnostics.ts`. */
226
+ this.diagnostics = new Diagnostics();
223
227
  // Whether to kern. Document-level: measuring (via `runAdvance`, which reads this off the metrics)
224
- // and drawing (the backend emits a `TJ`) both consult it, so they can never disagree. OFF while the
225
- // embedded-font path (kern/GPOS) is unbuilt - a standard-14-only kerning would set the 14 system
226
- // fonts finer than a user's own font, which is worse than none. Flip to on when GPOS lands.
228
+ // and drawing (the backend emits a `TJ`) both consult it, so they can never disagree. The default
229
+ // here is off; `renderPdf` turns it on unless the document says `kerning: false`.
227
230
  this.kerningEnabled = false;
228
231
  if (pageSize)
229
232
  this.pageFormat = pageFormats[pageSize];
@@ -247,9 +250,18 @@ export class PDFObjectManager {
247
250
  setCompress(on) {
248
251
  this.compress = on;
249
252
  }
250
- reportMissingGlyph(codePoints) {
253
+ reportMissingGlyph(codePoints, about, page) {
251
254
  for (const cp of codePoints)
252
255
  this.missingGlyphs.add(cp);
256
+ const chars = codePoints.map((cp) => String.fromCodePoint(cp)).join("");
257
+ this.diagnostics.report({
258
+ code: "missing-glyphs",
259
+ severity: "warning",
260
+ message: `No font can draw ${JSON.stringify(chars)}; removed (PDF/A forbids .notdef).`,
261
+ about,
262
+ page,
263
+ fallback: "silent",
264
+ });
253
265
  }
254
266
  /** The dropped code points, in the order first seen. Empty for a document that hit none. */
255
267
  getMissingGlyphs() {
@@ -263,7 +275,7 @@ export class PDFObjectManager {
263
275
  }
264
276
  // A unique, binary-safe placeholder for a stream's bytes, swapped for ciphertext in finalizeEncryption().
265
277
  encToken(id) {
266
- return `JENC:${id}`;
278
+ return `\x00\x01JENC:${id}\x01\x00`;
267
279
  }
268
280
  // The same trick for a literal STRING. A separate marker because the two are swapped differently: a
269
281
  // stream keeps its delimiters and only the payload changes, a string is replaced whole by a hex one.
@@ -282,15 +294,28 @@ export class PDFObjectManager {
282
294
  * A string INSIDE a content stream needs none of this - the stream is enciphered as a whole.
283
295
  */
284
296
  pdfString(text) {
297
+ // A TEXT string: PDFDocEncoding where it can say everything, else UTF-16BE (text/pdf-text-string.ts).
298
+ const bytes = encodePdfText(text);
285
299
  if (!this.security) {
286
- return `(${text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)")})`;
300
+ // Plain Latin-1 comes out of the Windows-1252 page encoder as the same bytes PDFDocEncoding reads
301
+ // back, so it stays a readable literal; anything else is written as hex, past that encoder.
302
+ if (isPlainPdfText(text)) {
303
+ return `(${text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)")})`;
304
+ }
305
+ return `<${[...bytes].map((b) => b.toString(16).padStart(2, "0")).join("")}>`;
287
306
  }
288
307
  // Encrypt the bytes the unencrypted path would have written, so a viewer decrypts to the same value.
289
308
  // No escaping is applied: the result is emitted as a HEX string, which has no escapes to get wrong.
290
- const bytes = new Uint8Array(getArrayBuffer(text));
291
309
  const id = this.strJobs.push(bytes) - 1;
292
310
  return this.strToken(id);
293
311
  }
312
+ /**
313
+ * A URI: 7-bit ASCII by definition, so anything else is percent-encoded as UTF-8 - the way a browser
314
+ * writes the link - rather than squeezed into a text encoding.
315
+ */
316
+ pdfUri(uri) {
317
+ return this.pdfString(uri.replace(/[^\x20-\x7e]/gu, (c) => encodeURIComponent(c)));
318
+ }
294
319
  // The single encryption choke-point every stream emitter routes through (stream(), registerImage). Without
295
320
  // a security handler: the bytes as a latin1 string + their length. With one: register the raw bytes for the
296
321
  // finalize pass and return a placeholder + the pre-computable encrypted length (16-byte IV + PKCS#7 padding).
@@ -473,7 +498,7 @@ endstream`;
473
498
  async finalizeEncryption() {
474
499
  if (!this.security)
475
500
  return;
476
- if (this.outputIntent !== undefined || this.attachments.length > 0) {
501
+ if (this.outputIntent !== undefined) {
477
502
  throw new Error("@jasy/pdf: cannot encrypt a PDF/A document - PDF/A forbids encryption.");
478
503
  }
479
504
  for (let id = 0; id < this.encJobs.length; id++) {
@@ -565,10 +590,10 @@ endstream`;
565
590
  }
566
591
  // Registers a font
567
592
  registerFont(fontName, fontStyle = FontStyle.Normal, fullName = fontName) {
568
- this.fontEpoch++;
569
593
  if (this.fonts.hasFont(fontName, fontStyle)) {
570
594
  return this.fonts.getFont(fontName, fontStyle); // Already exists? Return it!
571
595
  }
596
+ this.fontEpoch++;
572
597
  this.verticalsCache.delete(fontName);
573
598
  this.decorationCache.delete(fontName);
574
599
  const data = STANDARD_AFM[fullName];
@@ -577,7 +602,7 @@ endstream`;
577
602
  fontName,
578
603
  fontStyle,
579
604
  fullFontName: fullName,
580
- parser: new AFMParser(data),
605
+ parser: standardParser(fullName, data),
581
606
  });
582
607
  }
583
608
  const resourceNumber = this.objects.length + 1; // New resource number
@@ -596,7 +621,6 @@ endstream`;
596
621
  // metrics and emits its PDF font objects. All variants share the family `name`; bold/italic are
597
622
  // separate .ttf files registered under the same name with a different style.
598
623
  registerCustomFont(name, data, style = FontStyle.Normal) {
599
- this.fontEpoch++;
600
624
  let byStyle = this.customFonts.get(name);
601
625
  if (!byStyle) {
602
626
  byStyle = new Map();
@@ -616,6 +640,7 @@ endstream`;
616
640
  "is loaded asynchronously, so it happens one step earlier in renderPdf.");
617
641
  }
618
642
  byStyle.set(style, new TTFParser(isWoff(data) ? woffToSfnt(data) : data));
643
+ this.fontEpoch++; // only once the face is really there: a font that fails to parse adds nothing
619
644
  this.verticalsCache.delete(name); // this family now answers from the .ttf, not from an AFM
620
645
  this.decorationCache.delete(name);
621
646
  // Emission (the PDF font objects + page resource) is DEFERRED to first use via ensureEmitted(),
@@ -1004,7 +1029,8 @@ endstream`;
1004
1029
  for (let i = 0; i < chars.length - 1; i++) {
1005
1030
  const a = chars[i];
1006
1031
  const b = chars[i + 1];
1007
- if (a === " " || b === " ") {
1032
+ // A no-break space is a space: never kerned (the AFM names it `space`, with pairs of its own).
1033
+ if (a === " " || b === " " || a === "\u00a0" || b === "\u00a0") {
1008
1034
  out.push(0);
1009
1035
  }
1010
1036
  else if (ttf) {
@@ -1,3 +1,27 @@
1
+ import type { Offset } from "../layout/box-constraints.js";
2
+ import type { IRNode } from "../ir/display-list.js";
3
+ import type { PDFObjectManager } from "./pdf-object-manager.js";
4
+ /**
5
+ * Where an element is drawn. `origin` is the ABSOLUTE top-left that the element's own offset is
6
+ * relative to; a renderer adds it to its position and hands its children the origin THEY are relative
7
+ * to. `frame` is the absolute top-left of the nearest positioning frame (the page's content box, or a
8
+ * `relative` Box): a `Positioned` is laid out relative to it and draws from there, at its own place in
9
+ * the tree - tree order is paint order.
10
+ */
11
+ export interface RenderScope {
12
+ origin: Offset;
13
+ frame: Offset;
14
+ }
15
+ /** The scope for the children of an element at `offset` (relative to `scope.origin`). */
16
+ export declare function within(scope: RenderScope, offset: {
17
+ x?: number;
18
+ y?: number;
19
+ }): RenderScope;
20
+ /**
21
+ * Renders one element to IR. Typed so that a call which forgets to pass the scope on does not
22
+ * compile - a forgotten origin draws at the page corner without a word.
23
+ */
24
+ export type ElementRenderer = (element: any, objectManager: PDFObjectManager, scope: RenderScope) => Promise<IRNode[]>;
1
25
  /**
2
26
  * Thrown when an element reaches the render pass with no registered renderer.
3
27
  *
@@ -10,9 +34,26 @@ export declare class MissingRendererError extends Error {
10
34
  readonly elementName: string;
11
35
  constructor(elementName: string);
12
36
  }
37
+ /**
38
+ * Thrown when one element INSTANCE is drawn twice on the same page. An element holds one position - the
39
+ * place its last layout gave it - so both copies would land there and one would silently go missing.
40
+ */
41
+ export declare class ElementReusedError extends Error {
42
+ readonly elementName: string;
43
+ constructor(elementName: string);
44
+ }
13
45
  export declare class RendererRegistry {
14
46
  private static renderers;
15
- static register(elementClass: Function, renderer: Function): void;
16
- static getRenderer(element: object): Function;
47
+ private static drawnOnPage;
48
+ static register(elementClass: Function, renderer: ElementRenderer): void;
49
+ static getRenderer(element: object): ElementRenderer;
50
+ /** A new page starts for this document: nothing is drawn on it yet. */
51
+ static beginPage(objectManager: PDFObjectManager): void;
52
+ /**
53
+ * Draws `element` with its renderer. Every draw goes through here, so it is where reuse is caught - for
54
+ * an element that DRAWS something: one that draws nothing (a Spacer) has no place to land wrongly, and
55
+ * reusing it stays harmless.
56
+ */
57
+ static render(element: object, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
17
58
  static isRendererAsync(renderer: Function): boolean;
18
59
  }
@@ -1,3 +1,10 @@
1
+ /** The scope for the children of an element at `offset` (relative to `scope.origin`). */
2
+ export function within(scope, offset) {
3
+ return {
4
+ ...scope,
5
+ origin: { x: scope.origin.x + (offset.x ?? 0), y: scope.origin.y + (offset.y ?? 0) },
6
+ };
7
+ }
1
8
  /**
2
9
  * Thrown when an element reaches the render pass with no registered renderer.
3
10
  *
@@ -15,6 +22,18 @@ export class MissingRendererError extends Error {
15
22
  this.elementName = elementName;
16
23
  }
17
24
  }
25
+ /**
26
+ * Thrown when one element INSTANCE is drawn twice on the same page. An element holds one position - the
27
+ * place its last layout gave it - so both copies would land there and one would silently go missing.
28
+ */
29
+ export class ElementReusedError extends Error {
30
+ constructor(elementName) {
31
+ super(`@jasy/pdf: the same ${elementName} element is used twice on one page. An element has ONE ` +
32
+ `position, so both copies would be drawn in the same place. Create it twice instead - call the ` +
33
+ `factory again (e.g. a function \`const divider = () => Divider()\`) rather than reusing one value.`);
34
+ this.elementName = elementName;
35
+ }
36
+ }
18
37
  export class RendererRegistry {
19
38
  static register(elementClass, renderer) {
20
39
  if (!RendererRegistry.renderers.has(elementClass)) {
@@ -29,8 +48,29 @@ export class RendererRegistry {
29
48
  throw new MissingRendererError(element.constructor.name);
30
49
  return renderer;
31
50
  }
51
+ /** A new page starts for this document: nothing is drawn on it yet. */
52
+ static beginPage(objectManager) {
53
+ RendererRegistry.drawnOnPage.set(objectManager, new WeakSet());
54
+ }
55
+ /**
56
+ * Draws `element` with its renderer. Every draw goes through here, so it is where reuse is caught - for
57
+ * an element that DRAWS something: one that draws nothing (a Spacer) has no place to land wrongly, and
58
+ * reusing it stays harmless.
59
+ */
60
+ static async render(element, objectManager, scope) {
61
+ const nodes = await RendererRegistry.getRenderer(element)(element, objectManager, scope);
62
+ const drawn = RendererRegistry.drawnOnPage.get(objectManager);
63
+ if (drawn && nodes.length > 0) {
64
+ if (drawn.has(element))
65
+ throw new ElementReusedError(element.constructor.name);
66
+ drawn.add(element);
67
+ }
68
+ return nodes;
69
+ }
32
70
  static isRendererAsync(renderer) {
33
71
  return renderer.constructor.name === "AsyncFunction";
34
72
  }
35
73
  }
36
74
  RendererRegistry.renderers = new Map();
75
+ // What each document has drawn on its current page (keyed per document: two can render at once).
76
+ RendererRegistry.drawnOnPage = new WeakMap();
@@ -24,18 +24,42 @@ export declare class StructTree {
24
24
  private byStructId;
25
25
  private stack;
26
26
  private pageObjByStructParents;
27
+ private annotParents;
28
+ private carried;
27
29
  /** Register a structure element for a logical element (keyed by its stable `structId`, so a paragraph or
28
30
  * table split across pages resolves to the SAME element - MCIDs then accrue from every page). Its parent
29
31
  * is the currently-open container, else the Document. Containers also push()/pop() around their children. */
30
32
  openElement(structId: number, role: string, opts?: {
31
33
  alt?: string;
32
34
  }): number;
35
+ /**
36
+ * An element no layout element stands behind - an inline link inside a paragraph: a child of `parent`,
37
+ * new every time (nothing to deduplicate).
38
+ */
39
+ addElement(role: string, parent: number, opts?: {
40
+ alt?: string;
41
+ }): number;
42
+ /** A piece of paragraph `owner` ended inside link element `key`: its next piece may continue it. */
43
+ carryLink(owner: number, target: string, key: number): void;
44
+ /** The link the previous piece of `owner` ended in. Taken by the next piece, whether it continues it. */
45
+ takeCarriedLink(owner: number): {
46
+ target: string;
47
+ key: number;
48
+ } | undefined;
33
49
  push(key: number): void;
34
50
  pop(): void;
35
51
  /** Begin a page: allocates its StructParents index (its slot in the ParentTree). */
36
52
  beginPage(): PageStructContext;
37
53
  /** Tie a page's StructParents index to its now-known page object number (for /Pg references). */
38
54
  setPageObject(structParents: number, pageObjNum: number): void;
55
+ /**
56
+ * PDF/UA 7.18: an annotation belongs to a structure element (a link to its `/Link`, a widget to its
57
+ * `/Form`). Returns the `/StructParent` the annotation dictionary carries - its own slot in the
58
+ * ParentTree, numbered alongside the pages'.
59
+ */
60
+ annotationParent(key: number): number;
61
+ /** The annotation object, once written: an `/OBJR` kid of its element, on the page that lists it. */
62
+ linkAnnotation(key: number, obj: number, pageStructParents: number): void;
39
63
  /** Link a page-local MCID to an element (called by the backend while serializing). */
40
64
  linkMarkedContent(key: number, structParents: number, mcid: number): void;
41
65
  /** Emit the struct-tree objects and return the catalog additions (`/MarkInfo`, `/StructTreeRoot`, `/Lang`).
@@ -32,6 +32,9 @@ export class StructTree {
32
32
  this.byStructId = new Map(); // element structId -> element key (dedup across pages)
33
33
  this.stack = []; // open containers; top is the current parent
34
34
  this.pageObjByStructParents = new Map();
35
+ this.annotParents = new Map(); // an annotation's /StructParent -> its element key
36
+ // An inline link a page break cut: paragraph structId -> the link it ended in, for its next piece.
37
+ this.carried = new Map();
35
38
  }
36
39
  /** Register a structure element for a logical element (keyed by its stable `structId`, so a paragraph or
37
40
  * table split across pages resolves to the SAME element - MCIDs then accrue from every page). Its parent
@@ -48,9 +51,29 @@ export class StructTree {
48
51
  alt: opts?.alt,
49
52
  parent: this.stack[this.stack.length - 1],
50
53
  mc: [],
54
+ annots: [],
51
55
  });
52
56
  return key;
53
57
  }
58
+ /**
59
+ * An element no layout element stands behind - an inline link inside a paragraph: a child of `parent`,
60
+ * new every time (nothing to deduplicate).
61
+ */
62
+ addElement(role, parent, opts) {
63
+ const key = this.counter++;
64
+ this.elems.set(key, { key, role, alt: opts?.alt, parent, mc: [], annots: [] });
65
+ return key;
66
+ }
67
+ /** A piece of paragraph `owner` ended inside link element `key`: its next piece may continue it. */
68
+ carryLink(owner, target, key) {
69
+ this.carried.set(owner, { target, key });
70
+ }
71
+ /** The link the previous piece of `owner` ended in. Taken by the next piece, whether it continues it. */
72
+ takeCarriedLink(owner) {
73
+ const carried = this.carried.get(owner);
74
+ this.carried.delete(owner);
75
+ return carried;
76
+ }
54
77
  push(key) {
55
78
  this.stack.push(key);
56
79
  }
@@ -65,6 +88,20 @@ export class StructTree {
65
88
  setPageObject(structParents, pageObjNum) {
66
89
  this.pageObjByStructParents.set(structParents, pageObjNum);
67
90
  }
91
+ /**
92
+ * PDF/UA 7.18: an annotation belongs to a structure element (a link to its `/Link`, a widget to its
93
+ * `/Form`). Returns the `/StructParent` the annotation dictionary carries - its own slot in the
94
+ * ParentTree, numbered alongside the pages'.
95
+ */
96
+ annotationParent(key) {
97
+ const structParent = this.structParentsCounter++;
98
+ this.annotParents.set(structParent, key);
99
+ return structParent;
100
+ }
101
+ /** The annotation object, once written: an `/OBJR` kid of its element, on the page that lists it. */
102
+ linkAnnotation(key, obj, pageStructParents) {
103
+ this.elems.get(key)?.annots.push({ obj, structParents: pageStructParents });
104
+ }
68
105
  /** Link a page-local MCID to an element (called by the backend while serializing). */
69
106
  linkMarkedContent(key, structParents, mcid) {
70
107
  this.elems.get(key)?.mc.push({ structParents, mcid });
@@ -83,11 +120,24 @@ export class StructTree {
83
120
  objNum.set(key, om.addObject(""));
84
121
  const pg = (structParents) => this.pageObjByStructParents.get(structParents);
85
122
  const childrenOf = (parent) => [...this.elems.values()].filter((e) => e.parent === parent); // Map order = reading order
123
+ const before = (a, b) => a.structParents - b.structParents || a.mcid - b.mcid;
124
+ const firstOf = (e) => [...e.mc, ...childrenOf(e.key).flatMap((c) => firstOf(c) ?? [])].sort(before)[0];
125
+ const mcr = (m) => `<< /Type /MCR /Pg ${pg(m.structParents)} 0 R /MCID ${m.mcid} >>`;
86
126
  // One StructElem per registered element: /K = its child elements ++ its marked-content references.
127
+ // An element holding BOTH (a paragraph with an inline link) interleaves them in reading order.
87
128
  for (const e of this.elems.values()) {
129
+ const children = childrenOf(e.key);
130
+ const content = children.length > 0 && e.mc.length > 0
131
+ ? [
132
+ ...children.map((c) => ({ at: firstOf(c), ref: `${objNum.get(c.key)} 0 R` })),
133
+ ...e.mc.map((m) => ({ at: m, ref: mcr(m) })),
134
+ ]
135
+ .sort((a, b) => (a.at && b.at ? before(a.at, b.at) : 0))
136
+ .map((k) => k.ref)
137
+ : [...children.map((c) => `${objNum.get(c.key)} 0 R`), ...e.mc.map(mcr)];
88
138
  const kids = [
89
- ...childrenOf(e.key).map((c) => `${objNum.get(c.key)} 0 R`),
90
- ...e.mc.map((m) => `<< /Type /MCR /Pg ${pg(m.structParents)} 0 R /MCID ${m.mcid} >>`),
139
+ ...content,
140
+ ...e.annots.map((a) => `<< /Type /OBJR /Pg ${pg(a.structParents)} 0 R /Obj ${a.obj} 0 R >>`),
91
141
  ].join(" ");
92
142
  const alt = e.alt ? ` /Alt ${om.pdfString(e.alt)}` : "";
93
143
  // PDF/UA 7.5: a header cell needs a Scope so a reader can associate data cells with it. Our headers
@@ -110,17 +160,19 @@ export class StructTree {
110
160
  byPage.set(m.structParents, list);
111
161
  }
112
162
  }
113
- const nums = [...byPage.keys()]
114
- .sort((a, b) => a - b)
115
- .map((sp) => {
116
- const refs = byPage
117
- .get(sp)
163
+ // A page's entry is an array indexed by MCID; an annotation's is its element itself.
164
+ const entries = new Map();
165
+ for (const [sp, list] of byPage) {
166
+ const refs = list
118
167
  .slice()
119
168
  .sort((a, b) => a.mcid - b.mcid)
120
169
  .map((r) => `${objNum.get(r.key)} 0 R`)
121
170
  .join(" ");
122
- return `${sp} [ ${refs} ]`;
123
- });
171
+ entries.set(sp, `[ ${refs} ]`);
172
+ }
173
+ for (const [sp, key] of this.annotParents)
174
+ entries.set(sp, `${objNum.get(key)} 0 R`);
175
+ const nums = [...entries.keys()].sort((a, b) => a - b).map((sp) => `${sp} ${entries.get(sp)}`);
124
176
  om.replaceObject(parentTreeNum, `<< /Nums [ ${nums.join(" ")} ] >>`);
125
177
  om.replaceObject(rootNum, `<< /Type /StructTreeRoot /K ${docNum} 0 R /ParentTree ${parentTreeNum} 0 R ` +
126
178
  `/ParentTreeNextKey ${this.structParentsCounter} >>`);
@@ -51,6 +51,10 @@ export interface ColorGlyphLayer {
51
51
  }
52
52
  /** Sorts x-intervals and unions the overlapping ones. */
53
53
  export declare function mergeSpans(spans: Array<[number, number]>): Array<[number, number]>;
54
+ /** Thrown when font bytes are a format jasy cannot embed - said by name, never embedded broken. */
55
+ export declare class FontFormatError extends Error {
56
+ constructor(message: string);
57
+ }
54
58
  export declare class TTFParser {
55
59
  private data;
56
60
  unitsPerEm: number;
@@ -46,6 +46,12 @@ function applyAffine(m, x, y) {
46
46
  function detScale(m) {
47
47
  return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
48
48
  }
49
+ /** Thrown when font bytes are a format jasy cannot embed - said by name, never embedded broken. */
50
+ export class FontFormatError extends Error {
51
+ constructor(message) {
52
+ super(`@jasy/pdf: ${message}`);
53
+ }
54
+ }
49
55
  export class TTFParser {
50
56
  constructor(data) {
51
57
  this.data = data;
@@ -88,6 +94,17 @@ export class TTFParser {
88
94
  // subtables (coverage + classDef) per pair, and a long document repeats the same pairs thousands
89
95
  // of times, so the first lookup of a pair pays; the rest are O(1).
90
96
  this.kernCache = new Map();
97
+ // Every font path meets here (bytes, a file, a URL, an unwrapped WOFF/WOFF2). A CFF font has the
98
+ // same tables up front and measured fine, then went into the PDF as a TrueType program it is not.
99
+ const tag = String.fromCharCode(...data.subarray(0, 4));
100
+ if (tag === "OTTO") {
101
+ throw new FontFormatError("this is an OpenType font with PostScript (CFF) outlines, which jasy does not embed yet - " +
102
+ "use its TrueType-flavoured version (.ttf, or a .woff/.woff2 made from one)");
103
+ }
104
+ if (tag === "ttcf") {
105
+ throw new FontFormatError("this is a TrueType Collection (.ttc), which jasy does not read yet - extract the one face " +
106
+ "you need into a .ttf");
107
+ }
91
108
  this.readTableDirectory();
92
109
  const head = this.table("head").offset;
93
110
  this.unitsPerEm = u16(this.data, head + 18);
@@ -392,17 +409,23 @@ export class TTFParser {
392
409
  }
393
410
  if (kernLookups.size === 0)
394
411
  return;
395
- // The PairPos subtables of those lookups (a lookup of another type in the set is ignored).
412
+ // The PairPos subtables of those lookups (a lookup of another type in the set is ignored). A large
413
+ // font wraps its lookups in Extension subtables (type 9) for their 32-bit offsets - read through.
396
414
  const lookupCount = u16(this.data, lookupListOffset);
397
415
  for (const idx of kernLookups) {
398
416
  if (idx >= lookupCount)
399
417
  continue;
400
418
  const lookup = lookupListOffset + u16(this.data, lookupListOffset + 2 + idx * 2);
401
- if (u16(this.data, lookup) !== 2)
402
- continue; // lookupType 2 = pair adjustment
419
+ const type = u16(this.data, lookup);
420
+ if (type !== 2 && type !== 9)
421
+ continue; // 2 = pair adjustment, 9 = extension
403
422
  const subTableCount = u16(this.data, lookup + 4);
404
423
  for (let s = 0; s < subTableCount; s++) {
405
- this.gposPairPos.push(lookup + u16(this.data, lookup + 6 + s * 2));
424
+ const sub = lookup + u16(this.data, lookup + 6 + s * 2);
425
+ if (type === 2)
426
+ this.gposPairPos.push(sub);
427
+ else if (u16(this.data, sub + 2) === 2)
428
+ this.gposPairPos.push(sub + u32(this.data, sub + 4));
406
429
  }
407
430
  }
408
431
  if (this.gposPairPos.length > 0)
@@ -1,3 +1,4 @@
1
+ import { flexibleOf } from "../layout/flex-item.js";
1
2
  import { PDFDocumentElement } from "../elements/pdf-document-element.js";
2
3
  import { FlexiblePDFElement, hasChildProp, SizedPDFElement, } from "../elements/pdf-element.js";
3
4
  export class Validator {
@@ -58,7 +59,7 @@ export class Validator {
58
59
  }
59
60
  // Ensure a flexible element does not contain another flexible element
60
61
  if (hasChildProp(element)) {
61
- if (element.child instanceof FlexiblePDFElement) {
62
+ if (flexibleOf(element.child) !== undefined) {
62
63
  throw new Error(`Flexible element ${element.constructor.name} cannot hold another flexible element`);
63
64
  }
64
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasy/pdf",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.4",
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",
@@ -1,14 +0,0 @@
1
- import { SizedElement, WithChildren, SizedPDFElement, LayoutContext } from "../pdf-element.js";
2
- import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
- interface ContainerElementParams extends SizedElement, WithChildren {
4
- color?: [number, number, number];
5
- backgroundColor?: [number, number, number];
6
- borderWidth?: number;
7
- }
8
- export declare class SizedContainerElement extends SizedPDFElement {
9
- private children;
10
- constructor({ width, height, children }: ContainerElementParams);
11
- calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
12
- getProps(): ContainerElementParams;
13
- }
14
- export {};
@@ -1,32 +0,0 @@
1
- import { SizedPDFElement, } from "../pdf-element.js";
2
- import { BoxConstraints } from "../../layout/box-constraints.js";
3
- export class SizedContainerElement extends SizedPDFElement {
4
- constructor({ width, height, children }) {
5
- super({ x: 0, y: 0, width, height });
6
- this.children = children;
7
- }
8
- calculateLayout(constraints, offset, ctx) {
9
- if (constraints.hasBoundedWidth)
10
- this.width = constraints.maxWidth;
11
- if (constraints.hasBoundedHeight)
12
- this.height = constraints.maxHeight;
13
- // Absolute placement from the parent; assignment (not +=) so re-layout is idempotent.
14
- this.x = offset.x;
15
- this.y = offset.y;
16
- const width = this.width ?? 0;
17
- const height = this.height ?? 0;
18
- if (this.children)
19
- this.children.forEach((child) => child.calculateLayout(BoxConstraints.loose(width, height), { x: this.x, y: this.y }, ctx));
20
- // Top-left coordinates; the Y-flip now happens once at the IR -> backend seam.
21
- return { width, height };
22
- }
23
- getProps() {
24
- return {
25
- x: this.x,
26
- y: this.y,
27
- width: this.width,
28
- height: this.height,
29
- children: this.children,
30
- };
31
- }
32
- }