@jasy/pdf 1.0.0-alpha.5 → 1.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/content.d.ts +6 -3
- package/dist/api/content.js +12 -3
- package/dist/api/descriptor.js +24 -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 +112 -17
- package/dist/api/layout.js +118 -13
- package/dist/api/page-builder.d.ts +32 -0
- package/dist/api/page-builder.js +32 -0
- package/dist/api/structure.d.ts +5 -0
- package/dist/api/structure.js +1 -0
- package/dist/api/text.d.ts +24 -2
- package/dist/api/text.js +46 -4
- package/dist/elements/container-element.d.ts +26 -1
- package/dist/elements/container-element.js +79 -29
- package/dist/elements/image-element.d.ts +19 -1
- package/dist/elements/image-element.js +52 -11
- package/dist/elements/index.d.ts +8 -0
- package/dist/elements/index.js +8 -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/keep-together-element.d.ts +42 -0
- package/dist/elements/layout/keep-together-element.js +85 -0
- package/dist/elements/layout/link-element.d.ts +33 -0
- package/dist/elements/layout/link-element.js +39 -0
- package/dist/elements/layout/padding-element.d.ts +3 -0
- package/dist/elements/layout/padding-element.js +12 -0
- package/dist/elements/layout/page-break-element.d.ts +17 -0
- package/dist/elements/layout/page-break-element.js +24 -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/positioned-element.d.ts +10 -7
- package/dist/elements/layout/positioned-element.js +21 -9
- 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 +19 -1
- package/dist/elements/page-element.js +54 -17
- package/dist/elements/pdf-element.d.ts +59 -0
- package/dist/elements/pdf-element.js +51 -0
- package/dist/elements/rectangle-element.d.ts +20 -1
- package/dist/elements/rectangle-element.js +75 -18
- package/dist/elements/row-element.d.ts +19 -1
- package/dist/elements/row-element.js +45 -8
- package/dist/elements/text-element.d.ts +34 -4
- package/dist/elements/text-element.js +40 -17
- package/dist/ir/display-list.d.ts +60 -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.d.ts +12 -0
- package/dist/layout/fragmentation.js +45 -8
- 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/keep-together-renderer.d.ts +10 -0
- package/dist/renderer/keep-together-renderer.js +12 -0
- package/dist/renderer/link-renderer.d.ts +6 -0
- package/dist/renderer/link-renderer.js +11 -0
- package/dist/renderer/page-break-renderer.d.ts +14 -0
- package/dist/renderer/page-break-renderer.js +15 -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 +17 -0
- package/dist/renderer/pdf-backend.js +117 -7
- package/dist/renderer/pdf-document-class.js +3 -0
- package/dist/renderer/pdf-document-renderer.d.ts +16 -7
- package/dist/renderer/pdf-document-renderer.js +87 -36
- package/dist/renderer/pdf-renderer.js +53 -2
- package/dist/renderer/rotated-renderer.d.ts +7 -0
- package/dist/renderer/rotated-renderer.js +29 -0
- package/dist/renderer/text-renderer.d.ts +1 -1
- package/dist/renderer/text-renderer.js +123 -54
- package/dist/text/advance.d.ts +34 -0
- package/dist/text/advance.js +42 -0
- package/dist/text/line-breaker.d.ts +9 -8
- package/dist/text/line-breaker.js +28 -21
- package/dist/text/line-metrics.d.ts +49 -0
- package/dist/text/line-metrics.js +57 -0
- package/dist/text/text-decoration.d.ts +50 -0
- package/dist/text/text-decoration.js +62 -0
- package/dist/text/text-style.d.ts +13 -3
- package/dist/text/text-style.js +10 -3
- package/dist/utils/afm-parser.d.ts +34 -0
- package/dist/utils/afm-parser.js +73 -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/font-metrics.d.ts +14 -0
- package/dist/utils/outline.d.ts +21 -0
- package/dist/utils/outline.js +80 -0
- package/dist/utils/pdf-object-manager.d.ts +33 -1
- package/dist/utils/pdf-object-manager.js +0 -0
- package/dist/utils/ttf-parser.d.ts +53 -0
- package/dist/utils/ttf-parser.js +368 -0
- package/package.json +1 -1
package/dist/api/content.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ImageElement, CustomImage } from "../elements/image-element.js";
|
|
|
2
2
|
import { PDFElement } from "../elements/pdf-element.js";
|
|
3
3
|
import { ColorInput } from "./color.js";
|
|
4
4
|
import { Insets } from "./insets.js";
|
|
5
|
+
import { SizeInput } from "./dimension.js";
|
|
5
6
|
/** A horizontal rule (locked §4). */
|
|
6
7
|
export interface DividerOptions {
|
|
7
8
|
/** Line color (default a light gray). */
|
|
@@ -22,9 +23,11 @@ export type ImageSource = string | Uint8Array | CustomImage;
|
|
|
22
23
|
/** How the image fills its box (locked §4). Mirrors CSS `object-fit`. */
|
|
23
24
|
export type ImageFit = "none" | "contain" | "cover" | "fill";
|
|
24
25
|
export interface ImageOptions {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/** Size on each axis: points (fixed) or a percentage string like `"50%"` (a fraction of the offered
|
|
27
|
+
* space). Pin exactly ONE axis and the other follows the image's aspect ratio (CSS `height: auto`). */
|
|
28
|
+
width?: SizeInput;
|
|
29
|
+
height?: SizeInput;
|
|
30
|
+
/** Fit within the box (default `none`; `fill` when exactly one axis is pinned so it scales to fit). */
|
|
28
31
|
fit?: ImageFit;
|
|
29
32
|
/** Corner radius in points (rounds the image box). */
|
|
30
33
|
radius?: number;
|
package/dist/api/content.js
CHANGED
|
@@ -3,6 +3,7 @@ import { PaddingElement } from "../elements/layout/padding-element.js";
|
|
|
3
3
|
import { ImageElement, CustomLocalImage, CustomBytesImage, BoxFit, } from "../elements/image-element.js";
|
|
4
4
|
import { toColor } from "./color.js";
|
|
5
5
|
import { toEdges } from "./insets.js";
|
|
6
|
+
import { toDimension } from "./dimension.js";
|
|
6
7
|
const DEFAULT_DIVIDER_COLOR = "lightgray";
|
|
7
8
|
const DEFAULT_DIVIDER_MARGIN = { y: 6 };
|
|
8
9
|
/**
|
|
@@ -35,15 +36,23 @@ const FIT = {
|
|
|
35
36
|
* `CustomImage` for non-filesystem sources. Maps to an `ImageElement`.
|
|
36
37
|
*/
|
|
37
38
|
export function Image(src, opts = {}) {
|
|
39
|
+
const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
|
|
40
|
+
const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
|
|
41
|
+
// Exactly one axis pinned -> the other is derived from the aspect ratio, so scale the image to the
|
|
42
|
+
// resulting box (fit: fill). Both or neither pinned keeps the default fit (none).
|
|
43
|
+
const autoScale = (opts.width !== undefined) !== (opts.height !== undefined);
|
|
44
|
+
const fit = opts.fit ? FIT[opts.fit] : autoScale ? BoxFit.fill : undefined;
|
|
38
45
|
return new ImageElement({
|
|
39
46
|
image: typeof src === "string"
|
|
40
47
|
? new CustomLocalImage(src)
|
|
41
48
|
: src instanceof Uint8Array
|
|
42
49
|
? new CustomBytesImage(src)
|
|
43
50
|
: src,
|
|
44
|
-
width:
|
|
45
|
-
height:
|
|
46
|
-
|
|
51
|
+
width: w?.points,
|
|
52
|
+
height: h?.points,
|
|
53
|
+
widthFactor: w?.factor,
|
|
54
|
+
heightFactor: h?.factor,
|
|
55
|
+
fit,
|
|
47
56
|
radius: opts.radius,
|
|
48
57
|
alt: opts.alt,
|
|
49
58
|
});
|
package/dist/api/descriptor.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Text, Paragraph, span } from "./text.js";
|
|
2
|
-
import { Column, Row, Box, Padding, Spacer, Expanded, Positioned } from "./layout.js";
|
|
2
|
+
import { Column, Row, Box, Padding, Spacer, PageBreak, keepTogether, Expanded, Positioned, Link, Anchor, Bookmark, Rotated, RotatedBox, } from "./layout.js";
|
|
3
3
|
import { Divider, Image } from "./content.js";
|
|
4
4
|
import { Page, Document, DefaultTextStyle } from "./structure.js";
|
|
5
|
+
import { PageNumber, PageCount } from "./page-builder.js";
|
|
5
6
|
import { Table } from "./table.js";
|
|
6
7
|
// Element children (Column/Row/Box/Page/…) → built recursively. Text/Paragraph children are
|
|
7
8
|
// strings or `span` descriptors and become content instead.
|
|
@@ -50,6 +51,14 @@ function slotElement(node) {
|
|
|
50
51
|
const els = (node.children ?? []).map((c) => (typeof c === "string" ? Text(c) : build(c)));
|
|
51
52
|
return els.length === 1 ? els[0] : Column(els);
|
|
52
53
|
}
|
|
54
|
+
// The single child a wrapper takes (`Link`, `Anchor`, `Bookmark`, `Rotated`, ...). Several children are
|
|
55
|
+
// stacked in a Column, so `<JasyLink><Text/><Text/></JasyLink>` behaves like the factory would.
|
|
56
|
+
function wrappedChild(children) {
|
|
57
|
+
const els = children.map((c) => (typeof c === "string" ? Text(c) : build(c)));
|
|
58
|
+
if (els.length === 0)
|
|
59
|
+
throw new Error("A wrapper element needs exactly one child");
|
|
60
|
+
return els.length === 1 ? els[0] : Column(els);
|
|
61
|
+
}
|
|
53
62
|
const REGISTRY = {
|
|
54
63
|
document: (props, children) => {
|
|
55
64
|
const doc = Document(props, elementChildren(children));
|
|
@@ -76,6 +85,8 @@ const REGISTRY = {
|
|
|
76
85
|
padding: (props, children) => Padding(props.insets ?? 0, elementChildren(children)[0]),
|
|
77
86
|
expanded: (props, children) => Expanded(props, elementChildren(children)[0]),
|
|
78
87
|
spacer: (props) => Spacer(props?.flex),
|
|
88
|
+
"page-break": () => PageBreak(),
|
|
89
|
+
"keep-together": (_props, children) => keepTogether(elementChildren(children)),
|
|
79
90
|
divider: (props) => Divider(props),
|
|
80
91
|
image: (props) => Image(props.src, props),
|
|
81
92
|
text: (props, children) => Text(textContent(children), props),
|
|
@@ -89,6 +100,18 @@ const REGISTRY = {
|
|
|
89
100
|
},
|
|
90
101
|
positioned: (props, children) => Positioned(props, elementChildren(children)[0]),
|
|
91
102
|
"default-text-style": (props, children) => DefaultTextStyle(props, elementChildren(children)),
|
|
103
|
+
// Navigation. `Link` takes exactly one of `href` (a URL) or `to` (an `Anchor` name) - the factory
|
|
104
|
+
// enforces that, so a template typo surfaces as an error instead of a dead link.
|
|
105
|
+
link: (props, children) => Link(props, wrappedChild(children)),
|
|
106
|
+
anchor: (props, children) => Anchor(props, wrappedChild(children)),
|
|
107
|
+
bookmark: (props, children) => Bookmark(props, wrappedChild(children)),
|
|
108
|
+
// Transforms.
|
|
109
|
+
rotated: (props, children) => Rotated(props, wrappedChild(children)),
|
|
110
|
+
"rotated-box": (props, children) => RotatedBox(props, wrappedChild(children)),
|
|
111
|
+
// Page numbers. `PageBuilder` itself is deliberately NOT exposed: it takes a closure, which a template
|
|
112
|
+
// cannot express. These two cover what the closure exists for.
|
|
113
|
+
"page-number": (props) => PageNumber(props),
|
|
114
|
+
"page-count": (props) => PageCount(props),
|
|
92
115
|
};
|
|
93
116
|
/**
|
|
94
117
|
* Registers a custom element type, so a binding (or a user-defined component) can introduce
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size input for a Box's `width` / `height` (relative sizing). Either a number of PDF points (a
|
|
3
|
+
* fixed size) or a percentage string like `"50%"` - a fraction of the space the parent offers on
|
|
4
|
+
* that axis. Every form normalizes to a `Dimension` via `toDimension`, so the engine only ever sees
|
|
5
|
+
* points and factors, never a string. A percentage resolves against the parent's offered extent, so
|
|
6
|
+
* it only has meaning inside a bounded region (a fraction of an unbounded axis is a no-op).
|
|
7
|
+
*/
|
|
8
|
+
export type SizeInput = number | `${number}%`;
|
|
9
|
+
/** A resolved `SizeInput`: exactly one of an absolute point size or a 0..1 fraction of the parent. */
|
|
10
|
+
export interface Dimension {
|
|
11
|
+
/** Absolute size in PDF points. */
|
|
12
|
+
points?: number;
|
|
13
|
+
/** Fraction of the parent's offered extent on this axis; a `"50%"` input is `0.5`. */
|
|
14
|
+
factor?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Normalizes a `SizeInput` to a `Dimension` (a point size or a fraction). Rejects negatives at this
|
|
18
|
+
* boundary so `resolveExtent` and the layout never see a negative size.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toDimension(value: SizeInput): Dimension;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const PERCENT = /^(\d+(?:\.\d+)?)%$/; // no leading "-": a size cannot be negative
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes a `SizeInput` to a `Dimension` (a point size or a fraction). Rejects negatives at this
|
|
4
|
+
* boundary so `resolveExtent` and the layout never see a negative size.
|
|
5
|
+
*/
|
|
6
|
+
export function toDimension(value) {
|
|
7
|
+
if (typeof value === "number") {
|
|
8
|
+
if (value < 0)
|
|
9
|
+
throw new Error(`Invalid size ${value}: a size cannot be negative.`);
|
|
10
|
+
return { points: value };
|
|
11
|
+
}
|
|
12
|
+
const m = PERCENT.exec(value.trim());
|
|
13
|
+
if (m)
|
|
14
|
+
return { factor: parseFloat(m[1]) / 100 };
|
|
15
|
+
throw new Error(`Invalid size "${value}": use a number of points or a percentage like "50%".`);
|
|
16
|
+
}
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export * from "./color.js";
|
|
2
2
|
export * from "./insets.js";
|
|
3
|
+
export * from "./dimension.js";
|
|
3
4
|
export * from "./text.js";
|
|
4
5
|
export * from "./layout.js";
|
|
5
6
|
export * from "./content.js";
|
|
7
|
+
export * from "./page-builder.js";
|
|
6
8
|
export * from "./structure.js";
|
|
7
9
|
export * from "./table.js";
|
|
8
10
|
export * from "./descriptor.js";
|
package/dist/api/index.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// core classes stay untouched and remain exported for power users.
|
|
4
4
|
export * from "./color.js";
|
|
5
5
|
export * from "./insets.js";
|
|
6
|
+
export * from "./dimension.js";
|
|
6
7
|
export * from "./text.js";
|
|
7
8
|
export * from "./layout.js";
|
|
8
9
|
export * from "./content.js";
|
|
10
|
+
export * from "./page-builder.js";
|
|
9
11
|
export * from "./structure.js";
|
|
10
12
|
export * from "./table.js";
|
|
11
13
|
export * from "./descriptor.js";
|
package/dist/api/layout.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { ContainerElement } from "../elements/container-element.js";
|
|
2
|
-
import { RowElement } from "../elements/row-element.js";
|
|
3
|
-
import { RectangleElement } from "../elements/rectangle-element.js";
|
|
4
1
|
import { ExpandedElement } from "../elements/layout/expanded-element.js";
|
|
5
2
|
import { PaddingElement } from "../elements/layout/padding-element.js";
|
|
3
|
+
import { PageBreakElement } from "../elements/layout/page-break-element.js";
|
|
4
|
+
import { KeepTogetherElement } from "../elements/layout/keep-together-element.js";
|
|
6
5
|
import { PositionedElement, PositionedInsets } from "../elements/layout/positioned-element.js";
|
|
6
|
+
import { LinkElement } from "../elements/layout/link-element.js";
|
|
7
|
+
import { BookmarkElement } from "../elements/layout/bookmark-element.js";
|
|
8
|
+
import { AnchorElement } from "../elements/layout/anchor-element.js";
|
|
9
|
+
import { RotatedElement } from "../elements/layout/rotated-element.js";
|
|
10
|
+
import { RotatedBoxElement } from "../elements/layout/rotated-box-element.js";
|
|
7
11
|
import { PDFElement } from "../elements/pdf-element.js";
|
|
8
12
|
import { MainAlign, CrossAlign } from "../utils/flex-layout.js";
|
|
9
13
|
import { ColorInput } from "./color.js";
|
|
10
14
|
import { Insets } from "./insets.js";
|
|
15
|
+
import { SizeInput } from "./dimension.js";
|
|
11
16
|
/** Options shared by the `Column` and `Row` stacks (locked §4). */
|
|
12
17
|
export interface StackOptions {
|
|
13
18
|
/** Space inserted between children, in points. */
|
|
@@ -17,13 +22,33 @@ export interface StackOptions {
|
|
|
17
22
|
justify?: MainAlign;
|
|
18
23
|
/** Position across the axis (CSS `align-items`): start (default) · center · end · stretch. */
|
|
19
24
|
align?: CrossAlign;
|
|
25
|
+
/** Size on each axis: points (fixed) or a percentage string like `"50%"` (a fraction of the offered
|
|
26
|
+
* space). Omit to fill the offered width / shrink-wrap to the children. A percentage needs a bounded
|
|
27
|
+
* axis (`height: "50%"` only resolves where the parent bounds the height). */
|
|
28
|
+
width?: SizeInput;
|
|
29
|
+
height?: SizeInput;
|
|
30
|
+
/** Start this stack on a fresh page (CSS `break-before: page`, react-pdf's `break`). Ignored at the
|
|
31
|
+
* top of a page. */
|
|
32
|
+
breakBefore?: boolean;
|
|
33
|
+
/** Start everything after this stack on a fresh page (CSS `break-after: page`). */
|
|
34
|
+
breakAfter?: boolean;
|
|
35
|
+
/** Try to keep this stack on one page (CSS `break-inside: avoid`). If it does not fit here but would
|
|
36
|
+
* fit on a fresh page, it moves there whole; if it is taller than a whole page, it splits anyway. */
|
|
37
|
+
keepTogether?: boolean;
|
|
20
38
|
}
|
|
21
39
|
/** A vertical stack. `Column(children)` or `Column(opts, children)`. */
|
|
22
|
-
export declare function Column(children: PDFElement[]):
|
|
23
|
-
export declare function Column(opts: StackOptions, children: PDFElement[]):
|
|
40
|
+
export declare function Column(children: PDFElement[]): PDFElement;
|
|
41
|
+
export declare function Column(opts: StackOptions, children: PDFElement[]): PDFElement;
|
|
24
42
|
/** A horizontal stack. `Row(children)` or `Row(opts, children)`. */
|
|
25
|
-
export declare function Row(children: PDFElement[]):
|
|
26
|
-
export declare function Row(opts: StackOptions, children: PDFElement[]):
|
|
43
|
+
export declare function Row(children: PDFElement[]): PDFElement;
|
|
44
|
+
export declare function Row(opts: StackOptions, children: PDFElement[]): PDFElement;
|
|
45
|
+
/**
|
|
46
|
+
* Keep a group of elements on ONE page (CSS `break-inside: avoid`). `keepTogether([...])` stacks the
|
|
47
|
+
* children in a `Column` and refuses to split them across a page break - unless they do not fit on a
|
|
48
|
+
* fresh page at all, in which case they split anyway (so pagination always terminates). Equivalent to
|
|
49
|
+
* the `keepTogether` prop on `Box`/`Column`/`Row`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function keepTogether(children: PDFElement[]): KeepTogetherElement;
|
|
27
52
|
/** A bordered / filled box that wraps its children (locked §4). */
|
|
28
53
|
export interface BoxOptions {
|
|
29
54
|
/** Border (stroke) color. A box has a border only when `border` or `borderWidth` is set. */
|
|
@@ -41,9 +66,11 @@ export interface BoxOptions {
|
|
|
41
66
|
bg?: ColorInput;
|
|
42
67
|
/** Inner padding between the border and the children. */
|
|
43
68
|
padding?: Insets;
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
69
|
+
/** Size on each axis: a number of points (fixed) or a percentage string like `"50%"` (a fraction
|
|
70
|
+
* of the space the parent offers on that axis). Omit `width` to fill the offered width, omit
|
|
71
|
+
* `height` to shrink-wrap the content. A percentage only resolves in a bounded region. */
|
|
72
|
+
width?: SizeInput;
|
|
73
|
+
height?: SizeInput;
|
|
47
74
|
/** Corner radius in points. */
|
|
48
75
|
radius?: number;
|
|
49
76
|
/** Make this box a positioning frame: `Positioned` children placed inside it resolve their
|
|
@@ -52,29 +79,97 @@ export interface BoxOptions {
|
|
|
52
79
|
/** `"hidden"` crops children to the box (rounded corners included); `"visible"` (default) lets a
|
|
53
80
|
* `Positioned` child spill over the edge. */
|
|
54
81
|
overflow?: "hidden" | "visible";
|
|
82
|
+
/** Start this box on a fresh page (CSS `break-before: page`, react-pdf's `break`). Ignored at the top
|
|
83
|
+
* of a page. */
|
|
84
|
+
breakBefore?: boolean;
|
|
85
|
+
/** Start everything after this box on a fresh page (CSS `break-after: page`). */
|
|
86
|
+
breakAfter?: boolean;
|
|
87
|
+
/** Try to keep this box on one page (CSS `break-inside: avoid`). Moves whole to a fresh page if it
|
|
88
|
+
* would fit there; splits anyway if it is taller than a whole page. */
|
|
89
|
+
keepTogether?: boolean;
|
|
55
90
|
}
|
|
56
91
|
/**
|
|
57
92
|
* A box: maps to a `RectangleElement` (fill + border + radius) whose children are stacked
|
|
58
93
|
* inside it, optionally inset by `padding` (a `PaddingElement` wrapping a `Column` of the
|
|
59
94
|
* children). With no `border`/`borderWidth` the box has no outline (`borderWidth` 0).
|
|
60
95
|
*/
|
|
61
|
-
export declare function Box(children: PDFElement[]):
|
|
62
|
-
export declare function Box(opts: BoxOptions, children: PDFElement[]):
|
|
96
|
+
export declare function Box(children: PDFElement[]): PDFElement;
|
|
97
|
+
export declare function Box(opts: BoxOptions, children: PDFElement[]): PDFElement;
|
|
63
98
|
/** Insets a single child by `padding` (a number / `{x,y}` / `{top,…}` / 4-tuple). */
|
|
64
99
|
export declare function Padding(padding: Insets, child: PDFElement): PaddingElement;
|
|
65
100
|
/**
|
|
66
|
-
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box
|
|
67
|
-
* axis: pin to EDGES - `Positioned({ top, left, right,
|
|
68
|
-
* pokes into / out of the corner (a badge, a tab, a
|
|
69
|
-
* ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
70
|
-
* end-aligned with a pixel offset. An edge wins over an
|
|
101
|
+
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box - or, with none, to
|
|
102
|
+
* the page's content box. Two ways, pick per axis: pin to EDGES - `Positioned({ top, left, right,
|
|
103
|
+
* bottom }, child)`, where a negative value pokes into / out of the corner (a badge, a tab, a
|
|
104
|
+
* ribbon) and pinning both sides stretches; or ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
105
|
+
* x: -10, y: -8 }, child)`, i.e. centered / end-aligned with a pixel offset. An edge wins over an
|
|
106
|
+
* anchor on the same axis.
|
|
107
|
+
*
|
|
108
|
+
* A `Positioned` in a `Page`'s `header` or `footer` repeats on EVERY page and takes no space in the
|
|
109
|
+
* band, which is how you draw a watermark, a draft stamp or a corner ribbon across a document. It
|
|
110
|
+
* anchors to the page, not to the band, so `bottom: 0` is the foot of the page.
|
|
71
111
|
*/
|
|
72
112
|
export declare function Positioned(opts: PositionedInsets, child: PDFElement): PositionedElement;
|
|
113
|
+
/**
|
|
114
|
+
* Makes any `child` a clickable hyperlink - the child's whole box is the clickable region (an image, a
|
|
115
|
+
* box, a row). Pass `href` for an external URL, or `to` for an internal jump to an `Anchor({ name })`
|
|
116
|
+
* elsewhere in the document (e.g. a clickable table-of-contents row). Exactly one of the two. The link
|
|
117
|
+
* draws nothing itself, so style the child - e.g. `Link({ href }, Text("jasy.dev", { color: "#1450aa" }))`
|
|
118
|
+
* or `Link({ to: "chapter-2" }, Text("Chapter 2 ....... 5"))`. For a link on part of a line, put `href`/
|
|
119
|
+
* `to` on a `span` instead (`Text([span("Visit "), span("jasy.dev", { href })])`), linking just that run.
|
|
120
|
+
*/
|
|
121
|
+
export declare function Link(opts: {
|
|
122
|
+
href?: string;
|
|
123
|
+
to?: string;
|
|
124
|
+
}, child: PDFElement): LinkElement;
|
|
125
|
+
/**
|
|
126
|
+
* Marks `child` as a named jump target that an internal `Link({ to: name })` can point at (a table of
|
|
127
|
+
* contents linking to sections, cross-references). Layout-transparent: `child` renders exactly as it
|
|
128
|
+
* would on its own, on whatever page it lands on. `name` must match the link's `to`.
|
|
129
|
+
* `Anchor({ name: "chapter-2" }, Text("Chapter 2", { size: 24 }))`.
|
|
130
|
+
*/
|
|
131
|
+
export declare function Anchor(opts: {
|
|
132
|
+
name: string;
|
|
133
|
+
}, child: PDFElement): AnchorElement;
|
|
134
|
+
/**
|
|
135
|
+
* Adds a bookmark to the document outline (the viewer's sidebar) that jumps to `child`. `title` is the
|
|
136
|
+
* label; `level` (1-based, default 1) nests it - a `level: 2` bookmark hangs under the nearest preceding
|
|
137
|
+
* `level: 1`, so you get a collapsible tree (chapters -> sections). It is layout-transparent: `child`
|
|
138
|
+
* renders exactly as it would on its own. `Bookmark({ title: "Chapter 2", level: 1 }, Text("Chapter 2"))`.
|
|
139
|
+
*/
|
|
140
|
+
export declare function Bookmark(opts: {
|
|
141
|
+
title: string;
|
|
142
|
+
level?: number;
|
|
143
|
+
}, child: PDFElement): BookmarkElement;
|
|
144
|
+
/**
|
|
145
|
+
* Rotates `child` at PAINT time by `angle` degrees (clockwise), around its center - like CSS
|
|
146
|
+
* `transform: rotate()` / Flutter `Transform.rotate`. The child keeps its normal, unrotated layout box,
|
|
147
|
+
* so siblings do NOT reflow around the spun shape and the drawing may overflow its slot. Pair it with a
|
|
148
|
+
* `relative` Box + `Positioned` for a diagonal watermark or a "PAID" stamp laid over a document.
|
|
149
|
+
*/
|
|
150
|
+
export declare function Rotated(opts: {
|
|
151
|
+
angle: number;
|
|
152
|
+
}, child: PDFElement): RotatedElement;
|
|
153
|
+
/**
|
|
154
|
+
* Rotates `child` by whole quarter-turns and, unlike `Rotated`, is LAYOUT-AWARE: a 90 / 270 turn swaps
|
|
155
|
+
* the box's width and height so siblings reflow around it (a tall label becomes a narrow, tall strip
|
|
156
|
+
* that reserves exactly that footprint). `turns` = clockwise 90-degree steps (1 = 90, 2 = 180, 3 = 270).
|
|
157
|
+
* Use it for a vertical label beside a table; use `Rotated` for a free-angle stamp / watermark.
|
|
158
|
+
*/
|
|
159
|
+
export declare function RotatedBox(opts: {
|
|
160
|
+
turns: number;
|
|
161
|
+
}, child: PDFElement): RotatedBoxElement;
|
|
73
162
|
/**
|
|
74
163
|
* A flexible empty gap that pushes its siblings apart - `Row([a, Spacer(), b])` sends `a`
|
|
75
164
|
* and `b` to the edges. `flex` weights it against other flex children (default 1).
|
|
76
165
|
*/
|
|
77
166
|
export declare function Spacer(flex?: number): ExpandedElement;
|
|
167
|
+
/**
|
|
168
|
+
* A forced page break. Put it in a `Column` (or any vertical flow) and everything AFTER it starts on
|
|
169
|
+
* a fresh page, no matter how much space is left: `Column([intro, PageBreak(), chapterTwo])`. It takes
|
|
170
|
+
* no space and draws nothing. Works nested, and a break wins over `keepTogether`.
|
|
171
|
+
*/
|
|
172
|
+
export declare function PageBreak(): PageBreakElement;
|
|
78
173
|
export interface ExpandedOptions {
|
|
79
174
|
/** Share of the leftover space vs other flex children (default 1). */
|
|
80
175
|
flex?: number;
|
package/dist/api/layout.js
CHANGED
|
@@ -3,32 +3,67 @@ import { RowElement } from "../elements/row-element.js";
|
|
|
3
3
|
import { RectangleElement } from "../elements/rectangle-element.js";
|
|
4
4
|
import { ExpandedElement } from "../elements/layout/expanded-element.js";
|
|
5
5
|
import { PaddingElement } from "../elements/layout/padding-element.js";
|
|
6
|
+
import { PageBreakElement } from "../elements/layout/page-break-element.js";
|
|
7
|
+
import { KeepTogetherElement } from "../elements/layout/keep-together-element.js";
|
|
6
8
|
import { PositionedElement } from "../elements/layout/positioned-element.js";
|
|
9
|
+
import { LinkElement } from "../elements/layout/link-element.js";
|
|
10
|
+
import { BookmarkElement } from "../elements/layout/bookmark-element.js";
|
|
11
|
+
import { AnchorElement } from "../elements/layout/anchor-element.js";
|
|
12
|
+
import { RotatedElement } from "../elements/layout/rotated-element.js";
|
|
13
|
+
import { RotatedBoxElement } from "../elements/layout/rotated-box-element.js";
|
|
7
14
|
import { toColor } from "./color.js";
|
|
8
15
|
import { toEdges } from "./insets.js";
|
|
16
|
+
import { toDimension } from "./dimension.js";
|
|
9
17
|
import { splitArgs } from "./args.js";
|
|
18
|
+
/** Splits `StackOptions` width/height into the fixed points + fraction the stack elements expect. */
|
|
19
|
+
function stackSize(opts) {
|
|
20
|
+
const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
|
|
21
|
+
const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
|
|
22
|
+
return { width: w?.points, height: h?.points, widthFactor: w?.factor, heightFactor: h?.factor };
|
|
23
|
+
}
|
|
10
24
|
// The public default for `cross` is `start` (locked §5) - i.e. don't stretch a child unless
|
|
11
25
|
// asked. The engine default is `stretch`; the factory sets the friendlier public one.
|
|
12
26
|
const DEFAULT_CROSS = "start";
|
|
13
27
|
export function Column(a, b) {
|
|
14
28
|
const { opts, children } = splitArgs(a, b);
|
|
15
|
-
return new ContainerElement({
|
|
29
|
+
return maybeKeepTogether(opts, new ContainerElement({
|
|
16
30
|
x: 0,
|
|
17
31
|
y: 0,
|
|
18
32
|
children,
|
|
19
33
|
gap: opts.gap,
|
|
20
34
|
main: opts.justify, // undefined → engine default `start` (matches §5)
|
|
21
35
|
cross: opts.align ?? DEFAULT_CROSS,
|
|
22
|
-
|
|
36
|
+
breakBefore: opts.breakBefore,
|
|
37
|
+
breakAfter: opts.breakAfter,
|
|
38
|
+
...stackSize(opts),
|
|
39
|
+
}));
|
|
23
40
|
}
|
|
24
41
|
export function Row(a, b) {
|
|
25
42
|
const { opts, children } = splitArgs(a, b);
|
|
26
|
-
return new RowElement({
|
|
43
|
+
return maybeKeepTogether(opts, new RowElement({
|
|
27
44
|
children,
|
|
28
45
|
gap: opts.gap,
|
|
29
46
|
main: opts.justify,
|
|
30
47
|
cross: opts.align ?? DEFAULT_CROSS,
|
|
31
|
-
|
|
48
|
+
breakBefore: opts.breakBefore,
|
|
49
|
+
breakAfter: opts.breakAfter,
|
|
50
|
+
...stackSize(opts),
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
/** Wraps `element` in a `keepTogether` group when the `keepTogether` option is set; otherwise returns it
|
|
54
|
+
* unchanged (byte-identical). Shared by the `Box`/`Column`/`Row` prop shortcut. */
|
|
55
|
+
function maybeKeepTogether(opts, element) {
|
|
56
|
+
return opts.keepTogether ? new KeepTogetherElement({ child: element }) : element;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Keep a group of elements on ONE page (CSS `break-inside: avoid`). `keepTogether([...])` stacks the
|
|
60
|
+
* children in a `Column` and refuses to split them across a page break - unless they do not fit on a
|
|
61
|
+
* fresh page at all, in which case they split anyway (so pagination always terminates). Equivalent to
|
|
62
|
+
* the `keepTogether` prop on `Box`/`Column`/`Row`.
|
|
63
|
+
*/
|
|
64
|
+
export function keepTogether(children) {
|
|
65
|
+
const child = children.length === 1 ? children[0] : Column(children);
|
|
66
|
+
return new KeepTogetherElement({ child });
|
|
32
67
|
}
|
|
33
68
|
export function Box(a, b) {
|
|
34
69
|
const { opts, children } = splitArgs(a, b);
|
|
@@ -49,15 +84,20 @@ export function Box(a, b) {
|
|
|
49
84
|
return c !== undefined ? toColor(c) : undefined;
|
|
50
85
|
};
|
|
51
86
|
const hasBorder = opts.border !== undefined || opts.borderWidth !== undefined || hasPerSide;
|
|
52
|
-
|
|
87
|
+
// A point size fills `width`/`height`; a percentage fills `widthFactor`/`heightFactor`.
|
|
88
|
+
const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
|
|
89
|
+
const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
|
|
90
|
+
return maybeKeepTogether(opts, new RectangleElement({
|
|
53
91
|
x: 0,
|
|
54
92
|
y: 0,
|
|
55
93
|
children: content,
|
|
56
94
|
color: opts.border !== undefined ? toColor(opts.border) : undefined,
|
|
57
95
|
backgroundColor: opts.bg !== undefined ? toColor(opts.bg) : undefined,
|
|
58
96
|
borderWidth: hasBorder ? (opts.borderWidth ?? 1) : 0,
|
|
59
|
-
width:
|
|
60
|
-
height:
|
|
97
|
+
width: w?.points,
|
|
98
|
+
height: h?.points,
|
|
99
|
+
widthFactor: w?.factor,
|
|
100
|
+
heightFactor: h?.factor,
|
|
61
101
|
radius: opts.radius,
|
|
62
102
|
sideBorders: hasPerSide
|
|
63
103
|
? {
|
|
@@ -69,22 +109,79 @@ export function Box(a, b) {
|
|
|
69
109
|
: undefined,
|
|
70
110
|
relative: opts.relative,
|
|
71
111
|
overflow: opts.overflow,
|
|
72
|
-
|
|
112
|
+
breakBefore: opts.breakBefore,
|
|
113
|
+
breakAfter: opts.breakAfter,
|
|
114
|
+
}));
|
|
73
115
|
}
|
|
74
116
|
/** Insets a single child by `padding` (a number / `{x,y}` / `{top,…}` / 4-tuple). */
|
|
75
117
|
export function Padding(padding, child) {
|
|
76
118
|
return new PaddingElement({ margin: toEdges(padding), child });
|
|
77
119
|
}
|
|
78
120
|
/**
|
|
79
|
-
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box
|
|
80
|
-
* axis: pin to EDGES - `Positioned({ top, left, right,
|
|
81
|
-
* pokes into / out of the corner (a badge, a tab, a
|
|
82
|
-
* ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
83
|
-
* end-aligned with a pixel offset. An edge wins over an
|
|
121
|
+
* Places a child OUT OF FLOW, relative to the nearest enclosing `relative` Box - or, with none, to
|
|
122
|
+
* the page's content box. Two ways, pick per axis: pin to EDGES - `Positioned({ top, left, right,
|
|
123
|
+
* bottom }, child)`, where a negative value pokes into / out of the corner (a badge, a tab, a
|
|
124
|
+
* ribbon) and pinning both sides stretches; or ANCHOR + nudge - `Positioned({ h: "center", v: "end",
|
|
125
|
+
* x: -10, y: -8 }, child)`, i.e. centered / end-aligned with a pixel offset. An edge wins over an
|
|
126
|
+
* anchor on the same axis.
|
|
127
|
+
*
|
|
128
|
+
* A `Positioned` in a `Page`'s `header` or `footer` repeats on EVERY page and takes no space in the
|
|
129
|
+
* band, which is how you draw a watermark, a draft stamp or a corner ribbon across a document. It
|
|
130
|
+
* anchors to the page, not to the band, so `bottom: 0` is the foot of the page.
|
|
84
131
|
*/
|
|
85
132
|
export function Positioned(opts, child) {
|
|
86
133
|
return new PositionedElement({ child, ...opts });
|
|
87
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Makes any `child` a clickable hyperlink - the child's whole box is the clickable region (an image, a
|
|
137
|
+
* box, a row). Pass `href` for an external URL, or `to` for an internal jump to an `Anchor({ name })`
|
|
138
|
+
* elsewhere in the document (e.g. a clickable table-of-contents row). Exactly one of the two. The link
|
|
139
|
+
* draws nothing itself, so style the child - e.g. `Link({ href }, Text("jasy.dev", { color: "#1450aa" }))`
|
|
140
|
+
* or `Link({ to: "chapter-2" }, Text("Chapter 2 ....... 5"))`. For a link on part of a line, put `href`/
|
|
141
|
+
* `to` on a `span` instead (`Text([span("Visit "), span("jasy.dev", { href })])`), linking just that run.
|
|
142
|
+
*/
|
|
143
|
+
export function Link(opts, child) {
|
|
144
|
+
if ((opts.href === undefined) === (opts.to === undefined)) {
|
|
145
|
+
throw new Error("Link needs exactly one of `href` (external URL) or `to` (internal Anchor name).");
|
|
146
|
+
}
|
|
147
|
+
return new LinkElement({ href: opts.href, dest: opts.to, child });
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Marks `child` as a named jump target that an internal `Link({ to: name })` can point at (a table of
|
|
151
|
+
* contents linking to sections, cross-references). Layout-transparent: `child` renders exactly as it
|
|
152
|
+
* would on its own, on whatever page it lands on. `name` must match the link's `to`.
|
|
153
|
+
* `Anchor({ name: "chapter-2" }, Text("Chapter 2", { size: 24 }))`.
|
|
154
|
+
*/
|
|
155
|
+
export function Anchor(opts, child) {
|
|
156
|
+
return new AnchorElement({ name: opts.name, child });
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Adds a bookmark to the document outline (the viewer's sidebar) that jumps to `child`. `title` is the
|
|
160
|
+
* label; `level` (1-based, default 1) nests it - a `level: 2` bookmark hangs under the nearest preceding
|
|
161
|
+
* `level: 1`, so you get a collapsible tree (chapters -> sections). It is layout-transparent: `child`
|
|
162
|
+
* renders exactly as it would on its own. `Bookmark({ title: "Chapter 2", level: 1 }, Text("Chapter 2"))`.
|
|
163
|
+
*/
|
|
164
|
+
export function Bookmark(opts, child) {
|
|
165
|
+
return new BookmarkElement({ title: opts.title, level: opts.level, child });
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Rotates `child` at PAINT time by `angle` degrees (clockwise), around its center - like CSS
|
|
169
|
+
* `transform: rotate()` / Flutter `Transform.rotate`. The child keeps its normal, unrotated layout box,
|
|
170
|
+
* so siblings do NOT reflow around the spun shape and the drawing may overflow its slot. Pair it with a
|
|
171
|
+
* `relative` Box + `Positioned` for a diagonal watermark or a "PAID" stamp laid over a document.
|
|
172
|
+
*/
|
|
173
|
+
export function Rotated(opts, child) {
|
|
174
|
+
return new RotatedElement({ angle: opts.angle, child });
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Rotates `child` by whole quarter-turns and, unlike `Rotated`, is LAYOUT-AWARE: a 90 / 270 turn swaps
|
|
178
|
+
* the box's width and height so siblings reflow around it (a tall label becomes a narrow, tall strip
|
|
179
|
+
* that reserves exactly that footprint). `turns` = clockwise 90-degree steps (1 = 90, 2 = 180, 3 = 270).
|
|
180
|
+
* Use it for a vertical label beside a table; use `Rotated` for a free-angle stamp / watermark.
|
|
181
|
+
*/
|
|
182
|
+
export function RotatedBox(opts, child) {
|
|
183
|
+
return new RotatedBoxElement({ turns: opts.turns, child });
|
|
184
|
+
}
|
|
88
185
|
/**
|
|
89
186
|
* A flexible empty gap that pushes its siblings apart - `Row([a, Spacer(), b])` sends `a`
|
|
90
187
|
* and `b` to the edges. `flex` weights it against other flex children (default 1).
|
|
@@ -92,6 +189,14 @@ export function Positioned(opts, child) {
|
|
|
92
189
|
export function Spacer(flex = 1) {
|
|
93
190
|
return new ExpandedElement({ flex, child: Column([]) });
|
|
94
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* A forced page break. Put it in a `Column` (or any vertical flow) and everything AFTER it starts on
|
|
194
|
+
* a fresh page, no matter how much space is left: `Column([intro, PageBreak(), chapterTwo])`. It takes
|
|
195
|
+
* no space and draws nothing. Works nested, and a break wins over `keepTogether`.
|
|
196
|
+
*/
|
|
197
|
+
export function PageBreak() {
|
|
198
|
+
return new PageBreakElement();
|
|
199
|
+
}
|
|
95
200
|
export function Expanded(a, b) {
|
|
96
201
|
const isOptsForm = b !== undefined;
|
|
97
202
|
const opts = (isOptsForm ? a : {});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PageBuilderElement } from "../elements/layout/page-builder-element.js";
|
|
2
|
+
import { PageInfo, PDFElement } from "../elements/pdf-element.js";
|
|
3
|
+
import { TextOptions } from "./text.js";
|
|
4
|
+
export type { PageInfo };
|
|
5
|
+
/**
|
|
6
|
+
* Builds content from the page it ends up on - this is how you show `pageNumber` / `pageCount`. Place it
|
|
7
|
+
* anywhere: in a `Page`'s `header`/`footer`, in the body, in a table cell, several times on one page.
|
|
8
|
+
* Because you write the closure, formatting and conditions are yours:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* PageBuilder(({ pageNumber, pageCount }) => Text(`Seite ${pageNumber} von ${pageCount}`))
|
|
12
|
+
* PageBuilder(({ pageNumber }) => (pageNumber === 1 ? Logo() : Text("Fortsetzung")))
|
|
13
|
+
* PageBuilder(({ pageNumber, pageCount }) => Text(`${pageNumber - 1} / ${pageCount - 1}`)) // cover page
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* `pageNumber` is 1-based and counts PHYSICAL pages (an overflowing `Page` contributes several);
|
|
17
|
+
* `pageCount` is the document total; `pageSize` is the media box in points.
|
|
18
|
+
*
|
|
19
|
+
* The numbers themselves are always exact. Two sizing caveats, both from the same chicken-and-egg (the
|
|
20
|
+
* content decides the count that the content displays): in the flowing BODY the box is reserved before the
|
|
21
|
+
* total is known, so a much wider final string can paint slightly past it; and a conditional header may
|
|
22
|
+
* SHRINK on later pages but must not GROW, because the body band was measured against the first build.
|
|
23
|
+
*/
|
|
24
|
+
export declare function PageBuilder(build: (info: PageInfo) => PDFElement): PageBuilderElement;
|
|
25
|
+
/** `Text` options plus an `offset` added to the number (e.g. `-1` to not count a cover page). */
|
|
26
|
+
export interface PageNumberOptions extends TextOptions {
|
|
27
|
+
offset?: number;
|
|
28
|
+
}
|
|
29
|
+
/** The current page number as text. Sugar for `PageBuilder` - same thing, one word. */
|
|
30
|
+
export declare function PageNumber({ offset, ...style }?: PageNumberOptions): PageBuilderElement;
|
|
31
|
+
/** The document's total page count as text. Sugar for `PageBuilder`. */
|
|
32
|
+
export declare function PageCount({ offset, ...style }?: PageNumberOptions): PageBuilderElement;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PageBuilderElement } from "../elements/layout/page-builder-element.js";
|
|
2
|
+
import { Text } from "./text.js";
|
|
3
|
+
/**
|
|
4
|
+
* Builds content from the page it ends up on - this is how you show `pageNumber` / `pageCount`. Place it
|
|
5
|
+
* anywhere: in a `Page`'s `header`/`footer`, in the body, in a table cell, several times on one page.
|
|
6
|
+
* Because you write the closure, formatting and conditions are yours:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* PageBuilder(({ pageNumber, pageCount }) => Text(`Seite ${pageNumber} von ${pageCount}`))
|
|
10
|
+
* PageBuilder(({ pageNumber }) => (pageNumber === 1 ? Logo() : Text("Fortsetzung")))
|
|
11
|
+
* PageBuilder(({ pageNumber, pageCount }) => Text(`${pageNumber - 1} / ${pageCount - 1}`)) // cover page
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* `pageNumber` is 1-based and counts PHYSICAL pages (an overflowing `Page` contributes several);
|
|
15
|
+
* `pageCount` is the document total; `pageSize` is the media box in points.
|
|
16
|
+
*
|
|
17
|
+
* The numbers themselves are always exact. Two sizing caveats, both from the same chicken-and-egg (the
|
|
18
|
+
* content decides the count that the content displays): in the flowing BODY the box is reserved before the
|
|
19
|
+
* total is known, so a much wider final string can paint slightly past it; and a conditional header may
|
|
20
|
+
* SHRINK on later pages but must not GROW, because the body band was measured against the first build.
|
|
21
|
+
*/
|
|
22
|
+
export function PageBuilder(build) {
|
|
23
|
+
return new PageBuilderElement({ build });
|
|
24
|
+
}
|
|
25
|
+
/** The current page number as text. Sugar for `PageBuilder` - same thing, one word. */
|
|
26
|
+
export function PageNumber({ offset = 0, ...style } = {}) {
|
|
27
|
+
return PageBuilder(({ pageNumber }) => Text(String(pageNumber + offset), style));
|
|
28
|
+
}
|
|
29
|
+
/** The document's total page count as text. Sugar for `PageBuilder`. */
|
|
30
|
+
export function PageCount({ offset = 0, ...style } = {}) {
|
|
31
|
+
return PageBuilder(({ pageCount }) => Text(String(pageCount + offset), style));
|
|
32
|
+
}
|
package/dist/api/structure.d.ts
CHANGED
|
@@ -132,6 +132,11 @@ export interface RenderOptions {
|
|
|
132
132
|
standardFonts?: boolean;
|
|
133
133
|
/** FlateDecode-compress the streams (default true). Set false for a greppable, uncompressed PDF. */
|
|
134
134
|
compress?: boolean;
|
|
135
|
+
/** Kern the text - ON by default (still labelled experimental while we validate against react-pdf).
|
|
136
|
+
* Emits `TJ` adjustments from the font's own pairs: standard-14 (AFM), embedded `kern` table, and
|
|
137
|
+
* embedded `GPOS` Type 2, so every font path kerns. Set `false` to opt out (byte-identical to the
|
|
138
|
+
* pre-kerning output). Verified against headless Chrome and measured == drawn to the thousandth. */
|
|
139
|
+
kerning?: boolean;
|
|
135
140
|
/** What to do when content is taller than a page and cannot break: `"error"` throws (default),
|
|
136
141
|
* `"warn"` logs and clips, `"ignore"` clips silently. It is always clipped either way. */
|
|
137
142
|
onOverflow?: OverflowPolicy;
|
package/dist/api/structure.js
CHANGED
|
@@ -137,6 +137,7 @@ export async function renderPdf(doc, options) {
|
|
|
137
137
|
const om = this.objectManager;
|
|
138
138
|
om.setCompress(options?.compress !== false); // FlateDecode streams by default
|
|
139
139
|
om.setOverflowPolicy(options?.onOverflow ?? "error");
|
|
140
|
+
om.setKerning(options?.kerning !== false); // on by default (experimental); `kerning:false` opts out
|
|
140
141
|
for (const [name, value] of Object.entries(fonts)) {
|
|
141
142
|
if (isFontBytes(value)) {
|
|
142
143
|
om.registerCustomFont(name, value);
|