@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.
- package/README.md +5 -2
- package/dist/api/content.d.ts +9 -3
- package/dist/api/content.js +13 -3
- package/dist/api/descriptor.js +22 -1
- package/dist/api/dimension.d.ts +20 -0
- package/dist/api/dimension.js +16 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/layout.d.ts +65 -3
- package/dist/api/layout.js +71 -2
- package/dist/api/page-builder.d.ts +32 -0
- package/dist/api/page-builder.js +32 -0
- package/dist/api/structure.d.ts +19 -0
- package/dist/api/structure.js +25 -0
- package/dist/api/table.js +13 -5
- package/dist/api/text.d.ts +10 -1
- package/dist/api/text.js +32 -4
- package/dist/elements/container-element.d.ts +17 -1
- package/dist/elements/container-element.js +54 -22
- package/dist/elements/image-element.d.ts +23 -1
- package/dist/elements/image-element.js +54 -11
- package/dist/elements/index.d.ts +6 -0
- package/dist/elements/index.js +6 -0
- package/dist/elements/layout/anchor-element.d.ts +23 -0
- package/dist/elements/layout/anchor-element.js +27 -0
- package/dist/elements/layout/bookmark-element.d.ts +27 -0
- package/dist/elements/layout/bookmark-element.js +30 -0
- package/dist/elements/layout/link-element.d.ts +33 -0
- package/dist/elements/layout/link-element.js +39 -0
- package/dist/elements/layout/page-builder-element.d.ts +32 -0
- package/dist/elements/layout/page-builder-element.js +43 -0
- package/dist/elements/layout/rotated-box-element.d.ts +35 -0
- package/dist/elements/layout/rotated-box-element.js +54 -0
- package/dist/elements/layout/rotated-element.d.ts +32 -0
- package/dist/elements/layout/rotated-element.js +41 -0
- package/dist/elements/layout/struct-group.d.ts +28 -0
- package/dist/elements/layout/struct-group.js +42 -0
- package/dist/elements/page-element.d.ts +8 -0
- package/dist/elements/page-element.js +13 -1
- package/dist/elements/pdf-element.d.ts +35 -0
- package/dist/elements/pdf-element.js +34 -0
- package/dist/elements/rectangle-element.d.ts +11 -1
- package/dist/elements/rectangle-element.js +49 -13
- package/dist/elements/row-element.d.ts +11 -1
- package/dist/elements/row-element.js +37 -8
- package/dist/elements/text-element.d.ts +11 -1
- package/dist/elements/text-element.js +5 -2
- package/dist/ir/display-list.d.ts +137 -1
- package/dist/layout/box-constraints.d.ts +8 -0
- package/dist/layout/box-constraints.js +14 -0
- package/dist/layout/collect-images.d.ts +9 -0
- package/dist/layout/collect-images.js +34 -0
- package/dist/layout/fragmentation.js +9 -2
- package/dist/renderer/anchor-renderer.d.ts +6 -0
- package/dist/renderer/anchor-renderer.js +11 -0
- package/dist/renderer/bookmark-renderer.d.ts +6 -0
- package/dist/renderer/bookmark-renderer.js +11 -0
- package/dist/renderer/emoji-image.d.ts +9 -0
- package/dist/renderer/emoji-image.js +68 -0
- package/dist/renderer/image-renderer.d.ts +1 -1
- package/dist/renderer/image-renderer.js +10 -2
- package/dist/renderer/link-renderer.d.ts +6 -0
- package/dist/renderer/link-renderer.js +11 -0
- package/dist/renderer/page-builder-renderer.d.ts +6 -0
- package/dist/renderer/page-builder-renderer.js +11 -0
- package/dist/renderer/page-renderer.js +54 -4
- package/dist/renderer/pdf-backend.d.ts +16 -2
- package/dist/renderer/pdf-backend.js +135 -3
- package/dist/renderer/pdf-document-renderer.d.ts +16 -7
- package/dist/renderer/pdf-document-renderer.js +65 -33
- package/dist/renderer/pdf-renderer.js +58 -2
- package/dist/renderer/rotated-renderer.d.ts +7 -0
- package/dist/renderer/rotated-renderer.js +29 -0
- package/dist/renderer/struct-group-renderer.d.ts +10 -0
- package/dist/renderer/struct-group-renderer.js +24 -0
- package/dist/renderer/text-renderer.d.ts +3 -0
- package/dist/renderer/text-renderer.js +206 -5
- package/dist/text/emoji-codepoints.d.ts +1 -0
- package/dist/text/emoji-codepoints.js +29 -0
- package/dist/text/line-breaker.js +6 -3
- package/dist/utils/afm-parser.js +6 -4
- package/dist/utils/dest-registry.d.ts +19 -0
- package/dist/utils/dest-registry.js +32 -0
- package/dist/utils/flex-layout.d.ts +10 -3
- package/dist/utils/flex-layout.js +26 -6
- package/dist/utils/outline.d.ts +21 -0
- package/dist/utils/outline.js +80 -0
- package/dist/utils/pdf-object-manager.d.ts +26 -1
- package/dist/utils/pdf-object-manager.js +0 -0
- package/dist/utils/struct-tree.d.ts +44 -0
- package/dist/utils/struct-tree.js +130 -0
- package/dist/utils/ttf-parser.d.ts +73 -0
- package/dist/utils/ttf-parser.js +456 -16
- package/dist/utils/ua-xmp.d.ts +9 -0
- package/dist/utils/ua-xmp.js +32 -0
- 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
|
-
|
|
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
|
-
|
|
66
|
-
? constraints.constrainWidth(
|
|
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 =
|
|
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 =
|
|
100
|
+
const shrinkWrapHeight = explicitHeight === undefined && !constraints.hasBoundedHeight;
|
|
77
101
|
this.height =
|
|
78
|
-
|
|
79
|
-
? constraints.constrainHeight(
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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;
|