@jasy/pdf 1.0.0-alpha.5 → 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/dist/api/content.d.ts +6 -3
- package/dist/api/content.js +12 -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/text.d.ts +6 -0
- package/dist/api/text.js +31 -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 +19 -1
- package/dist/elements/image-element.js +52 -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/page-element.d.ts +8 -0
- package/dist/elements/page-element.js +13 -1
- package/dist/elements/pdf-element.d.ts +31 -0
- package/dist/elements/pdf-element.js +19 -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 +4 -0
- package/dist/ir/display-list.d.ts +59 -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/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/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 +37 -2
- package/dist/renderer/pdf-backend.d.ts +10 -0
- package/dist/renderer/pdf-backend.js +77 -1
- 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 +47 -2
- package/dist/renderer/rotated-renderer.d.ts +7 -0
- package/dist/renderer/rotated-renderer.js +29 -0
- package/dist/renderer/text-renderer.js +26 -5
- 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 +6 -0
- package/dist/utils/pdf-object-manager.js +0 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -49,6 +65,21 @@ export declare abstract class PDFElement {
|
|
|
49
65
|
* happens once at the IR -> backend seam.
|
|
50
66
|
*/
|
|
51
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;
|
|
52
83
|
}
|
|
53
84
|
export declare abstract class SizedPDFElement extends PDFElement {
|
|
54
85
|
protected x: number;
|
|
@@ -14,6 +14,25 @@ export class PDFElement {
|
|
|
14
14
|
this._structId = from._structId;
|
|
15
15
|
return this;
|
|
16
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
|
+
}
|
|
17
36
|
}
|
|
18
37
|
export class SizedPDFElement extends PDFElement {
|
|
19
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;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { resolveExtent } from "../layout/box-constraints.js";
|
|
1
2
|
import { FlexLayoutHelper, HORIZONTAL_AXIS } from "../utils/flex-layout.js";
|
|
2
|
-
import { SizedPDFElement } from "./pdf-element.js";
|
|
3
|
+
import { FlexiblePDFElement, SizedPDFElement, } from "./pdf-element.js";
|
|
3
4
|
/**
|
|
4
5
|
* Horizontal stack: the mirror of `ContainerElement` (Column). Children are laid out
|
|
5
6
|
* left-to-right via the shared `FlexLayoutHelper` on the horizontal axis; fixed children
|
|
@@ -13,26 +14,54 @@ import { SizedPDFElement } from "./pdf-element.js";
|
|
|
13
14
|
* Grid/Table concern.
|
|
14
15
|
*/
|
|
15
16
|
export class RowElement extends SizedPDFElement {
|
|
16
|
-
constructor({ children, gap, main, cross }) {
|
|
17
|
+
constructor({ children, gap, main, cross, width, height, widthFactor, heightFactor, }) {
|
|
17
18
|
super({ x: 0, y: 0 });
|
|
18
19
|
this.children = children;
|
|
19
20
|
this.gap = gap ?? 0;
|
|
20
21
|
this.main = main ?? "start";
|
|
21
22
|
this.cross = cross ?? "stretch";
|
|
23
|
+
this.requested = { width, height, widthFactor, heightFactor };
|
|
24
|
+
}
|
|
25
|
+
relativeSizeFactor(horizontal) {
|
|
26
|
+
return horizontal ? this.requested.widthFactor : this.requested.heightFactor;
|
|
27
|
+
}
|
|
28
|
+
/** The mirror of `ContainerElement`: a Row's OWN flex child needs a bounded width, and the need of any
|
|
29
|
+
* descendant propagates on both axes (see the Column for the reasoning). */
|
|
30
|
+
needsBoundedMain(horizontal) {
|
|
31
|
+
const requested = horizontal
|
|
32
|
+
? [this.requested.width, this.requested.widthFactor]
|
|
33
|
+
: [this.requested.height, this.requested.heightFactor];
|
|
34
|
+
if (requested.some((v) => v !== undefined))
|
|
35
|
+
return false;
|
|
36
|
+
const ownFlexChild = horizontal && this.children.some((c) => c instanceof FlexiblePDFElement);
|
|
37
|
+
return ownFlexChild || this.children.some((c) => c.needsBoundedMain(horizontal));
|
|
22
38
|
}
|
|
23
39
|
calculateLayout(constraints, offset, ctx) {
|
|
24
40
|
this.x = offset.x;
|
|
25
41
|
this.y = offset.y;
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
const
|
|
29
|
-
const
|
|
42
|
+
// Relative sizing: a pinned extent (fixed or a fraction of the offered box, clamped) wins; else
|
|
43
|
+
// width fills the offered space (flex children split the leftover) and height is the tallest child.
|
|
44
|
+
const explicitWidth = resolveExtent(this.requested.width, this.requested.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
|
|
45
|
+
const explicitHeight = resolveExtent(this.requested.height, this.requested.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
|
|
46
|
+
const boundedWidth = explicitWidth !== undefined
|
|
47
|
+
? constraints.constrainWidth(explicitWidth)
|
|
48
|
+
: constraints.hasBoundedWidth
|
|
49
|
+
? constraints.maxWidth
|
|
50
|
+
: undefined;
|
|
51
|
+
const boundedHeight = explicitHeight !== undefined
|
|
52
|
+
? constraints.constrainHeight(explicitHeight)
|
|
53
|
+
: constraints.hasBoundedHeight
|
|
54
|
+
? constraints.maxHeight
|
|
55
|
+
: undefined;
|
|
56
|
+
// Horizontal stack: main = width (children fill it), cross = height (children can stretch to it).
|
|
57
|
+
const mainAvail = boundedWidth ?? Infinity;
|
|
58
|
+
const crossAvail = boundedHeight ?? Infinity;
|
|
30
59
|
let result = { mainUsed: 0, crossUsed: 0 };
|
|
31
60
|
if (this.children.length > 0) {
|
|
32
61
|
result = FlexLayoutHelper.layout(this.children, HORIZONTAL_AXIS, mainAvail, crossAvail, this.x, this.y, { gap: this.gap, main: this.main, cross: this.cross }, ctx);
|
|
33
62
|
}
|
|
34
|
-
this.width =
|
|
35
|
-
this.height =
|
|
63
|
+
this.width = boundedWidth ?? result.mainUsed;
|
|
64
|
+
this.height = boundedHeight ?? result.crossUsed;
|
|
36
65
|
return { width: this.width, height: this.height };
|
|
37
66
|
}
|
|
38
67
|
getProps() {
|
|
@@ -10,6 +10,10 @@ export interface TextSegment {
|
|
|
10
10
|
fontColor?: Color;
|
|
11
11
|
fontFamily?: string;
|
|
12
12
|
fontSize?: number;
|
|
13
|
+
/** External URL: this segment becomes an inline hyperlink (a /Link annotation over its glyphs). */
|
|
14
|
+
href?: string;
|
|
15
|
+
/** Internal named destination (an `Anchor`): this segment links to it (a /GoTo /Link annotation). */
|
|
16
|
+
dest?: string;
|
|
13
17
|
}
|
|
14
18
|
/** Accessibility role for the tagged structure tree: a heading level or a paragraph (the default). */
|
|
15
19
|
export type TextRole = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p";
|
|
@@ -103,6 +103,64 @@ export interface ClipPush {
|
|
|
103
103
|
export interface ClipPop {
|
|
104
104
|
type: "clip-pop";
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* A hyperlink over a rectangle. Unlike every other IR node this draws NOTHING into the content stream -
|
|
108
|
+
* it becomes a PDF Link ANNOTATION on the page (`/Annots`), a side channel next to the drawing. Exactly
|
|
109
|
+
* one target is set: `href` is an external URL (a `/URI` action); `dest` is a named destination inside
|
|
110
|
+
* this document (a `/GoTo` action, resolved via the catalog `/Names /Dests` tree - see `Anchor`). The
|
|
111
|
+
* rect is the clickable region in top-left engine coords; the Y-flip converts it to PDF page space.
|
|
112
|
+
*
|
|
113
|
+
* NOTE: a Link inside a `TransformPush` is NOT yet rotated with it - the annotation rect lives in page
|
|
114
|
+
* space and ignores the content-stream matrix. Completing that is a planned follow-up (see todo.md).
|
|
115
|
+
*/
|
|
116
|
+
export interface Link {
|
|
117
|
+
type: "link";
|
|
118
|
+
x: number;
|
|
119
|
+
y: number;
|
|
120
|
+
width: number;
|
|
121
|
+
height: number;
|
|
122
|
+
href?: string;
|
|
123
|
+
dest?: string;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A named destination anchor - a jump TARGET for an internal `Link({ to })`. Like `Link` it draws
|
|
127
|
+
* NOTHING; it registers `name` -> (this page, `y`) in the catalog `/Names /Dests` name tree. `y` is the
|
|
128
|
+
* top of the target in top-left engine coords (the Y-flip turns it into the page-space scroll target).
|
|
129
|
+
*/
|
|
130
|
+
export interface Anchor {
|
|
131
|
+
type: "anchor";
|
|
132
|
+
y: number;
|
|
133
|
+
name: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A document-outline (bookmark) anchor. Like `Link` it draws NOTHING - it becomes an entry in the PDF
|
|
137
|
+
* outline tree (`/Outlines`), a side channel the viewer shows in its bookmark panel. `y` is the top of
|
|
138
|
+
* the target in top-left engine coords (the Y-flip turns it into the page-space scroll target); `level`
|
|
139
|
+
* (1-based) nests the entry under the nearest preceding entry of a smaller level.
|
|
140
|
+
*/
|
|
141
|
+
export interface Outline {
|
|
142
|
+
type: "outline";
|
|
143
|
+
y: number;
|
|
144
|
+
title: string;
|
|
145
|
+
level: number;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Pushes an affine transform (a `q` + `cm` in the content stream): everything between this and the
|
|
149
|
+
* matching `TransformPop` is painted through `matrix`. This is what `Rotated` wraps its child's nodes
|
|
150
|
+
* in, so a subtree draws rotated (or scaled/translated) around a pivot without touching its layout.
|
|
151
|
+
*
|
|
152
|
+
* `matrix` is `[a, b, c, d, e, f]` in the engine's TOP-LEFT coordinates - the same space every other IR
|
|
153
|
+
* node uses. The Y-flip at the IR -> backend seam converts it to PDF's bottom-left once per page, so
|
|
154
|
+
* producers stay coordinate-system-blind (they never see a flipped matrix).
|
|
155
|
+
*/
|
|
156
|
+
export interface TransformPush {
|
|
157
|
+
type: "transform-push";
|
|
158
|
+
matrix: [number, number, number, number, number, number];
|
|
159
|
+
}
|
|
160
|
+
/** Closes the most recent `TransformPush` (restores the graphics state). */
|
|
161
|
+
export interface TransformPop {
|
|
162
|
+
type: "transform-pop";
|
|
163
|
+
}
|
|
106
164
|
/**
|
|
107
165
|
* One drawing command of a `Path`, in absolute page coordinates. Curves are CUBIC because PDF
|
|
108
166
|
* content streams have no quadratic operator - a producer converts TrueType quads to cubics before
|
|
@@ -168,4 +226,4 @@ export interface Path {
|
|
|
168
226
|
tag?: StructTag;
|
|
169
227
|
}
|
|
170
228
|
/** The closed set of primitives the PDF backend knows how to draw. */
|
|
171
|
-
export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop | Path;
|
|
229
|
+
export type IRNode = TextRun | Rect | Line | Image | ClipPush | ClipPop | Path | Link | Outline | Anchor | TransformPush | TransformPop;
|
|
@@ -44,6 +44,14 @@ export declare class BoxConstraints {
|
|
|
44
44
|
/** Returns the constraints clamped to lie within `parent` (Flutter's enforce). */
|
|
45
45
|
enforce(parent: BoxConstraints): BoxConstraints;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolves a sized element's extent on one axis (relative sizing). An explicit point size (`fixed`)
|
|
49
|
+
* always wins; otherwise a `factor` (0..1) of the offered extent, but ONLY when that axis is bounded -
|
|
50
|
+
* a fraction of an unbounded axis has no meaning (Flutter's FractionallySizedBox under unbounded
|
|
51
|
+
* constraints). Returns `undefined` to mean "no explicit extent", i.e. fill the box or shrink-wrap.
|
|
52
|
+
* Shared by every sized element so `width: "50%"` behaves identically across Box / Column / Row / Image.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveExtent(fixed: number | undefined, factor: number | undefined, boundedMax: number, hasBounded: boolean): number | undefined;
|
|
47
55
|
/** The size an element resolves to and returns UP the tree. */
|
|
48
56
|
export interface Size {
|
|
49
57
|
width: number;
|
|
@@ -67,3 +67,17 @@ export class BoxConstraints {
|
|
|
67
67
|
return new BoxConstraints(Math.max(parent.minWidth, Math.min(this.minWidth, parent.maxWidth)), Math.max(parent.minWidth, Math.min(this.maxWidth, parent.maxWidth)), Math.max(parent.minHeight, Math.min(this.minHeight, parent.maxHeight)), Math.max(parent.minHeight, Math.min(this.maxHeight, parent.maxHeight)));
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolves a sized element's extent on one axis (relative sizing). An explicit point size (`fixed`)
|
|
72
|
+
* always wins; otherwise a `factor` (0..1) of the offered extent, but ONLY when that axis is bounded -
|
|
73
|
+
* a fraction of an unbounded axis has no meaning (Flutter's FractionallySizedBox under unbounded
|
|
74
|
+
* constraints). Returns `undefined` to mean "no explicit extent", i.e. fill the box or shrink-wrap.
|
|
75
|
+
* Shared by every sized element so `width: "50%"` behaves identically across Box / Column / Row / Image.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveExtent(fixed, factor, boundedMax, hasBounded) {
|
|
78
|
+
if (fixed !== undefined)
|
|
79
|
+
return fixed;
|
|
80
|
+
if (factor !== undefined && hasBounded)
|
|
81
|
+
return boundedMax * factor;
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PDFElement } from "../elements/pdf-element.js";
|
|
2
|
+
import { ImageElement } from "../elements/image-element.js";
|
|
3
|
+
/**
|
|
4
|
+
* Walks the element tree (via `getProps`, the same shallow-reflection style as the validator) and
|
|
5
|
+
* returns every `ImageElement`. A pre-layout pass uses this to resolve each image's intrinsic pixel
|
|
6
|
+
* size asynchronously: layout is synchronous and cannot await jimp, yet it needs the aspect ratio to
|
|
7
|
+
* give a width-only image its proportional height. Cheap - images are few and this runs once.
|
|
8
|
+
*/
|
|
9
|
+
export declare function collectImageElements(root: PDFElement): ImageElement[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { PDFElement, hasChildrenProp, hasChildProp } from "../elements/pdf-element.js";
|
|
2
|
+
import { ImageElement } from "../elements/image-element.js";
|
|
3
|
+
/**
|
|
4
|
+
* Walks the element tree (via `getProps`, the same shallow-reflection style as the validator) and
|
|
5
|
+
* returns every `ImageElement`. A pre-layout pass uses this to resolve each image's intrinsic pixel
|
|
6
|
+
* size asynchronously: layout is synchronous and cannot await jimp, yet it needs the aspect ratio to
|
|
7
|
+
* give a width-only image its proportional height. Cheap - images are few and this runs once.
|
|
8
|
+
*/
|
|
9
|
+
export function collectImageElements(root) {
|
|
10
|
+
const out = [];
|
|
11
|
+
const visit = (el) => {
|
|
12
|
+
if (el instanceof ImageElement)
|
|
13
|
+
out.push(el);
|
|
14
|
+
const props = el.getProps();
|
|
15
|
+
if (hasChildrenProp(props))
|
|
16
|
+
for (const c of props.children)
|
|
17
|
+
visit(c);
|
|
18
|
+
else if (hasChildProp(props))
|
|
19
|
+
visit(props.child);
|
|
20
|
+
// Wrappers that hold a subtree outside `children`/`child`: a Page's header/footer and a
|
|
21
|
+
// RepeatingHeaderElement's `body` (a Table with a header wraps its rows there). A
|
|
22
|
+
// DeferredElement's `composed` is null until layout runs, so it cannot be reached from this
|
|
23
|
+
// pre-layout pass; an image inside a deferred subtree just skips aspect pre-resolution.
|
|
24
|
+
const w = props;
|
|
25
|
+
if (w.header instanceof PDFElement)
|
|
26
|
+
visit(w.header);
|
|
27
|
+
if (w.footer instanceof PDFElement)
|
|
28
|
+
visit(w.footer);
|
|
29
|
+
if (w.body instanceof PDFElement)
|
|
30
|
+
visit(w.body);
|
|
31
|
+
};
|
|
32
|
+
visit(root);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { AnchorElement } from "../elements/layout/anchor-element.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
|
+
export declare class AnchorRenderer {
|
|
5
|
+
static render(element: AnchorElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
|
+
export class AnchorRenderer {
|
|
3
|
+
static async render(element, objectManager) {
|
|
4
|
+
const { name, child, y } = element.getProps();
|
|
5
|
+
const renderer = RendererRegistry.getRenderer(child);
|
|
6
|
+
const childNodes = renderer ? await renderer(child, objectManager) : [];
|
|
7
|
+
// The child draws normally; an Anchor IR node carries the name + scroll target for the page renderer
|
|
8
|
+
// to register as a named destination (it produces no content-stream ops itself).
|
|
9
|
+
return [...childNodes, { type: "anchor", y, name }];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
|
+
import { BookmarkElement } from "../elements/layout/bookmark-element.js";
|
|
3
|
+
import { IRNode } from "../ir/display-list.js";
|
|
4
|
+
export declare class BookmarkRenderer {
|
|
5
|
+
static render(element: BookmarkElement, objectManager: PDFObjectManager): Promise<IRNode[]>;
|
|
6
|
+
}
|