@jasy/pdf 1.0.0-alpha.8 → 1.0.0-alpha.9

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 (97) hide show
  1. package/README.md +4 -3
  2. package/dist/api/content.d.ts +5 -4
  3. package/dist/api/content.js +13 -7
  4. package/dist/api/dimension.d.ts +56 -0
  5. package/dist/api/dimension.js +27 -0
  6. package/dist/api/font-url.d.ts +28 -0
  7. package/dist/api/font-url.js +134 -0
  8. package/dist/api/gradient.d.ts +53 -0
  9. package/dist/api/gradient.js +97 -0
  10. package/dist/api/index.d.ts +2 -0
  11. package/dist/api/index.js +2 -0
  12. package/dist/api/insets.d.ts +19 -12
  13. package/dist/api/insets.js +18 -7
  14. package/dist/api/layout.d.ts +20 -6
  15. package/dist/api/layout.js +31 -6
  16. package/dist/api/page-builder.d.ts +8 -0
  17. package/dist/api/page-builder.js +12 -0
  18. package/dist/api/structure.d.ts +5 -0
  19. package/dist/api/structure.js +16 -4
  20. package/dist/api/text.d.ts +42 -6
  21. package/dist/api/text.js +29 -5
  22. package/dist/edit/document.d.ts +14 -4
  23. package/dist/edit/document.js +15 -22
  24. package/dist/edit/fill.d.ts +19 -2
  25. package/dist/edit/fill.js +63 -34
  26. package/dist/edit/flatten.d.ts +13 -1
  27. package/dist/edit/flatten.js +38 -13
  28. package/dist/edit/index.d.ts +2 -0
  29. package/dist/edit/index.js +3 -1
  30. package/dist/edit/objects.d.ts +5 -0
  31. package/dist/edit/objects.js +27 -0
  32. package/dist/edit/writer.d.ts +7 -0
  33. package/dist/edit/writer.js +23 -1
  34. package/dist/elements/container-element.d.ts +21 -1
  35. package/dist/elements/container-element.js +25 -10
  36. package/dist/elements/image-element.d.ts +18 -4
  37. package/dist/elements/image-element.js +27 -12
  38. package/dist/elements/layout/expanded-element.d.ts +3 -1
  39. package/dist/elements/layout/expanded-element.js +12 -3
  40. package/dist/elements/layout/padding-element.d.ts +4 -2
  41. package/dist/elements/layout/padding-element.js +4 -2
  42. package/dist/elements/layout/page-builder-element.js +2 -0
  43. package/dist/elements/pdf-element.d.ts +59 -1
  44. package/dist/elements/pdf-element.js +62 -1
  45. package/dist/elements/rectangle-element.d.ts +20 -6
  46. package/dist/elements/rectangle-element.js +25 -12
  47. package/dist/elements/row-element.d.ts +24 -1
  48. package/dist/elements/row-element.js +25 -10
  49. package/dist/elements/text-element.d.ts +60 -1
  50. package/dist/elements/text-element.js +132 -33
  51. package/dist/ir/display-list.d.ts +23 -6
  52. package/dist/ir/display-list.js +8 -1
  53. package/dist/layout/alignment.d.ts +10 -0
  54. package/dist/layout/alignment.js +6 -0
  55. package/dist/layout/box-constraints.d.ts +53 -0
  56. package/dist/layout/box-constraints.js +72 -0
  57. package/dist/layout/insets.d.ts +28 -0
  58. package/dist/layout/insets.js +20 -0
  59. package/dist/renderer/image-renderer.js +2 -1
  60. package/dist/renderer/pdf-backend.d.ts +6 -0
  61. package/dist/renderer/pdf-backend.js +78 -31
  62. package/dist/renderer/pdf-document-renderer.js +8 -1
  63. package/dist/renderer/rectangle-renderer.js +8 -2
  64. package/dist/renderer/text-renderer.d.ts +2 -2
  65. package/dist/renderer/text-renderer.js +150 -42
  66. package/dist/text/advance.js +5 -2
  67. package/dist/text/arabic.d.ts +20 -0
  68. package/dist/text/arabic.js +101 -0
  69. package/dist/text/bidi.d.ts +33 -0
  70. package/dist/text/bidi.js +94 -0
  71. package/dist/text/font-fallback.d.ts +25 -0
  72. package/dist/text/font-fallback.js +38 -0
  73. package/dist/text/line-breaker.d.ts +35 -1
  74. package/dist/text/line-breaker.js +76 -16
  75. package/dist/text/orphans-widows.d.ts +27 -0
  76. package/dist/text/orphans-widows.js +53 -0
  77. package/dist/text/shape.d.ts +29 -0
  78. package/dist/text/shape.js +67 -0
  79. package/dist/text/text-style.d.ts +25 -0
  80. package/dist/text/text-style.js +44 -1
  81. package/dist/utils/flex-layout.d.ts +24 -4
  82. package/dist/utils/flex-layout.js +169 -7
  83. package/dist/utils/font-metrics.d.ts +5 -0
  84. package/dist/utils/gsub.d.ts +36 -0
  85. package/dist/utils/gsub.js +178 -0
  86. package/dist/utils/inflate.d.ts +16 -0
  87. package/dist/utils/inflate.js +52 -0
  88. package/dist/utils/pdf-object-manager.d.ts +21 -0
  89. package/dist/utils/pdf-object-manager.js +107 -9
  90. package/dist/utils/ttf-parser.d.ts +6 -0
  91. package/dist/utils/ttf-parser.js +19 -0
  92. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  93. package/dist/utils/utf8-to-windows1252-encoder.js +10 -0
  94. package/dist/utils/woff.d.ts +10 -0
  95. package/dist/utils/woff.js +116 -0
  96. package/dist/validators/element-validator.js +9 -2
  97. package/package.json +2 -1
@@ -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
+ }
@@ -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 = {
@@ -58,6 +58,12 @@ export declare class PdfBackend {
58
58
  * (kappa ≈ 0.5523). Returns m/l/c/h ops WITHOUT the paint operator.
59
59
  */
60
60
  private static roundedRectPath;
61
+ /**
62
+ * Clamp the four radii into the box. Each is capped at half the box, and two radii sharing an edge
63
+ * are scaled down together when they would overlap - the CSS rule, and what keeps the path from
64
+ * folding back on itself.
65
+ */
66
+ private static clampRadii;
61
67
  /**
62
68
  * Serialize a single display-list node to its content-stream operators.
63
69
  * `om` is used only by primitives that allocate PDF resources (images, fonts).
@@ -1,3 +1,4 @@
1
+ import { isRounded } from "../ir/display-list.js";
1
2
  import { Color } from "../common/color.js";
2
3
  /**
3
4
  * PDF backend - turns display-list primitives into content-stream operators.
@@ -18,8 +19,13 @@ export class PdfBackend {
18
19
  static flipY(nodes, pageHeight) {
19
20
  return nodes.map((node) => {
20
21
  switch (node.type) {
21
- case "rect":
22
- return { ...node, y: pageHeight - node.y - node.height };
22
+ case "rect": {
23
+ // A gradient fill carries its own page-space anchors; flip them with the box (see "path").
24
+ const fill = node.fill === undefined || node.fill instanceof Color
25
+ ? node.fill
26
+ : { ...node.fill, y0: pageHeight - node.fill.y0, y1: pageHeight - node.fill.y1 };
27
+ return { ...node, y: pageHeight - node.y - node.height, fill };
28
+ }
23
29
  case "line":
24
30
  return {
25
31
  ...node,
@@ -193,26 +199,45 @@ export class PdfBackend {
193
199
  * (kappa ≈ 0.5523). Returns m/l/c/h ops WITHOUT the paint operator.
194
200
  */
195
201
  static roundedRectPath(x, y, w, h, radius) {
196
- const r = Math.min(radius, w / 2, h / 2);
197
- const c = r * 0.5523; // control-point offset that approximates a quarter circle
202
+ // Per corner, in PDF coordinates (y up). With all four equal this emits exactly the operators the
203
+ // single-radius version did - the byte-identity guard for every document that never asks for one.
204
+ const r = PdfBackend.clampRadii(radius, w, h);
205
+ const k = 0.5523; // control-point offset that approximates a quarter circle
198
206
  const f = (n) => n.toFixed(3);
199
- const xr = x + r;
200
- const xwr = x + w - r;
201
207
  const xw = x + w;
202
- const yr = y + r;
203
- const yhr = y + h - r;
204
208
  const yh = y + h;
205
- return (`${f(xr)} ${f(y)} m\n` +
206
- `${f(xwr)} ${f(y)} l\n` +
207
- `${f(xwr + c)} ${f(y)} ${f(xw)} ${f(yr - c)} ${f(xw)} ${f(yr)} c\n` +
208
- `${f(xw)} ${f(yhr)} l\n` +
209
- `${f(xw)} ${f(yhr + c)} ${f(xwr + c)} ${f(yh)} ${f(xwr)} ${f(yh)} c\n` +
210
- `${f(xr)} ${f(yh)} l\n` +
211
- `${f(xr - c)} ${f(yh)} ${f(x)} ${f(yhr + c)} ${f(x)} ${f(yhr)} c\n` +
212
- `${f(x)} ${f(yr)} l\n` +
213
- `${f(x)} ${f(yr - c)} ${f(xr - c)} ${f(y)} ${f(xr)} ${f(y)} c\n` +
209
+ return (`${f(x + r.bl)} ${f(y)} m\n` +
210
+ `${f(xw - r.br)} ${f(y)} l\n` +
211
+ `${f(xw - r.br + r.br * k)} ${f(y)} ${f(xw)} ${f(y + r.br - r.br * k)} ${f(xw)} ${f(y + r.br)} c\n` +
212
+ `${f(xw)} ${f(yh - r.tr)} l\n` +
213
+ `${f(xw)} ${f(yh - r.tr + r.tr * k)} ${f(xw - r.tr + r.tr * k)} ${f(yh)} ${f(xw - r.tr)} ${f(yh)} c\n` +
214
+ `${f(x + r.tl)} ${f(yh)} l\n` +
215
+ `${f(x + r.tl - r.tl * k)} ${f(yh)} ${f(x)} ${f(yh - r.tl + r.tl * k)} ${f(x)} ${f(yh - r.tl)} c\n` +
216
+ `${f(x)} ${f(y + r.bl)} l\n` +
217
+ `${f(x)} ${f(y + r.bl - r.bl * k)} ${f(x + r.bl - r.bl * k)} ${f(y)} ${f(x + r.bl)} ${f(y)} c\n` +
214
218
  `h`);
215
219
  }
220
+ /**
221
+ * Clamp the four radii into the box. Each is capped at half the box, and two radii sharing an edge
222
+ * are scaled down together when they would overlap - the CSS rule, and what keeps the path from
223
+ * folding back on itself.
224
+ */
225
+ static clampRadii(radius, w, h) {
226
+ const n = typeof radius === "number" ? radius : 0;
227
+ const cap = Math.max(0, Math.min(w, h) / 2);
228
+ const at = (v) => Math.max(0, Math.min(v ?? n, cap));
229
+ const r = typeof radius === "number"
230
+ ? { tl: at(radius), tr: at(radius), br: at(radius), bl: at(radius) }
231
+ : { tl: at(radius.tl), tr: at(radius.tr), br: at(radius.br), bl: at(radius.bl) };
232
+ // Per-edge overlap: scale the whole set by the tightest edge, so the corners stay proportional.
233
+ // An edge whose two corners are both 0 constrains NOTHING - it must not contribute a ratio of 0,
234
+ // which would scale every other corner away with it.
235
+ const ratio = (extent, a, b) => a + b > 0 ? extent / (a + b) : Infinity;
236
+ const scale = Math.min(1, ratio(w, r.tl, r.tr), ratio(w, r.bl, r.br), ratio(h, r.tl, r.bl), ratio(h, r.tr, r.br));
237
+ if (scale >= 1)
238
+ return r;
239
+ return { tl: r.tl * scale, tr: r.tr * scale, br: r.br * scale, bl: r.bl * scale };
240
+ }
216
241
  /**
217
242
  * Serialize a single display-list node to its content-stream operators.
218
243
  * `om` is used only by primitives that allocate PDF resources (images, fonts).
@@ -237,22 +262,37 @@ export class PdfBackend {
237
262
  const doStroke = !!node.stroke && (node.strokeWidth ?? 0) > 0;
238
263
  if (!node.fill && !doStroke)
239
264
  return "";
240
- let ops = "";
241
- if (doStroke) {
242
- ops += `${node.strokeWidth} w\n${node.stroke.toPDFColorString()} RG\n`;
243
- }
244
- if (node.fill)
245
- ops += `${node.fill.toPDFColorString()} rg\n`;
246
- const paint = node.fill ? (doStroke ? "B" : "f") : "S";
247
265
  // Rounded corners emit a Bézier path; sharp corners keep the plain `re`
248
266
  // (byte-identical when no radius is set).
249
- const path = (node.radius ?? 0) > 0
267
+ const path = isRounded(node.radius)
250
268
  ? PdfBackend.roundedRectPath(node.x, node.y, node.width, node.height, node.radius)
251
269
  : `${node.x} ${node.y} ${node.width} ${node.height} re`;
270
+ // A gradient cannot be a fill COLOUR - PDF paints one with `sh`, which floods the current
271
+ // clip. So the box becomes the clip, the shading is painted inside it, and a border is
272
+ // stroked afterwards on its own. Solid fills keep the plain operators, byte for byte.
273
+ const gradient = node.fill !== undefined && !(node.fill instanceof Color) ? node.fill : undefined;
274
+ if (gradient) {
275
+ const shading = om.registerShading(gradient);
276
+ let out = `q\n${path} W n\n/${shading} sh\nQ\n`;
277
+ if (doStroke) {
278
+ const gsStroke = PdfBackend.alphaPrefix(om, 1, node.stroke.getAlpha());
279
+ const stroke = `${node.strokeWidth} w\n${node.stroke.toPDFColorString()} RG\n` + `${path} S\n`;
280
+ out += gsStroke ? `q\n${gsStroke}${stroke}Q\n` : stroke;
281
+ }
282
+ return out;
283
+ }
284
+ const fill = node.fill;
285
+ let ops = "";
286
+ if (doStroke) {
287
+ ops += `${node.strokeWidth} w\n${node.stroke.toPDFColorString()} RG\n`;
288
+ }
289
+ if (fill)
290
+ ops += `${fill.toPDFColorString()} rg\n`;
291
+ const paint = fill ? (doStroke ? "B" : "f") : "S";
252
292
  const body = ops + `${path} ${paint}\n`;
253
293
  // Transparency needs an isolating q/Q so the state does not leak; opaque rects
254
294
  // keep their bare operators (byte-identical).
255
- const gs = PdfBackend.alphaPrefix(om, node.fill?.getAlpha() ?? 1, doStroke ? node.stroke.getAlpha() : 1);
295
+ const gs = PdfBackend.alphaPrefix(om, fill?.getAlpha() ?? 1, doStroke ? node.stroke.getAlpha() : 1);
256
296
  return gs ? `q\n${gs}${body}Q\n` : body;
257
297
  }
258
298
  case "text": {
@@ -270,15 +310,22 @@ export class PdfBackend {
270
310
  const encode = (t) => isCustom
271
311
  ? `<${om.encodeCustomText(node.fontFamily, t, node.fontStyle)}>`
272
312
  : `(${PdfBackend.escapePdfString(t)})`;
313
+ // A shaped run brings its own glyphs, already in drawing order - they cannot be derived from
314
+ // the text here.
315
+ const shapedHex = node.glyphs && isCustom
316
+ ? om.registerGlyphs(node.fontFamily, node.glyphs, node.fontStyle)
317
+ : undefined;
273
318
  // Kerning (if the document has it on): a `TJ` array with the font's per-pair adjustments
274
319
  // between glyph chunks, measured into the layout by the SAME numbers (runAdvance). A plain
275
320
  // `Tj` when the run has no kerning, byte-identical.
276
321
  const kerns = om.kerningEnabled
277
322
  ? om.getKernPairs(node.text, node.fontFamily, node.fontStyle)
278
323
  : [];
279
- const showOp = kerns.some((k) => k !== 0)
280
- ? `${PdfBackend.kernedArray(node.text, kerns, encode)} TJ`
281
- : `${encode(node.text)} Tj`;
324
+ const showOp = shapedHex
325
+ ? `<${shapedHex}> Tj`
326
+ : kerns.some((k) => k !== 0)
327
+ ? `${PdfBackend.kernedArray(node.text, kerns, encode)} TJ`
328
+ : `${encode(node.text)} Tj`;
282
329
  // letterSpacing is the `Tc` operator: extra advance after EVERY glyph (an embedded
283
330
  // Identity-H font too - that is `Tw`, word spacing, which only touches single-byte code 32).
284
331
  const spacing = node.letterSpacing ? `${node.letterSpacing.toFixed(3)} Tc\n` : "";
@@ -303,7 +350,7 @@ export class PdfBackend {
303
350
  // Clip to the frame (re … W n); rounded when a radius is set. The rectangular
304
351
  // path is byte-identical to before.
305
352
  const c = node.clip;
306
- const clipPath = (node.radius ?? 0) > 0
353
+ const clipPath = isRounded(node.radius)
307
354
  ? PdfBackend.roundedRectPath(c.x, c.y, c.width, c.height, node.radius)
308
355
  : `${c.x} ${c.y} ${c.width} ${c.height} re `;
309
356
  return `q\n${clipPath}\nW n \n` + draw + `Q\n`;
@@ -311,7 +358,7 @@ export class PdfBackend {
311
358
  case "clip-push": {
312
359
  // Save the graphics state and intersect the clip with this (rounded) rect. Everything
313
360
  // drawn until the matching clip-pop is cropped to it.
314
- const path = (node.radius ?? 0) > 0
361
+ const path = isRounded(node.radius)
315
362
  ? PdfBackend.roundedRectPath(node.x, node.y, node.width, node.height, node.radius)
316
363
  : `${node.x} ${node.y} ${node.width} ${node.height} re`;
317
364
  return `q\n${path}\nW n\n`;
@@ -14,8 +14,13 @@ export class PDFDocumentRenderer {
14
14
  // pure (`{ fitted, remainder }`), so the full list of physical pages can be settled up front. Only
15
15
  // then is the total page count known, which is what page numbers in a header/footer need.
16
16
  const physicalPages = [];
17
+ // Where each sheet sits WITHIN the logical page it came from. Pass A already knows: one logical
18
+ // `Page` element produces one run of sheets, so the run's length is that section's total.
19
+ const section = [];
17
20
  for (const page of document.getProps().children) {
18
- physicalPages.push(...PDFDocumentRenderer.paginateLogicalPage(page, ctx));
21
+ const sheets = PDFDocumentRenderer.paginateLogicalPage(page, ctx);
22
+ sheets.forEach((_, i) => section.push({ number: i + 1, total: sheets.length }));
23
+ physicalPages.push(...sheets);
19
24
  }
20
25
  // Pass B - draw them, in order, so the emitted page objects keep their previous numbering. Each page
21
26
  // is laid out again with its own PageInfo, which is how a `PageBuilder` anywhere in the tree (header,
@@ -25,6 +30,8 @@ export class PDFDocumentRenderer {
25
30
  const pageInfo = {
26
31
  pageNumber: index + 1,
27
32
  pageCount: physicalPages.length,
33
+ subPageNumber: section[index].number,
34
+ subPageTotalPages: section[index].total,
28
35
  pageSize: resolvePageSize(PDFDocumentRenderer.configOf(physical, ctx)),
29
36
  };
30
37
  pageNumbers.push(await PDFDocumentRenderer.renderPhysicalPage(physical, objectManager, {
@@ -1,14 +1,20 @@
1
1
  import { RendererRegistry } from "../utils/renderer-registry.js";
2
+ import { isGradientInput, resolveGradient } from "../api/gradient.js";
2
3
  export class RectangleRenderer {
3
4
  static async render(rectangleElement, objectManager) {
4
5
  const { x, y, width, height, children, color, backgroundColor, borderWidth, radius, sideBorders, overflow, } = rectangleElement.getProps();
5
6
  const h = height;
6
7
  const nodes = [];
8
+ // A gradient is written box-relative (an angle and stops); this is the first place the box is
9
+ // known, so it is resolved into the absolute page coordinates the IR wants.
10
+ const fill = backgroundColor !== undefined && isGradientInput(backgroundColor)
11
+ ? resolveGradient(backgroundColor, x, y, width, h)
12
+ : backgroundColor;
7
13
  if (sideBorders) {
8
14
  // Per-side borders: a fill-only box (no stroke), then a line per present side
9
15
  // (sharp corners). This is what lets cells draw grid lines.
10
16
  if (backgroundColor) {
11
- nodes.push({ type: "rect", x, y, width, height: h, strokeWidth: 0, fill: backgroundColor });
17
+ nodes.push({ type: "rect", x, y, width, height: h, strokeWidth: 0, fill });
12
18
  }
13
19
  nodes.push(...RectangleRenderer.sideLines(x, y, width, h, borderWidth, sideBorders));
14
20
  }
@@ -23,7 +29,7 @@ export class RectangleRenderer {
23
29
  height: h,
24
30
  stroke: color,
25
31
  strokeWidth: borderWidth,
26
- ...(backgroundColor ? { fill: backgroundColor } : {}),
32
+ ...(fill ? { fill } : {}),
27
33
  ...(radius ? { radius } : {}),
28
34
  };
29
35
  nodes.push(node);
@@ -2,9 +2,9 @@ import { TextElement, TextSegment } from "../elements/text-element.js";
2
2
  import { FontStyle, PDFObjectManager } from "../utils/pdf-object-manager.js";
3
3
  import type { FontMetrics } from "../utils/font-metrics.js";
4
4
  import { IRNode } from "../ir/display-list.js";
5
- import { TextOverflow } from "../text/line-breaker.js";
5
+ import { type LineOptions, TextOverflow } from "../text/line-breaker.js";
6
6
  export declare class TextRenderer {
7
- static calculateTextHeight(content: string | TextSegment[], fontSize: number, fontFamily: string, fontStyle: FontStyle, objectManager: FontMetrics, maxWidth: number, maxLines?: number, overflow?: TextOverflow, lineHeight?: number, letterSpacing?: number): number;
7
+ static calculateTextHeight(content: string | TextSegment[], fontSize: number, fontFamily: string, fontStyle: FontStyle, objectManager: FontMetrics, maxWidth: number, maxLines?: number, overflow?: TextOverflow, lineHeight?: number, letterSpacing?: number, lineOptions?: LineOptions): number;
8
8
  static render(textElement: TextElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
9
9
  private static _expandColorGlyphs;
10
10
  private static _glyphToPath;