@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
@@ -1,4 +1,5 @@
1
1
  import type { Radii } from "../ir/display-list.js";
2
+ import { type FitAlign, type ImageDimensions } from "../utils/image-helper.js";
2
3
  import { BoxConstraints, Offset, Size } from "../layout/box-constraints.js";
3
4
  import { LayoutContext, SizedPDFElement } from "./pdf-element.js";
4
5
  export declare enum BoxFit {
@@ -8,28 +9,33 @@ export declare enum BoxFit {
8
9
  fill = "FILL"
9
10
  }
10
11
  export declare abstract class CustomImage {
11
- abstract init(): void | Promise<void>;
12
+ /** Loads the image. `fresh`: a new render begins - a load that failed in an earlier one is tried again. */
13
+ abstract init(fresh?: boolean): void | Promise<void>;
12
14
  abstract getImageType(): string | Promise<string>;
13
15
  abstract getFileData(): string | Promise<string>;
14
- abstract getImageDimensions(): Promise<{
15
- width: number;
16
- height: number;
17
- }>;
16
+ abstract getImageDimensions(): Promise<ImageDimensions>;
17
+ /**
18
+ * Its dimensions if its bytes are already in hand (bytes, a local file), else `undefined` - then the
19
+ * async pass before layout fetches them. Reading a header from memory is all this may do: layout
20
+ * asks it, so an image built during layout (in a `PageBuilder`, a table cell) knows its size too.
21
+ */
22
+ dimensionsNow(): ImageDimensions | undefined;
18
23
  }
24
+ /**
25
+ * An image from a file path (Node). The file is read HERE, when the image is named - like an SVG or a
26
+ * font file - so a wrong path fails on the line that named it, and layout never waits on a disk.
27
+ */
19
28
  export declare class CustomLocalImage extends CustomImage {
20
29
  private imagePath;
21
30
  private fileBuffer;
22
31
  private fileRawData;
23
32
  private dims?;
24
33
  constructor(imagePath: string);
25
- init(): Promise<void>;
34
+ init(): void;
26
35
  getImageType(): Promise<string>;
27
- private loadImage;
36
+ dimensionsNow(): ImageDimensions | undefined;
28
37
  getFileData(): string;
29
- getImageDimensions(): Promise<{
30
- width: number;
31
- height: number;
32
- }>;
38
+ getImageDimensions(): Promise<ImageDimensions>;
33
39
  }
34
40
  /**
35
41
  * An image straight from raw bytes (a browser upload / fetch, no filesystem). The PDF filter is sniffed
@@ -43,10 +49,38 @@ export declare class CustomBytesImage extends CustomImage {
43
49
  init(): Promise<void>;
44
50
  getImageType(): Promise<string>;
45
51
  getFileData(): string;
46
- getImageDimensions(): Promise<{
47
- width: number;
48
- height: number;
49
- }>;
52
+ getImageDimensions(): Promise<ImageDimensions>;
53
+ dimensionsNow(): ImageDimensions | undefined;
54
+ }
55
+ /** Thrown when an image could not be fetched or is not one we can embed. Always says which URL. */
56
+ export declare class ImageUrlError extends Error {
57
+ constructor(message: string);
58
+ }
59
+ /** How long an image from a URL may take, and how large it may be - a raster image and an SVG alike. */
60
+ export declare const IMAGE_FETCH_LIMITS: {
61
+ timeoutMs: number;
62
+ maxBytes: number;
63
+ };
64
+ /** Whether an image source string is an address to fetch rather than a file path. */
65
+ export declare const isImageUrl: (src: string) => boolean;
66
+ /**
67
+ * An image from a URL - `http(s):` or a `data:` URL - fetched in the async pass before layout, with
68
+ * the same timeout and size ceiling as a font. What it IS comes from its bytes, never from the
69
+ * address, so `.../photo?id=7` works. A dead link stops the render with an `ImageUrlError` naming it.
70
+ */
71
+ export declare class CustomUrlImage extends CustomImage {
72
+ private readonly url;
73
+ private loading?;
74
+ private loaded?;
75
+ private failed;
76
+ constructor(url: string);
77
+ init(fresh?: boolean): Promise<void>;
78
+ private load;
79
+ private image;
80
+ getImageType(): Promise<string>;
81
+ getFileData(): string;
82
+ getImageDimensions(): Promise<ImageDimensions>;
83
+ dimensionsNow(): ImageDimensions | undefined;
50
84
  }
51
85
  interface ImageElementParams {
52
86
  image: CustomImage;
@@ -67,9 +101,12 @@ interface ImageElementParams {
67
101
  maxHeightFactor?: number;
68
102
  /** width / height; overrides the image's OWN ratio (CSS `aspect-ratio`), `fit` then places it. */
69
103
  aspectRatio?: number;
104
+ /** How the image fills its box (CSS `object-fit`); `fill` by default, as in CSS. */
70
105
  fit?: BoxFit;
71
106
  /** Corner radius in points - one number or per corner; rounds the image box (0 = sharp, default). */
72
107
  radius?: number | Radii;
108
+ /** Where the image sits in its box when `fit` leaves space or cuts it: 0 = start, 1 = end per axis. */
109
+ align?: FitAlign;
73
110
  /** Alternate text for accessibility (tagged PDF). With `alt` the image is a Figure; without, decoration. */
74
111
  alt?: string;
75
112
  }
@@ -79,21 +116,32 @@ export declare class ImageElement extends SizedPDFElement {
79
116
  private heightFactor?;
80
117
  private fit;
81
118
  private radius;
119
+ private align?;
82
120
  private readonly alt?;
83
121
  private requested;
84
122
  /** min/max/aspectRatio, kept as given so every layout pass re-resolves them. */
85
123
  private sizing;
86
124
  private intrinsic?;
87
- constructor({ image, width, height, widthFactor, heightFactor, fit, radius, alt, ...sizing }: ImageElementParams);
125
+ constructor({ image, width, height, widthFactor, heightFactor, fit, radius, align, alt, ...sizing }: ImageElementParams);
88
126
  /**
89
- * Loads the image and records its intrinsic pixel size. Runs in the async pre-layout pass (layout
90
- * itself is synchronous and cannot await jimp) so `calculateLayout` can turn a width-only image
91
- * into a proportional box. Best-effort: a load failure leaves the size unresolved (the renderer
92
- * surfaces the real error) and the image just keeps whatever explicit size it was given.
127
+ * Fetches what layout cannot wait for (an image from a URL) and records its own size. Runs in the
128
+ * async pass before layout; an image whose bytes are in hand has nothing to fetch. Best-effort: a
129
+ * failed fetch leaves the size unresolved (the renderer surfaces the real error) and the image keeps
130
+ * whatever explicit size it was given.
93
131
  */
94
132
  resolveIntrinsicSize(): Promise<void>;
133
+ /** Its own size in points: known now, or resolved by the pass before layout; `undefined` if neither. */
134
+ private ownSize;
95
135
  relativeSizeFactor(horizontal: boolean): number | undefined;
96
- calculateLayout(constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
136
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
137
+ protected computeSize(constraints: BoxConstraints): Size;
138
+ /** Its size was needed from the file and the file had not been read: say so, rather than draw it wrong. */
139
+ private reportUnsized;
140
+ /** Its size under these constraints - the one computation both `measure` and layout use. `guessed`:
141
+ * a side that would have followed the file's own size did not, because that size was unknown. */
142
+ private sizeFor;
143
+ /** The image's own extent on one axis, clamped by its min/max; unknown before the pre-pass. */
144
+ private ownExtent;
97
145
  getProps(): {
98
146
  x: number;
99
147
  y: number;
@@ -102,6 +150,7 @@ export declare class ImageElement extends SizedPDFElement {
102
150
  image: CustomImage;
103
151
  fit: BoxFit;
104
152
  radius: number | Radii;
153
+ align: FitAlign | undefined;
105
154
  alt: string | undefined;
106
155
  };
107
156
  }
@@ -1,8 +1,10 @@
1
- import { getImageDimensions } from "../utils/image-helper.js";
1
+ import { readImageDimensions, naturalSize, } from "../utils/image-helper.js";
2
2
  import { latin1FromBytes } from "../utils/bytes.js";
3
- import { readFileBytesAsync } from "../platform/node-fs.js";
4
- import { extentSpecs, resolveSize, } from "../layout/box-constraints.js";
3
+ import { readFileBytes } from "../platform/node-fs.js";
4
+ import { fetchBytes } from "../utils/fetch-bytes.js";
5
+ import { extentSpecs, resolveSize, sizeAtOwnRatio, } from "../layout/box-constraints.js";
5
6
  import { SizedPDFElement } from "./pdf-element.js";
7
+ import { diagnose, pageOf } from "../common/diagnostics.js";
6
8
  // path.extname without node:path (browser-safe): the substring from the last dot, if it sits after the
7
9
  // last slash (so a dot in a directory name does not count). Enough for image file extensions.
8
10
  function extname(p) {
@@ -18,23 +20,41 @@ export var BoxFit;
18
20
  BoxFit["fill"] = "FILL";
19
21
  })(BoxFit || (BoxFit = {}));
20
22
  export class CustomImage {
23
+ /**
24
+ * Its dimensions if its bytes are already in hand (bytes, a local file), else `undefined` - then the
25
+ * async pass before layout fetches them. Reading a header from memory is all this may do: layout
26
+ * asks it, so an image built during layout (in a `PageBuilder`, a table cell) knows its size too.
27
+ */
28
+ dimensionsNow() {
29
+ return undefined;
30
+ }
21
31
  }
32
+ /** Dimensions read from bytes in hand; a format we cannot read is `undefined` here - the render names it. */
33
+ function headerOf(bytes) {
34
+ try {
35
+ return readImageDimensions(bytes);
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ /**
42
+ * An image from a file path (Node). The file is read HERE, when the image is named - like an SVG or a
43
+ * font file - so a wrong path fails on the line that named it, and layout never waits on a disk.
44
+ */
22
45
  export class CustomLocalImage extends CustomImage {
23
46
  constructor(imagePath) {
24
47
  super();
25
48
  this.imagePath = imagePath;
26
- }
27
- async init() {
28
- if (this.fileBuffer)
29
- return; // idempotent: the pre-layout size pass and the renderer both call init()
30
49
  try {
31
- // Loading image and convert it to base64
32
- await this.loadImage(this.imagePath);
50
+ this.fileBuffer = readFileBytes(imagePath);
33
51
  }
34
- catch (error) {
35
- console.error("Error loading image:", error);
52
+ catch (e) {
53
+ throw new Error(`@jasy/pdf: could not read the image at ${imagePath}: ${String(e?.message ?? e)}`);
36
54
  }
55
+ this.fileRawData = latin1FromBytes(this.fileBuffer);
37
56
  }
57
+ init() { }
38
58
  async getImageType() {
39
59
  const ext = extname(this.imagePath).toLowerCase();
40
60
  switch (ext) {
@@ -51,23 +71,15 @@ export class CustomLocalImage extends CustomImage {
51
71
  throw new Error(`Unsupported image format: ${ext}`);
52
72
  }
53
73
  }
54
- async loadImage(imagePath) {
55
- const result = await readFileBytesAsync(imagePath);
56
- //const result = await convertImageToGrayscaleBuffer(imagePath);
57
- this.fileBuffer = result;
58
- this.fileRawData = latin1FromBytes(result);
59
- return result;
74
+ dimensionsNow() {
75
+ this.dims ?? (this.dims = headerOf(this.fileBuffer));
76
+ return this.dims;
60
77
  }
61
78
  getFileData() {
62
79
  return this.fileRawData;
63
80
  }
64
81
  async getImageDimensions() {
65
- if (this.dims)
66
- return this.dims; // reuse: the pre-layout size pass and the renderer both ask
67
- if (!this.fileBuffer) {
68
- throw new Error("You must first call the `loadAndConvertImage` method");
69
- }
70
- this.dims = await getImageDimensions(this.fileBuffer);
82
+ this.dims ?? (this.dims = readImageDimensions(this.fileBuffer)); // reuse across layout + render
71
83
  return this.dims;
72
84
  }
73
85
  }
@@ -94,13 +106,77 @@ export class CustomBytesImage extends CustomImage {
94
106
  return this.fileRawData;
95
107
  }
96
108
  async getImageDimensions() {
97
- if (!this.dims)
98
- this.dims = await getImageDimensions(this.bytes); // reuse across pre-pass + render
109
+ this.dims ?? (this.dims = readImageDimensions(this.bytes)); // reuse across layout + render
99
110
  return this.dims;
100
111
  }
112
+ dimensionsNow() {
113
+ this.dims ?? (this.dims = headerOf(this.bytes));
114
+ return this.dims;
115
+ }
116
+ }
117
+ /** Thrown when an image could not be fetched or is not one we can embed. Always says which URL. */
118
+ export class ImageUrlError extends Error {
119
+ constructor(message) {
120
+ super(`@jasy/pdf: ${message}`);
121
+ }
122
+ }
123
+ /** How long an image from a URL may take, and how large it may be - a raster image and an SVG alike. */
124
+ export const IMAGE_FETCH_LIMITS = { timeoutMs: 15000, maxBytes: 32 * 1024 * 1024 };
125
+ /** Whether an image source string is an address to fetch rather than a file path. */
126
+ export const isImageUrl = (src) => /^(https?:|data:)/i.test(src.trim());
127
+ /**
128
+ * An image from a URL - `http(s):` or a `data:` URL - fetched in the async pass before layout, with
129
+ * the same timeout and size ceiling as a font. What it IS comes from its bytes, never from the
130
+ * address, so `.../photo?id=7` works. A dead link stops the render with an `ImageUrlError` naming it.
131
+ */
132
+ export class CustomUrlImage extends CustomImage {
133
+ constructor(url) {
134
+ super();
135
+ this.url = url;
136
+ this.failed = false;
137
+ }
138
+ // One fetch per render, however often it is asked: the pre-layout size pass and the renderer both
139
+ // call init(). A failure sticks for that render only - the next one asks again (`fresh`).
140
+ async init(fresh = false) {
141
+ if (fresh && this.failed)
142
+ this.loading = undefined;
143
+ this.loading ?? (this.loading = this.load().then((image) => ((this.failed = false), image), (e) => {
144
+ this.failed = true;
145
+ throw e;
146
+ }));
147
+ this.loaded = await this.loading;
148
+ }
149
+ async load() {
150
+ const bytes = await fetchBytes(this.url, "image", ImageUrlError, IMAGE_FETCH_LIMITS);
151
+ const jpeg = bytes[0] === 0xff && bytes[1] === 0xd8;
152
+ const png = bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47;
153
+ if (!jpeg && !png) {
154
+ // An error page answers with a 200 often enough to name it rather than fail deep inside.
155
+ const looksLikeMarkup = new TextDecoder().decode(bytes.subarray(0, 64)).trimStart()[0] === "<";
156
+ throw new ImageUrlError(`the file at ${this.url} is ${looksLikeMarkup ? "markup (an HTML page, or an SVG - pass that to Svg(url))" : "not an image we can embed"} - jasy embeds PNG and JPEG`);
157
+ }
158
+ return new CustomBytesImage(bytes);
159
+ }
160
+ image() {
161
+ if (!this.loaded)
162
+ throw new Error("@jasy/pdf: an image was measured before it was loaded");
163
+ return this.loaded;
164
+ }
165
+ getImageType() {
166
+ return this.image().getImageType();
167
+ }
168
+ getFileData() {
169
+ return this.image().getFileData();
170
+ }
171
+ getImageDimensions() {
172
+ return this.image().getImageDimensions();
173
+ }
174
+ dimensionsNow() {
175
+ return this.loaded?.dimensionsNow();
176
+ }
101
177
  }
102
178
  export class ImageElement extends SizedPDFElement {
103
- constructor({ image, width, height, widthFactor, heightFactor, fit = BoxFit.none, radius, alt, ...sizing }) {
179
+ constructor({ image, width, height, widthFactor, heightFactor, fit = BoxFit.fill, radius, align, alt, ...sizing }) {
104
180
  // Seed this.width/height from the request so getProps() reflects it before layout; calculateLayout
105
181
  // then reads `requested` (never the mutated fields) and overwrites these with the laid-out size.
106
182
  super({ x: 0, y: 0, width });
@@ -112,29 +188,68 @@ export class ImageElement extends SizedPDFElement {
112
188
  this.sizing = sizing;
113
189
  this.fit = fit;
114
190
  this.radius = radius ?? 0;
191
+ this.align = align;
115
192
  this.alt = alt;
116
193
  }
117
194
  /**
118
- * Loads the image and records its intrinsic pixel size. Runs in the async pre-layout pass (layout
119
- * itself is synchronous and cannot await jimp) so `calculateLayout` can turn a width-only image
120
- * into a proportional box. Best-effort: a load failure leaves the size unresolved (the renderer
121
- * surfaces the real error) and the image just keeps whatever explicit size it was given.
195
+ * Fetches what layout cannot wait for (an image from a URL) and records its own size. Runs in the
196
+ * async pass before layout; an image whose bytes are in hand has nothing to fetch. Best-effort: a
197
+ * failed fetch leaves the size unresolved (the renderer surfaces the real error) and the image keeps
198
+ * whatever explicit size it was given.
122
199
  */
123
200
  async resolveIntrinsicSize() {
201
+ if (this.ownSize())
202
+ return;
124
203
  try {
125
- await this.image.init();
126
- this.intrinsic = await this.image.getImageDimensions();
204
+ await this.image.init(true); // a render begins here
205
+ this.intrinsic = naturalSize(await this.image.getImageDimensions());
127
206
  }
128
207
  catch {
129
208
  // leave intrinsic undefined - aspect derivation simply doesn't fire
130
209
  }
131
210
  }
211
+ /** Its own size in points: known now, or resolved by the pass before layout; `undefined` if neither. */
212
+ ownSize() {
213
+ if (!this.intrinsic) {
214
+ const dims = this.image.dimensionsNow();
215
+ if (dims)
216
+ this.intrinsic = naturalSize(dims);
217
+ }
218
+ return this.intrinsic;
219
+ }
132
220
  relativeSizeFactor(horizontal) {
133
221
  return horizontal ? this.widthFactor : this.heightFactor;
134
222
  }
135
- calculateLayout(constraints, offset, _ctx) {
223
+ calculateLayout(constraints, offset, ctx) {
224
+ const size = this.sizeFor(constraints);
225
+ if (size.guessed)
226
+ this.reportUnsized(ctx);
136
227
  this.x = offset.x;
137
228
  this.y = offset.y;
229
+ // Kept as resolved, `undefined` included: the renderer reads an unset axis differently from 0.
230
+ this.width = size.width;
231
+ this.height = size.height;
232
+ return { width: size.width ?? 0, height: size.height ?? 0 };
233
+ }
234
+ computeSize(constraints) {
235
+ const size = this.sizeFor(constraints);
236
+ return { width: size.width ?? 0, height: size.height ?? 0 };
237
+ }
238
+ /** Its size was needed from the file and the file had not been read: say so, rather than draw it wrong. */
239
+ reportUnsized(ctx) {
240
+ diagnose(ctx.diagnostics, {
241
+ code: "image-unsized",
242
+ severity: "warning",
243
+ message: "An image's own size was not known when it was laid out - an image from a URL that is first " +
244
+ "built in a PageBuilder cannot be fetched before layout. Its open side fills what it is " +
245
+ "offered. Give it a width and a height, or create it outside the PageBuilder.",
246
+ about: this,
247
+ page: pageOf(ctx),
248
+ });
249
+ }
250
+ /** Its size under these constraints - the one computation both `measure` and layout use. `guessed`:
251
+ * a side that would have followed the file's own size did not, because that size was unknown. */
252
+ sizeFor(constraints) {
138
253
  // Relative sizing: a fixed size or a fraction of the offered box (fraction only in a bounded axis).
139
254
  // Read from `requested` (not this.width/height, which we overwrite below) so every pass re-resolves.
140
255
  const specs = extentSpecs({
@@ -144,31 +259,36 @@ export class ImageElement extends SizedPDFElement {
144
259
  widthFactor: this.widthFactor,
145
260
  heightFactor: this.heightFactor,
146
261
  });
147
- // An explicit `aspectRatio` wins over the image's own, as it does in CSS - and only it may size a
148
- // box with NEITHER axis pinned. Left to the intrinsic ratio that would change what an unsized image
149
- // has always done here (fill both axes and let `fit` sort it out).
262
+ // An explicit `aspectRatio` wins over the image's own.
150
263
  const resolved = resolveSize(specs.width, specs.height, this.sizing.aspectRatio, constraints);
151
264
  let w = resolved.width;
152
265
  let h = resolved.height;
153
266
  const bounds = resolved.constraints;
154
- // Aspect auto-size: when exactly ONE axis is pinned, derive the other from the intrinsic ratio
155
- // (CSS `width: 50%; height: auto`). Only fires once the pre-pass resolved the pixel size.
156
- if (this.sizing.aspectRatio === undefined &&
157
- this.intrinsic &&
158
- this.intrinsic.width > 0 &&
159
- this.intrinsic.height > 0) {
160
- const ratio = this.intrinsic.width / this.intrinsic.height;
161
- if (w !== undefined && h === undefined)
162
- h = bounds.constrainHeight(w / ratio);
163
- else if (h !== undefined && w === undefined)
164
- w = bounds.constrainWidth(h * ratio);
267
+ // Its own ratio fills an open axis, once the pre-pass resolved its own size.
268
+ const own = this.ownSize();
269
+ if (this.sizing.aspectRatio === undefined && own && own.width > 0 && own.height > 0) {
270
+ ({ width: w, height: h } = sizeAtOwnRatio(w, h, own, bounds));
165
271
  }
166
- // Fall back to the prior behavior for an unpinned axis: a bounded axis fills, otherwise keep the
167
- // requested fixed size (or undefined). Read from `requested`, never the mutated this.width/height.
168
- this.width = w ?? (bounds.hasBoundedWidth ? bounds.maxWidth : this.requested.width);
169
- this.height = h ?? (bounds.hasBoundedHeight ? bounds.maxHeight : this.requested.height);
272
+ const guessed = own === undefined && (w === undefined || h === undefined);
273
+ // Still unpinned (no intrinsic size known): a bounded axis fills; an unbounded one takes the image's
274
+ // own extent (`naturalSize`). Read from `requested`, never the mutated this.width/height.
275
+ const width = w ??
276
+ (bounds.hasBoundedWidth
277
+ ? bounds.maxWidth
278
+ : (this.requested.width ?? this.ownExtent(bounds, "width")));
279
+ const height = h ??
280
+ (bounds.hasBoundedHeight
281
+ ? bounds.maxHeight
282
+ : (this.requested.height ?? this.ownExtent(bounds, "height")));
170
283
  // Top-left coordinates; the fit logic (renderer) and the Y-flip (seam) run later.
171
- return { width: this.width ?? 0, height: this.height ?? 0 };
284
+ return { width, height, guessed };
285
+ }
286
+ /** The image's own extent on one axis, clamped by its min/max; unknown before the pre-pass. */
287
+ ownExtent(bounds, axis) {
288
+ const own = this.ownSize()?.[axis];
289
+ if (own === undefined)
290
+ return undefined;
291
+ return axis === "width" ? bounds.constrainWidth(own) : bounds.constrainHeight(own);
172
292
  }
173
293
  getProps() {
174
294
  return {
@@ -179,6 +299,7 @@ export class ImageElement extends SizedPDFElement {
179
299
  image: this.image,
180
300
  fit: this.fit,
181
301
  radius: this.radius,
302
+ align: this.align,
182
303
  alt: this.alt,
183
304
  };
184
305
  }
@@ -20,6 +20,9 @@ export * from "./layout/link-element.js";
20
20
  export * from "./layout/bookmark-element.js";
21
21
  export * from "./layout/anchor-element.js";
22
22
  export * from "./layout/page-builder-element.js";
23
+ export * from "./layout/flow-element.js";
24
+ export * from "./layout/region-element.js";
25
+ export * from "./layout/column-break-element.js";
23
26
  export * from "./layout/rotated-element.js";
24
27
  export * from "./layout/rotated-box-element.js";
25
28
  export * from "./line-element.js";
@@ -20,6 +20,9 @@ export * from "./layout/link-element.js";
20
20
  export * from "./layout/bookmark-element.js";
21
21
  export * from "./layout/anchor-element.js";
22
22
  export * from "./layout/page-builder-element.js";
23
+ export * from "./layout/flow-element.js";
24
+ export * from "./layout/region-element.js";
25
+ export * from "./layout/column-break-element.js";
23
26
  export * from "./layout/rotated-element.js";
24
27
  export * from "./layout/rotated-box-element.js";
25
28
  export * from "./line-element.js";
@@ -1,20 +1,18 @@
1
- import { PDFElement, LayoutContext } from "../pdf-element.js";
2
- import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
1
+ import { PDFElement } from "../pdf-element.js";
2
+ import { ProxyElement } from "./proxy-element.js";
3
3
  /**
4
- * Marks its child as a named jump target for an internal `Link({ to: name })`. Layout-transparent: it
5
- * delegates `calculateLayout` to the child and records the child's top, which the renderer turns into an
6
- * `Anchor` IR node (and PDFRenderer into a /Names /Dests entry). It draws nothing - the child renders
7
- * exactly as it would on its own.
4
+ * Marks its child as a named jump target for an internal `Link({ to: name })`. Layout-transparent (a
5
+ * `ProxyElement`); the renderer turns the child's top into an `Anchor` IR node (and PDFRenderer into a
6
+ * /Names /Dests entry). It draws nothing. Split across pages, only the first piece is the target - a
7
+ * name must lead to one place.
8
8
  */
9
- export declare class AnchorElement extends PDFElement {
9
+ export declare class AnchorElement extends ProxyElement {
10
10
  private name;
11
- private child;
12
- private y;
13
11
  constructor({ name, child }: {
14
12
  name: string;
15
13
  child: PDFElement;
16
14
  });
17
- calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
15
+ protected rewrap(child: PDFElement, continued: boolean): PDFElement;
18
16
  getProps(): {
19
17
  name: string;
20
18
  child: PDFElement;
@@ -1,27 +1,19 @@
1
- import { PDFElement } from "../pdf-element.js";
1
+ import { ProxyElement } from "./proxy-element.js";
2
2
  /**
3
- * Marks its child as a named jump target for an internal `Link({ to: name })`. Layout-transparent: it
4
- * delegates `calculateLayout` to the child and records the child's top, which the renderer turns into an
5
- * `Anchor` IR node (and PDFRenderer into a /Names /Dests entry). It draws nothing - the child renders
6
- * exactly as it would on its own.
3
+ * Marks its child as a named jump target for an internal `Link({ to: name })`. Layout-transparent (a
4
+ * `ProxyElement`); the renderer turns the child's top into an `Anchor` IR node (and PDFRenderer into a
5
+ * /Names /Dests entry). It draws nothing. Split across pages, only the first piece is the target - a
6
+ * name must lead to one place.
7
7
  */
8
- export class AnchorElement extends PDFElement {
8
+ export class AnchorElement extends ProxyElement {
9
9
  constructor({ name, child }) {
10
- super();
11
- this.y = 0;
10
+ super(child);
12
11
  this.name = name;
13
- this.child = child;
14
12
  }
15
- calculateLayout(constraints, offset, ctx) {
16
- const size = this.child.calculateLayout(constraints, offset, ctx);
17
- this.y = offset.y; // the child's top - the scroll target for a link that jumps here
18
- return size;
13
+ rewrap(child, continued) {
14
+ return continued ? child : new AnchorElement({ name: this.name, child });
19
15
  }
20
16
  getProps() {
21
- return {
22
- name: this.name,
23
- child: this.child,
24
- y: this.y,
25
- };
17
+ return { name: this.name, child: this.child, y: this.y };
26
18
  }
27
19
  }
@@ -1,23 +1,21 @@
1
- import { PDFElement, LayoutContext } from "../pdf-element.js";
2
- import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
1
+ import { PDFElement } from "../pdf-element.js";
2
+ import { ProxyElement } from "./proxy-element.js";
3
3
  /**
4
- * Adds a document-outline entry (a bookmark) that jumps to its child. Layout-transparent: it delegates
5
- * `calculateLayout` to the child and records the child's top, which the renderer turns into an `Outline`
6
- * IR node (and PDFRenderer into a /Outlines tree entry). `title` is the label shown in the viewer's
7
- * bookmark panel; `level` (1-based, default 1) nests it under the nearest preceding smaller level. It
8
- * draws nothing - the child renders exactly as it would without the bookmark.
4
+ * Adds a document-outline entry (a bookmark) that jumps to its child. Layout-transparent (a
5
+ * `ProxyElement`); the renderer turns the child's top into an `Outline` IR node (and PDFRenderer into a
6
+ * /Outlines tree entry). `title` is the label shown in the viewer's bookmark panel; `level` (1-based,
7
+ * default 1) nests it under the nearest preceding smaller level. It draws nothing. Split across pages,
8
+ * only the first piece carries the entry - a chapter is listed once.
9
9
  */
10
- export declare class BookmarkElement extends PDFElement {
10
+ export declare class BookmarkElement extends ProxyElement {
11
11
  private title;
12
12
  private level;
13
- private child;
14
- private y;
15
13
  constructor({ title, level, child }: {
16
14
  title: string;
17
15
  level?: number;
18
16
  child: PDFElement;
19
17
  });
20
- calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
18
+ protected rewrap(child: PDFElement, continued: boolean): PDFElement;
21
19
  getProps(): {
22
20
  title: string;
23
21
  level: number;
@@ -1,30 +1,21 @@
1
- import { PDFElement } from "../pdf-element.js";
1
+ import { ProxyElement } from "./proxy-element.js";
2
2
  /**
3
- * Adds a document-outline entry (a bookmark) that jumps to its child. Layout-transparent: it delegates
4
- * `calculateLayout` to the child and records the child's top, which the renderer turns into an `Outline`
5
- * IR node (and PDFRenderer into a /Outlines tree entry). `title` is the label shown in the viewer's
6
- * bookmark panel; `level` (1-based, default 1) nests it under the nearest preceding smaller level. It
7
- * draws nothing - the child renders exactly as it would without the bookmark.
3
+ * Adds a document-outline entry (a bookmark) that jumps to its child. Layout-transparent (a
4
+ * `ProxyElement`); the renderer turns the child's top into an `Outline` IR node (and PDFRenderer into a
5
+ * /Outlines tree entry). `title` is the label shown in the viewer's bookmark panel; `level` (1-based,
6
+ * default 1) nests it under the nearest preceding smaller level. It draws nothing. Split across pages,
7
+ * only the first piece carries the entry - a chapter is listed once.
8
8
  */
9
- export class BookmarkElement extends PDFElement {
9
+ export class BookmarkElement extends ProxyElement {
10
10
  constructor({ title, level = 1, child }) {
11
- super();
12
- this.y = 0;
11
+ super(child);
13
12
  this.title = title;
14
13
  this.level = level;
15
- this.child = child;
16
14
  }
17
- calculateLayout(constraints, offset, ctx) {
18
- const size = this.child.calculateLayout(constraints, offset, ctx);
19
- this.y = offset.y; // the child's top - the scroll target for the bookmark
20
- return size;
15
+ rewrap(child, continued) {
16
+ return continued ? child : new BookmarkElement({ title: this.title, level: this.level, child });
21
17
  }
22
18
  getProps() {
23
- return {
24
- title: this.title,
25
- level: this.level,
26
- child: this.child,
27
- y: this.y,
28
- };
19
+ return { title: this.title, level: this.level, child: this.child, y: this.y };
29
20
  }
30
21
  }