@jasy/pdf 1.0.0-alpha.8 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +29 -10
  2. package/dist/api/color.js +69 -0
  3. package/dist/api/content.d.ts +45 -5
  4. package/dist/api/content.js +96 -7
  5. package/dist/api/descriptor.js +6 -1
  6. package/dist/api/dimension.d.ts +57 -0
  7. package/dist/api/dimension.js +27 -0
  8. package/dist/api/font-url.d.ts +28 -0
  9. package/dist/api/font-url.js +135 -0
  10. package/dist/api/gradient.d.ts +53 -0
  11. package/dist/api/gradient.js +97 -0
  12. package/dist/api/index.d.ts +2 -0
  13. package/dist/api/index.js +2 -0
  14. package/dist/api/insets.d.ts +19 -12
  15. package/dist/api/insets.js +18 -7
  16. package/dist/api/layout.d.ts +20 -6
  17. package/dist/api/layout.js +31 -6
  18. package/dist/api/page-builder.d.ts +8 -0
  19. package/dist/api/page-builder.js +12 -0
  20. package/dist/api/structure.d.ts +12 -0
  21. package/dist/api/structure.js +54 -7
  22. package/dist/api/text.d.ts +65 -8
  23. package/dist/api/text.js +38 -5
  24. package/dist/canvas/painter.d.ts +102 -0
  25. package/dist/canvas/painter.js +183 -0
  26. package/dist/edit/document.d.ts +14 -4
  27. package/dist/edit/document.js +15 -22
  28. package/dist/edit/fill.d.ts +19 -2
  29. package/dist/edit/fill.js +63 -34
  30. package/dist/edit/flatten.d.ts +13 -1
  31. package/dist/edit/flatten.js +38 -13
  32. package/dist/edit/index.d.ts +2 -0
  33. package/dist/edit/index.js +3 -1
  34. package/dist/edit/objects.d.ts +5 -0
  35. package/dist/edit/objects.js +27 -0
  36. package/dist/edit/writer.d.ts +7 -0
  37. package/dist/edit/writer.js +23 -1
  38. package/dist/elements/canvas-element.d.ts +38 -0
  39. package/dist/elements/canvas-element.js +47 -0
  40. package/dist/elements/container-element.d.ts +23 -1
  41. package/dist/elements/container-element.js +31 -10
  42. package/dist/elements/image-element.d.ts +18 -4
  43. package/dist/elements/image-element.js +27 -12
  44. package/dist/elements/index.d.ts +2 -0
  45. package/dist/elements/index.js +2 -0
  46. package/dist/elements/layout/expanded-element.d.ts +5 -1
  47. package/dist/elements/layout/expanded-element.js +16 -3
  48. package/dist/elements/layout/keep-together-element.d.ts +2 -0
  49. package/dist/elements/layout/keep-together-element.js +4 -0
  50. package/dist/elements/layout/padding-element.d.ts +9 -2
  51. package/dist/elements/layout/padding-element.js +12 -2
  52. package/dist/elements/layout/page-builder-element.js +2 -0
  53. package/dist/elements/layout/struct-group.d.ts +2 -0
  54. package/dist/elements/layout/struct-group.js +4 -0
  55. package/dist/elements/pdf-element.d.ts +75 -1
  56. package/dist/elements/pdf-element.js +80 -1
  57. package/dist/elements/rectangle-element.d.ts +22 -6
  58. package/dist/elements/rectangle-element.js +31 -12
  59. package/dist/elements/row-element.d.ts +26 -1
  60. package/dist/elements/row-element.js +31 -10
  61. package/dist/elements/svg-element.d.ts +41 -0
  62. package/dist/elements/svg-element.js +93 -0
  63. package/dist/elements/text-element.d.ts +92 -1
  64. package/dist/elements/text-element.js +208 -33
  65. package/dist/ir/display-list.d.ts +67 -11
  66. package/dist/ir/display-list.js +8 -1
  67. package/dist/layout/alignment.d.ts +10 -0
  68. package/dist/layout/alignment.js +6 -0
  69. package/dist/layout/box-constraints.d.ts +53 -0
  70. package/dist/layout/box-constraints.js +72 -0
  71. package/dist/layout/insets.d.ts +28 -0
  72. package/dist/layout/insets.js +20 -0
  73. package/dist/layout/min-intrinsic.d.ts +29 -0
  74. package/dist/layout/min-intrinsic.js +37 -0
  75. package/dist/renderer/anchor-renderer.js +1 -1
  76. package/dist/renderer/bookmark-renderer.js +1 -1
  77. package/dist/renderer/canvas-renderer.d.ts +14 -0
  78. package/dist/renderer/canvas-renderer.js +36 -0
  79. package/dist/renderer/container-renderer.js +1 -3
  80. package/dist/renderer/default-text-style-renderer.js +1 -1
  81. package/dist/renderer/deferred-renderer.js +1 -1
  82. package/dist/renderer/expanded-renderer.js +1 -1
  83. package/dist/renderer/image-renderer.js +2 -1
  84. package/dist/renderer/index.d.ts +2 -0
  85. package/dist/renderer/index.js +2 -0
  86. package/dist/renderer/keep-together-renderer.js +1 -1
  87. package/dist/renderer/link-renderer.js +1 -1
  88. package/dist/renderer/padding-renderer.js +1 -1
  89. package/dist/renderer/page-builder-renderer.js +1 -1
  90. package/dist/renderer/page-renderer.js +1 -3
  91. package/dist/renderer/pdf-backend.d.ts +21 -4
  92. package/dist/renderer/pdf-backend.js +198 -61
  93. package/dist/renderer/pdf-document-renderer.js +8 -1
  94. package/dist/renderer/pdf-renderer.js +5 -1
  95. package/dist/renderer/positioned-renderer.js +1 -1
  96. package/dist/renderer/rectangle-renderer.js +9 -5
  97. package/dist/renderer/repeating-header-renderer.js +1 -2
  98. package/dist/renderer/rotated-renderer.js +1 -1
  99. package/dist/renderer/row-renderer.js +1 -3
  100. package/dist/renderer/struct-group-renderer.js +1 -1
  101. package/dist/renderer/svg-renderer.d.ts +11 -0
  102. package/dist/renderer/svg-renderer.js +27 -0
  103. package/dist/renderer/text-renderer.d.ts +2 -2
  104. package/dist/renderer/text-renderer.js +176 -57
  105. package/dist/svg/css.d.ts +38 -0
  106. package/dist/svg/css.js +115 -0
  107. package/dist/svg/errors.d.ts +24 -0
  108. package/dist/svg/errors.js +30 -0
  109. package/dist/svg/gradients.d.ts +34 -0
  110. package/dist/svg/gradients.js +156 -0
  111. package/dist/svg/index.d.ts +22 -0
  112. package/dist/svg/index.js +331 -0
  113. package/dist/svg/shapes.d.ts +6 -0
  114. package/dist/svg/shapes.js +177 -0
  115. package/dist/svg/style.d.ts +61 -0
  116. package/dist/svg/style.js +144 -0
  117. package/dist/svg/transform.d.ts +13 -0
  118. package/dist/svg/transform.js +68 -0
  119. package/dist/svg/units.d.ts +11 -0
  120. package/dist/svg/units.js +73 -0
  121. package/dist/svg/xml.d.ts +24 -0
  122. package/dist/svg/xml.js +158 -0
  123. package/dist/text/advance.d.ts +3 -0
  124. package/dist/text/advance.js +8 -4
  125. package/dist/text/arabic.d.ts +20 -0
  126. package/dist/text/arabic.js +101 -0
  127. package/dist/text/bidi.d.ts +33 -0
  128. package/dist/text/bidi.js +94 -0
  129. package/dist/text/font-fallback.d.ts +25 -0
  130. package/dist/text/font-fallback.js +38 -0
  131. package/dist/text/glyph-coverage.d.ts +15 -0
  132. package/dist/text/glyph-coverage.js +77 -0
  133. package/dist/text/line-breaker.d.ts +43 -1
  134. package/dist/text/line-breaker.js +181 -45
  135. package/dist/text/orphans-widows.d.ts +27 -0
  136. package/dist/text/orphans-widows.js +53 -0
  137. package/dist/text/shape.d.ts +29 -0
  138. package/dist/text/shape.js +91 -0
  139. package/dist/text/text-style.d.ts +38 -0
  140. package/dist/text/text-style.js +50 -1
  141. package/dist/text/whitespace.d.ts +4 -0
  142. package/dist/text/whitespace.js +43 -0
  143. package/dist/text/word-splitting.d.ts +42 -0
  144. package/dist/text/word-splitting.js +72 -0
  145. package/dist/utils/flex-layout.d.ts +24 -4
  146. package/dist/utils/flex-layout.js +218 -7
  147. package/dist/utils/font-metrics.d.ts +13 -2
  148. package/dist/utils/gsub.d.ts +36 -0
  149. package/dist/utils/gsub.js +178 -0
  150. package/dist/utils/inflate.d.ts +16 -0
  151. package/dist/utils/inflate.js +52 -0
  152. package/dist/utils/pdf-object-manager.d.ts +44 -3
  153. package/dist/utils/pdf-object-manager.js +176 -14
  154. package/dist/utils/renderer-registry.d.ts +13 -1
  155. package/dist/utils/renderer-registry.js +22 -1
  156. package/dist/utils/ttf-parser.d.ts +6 -0
  157. package/dist/utils/ttf-parser.js +19 -0
  158. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  159. package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
  160. package/dist/utils/woff.d.ts +10 -0
  161. package/dist/utils/woff.js +116 -0
  162. package/dist/utils/woff2.d.ts +6 -0
  163. package/dist/utils/woff2.js +651 -0
  164. package/dist/validators/element-validator.js +9 -2
  165. package/dist/vector/path.d.ts +33 -0
  166. package/dist/vector/path.js +81 -0
  167. package/package.json +6 -2
@@ -32,6 +32,9 @@ export interface TextRun {
32
32
  x: number;
33
33
  y: number;
34
34
  text: string;
35
+ /** The glyphs to draw, when the run was SHAPED. The backend emits these instead of encoding `text`,
36
+ * which stays logical for `ToUnicode` and the accessible tree. */
37
+ glyphs?: number[];
35
38
  fontFamily: string;
36
39
  fontStyle: FontStyle;
37
40
  fontSize: number;
@@ -40,17 +43,31 @@ export interface TextRun {
40
43
  tag?: StructTag;
41
44
  }
42
45
  /** An axis-aligned rectangle. An absent `fill` or `stroke` means that part is not drawn. */
46
+ /**
47
+ * Corner radii in points (CSS `border-radius`). Any corner left out falls back to 0. A single number
48
+ * everywhere is the common case and stays a plain `number` on the nodes, so nothing changed for a
49
+ * document that never asks for different corners.
50
+ */
51
+ export interface Radii {
52
+ tl?: number;
53
+ tr?: number;
54
+ br?: number;
55
+ bl?: number;
56
+ }
57
+ /** True when any corner asks for a radius - the gate between a plain rect and a curved path. */
58
+ export declare function isRounded(radius: number | Radii | undefined): boolean;
43
59
  export interface Rect {
44
60
  type: "rect";
45
61
  x: number;
46
62
  y: number;
47
63
  width: number;
48
64
  height: number;
49
- fill?: Color;
65
+ /** Solid colour, or a gradient whose anchor points are in absolute page coordinates. */
66
+ fill?: Color | Gradient;
50
67
  stroke?: Color;
51
68
  strokeWidth: number;
52
- /** Corner radius in points; absent/0 = sharp corners (plain `re`). */
53
- radius?: number;
69
+ /** Corner radius in points - one number for all four, or per corner. Absent/0 = sharp (plain `re`). */
70
+ radius?: number | Radii;
54
71
  tag?: StructTag;
55
72
  }
56
73
  /** A straight line segment between two points. */
@@ -84,8 +101,8 @@ export interface Image {
84
101
  width: number;
85
102
  height: number;
86
103
  };
87
- /** Corner radius in points for the image box; absent/0 = sharp corners. */
88
- radius?: number;
104
+ /** Corner radius in points for the image box - one number or per corner; absent/0 = sharp. */
105
+ radius?: number | Radii;
89
106
  tag?: StructTag;
90
107
  }
91
108
  /**
@@ -99,7 +116,7 @@ export interface ClipPush {
99
116
  y: number;
100
117
  width: number;
101
118
  height: number;
102
- radius?: number;
119
+ radius?: number | Radii;
103
120
  }
104
121
  /** Closes the most recent `ClipPush` (restores the graphics state). */
105
122
  export interface ClipPop {
@@ -216,6 +233,9 @@ export interface LinearGradient {
216
233
  y1: number;
217
234
  stops: GradientStop[];
218
235
  extend: "pad" | "repeat" | "reflect";
236
+ /** One opacity for the whole shading (0-1). A PDF shading is DeviceRGB and has no alpha channel,
237
+ * so a PER-STOP alpha is not representable - but a uniform one is, as an ExtGState. */
238
+ alpha?: number;
219
239
  }
220
240
  /** A radial gradient between two circles, in absolute page coordinates. */
221
241
  export interface RadialGradient {
@@ -228,19 +248,55 @@ export interface RadialGradient {
228
248
  r1: number;
229
249
  stops: GradientStop[];
230
250
  extend: "pad" | "repeat" | "reflect";
251
+ /** One opacity for the whole shading (0-1). A PDF shading is DeviceRGB and has no alpha channel,
252
+ * so a PER-STOP alpha is not representable - but a uniform one is, as an ExtGState. */
253
+ alpha?: number;
231
254
  }
232
255
  export type Gradient = LinearGradient | RadialGradient;
233
256
  /**
234
- * A filled vector path - one or more closed subpaths sharing a single fill, using the nonzero
235
- * winding rule (so a glyph's inner contour cuts a hole). This is what a color glyph's layers draw
236
- * onto: each COLR layer is one `Path` filled with a solid `Color` (v0) or a `Gradient` (v1).
257
+ * How a path's outline is stroked. The names are SVG's, the semantics are the PDF operators they map
258
+ * to (`w`, `J`, `j`, `M`, `d`), and the defaults are SVG's initial values - not PDF's, which differ on
259
+ * the miter limit. Opacity rides on the `Color`, as everywhere else in the IR.
260
+ */
261
+ export interface StrokeStyle {
262
+ color: Color;
263
+ width: number;
264
+ /** `J`. Default `butt`. */
265
+ cap?: "butt" | "round" | "square";
266
+ /** `j`. Default `miter`. */
267
+ join?: "miter" | "round" | "bevel";
268
+ /** `M`. Default 4 (SVG); PDF's own default is 10, so we always emit it when a join can miter. */
269
+ miterLimit?: number;
270
+ /** `d`: dash lengths in points, plus the phase to start at. Absent or empty = solid. */
271
+ dash?: number[];
272
+ dashOffset?: number;
273
+ }
274
+ /**
275
+ * A vector path - one or more subpaths that share one fill and/or one stroke. `fill` uses the nonzero
276
+ * winding rule unless `fillRule` says otherwise, so a glyph's inner contour cuts a hole by default.
277
+ * A color glyph's layer is one `Path` filled with a solid `Color` (COLR v0) or a `Gradient` (v1); an
278
+ * SVG shape is one `Path` that may also carry a stroke, or carry ONLY a stroke.
237
279
  */
238
280
  export interface Path {
239
281
  type: "path";
240
282
  commands: PathCommand[];
241
- fill: Color | Gradient;
283
+ /** Absent = not filled (a stroke-only outline). */
284
+ fill?: Color | Gradient;
285
+ /** `evenodd` maps to the `*` operators; absent = nonzero, which is what glyphs need. */
286
+ fillRule?: "nonzero" | "evenodd";
287
+ stroke?: StrokeStyle;
242
288
  /** Accessibility structure tag (absent = drawn as an artifact); color-emoji layers stay untagged. */
243
289
  tag?: StructTag;
244
290
  }
291
+ /**
292
+ * Clips to an arbitrary path until the matching `clip-pop` - what SVG's `<clipPath>` needs, and what
293
+ * `ClipPush` cannot express, being a rectangle. The commands are in the same space as a `Path`'s.
294
+ */
295
+ export interface ClipPathPush {
296
+ type: "clip-path-push";
297
+ commands: PathCommand[];
298
+ /** `evenodd` maps to `W*`; absent = nonzero. SVG spells this `clip-rule`. */
299
+ fillRule?: "nonzero" | "evenodd";
300
+ }
245
301
  /** The closed set of primitives the PDF backend knows how to draw. */
246
- export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop | Path | Link | Outline | Anchor | FormFieldNode | TransformPush | TransformPop;
302
+ export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPathPush | ClipPop | Path | Link | Outline | Anchor | FormFieldNode | TransformPush | TransformPop;
@@ -1 +1,8 @@
1
- export {};
1
+ /** True when any corner asks for a radius - the gate between a plain rect and a curved path. */
2
+ export function isRounded(radius) {
3
+ if (radius === undefined)
4
+ return false;
5
+ if (typeof radius === "number")
6
+ return radius > 0;
7
+ return ((radius.tl ?? 0) > 0 || (radius.tr ?? 0) > 0 || (radius.br ?? 0) > 0 || (radius.bl ?? 0) > 0);
8
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Flex alignment vocabulary. It lives here rather than in `utils/flex-layout.ts` because the base
3
+ * `PDFElement` needs `CrossAlign` for `alignSelf`, and flex-layout imports the elements - defining it
4
+ * there would close a cycle.
5
+ */
6
+ /** Distribution along the MAIN axis (CSS `justify-content`). */
7
+ export type MainAlign = "start" | "center" | "end" | "between" | "around";
8
+ /** Position/size ACROSS the axis (CSS `align-items` / `align-self`). `stretch` fills the cross extent;
9
+ * the others place the child at its natural cross size. */
10
+ export type CrossAlign = "start" | "center" | "end" | "stretch";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Flex alignment vocabulary. It lives here rather than in `utils/flex-layout.ts` because the base
3
+ * `PDFElement` needs `CrossAlign` for `alignSelf`, and flex-layout imports the elements - defining it
4
+ * there would close a cycle.
5
+ */
6
+ export {};
@@ -41,6 +41,12 @@ export declare class BoxConstraints {
41
41
  * what a padding/border element hands its child after reserving its own insets.
42
42
  */
43
43
  deflate(horizontal: number, vertical: number): BoxConstraints;
44
+ /**
45
+ * Tightens this box by an element's own `min`/`max` on either axis. A max never widens the offered
46
+ * room, and a min may push past it - `minHeight: 500` in a 300pt region means 500, and pagination
47
+ * deals with the overflow, the same answer CSS gives.
48
+ */
49
+ narrow(minWidth?: number, maxWidth?: number, minHeight?: number, maxHeight?: number): BoxConstraints;
44
50
  /** Returns the constraints clamped to lie within `parent` (Flutter's enforce). */
45
51
  enforce(parent: BoxConstraints): BoxConstraints;
46
52
  }
@@ -52,6 +58,53 @@ export declare class BoxConstraints {
52
58
  * Shared by every sized element so `width: "50%"` behaves identically across Box / Column / Row / Image.
53
59
  */
54
60
  export declare function resolveExtent(fixed: number | undefined, factor: number | undefined, boundedMax: number, hasBounded: boolean): number | undefined;
61
+ /** What one axis of a sized element was asked for: a size, plus the bounds it must stay inside. */
62
+ export interface ExtentSpec {
63
+ fixed?: number;
64
+ factor?: number;
65
+ min?: number;
66
+ minFactor?: number;
67
+ max?: number;
68
+ maxFactor?: number;
69
+ }
70
+ /** The sizing options every sized element accepts. Each `*Factor` is the percentage form of its pair. */
71
+ export interface SizingParams {
72
+ width?: number;
73
+ height?: number;
74
+ widthFactor?: number;
75
+ heightFactor?: number;
76
+ minWidth?: number;
77
+ maxWidth?: number;
78
+ minHeight?: number;
79
+ maxHeight?: number;
80
+ minWidthFactor?: number;
81
+ maxWidthFactor?: number;
82
+ minHeightFactor?: number;
83
+ maxHeightFactor?: number;
84
+ /** width / height. Derives whichever axis is left open; see `resolveSize`. */
85
+ aspectRatio?: number;
86
+ }
87
+ /** Split `SizingParams` into the per-axis form `resolveSize` takes. */
88
+ export declare function extentSpecs(p: SizingParams): {
89
+ width: ExtentSpec;
90
+ height: ExtentSpec;
91
+ };
92
+ /**
93
+ * Resolve both axes of a sized element: relative sizing, then `aspectRatio`, then `min`/`max`.
94
+ *
95
+ * The order follows CSS: a ratio fills in the axis you left open, and min/max clamp afterwards - so an
96
+ * explicit bound wins over the ratio, exactly as `min-height` beats `aspect-ratio` in a browser.
97
+ *
98
+ * `min`/`max` also come back as NARROWED constraints, because an axis with no explicit size still has
99
+ * to obey them. `maxWidth` alone means "fill, but no wider than this", and the caller's own fill and
100
+ * shrink-wrap paths get that for free by using the returned constraints instead of the ones passed in.
101
+ * A `width` of `undefined` keeps its usual meaning: fill or shrink-wrap.
102
+ */
103
+ export declare function resolveSize(width: ExtentSpec, height: ExtentSpec, aspectRatio: number | undefined, constraints: BoxConstraints): {
104
+ width?: number;
105
+ height?: number;
106
+ constraints: BoxConstraints;
107
+ };
55
108
  /** The size an element resolves to and returns UP the tree. */
56
109
  export interface Size {
57
110
  width: number;
@@ -62,6 +62,16 @@ export class BoxConstraints {
62
62
  deflate(horizontal, vertical) {
63
63
  return new BoxConstraints(Math.max(0, this.minWidth - horizontal), Math.max(0, this.maxWidth - horizontal), Math.max(0, this.minHeight - vertical), Math.max(0, this.maxHeight - vertical));
64
64
  }
65
+ /**
66
+ * Tightens this box by an element's own `min`/`max` on either axis. A max never widens the offered
67
+ * room, and a min may push past it - `minHeight: 500` in a 300pt region means 500, and pagination
68
+ * deals with the overflow, the same answer CSS gives.
69
+ */
70
+ narrow(minWidth, maxWidth, minHeight, maxHeight) {
71
+ const lo = (own, current) => Math.max(current, own ?? 0);
72
+ const hi = (own, current) => own === undefined ? current : Math.min(current, own);
73
+ return new BoxConstraints(lo(minWidth, this.minWidth), Math.max(hi(maxWidth, this.maxWidth), lo(minWidth, this.minWidth)), lo(minHeight, this.minHeight), Math.max(hi(maxHeight, this.maxHeight), lo(minHeight, this.minHeight)));
74
+ }
65
75
  /** Returns the constraints clamped to lie within `parent` (Flutter's enforce). */
66
76
  enforce(parent) {
67
77
  return new BoxConstraints(Math.max(parent.minWidth, Math.min(this.minWidth, parent.maxWidth)), Math.max(parent.minWidth, Math.min(this.maxWidth, parent.maxWidth)), Math.max(parent.minHeight, Math.min(this.minHeight, parent.maxHeight)), Math.max(parent.minHeight, Math.min(this.maxHeight, parent.maxHeight)));
@@ -81,3 +91,65 @@ export function resolveExtent(fixed, factor, boundedMax, hasBounded) {
81
91
  return boundedMax * factor;
82
92
  return undefined;
83
93
  }
94
+ /** Split `SizingParams` into the per-axis form `resolveSize` takes. */
95
+ export function extentSpecs(p) {
96
+ return {
97
+ width: {
98
+ fixed: p.width,
99
+ factor: p.widthFactor,
100
+ min: p.minWidth,
101
+ minFactor: p.minWidthFactor,
102
+ max: p.maxWidth,
103
+ maxFactor: p.maxWidthFactor,
104
+ },
105
+ height: {
106
+ fixed: p.height,
107
+ factor: p.heightFactor,
108
+ min: p.minHeight,
109
+ minFactor: p.minHeightFactor,
110
+ max: p.maxHeight,
111
+ maxFactor: p.maxHeightFactor,
112
+ },
113
+ };
114
+ }
115
+ /**
116
+ * Resolve both axes of a sized element: relative sizing, then `aspectRatio`, then `min`/`max`.
117
+ *
118
+ * The order follows CSS: a ratio fills in the axis you left open, and min/max clamp afterwards - so an
119
+ * explicit bound wins over the ratio, exactly as `min-height` beats `aspect-ratio` in a browser.
120
+ *
121
+ * `min`/`max` also come back as NARROWED constraints, because an axis with no explicit size still has
122
+ * to obey them. `maxWidth` alone means "fill, but no wider than this", and the caller's own fill and
123
+ * shrink-wrap paths get that for free by using the returned constraints instead of the ones passed in.
124
+ * A `width` of `undefined` keeps its usual meaning: fill or shrink-wrap.
125
+ */
126
+ export function resolveSize(width, height, aspectRatio, constraints) {
127
+ const bounds = constraints.narrow(resolveExtent(width.min, width.minFactor, constraints.maxWidth, constraints.hasBoundedWidth), resolveExtent(width.max, width.maxFactor, constraints.maxWidth, constraints.hasBoundedWidth), resolveExtent(height.min, height.minFactor, constraints.maxHeight, constraints.hasBoundedHeight), resolveExtent(height.max, height.maxFactor, constraints.maxHeight, constraints.hasBoundedHeight));
128
+ // Against the OFFERED box, not the narrowed one: `width: "50%"` with `maxWidth: 100` in a 400pt
129
+ // region is 200 capped to 100, not 50% of 100. The clamp below applies the bound.
130
+ let w = resolveExtent(width.fixed, width.factor, constraints.maxWidth, constraints.hasBoundedWidth);
131
+ let h = resolveExtent(height.fixed, height.factor, constraints.maxHeight, constraints.hasBoundedHeight);
132
+ if (aspectRatio !== undefined && aspectRatio > 0) {
133
+ if (w !== undefined && h === undefined)
134
+ h = w / aspectRatio;
135
+ else if (h !== undefined && w === undefined)
136
+ w = h * aspectRatio;
137
+ else if (w === undefined && h === undefined) {
138
+ // Neither axis pinned: take the offered width and let the ratio give the height (CSS block
139
+ // behaviour). Falls back to the height when only that axis is bounded.
140
+ if (bounds.hasBoundedWidth) {
141
+ w = bounds.maxWidth;
142
+ h = w / aspectRatio;
143
+ }
144
+ else if (bounds.hasBoundedHeight) {
145
+ h = bounds.maxHeight;
146
+ w = h * aspectRatio;
147
+ }
148
+ }
149
+ }
150
+ return {
151
+ width: w === undefined ? undefined : bounds.constrainWidth(w),
152
+ height: h === undefined ? undefined : bounds.constrainHeight(h),
153
+ constraints: bounds,
154
+ };
155
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Resolved and unresolved edge insets, on the LAYOUT side of the seam.
3
+ *
4
+ * The public `Insets` input (points, percentages, four shapes) lives in `api/insets.ts` and normalizes
5
+ * into the specs below - the same split `dimension.ts` and `box-constraints.ts` already use for sizes,
6
+ * so an element never sees a percentage string and the api layer never sees layout geometry.
7
+ */
8
+ /** One side: an absolute point size, or a fraction of the available width. */
9
+ export interface EdgeSpec {
10
+ points?: number;
11
+ factor?: number;
12
+ }
13
+ /**
14
+ * `[top, right, bottom, left]`, each still unresolved.
15
+ *
16
+ * A plain number is accepted as shorthand for `{ points: n }`: the engine elements are exported for
17
+ * power users and used to take four numbers, so requiring `{ points: 5 }` would break them for a
18
+ * feature they are not using.
19
+ */
20
+ export type EdgeSpecs = [EdgeInput, EdgeInput, EdgeInput, EdgeInput];
21
+ /** A side as given: points for short, or a spec when it may be a percentage. */
22
+ export type EdgeInput = number | EdgeSpec;
23
+ /**
24
+ * Turn the specs into points. `available` is the WIDTH the element was offered - a percentage resolves
25
+ * against the width on ALL FOUR sides, top and bottom included, which is the CSS rule (and Yoga's, so
26
+ * react-pdf agrees). An unbounded width leaves a percentage at 0, the no-op a percentage size gives.
27
+ */
28
+ export declare function resolveEdges(specs: EdgeSpecs, available: number): [number, number, number, number];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Resolved and unresolved edge insets, on the LAYOUT side of the seam.
3
+ *
4
+ * The public `Insets` input (points, percentages, four shapes) lives in `api/insets.ts` and normalizes
5
+ * into the specs below - the same split `dimension.ts` and `box-constraints.ts` already use for sizes,
6
+ * so an element never sees a percentage string and the api layer never sees layout geometry.
7
+ */
8
+ /**
9
+ * Turn the specs into points. `available` is the WIDTH the element was offered - a percentage resolves
10
+ * against the width on ALL FOUR sides, top and bottom included, which is the CSS rule (and Yoga's, so
11
+ * react-pdf agrees). An unbounded width leaves a percentage at 0, the no-op a percentage size gives.
12
+ */
13
+ export function resolveEdges(specs, available) {
14
+ const one = (e) => {
15
+ if (typeof e === "number")
16
+ return e;
17
+ return (e.points ?? (e.factor !== undefined && Number.isFinite(available) ? available * e.factor : 0));
18
+ };
19
+ return [one(specs[0]), one(specs[1]), one(specs[2]), one(specs[3])];
20
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `min-content` sizing - the smallest extent a subtree can be squeezed into and still hold together.
3
+ *
4
+ * The shrink pass in `FlexLayoutHelper` floors every child at this, so an overflowing line stops
5
+ * squeezing instead of grinding a paragraph down to one word per line. It is the same rule a browser
6
+ * applies through the automatic `min-width` of a flex item (CSS Flexbox 4.5).
7
+ *
8
+ * Only the leaves know a real number - a `Text` its longest word, everything else nothing in
9
+ * particular. These helpers are how a container folds its children's answers into its own.
10
+ */
11
+ import type { LayoutContext } from "../elements/pdf-element.js";
12
+ import type { PDFElement } from "../elements/pdf-element.js";
13
+ /**
14
+ * The children's floor, folded along one axis: children laid out ALONG the queried axis have to fit
15
+ * side by side, so their floors add up (plus the gaps between them); across it they merely overlap,
16
+ * so the widest one wins.
17
+ *
18
+ * A wrapping stack adds up nothing: it may put every child on its own line, so it is only as wide as
19
+ * its widest child - which is what CSS says about a multi-line flex container too.
20
+ */
21
+ export declare function stackMinIntrinsic(children: PDFElement[], gap: number, alongAxis: boolean, horizontal: boolean, ctx: LayoutContext, wraps?: boolean): number;
22
+ /**
23
+ * Folds an element's own declared extent into the floor its content asks for. CSS calls these the
24
+ * "specified size suggestion" and the "content size suggestion" and takes the SMALLER: a box that
25
+ * declares 400pt around content needing 50 may still be squeezed to 50, while one declaring 400
26
+ * around content needing 500 cannot go below 400. An extent given as a fraction is not a suggestion
27
+ * at all - it has no size of its own until the parent is known - so it is ignored here.
28
+ */
29
+ export declare function withDeclaredExtent(contentFloor: number, declared: number | undefined, explicitMin?: number): number;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The children's floor, folded along one axis: children laid out ALONG the queried axis have to fit
3
+ * side by side, so their floors add up (plus the gaps between them); across it they merely overlap,
4
+ * so the widest one wins.
5
+ *
6
+ * A wrapping stack adds up nothing: it may put every child on its own line, so it is only as wide as
7
+ * its widest child - which is what CSS says about a multi-line flex container too.
8
+ */
9
+ export function stackMinIntrinsic(children, gap, alongAxis, horizontal, ctx, wraps = false) {
10
+ const floors = children.map((c) => c.minIntrinsicMain(horizontal, ctx));
11
+ if (floors.length === 0)
12
+ return 0;
13
+ if (!alongAxis || wraps)
14
+ return Math.max(...floors);
15
+ return floors.reduce((sum, f) => sum + f, 0) + gap * (floors.length - 1);
16
+ }
17
+ /**
18
+ * Folds an element's own declared extent into the floor its content asks for. CSS calls these the
19
+ * "specified size suggestion" and the "content size suggestion" and takes the SMALLER: a box that
20
+ * declares 400pt around content needing 50 may still be squeezed to 50, while one declaring 400
21
+ * around content needing 500 cannot go below 400. An extent given as a fraction is not a suggestion
22
+ * at all - it has no size of its own until the parent is known - so it is ignored here.
23
+ */
24
+ export function withDeclaredExtent(contentFloor, declared, explicitMin) {
25
+ // An explicit `minWidth`/`minHeight` REPLACES the automatic minimum - it does not compete with it.
26
+ // CSS applies the automatic minimum only while `min-width` is `auto`, so a smaller explicit value
27
+ // wins too: `min-width: 0` is the standard way to let a flex item shrink past its longest word, and
28
+ // taking the larger of the two would quietly ignore it. Measured in Chrome on one long word in a
29
+ // 200pt line: 132.08pt with `auto`, 84.64pt with `0`.
30
+ if (explicitMin !== undefined)
31
+ return explicitMin;
32
+ // Without an explicit bound the floor is CSS's content-based minimum: the smaller of what the
33
+ // element declares and what its content actually needs. It matters because the shrink pass must not
34
+ // aim below a size the child will refuse - the share it was supposed to give would never arrive,
35
+ // and the line would stay over its width.
36
+ return declared === undefined ? contentFloor : Math.min(declared, contentFloor);
37
+ }
@@ -3,7 +3,7 @@ export class AnchorRenderer {
3
3
  static async render(element, objectManager) {
4
4
  const { name, child, y } = element.getProps();
5
5
  const renderer = RendererRegistry.getRenderer(child);
6
- const childNodes = renderer ? await renderer(child, objectManager) : [];
6
+ const childNodes = await renderer(child, objectManager);
7
7
  // The child draws normally; an Anchor IR node carries the name + scroll target for the page renderer
8
8
  // to register as a named destination (it produces no content-stream ops itself).
9
9
  return [...childNodes, { type: "anchor", y, name }];
@@ -3,7 +3,7 @@ export class BookmarkRenderer {
3
3
  static async render(element, objectManager) {
4
4
  const { title, level, child, y } = element.getProps();
5
5
  const renderer = RendererRegistry.getRenderer(child);
6
- const childNodes = renderer ? await renderer(child, objectManager) : [];
6
+ const childNodes = await renderer(child, objectManager);
7
7
  // The child draws normally; an Outline IR node carries the title + nesting + scroll target for the
8
8
  // page renderer to collect into the /Outlines tree (it produces no content-stream ops itself).
9
9
  return [...childNodes, { type: "outline", y, title, level }];
@@ -0,0 +1,14 @@
1
+ import { PDFObjectManager } from "../utils/pdf-object-manager.js";
2
+ import { CanvasElement } from "../elements/canvas-element.js";
3
+ import type { IRNode } from "../ir/display-list.js";
4
+ /**
5
+ * Runs the caller's paint callback and hands back what it drew.
6
+ *
7
+ * The painter works in CANVAS coordinates (0,0 is the box's own corner), so the whole drawing is
8
+ * wrapped in one translate rather than every point being offset - the same shape `Svg` uses, and it
9
+ * keeps the callback independent of where the box ended up. The clip is the box: a drawing that runs
10
+ * past its own edge would otherwise bleed into the layout beside it.
11
+ */
12
+ export declare class CanvasRenderer {
13
+ static render(element: CanvasElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
14
+ }
@@ -0,0 +1,36 @@
1
+ import { CanvasPainter } from "../canvas/painter.js";
2
+ /**
3
+ * Runs the caller's paint callback and hands back what it drew.
4
+ *
5
+ * The painter works in CANVAS coordinates (0,0 is the box's own corner), so the whole drawing is
6
+ * wrapped in one translate rather than every point being offset - the same shape `Svg` uses, and it
7
+ * keeps the callback independent of where the box ended up. The clip is the box: a drawing that runs
8
+ * past its own edge would otherwise bleed into the layout beside it.
9
+ */
10
+ export class CanvasRenderer {
11
+ static async render(element, objectManager) {
12
+ const { x, y, width, height, paint, alt } = element.getProps();
13
+ // The layout bounds are kept as plain numbers, and the painter and the callback each get their
14
+ // OWN size object. Sharing one meant a callback that wrote to `size.width` moved the clip away
15
+ // from the box layout had reserved - the drawing would then bleed into what sits beside it.
16
+ const w = width ?? 0;
17
+ const h = height ?? 0;
18
+ const painter = new CanvasPainter({ width: w, height: h });
19
+ paint(painter, { width: w, height: h });
20
+ const drawn = painter.drawn();
21
+ const nodes = [
22
+ { type: "clip-push", x: x ?? 0, y: y ?? 0, width: w, height: h },
23
+ { type: "transform-push", matrix: [1, 0, 0, 1, x ?? 0, y ?? 0] },
24
+ ...drawn,
25
+ { type: "transform-pop" },
26
+ { type: "clip-pop" },
27
+ ];
28
+ if (!alt || !objectManager.struct.enabled)
29
+ return nodes;
30
+ const tag = {
31
+ role: "Figure",
32
+ key: objectManager.struct.openElement(element.structId, "Figure", { alt }),
33
+ };
34
+ return nodes.map((node) => (node.type === "path" ? { ...node, tag } : node));
35
+ }
36
+ }
@@ -6,9 +6,7 @@ export class ContainerRenderer {
6
6
  if (children)
7
7
  for (const child of children) {
8
8
  const renderer = RendererRegistry.getRenderer(child);
9
- if (renderer) {
10
- nodes.push(...(await renderer(child, objectManager)));
11
- }
9
+ nodes.push(...(await renderer(child, objectManager)));
12
10
  }
13
11
  return nodes;
14
12
  }
@@ -5,6 +5,6 @@ export class DefaultTextStyleRenderer {
5
5
  static async render(element, objectManager) {
6
6
  const { child } = element.getProps();
7
7
  const renderer = RendererRegistry.getRenderer(child);
8
- return renderer ? await renderer(child, objectManager) : [];
8
+ return renderer(child, objectManager);
9
9
  }
10
10
  }
@@ -5,6 +5,6 @@ export class DeferredRenderer {
5
5
  if (!composed)
6
6
  return [];
7
7
  const renderer = RendererRegistry.getRenderer(composed);
8
- return renderer ? renderer(composed, objectManager) : [];
8
+ return renderer(composed, objectManager);
9
9
  }
10
10
  }
@@ -3,6 +3,6 @@ export class ExpandedRenderer {
3
3
  static async render(expandedElement, objectManager) {
4
4
  const { child } = expandedElement.getProps();
5
5
  const renderer = RendererRegistry.getRenderer(child);
6
- return renderer ? await renderer(child, objectManager) : [];
6
+ return renderer(child, objectManager);
7
7
  }
8
8
  }
@@ -1,6 +1,7 @@
1
1
  import { BoxFit } from "../elements/image-element.js";
2
2
  import { bytesFromLatin1 } from "../utils/bytes.js";
3
3
  import { applyContainFit, applyCoverFit, applyFillFit, applyFitNone, decodePngToRgbFlate, } from "../utils/image-helper.js";
4
+ import { isRounded } from "../ir/display-list.js";
4
5
  export class ImageRenderer {
5
6
  static async render(imageElement, objectManager) {
6
7
  // Load the image and convert it in a binary string
@@ -56,7 +57,7 @@ export class ImageRenderer {
56
57
  }
57
58
  // A radius rounds the image BOX (the element frame), so it clips to that frame too -
58
59
  // independent of the cover/contain overflow clip.
59
- const wantsClip = mustCreateOverflowContainer || (radius ?? 0) > 0;
60
+ const wantsClip = mustCreateOverflowContainer || isRounded(radius);
60
61
  // The fitted geometry becomes a display-list primitive; the backend registers
61
62
  // the XObject and emits the placement (+ clip, rounded when a radius is set).
62
63
  const node = {
@@ -8,3 +8,5 @@ export * from "./text-renderer.js";
8
8
  export * from "./expanded-renderer.js";
9
9
  export * from "./line-renderer.js";
10
10
  export * from "./row-renderer.js";
11
+ export * from "./svg-renderer.js";
12
+ export * from "./canvas-renderer.js";
@@ -8,3 +8,5 @@ export * from "./text-renderer.js";
8
8
  export * from "./expanded-renderer.js";
9
9
  export * from "./line-renderer.js";
10
10
  export * from "./row-renderer.js";
11
+ export * from "./svg-renderer.js";
12
+ export * from "./canvas-renderer.js";
@@ -7,6 +7,6 @@ export class KeepTogetherRenderer {
7
7
  static async render(group, objectManager) {
8
8
  const { child } = group.getProps();
9
9
  const renderer = RendererRegistry.getRenderer(child);
10
- return renderer ? await renderer(child, objectManager) : [];
10
+ return renderer(child, objectManager);
11
11
  }
12
12
  }
@@ -3,7 +3,7 @@ export class LinkRenderer {
3
3
  static async render(element, objectManager) {
4
4
  const { href, dest, child, x, y, width, height } = element.getProps();
5
5
  const renderer = RendererRegistry.getRenderer(child);
6
- const childNodes = renderer ? await renderer(child, objectManager) : [];
6
+ const childNodes = await renderer(child, objectManager);
7
7
  // The child draws normally; a Link IR node carries the clickable rect + target (external href or
8
8
  // internal dest) for the page renderer to emit as a /Link annotation (no content-stream ops itself).
9
9
  return [...childNodes, { type: "link", x, y, width, height, href, dest }];
@@ -3,6 +3,6 @@ export class PaddingRenderer {
3
3
  static async render(paddingElement, objectManager) {
4
4
  const { child } = paddingElement.getProps();
5
5
  const renderer = RendererRegistry.getRenderer(child);
6
- return renderer ? await renderer(child, objectManager) : [];
6
+ return renderer(child, objectManager);
7
7
  }
8
8
  }
@@ -6,6 +6,6 @@ export class PageBuilderRenderer {
6
6
  if (!composed)
7
7
  return [];
8
8
  const renderer = RendererRegistry.getRenderer(composed);
9
- return renderer ? renderer(composed, objectManager) : [];
9
+ return renderer(composed, objectManager);
10
10
  }
11
11
  }
@@ -21,9 +21,7 @@ export class PageRenderer {
21
21
  const nodes = [];
22
22
  for (const element of renderables) {
23
23
  const renderer = RendererRegistry.getRenderer(element);
24
- if (renderer) {
25
- nodes.push(...(await renderer(element, objectManager)));
26
- }
24
+ nodes.push(...(await renderer(element, objectManager)));
27
25
  }
28
26
  // Accessible tagging: one struct context per page (allocates its StructParents index), threaded into
29
27
  // serialize so each drawable node is wrapped in marked content. Undefined = tagging off (byte-identical).