@segnavia/render 0.1.0 → 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/dist/index.js CHANGED
@@ -1,4 +1,9 @@
1
- import { PX_PER_MM, Page, sheetMm } from "@segnavia/format";
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";
2
7
  import { frameDocument, newNonce } from "@segnavia/frame/bridge";
3
8
  import { printableDeck } from "@segnavia/frame/print-document";
4
9
  import { PDFDocument } from "pdf-lib";
@@ -18,15 +23,9 @@ const NO_NETWORK_ARGS = [
18
23
  "--proxy-bypass-list=<-loopback>",
19
24
  "--force-webrtc-ip-handling-policy=disable_non_proxied_udp"
20
25
  ];
21
- var RendererUnavailableError = class extends Error {
22
- name = "RendererUnavailableError";
23
- };
24
26
  var RenderTimeoutError = class extends Error {
25
27
  name = "RenderTimeoutError";
26
28
  };
27
- var RenderRefusedError = class extends Error {
28
- name = "RenderRefusedError";
29
- };
30
29
  function fontFaces(fonts = []) {
31
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("");
32
31
  }
@@ -52,6 +51,13 @@ async function enforce(pdf, limits) {
52
51
  }
53
52
  return pdf;
54
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
+ }
55
61
  async function captureImage(tab, options, fullPage) {
56
62
  const format = options.format ?? "png";
57
63
  if (format !== "webp") return tab.screenshot({
@@ -171,8 +177,8 @@ async function openRenderer(options) {
171
177
  release();
172
178
  }
173
179
  }
174
- function pageDocument(page, options) {
175
- const css = `${fontFaces(options.fonts)}${options.css ?? ""}`;
180
+ function pageDocument(page, options, printing = false) {
181
+ const css = `${page.print ? printing ? printCss(page.print) : printProperties(page.print) : ""}${fontFaces(options.fonts)}${options.css ?? ""}`;
176
182
  return frameDocument(page.html, {
177
183
  scripts: page.scripts,
178
184
  nonce: newNonce(),
@@ -183,14 +189,8 @@ async function openRenderer(options) {
183
189
  reportHeight: false
184
190
  });
185
191
  }
186
- function pageViewport(page, width) {
187
- if (page.print) {
188
- const mm = sheetMm(page.print);
189
- return {
190
- width: Math.round(mm.width * PX_PER_MM),
191
- height: Math.round(mm.height * PX_PER_MM)
192
- };
193
- }
192
+ function pageViewport(page, width, printing = false) {
193
+ if (page.print) return printViewport(page.print, printing);
194
194
  if (page.size === "fluid") return {
195
195
  width: width ?? FLUID_WIDTH,
196
196
  height: 800
@@ -201,43 +201,62 @@ async function openRenderer(options) {
201
201
  };
202
202
  }
203
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
+ }
204
257
  return {
205
258
  async pagePdf(input, opts = {}) {
206
- const page = Page.parse(input);
207
- const viewport = pageViewport(page);
208
- return enforce(await withTab({
209
- viewport,
210
- scripts: page.scripts === "sandboxed",
211
- html: pageDocument(page, opts),
212
- waitForReady: page.scripts === "sandboxed" && opts.waitForPageReady !== false,
213
- boundMs: opts.boundMs,
214
- beforeOutput: opts.beforeOutput
215
- }, async (tab) => {
216
- if (page.print) {
217
- const mm = sheetMm(page.print);
218
- const margin = page.print.margin;
219
- return tab.pdf({
220
- width: `${mm.width}mm`,
221
- height: `${mm.height}mm`,
222
- printBackground: true,
223
- preferCSSPageSize: opts.preferCssPageSize ?? false,
224
- margin: margin ? {
225
- top: `${margin.top}mm`,
226
- right: `${margin.right}mm`,
227
- bottom: `${margin.bottom}mm`,
228
- left: `${margin.left}mm`
229
- } : void 0
230
- });
231
- }
232
- const fixedHeight = typeof page.size === "object" && page.size.height !== "auto" ? page.size.height : null;
233
- const height = fixedHeight ?? await contentHeight(tab);
234
- return tab.pdf({
235
- width: `${viewport.width}px`,
236
- height: `${height}px`,
237
- printBackground: true,
238
- pageRanges: fixedHeight ? void 0 : "1"
239
- });
240
- }), opts.limits);
259
+ return finishPdf(await renderPagePdf(Page.parse(input), opts), opts);
241
260
  },
242
261
  async pageImage(input, opts = {}) {
243
262
  const page = Page.parse(input);
@@ -256,31 +275,23 @@ async function openRenderer(options) {
256
275
  return image(tab, opts, false);
257
276
  });
258
277
  },
259
- async deckPdf(deck, opts = {}) {
260
- const css = `${fontFaces(opts.fonts)}${opts.css ?? ""}`;
261
- const html = printableDeck(deck, {
262
- ...opts,
263
- css
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
264
294
  });
265
- return enforce(await withTab({
266
- viewport: opts.layout === "notes" ? pageViewport({
267
- html: "",
268
- size: "fluid",
269
- scripts: "refuse",
270
- print: opts.notesPaper ?? {
271
- paper: "letter",
272
- orientation: "portrait"
273
- }
274
- }) : deck.size,
275
- scripts: deck.scripts === "sandboxed",
276
- html,
277
- waitForReady: false,
278
- boundMs: opts.boundMs,
279
- beforeOutput: opts.beforeOutput
280
- }, (tab) => tab.pdf({
281
- preferCSSPageSize: true,
282
- printBackground: true
283
- })), opts.limits);
284
295
  },
285
296
  async slideImage(deck, index, opts = {}) {
286
297
  const slide = deck.slides[index];
@@ -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 };
@@ -0,0 +1,145 @@
1
+ import { RenderRefusedError } from "../errors.js";
2
+ import { readProfile } from "../color/profile.js";
3
+ import { cachedPressColor } from "./color-policy.js";
4
+ import { groupAction, shadingAction } from "./policy.js";
5
+ import { rewriteContent } from "./content.js";
6
+ import { arrayNumbers, decodeContent, nameOf, numberOf, replaceArray, resolvedColorSpace } from "./pdf-objects.js";
7
+ import { walkPressGraph } from "./visitor.js";
8
+ import { inspectPressPdf } from "./report.js";
9
+ import { PDFDict, PDFDocument, PDFName, PDFRef, PDFString } from "pdf-lib";
10
+ import { deflateSync } from "node:zlib";
11
+ //#region src/press/adapter.ts
12
+ const key = (name) => PDFName.of(name);
13
+ const refuse = (found, supported) => {
14
+ throw new RenderRefusedError(`The PDF has unsupported ${found}; supported: ${supported}.`);
15
+ };
16
+ const resolved = (state, object) => object instanceof PDFRef ? state.document.context.lookup(object) : object;
17
+ function convertFunction(state, fn, policy) {
18
+ if (policy.action === "refuse") refuse(policy.found, policy.supported);
19
+ if (!fn) return;
20
+ if (numberOf(fn, "FunctionType") === 2) {
21
+ for (const name of ["C0", "C1"]) replaceArray(fn, name, state.convert(arrayNumbers(fn, name, 3)));
22
+ if (fn.has(key("Range"))) replaceArray(fn, "Range", [
23
+ 0,
24
+ 1,
25
+ 0,
26
+ 1,
27
+ 0,
28
+ 1,
29
+ 0,
30
+ 1
31
+ ]);
32
+ state.totals.gradientStops += 2;
33
+ return;
34
+ }
35
+ if (fn.has(key("Range"))) replaceArray(fn, "Range", [
36
+ 0,
37
+ 1,
38
+ 0,
39
+ 1,
40
+ 0,
41
+ 1,
42
+ 0,
43
+ 1
44
+ ]);
45
+ }
46
+ function convertShading(state, shading) {
47
+ if (state.seenShadings.has(shading)) return false;
48
+ state.seenShadings.add(shading);
49
+ const type = numberOf(shading, "ShadingType");
50
+ const space = resolvedColorSpace(state.document, resolved(state, shading.get(key("ColorSpace"))));
51
+ const policy = shadingAction(space.kind, type);
52
+ if (policy.action === "refuse") refuse(`ShadingType ${type ?? "missing"} color space ${space.label}`, policy.supported);
53
+ if (policy.action === "keep") return false;
54
+ if (shading.has(key("Background"))) replaceArray(shading, "Background", state.convert(arrayNumbers(shading, "Background", 3)));
55
+ shading.set(key("ColorSpace"), key("DeviceCMYK"));
56
+ return true;
57
+ }
58
+ function convertGroup(state, owner, maskSource) {
59
+ const group = owner.lookupMaybe(key("Group"), PDFDict);
60
+ if (!group || nameOf(group.get(key("S"))) !== "Transparency" || state.seenGroups.has(group)) return;
61
+ state.seenGroups.add(group);
62
+ const space = resolvedColorSpace(state.document, resolved(state, group.get(key("CS"))));
63
+ const policy = groupAction(space.kind, space.label, maskSource);
64
+ if (policy.action === "refuse") refuse(`transparency group color space ${space.label}`, policy.supported);
65
+ if (policy.action === "convert") {
66
+ group.set(key("CS"), key("DeviceCMYK"));
67
+ state.totals.groups++;
68
+ }
69
+ }
70
+ function streamDictionary(stream) {
71
+ const dictionary = { Filter: key("FlateDecode") };
72
+ for (const [name, value] of stream.dict.entries()) {
73
+ const label = name.toString().slice(1);
74
+ if (![
75
+ "Filter",
76
+ "DecodeParms",
77
+ "Length"
78
+ ].includes(label)) dictionary[label] = value;
79
+ }
80
+ return dictionary;
81
+ }
82
+ function convertContent(state, object, stream, setDirect) {
83
+ const rewritten = rewriteContent(decodeContent(stream, state.budget), state.convert);
84
+ state.totals.fills += rewritten.counts.fills;
85
+ state.totals.strokes += rewritten.counts.strokes;
86
+ const replacement = state.document.context.stream(deflateSync(rewritten.bytes), streamDictionary(stream));
87
+ const reference = object instanceof PDFRef ? object : state.document.context.getObjectRef(object);
88
+ if (reference) state.document.context.assign(reference, replacement);
89
+ else setDirect(replacement);
90
+ }
91
+ function newState(document, profile) {
92
+ return {
93
+ budget: { decodedBytes: 0 },
94
+ convert: cachedPressColor(profile),
95
+ document,
96
+ seenGroups: /* @__PURE__ */ new WeakSet(),
97
+ seenShadings: /* @__PURE__ */ new WeakSet(),
98
+ totals: {
99
+ fills: 0,
100
+ strokes: 0,
101
+ gradientStops: 0,
102
+ groups: 0
103
+ }
104
+ };
105
+ }
106
+ function rewritePressDocument(document, profile) {
107
+ const state = newState(document, profile);
108
+ walkPressGraph(document, {
109
+ content: (object, stream, replace) => convertContent(state, object, stream, replace),
110
+ function: (fn, decision) => convertFunction(state, fn, decision),
111
+ group: (owner, _stream, maskSource) => convertGroup(state, owner, maskSource),
112
+ image: () => void 0,
113
+ shading: (shading) => convertShading(state, shading)
114
+ });
115
+ return state.totals;
116
+ }
117
+ function addOutputIntent(document, profile) {
118
+ const profileRef = document.context.register(document.context.stream(profile, { N: 4 }));
119
+ const intentRef = document.context.register(document.context.obj({
120
+ Type: "OutputIntent",
121
+ S: "GTS_PDFX",
122
+ OutputConditionIdentifier: PDFString.of("Custom"),
123
+ DestOutputProfile: profileRef
124
+ }));
125
+ document.catalog.set(key("OutputIntents"), document.context.obj([intentRef]));
126
+ }
127
+ async function rewritePressPdf(bytes, profileBytes) {
128
+ try {
129
+ const profile = readProfile(profileBytes);
130
+ if (profile.colorSpace !== "CMYK") refuse(`press profile color space ${profile.colorSpace}`, "CMYK");
131
+ const document = await PDFDocument.load(bytes, { updateMetadata: false });
132
+ rewritePressDocument(document, profile);
133
+ addOutputIntent(document, profileBytes);
134
+ const output = await document.save({ useObjectStreams: false });
135
+ return {
136
+ bytes: output,
137
+ report: await inspectPressPdf(output)
138
+ };
139
+ } catch (error) {
140
+ if (error instanceof RenderRefusedError) throw error;
141
+ throw new RenderRefusedError("The PDF has unsupported malformed structure; supported: a valid PDF produced by the renderer.");
142
+ }
143
+ }
144
+ //#endregion
145
+ export { rewritePressDocument, rewritePressPdf };
@@ -0,0 +1,31 @@
1
+ import { toPress } from "../color/engine.js";
2
+ //#region src/press/color-policy.ts
3
+ const NEUTRAL_TOLERANCE = .001;
4
+ function pressColor(profile, rgb) {
5
+ const [red, green, blue] = rgb;
6
+ if (Math.max(red, green, blue) - Math.min(red, green, blue) <= NEUTRAL_TOLERANCE) return [
7
+ 0,
8
+ 0,
9
+ 0,
10
+ 1 - red
11
+ ];
12
+ return toPress(profile, rgb);
13
+ }
14
+ function formatPdfNumber(value) {
15
+ const rounded = Math.round(value * 1e6) / 1e6;
16
+ if (Object.is(rounded, -0)) return "0";
17
+ return rounded.toFixed(6).replace(/(?:\.0+|(?<=\.[0-9]*?)0+)$/, "");
18
+ }
19
+ function cachedPressColor(profile) {
20
+ const cache = /* @__PURE__ */ new Map();
21
+ return (rgb) => {
22
+ const key = rgb.join(",");
23
+ const cached = cache.get(key);
24
+ if (cached) return cached;
25
+ const converted = pressColor(profile, rgb);
26
+ cache.set(key, converted);
27
+ return converted;
28
+ };
29
+ }
30
+ //#endregion
31
+ export { cachedPressColor, formatPdfNumber, pressColor };