@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
package/dist/api/text.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextElement, TextSegment } from "../elements/text-element.js";
|
|
1
|
+
import { TextElement, TextRole, TextSegment } from "../elements/text-element.js";
|
|
2
2
|
import { TextOverflow } from "../text/line-breaker.js";
|
|
3
3
|
import { ResolvedTextStyle } from "../text/text-style.js";
|
|
4
4
|
import { ColorInput } from "./color.js";
|
|
@@ -11,6 +11,12 @@ export interface TextStyle {
|
|
|
11
11
|
bold?: boolean;
|
|
12
12
|
italic?: boolean;
|
|
13
13
|
color?: ColorInput;
|
|
14
|
+
/** External URL - makes this run an inline hyperlink (a /Link annotation over its glyphs). On a
|
|
15
|
+
* `span` it links just that run; on a whole `Text` (plain string) it links the whole text. */
|
|
16
|
+
href?: string;
|
|
17
|
+
/** Internal jump target - the `name` of an `Anchor` elsewhere in the document (like `href`, but a
|
|
18
|
+
* same-document /GoTo instead of a URL). Use for a clickable table of contents / cross-reference. */
|
|
19
|
+
to?: string;
|
|
14
20
|
}
|
|
15
21
|
export interface TextOptions extends TextStyle {
|
|
16
22
|
/** Text-internal alignment (left/center/right) - independent of a parent's `cross` (§5). */
|
|
@@ -23,6 +29,9 @@ export interface TextOptions extends TextStyle {
|
|
|
23
29
|
/** Line-height multiplier (default `1`). `1.4` gives roomier body copy; each line is
|
|
24
30
|
* `size * lineHeight` tall. */
|
|
25
31
|
lineHeight?: number;
|
|
32
|
+
/** Accessibility role for the tagged structure tree (only when rendered with `accessible`): a heading
|
|
33
|
+
* level `"h1"`..`"h6"` or `"p"` (default). Purely semantic - it does not change the visual style. */
|
|
34
|
+
role?: TextRole;
|
|
26
35
|
}
|
|
27
36
|
/**
|
|
28
37
|
* An inline run for mixed-style `Text` (`Text([span("a", {bold:true}), span("b")])`). Each
|
package/dist/api/text.js
CHANGED
|
@@ -17,6 +17,26 @@ const ALIGN = {
|
|
|
17
17
|
center: HorizontalAlignment.center,
|
|
18
18
|
right: HorizontalAlignment.right,
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Validates a font `size` (a number of points). Rejects anything that would silently become a NaN
|
|
22
|
+
* height deep in layout - notably `Document({ size: "A4" })`, where `"A4"` is the PAGE size and belongs
|
|
23
|
+
* on `Page`, not a font size. `undefined` passes through (unset = inherit the cascade).
|
|
24
|
+
*/
|
|
25
|
+
function toFontSize(size) {
|
|
26
|
+
if (size === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (typeof size !== "number" || !Number.isFinite(size) || size <= 0) {
|
|
29
|
+
// Quote a string so "A4" reads as a string; show a bare number/NaN as-is (JSON turns NaN into null).
|
|
30
|
+
const shown = typeof size === "string" ? JSON.stringify(size) : String(size);
|
|
31
|
+
// The Page-vs-font-size mix-up only happens with a string like "A4"; for a plain bad number
|
|
32
|
+
// (NaN, negative, 0) the hint would just be noise, so keep the message generic there.
|
|
33
|
+
const hint = typeof size === "string"
|
|
34
|
+
? ` Note: the page size goes on Page({ size: "A4" }), not on Document or Text.`
|
|
35
|
+
: "";
|
|
36
|
+
throw new Error(`Invalid font size ${shown} (expected a positive number of points).${hint}`);
|
|
37
|
+
}
|
|
38
|
+
return size;
|
|
39
|
+
}
|
|
20
40
|
/**
|
|
21
41
|
* An inline run for mixed-style `Text` (`Text([span("a", {bold:true}), span("b")])`). Each
|
|
22
42
|
* field overrides the enclosing `Text`'s defaults for just this run; omitted fields inherit.
|
|
@@ -24,12 +44,14 @@ const ALIGN = {
|
|
|
24
44
|
export function span(text, style = {}) {
|
|
25
45
|
return {
|
|
26
46
|
content: text,
|
|
27
|
-
fontSize: style.size,
|
|
47
|
+
fontSize: toFontSize(style.size),
|
|
28
48
|
fontFamily: style.font,
|
|
29
49
|
fontStyle: style.bold !== undefined || style.italic !== undefined
|
|
30
50
|
? toFontStyle(style.bold, style.italic)
|
|
31
51
|
: undefined,
|
|
32
52
|
fontColor: style.color !== undefined ? toColor(style.color) : undefined,
|
|
53
|
+
href: style.href,
|
|
54
|
+
dest: style.to,
|
|
33
55
|
};
|
|
34
56
|
}
|
|
35
57
|
/**
|
|
@@ -38,11 +60,16 @@ export function span(text, style = {}) {
|
|
|
38
60
|
* text-internal `align`.
|
|
39
61
|
*/
|
|
40
62
|
export function Text(content, opts = {}) {
|
|
63
|
+
// A plain string given an `href`/`to` becomes a single link span, so `Text("jasy.dev", { href })` (or
|
|
64
|
+
// `{ to }`) is a whole-text link (inline links use `span(...)`). This routes through the styled-segment
|
|
65
|
+
// path, which lays a single default-font segment out identically - only the /Link annotation is added.
|
|
66
|
+
const isLink = opts.href !== undefined || opts.to !== undefined;
|
|
67
|
+
const body = typeof content === "string" && isLink ? [span(content, opts)] : content;
|
|
41
68
|
// Unset properties are left undefined so they inherit the cascaded TextStyle (Document default /
|
|
42
69
|
// built-in). Only bold/italic that the caller actually set become an explicit FontStyle.
|
|
43
70
|
return new TextElement({
|
|
44
|
-
content,
|
|
45
|
-
fontSize: opts.size,
|
|
71
|
+
content: body,
|
|
72
|
+
fontSize: toFontSize(opts.size),
|
|
46
73
|
fontFamily: opts.font,
|
|
47
74
|
fontStyle: opts.bold !== undefined || opts.italic !== undefined
|
|
48
75
|
? toFontStyle(opts.bold, opts.italic)
|
|
@@ -52,6 +79,7 @@ export function Text(content, opts = {}) {
|
|
|
52
79
|
maxLines: opts.maxLines,
|
|
53
80
|
overflow: opts.overflow,
|
|
54
81
|
lineHeight: opts.lineHeight,
|
|
82
|
+
role: opts.role,
|
|
55
83
|
});
|
|
56
84
|
}
|
|
57
85
|
/** `Text` with body-paragraph defaults (same options; a separate name reads as intent). */
|
|
@@ -63,7 +91,7 @@ export function Paragraph(content, opts = {}) {
|
|
|
63
91
|
export function toTextStyleOverride(opts) {
|
|
64
92
|
const style = {};
|
|
65
93
|
if (opts.size !== undefined)
|
|
66
|
-
style.fontSize = opts.size;
|
|
94
|
+
style.fontSize = toFontSize(opts.size);
|
|
67
95
|
if (opts.font !== undefined)
|
|
68
96
|
style.fontFamily = opts.font;
|
|
69
97
|
if (opts.bold !== undefined || opts.italic !== undefined) {
|
|
@@ -9,13 +9,17 @@ interface ContainerElementParams extends SizedElement, WithChildren {
|
|
|
9
9
|
main?: MainAlign;
|
|
10
10
|
/** Horizontal alignment of each child (cross axis); defaults to `stretch`. */
|
|
11
11
|
cross?: CrossAlign;
|
|
12
|
+
/** Width/height as a fraction (0..1) of the offered box instead of `width`/`height` (relative sizing). */
|
|
13
|
+
widthFactor?: number;
|
|
14
|
+
heightFactor?: number;
|
|
12
15
|
}
|
|
13
16
|
export declare class ContainerElement extends SizedPDFElement implements Fragmentable {
|
|
14
17
|
private children;
|
|
15
18
|
private gap;
|
|
16
19
|
private main;
|
|
17
20
|
private cross;
|
|
18
|
-
|
|
21
|
+
private requested;
|
|
22
|
+
constructor({ x, y, width, height, children, gap, main, cross, widthFactor, heightFactor, }: ContainerElementParams);
|
|
19
23
|
/**
|
|
20
24
|
* Splits the vertical stack across pages. Children are measured against the content
|
|
21
25
|
* width and packed until one would exceed `maxHeight`. That straddling child is itself
|
|
@@ -29,6 +33,18 @@ export declare class ContainerElement extends SizedPDFElement implements Fragmen
|
|
|
29
33
|
*/
|
|
30
34
|
fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
|
|
31
35
|
private cloneWithChildren;
|
|
36
|
+
relativeSizeFactor(horizontal: boolean): number | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* A Column stacks vertically, so a flex child of its OWN only ever needs a bounded height. But the need
|
|
39
|
+
* also propagates: a `Spacer` two levels down is just as unable to resolve against an infinite axis, so
|
|
40
|
+
* we ask the children too - on both axes, since a nested Row's `Spacer` needs a bounded WIDTH and a
|
|
41
|
+
* Column nested in a Row is exactly where that width goes missing.
|
|
42
|
+
*
|
|
43
|
+
* An explicit extent on an axis short-circuits it: the element already knows how big it is there, and
|
|
44
|
+
* with no flex descendant nothing needs distributing, so it keeps shrink-wrapping. That is what leaves
|
|
45
|
+
* every existing layout untouched.
|
|
46
|
+
*/
|
|
47
|
+
needsBoundedMain(horizontal: boolean): boolean;
|
|
32
48
|
calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
|
|
33
49
|
getProps(): ContainerElementParams;
|
|
34
50
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { FlexLayoutHelper, VERTICAL_AXIS } from "../utils/flex-layout.js";
|
|
2
|
+
import { resolveExtent } from "../layout/box-constraints.js";
|
|
2
3
|
import { packChildren } from "../layout/fragmentation.js";
|
|
3
|
-
import { SizedPDFElement, } from "./pdf-element.js";
|
|
4
|
+
import { FlexiblePDFElement, SizedPDFElement, } from "./pdf-element.js";
|
|
4
5
|
export class ContainerElement extends SizedPDFElement {
|
|
5
|
-
constructor({ x, y, width, height, children, gap, main, cross }) {
|
|
6
|
+
constructor({ x, y, width, height, children, gap, main, cross, widthFactor, heightFactor, }) {
|
|
6
7
|
super({ x, y, width, height });
|
|
7
8
|
this.children = children;
|
|
8
9
|
this.gap = gap ?? 0;
|
|
9
10
|
this.main = main ?? "start";
|
|
10
11
|
this.cross = cross ?? "stretch";
|
|
12
|
+
this.requested = { width, height, widthFactor, heightFactor };
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* Splits the vertical stack across pages. Children are measured against the content
|
|
@@ -35,27 +37,59 @@ export class ContainerElement extends SizedPDFElement {
|
|
|
35
37
|
return new ContainerElement({
|
|
36
38
|
x: this.x,
|
|
37
39
|
y: this.y,
|
|
38
|
-
width
|
|
39
|
-
|
|
40
|
+
// Carry the requested WIDTH (a %-width column keeps its width on every page); the height is
|
|
41
|
+
// intentionally dropped so each fragment shrink-wraps to the content it actually holds.
|
|
42
|
+
width: this.requested.width,
|
|
43
|
+
widthFactor: this.requested.widthFactor,
|
|
40
44
|
children,
|
|
41
45
|
gap: this.gap,
|
|
42
46
|
main: this.main,
|
|
43
47
|
cross: this.cross,
|
|
44
48
|
});
|
|
45
49
|
}
|
|
50
|
+
relativeSizeFactor(horizontal) {
|
|
51
|
+
return horizontal ? this.requested.widthFactor : this.requested.heightFactor;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A Column stacks vertically, so a flex child of its OWN only ever needs a bounded height. But the need
|
|
55
|
+
* also propagates: a `Spacer` two levels down is just as unable to resolve against an infinite axis, so
|
|
56
|
+
* we ask the children too - on both axes, since a nested Row's `Spacer` needs a bounded WIDTH and a
|
|
57
|
+
* Column nested in a Row is exactly where that width goes missing.
|
|
58
|
+
*
|
|
59
|
+
* An explicit extent on an axis short-circuits it: the element already knows how big it is there, and
|
|
60
|
+
* with no flex descendant nothing needs distributing, so it keeps shrink-wrapping. That is what leaves
|
|
61
|
+
* every existing layout untouched.
|
|
62
|
+
*/
|
|
63
|
+
needsBoundedMain(horizontal) {
|
|
64
|
+
const requested = horizontal
|
|
65
|
+
? [this.requested.width, this.requested.widthFactor]
|
|
66
|
+
: [this.requested.height, this.requested.heightFactor];
|
|
67
|
+
if (requested.some((v) => v !== undefined))
|
|
68
|
+
return false;
|
|
69
|
+
const ownFlexChild = !horizontal && this.children.some((c) => c instanceof FlexiblePDFElement);
|
|
70
|
+
return ownFlexChild || this.children.some((c) => c.needsBoundedMain(horizontal));
|
|
71
|
+
}
|
|
46
72
|
calculateLayout(constraints, offset, ctx) {
|
|
47
|
-
// The container fills the width/height it is offered; when an axis is unbounded it
|
|
48
|
-
// shrink-wraps to its children instead (mirrors how Row shrink-wraps). Width unbounded
|
|
49
|
-
// happens for a Column nested in a Row (the Row offers its fixed children unbounded
|
|
50
|
-
// width); passing 0 there would collapse every child to width 0.
|
|
51
|
-
if (constraints.hasBoundedWidth)
|
|
52
|
-
this.width = constraints.maxWidth;
|
|
53
|
-
if (constraints.hasBoundedHeight)
|
|
54
|
-
this.height = constraints.maxHeight;
|
|
55
73
|
this.x = offset.x;
|
|
56
74
|
this.y = offset.y;
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
// Relative sizing: a pinned extent (fixed points or a fraction of the offered box, clamped into
|
|
76
|
+
// the constraints) wins; else fill a bounded axis; else stay `undefined` and shrink-wrap below.
|
|
77
|
+
// A Column nested in a Row gets unbounded width - passing 0 there would collapse children to 0.
|
|
78
|
+
const explicitWidth = resolveExtent(this.requested.width, this.requested.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
|
|
79
|
+
const explicitHeight = resolveExtent(this.requested.height, this.requested.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
|
|
80
|
+
const boundedWidth = explicitWidth !== undefined
|
|
81
|
+
? constraints.constrainWidth(explicitWidth)
|
|
82
|
+
: constraints.hasBoundedWidth
|
|
83
|
+
? constraints.maxWidth
|
|
84
|
+
: undefined;
|
|
85
|
+
const boundedHeight = explicitHeight !== undefined
|
|
86
|
+
? constraints.constrainHeight(explicitHeight)
|
|
87
|
+
: constraints.hasBoundedHeight
|
|
88
|
+
? constraints.maxHeight
|
|
89
|
+
: undefined;
|
|
90
|
+
// Vertical stack: cross = width (children fill it), main = height (the stacking extent).
|
|
91
|
+
const crossAvail = boundedWidth ?? Infinity;
|
|
92
|
+
const mainAvail = boundedHeight ?? Infinity;
|
|
59
93
|
let result = { mainUsed: 0, crossUsed: 0 };
|
|
60
94
|
if (this.children) {
|
|
61
95
|
// Vertical flex stack (main = height, cross = width). The shared helper measures
|
|
@@ -63,14 +97,12 @@ export class ContainerElement extends SizedPDFElement {
|
|
|
63
97
|
// in source order.
|
|
64
98
|
result = FlexLayoutHelper.layout(this.children, VERTICAL_AXIS, mainAvail, crossAvail, this.y, this.x, { gap: this.gap, main: this.main, cross: this.cross }, ctx);
|
|
65
99
|
}
|
|
66
|
-
//
|
|
67
|
-
// stack, width = the widest child). Top-left coordinates; the container draws nothing,
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.width
|
|
72
|
-
this.height = height;
|
|
73
|
-
return { width, height };
|
|
100
|
+
// Pinned/bounded axis takes that extent; an unbounded one shrink-wraps to the children (height =
|
|
101
|
+
// the stack, width = the widest child). Top-left coordinates; the container draws nothing, and
|
|
102
|
+
// the Y-flip happens once at the IR -> backend seam.
|
|
103
|
+
this.width = boundedWidth ?? result.crossUsed;
|
|
104
|
+
this.height = boundedHeight ?? result.mainUsed;
|
|
105
|
+
return { width: this.width, height: this.height };
|
|
74
106
|
}
|
|
75
107
|
getProps() {
|
|
76
108
|
return {
|
|
@@ -19,6 +19,7 @@ export declare class CustomLocalImage extends CustomImage {
|
|
|
19
19
|
private imagePath;
|
|
20
20
|
private fileBuffer;
|
|
21
21
|
private fileRawData;
|
|
22
|
+
private dims?;
|
|
22
23
|
constructor(imagePath: string);
|
|
23
24
|
init(): Promise<void>;
|
|
24
25
|
getImageType(): Promise<string>;
|
|
@@ -36,6 +37,7 @@ export declare class CustomLocalImage extends CustomImage {
|
|
|
36
37
|
export declare class CustomBytesImage extends CustomImage {
|
|
37
38
|
private bytes;
|
|
38
39
|
private fileRawData;
|
|
40
|
+
private dims?;
|
|
39
41
|
constructor(bytes: Uint8Array);
|
|
40
42
|
init(): Promise<void>;
|
|
41
43
|
getImageType(): Promise<string>;
|
|
@@ -49,15 +51,34 @@ interface ImageElementParams {
|
|
|
49
51
|
image: CustomImage;
|
|
50
52
|
width?: number;
|
|
51
53
|
height?: number;
|
|
54
|
+
/** Width as a fraction (0..1) of the offered width instead of a fixed `width` (relative sizing). */
|
|
55
|
+
widthFactor?: number;
|
|
56
|
+
/** Height as a fraction (0..1) of the offered height; see `widthFactor`. */
|
|
57
|
+
heightFactor?: number;
|
|
52
58
|
fit?: BoxFit;
|
|
53
59
|
/** Corner radius in points; rounds the image box (0 = sharp, default). */
|
|
54
60
|
radius?: number;
|
|
61
|
+
/** Alternate text for accessibility (tagged PDF). With `alt` the image is a Figure; without, decoration. */
|
|
62
|
+
alt?: string;
|
|
55
63
|
}
|
|
56
64
|
export declare class ImageElement extends SizedPDFElement {
|
|
57
65
|
private image;
|
|
66
|
+
private widthFactor?;
|
|
67
|
+
private heightFactor?;
|
|
58
68
|
private fit;
|
|
59
69
|
private radius;
|
|
60
|
-
|
|
70
|
+
private readonly alt?;
|
|
71
|
+
private requested;
|
|
72
|
+
private intrinsic?;
|
|
73
|
+
constructor({ image, width, height, widthFactor, heightFactor, fit, radius, alt, }: ImageElementParams);
|
|
74
|
+
/**
|
|
75
|
+
* Loads the image and records its intrinsic pixel size. Runs in the async pre-layout pass (layout
|
|
76
|
+
* itself is synchronous and cannot await jimp) so `calculateLayout` can turn a width-only image
|
|
77
|
+
* into a proportional box. Best-effort: a load failure leaves the size unresolved (the renderer
|
|
78
|
+
* surfaces the real error) and the image just keeps whatever explicit size it was given.
|
|
79
|
+
*/
|
|
80
|
+
resolveIntrinsicSize(): Promise<void>;
|
|
81
|
+
relativeSizeFactor(horizontal: boolean): number | undefined;
|
|
61
82
|
calculateLayout(constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
|
|
62
83
|
getProps(): {
|
|
63
84
|
x: number;
|
|
@@ -67,6 +88,7 @@ export declare class ImageElement extends SizedPDFElement {
|
|
|
67
88
|
image: CustomImage;
|
|
68
89
|
fit: BoxFit;
|
|
69
90
|
radius: number;
|
|
91
|
+
alt: string | undefined;
|
|
70
92
|
};
|
|
71
93
|
}
|
|
72
94
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getImageDimensions } from "../utils/image-helper.js";
|
|
2
2
|
import { latin1FromBytes } from "../utils/bytes.js";
|
|
3
3
|
import { readFileBytesAsync } from "../platform/node-fs.js";
|
|
4
|
+
import { resolveExtent } from "../layout/box-constraints.js";
|
|
4
5
|
import { SizedPDFElement } from "./pdf-element.js";
|
|
5
6
|
// path.extname without node:path (browser-safe): the substring from the last dot, if it sits after the
|
|
6
7
|
// last slash (so a dot in a directory name does not count). Enough for image file extensions.
|
|
@@ -24,6 +25,8 @@ export class CustomLocalImage extends CustomImage {
|
|
|
24
25
|
this.imagePath = imagePath;
|
|
25
26
|
}
|
|
26
27
|
async init() {
|
|
28
|
+
if (this.fileBuffer)
|
|
29
|
+
return; // idempotent: the pre-layout size pass and the renderer both call init()
|
|
27
30
|
try {
|
|
28
31
|
// Loading image and convert it to base64
|
|
29
32
|
await this.loadImage(this.imagePath);
|
|
@@ -59,13 +62,13 @@ export class CustomLocalImage extends CustomImage {
|
|
|
59
62
|
return this.fileRawData;
|
|
60
63
|
}
|
|
61
64
|
async getImageDimensions() {
|
|
65
|
+
if (this.dims)
|
|
66
|
+
return this.dims; // reuse: the pre-layout size pass and the renderer both ask
|
|
62
67
|
if (!this.fileBuffer) {
|
|
63
68
|
throw new Error("You must first call the `loadAndConvertImage` method");
|
|
64
69
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const dimensions = await getImageDimensions(this.fileBuffer);
|
|
68
|
-
return dimensions;
|
|
70
|
+
this.dims = await getImageDimensions(this.fileBuffer);
|
|
71
|
+
return this.dims;
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
/**
|
|
@@ -91,25 +94,64 @@ export class CustomBytesImage extends CustomImage {
|
|
|
91
94
|
return this.fileRawData;
|
|
92
95
|
}
|
|
93
96
|
async getImageDimensions() {
|
|
94
|
-
|
|
97
|
+
if (!this.dims)
|
|
98
|
+
this.dims = await getImageDimensions(this.bytes); // reuse across pre-pass + render
|
|
99
|
+
return this.dims;
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
export class ImageElement extends SizedPDFElement {
|
|
98
|
-
constructor({ image, width, height, fit = BoxFit.none, radius }) {
|
|
103
|
+
constructor({ image, width, height, widthFactor, heightFactor, fit = BoxFit.none, radius, alt, }) {
|
|
104
|
+
// Seed this.width/height from the request so getProps() reflects it before layout; calculateLayout
|
|
105
|
+
// then reads `requested` (never the mutated fields) and overwrites these with the laid-out size.
|
|
99
106
|
super({ x: 0, y: 0, width });
|
|
100
107
|
this.image = image;
|
|
101
108
|
this.height = height;
|
|
109
|
+
this.requested = { width, height };
|
|
110
|
+
this.widthFactor = widthFactor;
|
|
111
|
+
this.heightFactor = heightFactor;
|
|
102
112
|
this.fit = fit;
|
|
103
113
|
this.radius = radius ?? 0;
|
|
114
|
+
this.alt = alt;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Loads the image and records its intrinsic pixel size. Runs in the async pre-layout pass (layout
|
|
118
|
+
* itself is synchronous and cannot await jimp) so `calculateLayout` can turn a width-only image
|
|
119
|
+
* into a proportional box. Best-effort: a load failure leaves the size unresolved (the renderer
|
|
120
|
+
* surfaces the real error) and the image just keeps whatever explicit size it was given.
|
|
121
|
+
*/
|
|
122
|
+
async resolveIntrinsicSize() {
|
|
123
|
+
try {
|
|
124
|
+
await this.image.init();
|
|
125
|
+
this.intrinsic = await this.image.getImageDimensions();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// leave intrinsic undefined - aspect derivation simply doesn't fire
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
relativeSizeFactor(horizontal) {
|
|
132
|
+
return horizontal ? this.widthFactor : this.heightFactor;
|
|
104
133
|
}
|
|
105
134
|
calculateLayout(constraints, offset, _ctx) {
|
|
106
135
|
this.x = offset.x;
|
|
107
136
|
this.y = offset.y;
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
137
|
+
// Relative sizing: a fixed size or a fraction of the offered box (fraction only in a bounded axis).
|
|
138
|
+
// Read from `requested` (not this.width/height, which we overwrite below) so every pass re-resolves.
|
|
139
|
+
let w = resolveExtent(this.requested.width, this.widthFactor, constraints.maxWidth, constraints.hasBoundedWidth);
|
|
140
|
+
let h = resolveExtent(this.requested.height, this.heightFactor, constraints.maxHeight, constraints.hasBoundedHeight);
|
|
141
|
+
// Aspect auto-size: when the user pinned exactly ONE axis, derive the other from the intrinsic
|
|
142
|
+
// ratio (CSS `width: 50%; height: auto`). Only fires once the pre-pass resolved the pixel size.
|
|
143
|
+
if (this.intrinsic && this.intrinsic.width > 0 && this.intrinsic.height > 0) {
|
|
144
|
+
const ratio = this.intrinsic.width / this.intrinsic.height;
|
|
145
|
+
if (w !== undefined && h === undefined)
|
|
146
|
+
h = w / ratio;
|
|
147
|
+
else if (h !== undefined && w === undefined)
|
|
148
|
+
w = h * ratio;
|
|
149
|
+
}
|
|
150
|
+
// Fall back to the prior behavior for an unpinned axis: a bounded axis fills, otherwise keep the
|
|
151
|
+
// requested fixed size (or undefined). Read from `requested`, never the mutated this.width/height.
|
|
152
|
+
this.width = w ?? (constraints.hasBoundedWidth ? constraints.maxWidth : this.requested.width);
|
|
153
|
+
this.height =
|
|
154
|
+
h ?? (constraints.hasBoundedHeight ? constraints.maxHeight : this.requested.height);
|
|
113
155
|
// Top-left coordinates; the fit logic (renderer) and the Y-flip (seam) run later.
|
|
114
156
|
return { width: this.width ?? 0, height: this.height ?? 0 };
|
|
115
157
|
}
|
|
@@ -122,6 +164,7 @@ export class ImageElement extends SizedPDFElement {
|
|
|
122
164
|
image: this.image,
|
|
123
165
|
fit: this.fit,
|
|
124
166
|
radius: this.radius,
|
|
167
|
+
alt: this.alt,
|
|
125
168
|
};
|
|
126
169
|
}
|
|
127
170
|
}
|
package/dist/elements/index.d.ts
CHANGED
|
@@ -8,5 +8,11 @@ export * from "./layout/expanded-element.js";
|
|
|
8
8
|
export * from "./layout/padding-element.js";
|
|
9
9
|
export * from "./layout/default-text-style-element.js";
|
|
10
10
|
export * from "./layout/positioned-element.js";
|
|
11
|
+
export * from "./layout/link-element.js";
|
|
12
|
+
export * from "./layout/bookmark-element.js";
|
|
13
|
+
export * from "./layout/anchor-element.js";
|
|
14
|
+
export * from "./layout/page-builder-element.js";
|
|
15
|
+
export * from "./layout/rotated-element.js";
|
|
16
|
+
export * from "./layout/rotated-box-element.js";
|
|
11
17
|
export * from "./line-element.js";
|
|
12
18
|
export * from "./row-element.js";
|
package/dist/elements/index.js
CHANGED
|
@@ -8,5 +8,11 @@ export * from "./layout/expanded-element.js";
|
|
|
8
8
|
export * from "./layout/padding-element.js";
|
|
9
9
|
export * from "./layout/default-text-style-element.js";
|
|
10
10
|
export * from "./layout/positioned-element.js";
|
|
11
|
+
export * from "./layout/link-element.js";
|
|
12
|
+
export * from "./layout/bookmark-element.js";
|
|
13
|
+
export * from "./layout/anchor-element.js";
|
|
14
|
+
export * from "./layout/page-builder-element.js";
|
|
15
|
+
export * from "./layout/rotated-element.js";
|
|
16
|
+
export * from "./layout/rotated-box-element.js";
|
|
11
17
|
export * from "./line-element.js";
|
|
12
18
|
export * from "./row-element.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { PDFElement, LayoutContext } from "../pdf-element.js";
|
|
2
|
+
import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
|
|
3
|
+
/**
|
|
4
|
+
* Marks its child as a named jump target for an internal `Link({ to: name })`. Layout-transparent: it
|
|
5
|
+
* delegates `calculateLayout` to the child and records the child's top, which the renderer turns into an
|
|
6
|
+
* `Anchor` IR node (and PDFRenderer into a /Names /Dests entry). It draws nothing - the child renders
|
|
7
|
+
* exactly as it would on its own.
|
|
8
|
+
*/
|
|
9
|
+
export declare class AnchorElement extends PDFElement {
|
|
10
|
+
private name;
|
|
11
|
+
private child;
|
|
12
|
+
private y;
|
|
13
|
+
constructor({ name, child }: {
|
|
14
|
+
name: string;
|
|
15
|
+
child: PDFElement;
|
|
16
|
+
});
|
|
17
|
+
calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
|
|
18
|
+
getProps(): {
|
|
19
|
+
name: string;
|
|
20
|
+
child: PDFElement;
|
|
21
|
+
y: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PDFElement } from "../pdf-element.js";
|
|
2
|
+
/**
|
|
3
|
+
* Marks its child as a named jump target for an internal `Link({ to: name })`. Layout-transparent: it
|
|
4
|
+
* delegates `calculateLayout` to the child and records the child's top, which the renderer turns into an
|
|
5
|
+
* `Anchor` IR node (and PDFRenderer into a /Names /Dests entry). It draws nothing - the child renders
|
|
6
|
+
* exactly as it would on its own.
|
|
7
|
+
*/
|
|
8
|
+
export class AnchorElement extends PDFElement {
|
|
9
|
+
constructor({ name, child }) {
|
|
10
|
+
super();
|
|
11
|
+
this.y = 0;
|
|
12
|
+
this.name = name;
|
|
13
|
+
this.child = child;
|
|
14
|
+
}
|
|
15
|
+
calculateLayout(constraints, offset, ctx) {
|
|
16
|
+
const size = this.child.calculateLayout(constraints, offset, ctx);
|
|
17
|
+
this.y = offset.y; // the child's top - the scroll target for a link that jumps here
|
|
18
|
+
return size;
|
|
19
|
+
}
|
|
20
|
+
getProps() {
|
|
21
|
+
return {
|
|
22
|
+
name: this.name,
|
|
23
|
+
child: this.child,
|
|
24
|
+
y: this.y,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { PDFElement, LayoutContext } from "../pdf-element.js";
|
|
2
|
+
import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
|
|
3
|
+
/**
|
|
4
|
+
* Adds a document-outline entry (a bookmark) that jumps to its child. Layout-transparent: it delegates
|
|
5
|
+
* `calculateLayout` to the child and records the child's top, which the renderer turns into an `Outline`
|
|
6
|
+
* IR node (and PDFRenderer into a /Outlines tree entry). `title` is the label shown in the viewer's
|
|
7
|
+
* bookmark panel; `level` (1-based, default 1) nests it under the nearest preceding smaller level. It
|
|
8
|
+
* draws nothing - the child renders exactly as it would without the bookmark.
|
|
9
|
+
*/
|
|
10
|
+
export declare class BookmarkElement extends PDFElement {
|
|
11
|
+
private title;
|
|
12
|
+
private level;
|
|
13
|
+
private child;
|
|
14
|
+
private y;
|
|
15
|
+
constructor({ title, level, child }: {
|
|
16
|
+
title: string;
|
|
17
|
+
level?: number;
|
|
18
|
+
child: PDFElement;
|
|
19
|
+
});
|
|
20
|
+
calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
|
|
21
|
+
getProps(): {
|
|
22
|
+
title: string;
|
|
23
|
+
level: number;
|
|
24
|
+
child: PDFElement;
|
|
25
|
+
y: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PDFElement } from "../pdf-element.js";
|
|
2
|
+
/**
|
|
3
|
+
* Adds a document-outline entry (a bookmark) that jumps to its child. Layout-transparent: it delegates
|
|
4
|
+
* `calculateLayout` to the child and records the child's top, which the renderer turns into an `Outline`
|
|
5
|
+
* IR node (and PDFRenderer into a /Outlines tree entry). `title` is the label shown in the viewer's
|
|
6
|
+
* bookmark panel; `level` (1-based, default 1) nests it under the nearest preceding smaller level. It
|
|
7
|
+
* draws nothing - the child renders exactly as it would without the bookmark.
|
|
8
|
+
*/
|
|
9
|
+
export class BookmarkElement extends PDFElement {
|
|
10
|
+
constructor({ title, level = 1, child }) {
|
|
11
|
+
super();
|
|
12
|
+
this.y = 0;
|
|
13
|
+
this.title = title;
|
|
14
|
+
this.level = level;
|
|
15
|
+
this.child = child;
|
|
16
|
+
}
|
|
17
|
+
calculateLayout(constraints, offset, ctx) {
|
|
18
|
+
const size = this.child.calculateLayout(constraints, offset, ctx);
|
|
19
|
+
this.y = offset.y; // the child's top - the scroll target for the bookmark
|
|
20
|
+
return size;
|
|
21
|
+
}
|
|
22
|
+
getProps() {
|
|
23
|
+
return {
|
|
24
|
+
title: this.title,
|
|
25
|
+
level: this.level,
|
|
26
|
+
child: this.child,
|
|
27
|
+
y: this.y,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PDFElement, LayoutContext } from "../pdf-element.js";
|
|
2
|
+
import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
|
|
3
|
+
/**
|
|
4
|
+
* Makes its child a clickable hyperlink. `href` is an external URL; `dest` is an internal named
|
|
5
|
+
* destination (an `Anchor` elsewhere in the document) - exactly one is set. Layout-transparent: it
|
|
6
|
+
* delegates `calculateLayout` to the child and records the resulting box, which the renderer turns into
|
|
7
|
+
* a `Link` IR node (and the page renderer into a /Link annotation). The link draws nothing itself -
|
|
8
|
+
* style the child (e.g. a blue Text) if you want it to look like a link.
|
|
9
|
+
*/
|
|
10
|
+
export declare class LinkElement extends PDFElement {
|
|
11
|
+
private href?;
|
|
12
|
+
private dest?;
|
|
13
|
+
private child;
|
|
14
|
+
private x;
|
|
15
|
+
private y;
|
|
16
|
+
private width;
|
|
17
|
+
private height;
|
|
18
|
+
constructor({ href, dest, child }: {
|
|
19
|
+
href?: string;
|
|
20
|
+
dest?: string;
|
|
21
|
+
child: PDFElement;
|
|
22
|
+
});
|
|
23
|
+
calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
|
|
24
|
+
getProps(): {
|
|
25
|
+
href: string | undefined;
|
|
26
|
+
dest: string | undefined;
|
|
27
|
+
child: PDFElement;
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PDFElement } from "../pdf-element.js";
|
|
2
|
+
/**
|
|
3
|
+
* Makes its child a clickable hyperlink. `href` is an external URL; `dest` is an internal named
|
|
4
|
+
* destination (an `Anchor` elsewhere in the document) - exactly one is set. Layout-transparent: it
|
|
5
|
+
* delegates `calculateLayout` to the child and records the resulting box, which the renderer turns into
|
|
6
|
+
* a `Link` IR node (and the page renderer into a /Link annotation). The link draws nothing itself -
|
|
7
|
+
* style the child (e.g. a blue Text) if you want it to look like a link.
|
|
8
|
+
*/
|
|
9
|
+
export class LinkElement extends PDFElement {
|
|
10
|
+
constructor({ href, dest, child }) {
|
|
11
|
+
super();
|
|
12
|
+
this.x = 0;
|
|
13
|
+
this.y = 0;
|
|
14
|
+
this.width = 0;
|
|
15
|
+
this.height = 0;
|
|
16
|
+
this.href = href;
|
|
17
|
+
this.dest = dest;
|
|
18
|
+
this.child = child;
|
|
19
|
+
}
|
|
20
|
+
calculateLayout(constraints, offset, ctx) {
|
|
21
|
+
const size = this.child.calculateLayout(constraints, offset, ctx);
|
|
22
|
+
this.x = offset.x;
|
|
23
|
+
this.y = offset.y;
|
|
24
|
+
this.width = size.width;
|
|
25
|
+
this.height = size.height;
|
|
26
|
+
return size;
|
|
27
|
+
}
|
|
28
|
+
getProps() {
|
|
29
|
+
return {
|
|
30
|
+
href: this.href,
|
|
31
|
+
dest: this.dest,
|
|
32
|
+
child: this.child,
|
|
33
|
+
x: this.x,
|
|
34
|
+
y: this.y,
|
|
35
|
+
width: this.width,
|
|
36
|
+
height: this.height,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|