@jasy/pdf 1.0.0-beta.3 → 1.0.0-beta.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 (246) hide show
  1. package/dist/api/color.d.ts +1 -24
  2. package/dist/api/color.js +2 -278
  3. package/dist/api/content.d.ts +10 -5
  4. package/dist/api/content.js +29 -13
  5. package/dist/api/descriptor.js +30 -3
  6. package/dist/api/flow.d.ts +55 -0
  7. package/dist/api/flow.js +71 -0
  8. package/dist/api/font-url.d.ts +1 -2
  9. package/dist/api/font-url.js +8 -74
  10. package/dist/api/forms.d.ts +3 -0
  11. package/dist/api/forms.js +3 -0
  12. package/dist/api/gradient.d.ts +1 -53
  13. package/dist/api/gradient.js +2 -97
  14. package/dist/api/index.d.ts +4 -0
  15. package/dist/api/index.js +3 -0
  16. package/dist/api/layout.d.ts +6 -0
  17. package/dist/api/layout.js +9 -3
  18. package/dist/api/page-builder.d.ts +4 -0
  19. package/dist/api/page-builder.js +4 -0
  20. package/dist/api/structure.d.ts +10 -2
  21. package/dist/api/structure.js +9 -6
  22. package/dist/api/table.d.ts +5 -3
  23. package/dist/api/table.js +101 -22
  24. package/dist/api/text.d.ts +8 -8
  25. package/dist/api/text.js +6 -0
  26. package/dist/canvas/painter.d.ts +2 -2
  27. package/dist/canvas/painter.js +2 -2
  28. package/dist/common/color-input.d.ts +24 -0
  29. package/dist/common/color-input.js +278 -0
  30. package/dist/common/diagnostics.d.ts +61 -0
  31. package/dist/common/diagnostics.js +77 -0
  32. package/dist/common/gradient.d.ts +53 -0
  33. package/dist/common/gradient.js +97 -0
  34. package/dist/edit/acroform-reader.js +7 -7
  35. package/dist/edit/appearance.js +3 -3
  36. package/dist/edit/attachments.d.ts +52 -0
  37. package/dist/edit/attachments.js +178 -0
  38. package/dist/edit/document.d.ts +38 -8
  39. package/dist/edit/document.js +208 -51
  40. package/dist/edit/fill.js +8 -23
  41. package/dist/edit/filters.d.ts +15 -0
  42. package/dist/edit/filters.js +157 -0
  43. package/dist/edit/index.d.ts +4 -1
  44. package/dist/edit/index.js +3 -1
  45. package/dist/edit/objects.d.ts +3 -4
  46. package/dist/edit/objects.js +5 -18
  47. package/dist/elements/canvas-element.d.ts +3 -0
  48. package/dist/elements/canvas-element.js +14 -3
  49. package/dist/elements/container-element.d.ts +8 -1
  50. package/dist/elements/container-element.js +49 -17
  51. package/dist/elements/forms/checkbox-element.d.ts +2 -0
  52. package/dist/elements/forms/checkbox-element.js +12 -3
  53. package/dist/elements/forms/choice-element.d.ts +6 -1
  54. package/dist/elements/forms/choice-element.js +17 -6
  55. package/dist/elements/forms/field-font.d.ts +8 -0
  56. package/dist/elements/forms/field-font.js +5 -0
  57. package/dist/elements/forms/push-button-element.d.ts +6 -1
  58. package/dist/elements/forms/push-button-element.js +16 -4
  59. package/dist/elements/forms/radio-element.d.ts +2 -0
  60. package/dist/elements/forms/radio-element.js +12 -3
  61. package/dist/elements/forms/signature-element.d.ts +6 -1
  62. package/dist/elements/forms/signature-element.js +16 -4
  63. package/dist/elements/forms/text-field-element.d.ts +6 -1
  64. package/dist/elements/forms/text-field-element.js +16 -4
  65. package/dist/elements/image-element.d.ts +70 -21
  66. package/dist/elements/image-element.js +173 -52
  67. package/dist/elements/index.d.ts +3 -0
  68. package/dist/elements/index.js +3 -0
  69. package/dist/elements/layout/anchor-element.d.ts +8 -10
  70. package/dist/elements/layout/anchor-element.js +10 -18
  71. package/dist/elements/layout/bookmark-element.d.ts +9 -11
  72. package/dist/elements/layout/bookmark-element.js +11 -20
  73. package/dist/elements/layout/column-break-element.d.ts +17 -0
  74. package/dist/elements/layout/column-break-element.js +26 -0
  75. package/dist/elements/layout/default-text-style-element.d.ts +11 -13
  76. package/dist/elements/layout/default-text-style-element.js +14 -31
  77. package/dist/elements/layout/deferred-element.d.ts +20 -4
  78. package/dist/elements/layout/deferred-element.js +41 -9
  79. package/dist/elements/layout/expanded-element.d.ts +11 -0
  80. package/dist/elements/layout/expanded-element.js +53 -18
  81. package/dist/elements/layout/flow-element.d.ts +109 -0
  82. package/dist/elements/layout/flow-element.js +317 -0
  83. package/dist/elements/layout/keep-together-element.d.ts +8 -18
  84. package/dist/elements/layout/keep-together-element.js +33 -45
  85. package/dist/elements/layout/link-element.d.ts +8 -13
  86. package/dist/elements/layout/link-element.js +9 -19
  87. package/dist/elements/layout/padding-element.d.ts +18 -1
  88. package/dist/elements/layout/padding-element.js +63 -28
  89. package/dist/elements/layout/page-break-element.d.ts +4 -1
  90. package/dist/elements/layout/page-break-element.js +6 -1
  91. package/dist/elements/layout/page-builder-element.d.ts +5 -0
  92. package/dist/elements/layout/page-builder-element.js +13 -2
  93. package/dist/elements/layout/positioned-element.d.ts +23 -4
  94. package/dist/elements/layout/positioned-element.js +42 -12
  95. package/dist/elements/layout/proxy-element.d.ts +47 -0
  96. package/dist/elements/layout/proxy-element.js +106 -0
  97. package/dist/elements/layout/region-element.d.ts +53 -0
  98. package/dist/elements/layout/region-element.js +108 -0
  99. package/dist/elements/layout/repeating-header-element.d.ts +9 -0
  100. package/dist/elements/layout/repeating-header-element.js +55 -13
  101. package/dist/elements/layout/rotated-box-element.d.ts +6 -0
  102. package/dist/elements/layout/rotated-box-element.js +27 -14
  103. package/dist/elements/layout/rotated-element.d.ts +9 -0
  104. package/dist/elements/layout/rotated-element.js +33 -2
  105. package/dist/elements/layout/struct-group.d.ts +9 -17
  106. package/dist/elements/layout/struct-group.js +10 -35
  107. package/dist/elements/line-element.d.ts +1 -0
  108. package/dist/elements/line-element.js +6 -0
  109. package/dist/elements/page-element.d.ts +10 -3
  110. package/dist/elements/page-element.js +39 -31
  111. package/dist/elements/pdf-document-element.d.ts +3 -0
  112. package/dist/elements/pdf-document-element.js +7 -0
  113. package/dist/elements/pdf-element.d.ts +105 -16
  114. package/dist/elements/pdf-element.js +118 -19
  115. package/dist/elements/rectangle-element.d.ts +22 -2
  116. package/dist/elements/rectangle-element.js +119 -55
  117. package/dist/elements/row-element.d.ts +17 -12
  118. package/dist/elements/row-element.js +89 -9
  119. package/dist/elements/svg-element.d.ts +30 -6
  120. package/dist/elements/svg-element.js +91 -26
  121. package/dist/elements/text-element.d.ts +48 -13
  122. package/dist/elements/text-element.js +161 -116
  123. package/dist/forms/acroform.d.ts +9 -1
  124. package/dist/forms/acroform.js +83 -34
  125. package/dist/forms/appearance.d.ts +17 -6
  126. package/dist/forms/appearance.js +18 -16
  127. package/dist/ir/display-list.d.ts +18 -3
  128. package/dist/layout/box-constraints.d.ts +17 -0
  129. package/dist/layout/box-constraints.js +31 -0
  130. package/dist/layout/collect-images.d.ts +6 -4
  131. package/dist/layout/collect-images.js +14 -8
  132. package/dist/layout/flex-item.d.ts +11 -0
  133. package/dist/layout/flex-item.js +51 -0
  134. package/dist/layout/fragmentation.d.ts +23 -9
  135. package/dist/layout/fragmentation.js +62 -19
  136. package/dist/layout/layout-cache.d.ts +53 -0
  137. package/dist/layout/layout-cache.js +110 -0
  138. package/dist/platform/browser-fs.d.ts +1 -2
  139. package/dist/platform/browser-fs.js +3 -6
  140. package/dist/platform/node-fs.d.ts +0 -1
  141. package/dist/platform/node-fs.js +0 -4
  142. package/dist/renderer/anchor-renderer.d.ts +2 -1
  143. package/dist/renderer/anchor-renderer.js +5 -4
  144. package/dist/renderer/bookmark-renderer.d.ts +2 -1
  145. package/dist/renderer/bookmark-renderer.js +5 -4
  146. package/dist/renderer/canvas-renderer.d.ts +2 -1
  147. package/dist/renderer/canvas-renderer.js +5 -2
  148. package/dist/renderer/column-break-renderer.d.ts +8 -0
  149. package/dist/renderer/column-break-renderer.js +15 -0
  150. package/dist/renderer/container-renderer.d.ts +2 -1
  151. package/dist/renderer/container-renderer.js +6 -5
  152. package/dist/renderer/default-text-style-renderer.d.ts +2 -1
  153. package/dist/renderer/default-text-style-renderer.js +2 -3
  154. package/dist/renderer/deferred-renderer.d.ts +2 -1
  155. package/dist/renderer/deferred-renderer.js +2 -3
  156. package/dist/renderer/emoji-image.js +3 -1
  157. package/dist/renderer/expanded-renderer.d.ts +2 -1
  158. package/dist/renderer/expanded-renderer.js +2 -3
  159. package/dist/renderer/flow-renderer.d.ts +16 -0
  160. package/dist/renderer/flow-renderer.js +21 -0
  161. package/dist/renderer/form-field-renderer.d.ts +2 -1
  162. package/dist/renderer/form-field-renderer.js +16 -3
  163. package/dist/renderer/image-renderer.d.ts +2 -1
  164. package/dist/renderer/image-renderer.js +33 -53
  165. package/dist/renderer/index.d.ts +2 -0
  166. package/dist/renderer/index.js +2 -0
  167. package/dist/renderer/keep-together-renderer.d.ts +2 -1
  168. package/dist/renderer/keep-together-renderer.js +2 -3
  169. package/dist/renderer/line-renderer.d.ts +2 -1
  170. package/dist/renderer/line-renderer.js +7 -2
  171. package/dist/renderer/link-renderer.d.ts +2 -1
  172. package/dist/renderer/link-renderer.js +36 -5
  173. package/dist/renderer/padding-renderer.d.ts +2 -1
  174. package/dist/renderer/padding-renderer.js +5 -5
  175. package/dist/renderer/page-break-renderer.d.ts +6 -5
  176. package/dist/renderer/page-break-renderer.js +14 -7
  177. package/dist/renderer/page-builder-renderer.d.ts +2 -1
  178. package/dist/renderer/page-builder-renderer.js +2 -3
  179. package/dist/renderer/page-renderer.js +30 -10
  180. package/dist/renderer/pdf-backend.d.ts +4 -3
  181. package/dist/renderer/pdf-backend.js +14 -7
  182. package/dist/renderer/pdf-document-renderer.d.ts +1 -8
  183. package/dist/renderer/pdf-document-renderer.js +17 -17
  184. package/dist/renderer/pdf-renderer.js +20 -11
  185. package/dist/renderer/positioned-renderer.d.ts +18 -1
  186. package/dist/renderer/positioned-renderer.js +43 -6
  187. package/dist/renderer/rectangle-renderer.d.ts +2 -1
  188. package/dist/renderer/rectangle-renderer.js +18 -8
  189. package/dist/renderer/repeating-header-renderer.d.ts +2 -1
  190. package/dist/renderer/repeating-header-renderer.js +6 -5
  191. package/dist/renderer/rotated-renderer.d.ts +2 -1
  192. package/dist/renderer/rotated-renderer.js +9 -5
  193. package/dist/renderer/row-renderer.d.ts +2 -1
  194. package/dist/renderer/row-renderer.js +6 -5
  195. package/dist/renderer/struct-group-renderer.d.ts +2 -1
  196. package/dist/renderer/struct-group-renderer.js +2 -3
  197. package/dist/renderer/svg-renderer.d.ts +2 -1
  198. package/dist/renderer/svg-renderer.js +8 -2
  199. package/dist/renderer/text-renderer.d.ts +4 -6
  200. package/dist/renderer/text-renderer.js +114 -127
  201. package/dist/svg/gradients.js +5 -0
  202. package/dist/svg/index.d.ts +1 -1
  203. package/dist/svg/index.js +36 -6
  204. package/dist/svg/style.d.ts +2 -0
  205. package/dist/svg/style.js +12 -1
  206. package/dist/text/advance.d.ts +4 -0
  207. package/dist/text/advance.js +17 -4
  208. package/dist/text/bidi.d.ts +0 -2
  209. package/dist/text/bidi.js +0 -10
  210. package/dist/text/glyph-coverage.d.ts +1 -1
  211. package/dist/text/glyph-coverage.js +7 -3
  212. package/dist/text/hyphen-breaks.d.ts +17 -0
  213. package/dist/text/hyphen-breaks.js +54 -0
  214. package/dist/text/line-breaker.d.ts +55 -29
  215. package/dist/text/line-breaker.js +453 -188
  216. package/dist/text/line-metrics.d.ts +8 -3
  217. package/dist/text/line-metrics.js +11 -2
  218. package/dist/text/pdf-text-string.d.ts +21 -0
  219. package/dist/text/pdf-text-string.js +114 -0
  220. package/dist/text/text-decoration.d.ts +5 -5
  221. package/dist/text/text-decoration.js +5 -5
  222. package/dist/text/word-splitting.d.ts +15 -2
  223. package/dist/text/word-splitting.js +45 -13
  224. package/dist/utils/afm-parser.d.ts +2 -0
  225. package/dist/utils/afm-parser.js +10 -1
  226. package/dist/utils/dest-registry.d.ts +1 -1
  227. package/dist/utils/dest-registry.js +3 -2
  228. package/dist/utils/fetch-bytes.d.ts +19 -0
  229. package/dist/utils/fetch-bytes.js +79 -0
  230. package/dist/utils/flex-layout.d.ts +16 -14
  231. package/dist/utils/flex-layout.js +427 -217
  232. package/dist/utils/font-metrics.d.ts +4 -3
  233. package/dist/utils/image-helper.d.ts +59 -11
  234. package/dist/utils/image-helper.js +209 -81
  235. package/dist/utils/pdf-object-manager.d.ts +17 -6
  236. package/dist/utils/pdf-object-manager.js +70 -20
  237. package/dist/utils/renderer-registry.d.ts +49 -2
  238. package/dist/utils/renderer-registry.js +40 -0
  239. package/dist/utils/struct-tree.d.ts +24 -0
  240. package/dist/utils/struct-tree.js +61 -9
  241. package/dist/utils/ttf-parser.d.ts +4 -0
  242. package/dist/utils/ttf-parser.js +30 -6
  243. package/dist/validators/element-validator.js +2 -1
  244. package/package.json +1 -1
  245. package/dist/elements/layout/sized-container-element.d.ts +0 -14
  246. package/dist/elements/layout/sized-container-element.js +0 -32
@@ -0,0 +1,16 @@
1
+ import { FlowElement, FlowPieceElement } from "../elements/layout/flow-element.js";
2
+ import { RegionElement } from "../elements/layout/region-element.js";
3
+ import { PDFObjectManager } from "../utils/pdf-object-manager.js";
4
+ import { IRNode } from "../ir/display-list.js";
5
+ import { type RenderScope } from "../utils/renderer-registry.js";
6
+ /** A Flow draws the page it made (laid out in place, in a box with a height). */
7
+ export declare class FlowRenderer {
8
+ static render(flow: FlowElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
9
+ }
10
+ /** One page of a Flow: its template, whose Regions draw what they hold. */
11
+ export declare class FlowPieceRenderer {
12
+ static render(piece: FlowPieceElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
13
+ }
14
+ export declare class RegionRenderer {
15
+ static render(region: RegionElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
16
+ }
@@ -0,0 +1,21 @@
1
+ import { RendererRegistry, within } from "../utils/renderer-registry.js";
2
+ /** A Flow draws the page it made (laid out in place, in a box with a height). */
3
+ export class FlowRenderer {
4
+ static async render(flow, objectManager, scope) {
5
+ const { x, y, piece } = flow.getProps();
6
+ return piece ? RendererRegistry.render(piece, objectManager, within(scope, { x, y })) : [];
7
+ }
8
+ }
9
+ /** One page of a Flow: its template, whose Regions draw what they hold. */
10
+ export class FlowPieceRenderer {
11
+ static async render(piece, objectManager, scope) {
12
+ const { x, y, child } = piece.getProps();
13
+ return RendererRegistry.render(child, objectManager, within(scope, { x, y }));
14
+ }
15
+ }
16
+ export class RegionRenderer {
17
+ static async render(region, objectManager, scope) {
18
+ const { x, y, child } = region.getProps();
19
+ return child ? RendererRegistry.render(child, objectManager, within(scope, { x, y })) : [];
20
+ }
21
+ }
@@ -1,3 +1,4 @@
1
+ import type { RenderScope } from "../utils/renderer-registry.js";
1
2
  import { PDFObjectManager } from "../utils/pdf-object-manager.js";
2
3
  import { TextFieldElement } from "../elements/forms/text-field-element.js";
3
4
  import { CheckboxElement } from "../elements/forms/checkbox-element.js";
@@ -15,6 +16,6 @@ type FormFieldElement = TextFieldElement | CheckboxElement | RadioElement | Choi
15
16
  * discriminates downstream).
16
17
  */
17
18
  export declare class FormFieldRenderer {
18
- static render(element: FormFieldElement, _objectManager: PDFObjectManager): Promise<IRNode[]>;
19
+ static render(element: FormFieldElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
19
20
  }
20
21
  export {};
@@ -5,8 +5,21 @@
5
5
  * discriminates downstream).
6
6
  */
7
7
  export class FormFieldRenderer {
8
- static async render(element, _objectManager) {
9
- const { x, y, width, height, spec, style } = element.getProps();
10
- return [{ type: "formfield", x, y, width, height, field: spec, style }];
8
+ static async render(element, objectManager, scope) {
9
+ const props = element.getProps();
10
+ const { x: ownX, y: ownY, width, height, spec, style } = props;
11
+ const font = "font" in props ? props.font : undefined;
12
+ // Its own position is relative to `scope.origin`; the IR wants page coordinates.
13
+ const x = scope.origin.x + (ownX ?? 0);
14
+ const y = scope.origin.y + (ownY ?? 0);
15
+ // Tagged: the widget goes into a /Form element, named by its tooltip (/TU) or else its field name
16
+ // (PDF/UA 7.18.1, 7.18.4). A radio button is one answer of its group.
17
+ const name = spec.kind === "radio" ? `${spec.group}: ${spec.value}` : spec.name;
18
+ const structKey = objectManager.struct.enabled
19
+ ? objectManager.struct.openElement(element.structId, "Form", {
20
+ alt: spec.tooltip === undefined ? name : undefined,
21
+ })
22
+ : undefined;
23
+ return [{ type: "formfield", x, y, width, height, field: spec, style, structKey, font }];
11
24
  }
12
25
  }
@@ -1,6 +1,7 @@
1
+ import type { RenderScope } from "../utils/renderer-registry.js";
1
2
  import { ImageElement } from "../elements/image-element.js";
2
3
  import { PDFObjectManager } from "../utils/pdf-object-manager.js";
3
4
  import { IRNode } from "../ir/display-list.js";
4
5
  export declare class ImageRenderer {
5
- static render(imageElement: ImageElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
6
+ static render(imageElement: ImageElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
6
7
  }
@@ -1,15 +1,20 @@
1
1
  import { BoxFit } from "../elements/image-element.js";
2
2
  import { bytesFromLatin1 } from "../utils/bytes.js";
3
- import { applyContainFit, applyCoverFit, applyFillFit, applyFitNone, decodePngToRgbFlate, } from "../utils/image-helper.js";
3
+ import { applyContainFit, applyCoverFit, applyFillFit, applyFitNone, decodePngToRgbFlate, jpegColor, naturalSize, } from "../utils/image-helper.js";
4
4
  import { isRounded } from "../ir/display-list.js";
5
5
  export class ImageRenderer {
6
- static async render(imageElement, objectManager) {
6
+ static async render(imageElement, objectManager, scope) {
7
7
  // Load the image and convert it in a binary string
8
- let { x, y, width, height, image, fit, radius, alt } = imageElement.getProps();
8
+ const { x: ownX, y: ownY, width, height, image, fit, radius, align, alt, } = imageElement.getProps();
9
+ // Its own position is relative to `scope.origin`; the IR wants page coordinates.
10
+ const x = scope.origin.x + (ownX ?? 0);
11
+ const y = scope.origin.y + (ownY ?? 0);
9
12
  await image.init(); // Load and initialize the image
10
13
  const imageType = await image.getImageType(); // For the moment we can handle `png` and `jpg/jpeg` files
11
14
  const fileData = await image.getFileData();
12
15
  const dimensions = await image.getImageDimensions();
16
+ // Laid out at its own size in points; the pixels only describe the XObject.
17
+ const own = naturalSize(dimensions);
13
18
  if (!fileData) {
14
19
  throw new Error("File data cannot be `null`");
15
20
  }
@@ -17,71 +22,46 @@ export class ImageRenderer {
17
22
  // so decode it to raw DeviceRGB samples that the FlateDecode XObject path expects.
18
23
  let embedData = fileData;
19
24
  let smask;
25
+ // A JPEG is embedded as it is, so it keeps the color space it was saved in; a PNG becomes RGB.
26
+ const color = imageType === "DCTDecode" ? jpegColor(bytesFromLatin1(fileData)) : undefined;
20
27
  if (imageType === "FlateDecode") {
21
28
  const decoded = await decodePngToRgbFlate(bytesFromLatin1(fileData));
22
29
  embedData = decoded.data;
23
30
  smask = decoded.smask;
24
31
  }
25
- // Now we check the `fit` property and changing the dimensions of the image
26
- // Optionally we must add an overflow container
27
- let mustCreateOverflowContainer = false;
28
- const containerDimensions = JSON.parse(JSON.stringify({ x, y, width, height })); // Deep clone images dimensions
29
- switch (fit) {
30
- case BoxFit.cover:
31
- mustCreateOverflowContainer = true;
32
- const fitCoverResult = applyCoverFit(dimensions.width, dimensions.height, width ?? 0, height ?? 0);
33
- x += fitCoverResult.offsetX;
34
- y += fitCoverResult.offsetY;
35
- width = fitCoverResult.width;
36
- height = fitCoverResult.height;
37
- break;
38
- case BoxFit.contain:
39
- mustCreateOverflowContainer = true;
40
- const fitContainResult = applyContainFit(dimensions.width, dimensions.height, width ?? 0, height ?? 0);
41
- x += fitContainResult.offsetX;
42
- y += fitContainResult.offsetY;
43
- width = fitContainResult.width;
44
- height = fitContainResult.height;
45
- break;
46
- case BoxFit.none:
47
- const fitNoneResult = applyFitNone(dimensions.width, dimensions.height, width ?? 0, height ?? 0);
48
- x += fitNoneResult.offsetX;
49
- y += fitNoneResult.offsetY;
50
- width = fitNoneResult.width;
51
- height = fitNoneResult.height;
52
- break;
53
- case BoxFit.fill:
54
- const fitFillResult = applyFillFit(width ?? 0, height ?? 0);
55
- width = fitFillResult.width;
56
- height = fitFillResult.height;
57
- }
58
- // A radius rounds the image BOX (the element frame), so it clips to that frame too -
59
- // independent of the cover/contain overflow clip.
60
- const wantsClip = mustCreateOverflowContainer || isRounded(radius);
32
+ // The box laid out for it; `fit` decides what is drawn in it and where.
33
+ const box = {
34
+ x,
35
+ y,
36
+ width: width ?? own.width,
37
+ height: height ?? own.height,
38
+ };
39
+ const fitted = fit === BoxFit.cover
40
+ ? applyCoverFit(own.width, own.height, box.width, box.height, align)
41
+ : fit === BoxFit.contain
42
+ ? applyContainFit(own.width, own.height, box.width, box.height, align)
43
+ : fit === BoxFit.none
44
+ ? applyFitNone(own.width, own.height, box.width, box.height, align)
45
+ : applyFillFit(box.width, box.height);
46
+ // Anything but `fill` may leave the box, so it is clipped to it; a radius rounds the box too.
47
+ const wantsClip = fit !== BoxFit.fill || isRounded(radius);
61
48
  // The fitted geometry becomes a display-list primitive; the backend registers
62
49
  // the XObject and emits the placement (+ clip, rounded when a radius is set).
63
50
  const node = {
64
51
  type: "image",
65
- x,
66
- y,
67
- width: width,
68
- height: height,
52
+ x: x + fitted.offsetX,
53
+ y: y + fitted.offsetY,
54
+ width: fitted.width,
55
+ height: fitted.height,
69
56
  intrinsicWidth: dimensions.width,
70
57
  intrinsicHeight: dimensions.height,
71
58
  data: embedData,
72
59
  imageType,
60
+ ...(color ? { color } : {}),
61
+ ...(dimensions.orientation ? { orientation: dimensions.orientation } : {}),
73
62
  ...(smask ? { smask } : {}),
74
63
  ...(radius ? { radius } : {}),
75
- ...(wantsClip
76
- ? {
77
- clip: {
78
- x: containerDimensions.x,
79
- y: containerDimensions.y,
80
- width: containerDimensions.width,
81
- height: containerDimensions.height,
82
- },
83
- }
84
- : {}),
64
+ ...(wantsClip ? { clip: box } : {}),
85
65
  };
86
66
  // Accessible tagging: an image WITH alt text is a Figure; without, it stays untagged and the backend
87
67
  // treats it as decoration (an Artifact). One struct element per image.
@@ -10,3 +10,5 @@ export * from "./line-renderer.js";
10
10
  export * from "./row-renderer.js";
11
11
  export * from "./svg-renderer.js";
12
12
  export * from "./canvas-renderer.js";
13
+ export * from "./flow-renderer.js";
14
+ export * from "./column-break-renderer.js";
@@ -10,3 +10,5 @@ export * from "./line-renderer.js";
10
10
  export * from "./row-renderer.js";
11
11
  export * from "./svg-renderer.js";
12
12
  export * from "./canvas-renderer.js";
13
+ export * from "./flow-renderer.js";
14
+ export * from "./column-break-renderer.js";
@@ -1,10 +1,11 @@
1
1
  import { KeepTogetherElement } from "../elements/layout/keep-together-element.js";
2
2
  import { PDFObjectManager } from "../utils/pdf-object-manager.js";
3
3
  import { IRNode } from "../ir/display-list.js";
4
+ import { type RenderScope } from "../utils/renderer-registry.js";
4
5
  /**
5
6
  * A `keepTogether` is layout-transparent: it just renders its child. All of its work happens in the
6
7
  * layout pass (`fragment`), never here.
7
8
  */
8
9
  export declare class KeepTogetherRenderer {
9
- static render(group: KeepTogetherElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
10
+ static render(group: KeepTogetherElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
10
11
  }
@@ -4,9 +4,8 @@ import { RendererRegistry } from "../utils/renderer-registry.js";
4
4
  * layout pass (`fragment`), never here.
5
5
  */
6
6
  export class KeepTogetherRenderer {
7
- static async render(group, objectManager) {
7
+ static async render(group, objectManager, scope) {
8
8
  const { child } = group.getProps();
9
- const renderer = RendererRegistry.getRenderer(child);
10
- return renderer(child, objectManager);
9
+ return RendererRegistry.render(child, objectManager, scope);
11
10
  }
12
11
  }
@@ -1,6 +1,7 @@
1
+ import type { RenderScope } from "../utils/renderer-registry.js";
1
2
  import { PDFObjectManager } from "../utils/pdf-object-manager.js";
2
3
  import { LineElement } from "../elements/index.js";
3
4
  import { IRNode } from "../ir/display-list.js";
4
5
  export declare class LineRenderer {
5
- static render(lineElement: LineElement, _objectManager: PDFObjectManager): Promise<IRNode[]>;
6
+ static render(lineElement: LineElement, _objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
6
7
  }
@@ -1,6 +1,11 @@
1
1
  export class LineRenderer {
2
- static async render(lineElement, _objectManager) {
3
- const { x, y, xEnd, yEnd, color, strokeWidth } = lineElement.getProps();
2
+ static async render(lineElement, _objectManager, scope) {
3
+ const { x: ownX, y: ownY, xEnd: ownXEnd, yEnd: ownYEnd, color, strokeWidth, } = lineElement.getProps();
4
+ // Its own position is relative to `scope.origin`; the IR wants page coordinates.
5
+ const x = scope.origin.x + (ownX ?? 0);
6
+ const y = scope.origin.y + (ownY ?? 0);
7
+ const xEnd = scope.origin.x + (ownXEnd ?? 0);
8
+ const yEnd = scope.origin.y + (ownYEnd ?? 0);
4
9
  const node = {
5
10
  type: "line",
6
11
  x1: x,
@@ -1,6 +1,7 @@
1
1
  import { PDFObjectManager } from "../utils/pdf-object-manager.js";
2
2
  import { LinkElement } from "../elements/layout/link-element.js";
3
+ import { type RenderScope } from "../utils/renderer-registry.js";
3
4
  import { IRNode } from "../ir/display-list.js";
4
5
  export declare class LinkRenderer {
5
- static render(element: LinkElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
6
+ static render(element: LinkElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
6
7
  }
@@ -1,11 +1,42 @@
1
1
  import { RendererRegistry } from "../utils/renderer-registry.js";
2
+ import { TextElement } from "../elements/text-element.js";
3
+ /** The text a link covers, as read - not run by run, where a justified line is one run per word. */
4
+ function textOf(element) {
5
+ if (element instanceof TextElement) {
6
+ return [
7
+ element
8
+ .getProps()
9
+ .spans.map((s) => s.content)
10
+ .join(""),
11
+ ];
12
+ }
13
+ return element.layoutChildren().flatMap(textOf);
14
+ }
2
15
  export class LinkRenderer {
3
- static async render(element, objectManager) {
4
- const { href, dest, child, x, y, width, height } = element.getProps();
5
- const renderer = RendererRegistry.getRenderer(child);
6
- const childNodes = await renderer(child, objectManager);
16
+ static async render(element, objectManager, scope) {
17
+ const { href, dest, child, x: ownX, y: ownY, width, height } = element.getProps();
18
+ // Its own position is relative to `scope.origin`; the IR wants page coordinates.
19
+ const x = scope.origin.x + (ownX ?? 0);
20
+ const y = scope.origin.y + (ownY ?? 0);
21
+ const renderChild = () => RendererRegistry.render(child, objectManager, scope);
7
22
  // The child draws normally; a Link IR node carries the clickable rect + target (external href or
8
23
  // internal dest) for the page renderer to emit as a /Link annotation (no content-stream ops itself).
9
- return [...childNodes, { type: "link", x, y, width, height, href, dest }];
24
+ if (!objectManager.struct.enabled) {
25
+ return [...(await renderChild()), { type: "link", x, y, width, height, href, dest }];
26
+ }
27
+ // Tagged: the link's content sits in a /Link element, which also takes the annotation, and the
28
+ // annotation describes itself by the text it covers (PDF/UA 7.18.5). A split link is ONE element.
29
+ const struct = objectManager.struct;
30
+ const structKey = struct.openElement(element.structId, "Link");
31
+ struct.push(structKey);
32
+ let childNodes;
33
+ try {
34
+ childNodes = await renderChild();
35
+ }
36
+ finally {
37
+ struct.pop();
38
+ }
39
+ const alt = textOf(child).join(" ").replace(/\s+/g, " ").trim() || (href ?? dest ?? "");
40
+ return [...childNodes, { type: "link", x, y, width, height, href, dest, structKey, alt }];
10
41
  }
11
42
  }
@@ -1,6 +1,7 @@
1
1
  import { PDFObjectManager } from "../utils/pdf-object-manager.js";
2
+ import { type RenderScope } from "../utils/renderer-registry.js";
2
3
  import { PaddingElement } from "../elements/layout/padding-element.js";
3
4
  import { IRNode } from "../ir/display-list.js";
4
5
  export declare class PaddingRenderer {
5
- static render(paddingElement: PaddingElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
6
+ static render(paddingElement: PaddingElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
6
7
  }
@@ -1,8 +1,8 @@
1
- import { RendererRegistry } from "../utils/renderer-registry.js";
1
+ import { RendererRegistry, within } from "../utils/renderer-registry.js";
2
2
  export class PaddingRenderer {
3
- static async render(paddingElement, objectManager) {
4
- const { child } = paddingElement.getProps();
5
- const renderer = RendererRegistry.getRenderer(child);
6
- return renderer(child, objectManager);
3
+ static async render(paddingElement, objectManager, scope) {
4
+ const { x, y, child } = paddingElement.getProps();
5
+ // Its child is placed relative to its own top-left, inset by the margins.
6
+ return RendererRegistry.render(child, objectManager, within(scope, { x, y }));
7
7
  }
8
8
  }
@@ -1,14 +1,15 @@
1
+ import type { RenderScope } from "../utils/renderer-registry.js";
1
2
  import { PDFObjectManager } from "../utils/pdf-object-manager.js";
2
3
  import { PageBreakElement } from "../elements/layout/page-break-element.js";
3
4
  import { IRNode } from "../ir/display-list.js";
4
5
  /**
5
6
  * A `PageBreak` draws nothing - the pagination packer consumes it at the cut, so in a vertical flow it
6
7
  * never reaches this pass. Reaching it therefore means the break was ineffective: it sat somewhere that
7
- * does not paginate, like inside a horizontal `Row` (a row is one line, you cannot split it across
8
- * pages - CSS and react-pdf ignore a forced break there too). We degrade to drawing nothing, but warn
9
- * once so the mistake is not silent. This one choke-point catches every ineffective break; no element
10
- * needs a special case.
8
+ * does not paginate - inside a horizontal `Row` (one line; CSS and react-pdf ignore a forced break there
9
+ * too), or inside a `PageBuilder`, whose content is built for the page it lands on and placed whole. We
10
+ * degrade to drawing nothing, but warn so the mistake is not silent. This one choke-point catches every
11
+ * ineffective break; no element needs a special case.
11
12
  */
12
13
  export declare class PageBreakRenderer {
13
- static render(_element: PageBreakElement, _objectManager: PDFObjectManager): Promise<IRNode[]>;
14
+ static render(element: PageBreakElement, objectManager: PDFObjectManager, _scope: RenderScope): Promise<IRNode[]>;
14
15
  }
@@ -1,15 +1,22 @@
1
1
  /**
2
2
  * A `PageBreak` draws nothing - the pagination packer consumes it at the cut, so in a vertical flow it
3
3
  * never reaches this pass. Reaching it therefore means the break was ineffective: it sat somewhere that
4
- * does not paginate, like inside a horizontal `Row` (a row is one line, you cannot split it across
5
- * pages - CSS and react-pdf ignore a forced break there too). We degrade to drawing nothing, but warn
6
- * once so the mistake is not silent. This one choke-point catches every ineffective break; no element
7
- * needs a special case.
4
+ * does not paginate - inside a horizontal `Row` (one line; CSS and react-pdf ignore a forced break there
5
+ * too), or inside a `PageBuilder`, whose content is built for the page it lands on and placed whole. We
6
+ * degrade to drawing nothing, but warn so the mistake is not silent. This one choke-point catches every
7
+ * ineffective break; no element needs a special case.
8
8
  */
9
9
  export class PageBreakRenderer {
10
- static async render(_element, _objectManager) {
11
- console.warn("PageBreak had no effect: it is not in a paginating flow (e.g. it is inside a Row, which is a " +
12
- "single horizontal line). Put the break as a sibling in a Column to break after that content.");
10
+ static async render(element, objectManager, _scope) {
11
+ objectManager.diagnostics.report({
12
+ code: "page-break-ignored",
13
+ severity: "warning",
14
+ message: "PageBreak had no effect: it is not in a paginating flow - it is inside a Row (a single " +
15
+ "horizontal line) or a PageBuilder (built for one page and placed whole, Columns inside it " +
16
+ "included). Move the break out, into a Column that flows across pages.",
17
+ about: element,
18
+ page: element.page,
19
+ });
13
20
  return [];
14
21
  }
15
22
  }
@@ -1,6 +1,7 @@
1
1
  import { PDFObjectManager } from "../utils/pdf-object-manager.js";
2
2
  import { PageBuilderElement } from "../elements/layout/page-builder-element.js";
3
+ import { type RenderScope } from "../utils/renderer-registry.js";
3
4
  import { IRNode } from "../ir/display-list.js";
4
5
  export declare class PageBuilderRenderer {
5
- static render(element: PageBuilderElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
6
+ static render(element: PageBuilderElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
6
7
  }
@@ -1,11 +1,10 @@
1
1
  import { RendererRegistry } from "../utils/renderer-registry.js";
2
2
  export class PageBuilderRenderer {
3
- static async render(element, objectManager) {
3
+ static async render(element, objectManager, scope) {
4
4
  // `composed` is whatever the closure built for THIS page during the render pass's layout.
5
5
  const { composed } = element.getProps();
6
6
  if (!composed)
7
7
  return [];
8
- const renderer = RendererRegistry.getRenderer(composed);
9
- return renderer(composed, objectManager);
8
+ return RendererRegistry.render(composed, objectManager, scope);
10
9
  }
11
10
  }
@@ -1,10 +1,19 @@
1
1
  import { pageFormats, PageSize } from "../constants/page-sizes.js";
2
+ import { resolvePageContentBox } from "../elements/page-element.js";
2
3
  import { RendererRegistry } from "../utils/renderer-registry.js";
3
4
  import { Orientation } from "./pdf-config.js";
4
5
  import { PdfBackend } from "./pdf-backend.js";
6
+ import { PositionedRenderer } from "./positioned-renderer.js";
5
7
  export class PageRenderer {
6
8
  static async render(page, objectManager) {
7
9
  const { children, config, header, footer } = page.getProps();
10
+ RendererRegistry.beginPage(objectManager); // an element may be drawn once per page
11
+ // A page is the outermost box: its children's offsets are page coordinates. Its content box is the
12
+ // frame a `Positioned` with no `relative` ancestor was placed against.
13
+ const scope = {
14
+ origin: { x: 0, y: 0 },
15
+ frame: resolvePageContentBox(config).origin, // config is resolved by layout
16
+ };
8
17
  // Header (top band) and footer (bottom band) sit around the body and repeat on every
9
18
  // physical page; they are placed by `PageElement.calculateLayout` / the page driver.
10
19
  const renderables = [...(header ? [header] : []), ...children, ...(footer ? [footer] : [])];
@@ -18,11 +27,8 @@ export class PageRenderer {
18
27
  // Collect the whole page as a display list (top-left coordinates), flip it to PDF
19
28
  // coordinates at this one seam, then serialize once. Serializing registers the
20
29
  // fonts/images used below, so it must run before the resource section.
21
- const nodes = [];
22
- for (const element of renderables) {
23
- const renderer = RendererRegistry.getRenderer(element);
24
- nodes.push(...(await renderer(element, objectManager)));
25
- }
30
+ // The page is the outermost stacking context: header, body and footer share one paint order.
31
+ const nodes = await PositionedRenderer.paintStacked(renderables, objectManager, scope);
26
32
  // Accessible tagging: one struct context per page (allocates its StructParents index), threaded into
27
33
  // serialize so each drawable node is wrapped in marked content. Undefined = tagging off (byte-identical).
28
34
  const structCtx = objectManager.struct.enabled ? objectManager.struct.beginPage() : undefined;
@@ -72,21 +78,35 @@ export class PageRenderer {
72
78
  const num = (n) => Number(n.toFixed(2));
73
79
  const annotRefs = [];
74
80
  for (const node of flipped) {
81
+ if (node.type !== "link" && node.type !== "formfield")
82
+ continue;
83
+ // Tagged, every annotation names its structure element (PDF/UA 7.18).
84
+ const structKey = structCtx ? node.structKey : undefined;
85
+ const structParent = structKey === undefined ? undefined : objectManager.struct.annotationParent(structKey);
86
+ const tag = (obj) => {
87
+ if (structKey !== undefined) {
88
+ objectManager.struct.linkAnnotation(structKey, obj, structCtx.structParents);
89
+ }
90
+ };
75
91
  // A form field: build its Widget annotation + register it as an /AcroForm field (the logic lives in
76
92
  // forms/acroform.ts), then reference it here in the page /Annots - the same side channel as a link.
77
93
  if (node.type === "formfield") {
78
- annotRefs.push(`${objectManager.acroform.addField(node, objectManager)} 0 R`);
94
+ const widget = objectManager.acroform.addField(node, objectManager, structParent);
95
+ tag(widget);
96
+ annotRefs.push(`${widget} 0 R`);
79
97
  continue;
80
98
  }
81
- if (node.type !== "link")
82
- continue;
83
99
  const rect = `[${num(node.x)} ${num(node.y)} ${num(node.x + node.width)} ${num(node.y + node.height)}]`;
84
100
  // Either an external URL (/URI) or an internal named destination (/GoTo, resolved via /Names /Dests).
85
101
  // The Link IR guarantees exactly one is set; prefer `dest` when present.
86
102
  const action = node.dest !== undefined
87
103
  ? `/A << /S /GoTo /D ${objectManager.pdfString(node.dest)} >>`
88
- : `/A << /S /URI /URI ${objectManager.pdfString(node.href ?? "")} >>`;
89
- const annot = objectManager.addObject(`<< /Type /Annot /Subtype /Link /Rect ${rect} /Border [0 0 0] ${action} >>`);
104
+ : `/A << /S /URI /URI ${objectManager.pdfUri(node.href ?? "")} >>`;
105
+ const tagged = structParent === undefined
106
+ ? ""
107
+ : ` /StructParent ${structParent} /Contents ${objectManager.pdfString(node.alt ?? "")}`;
108
+ const annot = objectManager.addObject(`<< /Type /Annot /Subtype /Link /Rect ${rect} /Border [0 0 0] ${action}${tagged} >>`);
109
+ tag(annot);
90
110
  annotRefs.push(`${annot} 0 R`);
91
111
  }
92
112
  const annotsCode = annotRefs.length > 0 ? ` /Annots [${annotRefs.join(" ")}]` : "";
@@ -70,9 +70,10 @@ export declare class PdfBackend {
70
70
  */
71
71
  private static roundedRectPath;
72
72
  /**
73
- * Clamp the four radii into the box. Each is capped at half the box, and two radii sharing an edge
74
- * are scaled down together when they would overlap - the CSS rule, and what keeps the path from
75
- * folding back on itself.
73
+ * Clamp the four radii into the box: two radii sharing an edge are scaled down together when they
74
+ * would overlap - the CSS rule (css-backgrounds-3, corner overlap), and what keeps the path from
75
+ * folding back on itself. A single corner is NOT capped at half the box: on a 200x40 box a lone
76
+ * 30pt corner stays 30, as in CSS; equal corners still end at half the short side.
76
77
  */
77
78
  private static clampRadii;
78
79
  /**
@@ -1,5 +1,6 @@
1
1
  import { isRounded, } from "../ir/display-list.js";
2
2
  import { Color } from "../common/color.js";
3
+ import { ORIENTATION_MATRIX } from "../utils/image-helper.js";
3
4
  /**
4
5
  * PDF backend - turns display-list primitives into content-stream operators.
5
6
  *
@@ -279,14 +280,14 @@ export class PdfBackend {
279
280
  `h`);
280
281
  }
281
282
  /**
282
- * Clamp the four radii into the box. Each is capped at half the box, and two radii sharing an edge
283
- * are scaled down together when they would overlap - the CSS rule, and what keeps the path from
284
- * folding back on itself.
283
+ * Clamp the four radii into the box: two radii sharing an edge are scaled down together when they
284
+ * would overlap - the CSS rule (css-backgrounds-3, corner overlap), and what keeps the path from
285
+ * folding back on itself. A single corner is NOT capped at half the box: on a 200x40 box a lone
286
+ * 30pt corner stays 30, as in CSS; equal corners still end at half the short side.
285
287
  */
286
288
  static clampRadii(radius, w, h) {
287
289
  const n = typeof radius === "number" ? radius : 0;
288
- const cap = Math.max(0, Math.min(w, h) / 2);
289
- const at = (v) => Math.max(0, Math.min(v ?? n, cap));
290
+ const at = (v) => Math.max(0, v ?? n);
290
291
  const r = typeof radius === "number"
291
292
  ? { tl: at(radius), tr: at(radius), br: at(radius), bl: at(radius) }
292
293
  : { tl: at(radius.tl), tr: at(radius.tr), br: at(radius.br), bl: at(radius.bl) };
@@ -416,8 +417,14 @@ export class PdfBackend {
416
417
  case "image": {
417
418
  // The backend owns PDF resource creation: register the XObject (using the
418
419
  // source pixel dimensions) and then place it with a scaling matrix.
419
- const ref = om.registerImage(node.intrinsicWidth, node.intrinsicHeight, node.imageType, node.data, node.smask);
420
- const draw = `q\n${node.width} 0 0 ${node.height} ${node.x} ${node.y} cm\n` + `/IM${ref} Do\nQ\n`;
420
+ const ref = om.registerImage(node.intrinsicWidth, node.intrinsicHeight, node.imageType, node.data, node.smask, node.color);
421
+ // The placement scales the unit square to the box; an orientation turns the pixels inside it.
422
+ const upright = node.orientation
423
+ ? `${ORIENTATION_MATRIX[node.orientation].join(" ")} cm\n`
424
+ : "";
425
+ const draw = `q\n${node.width} 0 0 ${node.height} ${node.x} ${node.y} cm\n` +
426
+ upright +
427
+ `/IM${ref} Do\nQ\n`;
421
428
  if (!node.clip)
422
429
  return draw;
423
430
  // Clip to the frame (re … W n); rounded when a radius is set. The rectangular
@@ -3,14 +3,7 @@ import { LayoutContext } from "../elements/pdf-element.js";
3
3
  import { PDFObjectManager } from "../utils/pdf-object-manager.js";
4
4
  export declare class PDFDocumentRenderer {
5
5
  static render(document: PDFDocumentElement, objectManager: PDFObjectManager, ctx: LayoutContext): Promise<number>;
6
- /**
7
- * The page geometry a physical page will use. `PageElement.calculateLayout` has already folded the
8
- * document defaults into every page's config by the time we get here (`PDFRenderer` lays the document
9
- * out before it calls us, and it is our only caller). Merging again is therefore a no-op today - it is
10
- * here so this stays correct BY CONSTRUCTION rather than by call order: were that order ever changed,
11
- * an unmerged config would otherwise make `resolvePageSize` fall back to A4 and quietly report the
12
- * wrong page size.
13
- */
6
+ /** The page geometry a physical page will use: resolved, never read back from an earlier layout. */
14
7
  private static configOf;
15
8
  /**
16
9
  * Splits one logical page into the physical pages it will occupy, without rendering.