@jasy/pdf 1.0.0-alpha.4 → 1.0.0-alpha.6

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 (96) hide show
  1. package/README.md +5 -2
  2. package/dist/api/content.d.ts +9 -3
  3. package/dist/api/content.js +13 -3
  4. package/dist/api/descriptor.js +22 -1
  5. package/dist/api/dimension.d.ts +20 -0
  6. package/dist/api/dimension.js +16 -0
  7. package/dist/api/index.d.ts +2 -0
  8. package/dist/api/index.js +2 -0
  9. package/dist/api/layout.d.ts +65 -3
  10. package/dist/api/layout.js +71 -2
  11. package/dist/api/page-builder.d.ts +32 -0
  12. package/dist/api/page-builder.js +32 -0
  13. package/dist/api/structure.d.ts +19 -0
  14. package/dist/api/structure.js +25 -0
  15. package/dist/api/table.js +13 -5
  16. package/dist/api/text.d.ts +10 -1
  17. package/dist/api/text.js +32 -4
  18. package/dist/elements/container-element.d.ts +17 -1
  19. package/dist/elements/container-element.js +54 -22
  20. package/dist/elements/image-element.d.ts +23 -1
  21. package/dist/elements/image-element.js +54 -11
  22. package/dist/elements/index.d.ts +6 -0
  23. package/dist/elements/index.js +6 -0
  24. package/dist/elements/layout/anchor-element.d.ts +23 -0
  25. package/dist/elements/layout/anchor-element.js +27 -0
  26. package/dist/elements/layout/bookmark-element.d.ts +27 -0
  27. package/dist/elements/layout/bookmark-element.js +30 -0
  28. package/dist/elements/layout/link-element.d.ts +33 -0
  29. package/dist/elements/layout/link-element.js +39 -0
  30. package/dist/elements/layout/page-builder-element.d.ts +32 -0
  31. package/dist/elements/layout/page-builder-element.js +43 -0
  32. package/dist/elements/layout/rotated-box-element.d.ts +35 -0
  33. package/dist/elements/layout/rotated-box-element.js +54 -0
  34. package/dist/elements/layout/rotated-element.d.ts +32 -0
  35. package/dist/elements/layout/rotated-element.js +41 -0
  36. package/dist/elements/layout/struct-group.d.ts +28 -0
  37. package/dist/elements/layout/struct-group.js +42 -0
  38. package/dist/elements/page-element.d.ts +8 -0
  39. package/dist/elements/page-element.js +13 -1
  40. package/dist/elements/pdf-element.d.ts +35 -0
  41. package/dist/elements/pdf-element.js +34 -0
  42. package/dist/elements/rectangle-element.d.ts +11 -1
  43. package/dist/elements/rectangle-element.js +49 -13
  44. package/dist/elements/row-element.d.ts +11 -1
  45. package/dist/elements/row-element.js +37 -8
  46. package/dist/elements/text-element.d.ts +11 -1
  47. package/dist/elements/text-element.js +5 -2
  48. package/dist/ir/display-list.d.ts +137 -1
  49. package/dist/layout/box-constraints.d.ts +8 -0
  50. package/dist/layout/box-constraints.js +14 -0
  51. package/dist/layout/collect-images.d.ts +9 -0
  52. package/dist/layout/collect-images.js +34 -0
  53. package/dist/layout/fragmentation.js +9 -2
  54. package/dist/renderer/anchor-renderer.d.ts +6 -0
  55. package/dist/renderer/anchor-renderer.js +11 -0
  56. package/dist/renderer/bookmark-renderer.d.ts +6 -0
  57. package/dist/renderer/bookmark-renderer.js +11 -0
  58. package/dist/renderer/emoji-image.d.ts +9 -0
  59. package/dist/renderer/emoji-image.js +68 -0
  60. package/dist/renderer/image-renderer.d.ts +1 -1
  61. package/dist/renderer/image-renderer.js +10 -2
  62. package/dist/renderer/link-renderer.d.ts +6 -0
  63. package/dist/renderer/link-renderer.js +11 -0
  64. package/dist/renderer/page-builder-renderer.d.ts +6 -0
  65. package/dist/renderer/page-builder-renderer.js +11 -0
  66. package/dist/renderer/page-renderer.js +54 -4
  67. package/dist/renderer/pdf-backend.d.ts +16 -2
  68. package/dist/renderer/pdf-backend.js +135 -3
  69. package/dist/renderer/pdf-document-renderer.d.ts +16 -7
  70. package/dist/renderer/pdf-document-renderer.js +65 -33
  71. package/dist/renderer/pdf-renderer.js +58 -2
  72. package/dist/renderer/rotated-renderer.d.ts +7 -0
  73. package/dist/renderer/rotated-renderer.js +29 -0
  74. package/dist/renderer/struct-group-renderer.d.ts +10 -0
  75. package/dist/renderer/struct-group-renderer.js +24 -0
  76. package/dist/renderer/text-renderer.d.ts +3 -0
  77. package/dist/renderer/text-renderer.js +206 -5
  78. package/dist/text/emoji-codepoints.d.ts +1 -0
  79. package/dist/text/emoji-codepoints.js +29 -0
  80. package/dist/text/line-breaker.js +6 -3
  81. package/dist/utils/afm-parser.js +6 -4
  82. package/dist/utils/dest-registry.d.ts +19 -0
  83. package/dist/utils/dest-registry.js +32 -0
  84. package/dist/utils/flex-layout.d.ts +10 -3
  85. package/dist/utils/flex-layout.js +26 -6
  86. package/dist/utils/outline.d.ts +21 -0
  87. package/dist/utils/outline.js +80 -0
  88. package/dist/utils/pdf-object-manager.d.ts +26 -1
  89. package/dist/utils/pdf-object-manager.js +0 -0
  90. package/dist/utils/struct-tree.d.ts +44 -0
  91. package/dist/utils/struct-tree.js +130 -0
  92. package/dist/utils/ttf-parser.d.ts +73 -0
  93. package/dist/utils/ttf-parser.js +456 -16
  94. package/dist/utils/ua-xmp.d.ts +9 -0
  95. package/dist/utils/ua-xmp.js +32 -0
  96. package/package.json +1 -1
@@ -0,0 +1,32 @@
1
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
2
+ import { LayoutContext, PageInfo, PDFElement } from "../pdf-element.js";
3
+ /**
4
+ * Builds its subtree from the page it lands on, so a header, a footer or plain body content can show
5
+ * `pageNumber` / `pageCount`. The closure runs on EVERY layout, because the numbers differ between the
6
+ * two driver passes: pagination has to size the element before the page total exists, so it builds
7
+ * against a provisional "1 of 1"; the render pass then rebuilds it with the real `PageInfo`.
8
+ *
9
+ * Two consequences, and neither is magic - both come from the same chicken-and-egg: the content decides
10
+ * the page count that the content displays.
11
+ *
12
+ * 1. In the flowing BODY the box is reserved by the provisional build, so a much wider final string
13
+ * (`"9 of 10"` vs `"1 of 1"`) can paint slightly past it. Keep dynamic body content roughly constant
14
+ * in width.
15
+ * 2. A header/footer is re-laid out per page, so its real height is honoured - but the BODY band was
16
+ * already sized against the provisional build. A later page whose header is SHORTER simply gains
17
+ * room (harmless). A later page whose header is TALLER shrinks the band its body was measured for,
18
+ * and that body can overflow. So: a conditional header may shrink on later pages, never grow.
19
+ */
20
+ export declare class PageBuilderElement extends PDFElement {
21
+ private composed?;
22
+ private readonly build;
23
+ constructor({ build }: {
24
+ build: (info: PageInfo) => PDFElement;
25
+ });
26
+ /** The real page info during the render pass; a provisional "1 of 1" while paginating. */
27
+ private infoFrom;
28
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
29
+ getProps(): {
30
+ composed: PDFElement | undefined;
31
+ };
32
+ }
@@ -0,0 +1,43 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ import { resolvePageSize } from "../page-element.js";
3
+ /**
4
+ * Builds its subtree from the page it lands on, so a header, a footer or plain body content can show
5
+ * `pageNumber` / `pageCount`. The closure runs on EVERY layout, because the numbers differ between the
6
+ * two driver passes: pagination has to size the element before the page total exists, so it builds
7
+ * against a provisional "1 of 1"; the render pass then rebuilds it with the real `PageInfo`.
8
+ *
9
+ * Two consequences, and neither is magic - both come from the same chicken-and-egg: the content decides
10
+ * the page count that the content displays.
11
+ *
12
+ * 1. In the flowing BODY the box is reserved by the provisional build, so a much wider final string
13
+ * (`"9 of 10"` vs `"1 of 1"`) can paint slightly past it. Keep dynamic body content roughly constant
14
+ * in width.
15
+ * 2. A header/footer is re-laid out per page, so its real height is honoured - but the BODY band was
16
+ * already sized against the provisional build. A later page whose header is SHORTER simply gains
17
+ * room (harmless). A later page whose header is TALLER shrinks the band its body was measured for,
18
+ * and that body can overflow. So: a conditional header may shrink on later pages, never grow.
19
+ */
20
+ export class PageBuilderElement extends PDFElement {
21
+ constructor({ build }) {
22
+ super();
23
+ this.build = build;
24
+ }
25
+ /** The real page info during the render pass; a provisional "1 of 1" while paginating. */
26
+ infoFrom(ctx) {
27
+ return (ctx.pageInfo ?? {
28
+ pageNumber: 1,
29
+ pageCount: 1,
30
+ pageSize: resolvePageSize(ctx.pageConfig),
31
+ });
32
+ }
33
+ calculateLayout(constraints, offset, ctx) {
34
+ this.composed = this.build(this.infoFrom(ctx));
35
+ return this.composed.calculateLayout(constraints, offset, ctx);
36
+ }
37
+ // Named `composed` (not `child`) like DeferredElement: the pre-layout image walk keys on `child`, and
38
+ // this subtree does not exist yet when it runs. So an Image inside a PageBuilder skips aspect-ratio
39
+ // pre-resolution - give it both width and height, exactly as inside a Deferred/Table subtree.
40
+ getProps() {
41
+ return { composed: this.composed };
42
+ }
43
+ }
@@ -0,0 +1,35 @@
1
+ import { PDFElement, LayoutContext } from "../pdf-element.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ /**
4
+ * Rotates its child by whole quarter-turns and, unlike `RotatedElement` (paint-only), makes the
5
+ * rotation LAYOUT-AWARE: for a 90 / 270 turn it SWAPS width and height, so the box reports the rotated
6
+ * footprint UP and siblings reflow around it. A tall text becomes a narrow vertical strip that actually
7
+ * reserves its narrow width and its long height - the "vertical label beside a table" case.
8
+ *
9
+ * `turns` is the number of 90-degree CLOCKWISE quarter-turns (like Flutter's `RotatedBox.quarterTurns`):
10
+ * 0 = none, 1 = 90, 2 = 180, 3 = 270. The child lays out with the box's constraints swapped, then it is
11
+ * placed centered on the box's center so a plain center-rotation (the shared renderer) maps it exactly
12
+ * onto the swapped box.
13
+ */
14
+ export declare class RotatedBoxElement extends PDFElement {
15
+ private turns;
16
+ private child;
17
+ private x;
18
+ private y;
19
+ private width;
20
+ private height;
21
+ constructor({ turns, child }: {
22
+ turns: number;
23
+ child: PDFElement;
24
+ });
25
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
26
+ /** Same shape as `RotatedElement` (angle + box), so both share `RotatedRenderer`. */
27
+ getProps(): {
28
+ angle: number;
29
+ child: PDFElement;
30
+ x: number;
31
+ y: number;
32
+ width: number;
33
+ height: number;
34
+ };
35
+ }
@@ -0,0 +1,54 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ import { BoxConstraints } from "../../layout/box-constraints.js";
3
+ /**
4
+ * Rotates its child by whole quarter-turns and, unlike `RotatedElement` (paint-only), makes the
5
+ * rotation LAYOUT-AWARE: for a 90 / 270 turn it SWAPS width and height, so the box reports the rotated
6
+ * footprint UP and siblings reflow around it. A tall text becomes a narrow vertical strip that actually
7
+ * reserves its narrow width and its long height - the "vertical label beside a table" case.
8
+ *
9
+ * `turns` is the number of 90-degree CLOCKWISE quarter-turns (like Flutter's `RotatedBox.quarterTurns`):
10
+ * 0 = none, 1 = 90, 2 = 180, 3 = 270. The child lays out with the box's constraints swapped, then it is
11
+ * placed centered on the box's center so a plain center-rotation (the shared renderer) maps it exactly
12
+ * onto the swapped box.
13
+ */
14
+ export class RotatedBoxElement extends PDFElement {
15
+ constructor({ turns, child }) {
16
+ super();
17
+ this.x = 0;
18
+ this.y = 0;
19
+ this.width = 0;
20
+ this.height = 0;
21
+ this.turns = ((Math.trunc(turns) % 4) + 4) % 4;
22
+ this.child = child;
23
+ }
24
+ calculateLayout(constraints, offset, ctx) {
25
+ const quarter = this.turns % 2 === 1; // 90 or 270: axes swap
26
+ // A quarter-turned child experiences the box's constraints with the axes swapped.
27
+ const childConstraints = quarter
28
+ ? new BoxConstraints(constraints.minHeight, constraints.maxHeight, constraints.minWidth, constraints.maxWidth)
29
+ : constraints;
30
+ // Measure the child to learn its natural size, then take that size swapped as our own box.
31
+ const child = this.child.calculateLayout(childConstraints, offset, ctx);
32
+ this.width = quarter ? child.height : child.width;
33
+ this.height = quarter ? child.width : child.height;
34
+ this.x = offset.x;
35
+ this.y = offset.y;
36
+ // Re-place the child CENTERED on the box center: a rotation around that center then maps the
37
+ // child's (w x h) box exactly onto our (swapped) box - so the shared renderer needs no special case.
38
+ const cx = this.x + this.width / 2;
39
+ const cy = this.y + this.height / 2;
40
+ this.child.calculateLayout(childConstraints, { x: cx - child.width / 2, y: cy - child.height / 2 }, ctx);
41
+ return { width: this.width, height: this.height };
42
+ }
43
+ /** Same shape as `RotatedElement` (angle + box), so both share `RotatedRenderer`. */
44
+ getProps() {
45
+ return {
46
+ angle: this.turns * 90,
47
+ child: this.child,
48
+ x: this.x,
49
+ y: this.y,
50
+ width: this.width,
51
+ height: this.height,
52
+ };
53
+ }
54
+ }
@@ -0,0 +1,32 @@
1
+ import { PDFElement, LayoutContext } from "../pdf-element.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ /**
4
+ * Rotates its single child at PAINT time only (like Flutter's `Transform.rotate`): the child lays out
5
+ * in its normal, axis-aligned box and the element reports that same box UP, so siblings never reflow
6
+ * around the rotated shape. Only the drawing is rotated, around the box's center, by the renderer.
7
+ *
8
+ * Layout-transparent: it delegates `calculateLayout` to the child and just records the resulting box,
9
+ * which the renderer needs as the pivot. Not `Fragmentable` on purpose - a rotated subtree is atomic
10
+ * w.r.t. pagination (it moves whole to the next page rather than splitting mid-rotation).
11
+ */
12
+ export declare class RotatedElement extends PDFElement {
13
+ private angle;
14
+ private child;
15
+ private x;
16
+ private y;
17
+ private width;
18
+ private height;
19
+ constructor({ angle, child }: {
20
+ angle: number;
21
+ child: PDFElement;
22
+ });
23
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
24
+ getProps(): {
25
+ angle: number;
26
+ child: PDFElement;
27
+ x: number;
28
+ y: number;
29
+ width: number;
30
+ height: number;
31
+ };
32
+ }
@@ -0,0 +1,41 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ /**
3
+ * Rotates its single child at PAINT time only (like Flutter's `Transform.rotate`): the child lays out
4
+ * in its normal, axis-aligned box and the element reports that same box UP, so siblings never reflow
5
+ * around the rotated shape. Only the drawing is rotated, around the box's center, by the renderer.
6
+ *
7
+ * Layout-transparent: it delegates `calculateLayout` to the child and just records the resulting box,
8
+ * which the renderer needs as the pivot. Not `Fragmentable` on purpose - a rotated subtree is atomic
9
+ * w.r.t. pagination (it moves whole to the next page rather than splitting mid-rotation).
10
+ */
11
+ export class RotatedElement extends PDFElement {
12
+ constructor({ angle, child }) {
13
+ super();
14
+ this.x = 0;
15
+ this.y = 0;
16
+ this.width = 0;
17
+ this.height = 0;
18
+ this.angle = angle;
19
+ this.child = child;
20
+ }
21
+ calculateLayout(constraints, offset, ctx) {
22
+ // Delegate entirely to the child, then record its box (position + size) as our own - the
23
+ // rotation does not change what space we take, only how the child is painted.
24
+ const size = this.child.calculateLayout(constraints, offset, ctx);
25
+ this.x = offset.x;
26
+ this.y = offset.y;
27
+ this.width = size.width;
28
+ this.height = size.height;
29
+ return size;
30
+ }
31
+ getProps() {
32
+ return {
33
+ angle: this.angle,
34
+ child: this.child,
35
+ x: this.x,
36
+ y: this.y,
37
+ width: this.width,
38
+ height: this.height,
39
+ };
40
+ }
41
+ }
@@ -0,0 +1,28 @@
1
+ import { PDFElement, LayoutContext } from "../pdf-element.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ import { Fragmentable, FragmentResult } from "../../layout/fragmentation.js";
4
+ /**
5
+ * A layout-TRANSPARENT wrapper that assigns an accessibility structure role (Table / TR / TH / TD / …) to
6
+ * its single child. It delegates ALL layout and fragmentation to the child, so the visual result is byte-
7
+ * identical; only its renderer opens a structure element and nests the child's tags underneath.
8
+ *
9
+ * Split across pages, the fitted and remainder halves share this group's `structId`, so it stays ONE
10
+ * logical structure element (a table broken over pages is a single <Table>, exactly as Acrobat produces).
11
+ */
12
+ export declare class StructGroup extends PDFElement implements Fragmentable {
13
+ private role;
14
+ private child;
15
+ constructor({ role, child }: {
16
+ role: string;
17
+ child: PDFElement;
18
+ });
19
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
20
+ /** Only splittable when the wrapped child is - so a StructGroup(Row) is moved whole to the next page,
21
+ * not clipped at the boundary (isFragmentable honours this veto). */
22
+ canFragment(): boolean;
23
+ fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
24
+ getProps(): {
25
+ role: string;
26
+ child: PDFElement;
27
+ };
28
+ }
@@ -0,0 +1,42 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ import { isFragmentable } from "../../layout/fragmentation.js";
3
+ /**
4
+ * A layout-TRANSPARENT wrapper that assigns an accessibility structure role (Table / TR / TH / TD / …) to
5
+ * its single child. It delegates ALL layout and fragmentation to the child, so the visual result is byte-
6
+ * identical; only its renderer opens a structure element and nests the child's tags underneath.
7
+ *
8
+ * Split across pages, the fitted and remainder halves share this group's `structId`, so it stays ONE
9
+ * logical structure element (a table broken over pages is a single <Table>, exactly as Acrobat produces).
10
+ */
11
+ export class StructGroup extends PDFElement {
12
+ constructor({ role, child }) {
13
+ super();
14
+ this.role = role;
15
+ this.child = child;
16
+ }
17
+ calculateLayout(constraints, offset, ctx) {
18
+ return this.child.calculateLayout(constraints, offset, ctx);
19
+ }
20
+ /** Only splittable when the wrapped child is - so a StructGroup(Row) is moved whole to the next page,
21
+ * not clipped at the boundary (isFragmentable honours this veto). */
22
+ canFragment() {
23
+ return isFragmentable(this.child);
24
+ }
25
+ fragment(maxHeight, width, ctx) {
26
+ if (!isFragmentable(this.child))
27
+ return { fitted: null, remainder: this };
28
+ const { fitted, remainder } = this.child.fragment(maxHeight, width, ctx);
29
+ // Re-wrap each half with the SAME role + identity, so both pages point at one structure element.
30
+ return {
31
+ fitted: fitted
32
+ ? new StructGroup({ role: this.role, child: fitted }).adoptStructId(this)
33
+ : null,
34
+ remainder: remainder
35
+ ? new StructGroup({ role: this.role, child: remainder }).adoptStructId(this)
36
+ : null,
37
+ };
38
+ }
39
+ getProps() {
40
+ return { role: this.role, child: this.child };
41
+ }
42
+ }
@@ -20,6 +20,14 @@ interface PDFPageParams extends WithChildren {
20
20
  /** Laid out at the BOTTOM of the content box, repeated on every physical page. */
21
21
  footer?: PDFElement;
22
22
  }
23
+ /**
24
+ * The full page (media box) size in points, orientation applied. This is what a `PageBuilder` sees as
25
+ * `pageSize`; `resolvePageContentBox` below then subtracts the margins.
26
+ */
27
+ export declare function resolvePageSize(config: PDFPageConfig): {
28
+ width: number;
29
+ height: number;
30
+ };
23
31
  /**
24
32
  * The content box of a page (inside the margins, orientation applied) for a fully
25
33
  * resolved config. Single source of truth, shared by `PageElement` layout and the page
@@ -1,7 +1,18 @@
1
- import { pageFormats } from "../constants/page-sizes.js";
1
+ import { pageFormats, PageSize } from "../constants/page-sizes.js";
2
2
  import { Orientation } from "../renderer/pdf-config.js";
3
3
  import { BoxConstraints } from "../layout/box-constraints.js";
4
4
  import { PDFElement } from "./pdf-element.js";
5
+ /**
6
+ * The full page (media box) size in points, orientation applied. This is what a `PageBuilder` sees as
7
+ * `pageSize`; `resolvePageContentBox` below then subtracts the margins.
8
+ */
9
+ export function resolvePageSize(config) {
10
+ // Defaults to A4 when the config is not fully resolved, matching what PageRenderer does for the MediaBox.
11
+ const [pageW, pageH] = config.customSize ?? pageFormats[config.pageSize ?? PageSize.A4];
12
+ return config.orientation === Orientation.landscape
13
+ ? { width: pageH, height: pageW }
14
+ : { width: pageW, height: pageH };
15
+ }
5
16
  /**
6
17
  * The content box of a page (inside the margins, orientation applied) for a fully
7
18
  * resolved config. Single source of truth, shared by `PageElement` layout and the page
@@ -63,6 +74,7 @@ export class PageElement extends PDFElement {
63
74
  pageConfig: this.config,
64
75
  textStyle: ctx.textStyle,
65
76
  onOverflow: ctx.onOverflow,
77
+ pageInfo: ctx.pageInfo, // the render pass supplies it; absent during pagination
66
78
  };
67
79
  // Place the header/footer bands; the body gets the region left in between (the whole
68
80
  // content box when there is neither - byte-identical to a plain page).
@@ -23,9 +23,25 @@ export interface PositioningFrame {
23
23
  size: Size;
24
24
  }, ctx: LayoutContext) => void>;
25
25
  }
26
+ /**
27
+ * What a `PageBuilder` learns about the page it landed on. `pageNumber` is 1-based and counts PHYSICAL
28
+ * pages across the whole document (a logical `Page` that overflows contributes several); `pageCount` is
29
+ * the document's final physical page total. `pageSize` is the media box in points, orientation applied.
30
+ */
31
+ export interface PageInfo {
32
+ pageNumber: number;
33
+ pageCount: number;
34
+ pageSize: {
35
+ width: number;
36
+ height: number;
37
+ };
38
+ }
26
39
  export interface LayoutContext {
27
40
  metrics: FontMetrics;
28
41
  pageConfig: PDFPageConfig;
42
+ /** The page a `PageBuilder` is being laid out on. Only the render pass knows the real numbers, so
43
+ * during pagination this is absent and a `PageBuilder` builds against a provisional "1 of 1". */
44
+ pageInfo?: PageInfo;
29
45
  /** The nearest enclosing positioning frame, if any (set by a `relative` Box). */
30
46
  frame?: PositioningFrame;
31
47
  /** The cascaded text style descendants inherit (CSS/Flutter-style). Seeded at the document root
@@ -37,6 +53,10 @@ export interface LayoutContext {
37
53
  onOverflow?: OverflowPolicy;
38
54
  }
39
55
  export declare abstract class PDFElement {
56
+ private _structId;
57
+ get structId(): number;
58
+ /** Make this element share `from`'s tagging identity - used by fragmentation clones. Returns this. */
59
+ adoptStructId(from: PDFElement): this;
40
60
  abstract getProps(): unknown;
41
61
  /**
42
62
  * Lays the element out: `constraints` bound its size (flows DOWN), `offset` is the
@@ -45,6 +65,21 @@ export declare abstract class PDFElement {
45
65
  * happens once at the IR -> backend seam.
46
66
  */
47
67
  abstract calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
68
+ /**
69
+ * The relative-size fraction (0..1) this element asked for on the given axis, if any (`horizontal` =
70
+ * width). A flex parent reads it to resolve a percentage child against its OWN main size - e.g. a
71
+ * `Box({ width: "50%" })` inside a Row, where the child otherwise gets an unbounded main axis and the
72
+ * fraction would have nothing to resolve against. Default: the element is not relatively sized.
73
+ */
74
+ relativeSizeFactor(_horizontal: boolean): number | undefined;
75
+ /**
76
+ * Whether this element needs a BOUNDED extent on the given axis to lay itself out (`true` for a
77
+ * horizontal axis = width). A flex parent measures its fixed children with an unbounded main axis, so
78
+ * they keep their natural size; but a stack holding an `Expanded`/`Spacer` has no leftover space to
79
+ * distribute unless it is told how much main axis it may occupy. Such a stack answers `true` here and
80
+ * the parent hands it the line's bounded main extent instead of `Infinity`. Default: no.
81
+ */
82
+ needsBoundedMain(_horizontal: boolean): boolean;
48
83
  }
49
84
  export declare abstract class SizedPDFElement extends PDFElement {
50
85
  protected x: number;
@@ -1,4 +1,38 @@
1
+ let _nextStructId = 0;
1
2
  export class PDFElement {
3
+ constructor() {
4
+ // Stable identity for accessible (PDF/UA) tagging: assigned once at construction and carried through
5
+ // fragmentation copies (adoptStructId), so a paragraph or table split across pages stays ONE structure
6
+ // element. Only the elements that actually tag (Text, Image, StructGroup) read it.
7
+ this._structId = _nextStructId++;
8
+ }
9
+ get structId() {
10
+ return this._structId;
11
+ }
12
+ /** Make this element share `from`'s tagging identity - used by fragmentation clones. Returns this. */
13
+ adoptStructId(from) {
14
+ this._structId = from._structId;
15
+ return this;
16
+ }
17
+ /**
18
+ * The relative-size fraction (0..1) this element asked for on the given axis, if any (`horizontal` =
19
+ * width). A flex parent reads it to resolve a percentage child against its OWN main size - e.g. a
20
+ * `Box({ width: "50%" })` inside a Row, where the child otherwise gets an unbounded main axis and the
21
+ * fraction would have nothing to resolve against. Default: the element is not relatively sized.
22
+ */
23
+ relativeSizeFactor(_horizontal) {
24
+ return undefined;
25
+ }
26
+ /**
27
+ * Whether this element needs a BOUNDED extent on the given axis to lay itself out (`true` for a
28
+ * horizontal axis = width). A flex parent measures its fixed children with an unbounded main axis, so
29
+ * they keep their natural size; but a stack holding an `Expanded`/`Spacer` has no leftover space to
30
+ * distribute unless it is told how much main axis it may occupy. Such a stack answers `true` here and
31
+ * the parent hands it the line's bounded main extent instead of `Infinity`. Default: no.
32
+ */
33
+ needsBoundedMain(_horizontal) {
34
+ return false;
35
+ }
2
36
  }
3
37
  export class SizedPDFElement extends PDFElement {
4
38
  constructor(data) {
@@ -23,6 +23,11 @@ interface RectangleElementParams extends SizedElement, WithChildren {
23
23
  /** `"hidden"` crops children to the box (CSS `overflow: hidden`); `"visible"` (default) lets a
24
24
  * `Positioned` child spill over the edge. */
25
25
  overflow?: "hidden" | "visible";
26
+ /** Width as a fraction (0..1) of the offered width, instead of a fixed `width` (CSS `50%`). Only
27
+ * resolves in a bounded region; an explicit `width` wins over it. */
28
+ widthFactor?: number;
29
+ /** Height as a fraction (0..1) of the offered height; see `widthFactor`. */
30
+ heightFactor?: number;
26
31
  }
27
32
  export declare class RectangleElement extends SizedPDFElement implements Fragmentable {
28
33
  private children;
@@ -34,7 +39,7 @@ export declare class RectangleElement extends SizedPDFElement implements Fragmen
34
39
  private relative;
35
40
  private overflow;
36
41
  private sizeMemory;
37
- constructor({ children, color, backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, }: RectangleElementParams);
42
+ constructor({ children, color, backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, widthFactor, heightFactor, }: RectangleElementParams);
38
43
  /**
39
44
  * Splits the bordered box across pages (box-decoration-break: clone - every fragment
40
45
  * gets its own full border). The children stack is packed into the space left after
@@ -45,6 +50,11 @@ export declare class RectangleElement extends SizedPDFElement implements Fragmen
45
50
  */
46
51
  fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
47
52
  private cloneWithChildren;
53
+ relativeSizeFactor(horizontal: boolean): number | undefined;
54
+ /** A box has no flex children of its own, but it must not swallow the need of the stack inside it: a
55
+ * `Spacer` in a box can only resolve if the box itself was given a bounded extent to pass down. With an
56
+ * explicit size on an axis the box already knows its extent there and asks for nothing. */
57
+ needsBoundedMain(horizontal: boolean): boolean;
48
58
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
49
59
  getProps(): {
50
60
  x: number;
@@ -1,9 +1,9 @@
1
1
  import { Color } from "../common/color.js";
2
- import { BoxConstraints } from "../layout/box-constraints.js";
2
+ import { BoxConstraints, resolveExtent } from "../layout/box-constraints.js";
3
3
  import { packChildren } from "../layout/fragmentation.js";
4
4
  import { SizedPDFElement, } from "./pdf-element.js";
5
5
  export class RectangleElement extends SizedPDFElement {
6
- constructor({ children = [], color = new Color(0, 0, 0), backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, }) {
6
+ constructor({ children = [], color = new Color(0, 0, 0), backgroundColor, borderWidth, width, height, radius, sideBorders, relative, overflow, widthFactor, heightFactor, }) {
7
7
  super({ x: 0, y: 0, width, height });
8
8
  this.children = [];
9
9
  this.children = children;
@@ -15,7 +15,7 @@ export class RectangleElement extends SizedPDFElement {
15
15
  this.sideBorders = sideBorders;
16
16
  this.relative = relative ?? false;
17
17
  this.overflow = overflow ?? "visible";
18
- this.sizeMemory = { x: 0, y: 0, width, height };
18
+ this.sizeMemory = { x: 0, y: 0, width, height, widthFactor, heightFactor };
19
19
  }
20
20
  /**
21
21
  * Splits the bordered box across pages (box-decoration-break: clone - every fragment
@@ -26,7 +26,10 @@ export class RectangleElement extends SizedPDFElement {
26
26
  * box is sized by what it actually holds on each page.
27
27
  */
28
28
  fragment(maxHeight, width, ctx) {
29
- const boxWidth = this.sizeMemory.width ?? width;
29
+ // A percentage width resolves against the region width the page driver hands us, so a
30
+ // paginating %-box keeps its fraction on every page.
31
+ const boxWidth = this.sizeMemory.width ??
32
+ (this.sizeMemory.widthFactor !== undefined ? width * this.sizeMemory.widthFactor : width);
30
33
  const innerWidth = Math.max(0, boxWidth - 2 * this.borderWidth);
31
34
  // Border-box: the content is inset by the border on top and bottom, so a fragment
32
35
  // holding `c` of content is `c + 2*border` tall. (Derived, not a fudge factor.)
@@ -47,6 +50,9 @@ export class RectangleElement extends SizedPDFElement {
47
50
  x: 0,
48
51
  y: 0,
49
52
  width: this.sizeMemory.width,
53
+ widthFactor: this.sizeMemory.widthFactor,
54
+ // A split box is sized by its per-page content (height passed in), so the height factor
55
+ // is intentionally dropped here - box-decoration-break, same as an explicit height.
50
56
  height,
51
57
  children,
52
58
  color: this.color,
@@ -58,25 +64,43 @@ export class RectangleElement extends SizedPDFElement {
58
64
  overflow: this.overflow,
59
65
  });
60
66
  }
67
+ relativeSizeFactor(horizontal) {
68
+ return horizontal ? this.sizeMemory.widthFactor : this.sizeMemory.heightFactor;
69
+ }
70
+ /** A box has no flex children of its own, but it must not swallow the need of the stack inside it: a
71
+ * `Spacer` in a box can only resolve if the box itself was given a bounded extent to pass down. With an
72
+ * explicit size on an axis the box already knows its extent there and asks for nothing. */
73
+ needsBoundedMain(horizontal) {
74
+ const requested = horizontal
75
+ ? [this.sizeMemory.width, this.sizeMemory.widthFactor]
76
+ : [this.sizeMemory.height, this.sizeMemory.heightFactor];
77
+ if (requested.some((v) => v !== undefined))
78
+ return false;
79
+ return this.children.some((c) => c.needsBoundedMain(horizontal));
80
+ }
61
81
  calculateLayout(constraints, offset, ctx) {
82
+ // An explicit extent is a fixed point size or a fraction of the offered box (relative sizing);
83
+ // the fraction only resolves in a bounded region. `undefined` = fill / shrink-wrap below.
84
+ const explicitWidth = resolveExtent(this.sizeMemory.width, this.sizeMemory.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
85
+ const explicitHeight = resolveExtent(this.sizeMemory.height, this.sizeMemory.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
62
86
  // Width: an explicit size wins (clamped), else fill the offered box. (Without this a
63
87
  // fixed box would balloon to the parent's size.)
64
88
  this.width =
65
- this.sizeMemory.width !== undefined
66
- ? constraints.constrainWidth(this.sizeMemory.width)
89
+ explicitWidth !== undefined
90
+ ? constraints.constrainWidth(explicitWidth)
67
91
  : constraints.hasBoundedWidth
68
92
  ? constraints.maxWidth
69
93
  : this.width;
70
94
  // Width shrink-wrap: no explicit width AND an unbounded region (e.g. a `Box` badge inside a
71
95
  // `Positioned`). Resolved after the children are measured, just below.
72
- const shrinkWrapWidth = this.sizeMemory.width === undefined && !constraints.hasBoundedWidth;
96
+ const shrinkWrapWidth = explicitWidth === undefined && !constraints.hasBoundedWidth;
73
97
  // Height: explicit wins; otherwise FILL a bounded region (e.g. inside an Expanded) but
74
98
  // SHRINK-WRAP the content in an unbounded one (a note box in a stack). Shrink-wrap is
75
99
  // resolved after the children are measured, just below.
76
- const shrinkWrapHeight = this.sizeMemory.height === undefined && !constraints.hasBoundedHeight;
100
+ const shrinkWrapHeight = explicitHeight === undefined && !constraints.hasBoundedHeight;
77
101
  this.height =
78
- this.sizeMemory.height !== undefined
79
- ? constraints.constrainHeight(this.sizeMemory.height)
102
+ explicitHeight !== undefined
103
+ ? constraints.constrainHeight(explicitHeight)
80
104
  : constraints.hasBoundedHeight
81
105
  ? constraints.maxHeight
82
106
  : this.height;
@@ -89,16 +113,28 @@ export class RectangleElement extends SizedPDFElement {
89
113
  ? { origin: { x: 0, y: 0 }, size: { width: 0, height: 0 }, place: [] }
90
114
  : undefined;
91
115
  const childCtx = frame ? { ...ctx, frame } : ctx;
92
- // Lay out children stacked inside the border (inset by the border width). Width is
93
- // finalized here; height is left unbounded so each child sizes to its own content.
116
+ // Lay out children stacked inside the border (inset by the border width). Width is finalized here;
117
+ // height is left unbounded so each child sizes to its own content - EXCEPT for a child that cannot
118
+ // lay itself out without a bound (a stack holding an `Expanded`/`Spacer`). That one gets the height
119
+ // still free inside the box, so its flex child has real leftover space instead of an infinite one.
94
120
  const innerWidth = shrinkWrapWidth
95
121
  ? Infinity
96
122
  : Math.max(0, (this.width ?? 0) - 2 * this.borderWidth);
123
+ const innerHeight = shrinkWrapHeight
124
+ ? Infinity
125
+ : Math.max(0, (this.height ?? 0) - 2 * this.borderWidth);
97
126
  let contentWidth = 0;
98
127
  let contentHeight = 0;
99
128
  let yCursor = this.y + this.borderWidth;
100
129
  for (const child of this.children) {
101
- const childSize = child.calculateLayout(BoxConstraints.loose(innerWidth, Infinity), { x: this.x + this.borderWidth, y: yCursor }, childCtx);
130
+ const consumed = yCursor - (this.y + this.borderWidth);
131
+ // Same rule the flex helper applies: a child gets a finite main extent when it cannot lay itself
132
+ // out without one - a stack holding an `Expanded`/`Spacer`, or a child sized as a percentage of us.
133
+ const needsBound = child.needsBoundedMain(false) || child.relativeSizeFactor(false) !== undefined;
134
+ const childHeight = needsBound
135
+ ? Math.max(0, innerHeight - consumed) // Infinity - consumed stays Infinity, as it should
136
+ : Infinity;
137
+ const childSize = child.calculateLayout(BoxConstraints.loose(innerWidth, childHeight), { x: this.x + this.borderWidth, y: yCursor }, childCtx);
102
138
  yCursor += childSize.height;
103
139
  contentHeight += childSize.height;
104
140
  contentWidth = Math.max(contentWidth, childSize.width);
@@ -8,6 +8,11 @@ interface RowElementParams extends WithChildren {
8
8
  main?: MainAlign;
9
9
  /** Vertical alignment of each child (cross axis); defaults to `stretch`. */
10
10
  cross?: CrossAlign;
11
+ /** Width/height as points (fixed) or a fraction (0..1) of the offered box (relative sizing). */
12
+ width?: number;
13
+ height?: number;
14
+ widthFactor?: number;
15
+ heightFactor?: number;
11
16
  }
12
17
  /**
13
18
  * Horizontal stack: the mirror of `ContainerElement` (Column). Children are laid out
@@ -26,7 +31,12 @@ export declare class RowElement extends SizedPDFElement {
26
31
  private gap;
27
32
  private main;
28
33
  private cross;
29
- constructor({ children, gap, main, cross }: RowElementParams);
34
+ private requested;
35
+ constructor({ children, gap, main, cross, width, height, widthFactor, heightFactor, }: RowElementParams);
36
+ relativeSizeFactor(horizontal: boolean): number | undefined;
37
+ /** The mirror of `ContainerElement`: a Row's OWN flex child needs a bounded width, and the need of any
38
+ * descendant propagates on both axes (see the Column for the reasoning). */
39
+ needsBoundedMain(horizontal: boolean): boolean;
30
40
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
31
41
  getProps(): {
32
42
  x: number;