@jasy/pdf 1.0.0-alpha.5 → 1.0.0-alpha.7

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 (105) hide show
  1. package/dist/api/content.d.ts +6 -3
  2. package/dist/api/content.js +12 -3
  3. package/dist/api/descriptor.js +24 -1
  4. package/dist/api/dimension.d.ts +20 -0
  5. package/dist/api/dimension.js +16 -0
  6. package/dist/api/index.d.ts +2 -0
  7. package/dist/api/index.js +2 -0
  8. package/dist/api/layout.d.ts +112 -17
  9. package/dist/api/layout.js +118 -13
  10. package/dist/api/page-builder.d.ts +32 -0
  11. package/dist/api/page-builder.js +32 -0
  12. package/dist/api/structure.d.ts +5 -0
  13. package/dist/api/structure.js +1 -0
  14. package/dist/api/text.d.ts +24 -2
  15. package/dist/api/text.js +46 -4
  16. package/dist/elements/container-element.d.ts +26 -1
  17. package/dist/elements/container-element.js +79 -29
  18. package/dist/elements/image-element.d.ts +19 -1
  19. package/dist/elements/image-element.js +52 -11
  20. package/dist/elements/index.d.ts +8 -0
  21. package/dist/elements/index.js +8 -0
  22. package/dist/elements/layout/anchor-element.d.ts +23 -0
  23. package/dist/elements/layout/anchor-element.js +27 -0
  24. package/dist/elements/layout/bookmark-element.d.ts +27 -0
  25. package/dist/elements/layout/bookmark-element.js +30 -0
  26. package/dist/elements/layout/keep-together-element.d.ts +42 -0
  27. package/dist/elements/layout/keep-together-element.js +85 -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/padding-element.d.ts +3 -0
  31. package/dist/elements/layout/padding-element.js +12 -0
  32. package/dist/elements/layout/page-break-element.d.ts +17 -0
  33. package/dist/elements/layout/page-break-element.js +24 -0
  34. package/dist/elements/layout/page-builder-element.d.ts +32 -0
  35. package/dist/elements/layout/page-builder-element.js +43 -0
  36. package/dist/elements/layout/positioned-element.d.ts +10 -7
  37. package/dist/elements/layout/positioned-element.js +21 -9
  38. package/dist/elements/layout/rotated-box-element.d.ts +35 -0
  39. package/dist/elements/layout/rotated-box-element.js +54 -0
  40. package/dist/elements/layout/rotated-element.d.ts +32 -0
  41. package/dist/elements/layout/rotated-element.js +41 -0
  42. package/dist/elements/page-element.d.ts +19 -1
  43. package/dist/elements/page-element.js +54 -17
  44. package/dist/elements/pdf-element.d.ts +59 -0
  45. package/dist/elements/pdf-element.js +51 -0
  46. package/dist/elements/rectangle-element.d.ts +20 -1
  47. package/dist/elements/rectangle-element.js +75 -18
  48. package/dist/elements/row-element.d.ts +19 -1
  49. package/dist/elements/row-element.js +45 -8
  50. package/dist/elements/text-element.d.ts +34 -4
  51. package/dist/elements/text-element.js +40 -17
  52. package/dist/ir/display-list.d.ts +60 -1
  53. package/dist/layout/box-constraints.d.ts +8 -0
  54. package/dist/layout/box-constraints.js +14 -0
  55. package/dist/layout/collect-images.d.ts +9 -0
  56. package/dist/layout/collect-images.js +34 -0
  57. package/dist/layout/fragmentation.d.ts +12 -0
  58. package/dist/layout/fragmentation.js +45 -8
  59. package/dist/renderer/anchor-renderer.d.ts +6 -0
  60. package/dist/renderer/anchor-renderer.js +11 -0
  61. package/dist/renderer/bookmark-renderer.d.ts +6 -0
  62. package/dist/renderer/bookmark-renderer.js +11 -0
  63. package/dist/renderer/keep-together-renderer.d.ts +10 -0
  64. package/dist/renderer/keep-together-renderer.js +12 -0
  65. package/dist/renderer/link-renderer.d.ts +6 -0
  66. package/dist/renderer/link-renderer.js +11 -0
  67. package/dist/renderer/page-break-renderer.d.ts +14 -0
  68. package/dist/renderer/page-break-renderer.js +15 -0
  69. package/dist/renderer/page-builder-renderer.d.ts +6 -0
  70. package/dist/renderer/page-builder-renderer.js +11 -0
  71. package/dist/renderer/page-renderer.js +37 -2
  72. package/dist/renderer/pdf-backend.d.ts +17 -0
  73. package/dist/renderer/pdf-backend.js +117 -7
  74. package/dist/renderer/pdf-document-class.js +3 -0
  75. package/dist/renderer/pdf-document-renderer.d.ts +16 -7
  76. package/dist/renderer/pdf-document-renderer.js +87 -36
  77. package/dist/renderer/pdf-renderer.js +53 -2
  78. package/dist/renderer/rotated-renderer.d.ts +7 -0
  79. package/dist/renderer/rotated-renderer.js +29 -0
  80. package/dist/renderer/text-renderer.d.ts +1 -1
  81. package/dist/renderer/text-renderer.js +123 -54
  82. package/dist/text/advance.d.ts +34 -0
  83. package/dist/text/advance.js +42 -0
  84. package/dist/text/line-breaker.d.ts +9 -8
  85. package/dist/text/line-breaker.js +28 -21
  86. package/dist/text/line-metrics.d.ts +49 -0
  87. package/dist/text/line-metrics.js +57 -0
  88. package/dist/text/text-decoration.d.ts +50 -0
  89. package/dist/text/text-decoration.js +62 -0
  90. package/dist/text/text-style.d.ts +13 -3
  91. package/dist/text/text-style.js +10 -3
  92. package/dist/utils/afm-parser.d.ts +34 -0
  93. package/dist/utils/afm-parser.js +73 -4
  94. package/dist/utils/dest-registry.d.ts +19 -0
  95. package/dist/utils/dest-registry.js +32 -0
  96. package/dist/utils/flex-layout.d.ts +10 -3
  97. package/dist/utils/flex-layout.js +26 -6
  98. package/dist/utils/font-metrics.d.ts +14 -0
  99. package/dist/utils/outline.d.ts +21 -0
  100. package/dist/utils/outline.js +80 -0
  101. package/dist/utils/pdf-object-manager.d.ts +33 -1
  102. package/dist/utils/pdf-object-manager.js +0 -0
  103. package/dist/utils/ttf-parser.d.ts +53 -0
  104. package/dist/utils/ttf-parser.js +368 -0
  105. package/package.json +1 -1
@@ -0,0 +1,42 @@
1
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
2
+ import { Fragmentable, FragmentResult } from "../../layout/fragmentation.js";
3
+ import { LayoutContext, PDFElement } from "../pdf-element.js";
4
+ /**
5
+ * A layout-TRANSPARENT wrapper that tries to keep its content on ONE page (CSS `break-inside: avoid`).
6
+ * It delegates all layout and rendering to its child, so the visual result is byte-identical to the
7
+ * unwrapped content; only pagination behaves differently.
8
+ *
9
+ * The rule, decided in `fragment`:
10
+ * 1. Fits in the space offered here -> place it whole.
11
+ * 2. Does not fit here but fits on a FRESH -> veto the split: defer the whole group to the next
12
+ * full page page (`fitted: null`), so it lands together there.
13
+ * 3. Taller than a whole page -> drop the veto and split normally. A group that can
14
+ * never fit on one page must break, or pagination
15
+ * could not terminate. Inner `keepTogether`s survive
16
+ * this (they are separate elements, re-evaluated when
17
+ * the child splits) - graceful degrade, outer->inner.
18
+ *
19
+ * A forced page break inside the group (a `PageBreak`, or a `breakBefore`/`breakAfter`) contradicts
20
+ * keeping it together: the break wins (CSS behaves the same), and we WARN once rather than silently
21
+ * swallowing either intent.
22
+ */
23
+ export declare class KeepTogetherElement extends PDFElement implements Fragmentable {
24
+ private child;
25
+ private warnedBreakConflict;
26
+ constructor({ child }: {
27
+ child: PDFElement;
28
+ });
29
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
30
+ relativeSizeFactor(horizontal: boolean): number | undefined;
31
+ needsBoundedMain(horizontal: boolean): boolean;
32
+ hasForcedBreak(): boolean;
33
+ breaksBefore(): boolean;
34
+ breaksAfter(): boolean;
35
+ fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
36
+ /** Delegate the actual split to the child, unwrapping - once we have decided to break the group, the
37
+ * pieces just flow (any INNER keepTogether lives in the child subtree and still vetoes there). */
38
+ private splitChild;
39
+ getProps(): {
40
+ child: PDFElement;
41
+ };
42
+ }
@@ -0,0 +1,85 @@
1
+ import { BoxConstraints } from "../../layout/box-constraints.js";
2
+ import { isFragmentable } from "../../layout/fragmentation.js";
3
+ import { PDFElement } from "../pdf-element.js";
4
+ /**
5
+ * A layout-TRANSPARENT wrapper that tries to keep its content on ONE page (CSS `break-inside: avoid`).
6
+ * It delegates all layout and rendering to its child, so the visual result is byte-identical to the
7
+ * unwrapped content; only pagination behaves differently.
8
+ *
9
+ * The rule, decided in `fragment`:
10
+ * 1. Fits in the space offered here -> place it whole.
11
+ * 2. Does not fit here but fits on a FRESH -> veto the split: defer the whole group to the next
12
+ * full page page (`fitted: null`), so it lands together there.
13
+ * 3. Taller than a whole page -> drop the veto and split normally. A group that can
14
+ * never fit on one page must break, or pagination
15
+ * could not terminate. Inner `keepTogether`s survive
16
+ * this (they are separate elements, re-evaluated when
17
+ * the child splits) - graceful degrade, outer->inner.
18
+ *
19
+ * A forced page break inside the group (a `PageBreak`, or a `breakBefore`/`breakAfter`) contradicts
20
+ * keeping it together: the break wins (CSS behaves the same), and we WARN once rather than silently
21
+ * swallowing either intent.
22
+ */
23
+ export class KeepTogetherElement extends PDFElement {
24
+ constructor({ child }) {
25
+ super();
26
+ this.warnedBreakConflict = false;
27
+ this.child = child;
28
+ }
29
+ calculateLayout(constraints, offset, ctx) {
30
+ return this.child.calculateLayout(constraints, offset, ctx);
31
+ }
32
+ // Transparent: forward the sizing hints the flex parent reads, so a %-sized or Spacer-holding child
33
+ // keeps working when it is wrapped (the wrapper must not swallow these).
34
+ relativeSizeFactor(horizontal) {
35
+ return this.child.relativeSizeFactor(horizontal);
36
+ }
37
+ needsBoundedMain(horizontal) {
38
+ return this.child.needsBoundedMain(horizontal);
39
+ }
40
+ // Transparent to break control: a break-before/after on the wrapped element still applies to the group
41
+ // as a whole, and an inner forced break still forces the parent to fragment us (so `fragment` runs).
42
+ hasForcedBreak() {
43
+ return this.child.hasForcedBreak();
44
+ }
45
+ breaksBefore() {
46
+ return this.child.breaksBefore();
47
+ }
48
+ breaksAfter() {
49
+ return this.child.breaksAfter();
50
+ }
51
+ fragment(maxHeight, width, ctx) {
52
+ // Contradiction: a forced break inside the group beats keeping it together. Split at the break by
53
+ // delegating to the child (which honours it), and say so once - never keep such a group whole.
54
+ if (this.child.hasForcedBreak()) {
55
+ if (!this.warnedBreakConflict) {
56
+ this.warnedBreakConflict = true;
57
+ console.warn("keepTogether contains a forced page break (PageBreak / breakBefore / breakAfter). The break " +
58
+ "wins and the group is split. Remove the break, or the keepTogether, to resolve this.");
59
+ }
60
+ return this.splitChild(maxHeight, width, ctx);
61
+ }
62
+ const natural = this.child.calculateLayout(BoxConstraints.loose(width, Infinity), { x: 0, y: 0 }, ctx).height;
63
+ // 1. Fits in the offered space: keep whole.
64
+ if (natural <= maxHeight)
65
+ return { fitted: this, remainder: null };
66
+ // 2. Would fit on a fresh full page: veto the split, defer the whole group to the next page. Only
67
+ // reachable in a PARTIAL region (something is already on this page); alone on a full page,
68
+ // `maxHeight` IS the page body, so `natural > maxHeight` means case 3, never this.
69
+ const pageBody = ctx.pageBodyHeight ?? maxHeight;
70
+ if (natural <= pageBody)
71
+ return { fitted: null, remainder: this };
72
+ // 3. Bigger than a whole page: it can never be kept together, so split it.
73
+ return this.splitChild(maxHeight, width, ctx);
74
+ }
75
+ /** Delegate the actual split to the child, unwrapping - once we have decided to break the group, the
76
+ * pieces just flow (any INNER keepTogether lives in the child subtree and still vetoes there). */
77
+ splitChild(maxHeight, width, ctx) {
78
+ if (!isFragmentable(this.child))
79
+ return { fitted: null, remainder: this };
80
+ return this.child.fragment(maxHeight, width, ctx);
81
+ }
82
+ getProps() {
83
+ return { child: this.child };
84
+ }
85
+ }
@@ -0,0 +1,33 @@
1
+ import { PDFElement, LayoutContext } from "../pdf-element.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ /**
4
+ * Makes its child a clickable hyperlink. `href` is an external URL; `dest` is an internal named
5
+ * destination (an `Anchor` elsewhere in the document) - exactly one is set. Layout-transparent: it
6
+ * delegates `calculateLayout` to the child and records the resulting box, which the renderer turns into
7
+ * a `Link` IR node (and the page renderer into a /Link annotation). The link draws nothing itself -
8
+ * style the child (e.g. a blue Text) if you want it to look like a link.
9
+ */
10
+ export declare class LinkElement extends PDFElement {
11
+ private href?;
12
+ private dest?;
13
+ private child;
14
+ private x;
15
+ private y;
16
+ private width;
17
+ private height;
18
+ constructor({ href, dest, child }: {
19
+ href?: string;
20
+ dest?: string;
21
+ child: PDFElement;
22
+ });
23
+ calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
24
+ getProps(): {
25
+ href: string | undefined;
26
+ dest: string | undefined;
27
+ child: PDFElement;
28
+ x: number;
29
+ y: number;
30
+ width: number;
31
+ height: number;
32
+ };
33
+ }
@@ -0,0 +1,39 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ /**
3
+ * Makes its child a clickable hyperlink. `href` is an external URL; `dest` is an internal named
4
+ * destination (an `Anchor` elsewhere in the document) - exactly one is set. Layout-transparent: it
5
+ * delegates `calculateLayout` to the child and records the resulting box, which the renderer turns into
6
+ * a `Link` IR node (and the page renderer into a /Link annotation). The link draws nothing itself -
7
+ * style the child (e.g. a blue Text) if you want it to look like a link.
8
+ */
9
+ export class LinkElement extends PDFElement {
10
+ constructor({ href, dest, child }) {
11
+ super();
12
+ this.x = 0;
13
+ this.y = 0;
14
+ this.width = 0;
15
+ this.height = 0;
16
+ this.href = href;
17
+ this.dest = dest;
18
+ this.child = child;
19
+ }
20
+ calculateLayout(constraints, offset, ctx) {
21
+ const size = this.child.calculateLayout(constraints, offset, ctx);
22
+ this.x = offset.x;
23
+ this.y = offset.y;
24
+ this.width = size.width;
25
+ this.height = size.height;
26
+ return size;
27
+ }
28
+ getProps() {
29
+ return {
30
+ href: this.href,
31
+ dest: this.dest,
32
+ child: this.child,
33
+ x: this.x,
34
+ y: this.y,
35
+ width: this.width,
36
+ height: this.height,
37
+ };
38
+ }
39
+ }
@@ -15,6 +15,9 @@ export declare class PaddingElement extends SizedPDFElement implements Fragmenta
15
15
  * child can't be split, the whole padding moves on as the remainder.
16
16
  */
17
17
  fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
18
+ hasForcedBreak(): boolean;
19
+ breaksBefore(): boolean;
20
+ breaksAfter(): boolean;
18
21
  private cloneWithChild;
19
22
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
20
23
  getProps(): {
@@ -25,8 +25,20 @@ export class PaddingElement extends SizedPDFElement {
25
25
  return {
26
26
  fitted: split.fitted ? this.cloneWithChild(split.fitted) : null,
27
27
  remainder: split.remainder ? this.cloneWithChild(split.remainder) : null,
28
+ forceBreak: split.forceBreak,
28
29
  };
29
30
  }
31
+ hasForcedBreak() {
32
+ return this.child.hasForcedBreak();
33
+ }
34
+ // Padding is a transparent single-child wrapper, so a break-before/after on the wrapped element must
35
+ // still be seen by the parent packer at this boundary (otherwise it is swallowed).
36
+ breaksBefore() {
37
+ return this.child.breaksBefore();
38
+ }
39
+ breaksAfter() {
40
+ return this.child.breaksAfter();
41
+ }
30
42
  cloneWithChild(child) {
31
43
  return new PaddingElement({ margin: this.margin, child });
32
44
  }
@@ -0,0 +1,17 @@
1
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
2
+ import { LayoutContext, PDFElement } from "../pdf-element.js";
3
+ /**
4
+ * A forced page break. Placed in a flow (a `Column`/`Row`/`Box`), it makes everything AFTER it start
5
+ * on a fresh page, no matter how much space is left. It takes zero space and draws nothing - the
6
+ * pagination packer consumes it at the cut, so it never reaches the render pass.
7
+ *
8
+ * Nesting works: a break deep inside a container is honoured because the packer fragments any child
9
+ * whose subtree `hasForcedBreak()`, and the resulting `forceBreak` bubbles up so the outer flow stops
10
+ * too. A break wins over a `keepTogether` (a forced break cannot be avoided) - CSS behaves the same.
11
+ */
12
+ export declare class PageBreakElement extends PDFElement {
13
+ calculateLayout(_constraints: BoxConstraints, _offset: Offset, _ctx: LayoutContext): Size;
14
+ isPageBreak(): boolean;
15
+ hasForcedBreak(): boolean;
16
+ getProps(): Record<string, never>;
17
+ }
@@ -0,0 +1,24 @@
1
+ import { PDFElement } from "../pdf-element.js";
2
+ /**
3
+ * A forced page break. Placed in a flow (a `Column`/`Row`/`Box`), it makes everything AFTER it start
4
+ * on a fresh page, no matter how much space is left. It takes zero space and draws nothing - the
5
+ * pagination packer consumes it at the cut, so it never reaches the render pass.
6
+ *
7
+ * Nesting works: a break deep inside a container is honoured because the packer fragments any child
8
+ * whose subtree `hasForcedBreak()`, and the resulting `forceBreak` bubbles up so the outer flow stops
9
+ * too. A break wins over a `keepTogether` (a forced break cannot be avoided) - CSS behaves the same.
10
+ */
11
+ export class PageBreakElement extends PDFElement {
12
+ calculateLayout(_constraints, _offset, _ctx) {
13
+ return { width: 0, height: 0 };
14
+ }
15
+ isPageBreak() {
16
+ return true;
17
+ }
18
+ hasForcedBreak() {
19
+ return true;
20
+ }
21
+ getProps() {
22
+ return {};
23
+ }
24
+ }
@@ -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
+ }
@@ -23,14 +23,17 @@ export interface PositionedInsets {
23
23
  interface PositionedElementParams extends WithChild, PositionedInsets {
24
24
  }
25
25
  /**
26
- * An out-of-flow child, placed relative to the nearest enclosing positioning frame (a `relative`
27
- * Box). It takes ZERO space in the normal flow - `calculateLayout` returns `Size(0,0)` and instead
28
- * registers a placement closure on the frame. The frame runs that closure once it knows its own
29
- * size, so `right`/`bottom` resolve against the final box and the child can overflow it (a negative
30
- * `top`/`left` makes it poke into / out of the corner, e.g. a badge or tab).
26
+ * An out-of-flow child, placed relative to the nearest enclosing positioning frame: a `relative`
27
+ * Box, or failing that the page's content box. It takes ZERO space in the normal flow -
28
+ * `calculateLayout` returns `Size(0,0)` and instead registers a placement closure on the frame. The
29
+ * frame runs that closure once it knows its own size, so `right`/`bottom` resolve against the final
30
+ * box and the child can overflow it (a negative `top`/`left` makes it poke into / out of the
31
+ * corner, e.g. a badge or tab).
31
32
  *
32
- * With no frame in scope the element is a no-op (nothing is drawn) - `Positioned` only makes sense
33
- * inside a `relative` Box.
33
+ * Every page supplies a frame - header, footer and body alike - so within a document there is
34
+ * always one. Outside of one we REFUSE rather than draw: the element used to fall through to its
35
+ * child's default (0, 0), which is how a header watermark landed silently in the page corner
36
+ * (ISSUE-4). Content that lands somewhere unasked is worse than content that does not render.
34
37
  */
35
38
  export declare class PositionedElement extends PDFElement {
36
39
  private child;
@@ -1,14 +1,17 @@
1
1
  import { BoxConstraints } from "../../layout/box-constraints.js";
2
2
  import { PDFElement } from "../pdf-element.js";
3
3
  /**
4
- * An out-of-flow child, placed relative to the nearest enclosing positioning frame (a `relative`
5
- * Box). It takes ZERO space in the normal flow - `calculateLayout` returns `Size(0,0)` and instead
6
- * registers a placement closure on the frame. The frame runs that closure once it knows its own
7
- * size, so `right`/`bottom` resolve against the final box and the child can overflow it (a negative
8
- * `top`/`left` makes it poke into / out of the corner, e.g. a badge or tab).
4
+ * An out-of-flow child, placed relative to the nearest enclosing positioning frame: a `relative`
5
+ * Box, or failing that the page's content box. It takes ZERO space in the normal flow -
6
+ * `calculateLayout` returns `Size(0,0)` and instead registers a placement closure on the frame. The
7
+ * frame runs that closure once it knows its own size, so `right`/`bottom` resolve against the final
8
+ * box and the child can overflow it (a negative `top`/`left` makes it poke into / out of the
9
+ * corner, e.g. a badge or tab).
9
10
  *
10
- * With no frame in scope the element is a no-op (nothing is drawn) - `Positioned` only makes sense
11
- * inside a `relative` Box.
11
+ * Every page supplies a frame - header, footer and body alike - so within a document there is
12
+ * always one. Outside of one we REFUSE rather than draw: the element used to fall through to its
13
+ * child's default (0, 0), which is how a header watermark landed silently in the page corner
14
+ * (ISSUE-4). Content that lands somewhere unasked is worse than content that does not render.
12
15
  */
13
16
  export class PositionedElement extends PDFElement {
14
17
  constructor({ child, top, right, bottom, left, h, v, x, y }) {
@@ -17,8 +20,12 @@ export class PositionedElement extends PDFElement {
17
20
  this.insets = { top, right, bottom, left, h, v, x, y };
18
21
  }
19
22
  calculateLayout(_constraints, _offset, ctx) {
23
+ if (!ctx.frame) {
24
+ throw new Error("Positioned found no positioning frame. It must sit inside a Page (header, footer or body) " +
25
+ "or inside a Box({ relative: true }).");
26
+ }
20
27
  // Defer to the frame: it calls back once it has sized itself. Out of flow either way.
21
- ctx.frame?.place.push((frame, frameCtx) => this.placeInFrame(frame, frameCtx));
28
+ ctx.frame.place.push((frame, frameCtx) => this.placeInFrame(frame, frameCtx));
22
29
  return { width: 0, height: 0 };
23
30
  }
24
31
  /** Lays the child out at the resolved position inside (or overflowing) the frame box. */
@@ -35,7 +42,12 @@ export class PositionedElement extends PDFElement {
35
42
  : Infinity;
36
43
  const constraints = BoxConstraints.loose(width, height);
37
44
  // Measure first (so end/center anchors and right/bottom edges resolve against the child's size).
38
- const measured = this.child.calculateLayout(constraints, { x: 0, y: 0 }, ctx);
45
+ // The measuring pass gets a throwaway `place` list: this subtree is laid out twice, and a nested
46
+ // out-of-flow child would otherwise register its placement on both passes.
47
+ const measureCtx = ctx.frame
48
+ ? { ...ctx, frame: { ...ctx.frame, place: [] } }
49
+ : ctx;
50
+ const measured = this.child.calculateLayout(constraints, { x: 0, y: 0 }, measureCtx);
39
51
  // Resolve one axis: a near-edge pins to origin, a far-edge pins to the far side; otherwise the
40
52
  // anchor (default `start`) positions the child and the nudge shifts it from there.
41
53
  const place = (origin, frameExtent, childExtent, nearEdge, farEdge, anchor, nudge) => {
@@ -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
+ }
@@ -2,7 +2,7 @@ import { PageSize } from "../constants/page-sizes.js";
2
2
  import { Orientation } from "../renderer/pdf-config.js";
3
3
  import type { ColorMode, DefaultFont, Margin } from "../renderer/index.js";
4
4
  import { BoxConstraints, Offset, Size } from "../layout/box-constraints.js";
5
- import { LayoutContext, PDFElement, WithChildren } from "./pdf-element.js";
5
+ import { LayoutContext, PDFElement, PositioningFrame, WithChildren } from "./pdf-element.js";
6
6
  import { TextElement } from "./text-element.js";
7
7
  export interface PDFPageConfig {
8
8
  pageSize?: PageSize;
@@ -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
@@ -46,6 +54,16 @@ export interface PageBands {
46
54
  * no header/footer the bands are zero and the body equals the full content box - identical
47
55
  * to a page without them.
48
56
  */
57
+ /**
58
+ * The page's positioning frame: its CONTENT box (paper minus margins). Header, footer and body all
59
+ * resolve a `Positioned` against this same box, so `top`/`bottom` mean the same thing in each.
60
+ *
61
+ * `layoutPageBands` lays the bands out, so its caller must hand it a context carrying this frame -
62
+ * otherwise a `Positioned` in a band has nothing to resolve against (ISSUE-4). A caller that only
63
+ * wants the band HEIGHTS may throw the frame away without draining it: an out-of-flow child
64
+ * contributes no height.
65
+ */
66
+ export declare function pageFrame(config: PDFPageConfig): PositioningFrame;
49
67
  export declare function layoutPageBands(config: PDFPageConfig, header: PDFElement | undefined, footer: PDFElement | undefined, ctx: LayoutContext): PageBands;
50
68
  export declare class PageElement extends PDFElement {
51
69
  private config;