@segnavia/render 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/dist/color/binary.js +27 -0
- package/dist/color/black-point.js +121 -0
- package/dist/color/engine.js +18 -0
- package/dist/color/interpolation.js +74 -0
- package/dist/color/profile.js +98 -0
- package/dist/color/spaces.js +138 -0
- package/dist/color/tables.js +255 -0
- package/dist/color/types.js +21 -0
- package/dist/deck-pdf.js +152 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +9 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +322 -0
- package/dist/page-paper.js +94 -0
- package/dist/pages-imposition.js +65 -0
- package/dist/pages-pdf.js +66 -0
- package/dist/pdf-compose.js +35 -0
- package/dist/pdf-geometry.js +27 -0
- package/dist/press/adapter.js +145 -0
- package/dist/press/color-policy.js +31 -0
- package/dist/press/content.js +269 -0
- package/dist/press/pdf-objects.js +88 -0
- package/dist/press/policy.js +120 -0
- package/dist/press/report.js +79 -0
- package/dist/press/types.d.ts +16 -0
- package/dist/press/visitor.js +140 -0
- package/package.json +32 -4
- package/README.md +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { RenderRefusedError, RendererUnavailableError } from "./errors.js";
|
|
2
|
+
import { renderDeckPdf } from "./deck-pdf.js";
|
|
3
|
+
import { assertPrintOptions, printCss, printPage, printViewport } from "./page-paper.js";
|
|
4
|
+
import { renderPagesPdf } from "./pages-pdf.js";
|
|
5
|
+
import { rewritePressPdf } from "./press/adapter.js";
|
|
6
|
+
import { Page, printProperties } from "@segnavia/format";
|
|
7
|
+
import { frameDocument, newNonce } from "@segnavia/frame/bridge";
|
|
8
|
+
import { printableDeck } from "@segnavia/frame/print-document";
|
|
9
|
+
import { PDFDocument } from "pdf-lib";
|
|
10
|
+
import { chromium } from "playwright-core";
|
|
11
|
+
//#region src/index.ts
|
|
12
|
+
const RENDER_BOUND_MS = 6e4;
|
|
13
|
+
const PAGE_READY_BOUND_MS = 1e4;
|
|
14
|
+
const FLUID_WIDTH = 1024;
|
|
15
|
+
const CONTAINER_ARGS = [
|
|
16
|
+
"--no-sandbox",
|
|
17
|
+
"--disable-setuid-sandbox",
|
|
18
|
+
"--disable-dev-shm-usage",
|
|
19
|
+
"--disable-gpu"
|
|
20
|
+
];
|
|
21
|
+
const NO_NETWORK_ARGS = [
|
|
22
|
+
"--proxy-server=socks5://127.0.0.1:9",
|
|
23
|
+
"--proxy-bypass-list=<-loopback>",
|
|
24
|
+
"--force-webrtc-ip-handling-policy=disable_non_proxied_udp"
|
|
25
|
+
];
|
|
26
|
+
var RenderTimeoutError = class extends Error {
|
|
27
|
+
name = "RenderTimeoutError";
|
|
28
|
+
};
|
|
29
|
+
function fontFaces(fonts = []) {
|
|
30
|
+
return fonts.map((font) => `@font-face{font-family:${JSON.stringify(font.family)};src:url(data:font/${font.format === "truetype" ? "ttf" : font.format === "opentype" ? "otf" : font.format};base64,${Buffer.from(font.data).toString("base64")}) format(${JSON.stringify(font.format)});font-weight:${font.weight ?? "normal"};font-style:${font.style ?? "normal"};font-display:block}`).join("");
|
|
31
|
+
}
|
|
32
|
+
async function withinBound(ms, work) {
|
|
33
|
+
let timer;
|
|
34
|
+
const bound = new Promise((_, reject) => {
|
|
35
|
+
timer = setTimeout(() => reject(new RenderTimeoutError(`The render did not finish within ${ms / 1e3}s.`)), ms);
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
return await Promise.race([work, bound]);
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function sheets(pdf) {
|
|
44
|
+
return (await PDFDocument.load(pdf)).getPageCount();
|
|
45
|
+
}
|
|
46
|
+
async function enforce(pdf, limits) {
|
|
47
|
+
if (limits?.maxBytes !== void 0 && pdf.byteLength > limits.maxBytes) throw new RenderRefusedError(`The PDF is ${pdf.byteLength} bytes; the limit is ${limits.maxBytes}.`);
|
|
48
|
+
if (limits?.maxSheets !== void 0) {
|
|
49
|
+
const count = await sheets(pdf);
|
|
50
|
+
if (count > limits.maxSheets) throw new RenderRefusedError(`The PDF has ${count} sheets; the limit is ${limits.maxSheets}.`);
|
|
51
|
+
}
|
|
52
|
+
return pdf;
|
|
53
|
+
}
|
|
54
|
+
async function finishPdf(pdf, options) {
|
|
55
|
+
if (!options.press) return enforce(pdf, options.limits);
|
|
56
|
+
if (options.limits?.maxBytes !== void 0 && pdf.byteLength > options.limits.maxBytes) throw new RenderRefusedError(`The PDF is ${pdf.byteLength} bytes; the limit is ${options.limits.maxBytes}.`);
|
|
57
|
+
const converted = await rewritePressPdf(pdf, options.press.profile);
|
|
58
|
+
options.press.report?.(converted.report);
|
|
59
|
+
return enforce(converted.bytes, options.limits);
|
|
60
|
+
}
|
|
61
|
+
async function captureImage(tab, options, fullPage) {
|
|
62
|
+
const format = options.format ?? "png";
|
|
63
|
+
if (format !== "webp") return tab.screenshot({
|
|
64
|
+
type: format,
|
|
65
|
+
fullPage,
|
|
66
|
+
...format === "jpeg" ? { quality: options.quality?.[0] ?? 90 } : {}
|
|
67
|
+
});
|
|
68
|
+
const session = await tab.context().newCDPSession(tab);
|
|
69
|
+
try {
|
|
70
|
+
const clip = await tab.evaluate((wholePage) => {
|
|
71
|
+
const scale = devicePixelRatio;
|
|
72
|
+
if (wholePage) return {
|
|
73
|
+
x: 0,
|
|
74
|
+
y: 0,
|
|
75
|
+
width: document.documentElement.scrollWidth,
|
|
76
|
+
height: document.documentElement.scrollHeight,
|
|
77
|
+
scale
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
x: scrollX,
|
|
81
|
+
y: scrollY,
|
|
82
|
+
width: innerWidth,
|
|
83
|
+
height: innerHeight,
|
|
84
|
+
scale
|
|
85
|
+
};
|
|
86
|
+
}, fullPage);
|
|
87
|
+
for (const quality of options.quality ?? [
|
|
88
|
+
90,
|
|
89
|
+
80,
|
|
90
|
+
70
|
|
91
|
+
]) {
|
|
92
|
+
const { data } = await session.send("Page.captureScreenshot", {
|
|
93
|
+
format: "webp",
|
|
94
|
+
quality,
|
|
95
|
+
captureBeyondViewport: fullPage,
|
|
96
|
+
clip
|
|
97
|
+
});
|
|
98
|
+
const bytes = Buffer.from(data, "base64");
|
|
99
|
+
if (options.limits?.maxBytes === void 0 || bytes.byteLength <= options.limits.maxBytes) return bytes;
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
await session.detach();
|
|
103
|
+
}
|
|
104
|
+
throw new RenderRefusedError(`The image is larger than ${options.limits?.maxBytes} bytes at every quality tried.`);
|
|
105
|
+
}
|
|
106
|
+
async function contentHeight(tab) {
|
|
107
|
+
const width = tab.viewportSize()?.width ?? FLUID_WIDTH;
|
|
108
|
+
await tab.setViewportSize({
|
|
109
|
+
width,
|
|
110
|
+
height: 1
|
|
111
|
+
});
|
|
112
|
+
const height = await tab.evaluate(() => Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0));
|
|
113
|
+
await tab.setViewportSize({
|
|
114
|
+
width,
|
|
115
|
+
height
|
|
116
|
+
});
|
|
117
|
+
return height;
|
|
118
|
+
}
|
|
119
|
+
async function openRenderer(options) {
|
|
120
|
+
let browser;
|
|
121
|
+
try {
|
|
122
|
+
browser = await chromium.launch({
|
|
123
|
+
executablePath: options.executablePath,
|
|
124
|
+
args: [
|
|
125
|
+
...options.args ?? [],
|
|
126
|
+
...NO_NETWORK_ARGS,
|
|
127
|
+
...options.jsHeapMb ? [`--js-flags=--max-old-space-size=${options.jsHeapMb}`] : []
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new RendererUnavailableError(`Chromium at ${options.executablePath} could not start: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
const limit = options.concurrency ?? 2;
|
|
134
|
+
let running = 0;
|
|
135
|
+
const waiting = [];
|
|
136
|
+
const acquire = () => {
|
|
137
|
+
if (running < limit) {
|
|
138
|
+
running++;
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
}
|
|
141
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
142
|
+
};
|
|
143
|
+
const release = () => {
|
|
144
|
+
const next = waiting.shift();
|
|
145
|
+
if (next) next();
|
|
146
|
+
else running--;
|
|
147
|
+
};
|
|
148
|
+
async function withTab(setup, body) {
|
|
149
|
+
await acquire();
|
|
150
|
+
let context;
|
|
151
|
+
try {
|
|
152
|
+
return await withinBound(setup.boundMs ?? 6e4, (async () => {
|
|
153
|
+
context = await browser.newContext({
|
|
154
|
+
viewport: setup.viewport,
|
|
155
|
+
deviceScaleFactor: setup.scale ?? 1,
|
|
156
|
+
javaScriptEnabled: setup.scripts,
|
|
157
|
+
offline: true
|
|
158
|
+
});
|
|
159
|
+
await context.route("**/*", (route) => route.request().url().startsWith("data:") ? route.continue() : route.abort("blockedbyclient"));
|
|
160
|
+
await context.routeWebSocket("**/*", (socket) => socket.close());
|
|
161
|
+
const tab = await context.newPage();
|
|
162
|
+
let ready;
|
|
163
|
+
const readyPromise = new Promise((resolve) => {
|
|
164
|
+
ready = resolve;
|
|
165
|
+
});
|
|
166
|
+
if (setup.waitForReady) await tab.exposeFunction("pageReady", () => ready?.());
|
|
167
|
+
await tab.setContent(setup.html, { waitUntil: "load" });
|
|
168
|
+
if (setup.waitForReady) await withinBound(PAGE_READY_BOUND_MS, readyPromise).catch(() => {
|
|
169
|
+
throw new RenderRefusedError(`The page did not call window.pageReady() within ${PAGE_READY_BOUND_MS / 1e3}s.`);
|
|
170
|
+
});
|
|
171
|
+
await tab.evaluate(() => document.fonts.ready.then(() => true)).catch(() => {});
|
|
172
|
+
await setup.beforeOutput?.(tab);
|
|
173
|
+
return body(tab);
|
|
174
|
+
})());
|
|
175
|
+
} finally {
|
|
176
|
+
await context?.close().catch(() => {});
|
|
177
|
+
release();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function pageDocument(page, options, printing = false) {
|
|
181
|
+
const css = `${page.print ? printing ? printCss(page.print) : printProperties(page.print) : ""}${fontFaces(options.fonts)}${options.css ?? ""}`;
|
|
182
|
+
return frameDocument(page.html, {
|
|
183
|
+
scripts: page.scripts,
|
|
184
|
+
nonce: newNonce(),
|
|
185
|
+
dir: page.dir,
|
|
186
|
+
lang: page.lang,
|
|
187
|
+
css,
|
|
188
|
+
relayKeys: false,
|
|
189
|
+
reportHeight: false
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function pageViewport(page, width, printing = false) {
|
|
193
|
+
if (page.print) return printViewport(page.print, printing);
|
|
194
|
+
if (page.size === "fluid") return {
|
|
195
|
+
width: width ?? FLUID_WIDTH,
|
|
196
|
+
height: 800
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
width: page.size.width,
|
|
200
|
+
height: page.size.height === "auto" ? 800 : page.size.height
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const image = captureImage;
|
|
204
|
+
async function renderPagePdf(page, opts) {
|
|
205
|
+
if (page.print) assertPrintOptions(page.print, opts.preferCssPageSize);
|
|
206
|
+
const viewport = pageViewport(page, void 0, true);
|
|
207
|
+
return await withTab({
|
|
208
|
+
viewport,
|
|
209
|
+
scripts: page.scripts === "sandboxed",
|
|
210
|
+
html: pageDocument(page, opts, true),
|
|
211
|
+
waitForReady: page.scripts === "sandboxed" && opts.waitForPageReady !== false,
|
|
212
|
+
boundMs: opts.boundMs,
|
|
213
|
+
beforeOutput: opts.beforeOutput
|
|
214
|
+
}, async (tab) => {
|
|
215
|
+
if (page.print) return printPage(tab, page.print, opts.preferCssPageSize);
|
|
216
|
+
const fixedHeight = typeof page.size === "object" && page.size.height !== "auto" ? page.size.height : null;
|
|
217
|
+
const height = fixedHeight ?? await contentHeight(tab);
|
|
218
|
+
return tab.pdf({
|
|
219
|
+
width: `${viewport.width}px`,
|
|
220
|
+
height: `${height}px`,
|
|
221
|
+
printBackground: true,
|
|
222
|
+
pageRanges: fixedHeight ? void 0 : "1"
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async function renderPiecePdf(page, geometry, css, opts) {
|
|
227
|
+
const html = frameDocument(page.html, {
|
|
228
|
+
scripts: page.scripts,
|
|
229
|
+
nonce: newNonce(),
|
|
230
|
+
dir: page.dir,
|
|
231
|
+
lang: page.lang,
|
|
232
|
+
css,
|
|
233
|
+
relayKeys: false,
|
|
234
|
+
reportHeight: false
|
|
235
|
+
});
|
|
236
|
+
return withTab({
|
|
237
|
+
viewport: geometry.viewport,
|
|
238
|
+
scripts: page.scripts === "sandboxed",
|
|
239
|
+
html,
|
|
240
|
+
waitForReady: page.scripts === "sandboxed" && opts.waitForPageReady !== false,
|
|
241
|
+
boundMs: opts.boundMs,
|
|
242
|
+
beforeOutput: opts.beforeOutput
|
|
243
|
+
}, (tab) => tab.pdf({
|
|
244
|
+
width: geometry.pdf.width,
|
|
245
|
+
height: geometry.pdf.height,
|
|
246
|
+
printBackground: true,
|
|
247
|
+
preferCSSPageSize: false,
|
|
248
|
+
pageRanges: "1",
|
|
249
|
+
margin: {
|
|
250
|
+
top: "0mm",
|
|
251
|
+
right: "0mm",
|
|
252
|
+
bottom: "0mm",
|
|
253
|
+
left: "0mm"
|
|
254
|
+
}
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
async pagePdf(input, opts = {}) {
|
|
259
|
+
return finishPdf(await renderPagePdf(Page.parse(input), opts), opts);
|
|
260
|
+
},
|
|
261
|
+
async pageImage(input, opts = {}) {
|
|
262
|
+
const page = Page.parse(input);
|
|
263
|
+
const viewport = pageViewport(page, opts.width);
|
|
264
|
+
const grows = page.size === "fluid" || typeof page.size === "object" && page.size.height === "auto";
|
|
265
|
+
return withTab({
|
|
266
|
+
viewport,
|
|
267
|
+
scripts: page.scripts === "sandboxed",
|
|
268
|
+
scale: opts.scale,
|
|
269
|
+
html: pageDocument(page, opts),
|
|
270
|
+
waitForReady: page.scripts === "sandboxed" && opts.waitForPageReady !== false,
|
|
271
|
+
boundMs: opts.boundMs,
|
|
272
|
+
beforeOutput: opts.beforeOutput
|
|
273
|
+
}, async (tab) => {
|
|
274
|
+
if (grows) await contentHeight(tab);
|
|
275
|
+
return image(tab, opts, false);
|
|
276
|
+
});
|
|
277
|
+
},
|
|
278
|
+
pagesPdf(inputs, opts = {}) {
|
|
279
|
+
return renderPagesPdf(inputs, opts, {
|
|
280
|
+
pagePdf: renderPagePdf,
|
|
281
|
+
piecePdf: renderPiecePdf,
|
|
282
|
+
fontFaces: (options) => fontFaces(options.fonts),
|
|
283
|
+
finish: finishPdf
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
deckPdf(deck, opts = {}) {
|
|
287
|
+
return renderDeckPdf(deck, opts, {
|
|
288
|
+
renderPdf: (setup, pdf) => withTab({
|
|
289
|
+
...setup,
|
|
290
|
+
waitForReady: false
|
|
291
|
+
}, (tab) => tab.pdf(pdf)),
|
|
292
|
+
fontFaces: (options) => fontFaces(options.fonts),
|
|
293
|
+
finish: finishPdf
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
async slideImage(deck, index, opts = {}) {
|
|
297
|
+
const slide = deck.slides[index];
|
|
298
|
+
if (!slide) throw new RangeError(`The deck has no slide ${index + 1}.`);
|
|
299
|
+
const single = {
|
|
300
|
+
...deck,
|
|
301
|
+
slides: [slide]
|
|
302
|
+
};
|
|
303
|
+
const css = `${fontFaces(opts.fonts)}${opts.css ?? ""}`;
|
|
304
|
+
const html = printableDeck(single, {
|
|
305
|
+
inline: opts.inline,
|
|
306
|
+
css
|
|
307
|
+
});
|
|
308
|
+
return withTab({
|
|
309
|
+
viewport: deck.size,
|
|
310
|
+
scripts: deck.scripts === "sandboxed",
|
|
311
|
+
scale: opts.scale,
|
|
312
|
+
html,
|
|
313
|
+
waitForReady: false,
|
|
314
|
+
boundMs: opts.boundMs,
|
|
315
|
+
beforeOutput: opts.beforeOutput
|
|
316
|
+
}, (tab) => image(tab, opts, false));
|
|
317
|
+
},
|
|
318
|
+
close: () => browser.close()
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
export { CONTAINER_ARGS, PAGE_READY_BOUND_MS, RENDER_BOUND_MS, RenderRefusedError, RenderTimeoutError, RendererUnavailableError, captureImage, fontFaces, openRenderer };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { RenderRefusedError } from "./errors.js";
|
|
2
|
+
import { pdfBox, pdfPoint, pt } from "./pdf-geometry.js";
|
|
3
|
+
import { PX_PER_MM, printBoxes, printPadding, printProperties, sheetMm } from "@segnavia/format";
|
|
4
|
+
import { PDFDocument, PDFName, rgb } from "pdf-lib";
|
|
5
|
+
//#region src/page-paper.ts
|
|
6
|
+
const usesPressLayout = (print) => print.bleed > 0 || print.marks;
|
|
7
|
+
function printCss(print) {
|
|
8
|
+
const properties = printProperties(print);
|
|
9
|
+
if (!usesPressLayout(print)) return properties;
|
|
10
|
+
const margin = print.margin ?? {
|
|
11
|
+
top: 0,
|
|
12
|
+
right: 0,
|
|
13
|
+
bottom: 0,
|
|
14
|
+
left: 0
|
|
15
|
+
};
|
|
16
|
+
return `${properties}${printPadding({
|
|
17
|
+
top: margin.top + print.bleed,
|
|
18
|
+
right: margin.right + print.bleed,
|
|
19
|
+
bottom: margin.bottom + print.bleed,
|
|
20
|
+
left: margin.left + print.bleed
|
|
21
|
+
})}`;
|
|
22
|
+
}
|
|
23
|
+
function printViewport(print, pressLayout) {
|
|
24
|
+
const size = pressLayout && usesPressLayout(print) ? printBoxes(print).bleedBox : sheetMm(print);
|
|
25
|
+
return {
|
|
26
|
+
width: Math.round(size.width * PX_PER_MM),
|
|
27
|
+
height: Math.round(size.height * PX_PER_MM)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function assertPrintOptions(print, preferCssPageSize) {
|
|
31
|
+
if (usesPressLayout(print) && preferCssPageSize) throw new RenderRefusedError("A page with bleed or marks takes its size from its print setting. Remove preferCssPageSize, or remove the bleed and marks, to clear this refusal.");
|
|
32
|
+
}
|
|
33
|
+
async function addPressBoxes(pdf, print) {
|
|
34
|
+
const boxes = printBoxes(print);
|
|
35
|
+
const document = await PDFDocument.load(pdf);
|
|
36
|
+
const source = document.getPage(0);
|
|
37
|
+
const content = await document.embedPage(source);
|
|
38
|
+
const page = document.addPage([pt(boxes.media.width), pt(boxes.media.height)]);
|
|
39
|
+
const bleedBox = pdfBox(boxes.bleedBox, boxes.media.height);
|
|
40
|
+
const trimBox = pdfBox(boxes.trim, boxes.media.height);
|
|
41
|
+
page.drawPage(content, {
|
|
42
|
+
x: bleedBox.x,
|
|
43
|
+
y: bleedBox.y,
|
|
44
|
+
width: source.getWidth(),
|
|
45
|
+
height: source.getHeight()
|
|
46
|
+
});
|
|
47
|
+
document.removePage(0);
|
|
48
|
+
page.node.delete(PDFName.of("CropBox"));
|
|
49
|
+
page.setBleedBox(bleedBox.x, bleedBox.y, bleedBox.width, bleedBox.height);
|
|
50
|
+
page.setTrimBox(trimBox.x, trimBox.y, trimBox.width, trimBox.height);
|
|
51
|
+
for (const mark of boxes.marks) page.drawLine({
|
|
52
|
+
start: pdfPoint(mark.x1, mark.y1, boxes.media.height),
|
|
53
|
+
end: pdfPoint(mark.x2, mark.y2, boxes.media.height),
|
|
54
|
+
thickness: .25,
|
|
55
|
+
color: rgb(0, 0, 0)
|
|
56
|
+
});
|
|
57
|
+
return document.save();
|
|
58
|
+
}
|
|
59
|
+
async function printPage(tab, print, preferCssPageSize) {
|
|
60
|
+
if (!usesPressLayout(print)) {
|
|
61
|
+
const mm = sheetMm(print);
|
|
62
|
+
const margin = print.margin;
|
|
63
|
+
return tab.pdf({
|
|
64
|
+
width: `${mm.width}mm`,
|
|
65
|
+
height: `${mm.height}mm`,
|
|
66
|
+
printBackground: true,
|
|
67
|
+
preferCSSPageSize: preferCssPageSize ?? false,
|
|
68
|
+
margin: margin ? {
|
|
69
|
+
top: `${margin.top}mm`,
|
|
70
|
+
right: `${margin.right}mm`,
|
|
71
|
+
bottom: `${margin.bottom}mm`,
|
|
72
|
+
left: `${margin.left}mm`
|
|
73
|
+
} : void 0
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const boxes = printBoxes(print);
|
|
77
|
+
const pdf = await tab.pdf({
|
|
78
|
+
width: `${boxes.bleedBox.width}mm`,
|
|
79
|
+
height: `${boxes.bleedBox.height}mm`,
|
|
80
|
+
printBackground: true,
|
|
81
|
+
preferCSSPageSize: false,
|
|
82
|
+
margin: {
|
|
83
|
+
top: "0mm",
|
|
84
|
+
right: "0mm",
|
|
85
|
+
bottom: "0mm",
|
|
86
|
+
left: "0mm"
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const papers = (await PDFDocument.load(pdf)).getPageCount();
|
|
90
|
+
if (papers > 1) throw new RenderRefusedError(`A page with bleed or marks is one printed piece, but the content needs ${papers} papers. Shorten the content, or remove the bleed and marks.`);
|
|
91
|
+
return addPressBoxes(pdf, print);
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { assertPrintOptions, printCss, printPage, printViewport };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { pdfBox } from "./pdf-geometry.js";
|
|
2
|
+
import { fitPage, pagePlacementLayout } from "@segnavia/format";
|
|
3
|
+
//#region src/pages-imposition.ts
|
|
4
|
+
const geometryKey = (page, pageValue, placement) => pageValue.size === "fluid" ? JSON.stringify([
|
|
5
|
+
page,
|
|
6
|
+
placement.bleedBox.width,
|
|
7
|
+
placement.bleedBox.height,
|
|
8
|
+
placement.bleed.top,
|
|
9
|
+
placement.bleed.right,
|
|
10
|
+
placement.bleed.bottom,
|
|
11
|
+
placement.bleed.left
|
|
12
|
+
]) : String(page);
|
|
13
|
+
function groupPieces(pages, imposed, mode) {
|
|
14
|
+
const groups = /* @__PURE__ */ new Map();
|
|
15
|
+
for (const placement of imposed.placements) {
|
|
16
|
+
const page = pages[placement.page];
|
|
17
|
+
if (!page) throw new RangeError(`The placement names missing page ${placement.page}.`);
|
|
18
|
+
const fit = fitPage(page, placement, mode);
|
|
19
|
+
const layout = pagePlacementLayout(page.size, placement, fit);
|
|
20
|
+
const key = geometryKey(placement.page, page, placement);
|
|
21
|
+
const group = groups.get(key) ?? {
|
|
22
|
+
key,
|
|
23
|
+
page: placement.page,
|
|
24
|
+
placements: []
|
|
25
|
+
};
|
|
26
|
+
group.placements.push({
|
|
27
|
+
placement,
|
|
28
|
+
layout
|
|
29
|
+
});
|
|
30
|
+
groups.set(key, group);
|
|
31
|
+
}
|
|
32
|
+
return [...groups.values()];
|
|
33
|
+
}
|
|
34
|
+
function pieceGeometry(item) {
|
|
35
|
+
const { placement, layout } = item;
|
|
36
|
+
if (layout.kind === "fluid") return {
|
|
37
|
+
viewport: {
|
|
38
|
+
width: Math.round(layout.piece.width),
|
|
39
|
+
height: Math.round(layout.piece.height)
|
|
40
|
+
},
|
|
41
|
+
pdf: {
|
|
42
|
+
width: `${layout.paper.width}mm`,
|
|
43
|
+
height: `${layout.paper.height}mm`
|
|
44
|
+
},
|
|
45
|
+
padding: layout.inset ? placement.bleed : void 0
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
viewport: {
|
|
49
|
+
width: layout.piece.width,
|
|
50
|
+
height: Math.round(layout.piece.height)
|
|
51
|
+
},
|
|
52
|
+
pdf: {
|
|
53
|
+
width: `${layout.piece.width}px`,
|
|
54
|
+
height: `${layout.piece.height}px`
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function placementPdfGeometry(placement, layout, paperHeight) {
|
|
59
|
+
return {
|
|
60
|
+
draw: pdfBox(layout.paper, paperHeight),
|
|
61
|
+
clip: pdfBox(placement.bleedBox, paperHeight)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { groupPieces, pieceGeometry, placementPdfGeometry };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { RenderRefusedError } from "./errors.js";
|
|
2
|
+
import { groupPieces, pieceGeometry } from "./pages-imposition.js";
|
|
3
|
+
import { composeImposedPdf } from "./pdf-compose.js";
|
|
4
|
+
import { Imposition, Page, impose, printPadding, printProperties } from "@segnavia/format";
|
|
5
|
+
import { PDFDocument } from "pdf-lib";
|
|
6
|
+
//#region src/pages-pdf.ts
|
|
7
|
+
async function joinPdfs(pdfs) {
|
|
8
|
+
const joined = await PDFDocument.create();
|
|
9
|
+
for (const bytes of pdfs) {
|
|
10
|
+
const source = await PDFDocument.load(bytes);
|
|
11
|
+
const pages = await joined.copyPages(source, source.getPageIndices());
|
|
12
|
+
for (const page of pages) joined.addPage(page);
|
|
13
|
+
}
|
|
14
|
+
return joined.save();
|
|
15
|
+
}
|
|
16
|
+
function namePageError(error, page) {
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
error.message = `Page at index ${page} could not be rendered: ${error.message}`;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Page at index ${page} could not be rendered: ${String(error)}`);
|
|
22
|
+
}
|
|
23
|
+
async function composedPdf(pages, imposition, opts, services) {
|
|
24
|
+
const imposed = impose(imposition, pages.length);
|
|
25
|
+
const groups = groupPieces(pages, imposed, imposition.fit);
|
|
26
|
+
const properties = printProperties(imposition, imposition.unit);
|
|
27
|
+
const hostCss = `${services.fontFaces(opts)}${opts.css ?? ""}`;
|
|
28
|
+
const rendered = await Promise.all(groups.map(async (group) => {
|
|
29
|
+
const page = pages[group.page];
|
|
30
|
+
const first = group.placements[0];
|
|
31
|
+
if (!page || !first) throw new Error(`The piece for page ${group.page} has no placement.`);
|
|
32
|
+
const geometry = pieceGeometry(first);
|
|
33
|
+
const css = `${properties}${geometry.padding ? printPadding(geometry.padding) : ""}${hostCss}`;
|
|
34
|
+
try {
|
|
35
|
+
return {
|
|
36
|
+
group,
|
|
37
|
+
bytes: await services.piecePdf(page, geometry, css, opts)
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
namePageError(error, group.page);
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
return composeImposedPdf(imposed, rendered.map(({ group, bytes }) => ({
|
|
44
|
+
key: group.key,
|
|
45
|
+
name: `The rendered piece for page ${group.page}`,
|
|
46
|
+
bytes,
|
|
47
|
+
placements: group.placements
|
|
48
|
+
})));
|
|
49
|
+
}
|
|
50
|
+
async function renderPagesPdf(inputs, opts, services) {
|
|
51
|
+
if (inputs.length === 0) throw new RenderRefusedError("There is nothing to render. Pass at least one page to clear this refusal.");
|
|
52
|
+
const pages = inputs.map((input) => Page.parse(input));
|
|
53
|
+
if (!opts.imposition) {
|
|
54
|
+
const rendered = await Promise.all(pages.map((page) => services.pagePdf(page, {
|
|
55
|
+
...opts,
|
|
56
|
+
limits: void 0,
|
|
57
|
+
press: void 0
|
|
58
|
+
})));
|
|
59
|
+
return services.finish(await joinPdfs(rendered), opts);
|
|
60
|
+
}
|
|
61
|
+
const imposition = Imposition.parse(opts.imposition);
|
|
62
|
+
if (opts.preferCssPageSize) throw new RenderRefusedError("Pages in an imposition take their size from its paper. Remove preferCssPageSize, or remove the imposition, to clear this refusal.");
|
|
63
|
+
return services.finish(await composedPdf(pages, imposition, opts, services), opts);
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
export { renderPagesPdf };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { placementPdfGeometry } from "./pages-imposition.js";
|
|
2
|
+
import { MM_PER_PT } from "@segnavia/format";
|
|
3
|
+
import { PDFDocument, clip, endPath, popGraphicsState, pushGraphicsState, rectangle } from "pdf-lib";
|
|
4
|
+
//#region src/pdf-compose.ts
|
|
5
|
+
async function embedOnePage(output, bytes, name) {
|
|
6
|
+
const [embedded] = await output.embedPdf(bytes, [0]);
|
|
7
|
+
if (!embedded) throw new Error(`${name} has no PDF page.`);
|
|
8
|
+
return embedded;
|
|
9
|
+
}
|
|
10
|
+
function drawClippedPage(target, embedded, draw, clipBox) {
|
|
11
|
+
target.pushOperators(pushGraphicsState(), rectangle(clipBox.x, clipBox.y, clipBox.width, clipBox.height), clip(), endPath());
|
|
12
|
+
target.drawPage(embedded, draw);
|
|
13
|
+
target.pushOperators(popGraphicsState());
|
|
14
|
+
}
|
|
15
|
+
function drawImposedSide(output, imposed, embedded, pieces, paperIndex, side) {
|
|
16
|
+
const paper = output.addPage([imposed.paper.width / MM_PER_PT, imposed.paper.height / MM_PER_PT]);
|
|
17
|
+
for (const piece of pieces) {
|
|
18
|
+
const image = embedded.get(piece.key);
|
|
19
|
+
if (!image) throw new Error(`${piece.name} was not embedded.`);
|
|
20
|
+
for (const { placement, layout } of piece.placements) {
|
|
21
|
+
if (placement.paper !== paperIndex || placement.side !== side) continue;
|
|
22
|
+
const { draw, clip: clipBox } = placementPdfGeometry(placement, layout, imposed.paper.height);
|
|
23
|
+
drawClippedPage(paper, image, draw, clipBox);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function composeImposedPdf(imposed, pieces) {
|
|
28
|
+
const output = await PDFDocument.create();
|
|
29
|
+
const embedded = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const piece of pieces) embedded.set(piece.key, await embedOnePage(output, piece.bytes, piece.name));
|
|
31
|
+
for (let paperIndex = 0; paperIndex < imposed.papers; paperIndex += 1) for (const side of ["front", "back"].slice(0, imposed.sides)) drawImposedSide(output, imposed, embedded, pieces, paperIndex, side);
|
|
32
|
+
return output.save();
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { composeImposedPdf, drawClippedPage, embedOnePage };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MM_PER_PT, PX_PER_MM } from "@segnavia/format";
|
|
2
|
+
//#region src/pdf-geometry.ts
|
|
3
|
+
const pt = (millimeters) => millimeters / MM_PER_PT;
|
|
4
|
+
const pixelPt = (pixels) => pt(pixels / PX_PER_MM);
|
|
5
|
+
function pdfPoint(x, y, mediaHeight) {
|
|
6
|
+
return {
|
|
7
|
+
x: pt(x),
|
|
8
|
+
y: pt(mediaHeight - y)
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function pdfBox(box, mediaHeight) {
|
|
12
|
+
return {
|
|
13
|
+
...pdfPoint(box.x, box.y + box.height, mediaHeight),
|
|
14
|
+
width: pt(box.width),
|
|
15
|
+
height: pt(box.height)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function pdfPixelBox(box, mediaHeight) {
|
|
19
|
+
return {
|
|
20
|
+
x: pixelPt(box.x),
|
|
21
|
+
y: pixelPt(mediaHeight - box.y - box.height),
|
|
22
|
+
width: pixelPt(box.width),
|
|
23
|
+
height: pixelPt(box.height)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { pdfBox, pdfPixelBox, pdfPoint, pixelPt, pt };
|