@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
@@ -0,0 +1,53 @@
1
+ import { type ColorInput } from "./color.js";
2
+ import type { Gradient } from "../ir/display-list.js";
3
+ /**
4
+ * Gradients for a box background.
5
+ *
6
+ * The public form is BOX-RELATIVE - an angle and some stops - because that is what an author knows
7
+ * when writing the document. The engine's `Gradient` wants absolute page coordinates, so the renderer
8
+ * resolves one against the box it is painting; see `resolveGradient`.
9
+ */
10
+ /** A stop as written: a colour, optionally pinned to a position 0..1 along the axis. */
11
+ export type StopInput = ColorInput | {
12
+ color: ColorInput;
13
+ at: number;
14
+ };
15
+ export interface LinearGradientInput {
16
+ kind: "linear";
17
+ /** Direction in degrees, CSS convention: 0 = to the top, 90 = to the right (default 180, downwards). */
18
+ angle: number;
19
+ stops: StopInput[];
20
+ }
21
+ export interface RadialGradientInput {
22
+ kind: "radial";
23
+ /** Centre as a fraction of the box, `[0.5, 0.5]` being the middle (the default). */
24
+ center: [number, number];
25
+ /** Outer radius as a fraction of the box's larger side (default 0.5, i.e. it touches the edges). */
26
+ radius: number;
27
+ stops: StopInput[];
28
+ }
29
+ export type GradientInput = LinearGradientInput | RadialGradientInput;
30
+ export declare const isGradientInput: (v: unknown) => v is GradientInput;
31
+ /**
32
+ * A linear gradient. `linearGradient("#fff", "#000")` runs top to bottom; pass `{ angle }` for any
33
+ * other direction, and `{ color, at }` for a stop that is not evenly spaced.
34
+ */
35
+ export declare function linearGradient(...args: [...StopInput[]] | [{
36
+ angle?: number;
37
+ stops: StopInput[];
38
+ }]): LinearGradientInput;
39
+ /** A radial gradient, centred by default and reaching the edges. */
40
+ export declare function radialGradient(...args: [...StopInput[]] | [{
41
+ center?: [number, number];
42
+ radius?: number;
43
+ stops: StopInput[];
44
+ }]): RadialGradientInput;
45
+ /**
46
+ * Resolve a box-relative gradient against the box it paints, into the absolute page coordinates the
47
+ * IR wants. Called by the renderer, which is the first place the box's geometry is known.
48
+ *
49
+ * The angle follows CSS: 0 points to the top and it turns clockwise, so 90 goes to the right. The
50
+ * axis runs through the box centre and is scaled so it spans the box on that diagonal - which is why
51
+ * a 45-degree gradient reaches the corners rather than stopping short of them.
52
+ */
53
+ export declare function resolveGradient(g: GradientInput, x: number, y: number, width: number, height: number): Gradient;
@@ -0,0 +1,97 @@
1
+ import { toColor } from "./color.js";
2
+ export const isGradientInput = (v) => typeof v === "object" && v !== null && "kind" in v && "stops" in v;
3
+ /**
4
+ * A linear gradient. `linearGradient("#fff", "#000")` runs top to bottom; pass `{ angle }` for any
5
+ * other direction, and `{ color, at }` for a stop that is not evenly spaced.
6
+ */
7
+ export function linearGradient(...args) {
8
+ const first = args[0];
9
+ if (args.length === 1 && typeof first === "object" && first !== null && "stops" in first) {
10
+ return { kind: "linear", angle: first.angle ?? 180, stops: first.stops };
11
+ }
12
+ return { kind: "linear", angle: 180, stops: args };
13
+ }
14
+ /** A radial gradient, centred by default and reaching the edges. */
15
+ export function radialGradient(...args) {
16
+ const first = args[0];
17
+ if (args.length === 1 && typeof first === "object" && first !== null && "stops" in first) {
18
+ return {
19
+ kind: "radial",
20
+ center: first.center ?? [0.5, 0.5],
21
+ radius: first.radius ?? 0.5,
22
+ stops: first.stops,
23
+ };
24
+ }
25
+ return { kind: "radial", center: [0.5, 0.5], radius: 0.5, stops: args };
26
+ }
27
+ /**
28
+ * Spread the stops that carry no `at` evenly, first to last, the way CSS does - and check the pinned
29
+ * ones, because they end up in a PDF stitching function's `/Bounds`, which the format requires to be
30
+ * strictly increasing inside the domain. A bad offset there is not a wrong-looking gradient, it is a
31
+ * malformed file, so it is refused by name here rather than emitted.
32
+ */
33
+ function toStops(stops) {
34
+ if (stops.length < 2) {
35
+ throw new Error("A gradient needs at least two colour stops.");
36
+ }
37
+ const last = stops.length - 1;
38
+ const out = stops.map((s, i) => {
39
+ const pinned = typeof s === "object" && s !== null && "at" in s;
40
+ const at = pinned ? s.at : i / last;
41
+ if (!Number.isFinite(at) || at < 0 || at > 1) {
42
+ throw new Error(`Invalid gradient stop position ${at}: it must be a number between 0 and 1.`);
43
+ }
44
+ return {
45
+ offset: at,
46
+ color: toColor(pinned ? s.color : s),
47
+ };
48
+ });
49
+ for (let i = 1; i < out.length; i++) {
50
+ if (out[i].offset <= out[i - 1].offset) {
51
+ throw new Error(`Gradient stops must move forward: stop ${i} sits at ${out[i].offset}, which is not after ` +
52
+ `${out[i - 1].offset}. (Two stops at the SAME position would be a hard colour edge in CSS; ` +
53
+ `PDF cannot express that in one shading, so it is refused rather than drawn wrongly.)`);
54
+ }
55
+ }
56
+ // A pinned first or last stop would otherwise be IGNORED - the shading's domain is always 0..1 and
57
+ // only the INTERIOR offsets reach /Bounds. Carrying the edge colour outwards is what CSS does.
58
+ if (out[0].offset > 0)
59
+ out.unshift({ offset: 0, color: out[0].color });
60
+ if (out[out.length - 1].offset < 1)
61
+ out.push({ offset: 1, color: out[out.length - 1].color });
62
+ return out;
63
+ }
64
+ /**
65
+ * Resolve a box-relative gradient against the box it paints, into the absolute page coordinates the
66
+ * IR wants. Called by the renderer, which is the first place the box's geometry is known.
67
+ *
68
+ * The angle follows CSS: 0 points to the top and it turns clockwise, so 90 goes to the right. The
69
+ * axis runs through the box centre and is scaled so it spans the box on that diagonal - which is why
70
+ * a 45-degree gradient reaches the corners rather than stopping short of them.
71
+ */
72
+ export function resolveGradient(g, x, y, width, height) {
73
+ const stops = toStops(g.stops);
74
+ if (g.kind === "radial") {
75
+ const cx = x + g.center[0] * width;
76
+ const cy = y + g.center[1] * height;
77
+ const r = g.radius * Math.max(width, height);
78
+ return { type: "radial", x0: cx, y0: cy, r0: 0, x1: cx, y1: cy, r1: r, stops, extend: "pad" };
79
+ }
80
+ const rad = ((g.angle % 360) * Math.PI) / 180;
81
+ // CSS angles: 0 = up, clockwise. The engine lays out top-left down, so "up" is -y here.
82
+ const dx = Math.sin(rad);
83
+ const dy = -Math.cos(rad);
84
+ // Half the box's extent along the axis, so the line covers the whole box for any angle.
85
+ const half = (Math.abs(dx) * width + Math.abs(dy) * height) / 2;
86
+ const cx = x + width / 2;
87
+ const cy = y + height / 2;
88
+ return {
89
+ type: "linear",
90
+ x0: cx - dx * half,
91
+ y0: cy - dy * half,
92
+ x1: cx + dx * half,
93
+ y1: cy + dy * half,
94
+ stops,
95
+ extend: "pad",
96
+ };
97
+ }
@@ -1,5 +1,7 @@
1
1
  export * from "./color.js";
2
2
  export * from "./insets.js";
3
+ export * from "./gradient.js";
4
+ export * from "./font-url.js";
3
5
  export * from "./dimension.js";
4
6
  export * from "./text.js";
5
7
  export * from "./layout.js";
package/dist/api/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  // core classes stay untouched and remain exported for power users.
4
4
  export * from "./color.js";
5
5
  export * from "./insets.js";
6
+ export * from "./gradient.js";
7
+ export * from "./font-url.js";
6
8
  export * from "./dimension.js";
7
9
  export * from "./text.js";
8
10
  export * from "./layout.js";
@@ -1,16 +1,23 @@
1
+ import { SizeInput } from "./dimension.js";
2
+ import type { EdgeSpecs } from "../layout/insets.js";
1
3
  /**
2
- * Spacing input for padding / margin (locked design §3). Units are PDF points. Every form
3
- * normalizes to the engine's `[top, right, bottom, left]` via `toEdges`, so you can reach
4
+ * Spacing input for padding / margin (locked design §3). Points, or a percentage string like `"5%"`.
5
+ * Every form normalizes to the engine's `[top, right, bottom, left]` via `toEdges`, so you can reach
4
6
  * for whichever shape fits and never think about the engine order.
7
+ *
8
+ * **A percentage resolves against the available WIDTH - on all four sides, top and bottom included.**
9
+ * That is the CSS rule, and Yoga's, so react-pdf behaves the same. It reads as wrong the first time and
10
+ * is what makes `padding-bottom: 56.25%` a 16:9 box on the web. A percentage needs a bounded width; in
11
+ * an unbounded region it resolves to 0, the same no-op a percentage SIZE gives there.
5
12
  */
6
- export type Insets = number | {
7
- x?: number;
8
- y?: number;
13
+ export type Insets = SizeInput | {
14
+ x?: SizeInput;
15
+ y?: SizeInput;
9
16
  } | {
10
- top?: number;
11
- right?: number;
12
- bottom?: number;
13
- left?: number;
14
- } | [number, number, number, number];
15
- /** Normalizes any `Insets` to the engine's `[top, right, bottom, left]`. */
16
- export declare function toEdges(i: Insets): [number, number, number, number];
17
+ top?: SizeInput;
18
+ right?: SizeInput;
19
+ bottom?: SizeInput;
20
+ left?: SizeInput;
21
+ } | [SizeInput, SizeInput, SizeInput, SizeInput];
22
+ /** Normalizes any `Insets` to the engine's `[top, right, bottom, left]`, percentages still unresolved. */
23
+ export declare function toEdges(i: Insets): EdgeSpecs;
@@ -1,17 +1,28 @@
1
- /** Normalizes any `Insets` to the engine's `[top, right, bottom, left]`. */
1
+ import { toDimension } from "./dimension.js";
2
+ // A fixed side stays a plain NUMBER, so the output is unchanged for every input that was legal before
3
+ // this file learned percentages - `toEdges` is publicly exported and did not need to break.
4
+ const spec = (v) => {
5
+ if (v === undefined)
6
+ return 0;
7
+ const d = toDimension(v);
8
+ return d.points ?? { factor: d.factor };
9
+ };
10
+ /** Normalizes any `Insets` to the engine's `[top, right, bottom, left]`, percentages still unresolved. */
2
11
  export function toEdges(i) {
3
- if (typeof i === "number")
4
- return [i, i, i, i];
12
+ if (typeof i === "number" || typeof i === "string") {
13
+ const s = spec(i);
14
+ return [s, s, s, s];
15
+ }
5
16
  if (Array.isArray(i))
6
- return i;
17
+ return [spec(i[0]), spec(i[1]), spec(i[2]), spec(i[3])];
7
18
  // Axis form ({x, y}) and per-side form ({top,...}) are both all-optional objects (TS
8
19
  // can't discriminate them), so read through one shape: an axis key present picks the
9
20
  // axis interpretation (an empty object is all-zero either way).
10
21
  const o = i;
11
22
  if (o.x !== undefined || o.y !== undefined) {
12
- const x = o.x ?? 0;
13
- const y = o.y ?? 0;
23
+ const x = spec(o.x);
24
+ const y = spec(o.y);
14
25
  return [y, x, y, x];
15
26
  }
16
- return [o.top ?? 0, o.right ?? 0, o.bottom ?? 0, o.left ?? 0];
27
+ return [spec(o.top), spec(o.right), spec(o.bottom), spec(o.left)];
17
28
  }
@@ -11,10 +11,11 @@ import { RotatedBoxElement } from "../elements/layout/rotated-box-element.js";
11
11
  import { PDFElement } from "../elements/pdf-element.js";
12
12
  import { MainAlign, CrossAlign } from "../utils/flex-layout.js";
13
13
  import { ColorInput } from "./color.js";
14
+ import { type GradientInput } from "./gradient.js";
14
15
  import { Insets } from "./insets.js";
15
- import { SizeInput } from "./dimension.js";
16
+ import { BoundsInput, SizeInput, RadiusInput } from "./dimension.js";
16
17
  /** Options shared by the `Column` and `Row` stacks (locked §4). */
17
- export interface StackOptions {
18
+ export interface StackOptions extends BoundsInput {
18
19
  /** Space inserted between children, in points. */
19
20
  gap?: number;
20
21
  /** Distribution along the main axis (CSS `justify-content`): start (default) · center · end ·
@@ -35,6 +36,13 @@ export interface StackOptions {
35
36
  /** Try to keep this stack on one page (CSS `break-inside: avoid`). If it does not fit here but would
36
37
  * fit on a fresh page, it moves there whole; if it is taller than a whole page, it splits anyway. */
37
38
  keepTogether?: boolean;
39
+ /** Lay the children out backwards along the axis (CSS `row-reverse` / `column-reverse`). */
40
+ reverse?: boolean;
41
+ /** Let the children flow onto further lines when they do not fit (CSS `flex-wrap: wrap`). The `gap`
42
+ * is used BETWEEN the lines as well as between the items. */
43
+ wrap?: boolean;
44
+ /** How the block of wrapped lines sits across the axis (CSS `align-content`). Default `start`. */
45
+ alignContent?: MainAlign;
38
46
  }
39
47
  /** A vertical stack. `Column(children)` or `Column(opts, children)`. */
40
48
  export declare function Column(children: PDFElement[]): PDFElement;
@@ -50,7 +58,7 @@ export declare function Row(opts: StackOptions, children: PDFElement[]): PDFElem
50
58
  */
51
59
  export declare function keepTogether(children: PDFElement[]): KeepTogetherElement;
52
60
  /** A bordered / filled box that wraps its children (locked §4). */
53
- export interface BoxOptions {
61
+ export interface BoxOptions extends BoundsInput {
54
62
  /** Border (stroke) color. A box has a border only when `border` or `borderWidth` is set. */
55
63
  border?: ColorInput;
56
64
  /** Per-side border colors - override/add to `border`. Any of these makes the box draw
@@ -62,8 +70,8 @@ export interface BoxOptions {
62
70
  borderLeft?: ColorInput;
63
71
  /** Border thickness in points (default 1 when a border is present). */
64
72
  borderWidth?: number;
65
- /** Background fill color. */
66
- bg?: ColorInput;
73
+ /** Background: a colour, or a gradient from `linearGradient()` / `radialGradient()`. */
74
+ bg?: ColorInput | GradientInput;
67
75
  /** Inner padding between the border and the children. */
68
76
  padding?: Insets;
69
77
  /** Size on each axis: a number of points (fixed) or a percentage string like `"50%"` (a fraction
@@ -72,7 +80,7 @@ export interface BoxOptions {
72
80
  width?: SizeInput;
73
81
  height?: SizeInput;
74
82
  /** Corner radius in points. */
75
- radius?: number;
83
+ radius?: RadiusInput;
76
84
  /** Make this box a positioning frame: `Positioned` children placed inside it resolve their
77
85
  * offsets against this box (CSS `position: relative`). */
78
86
  relative?: boolean;
@@ -173,6 +181,12 @@ export declare function PageBreak(): PageBreakElement;
173
181
  export interface ExpandedOptions {
174
182
  /** Share of the leftover space vs other flex children (default 1). */
175
183
  flex?: number;
184
+ /**
185
+ * Main-axis size reserved BEFORE the leftover is shared out (CSS `flex-basis`): points, or a
186
+ * percentage of what the line offers its items. The child ends up with `basis + its share of the
187
+ * rest`, so `flexBasis: 120` with `flex: 0` is simply a fixed 120pt slot. Default 0.
188
+ */
189
+ flexBasis?: SizeInput;
176
190
  }
177
191
  /**
178
192
  * Makes a child fill the leftover space along the stack's main axis (height in a Column,
@@ -12,14 +12,21 @@ import { AnchorElement } from "../elements/layout/anchor-element.js";
12
12
  import { RotatedElement } from "../elements/layout/rotated-element.js";
13
13
  import { RotatedBoxElement } from "../elements/layout/rotated-box-element.js";
14
14
  import { toColor } from "./color.js";
15
+ import { isGradientInput } from "./gradient.js";
15
16
  import { toEdges } from "./insets.js";
16
- import { toDimension } from "./dimension.js";
17
+ import { toBounds, toDimension, toRadius, } from "./dimension.js";
17
18
  import { splitArgs } from "./args.js";
18
19
  /** Splits `StackOptions` width/height into the fixed points + fraction the stack elements expect. */
19
20
  function stackSize(opts) {
20
21
  const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
21
22
  const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
22
- return { width: w?.points, height: h?.points, widthFactor: w?.factor, heightFactor: h?.factor };
23
+ return {
24
+ width: w?.points,
25
+ height: h?.points,
26
+ widthFactor: w?.factor,
27
+ heightFactor: h?.factor,
28
+ ...toBounds(opts),
29
+ };
23
30
  }
24
31
  // The public default for `cross` is `start` (locked §5) - i.e. don't stretch a child unless
25
32
  // asked. The engine default is `stretch`; the factory sets the friendlier public one.
@@ -33,6 +40,9 @@ export function Column(a, b) {
33
40
  gap: opts.gap,
34
41
  main: opts.justify, // undefined → engine default `start` (matches §5)
35
42
  cross: opts.align ?? DEFAULT_CROSS,
43
+ reverse: opts.reverse,
44
+ wrap: opts.wrap,
45
+ alignContent: opts.alignContent,
36
46
  breakBefore: opts.breakBefore,
37
47
  breakAfter: opts.breakAfter,
38
48
  ...stackSize(opts),
@@ -45,6 +55,9 @@ export function Row(a, b) {
45
55
  gap: opts.gap,
46
56
  main: opts.justify,
47
57
  cross: opts.align ?? DEFAULT_CROSS,
58
+ reverse: opts.reverse,
59
+ wrap: opts.wrap,
60
+ alignContent: opts.alignContent,
48
61
  breakBefore: opts.breakBefore,
49
62
  breakAfter: opts.breakAfter,
50
63
  ...stackSize(opts),
@@ -53,7 +66,12 @@ export function Row(a, b) {
53
66
  /** Wraps `element` in a `keepTogether` group when the `keepTogether` option is set; otherwise returns it
54
67
  * unchanged (byte-identical). Shared by the `Box`/`Column`/`Row` prop shortcut. */
55
68
  function maybeKeepTogether(opts, element) {
56
- return opts.keepTogether ? new KeepTogetherElement({ child: element }) : element;
69
+ // alignSelf and order go on the OUTERMOST element - that is the one the surrounding stack places.
70
+ const wrapped = opts.keepTogether ? new KeepTogetherElement({ child: element }) : element;
71
+ return wrapped
72
+ .withAlignSelf(opts.alignSelf)
73
+ .withOrder(opts.order)
74
+ .withFlexShrink(opts.flexShrink);
57
75
  }
58
76
  /**
59
77
  * Keep a group of elements on ONE page (CSS `break-inside: avoid`). `keepTogether([...])` stacks the
@@ -92,13 +110,14 @@ export function Box(a, b) {
92
110
  y: 0,
93
111
  children: content,
94
112
  color: opts.border !== undefined ? toColor(opts.border) : undefined,
95
- backgroundColor: opts.bg !== undefined ? toColor(opts.bg) : undefined,
113
+ backgroundColor: opts.bg === undefined ? undefined : isGradientInput(opts.bg) ? opts.bg : toColor(opts.bg),
96
114
  borderWidth: hasBorder ? (opts.borderWidth ?? 1) : 0,
97
115
  width: w?.points,
98
116
  height: h?.points,
99
117
  widthFactor: w?.factor,
100
118
  heightFactor: h?.factor,
101
- radius: opts.radius,
119
+ ...toBounds(opts),
120
+ radius: opts.radius !== undefined ? toRadius(opts.radius) : undefined,
102
121
  sideBorders: hasPerSide
103
122
  ? {
104
123
  top: side(opts.borderTop),
@@ -201,5 +220,11 @@ export function Expanded(a, b) {
201
220
  const isOptsForm = b !== undefined;
202
221
  const opts = (isOptsForm ? a : {});
203
222
  const child = (isOptsForm ? b : a);
204
- return new ExpandedElement({ flex: opts.flex ?? 1, child });
223
+ const basis = opts.flexBasis !== undefined ? toDimension(opts.flexBasis) : undefined;
224
+ return new ExpandedElement({
225
+ flex: opts.flex ?? 1,
226
+ basis: basis?.points,
227
+ basisFactor: basis?.factor,
228
+ child,
229
+ });
205
230
  }
@@ -16,6 +16,10 @@ export type { PageInfo };
16
16
  * `pageNumber` is 1-based and counts PHYSICAL pages (an overflowing `Page` contributes several);
17
17
  * `pageCount` is the document total; `pageSize` is the media box in points.
18
18
  *
19
+ * `subPageNumber` / `subPageTotalPages` are the same counts WITHIN one logical `Page` element - what you
20
+ * want when a document is several sections and the footer should say "Attachment, page 1 of 4" beside
21
+ * "sheet 4 of 7". With a single `Page` they are identical to the document counts.
22
+ *
19
23
  * The numbers themselves are always exact. Two sizing caveats, both from the same chicken-and-egg (the
20
24
  * content decides the count that the content displays): in the flowing BODY the box is reserved before the
21
25
  * total is known, so a much wider final string can paint slightly past it; and a conditional header may
@@ -30,3 +34,7 @@ export interface PageNumberOptions extends TextOptions {
30
34
  export declare function PageNumber({ offset, ...style }?: PageNumberOptions): PageBuilderElement;
31
35
  /** The document's total page count as text. Sugar for `PageBuilder`. */
32
36
  export declare function PageCount({ offset, ...style }?: PageNumberOptions): PageBuilderElement;
37
+ /** The page number WITHIN its logical `Page` (a section), as text. Sugar for `PageBuilder`. */
38
+ export declare function SubPageNumber({ offset, ...style }?: PageNumberOptions): PageBuilderElement;
39
+ /** How many pages the surrounding logical `Page` produced, as text. Sugar for `PageBuilder`. */
40
+ export declare function SubPageCount({ offset, ...style }?: PageNumberOptions): PageBuilderElement;
@@ -14,6 +14,10 @@ import { Text } from "./text.js";
14
14
  * `pageNumber` is 1-based and counts PHYSICAL pages (an overflowing `Page` contributes several);
15
15
  * `pageCount` is the document total; `pageSize` is the media box in points.
16
16
  *
17
+ * `subPageNumber` / `subPageTotalPages` are the same counts WITHIN one logical `Page` element - what you
18
+ * want when a document is several sections and the footer should say "Attachment, page 1 of 4" beside
19
+ * "sheet 4 of 7". With a single `Page` they are identical to the document counts.
20
+ *
17
21
  * The numbers themselves are always exact. Two sizing caveats, both from the same chicken-and-egg (the
18
22
  * content decides the count that the content displays): in the flowing BODY the box is reserved before the
19
23
  * total is known, so a much wider final string can paint slightly past it; and a conditional header may
@@ -30,3 +34,11 @@ export function PageNumber({ offset = 0, ...style } = {}) {
30
34
  export function PageCount({ offset = 0, ...style } = {}) {
31
35
  return PageBuilder(({ pageCount }) => Text(String(pageCount + offset), style));
32
36
  }
37
+ /** The page number WITHIN its logical `Page` (a section), as text. Sugar for `PageBuilder`. */
38
+ export function SubPageNumber({ offset = 0, ...style } = {}) {
39
+ return PageBuilder(({ subPageNumber }) => Text(String(subPageNumber + offset), style));
40
+ }
41
+ /** How many pages the surrounding logical `Page` produced, as text. Sugar for `PageBuilder`. */
42
+ export function SubPageCount({ offset = 0, ...style } = {}) {
43
+ return PageBuilder(({ subPageTotalPages }) => Text(String(subPageTotalPages + offset), style));
44
+ }
@@ -7,6 +7,7 @@ import { PageSize } from "../constants/page-sizes.js";
7
7
  import { type EncryptOptions } from "../crypto/security-handler.js";
8
8
  export type { EncryptOptions, Permissions } from "../crypto/security-handler.js";
9
9
  import { StackOptions } from "./layout.js";
10
+ import { type UrlFontSource } from "./font-url.js";
10
11
  import { Insets } from "./insets.js";
11
12
  import { TextDefaults } from "./text.js";
12
13
  /** A custom page size. Use the `mm()` helper for millimetres, or pass points directly. */
@@ -78,6 +79,10 @@ export interface JasyDocument extends PDFDocumentElement {
78
79
  * path (read now, on Node), raw bytes, or a styled family. Re-adding a name overwrites it.
79
80
  * A registered font that no `Text` actually uses is dropped at render and costs nothing. */
80
81
  addFont(name: string, source: FontSource): this;
82
+ /** The same, from a URL. Async because the fetch happens NOW, exactly as `addFont` reads a path
83
+ * now - so a broken link fails where you asked for the font, not inside a later render.
84
+ * `await doc.addFontFromUrl("Inter", "https://…/Inter-Regular.ttf")`, or a styled family of URLs. */
85
+ addFontFromUrl(name: string, source: UrlFontSource): Promise<this>;
81
86
  /** The names of the registered fonts. */
82
87
  getFonts(): string[];
83
88
  /** Whether a font is registered under `name`. */
@@ -146,6 +151,13 @@ export interface RenderOptions {
146
151
  /** What to do when content is taller than a page and cannot break: `"error"` throws (default),
147
152
  * `"warn"` logs and clips, `"ignore"` clips silently. It is always clipped either way. */
148
153
  onOverflow?: OverflowPolicy;
154
+ /**
155
+ * Called once, after the render, with the characters no font could draw - they were removed rather
156
+ * than emitted as `.notdef`, which PDF/A forbids. Deliberately a callback and not a policy: a
157
+ * missing tick mark must never stop an invoice from being produced, but the application should be
158
+ * able to SAY which character went missing, where a server log would never be read.
159
+ */
160
+ onMissingGlyphs?: (characters: string[]) => void;
149
161
  /** Encrypt the PDF with a password (AES-256, the newest standard). NOT compatible with PDF/A
150
162
  * (ZUGFeRD invoices) - encrypting one throws, since PDF/A forbids encryption. */
151
163
  encrypt?: EncryptOptions;
@@ -2,7 +2,8 @@ import { readFileBytes } from "../platform/node-fs.js";
2
2
  import { PDFDocumentElement } from "../elements/pdf-document-element.js";
3
3
  import { PageElement } from "../elements/page-element.js";
4
4
  import { DefaultTextStyleElement } from "../elements/layout/default-text-style-element.js";
5
- import { PageSize } from "../constants/page-sizes.js";
5
+ import { PageSize, pageFormats } from "../constants/page-sizes.js";
6
+ import { resolveEdges } from "../layout/insets.js";
6
7
  import { Orientation } from "../renderer/pdf-config.js";
7
8
  import { PDFDocument } from "../renderer/pdf-document-class.js";
8
9
  import { FontStyle } from "../utils/pdf-object-manager.js";
@@ -10,8 +11,10 @@ import { getArrayBuffer } from "../utils/utf8-to-windows1252-encoder.js";
10
11
  import { createSecurityHandler } from "../crypto/security-handler.js";
11
12
  import { uaXmp } from "../utils/ua-xmp.js";
12
13
  import { Column } from "./layout.js";
14
+ import { loadFontFromUrl } from "./font-url.js";
13
15
  import { toEdges } from "./insets.js";
14
16
  import { toTextStyleOverride } from "./text.js";
17
+ import { isWoff2, woff2ToSfnt } from "../utils/woff2.js";
15
18
  const MM_TO_PT = 72 / 25.4; // 1 mm in PDF points
16
19
  /** Millimetres → a custom page size, e.g. a 50×65 mm label: `Page({ size: mm(50, 65) }, …)`. */
17
20
  export function mm(width, height) {
@@ -38,10 +41,16 @@ export function Page(a, b) {
38
41
  const isOpts = !Array.isArray(a);
39
42
  const opts = (isOpts ? a : {});
40
43
  const children = (isOpts ? (b ?? []) : a);
41
- const [top, right, bottom, left] = toEdges(opts.margin ?? DEFAULT_MARGIN);
44
+ const size = resolveSize(opts.size);
45
+ const landscape = opts.orientation === "landscape";
46
+ // A percentage margin resolves against the page WIDTH (see `Insets`), which is known right here -
47
+ // the page is the one box whose geometry does not depend on a parent.
48
+ const [w, h] = size.customSize ?? pageFormats[size.pageSize ?? PageSize.A4];
49
+ const pageWidth = landscape ? h : w;
50
+ const [top, right, bottom, left] = resolveEdges(toEdges(opts.margin ?? DEFAULT_MARGIN), pageWidth);
42
51
  const config = {
43
- ...resolveSize(opts.size),
44
- orientation: opts.orientation === "landscape" ? Orientation.landscape : Orientation.portrait,
52
+ ...size,
53
+ orientation: landscape ? Orientation.landscape : Orientation.portrait,
45
54
  margin: { top, right, bottom, left },
46
55
  };
47
56
  return new PageElement({
@@ -59,6 +68,29 @@ const docMeta = new WeakMap();
59
68
  const docFonts = new WeakMap();
60
69
  // The document's color-emoji fallback source, kept beside the element and applied at render time.
61
70
  const docEmoji = new WeakMap();
71
+ /**
72
+ * Unwraps a WOFF2 into plain sfnt bytes. WOFF1 is left alone - `registerCustomFont` handles it
73
+ * synchronously, having no dependency to load - so only the async case needs to happen up here.
74
+ */
75
+ async function unwrapFontContainers(fonts) {
76
+ const convert = async (bytes) => isWoff2(bytes) ? await woff2ToSfnt(bytes) : bytes;
77
+ const out = {};
78
+ for (const [name, value] of Object.entries(fonts)) {
79
+ if (isFontBytes(value)) {
80
+ out[name] = await convert(value);
81
+ continue;
82
+ }
83
+ const family = { normal: await convert(value.normal) };
84
+ if (value.bold)
85
+ family.bold = await convert(value.bold);
86
+ if (value.italic)
87
+ family.italic = await convert(value.italic);
88
+ if (value.boldItalic)
89
+ family.boldItalic = await convert(value.boldItalic);
90
+ out[name] = family;
91
+ }
92
+ return out;
93
+ }
62
94
  /** Reads any path sources to bytes, leaving bytes / families as-is. */
63
95
  function resolveFontSource(source) {
64
96
  const read = (s) => (typeof s === "string" ? readFileBytes(s) : s);
@@ -93,6 +125,10 @@ export function Document(a, b) {
93
125
  registry.set(name, resolveFontSource(source));
94
126
  return doc;
95
127
  };
128
+ doc.addFontFromUrl = async (name, source) => {
129
+ registry.set(name, await loadFontFromUrl(source));
130
+ return doc;
131
+ };
96
132
  doc.getFonts = () => [...registry.keys()];
97
133
  doc.hasFont = (name) => registry.has(name);
98
134
  return doc;
@@ -118,10 +154,13 @@ export async function renderPdf(doc, options) {
118
154
  };
119
155
  // Fonts registered via doc.addFont(...) plus any passed in options (options win on a name clash).
120
156
  const registered = docFonts.get(doc);
121
- const fonts = {
157
+ // Containers are unwrapped HERE, before the (synchronous) document constructor registers anything.
158
+ // WOFF2 needs Brotli, which is imported lazily and is therefore async - and this is the last async
159
+ // point every font passes through, whether it came from `addFont`, a URL, or these options.
160
+ const fonts = await unwrapFontContainers({
122
161
  ...Object.fromEntries(registered ?? []),
123
162
  ...options?.fonts,
124
- };
163
+ });
125
164
  const attachments = options?.attachments ?? [];
126
165
  // The security handler's key derivation is async (WebCrypto), so build it here, before the sync
127
166
  // PDFDocument constructor wires it into the object manager.
@@ -131,10 +170,14 @@ export async function renderPdf(doc, options) {
131
170
  // A throwaway PDFDocument whose build() yields this tree, reusing the engine's standard
132
171
  // font registration + config handling (the constructor does both). Custom fonts are
133
172
  // registered here, before layout/render, so both the metrics and the backend see them.
173
+ // Captured so the caller can be told what had to be dropped: `render()` is static and builds its own
174
+ // instance, so this is the only place the object manager surfaces.
175
+ let manager;
134
176
  const Anon = class extends PDFDocument {
135
177
  constructor() {
136
178
  super(config);
137
179
  const om = this.objectManager;
180
+ manager = om;
138
181
  om.setCompress(options?.compress !== false); // FlateDecode streams by default
139
182
  om.setOverflowPolicy(options?.onOverflow ?? "error");
140
183
  om.setKerning(options?.kerning !== false); // on by default (experimental); `kerning:false` opts out
@@ -195,7 +238,11 @@ export async function renderPdf(doc, options) {
195
238
  return doc;
196
239
  }
197
240
  };
198
- return Anon.render();
241
+ const pdf = await Anon.render();
242
+ const missing = manager?.getMissingGlyphs() ?? [];
243
+ if (missing.length > 0)
244
+ options?.onMissingGlyphs?.(missing.map((cp) => String.fromCodePoint(cp)));
245
+ return pdf;
199
246
  }
200
247
  /** Renders a `Document(...)` tree to PDF bytes (e.g. for a download / save dialog). */
201
248
  export async function renderToBytes(doc, options) {