@jasy/pdf 1.0.0-beta.4 → 1.0.0-beta.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 +3 -2
- package/dist/api/color.d.ts +1 -24
- package/dist/api/color.js +2 -278
- package/dist/api/content.d.ts +7 -2
- package/dist/api/content.js +17 -7
- package/dist/api/descriptor.js +30 -3
- package/dist/api/flow.d.ts +55 -0
- package/dist/api/flow.js +71 -0
- package/dist/api/gradient.d.ts +1 -53
- package/dist/api/gradient.js +2 -97
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/layout.js +4 -3
- package/dist/api/structure.d.ts +1 -1
- package/dist/api/table.js +3 -1
- package/dist/api/text.d.ts +8 -8
- package/dist/api/text.js +6 -0
- package/dist/canvas/painter.d.ts +2 -2
- package/dist/canvas/painter.js +2 -2
- package/dist/common/color-input.d.ts +24 -0
- package/dist/common/color-input.js +278 -0
- package/dist/common/diagnostics.d.ts +9 -1
- package/dist/common/gradient.d.ts +53 -0
- package/dist/common/gradient.js +97 -0
- package/dist/elements/container-element.d.ts +3 -0
- package/dist/elements/container-element.js +17 -5
- package/dist/elements/image-element.d.ts +37 -26
- package/dist/elements/image-element.js +88 -50
- package/dist/elements/index.d.ts +3 -0
- package/dist/elements/index.js +3 -0
- package/dist/elements/layout/column-break-element.d.ts +17 -0
- package/dist/elements/layout/column-break-element.js +26 -0
- package/dist/elements/layout/deferred-element.d.ts +9 -1
- package/dist/elements/layout/deferred-element.js +18 -3
- package/dist/elements/layout/expanded-element.d.ts +4 -0
- package/dist/elements/layout/expanded-element.js +14 -1
- package/dist/elements/layout/flow-element.d.ts +109 -0
- package/dist/elements/layout/flow-element.js +317 -0
- package/dist/elements/layout/keep-together-element.js +14 -2
- package/dist/elements/layout/padding-element.d.ts +3 -0
- package/dist/elements/layout/padding-element.js +9 -2
- package/dist/elements/layout/page-builder-element.js +2 -2
- package/dist/elements/layout/positioned-element.d.ts +9 -2
- package/dist/elements/layout/positioned-element.js +6 -2
- package/dist/elements/layout/proxy-element.d.ts +4 -0
- package/dist/elements/layout/proxy-element.js +12 -0
- package/dist/elements/layout/region-element.d.ts +53 -0
- package/dist/elements/layout/region-element.js +109 -0
- package/dist/elements/layout/repeating-header-element.d.ts +3 -0
- package/dist/elements/layout/repeating-header-element.js +16 -1
- package/dist/elements/layout/rotated-element.d.ts +2 -0
- package/dist/elements/layout/rotated-element.js +8 -0
- package/dist/elements/pdf-element.d.ts +32 -3
- package/dist/elements/pdf-element.js +50 -1
- package/dist/elements/rectangle-element.d.ts +12 -6
- package/dist/elements/rectangle-element.js +31 -12
- package/dist/elements/row-element.d.ts +6 -12
- package/dist/elements/row-element.js +34 -4
- package/dist/elements/svg-element.js +4 -10
- package/dist/elements/text-element.d.ts +17 -1
- package/dist/elements/text-element.js +63 -40
- package/dist/ir/display-list.d.ts +6 -1
- package/dist/layout/box-constraints.d.ts +12 -0
- package/dist/layout/box-constraints.js +17 -0
- package/dist/layout/collect-images.js +8 -4
- package/dist/layout/flex-item.d.ts +2 -0
- package/dist/layout/flex-item.js +14 -2
- package/dist/layout/fragmentation.d.ts +11 -5
- package/dist/layout/fragmentation.js +27 -7
- package/dist/layout/layout-cache.d.ts +8 -8
- package/dist/layout/layout-cache.js +10 -7
- package/dist/platform/browser-fs.d.ts +1 -2
- package/dist/platform/browser-fs.js +3 -6
- package/dist/platform/node-fs.d.ts +0 -1
- package/dist/platform/node-fs.js +0 -4
- package/dist/renderer/column-break-renderer.d.ts +8 -0
- package/dist/renderer/column-break-renderer.js +15 -0
- package/dist/renderer/emoji-image.js +3 -1
- package/dist/renderer/flow-renderer.d.ts +16 -0
- package/dist/renderer/flow-renderer.js +21 -0
- package/dist/renderer/image-renderer.js +31 -54
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/page-renderer.js +3 -4
- package/dist/renderer/pdf-backend.js +9 -2
- package/dist/renderer/pdf-document-renderer.js +2 -1
- package/dist/renderer/pdf-renderer.js +9 -0
- package/dist/renderer/positioned-renderer.d.ts +14 -2
- package/dist/renderer/positioned-renderer.js +40 -4
- package/dist/renderer/rectangle-renderer.d.ts +1 -1
- package/dist/renderer/rectangle-renderer.js +11 -6
- package/dist/renderer/rotated-renderer.js +3 -2
- package/dist/renderer/text-renderer.js +25 -28
- package/dist/svg/index.d.ts +1 -1
- package/dist/svg/index.js +26 -5
- package/dist/svg/style.js +1 -1
- package/dist/text/line-breaker.d.ts +23 -0
- package/dist/text/line-breaker.js +142 -65
- package/dist/text/line-metrics.d.ts +2 -0
- package/dist/text/line-metrics.js +5 -1
- package/dist/text/word-splitting.js +11 -5
- package/dist/utils/flex-layout.d.ts +3 -1
- package/dist/utils/flex-layout.js +141 -33
- package/dist/utils/image-helper.d.ts +59 -11
- package/dist/utils/image-helper.js +207 -80
- package/dist/utils/pdf-object-manager.d.ts +6 -3
- package/dist/utils/pdf-object-manager.js +31 -7
- package/dist/utils/renderer-registry.d.ts +6 -0
- package/dist/utils/renderer-registry.js +1 -1
- package/dist/utils/ttf-parser.js +3 -2
- package/package.json +9 -2
|
@@ -22,18 +22,21 @@ export function insetContext(ctx, chrome) {
|
|
|
22
22
|
return { ...ctx, pageBodyHeight: ctx.pageBodyHeight - chrome };
|
|
23
23
|
}
|
|
24
24
|
export function reportOverflow(child, childHeight, maxHeight, ctx) {
|
|
25
|
-
const policy = ctx.onOverflow ?? "ignore";
|
|
26
25
|
const name = child.constructor.name.replace(/Element$/, "");
|
|
27
|
-
|
|
26
|
+
reportClipped(child, `${name} is ${Math.round(childHeight)}pt tall but the page region is only ` +
|
|
28
27
|
`${Math.round(maxHeight)}pt and it cannot be broken - reduce its size, give it a bounded ` +
|
|
29
|
-
`height, or let it split across pages
|
|
28
|
+
`height, or let it split across pages.`, ctx);
|
|
29
|
+
}
|
|
30
|
+
/** Content cut off at the bottom, under the author's `onOverflow` policy. */
|
|
31
|
+
export function reportClipped(about, detail, ctx) {
|
|
32
|
+
const policy = ctx.onOverflow ?? "ignore";
|
|
30
33
|
if (policy === "error")
|
|
31
34
|
throw new Error(`Layout overflow: ${detail}`);
|
|
32
35
|
diagnose(ctx.diagnostics, {
|
|
33
36
|
code: "overflow-clipped",
|
|
34
37
|
severity: "warning",
|
|
35
38
|
message: `Layout overflow (clipped): ${detail}`,
|
|
36
|
-
about
|
|
39
|
+
about,
|
|
37
40
|
page: pageOf(ctx),
|
|
38
41
|
// `ignore` means "no console" - an `onDiagnostic` handler still hears of it.
|
|
39
42
|
fallback: policy === "ignore" ? "silent" : "warn",
|
|
@@ -49,6 +52,10 @@ export function reportOverflow(child, childHeight, maxHeight, ctx) {
|
|
|
49
52
|
export function childrenForceBreak(children) {
|
|
50
53
|
return children.some((child, i) => child.hasForcedBreak() || (i > 0 && child.breaksBefore()) || child.breaksAfter());
|
|
51
54
|
}
|
|
55
|
+
/** The packer kept every child as it was - none came back as a piece of itself (a sized Flow). */
|
|
56
|
+
export function packedUnchanged(children, fitted) {
|
|
57
|
+
return fitted.length === children.length && fitted.every((child, i) => child === children[i]);
|
|
58
|
+
}
|
|
52
59
|
/**
|
|
53
60
|
* Packs a vertical stack of children into `maxHeight`, in order. Children are measured
|
|
54
61
|
* against `width` (unbounded height) and added until one would overflow; that straddling
|
|
@@ -80,6 +87,13 @@ export function packChildren(children, maxHeight, width, ctx, gap = 0) {
|
|
|
80
87
|
// page, regardless of how much room is left.
|
|
81
88
|
if (child.isPageBreak())
|
|
82
89
|
return spill(i + 1, true);
|
|
90
|
+
// A column break cuts a Flow's Region; anywhere else it stays in place, draws nothing, and warns.
|
|
91
|
+
if (child.isColumnBreak()) {
|
|
92
|
+
if (ctx.inFlowRegion)
|
|
93
|
+
return spill(i + 1, "column");
|
|
94
|
+
fitted.push(child);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
83
97
|
// break-before: this child wants a fresh page. Honour it only if something is already on this region
|
|
84
98
|
// (CSS ignores break-before at the top of a page); that guard also prevents an empty page + a loop.
|
|
85
99
|
if (child.breaksBefore() && fitted.length > 0)
|
|
@@ -89,8 +103,10 @@ export function packChildren(children, maxHeight, width, ctx, gap = 0) {
|
|
|
89
103
|
const lead = fitted.length > 0 ? gap : 0;
|
|
90
104
|
// Place whole only if it fits AND holds no forced break: a child that fits by height but contains
|
|
91
105
|
// a `PageBreak` (or a break-before/after) in its subtree must still be fragmented, or the break
|
|
92
|
-
// would be swallowed.
|
|
93
|
-
if (usedHeight + lead + childHeight <= maxHeight &&
|
|
106
|
+
// would be swallowed. A region-sized child (a Flow) has no height of its own to fit by.
|
|
107
|
+
if (usedHeight + lead + childHeight <= maxHeight &&
|
|
108
|
+
!child.hasForcedBreak() &&
|
|
109
|
+
!child.needsRegion()) {
|
|
94
110
|
fitted.push(child);
|
|
95
111
|
usedHeight += lead + childHeight;
|
|
96
112
|
// break-after: everything after this child starts a fresh page.
|
|
@@ -114,7 +130,11 @@ export function packChildren(children, maxHeight, width, ctx, gap = 0) {
|
|
|
114
130
|
// break-before at the top of the region, which CSS ignores. Keep packing the siblings after
|
|
115
131
|
// it instead of spilling them, so break-before does not accidentally push later content away.
|
|
116
132
|
if (!split.remainder && !childBroke) {
|
|
117
|
-
|
|
133
|
+
// A region-sized child comes back as a different, sized piece: count what it holds.
|
|
134
|
+
const placed = split.fitted === child
|
|
135
|
+
? childHeight
|
|
136
|
+
: split.fitted.measure(BoxConstraints.loose(width, Infinity), ctx).height;
|
|
137
|
+
usedHeight += lead + placed;
|
|
118
138
|
continue;
|
|
119
139
|
}
|
|
120
140
|
if (split.remainder)
|
|
@@ -39,15 +39,15 @@ export declare class SizeCache {
|
|
|
39
39
|
set(constraints: BoxConstraints, ctx: LayoutContext, onPage: boolean, size: Size): void;
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
* columns are measured from its cells and fitted to its width). It is built again
|
|
44
|
-
* context key or the width changes - the size rules and the one constraint building
|
|
45
|
-
* page belongs to the key is decided by what was built: a subtree
|
|
46
|
-
* on every page.
|
|
42
|
+
* What an element builds from the context at layout time: a subtree (`Deferred`: a table whose `auto`
|
|
43
|
+
* columns are measured from its cells and fitted to its width), or a Text's lines. It is built again
|
|
44
|
+
* only when the context key or the width changes - the size rules and the one constraint building
|
|
45
|
+
* reads. Whether the page belongs to the key is decided by what was built (`onPage`): a subtree
|
|
46
|
+
* without a `PageBuilder` is the same on every page.
|
|
47
47
|
*/
|
|
48
|
-
export declare class BuiltFromContext<T
|
|
49
|
-
|
|
50
|
-
}> {
|
|
48
|
+
export declare class BuiltFromContext<T> {
|
|
49
|
+
private readonly onPage;
|
|
51
50
|
private last?;
|
|
51
|
+
constructor(onPage: (value: T) => boolean);
|
|
52
52
|
get(ctx: LayoutContext, width: number, build: () => T): T;
|
|
53
53
|
}
|
|
@@ -86,22 +86,25 @@ export class SizeCache {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
90
|
-
* columns are measured from its cells and fitted to its width). It is built again
|
|
91
|
-
* context key or the width changes - the size rules and the one constraint building
|
|
92
|
-
* page belongs to the key is decided by what was built: a subtree
|
|
93
|
-
* on every page.
|
|
89
|
+
* What an element builds from the context at layout time: a subtree (`Deferred`: a table whose `auto`
|
|
90
|
+
* columns are measured from its cells and fitted to its width), or a Text's lines. It is built again
|
|
91
|
+
* only when the context key or the width changes - the size rules and the one constraint building
|
|
92
|
+
* reads. Whether the page belongs to the key is decided by what was built (`onPage`): a subtree
|
|
93
|
+
* without a `PageBuilder` is the same on every page.
|
|
94
94
|
*/
|
|
95
95
|
export class BuiltFromContext {
|
|
96
|
+
constructor(onPage) {
|
|
97
|
+
this.onPage = onPage;
|
|
98
|
+
}
|
|
96
99
|
get(ctx, width, build) {
|
|
97
100
|
const last = this.last;
|
|
98
101
|
if (layoutCache.enabled &&
|
|
99
102
|
last &&
|
|
100
103
|
last.width === width &&
|
|
101
|
-
sameContext(last.key, contextKeyOf(ctx, last.value
|
|
104
|
+
sameContext(last.key, contextKeyOf(ctx, this.onPage(last.value))))
|
|
102
105
|
return last.value;
|
|
103
106
|
const value = build();
|
|
104
|
-
this.last = { key: contextKeyOf(ctx,
|
|
107
|
+
this.last = { key: contextKeyOf(ctx, this.onPage(value)), width, value };
|
|
105
108
|
return value;
|
|
106
109
|
}
|
|
107
110
|
}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export declare function readFileBytes(
|
|
2
|
-
export declare function readFileBytesAsync(_path: string): Promise<Uint8Array>;
|
|
1
|
+
export declare function readFileBytes(path: string): Uint8Array;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// Browser stand-in for platform/node-fs.ts (selected by the package.json "browser" field). The browser has
|
|
2
2
|
// no file paths, so loading from one throws a clear, actionable error - pass bytes instead.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
export function readFileBytesAsync(_path) {
|
|
8
|
-
throw new Error(`Loading an image from a file path ${hint}`);
|
|
3
|
+
export function readFileBytes(path) {
|
|
4
|
+
throw new Error(`Loading "${path}" from a file path needs Node. In the browser pass the bytes (a Uint8Array) ` +
|
|
5
|
+
"instead - e.g. import the file as a URL and fetch() it.");
|
|
9
6
|
}
|
package/dist/platform/node-fs.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
// Node implementation of the platform file reads. In a browser bundle this whole module is swapped for
|
|
2
2
|
// platform/browser-fs.ts via the package.json "browser" field, so `node:fs` never reaches the browser.
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
-
import { readFile } from "node:fs/promises";
|
|
5
4
|
export function readFileBytes(path) {
|
|
6
5
|
return readFileSync(path);
|
|
7
6
|
}
|
|
8
|
-
export function readFileBytesAsync(path) {
|
|
9
|
-
return readFile(path);
|
|
10
|
-
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RenderScope } from "../utils/renderer-registry.js";
|
|
2
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
3
|
+
import { ColumnBreakElement } from "../elements/layout/column-break-element.js";
|
|
4
|
+
import { IRNode } from "../ir/display-list.js";
|
|
5
|
+
/** A `ColumnBreak` that reaches this pass was nowhere a Flow could break at it - it had no effect. */
|
|
6
|
+
export declare class ColumnBreakRenderer {
|
|
7
|
+
static render(element: ColumnBreakElement, objectManager: PDFObjectManager, _scope: RenderScope): Promise<IRNode[]>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A `ColumnBreak` that reaches this pass was nowhere a Flow could break at it - it had no effect. */
|
|
2
|
+
export class ColumnBreakRenderer {
|
|
3
|
+
static async render(element, objectManager, _scope) {
|
|
4
|
+
objectManager.diagnostics.report({
|
|
5
|
+
code: "column-break-ignored",
|
|
6
|
+
severity: "warning",
|
|
7
|
+
message: "ColumnBreak had no effect: it breaks only where a Flow's content runs down a Region - not " +
|
|
8
|
+
"outside a Flow, and not in a Row or a table row, which never break. To start a new page, " +
|
|
9
|
+
"use PageBreak.",
|
|
10
|
+
about: element,
|
|
11
|
+
page: element.page,
|
|
12
|
+
});
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CustomBytesImage } from "../elements/image-element.js";
|
|
2
|
-
import { decodePngToRgbFlate } from "../utils/image-helper.js";
|
|
2
|
+
import { decodePngToRgbFlate, jpegColor } from "../utils/image-helper.js";
|
|
3
3
|
import { bytesFromLatin1 } from "../utils/bytes.js";
|
|
4
4
|
// Per-URL fetch cache (process-wide): an emoji image is fetched once, however many times it appears.
|
|
5
5
|
const cache = new Map();
|
|
@@ -48,6 +48,7 @@ export async function emojiImageNode(codePoint, source, x, baselineY, em) {
|
|
|
48
48
|
// JPEG embeds raw; PNG is decoded to DeviceRGB + an /SMask alpha channel (transparency).
|
|
49
49
|
let data = fileData;
|
|
50
50
|
let smask;
|
|
51
|
+
const color = imageType === "DCTDecode" ? jpegColor(bytesFromLatin1(fileData)) : undefined;
|
|
51
52
|
if (imageType === "FlateDecode") {
|
|
52
53
|
const decoded = await decodePngToRgbFlate(bytesFromLatin1(fileData));
|
|
53
54
|
data = decoded.data;
|
|
@@ -63,6 +64,7 @@ export async function emojiImageNode(codePoint, source, x, baselineY, em) {
|
|
|
63
64
|
intrinsicHeight: dims.height,
|
|
64
65
|
data,
|
|
65
66
|
imageType,
|
|
67
|
+
...(color ? { color } : {}),
|
|
66
68
|
...(smask ? { smask } : {}),
|
|
67
69
|
};
|
|
68
70
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FlowElement, FlowPieceElement } from "../elements/layout/flow-element.js";
|
|
2
|
+
import { RegionElement } from "../elements/layout/region-element.js";
|
|
3
|
+
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
4
|
+
import { IRNode } from "../ir/display-list.js";
|
|
5
|
+
import { type RenderScope } from "../utils/renderer-registry.js";
|
|
6
|
+
/** A Flow draws the page it made (laid out in place, in a box with a height). */
|
|
7
|
+
export declare class FlowRenderer {
|
|
8
|
+
static render(flow: FlowElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
|
|
9
|
+
}
|
|
10
|
+
/** One page of a Flow: its template, whose Regions draw what they hold. */
|
|
11
|
+
export declare class FlowPieceRenderer {
|
|
12
|
+
static render(piece: FlowPieceElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
|
|
13
|
+
}
|
|
14
|
+
export declare class RegionRenderer {
|
|
15
|
+
static render(region: RegionElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RendererRegistry, within } from "../utils/renderer-registry.js";
|
|
2
|
+
/** A Flow draws the page it made (laid out in place, in a box with a height). */
|
|
3
|
+
export class FlowRenderer {
|
|
4
|
+
static async render(flow, objectManager, scope) {
|
|
5
|
+
const { x, y, piece } = flow.getProps();
|
|
6
|
+
return piece ? RendererRegistry.render(piece, objectManager, within(scope, { x, y })) : [];
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** One page of a Flow: its template, whose Regions draw what they hold. */
|
|
10
|
+
export class FlowPieceRenderer {
|
|
11
|
+
static async render(piece, objectManager, scope) {
|
|
12
|
+
const { x, y, child } = piece.getProps();
|
|
13
|
+
return RendererRegistry.render(child, objectManager, within(scope, { x, y }));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class RegionRenderer {
|
|
17
|
+
static async render(region, objectManager, scope) {
|
|
18
|
+
const { x, y, child } = region.getProps();
|
|
19
|
+
return child ? RendererRegistry.render(child, objectManager, within(scope, { x, y })) : [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { BoxFit } from "../elements/image-element.js";
|
|
2
2
|
import { bytesFromLatin1 } from "../utils/bytes.js";
|
|
3
|
-
import { applyContainFit, applyCoverFit, applyFillFit, applyFitNone, decodePngToRgbFlate, } from "../utils/image-helper.js";
|
|
3
|
+
import { applyContainFit, applyCoverFit, applyFillFit, applyFitNone, decodePngToRgbFlate, jpegColor, naturalSize, } from "../utils/image-helper.js";
|
|
4
4
|
import { isRounded } from "../ir/display-list.js";
|
|
5
5
|
export class ImageRenderer {
|
|
6
6
|
static async render(imageElement, objectManager, scope) {
|
|
7
7
|
// Load the image and convert it in a binary string
|
|
8
|
-
|
|
8
|
+
const { x: ownX, y: ownY, width, height, image, fit, radius, align, alt, } = imageElement.getProps();
|
|
9
9
|
// Its own position is relative to `scope.origin`; the IR wants page coordinates.
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const x = scope.origin.x + (ownX ?? 0);
|
|
11
|
+
const y = scope.origin.y + (ownY ?? 0);
|
|
12
12
|
await image.init(); // Load and initialize the image
|
|
13
13
|
const imageType = await image.getImageType(); // For the moment we can handle `png` and `jpg/jpeg` files
|
|
14
14
|
const fileData = await image.getFileData();
|
|
15
15
|
const dimensions = await image.getImageDimensions();
|
|
16
|
+
// Laid out at its own size in points; the pixels only describe the XObject.
|
|
17
|
+
const own = naturalSize(dimensions);
|
|
16
18
|
if (!fileData) {
|
|
17
19
|
throw new Error("File data cannot be `null`");
|
|
18
20
|
}
|
|
@@ -20,71 +22,46 @@ export class ImageRenderer {
|
|
|
20
22
|
// so decode it to raw DeviceRGB samples that the FlateDecode XObject path expects.
|
|
21
23
|
let embedData = fileData;
|
|
22
24
|
let smask;
|
|
25
|
+
// A JPEG is embedded as it is, so it keeps the color space it was saved in; a PNG becomes RGB.
|
|
26
|
+
const color = imageType === "DCTDecode" ? jpegColor(bytesFromLatin1(fileData)) : undefined;
|
|
23
27
|
if (imageType === "FlateDecode") {
|
|
24
28
|
const decoded = await decodePngToRgbFlate(bytesFromLatin1(fileData));
|
|
25
29
|
embedData = decoded.data;
|
|
26
30
|
smask = decoded.smask;
|
|
27
31
|
}
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
width
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
x += fitContainResult.offsetX;
|
|
45
|
-
y += fitContainResult.offsetY;
|
|
46
|
-
width = fitContainResult.width;
|
|
47
|
-
height = fitContainResult.height;
|
|
48
|
-
break;
|
|
49
|
-
case BoxFit.none:
|
|
50
|
-
const fitNoneResult = applyFitNone(dimensions.width, dimensions.height, width ?? 0, height ?? 0);
|
|
51
|
-
x += fitNoneResult.offsetX;
|
|
52
|
-
y += fitNoneResult.offsetY;
|
|
53
|
-
width = fitNoneResult.width;
|
|
54
|
-
height = fitNoneResult.height;
|
|
55
|
-
break;
|
|
56
|
-
case BoxFit.fill:
|
|
57
|
-
const fitFillResult = applyFillFit(width ?? 0, height ?? 0);
|
|
58
|
-
width = fitFillResult.width;
|
|
59
|
-
height = fitFillResult.height;
|
|
60
|
-
}
|
|
61
|
-
// A radius rounds the image BOX (the element frame), so it clips to that frame too -
|
|
62
|
-
// independent of the cover/contain overflow clip.
|
|
63
|
-
const wantsClip = mustCreateOverflowContainer || isRounded(radius);
|
|
32
|
+
// The box laid out for it; `fit` decides what is drawn in it and where.
|
|
33
|
+
const box = {
|
|
34
|
+
x,
|
|
35
|
+
y,
|
|
36
|
+
width: width ?? own.width,
|
|
37
|
+
height: height ?? own.height,
|
|
38
|
+
};
|
|
39
|
+
const fitted = fit === BoxFit.cover
|
|
40
|
+
? applyCoverFit(own.width, own.height, box.width, box.height, align)
|
|
41
|
+
: fit === BoxFit.contain
|
|
42
|
+
? applyContainFit(own.width, own.height, box.width, box.height, align)
|
|
43
|
+
: fit === BoxFit.none
|
|
44
|
+
? applyFitNone(own.width, own.height, box.width, box.height, align)
|
|
45
|
+
: applyFillFit(box.width, box.height);
|
|
46
|
+
// Anything but `fill` may leave the box, so it is clipped to it; a radius rounds the box too.
|
|
47
|
+
const wantsClip = fit !== BoxFit.fill || isRounded(radius);
|
|
64
48
|
// The fitted geometry becomes a display-list primitive; the backend registers
|
|
65
49
|
// the XObject and emits the placement (+ clip, rounded when a radius is set).
|
|
66
50
|
const node = {
|
|
67
51
|
type: "image",
|
|
68
|
-
x,
|
|
69
|
-
y,
|
|
70
|
-
width: width,
|
|
71
|
-
height: height,
|
|
52
|
+
x: x + fitted.offsetX,
|
|
53
|
+
y: y + fitted.offsetY,
|
|
54
|
+
width: fitted.width,
|
|
55
|
+
height: fitted.height,
|
|
72
56
|
intrinsicWidth: dimensions.width,
|
|
73
57
|
intrinsicHeight: dimensions.height,
|
|
74
58
|
data: embedData,
|
|
75
59
|
imageType,
|
|
60
|
+
...(color ? { color } : {}),
|
|
61
|
+
...(dimensions.orientation ? { orientation: dimensions.orientation } : {}),
|
|
76
62
|
...(smask ? { smask } : {}),
|
|
77
63
|
...(radius ? { radius } : {}),
|
|
78
|
-
...(wantsClip
|
|
79
|
-
? {
|
|
80
|
-
clip: {
|
|
81
|
-
x: containerDimensions.x,
|
|
82
|
-
y: containerDimensions.y,
|
|
83
|
-
width: containerDimensions.width,
|
|
84
|
-
height: containerDimensions.height,
|
|
85
|
-
},
|
|
86
|
-
}
|
|
87
|
-
: {}),
|
|
64
|
+
...(wantsClip ? { clip: box } : {}),
|
|
88
65
|
};
|
|
89
66
|
// Accessible tagging: an image WITH alt text is a Figure; without, it stays untagged and the backend
|
|
90
67
|
// treats it as decoration (an Artifact). One struct element per image.
|
package/dist/renderer/index.d.ts
CHANGED
package/dist/renderer/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolvePageContentBox } from "../elements/page-element.js";
|
|
|
3
3
|
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
4
4
|
import { Orientation } from "./pdf-config.js";
|
|
5
5
|
import { PdfBackend } from "./pdf-backend.js";
|
|
6
|
+
import { PositionedRenderer } from "./positioned-renderer.js";
|
|
6
7
|
export class PageRenderer {
|
|
7
8
|
static async render(page, objectManager) {
|
|
8
9
|
const { children, config, header, footer } = page.getProps();
|
|
@@ -26,10 +27,8 @@ export class PageRenderer {
|
|
|
26
27
|
// Collect the whole page as a display list (top-left coordinates), flip it to PDF
|
|
27
28
|
// coordinates at this one seam, then serialize once. Serializing registers the
|
|
28
29
|
// fonts/images used below, so it must run before the resource section.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
nodes.push(...(await RendererRegistry.render(element, objectManager, scope)));
|
|
32
|
-
}
|
|
30
|
+
// The page is the outermost stacking context: header, body and footer share one paint order.
|
|
31
|
+
const nodes = await PositionedRenderer.paintStacked(renderables, objectManager, scope);
|
|
33
32
|
// Accessible tagging: one struct context per page (allocates its StructParents index), threaded into
|
|
34
33
|
// serialize so each drawable node is wrapped in marked content. Undefined = tagging off (byte-identical).
|
|
35
34
|
const structCtx = objectManager.struct.enabled ? objectManager.struct.beginPage() : undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isRounded, } from "../ir/display-list.js";
|
|
2
2
|
import { Color } from "../common/color.js";
|
|
3
|
+
import { ORIENTATION_MATRIX } from "../utils/image-helper.js";
|
|
3
4
|
/**
|
|
4
5
|
* PDF backend - turns display-list primitives into content-stream operators.
|
|
5
6
|
*
|
|
@@ -416,8 +417,14 @@ export class PdfBackend {
|
|
|
416
417
|
case "image": {
|
|
417
418
|
// The backend owns PDF resource creation: register the XObject (using the
|
|
418
419
|
// source pixel dimensions) and then place it with a scaling matrix.
|
|
419
|
-
const ref = om.registerImage(node.intrinsicWidth, node.intrinsicHeight, node.imageType, node.data, node.smask);
|
|
420
|
-
|
|
420
|
+
const ref = om.registerImage(node.intrinsicWidth, node.intrinsicHeight, node.imageType, node.data, node.smask, node.color);
|
|
421
|
+
// The placement scales the unit square to the box; an orientation turns the pixels inside it.
|
|
422
|
+
const upright = node.orientation
|
|
423
|
+
? `${ORIENTATION_MATRIX[node.orientation].join(" ")} cm\n`
|
|
424
|
+
: "";
|
|
425
|
+
const draw = `q\n${node.width} 0 0 ${node.height} ${node.x} ${node.y} cm\n` +
|
|
426
|
+
upright +
|
|
427
|
+
`/IM${ref} Do\nQ\n`;
|
|
421
428
|
if (!node.clip)
|
|
422
429
|
return draw;
|
|
423
430
|
// Clip to the frame (re … W n); rounded when a radius is set. The rectangular
|
|
@@ -102,7 +102,8 @@ export class PDFDocumentRenderer {
|
|
|
102
102
|
// restores their real positions (layout is deterministic). A consumed trailing `PageBreak`
|
|
103
103
|
// (forceBreak with no remainder) is the exception: render the packed `fitted` instead, which no
|
|
104
104
|
// longer holds the break marker, so the original with the break does not leak into the render pass.
|
|
105
|
-
|
|
105
|
+
// So is a region that came back as a sized piece (a Flow): it is drawn as it was paginated.
|
|
106
|
+
if (isFirstRegion && remainder === null && !forceBreak && fitted === region) {
|
|
106
107
|
pages.push({ kind: "whole", page });
|
|
107
108
|
break;
|
|
108
109
|
}
|
|
@@ -43,6 +43,11 @@ import { SignatureFieldElement } from "../elements/forms/signature-element.js";
|
|
|
43
43
|
import { FormFieldRenderer } from "./form-field-renderer.js";
|
|
44
44
|
import { PageBuilderElement } from "../elements/layout/page-builder-element.js";
|
|
45
45
|
import { PageBuilderRenderer } from "./page-builder-renderer.js";
|
|
46
|
+
import { FlowElement, FlowPieceElement } from "../elements/layout/flow-element.js";
|
|
47
|
+
import { RegionElement } from "../elements/layout/region-element.js";
|
|
48
|
+
import { FlowPieceRenderer, FlowRenderer, RegionRenderer } from "./flow-renderer.js";
|
|
49
|
+
import { ColumnBreakElement } from "../elements/layout/column-break-element.js";
|
|
50
|
+
import { ColumnBreakRenderer } from "./column-break-renderer.js";
|
|
46
51
|
import { RotatedElement } from "../elements/layout/rotated-element.js";
|
|
47
52
|
import { RotatedBoxElement } from "../elements/layout/rotated-box-element.js";
|
|
48
53
|
import { RotatedRenderer } from "./rotated-renderer.js";
|
|
@@ -78,6 +83,10 @@ export class PDFRenderer {
|
|
|
78
83
|
RendererRegistry.register(BookmarkElement, BookmarkRenderer.render);
|
|
79
84
|
RendererRegistry.register(AnchorElement, AnchorRenderer.render);
|
|
80
85
|
RendererRegistry.register(PageBuilderElement, PageBuilderRenderer.render);
|
|
86
|
+
RendererRegistry.register(FlowElement, FlowRenderer.render);
|
|
87
|
+
RendererRegistry.register(FlowPieceElement, FlowPieceRenderer.render);
|
|
88
|
+
RendererRegistry.register(RegionElement, RegionRenderer.render);
|
|
89
|
+
RendererRegistry.register(ColumnBreakElement, ColumnBreakRenderer.render);
|
|
81
90
|
RendererRegistry.register(RotatedElement, RotatedRenderer.render);
|
|
82
91
|
RendererRegistry.register(RotatedBoxElement, RotatedRenderer.render);
|
|
83
92
|
let pdfContent = "";
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
2
|
import { PositionedElement } from "../elements/layout/positioned-element.js";
|
|
3
|
+
import type { PDFElement } from "../elements/pdf-element.js";
|
|
3
4
|
import { type RenderScope } from "../utils/renderer-registry.js";
|
|
4
5
|
import { IRNode } from "../ir/display-list.js";
|
|
5
6
|
export declare class PositionedRenderer {
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* Rendered at its own place in the tree, from its frame: the child was laid out relative to the
|
|
9
|
+
* frame's top-left, not to the flow it sits in. Without a `zIndex` that place is also where it paints.
|
|
10
|
+
* With one it hands its drawing to the nearest stacking context (`paintStacked`), which sorts it - so
|
|
11
|
+
* tagging and everything else registered while rendering still sees the tree it stands in.
|
|
9
12
|
*/
|
|
10
13
|
static render(positionedElement: PositionedElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Paints the children of a stacking context (a frame, a clipping Box, a rotation, the page): the
|
|
16
|
+
* `Positioned` below it with a negative `zIndex` first, then the children in tree order, then the
|
|
17
|
+
* positive ones - lowest first, ties in tree order. Without a `zIndex` below it, this is the children
|
|
18
|
+
* in order.
|
|
19
|
+
*/
|
|
20
|
+
static paintStacked(children: readonly PDFElement[], objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
|
|
21
|
+
/** Children in tree order - what any element that is not a stacking context draws. */
|
|
22
|
+
static paintInOrder(children: readonly PDFElement[], objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
|
|
11
23
|
}
|
|
@@ -1,11 +1,47 @@
|
|
|
1
1
|
import { RendererRegistry } from "../utils/renderer-registry.js";
|
|
2
2
|
export class PositionedRenderer {
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Rendered at its own place in the tree, from its frame: the child was laid out relative to the
|
|
5
|
+
* frame's top-left, not to the flow it sits in. Without a `zIndex` that place is also where it paints.
|
|
6
|
+
* With one it hands its drawing to the nearest stacking context (`paintStacked`), which sorts it - so
|
|
7
|
+
* tagging and everything else registered while rendering still sees the tree it stands in.
|
|
6
8
|
*/
|
|
7
9
|
static async render(positionedElement, objectManager, scope) {
|
|
8
|
-
const { child } = positionedElement.getProps();
|
|
9
|
-
|
|
10
|
+
const { child, zIndex } = positionedElement.getProps();
|
|
11
|
+
const render = () => RendererRegistry.render(child, objectManager, { ...scope, origin: scope.frame });
|
|
12
|
+
if (zIndex === 0 || !scope.stacked)
|
|
13
|
+
return render();
|
|
14
|
+
// Its place in the stack is taken BEFORE its child renders: a nested Positioned with the same
|
|
15
|
+
// zIndex hands its drawing in during that, and must come after its outer one, in tree order.
|
|
16
|
+
const entry = { zIndex, nodes: [] };
|
|
17
|
+
scope.stacked.push(entry);
|
|
18
|
+
entry.nodes = await render();
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Paints the children of a stacking context (a frame, a clipping Box, a rotation, the page): the
|
|
23
|
+
* `Positioned` below it with a negative `zIndex` first, then the children in tree order, then the
|
|
24
|
+
* positive ones - lowest first, ties in tree order. Without a `zIndex` below it, this is the children
|
|
25
|
+
* in order.
|
|
26
|
+
*/
|
|
27
|
+
static async paintStacked(children, objectManager, scope) {
|
|
28
|
+
const stacked = [];
|
|
29
|
+
const flow = await PositionedRenderer.paintInOrder(children, objectManager, {
|
|
30
|
+
...scope,
|
|
31
|
+
stacked,
|
|
32
|
+
});
|
|
33
|
+
stacked.sort((a, b) => a.zIndex - b.zIndex); // stable: ties keep tree order
|
|
34
|
+
return [
|
|
35
|
+
...stacked.filter((s) => s.zIndex < 0).flatMap((s) => s.nodes),
|
|
36
|
+
...flow,
|
|
37
|
+
...stacked.filter((s) => s.zIndex > 0).flatMap((s) => s.nodes),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
/** Children in tree order - what any element that is not a stacking context draws. */
|
|
41
|
+
static async paintInOrder(children, objectManager, scope) {
|
|
42
|
+
const nodes = [];
|
|
43
|
+
for (const child of children)
|
|
44
|
+
nodes.push(...(await RendererRegistry.render(child, objectManager, scope)));
|
|
45
|
+
return nodes;
|
|
10
46
|
}
|
|
11
47
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PDFObjectManager } from "../utils/pdf-object-manager.js";
|
|
2
2
|
import { RectangleElement } from "../elements/rectangle-element.js";
|
|
3
|
-
import {
|
|
3
|
+
import type { RenderScope } from "../utils/renderer-registry.js";
|
|
4
4
|
import { IRNode } from "../ir/display-list.js";
|
|
5
5
|
export declare class RectangleRenderer {
|
|
6
6
|
static render(rectangleElement: RectangleElement, objectManager: PDFObjectManager, scope: RenderScope): Promise<IRNode[]>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isGradientInput, resolveGradient } from "../
|
|
1
|
+
import { PositionedRenderer } from "./positioned-renderer.js";
|
|
2
|
+
import { isGradientInput, resolveGradient } from "../common/gradient.js";
|
|
3
3
|
export class RectangleRenderer {
|
|
4
4
|
static async render(rectangleElement, objectManager, scope) {
|
|
5
5
|
const { x: ownX, y: ownY, width, height, children, color, backgroundColor, borderWidth, radius, sideBorders, overflow, relative, } = rectangleElement.getProps();
|
|
@@ -46,11 +46,16 @@ export class RectangleRenderer {
|
|
|
46
46
|
}
|
|
47
47
|
// Its children are placed relative to its own top-left; a `relative` box is also the frame its
|
|
48
48
|
// `Positioned` descendants were placed against.
|
|
49
|
-
const childScope = {
|
|
49
|
+
const childScope = {
|
|
50
|
+
...scope,
|
|
51
|
+
origin: { x, y },
|
|
52
|
+
frame: relative ? { x, y } : scope.frame,
|
|
53
|
+
};
|
|
54
|
+
// A frame or a clip paints its own `zIndex` order; any other box draws its children as they come.
|
|
50
55
|
if (children)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
nodes.push(...(rectangleElement.isStackingContext()
|
|
57
|
+
? await PositionedRenderer.paintStacked(children, objectManager, childScope)
|
|
58
|
+
: await PositionedRenderer.paintInOrder(children, objectManager, childScope)));
|
|
54
59
|
if (clip)
|
|
55
60
|
nodes.push({ type: "clip-pop" });
|
|
56
61
|
return nodes;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { within } from "../utils/renderer-registry.js";
|
|
2
|
+
import { PositionedRenderer } from "./positioned-renderer.js";
|
|
2
3
|
export class RotatedRenderer {
|
|
3
4
|
// Both `Rotated` (paint-only) and `RotatedBox` (layout-aware quarter-turns) expose the same
|
|
4
5
|
// `{ angle, child, x, y, width, height }` and rotate the child around the box center - shared here.
|
|
@@ -25,7 +26,7 @@ export class RotatedRenderer {
|
|
|
25
26
|
cy - sin * cx - cos * cy,
|
|
26
27
|
];
|
|
27
28
|
// The child is placed relative to the box top-left.
|
|
28
|
-
const childNodes = await
|
|
29
|
+
const childNodes = await PositionedRenderer.paintStacked([child], objectManager, within(scope, { x: ownX, y: ownY }));
|
|
29
30
|
// Wrap the child's drawing between a push (apply the matrix) and a pop (restore).
|
|
30
31
|
return [{ type: "transform-push", matrix }, ...childNodes, { type: "transform-pop" }];
|
|
31
32
|
}
|