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

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 (116) hide show
  1. package/README.md +7 -7
  2. package/dist/api/descriptor.js +26 -1
  3. package/dist/api/forms.d.ts +347 -0
  4. package/dist/api/forms.js +189 -0
  5. package/dist/api/index.d.ts +1 -0
  6. package/dist/api/index.js +1 -0
  7. package/dist/api/layout.d.ts +47 -14
  8. package/dist/api/layout.js +47 -11
  9. package/dist/api/structure.d.ts +11 -0
  10. package/dist/api/structure.js +2 -0
  11. package/dist/api/text.d.ts +18 -2
  12. package/dist/api/text.js +15 -0
  13. package/dist/crypto/legacy-handler.d.ts +43 -0
  14. package/dist/crypto/legacy-handler.js +145 -0
  15. package/dist/crypto/security-handler.d.ts +16 -0
  16. package/dist/crypto/security-handler.js +26 -0
  17. package/dist/edit/acroform-reader.d.ts +70 -0
  18. package/dist/edit/acroform-reader.js +126 -0
  19. package/dist/edit/appearance.d.ts +47 -0
  20. package/dist/edit/appearance.js +171 -0
  21. package/dist/edit/document.d.ts +135 -0
  22. package/dist/edit/document.js +649 -0
  23. package/dist/edit/fill.d.ts +48 -0
  24. package/dist/edit/fill.js +395 -0
  25. package/dist/edit/flatten.d.ts +42 -0
  26. package/dist/edit/flatten.js +437 -0
  27. package/dist/edit/index.d.ts +18 -0
  28. package/dist/edit/index.js +16 -0
  29. package/dist/edit/lexer.d.ts +30 -0
  30. package/dist/edit/lexer.js +285 -0
  31. package/dist/edit/objects.d.ts +63 -0
  32. package/dist/edit/objects.js +56 -0
  33. package/dist/edit/writer.d.ts +53 -0
  34. package/dist/edit/writer.js +261 -0
  35. package/dist/elements/container-element.d.ts +10 -1
  36. package/dist/elements/container-element.js +26 -8
  37. package/dist/elements/forms/checkbox-element.d.ts +51 -0
  38. package/dist/elements/forms/checkbox-element.js +48 -0
  39. package/dist/elements/forms/choice-element.d.ts +53 -0
  40. package/dist/elements/forms/choice-element.js +59 -0
  41. package/dist/elements/forms/push-button-element.d.ts +56 -0
  42. package/dist/elements/forms/push-button-element.js +49 -0
  43. package/dist/elements/forms/radio-element.d.ts +52 -0
  44. package/dist/elements/forms/radio-element.js +49 -0
  45. package/dist/elements/forms/signature-element.d.ts +54 -0
  46. package/dist/elements/forms/signature-element.js +47 -0
  47. package/dist/elements/forms/text-field-element.d.ts +61 -0
  48. package/dist/elements/forms/text-field-element.js +54 -0
  49. package/dist/elements/index.d.ts +8 -0
  50. package/dist/elements/index.js +8 -0
  51. package/dist/elements/layout/keep-together-element.d.ts +42 -0
  52. package/dist/elements/layout/keep-together-element.js +85 -0
  53. package/dist/elements/layout/padding-element.d.ts +3 -0
  54. package/dist/elements/layout/padding-element.js +12 -0
  55. package/dist/elements/layout/page-break-element.d.ts +17 -0
  56. package/dist/elements/layout/page-break-element.js +24 -0
  57. package/dist/elements/layout/positioned-element.d.ts +10 -7
  58. package/dist/elements/layout/positioned-element.js +21 -9
  59. package/dist/elements/page-element.d.ts +11 -1
  60. package/dist/elements/page-element.js +41 -16
  61. package/dist/elements/pdf-element.d.ts +28 -0
  62. package/dist/elements/pdf-element.js +32 -0
  63. package/dist/elements/rectangle-element.d.ts +10 -1
  64. package/dist/elements/rectangle-element.js +27 -6
  65. package/dist/elements/row-element.d.ts +9 -1
  66. package/dist/elements/row-element.js +9 -1
  67. package/dist/elements/text-element.d.ts +30 -4
  68. package/dist/elements/text-element.js +40 -17
  69. package/dist/forms/acroform.d.ts +63 -0
  70. package/dist/forms/acroform.js +427 -0
  71. package/dist/forms/appearance.d.ts +58 -0
  72. package/dist/forms/appearance.js +204 -0
  73. package/dist/forms/field.d.ts +164 -0
  74. package/dist/forms/field.js +1 -0
  75. package/dist/forms/pdf.d.ts +19 -0
  76. package/dist/forms/pdf.js +28 -0
  77. package/dist/ir/display-list.d.ts +18 -1
  78. package/dist/layout/fragmentation.d.ts +12 -0
  79. package/dist/layout/fragmentation.js +45 -8
  80. package/dist/renderer/form-field-renderer.d.ts +20 -0
  81. package/dist/renderer/form-field-renderer.js +12 -0
  82. package/dist/renderer/keep-together-renderer.d.ts +10 -0
  83. package/dist/renderer/keep-together-renderer.js +12 -0
  84. package/dist/renderer/page-break-renderer.d.ts +14 -0
  85. package/dist/renderer/page-break-renderer.js +15 -0
  86. package/dist/renderer/page-renderer.js +8 -2
  87. package/dist/renderer/pdf-backend.d.ts +7 -0
  88. package/dist/renderer/pdf-backend.js +48 -6
  89. package/dist/renderer/pdf-document-class.js +3 -0
  90. package/dist/renderer/pdf-document-renderer.js +26 -7
  91. package/dist/renderer/pdf-renderer.js +25 -1
  92. package/dist/renderer/text-renderer.d.ts +1 -1
  93. package/dist/renderer/text-renderer.js +107 -59
  94. package/dist/text/advance.d.ts +34 -0
  95. package/dist/text/advance.js +42 -0
  96. package/dist/text/line-breaker.d.ts +9 -8
  97. package/dist/text/line-breaker.js +28 -21
  98. package/dist/text/line-metrics.d.ts +49 -0
  99. package/dist/text/line-metrics.js +57 -0
  100. package/dist/text/text-decoration.d.ts +50 -0
  101. package/dist/text/text-decoration.js +62 -0
  102. package/dist/text/text-style.d.ts +13 -3
  103. package/dist/text/text-style.js +10 -3
  104. package/dist/utils/afm-parser.d.ts +34 -0
  105. package/dist/utils/afm-parser.js +67 -0
  106. package/dist/utils/dest-registry.d.ts +5 -2
  107. package/dist/utils/dest-registry.js +5 -5
  108. package/dist/utils/font-metrics.d.ts +14 -0
  109. package/dist/utils/outline.js +1 -2
  110. package/dist/utils/pdf-object-manager.d.ts +45 -1
  111. package/dist/utils/pdf-object-manager.js +193 -26
  112. package/dist/utils/struct-tree.js +2 -3
  113. package/dist/utils/ttf-parser.d.ts +53 -0
  114. package/dist/utils/ttf-parser.js +368 -0
  115. package/dist/validators/element-validator.js +7 -3
  116. package/package.json +9 -2
@@ -0,0 +1,49 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { SizedPDFElement } from "../pdf-element.js";
3
+ /**
4
+ * A push button (AcroForm /Btn, Pushbutton flag). Holds no value - it fires an action. Draws nothing
5
+ * itself: it reserves its box and emits a `formfield` IR node whose caption is baked into an /AP at the
6
+ * seam, so the button looks identical in every viewer and in print.
7
+ */
8
+ export class PushButtonElement extends SizedPDFElement {
9
+ constructor(p) {
10
+ super({ x: 0, y: 0, width: p.width, height: p.height });
11
+ const fontSize = p.fontSize ?? 12;
12
+ this.spec = {
13
+ kind: "pushbutton",
14
+ name: p.name,
15
+ label: p.label,
16
+ action: p.action,
17
+ tooltip: p.tooltip,
18
+ readOnly: p.readOnly,
19
+ required: p.required,
20
+ hidden: p.hidden,
21
+ print: p.print,
22
+ };
23
+ this.style = {
24
+ border: p.border ?? new Color(154, 164, 178),
25
+ background: p.background ?? new Color(238, 240, 245),
26
+ color: p.color ?? new Color(0, 0, 0),
27
+ fontSize,
28
+ borderWidth: p.borderWidth ?? 1,
29
+ };
30
+ this.requested = { width: p.width, height: p.height, fontSize };
31
+ }
32
+ calculateLayout(constraints, offset, _ctx) {
33
+ this.x = offset.x;
34
+ this.y = offset.y;
35
+ this.width = this.requested.width ?? (constraints.hasBoundedWidth ? constraints.maxWidth : 120);
36
+ this.height = this.requested.height ?? this.requested.fontSize + 14;
37
+ return { width: this.width, height: this.height };
38
+ }
39
+ getProps() {
40
+ return {
41
+ x: this.x,
42
+ y: this.y,
43
+ width: this.width ?? 0,
44
+ height: this.height ?? 0,
45
+ spec: this.spec,
46
+ style: this.style,
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,52 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ import { LayoutContext, SizedPDFElement } from "../pdf-element.js";
4
+ import type { FieldStyle, RadioSpec } from "../../forms/field.js";
5
+ export interface RadioParams {
6
+ /** The shared group name - radios with the same `group` are one mutually-exclusive field. */
7
+ group: string;
8
+ /** This button's export value - unique within the group. */
9
+ value: string;
10
+ /** Whether this button starts selected (at most one per group should be). */
11
+ selected?: boolean;
12
+ /** Tooltip / accessible name. */
13
+ tooltip?: string;
14
+ /** Show but do not allow changing the group. */
15
+ readOnly?: boolean;
16
+ /** Mark it as a field that must be filled in before submitting. */
17
+ required?: boolean;
18
+ /** Hide the widget entirely - neither on screen nor in print. */
19
+ hidden?: boolean;
20
+ /** Include the widget when printing (default true). */
21
+ print?: boolean;
22
+ /** Button diameter in points (default 14). */
23
+ size?: number;
24
+ /** Dot colour (default black). */
25
+ color?: Color;
26
+ /** Ring colour (default a mid grey). */
27
+ border?: Color;
28
+ /** Fill behind the ring. */
29
+ background?: Color;
30
+ /** Ring thickness (default 1). */
31
+ borderWidth?: number;
32
+ }
33
+ /**
34
+ * One radio button (AcroForm /Btn, Radio flag). A small round leaf; several sharing a `group` become one
35
+ * mutually-exclusive field (see `RadioGroup` for the ergonomic wrapper). Draws nothing itself - it emits a
36
+ * `formfield` IR node whose radio spec bakes the ring + dot /AP at the seam.
37
+ */
38
+ export declare class RadioElement extends SizedPDFElement {
39
+ private spec;
40
+ private style;
41
+ private size;
42
+ constructor(p: RadioParams);
43
+ calculateLayout(_constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
44
+ getProps(): {
45
+ x: number;
46
+ y: number;
47
+ width: number;
48
+ height: number;
49
+ spec: RadioSpec;
50
+ style: FieldStyle;
51
+ };
52
+ }
@@ -0,0 +1,49 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { SizedPDFElement } from "../pdf-element.js";
3
+ /**
4
+ * One radio button (AcroForm /Btn, Radio flag). A small round leaf; several sharing a `group` become one
5
+ * mutually-exclusive field (see `RadioGroup` for the ergonomic wrapper). Draws nothing itself - it emits a
6
+ * `formfield` IR node whose radio spec bakes the ring + dot /AP at the seam.
7
+ */
8
+ export class RadioElement extends SizedPDFElement {
9
+ constructor(p) {
10
+ const size = p.size ?? 14;
11
+ super({ x: 0, y: 0, width: size, height: size });
12
+ this.size = size;
13
+ this.spec = {
14
+ kind: "radio",
15
+ group: p.group,
16
+ value: p.value,
17
+ selected: p.selected,
18
+ tooltip: p.tooltip,
19
+ readOnly: p.readOnly,
20
+ required: p.required,
21
+ hidden: p.hidden,
22
+ print: p.print,
23
+ };
24
+ this.style = {
25
+ border: p.border ?? new Color(102, 102, 102),
26
+ background: p.background,
27
+ color: p.color ?? new Color(0, 0, 0),
28
+ fontSize: 0,
29
+ borderWidth: p.borderWidth ?? 1,
30
+ };
31
+ }
32
+ calculateLayout(_constraints, offset, _ctx) {
33
+ this.x = offset.x;
34
+ this.y = offset.y;
35
+ this.width = this.size;
36
+ this.height = this.size;
37
+ return { width: this.size, height: this.size };
38
+ }
39
+ getProps() {
40
+ return {
41
+ x: this.x,
42
+ y: this.y,
43
+ width: this.width ?? 0,
44
+ height: this.height ?? 0,
45
+ spec: this.spec,
46
+ style: this.style,
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,54 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ import { LayoutContext, SizedPDFElement } from "../pdf-element.js";
4
+ import type { FieldStyle, SignatureSpec } from "../../forms/field.js";
5
+ export interface SignatureFieldParams {
6
+ /** The field name (/T) - unique in the document. */
7
+ name: string;
8
+ /** A hint drawn inside the empty box, e.g. "Signature". */
9
+ label?: string;
10
+ /** Tooltip / accessible name. */
11
+ tooltip?: string;
12
+ /** Draw it but do not let it be signed. */
13
+ readOnly?: boolean;
14
+ /** Mark it as a field that must be filled in before submitting. */
15
+ required?: boolean;
16
+ /** Hide the widget entirely - neither on screen nor in print. */
17
+ hidden?: boolean;
18
+ /** Include the widget when printing (default true). */
19
+ print?: boolean;
20
+ /** Box width in points; omit to fill the offered width. */
21
+ width?: number;
22
+ /** Box height in points (default 48 - room for a signature). */
23
+ height?: number;
24
+ /** Hint font size (default 9). */
25
+ fontSize?: number;
26
+ /** Hint colour (default a muted grey). */
27
+ color?: Color;
28
+ /** Box border + signing-rule colour (default a mid grey). */
29
+ border?: Color;
30
+ /** Box background fill. */
31
+ background?: Color;
32
+ /** Border thickness (default 1). */
33
+ borderWidth?: number;
34
+ }
35
+ /**
36
+ * A signature field (AcroForm /Sig) - a PLACEHOLDER someone can sign later. jasy creates the field and
37
+ * its empty "sign here" appearance; it does not sign (that needs a certificate and a byte-range digest -
38
+ * a separate feature). Draws nothing itself: it reserves its box and emits a `formfield` IR node.
39
+ */
40
+ export declare class SignatureFieldElement extends SizedPDFElement {
41
+ private spec;
42
+ private style;
43
+ private requested;
44
+ constructor(p: SignatureFieldParams);
45
+ calculateLayout(constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
46
+ getProps(): {
47
+ x: number;
48
+ y: number;
49
+ width: number;
50
+ height: number;
51
+ spec: SignatureSpec;
52
+ style: FieldStyle;
53
+ };
54
+ }
@@ -0,0 +1,47 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { SizedPDFElement } from "../pdf-element.js";
3
+ /**
4
+ * A signature field (AcroForm /Sig) - a PLACEHOLDER someone can sign later. jasy creates the field and
5
+ * its empty "sign here" appearance; it does not sign (that needs a certificate and a byte-range digest -
6
+ * a separate feature). Draws nothing itself: it reserves its box and emits a `formfield` IR node.
7
+ */
8
+ export class SignatureFieldElement extends SizedPDFElement {
9
+ constructor(p) {
10
+ super({ x: 0, y: 0, width: p.width, height: p.height });
11
+ this.spec = {
12
+ kind: "signature",
13
+ name: p.name,
14
+ label: p.label,
15
+ tooltip: p.tooltip,
16
+ readOnly: p.readOnly,
17
+ required: p.required,
18
+ hidden: p.hidden,
19
+ print: p.print,
20
+ };
21
+ this.style = {
22
+ border: p.border ?? new Color(154, 164, 178),
23
+ background: p.background,
24
+ color: p.color ?? new Color(107, 114, 128),
25
+ fontSize: p.fontSize ?? 9,
26
+ borderWidth: p.borderWidth ?? 1,
27
+ };
28
+ this.requested = { width: p.width, height: p.height };
29
+ }
30
+ calculateLayout(constraints, offset, _ctx) {
31
+ this.x = offset.x;
32
+ this.y = offset.y;
33
+ this.width = this.requested.width ?? (constraints.hasBoundedWidth ? constraints.maxWidth : 240);
34
+ this.height = this.requested.height ?? 48;
35
+ return { width: this.width, height: this.height };
36
+ }
37
+ getProps() {
38
+ return {
39
+ x: this.x,
40
+ y: this.y,
41
+ width: this.width ?? 0,
42
+ height: this.height ?? 0,
43
+ spec: this.spec,
44
+ style: this.style,
45
+ };
46
+ }
47
+ }
@@ -0,0 +1,61 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ import { LayoutContext, SizedPDFElement } from "../pdf-element.js";
4
+ import type { FieldAlign, FieldStyle, TextFieldSpec } from "../../forms/field.js";
5
+ export interface TextFieldParams {
6
+ /** The field name (/T) - unique in the document; you read the value back by it. */
7
+ name: string;
8
+ /** Initial value (/V). */
9
+ value?: string;
10
+ /** Tooltip / accessible name (/TU). */
11
+ tooltip?: string;
12
+ /** Wrap across lines instead of one line. */
13
+ multiline?: boolean;
14
+ /** Mask the input. */
15
+ password?: boolean;
16
+ /** Maximum character count. */
17
+ maxLength?: number;
18
+ /** Show but do not allow editing. */
19
+ readOnly?: boolean;
20
+ /** How the value sits in the box (default left). */
21
+ align?: FieldAlign;
22
+ /** Mark it as a field that must be filled in before submitting. */
23
+ required?: boolean;
24
+ /** Hide the widget entirely - neither on screen nor in print. */
25
+ hidden?: boolean;
26
+ /** Include the widget when printing (default true). */
27
+ print?: boolean;
28
+ /** Box width in points; omit to fill the offered width. */
29
+ width?: number;
30
+ /** Box height in points; omit for a single-line default (`multiline` wants an explicit height). */
31
+ height?: number;
32
+ /** Value font size; `0` = viewer auto-size. */
33
+ fontSize?: number;
34
+ /** Value text colour. */
35
+ color?: Color;
36
+ /** Box border colour (omit for no border). */
37
+ border?: Color;
38
+ /** Box background fill (omit for transparent). */
39
+ background?: Color;
40
+ /** Border thickness in points. */
41
+ borderWidth?: number;
42
+ }
43
+ /**
44
+ * A text input field (AcroForm /Tx). A leaf that draws NOTHING itself: it reserves its box in the layout
45
+ * and emits a `formfield` IR node that becomes a Widget annotation + /AcroForm field at the seam.
46
+ */
47
+ export declare class TextFieldElement extends SizedPDFElement {
48
+ private spec;
49
+ private style;
50
+ private requested;
51
+ constructor(p: TextFieldParams);
52
+ calculateLayout(constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
53
+ getProps(): {
54
+ x: number;
55
+ y: number;
56
+ width: number;
57
+ height: number;
58
+ spec: TextFieldSpec;
59
+ style: FieldStyle;
60
+ };
61
+ }
@@ -0,0 +1,54 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { SizedPDFElement } from "../pdf-element.js";
3
+ /**
4
+ * A text input field (AcroForm /Tx). A leaf that draws NOTHING itself: it reserves its box in the layout
5
+ * and emits a `formfield` IR node that becomes a Widget annotation + /AcroForm field at the seam.
6
+ */
7
+ export class TextFieldElement extends SizedPDFElement {
8
+ constructor(p) {
9
+ super({ x: 0, y: 0, width: p.width, height: p.height });
10
+ const fontSize = p.fontSize ?? 12;
11
+ this.spec = {
12
+ kind: "text",
13
+ name: p.name,
14
+ value: p.value,
15
+ tooltip: p.tooltip,
16
+ multiline: p.multiline,
17
+ password: p.password,
18
+ maxLength: p.maxLength,
19
+ readOnly: p.readOnly,
20
+ align: p.align,
21
+ required: p.required,
22
+ hidden: p.hidden,
23
+ print: p.print,
24
+ };
25
+ this.style = {
26
+ border: p.border,
27
+ background: p.background,
28
+ color: p.color ?? new Color(0, 0, 0),
29
+ fontSize,
30
+ borderWidth: p.borderWidth ?? 1,
31
+ };
32
+ this.requested = { width: p.width, height: p.height, fontSize };
33
+ }
34
+ calculateLayout(constraints, offset, _ctx) {
35
+ this.x = offset.x;
36
+ this.y = offset.y;
37
+ // Width: explicit, else fill the offered box (a form field with no width spans the column).
38
+ this.width = this.requested.width ?? (constraints.hasBoundedWidth ? constraints.maxWidth : 200);
39
+ // Height: explicit, else a comfortable single line (font size + padding). Multiline without an
40
+ // explicit height still gets the single-line default - the caller should size a multiline box.
41
+ this.height = this.requested.height ?? this.requested.fontSize + 9;
42
+ return { width: this.width, height: this.height };
43
+ }
44
+ getProps() {
45
+ return {
46
+ x: this.x,
47
+ y: this.y,
48
+ width: this.width ?? 0,
49
+ height: this.height ?? 0,
50
+ spec: this.spec,
51
+ style: this.style,
52
+ };
53
+ }
54
+ }
@@ -6,6 +6,14 @@ export * from "./rectangle-element.js";
6
6
  export * from "./image-element.js";
7
7
  export * from "./layout/expanded-element.js";
8
8
  export * from "./layout/padding-element.js";
9
+ export * from "./layout/page-break-element.js";
10
+ export * from "./layout/keep-together-element.js";
11
+ export * from "./forms/text-field-element.js";
12
+ export * from "./forms/checkbox-element.js";
13
+ export * from "./forms/radio-element.js";
14
+ export * from "./forms/choice-element.js";
15
+ export * from "./forms/push-button-element.js";
16
+ export * from "./forms/signature-element.js";
9
17
  export * from "./layout/default-text-style-element.js";
10
18
  export * from "./layout/positioned-element.js";
11
19
  export * from "./layout/link-element.js";
@@ -6,6 +6,14 @@ export * from "./rectangle-element.js";
6
6
  export * from "./image-element.js";
7
7
  export * from "./layout/expanded-element.js";
8
8
  export * from "./layout/padding-element.js";
9
+ export * from "./layout/page-break-element.js";
10
+ export * from "./layout/keep-together-element.js";
11
+ export * from "./forms/text-field-element.js";
12
+ export * from "./forms/checkbox-element.js";
13
+ export * from "./forms/radio-element.js";
14
+ export * from "./forms/choice-element.js";
15
+ export * from "./forms/push-button-element.js";
16
+ export * from "./forms/signature-element.js";
9
17
  export * from "./layout/default-text-style-element.js";
10
18
  export * from "./layout/positioned-element.js";
11
19
  export * from "./layout/link-element.js";
@@ -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
+ }
@@ -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
+ }
@@ -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;