@segnavia/render 0.12.0 → 0.13.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/book-pdf.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { marginDocument } from "./book-margins.js";
2
2
  import { RenderRefusedError } from "./errors.js";
3
3
  import { namedDestinations, rewriteBookLinks } from "./book-destinations.js";
4
+ import { loadPdf } from "./pdf-load.js";
4
5
  import { Book, formatNumber, resolveDirection, sheetMm } from "@segnavia/format";
5
- import { PDFDocument } from "pdf-lib";
6
6
  import { bookDocument, bookPrintPart } from "@segnavia/html";
7
+ import { PDFDocument } from "pdf-lib";
7
8
  //#region src/book-pdf.ts
8
9
  async function inspect(output) {
9
- const document = await PDFDocument.load(output.bytes);
10
+ const document = await loadPdf(output.bytes);
10
11
  return {
11
12
  bytes: output.bytes,
12
13
  overflow: output.overflow,
@@ -51,7 +52,7 @@ function verifyStarts(label, sections, expected, actual) {
51
52
  }
52
53
  async function appendBlank(part) {
53
54
  if (!part.bytes) return part;
54
- const document = await PDFDocument.load(part.bytes);
55
+ const document = await loadPdf(part.bytes);
55
56
  const last = document.getPage(document.getPageCount() - 1);
56
57
  document.addPage([last.getWidth(), last.getHeight()]);
57
58
  return {
@@ -193,10 +194,10 @@ async function join(front, body, marginBytes) {
193
194
  const output = await PDFDocument.create();
194
195
  for (const part of [front, body]) {
195
196
  if (!part.bytes) continue;
196
- const source = await PDFDocument.load(part.bytes);
197
+ const source = await loadPdf(part.bytes);
197
198
  for (const page of await output.copyPages(source, source.getPageIndices())) output.addPage(page);
198
199
  }
199
- const layer = await PDFDocument.load(marginBytes);
200
+ const layer = await loadPdf(marginBytes);
200
201
  if (layer.getPageCount() !== output.getPageCount()) throw new RenderRefusedError(`The margin layer has ${layer.getPageCount()} pages, but the book has ${output.getPageCount()}.`);
201
202
  for (const [index, page] of layer.getPages().entries()) {
202
203
  const [embedded] = await output.embedPdf(layer, [index]);
package/dist/deck-pdf.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { RenderRefusedError } from "./errors.js";
2
+ import { loadPdf } from "./pdf-load.js";
2
3
  import { containFixedPageOverflow } from "./overflow-containment.js";
3
4
  import { pdfPixelBox } from "./pdf-geometry.js";
4
5
  import { composeImposedPdf, drawClippedPage, embedOnePage } from "./pdf-compose.js";
5
6
  import { Imposition, fitPage, impose, pagePlacementLayout } from "@segnavia/format";
6
7
  import { newNonce } from "@segnavia/frame/bridge";
7
8
  import { htmlSlideDocument, notesLayoutGeometry, paginateNotes, printableDeck } from "@segnavia/frame/print-document";
8
- import { PDFDocument } from "pdf-lib";
9
9
  import { fitImage, imageLayerStyles } from "@segnavia/frame/image-fit";
10
10
  //#region src/deck-pdf.ts
11
11
  function nameSlideError(error, id) {
@@ -118,7 +118,7 @@ async function regularDeckPdf(deck, opts, services) {
118
118
  nameSlideError(error, id);
119
119
  }
120
120
  }))]);
121
- const output = await PDFDocument.load(printable);
121
+ const output = await loadPdf(printable);
122
122
  const pages = output.getPages();
123
123
  const slideBox = notesGeometry?.slide ?? {
124
124
  x: 0,
@@ -1,4 +1,5 @@
1
1
  import { RenderRefusedError } from "../errors.js";
2
+ import { loadPdf } from "../pdf-load.js";
2
3
  import { IDENTITY_MATRIX, multiplyAffine } from "../press/affine.js";
3
4
  import { visitContentOperations } from "../press/content.js";
4
5
  import { decodeContent, dictionaryOf, nameOf, resolvedColorSpace } from "../press/pdf-objects.js";
@@ -8,7 +9,7 @@ import { softMaskAction } from "./policy.js";
8
9
  import { rule1EffectImage } from "./rule-1.js";
9
10
  import { EMPTY_EFFECT_GRAPHICS_STATE, advanceEffectGraphicsState, effectFormStructure, effectGraphicsStateOpacity, effectGraphicsStateStructure, effectOpacity } from "./structure.js";
10
11
  import { buildRuleCImage, ruleCPlan } from "./rule-c.js";
11
- import { PDFArray, PDFDict, PDFDocument, PDFName, PDFNumber, PDFRawStream, PDFRef } from "pdf-lib";
12
+ import { PDFArray, PDFDict, PDFName, PDFNumber, PDFRawStream, PDFRef } from "pdf-lib";
12
13
  import { deflateSync } from "node:zlib";
13
14
  //#region src/effects/adapter.ts
14
15
  const key = (name) => PDFName.of(name);
@@ -321,7 +322,7 @@ function rewriteEffectsDocument(document) {
321
322
  }
322
323
  async function rewriteEffectsPdf(bytes) {
323
324
  try {
324
- const document = await PDFDocument.load(bytes, { updateMetadata: false });
325
+ const document = await loadPdf(bytes, { updateMetadata: false });
325
326
  const report = rewriteEffectsDocument(document);
326
327
  return {
327
328
  bytes: report.rule1 === 0 && report.ruleC === 0 ? bytes : await document.save(),
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { RenderRefusedError, RendererUnavailableError } from "./errors.js";
2
+ import { loadPdf } from "./pdf-load.js";
2
3
  import { renderBrowserBookPdf } from "./book-render.js";
3
4
  import { browserJpegDecoder } from "./browser-jpeg.js";
4
5
  import { containFixedPageOverflow } from "./overflow-containment.js";
@@ -12,7 +13,6 @@ import { rewritePressPdf } from "./press/adapter.js";
12
13
  import { Page, printProperties } from "@segnavia/format";
13
14
  import { frameDocument, newNonce } from "@segnavia/frame/bridge";
14
15
  import { printableDeck } from "@segnavia/frame/print-document";
15
- import { PDFDocument } from "pdf-lib";
16
16
  import { chromium } from "playwright-core";
17
17
  //#region src/index.ts
18
18
  const RENDER_BOUND_MS = 6e4;
@@ -47,7 +47,7 @@ async function withinBound(ms, work) {
47
47
  }
48
48
  }
49
49
  async function sheets(pdf) {
50
- return (await PDFDocument.load(pdf)).getPageCount();
50
+ return (await loadPdf(pdf)).getPageCount();
51
51
  }
52
52
  async function enforce(pdf, limits) {
53
53
  if (limits?.maxBytes !== void 0 && pdf.byteLength > limits.maxBytes) throw new RenderRefusedError(`The PDF is ${pdf.byteLength} bytes; the limit is ${limits.maxBytes}.`);
@@ -1,8 +1,9 @@
1
1
  import { RenderRefusedError } from "./errors.js";
2
+ import { loadPdf } from "./pdf-load.js";
2
3
  import { pdfBox, pdfPoint, pt } from "./pdf-geometry.js";
3
4
  import { printWithOverflow } from "./overflow.js";
4
5
  import { PX_PER_MM, printBoxes, printPadding, printProperties, sheetMm } from "@segnavia/format";
5
- import { PDFDocument, PDFName, rgb } from "pdf-lib";
6
+ import { PDFName, rgb } from "pdf-lib";
6
7
  //#region src/page-paper.ts
7
8
  const usesPressLayout = (print) => print.bleed > 0 || print.marks;
8
9
  function printCss(print) {
@@ -33,18 +34,16 @@ function assertPrintOptions(print, preferCssPageSize) {
33
34
  }
34
35
  async function addPressBoxes(pdf, print) {
35
36
  const boxes = printBoxes(print);
36
- const document = await PDFDocument.load(pdf);
37
+ const document = await loadPdf(pdf);
37
38
  const source = document.getPage(0);
38
39
  const content = await document.embedPage(source);
39
40
  const page = document.addPage([pt(boxes.media.width), pt(boxes.media.height)]);
40
41
  const bleedBox = pdfBox(boxes.bleedBox, boxes.media.height);
41
42
  const trimBox = pdfBox(boxes.trim, boxes.media.height);
42
- page.drawPage(content, {
43
- x: bleedBox.x,
44
- y: bleedBox.y,
43
+ page.drawPage(content, pressContentPlacement({
45
44
  width: source.getWidth(),
46
45
  height: source.getHeight()
47
- });
46
+ }, bleedBox));
48
47
  document.removePage(0);
49
48
  page.node.delete(PDFName.of("CropBox"));
50
49
  page.setBleedBox(bleedBox.x, bleedBox.y, bleedBox.width, bleedBox.height);
@@ -57,6 +56,14 @@ async function addPressBoxes(pdf, print) {
57
56
  });
58
57
  return document.save();
59
58
  }
59
+ function pressContentPlacement(source, bleedBox) {
60
+ return {
61
+ x: bleedBox.x,
62
+ y: bleedBox.y + bleedBox.height - source.height,
63
+ width: source.width,
64
+ height: source.height
65
+ };
66
+ }
60
67
  async function printPage(tab, print, preferCssPageSize, inputIndex) {
61
68
  if (!usesPressLayout(print)) {
62
69
  const mm = sheetMm(print);
@@ -88,7 +95,7 @@ async function printPage(tab, print, preferCssPageSize, inputIndex) {
88
95
  left: "0mm"
89
96
  }
90
97
  }, inputIndex);
91
- const papers = (await PDFDocument.load(printed.bytes)).getPageCount();
98
+ const papers = (await loadPdf(printed.bytes)).getPageCount();
92
99
  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.`);
93
100
  return {
94
101
  bytes: await addPressBoxes(printed.bytes, print),
@@ -96,4 +103,4 @@ async function printPage(tab, print, preferCssPageSize, inputIndex) {
96
103
  };
97
104
  }
98
105
  //#endregion
99
- export { assertPrintOptions, printCss, printPage, printViewport };
106
+ export { assertPrintOptions, pressContentPlacement, printCss, printPage, printViewport };
package/dist/pages-pdf.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { RenderRefusedError } from "./errors.js";
2
+ import { loadPdf } from "./pdf-load.js";
2
3
  import { groupPieces, pieceGeometry } from "./pages-imposition.js";
3
4
  import { composeImposedPdf } from "./pdf-compose.js";
4
5
  import { Imposition, Page, impose, printPadding, printProperties } from "@segnavia/format";
@@ -7,7 +8,7 @@ import { PDFDocument } from "pdf-lib";
7
8
  async function joinPdfs(pdfs) {
8
9
  const joined = await PDFDocument.create();
9
10
  for (const bytes of pdfs) {
10
- const source = await PDFDocument.load(bytes);
11
+ const source = await loadPdf(bytes);
11
12
  const pages = await joined.copyPages(source, source.getPageIndices());
12
13
  for (const page of pages) joined.addPage(page);
13
14
  }
@@ -1,9 +1,11 @@
1
+ import { loadPdf } from "./pdf-load.js";
1
2
  import { placementPdfGeometry } from "./pages-imposition.js";
2
3
  import { MM_PER_PT } from "@segnavia/format";
3
4
  import { PDFDocument, clip, endPath, popGraphicsState, pushGraphicsState, rectangle } from "pdf-lib";
4
5
  //#region src/pdf-compose.ts
5
6
  async function embedOnePage(output, bytes, name) {
6
- const [embedded] = await output.embedPdf(bytes, [0]);
7
+ const source = await loadPdf(bytes);
8
+ const [embedded] = await output.embedPdf(source, [0]);
7
9
  if (!embedded) throw new Error(`${name} has no PDF page.`);
8
10
  return embedded;
9
11
  }
@@ -0,0 +1,120 @@
1
+ import { RenderRefusedError } from "./errors.js";
2
+ import { PDFDocument } from "pdf-lib";
3
+ import { Buffer } from "node:buffer";
4
+ import { inflateSync } from "node:zlib";
5
+ //#region src/pdf-load.ts
6
+ const MAX_PDF_STREAM_BYTES = 16777216;
7
+ const whitespace = "[\\x00\\x09\\x0a\\x0c\\x0d\\x20]";
8
+ function nextObject(bytes, from) {
9
+ let marker = bytes.indexOf(" obj", from, "latin1");
10
+ while (marker !== -1) {
11
+ const match = bytes.toString("latin1", Math.max(0, marker - 48), marker).match(/(\d+)\s+(\d+)\s*$/);
12
+ if (match?.[1]) return {
13
+ body: marker + 4,
14
+ number: Number(match[1])
15
+ };
16
+ marker = bytes.indexOf(" obj", marker + 4, "latin1");
17
+ }
18
+ }
19
+ function streamStart(bytes, from, end) {
20
+ let marker = bytes.indexOf("stream", from, "latin1");
21
+ while (marker !== -1 && marker < end) {
22
+ const before = bytes[marker - 1];
23
+ const after = bytes[marker + 6];
24
+ const delimited = before !== void 0 && (before === 32 || before === 10 || before === 13 || before === 9);
25
+ if (delimited && after === 10) return marker + 7;
26
+ if (delimited && after === 13 && bytes[marker + 7] === 10) return marker + 8;
27
+ marker = bytes.indexOf("stream", marker + 6, "latin1");
28
+ }
29
+ }
30
+ function referencedValue(bytes, object, generation) {
31
+ const marker = Buffer.from(`${object} ${generation} obj`, "latin1");
32
+ let start = bytes.indexOf(marker);
33
+ while (start !== -1) {
34
+ const before = bytes[start - 1];
35
+ if (start === 0 || before === 10 || before === 13 || before === 32) {
36
+ const body = start + marker.length;
37
+ const end = bytes.indexOf("endobj", body, "latin1");
38
+ if (end !== -1) return bytes.toString("latin1", body, end).trim();
39
+ }
40
+ start = bytes.indexOf(marker, start + marker.length);
41
+ }
42
+ }
43
+ function dictionaryValue(dictionary, key, bytes) {
44
+ const direct = dictionary.match(new RegExp(`/${key}${whitespace}+(\\d+)(?:${whitespace}+(\\d+)${whitespace}+R)?`));
45
+ if (direct?.[1] && direct[2]) return referencedValue(bytes, Number(direct[1]), Number(direct[2]));
46
+ return direct?.[1];
47
+ }
48
+ function streamType(dictionary, bytes) {
49
+ const direct = dictionary.match(new RegExp(`/Type${whitespace}*/(ObjStm|XRef)\\b`))?.[1];
50
+ if (direct === "ObjStm" || direct === "XRef") return direct;
51
+ const reference = dictionary.match(new RegExp(`/Type${whitespace}+(\\d+)${whitespace}+(\\d+)${whitespace}+R`));
52
+ if (!reference?.[1] || !reference[2]) return void 0;
53
+ const value = referencedValue(bytes, Number(reference[1]), Number(reference[2]));
54
+ return value === "/ObjStm" ? "ObjStm" : value === "/XRef" ? "XRef" : void 0;
55
+ }
56
+ function filters(dictionary) {
57
+ const value = dictionary.match(new RegExp(`/Filter${whitespace}+(\\[[^\\]]*\\]|/[A-Za-z0-9]+)`))?.[1];
58
+ if (!value) return [];
59
+ const names = [...value.matchAll(/\/([A-Za-z0-9]+)/g)].map((match) => match[1]);
60
+ return names.length > 0 ? names : void 0;
61
+ }
62
+ function refuseUnknown(number, fact) {
63
+ throw new RenderRefusedError(`PDF stream object ${number}'s inflated size could not be established before loading because its ${fact} is unsupported. Use a valid PDF produced by this renderer with a direct length and no filter or one FlateDecode filter to clear this refusal.`);
64
+ }
65
+ function streamObjects(pdf) {
66
+ const bytes = Buffer.from(pdf.buffer, pdf.byteOffset, pdf.byteLength);
67
+ const streams = [];
68
+ let cursor = 0;
69
+ while (cursor < bytes.length) {
70
+ const object = nextObject(bytes, cursor);
71
+ if (!object) break;
72
+ const endObject = bytes.indexOf("endobj", object.body, "latin1");
73
+ if (endObject === -1) break;
74
+ const start = streamStart(bytes, object.body, endObject);
75
+ if (start === void 0) {
76
+ cursor = endObject + 6;
77
+ continue;
78
+ }
79
+ const dictionary = bytes.toString("latin1", object.body, start - 6);
80
+ const length = dictionaryValue(dictionary, "Length", bytes)?.match(/^\s*(\d+)\s*$/)?.[1];
81
+ if (streamType(dictionary, bytes)) {
82
+ if (!length) refuseUnknown(object.number, "declared length");
83
+ streams.push({
84
+ dictionary,
85
+ number: object.number,
86
+ start
87
+ });
88
+ }
89
+ cursor = length ? Math.min(bytes.length, start + Number(length)) : endObject + 6;
90
+ }
91
+ return streams;
92
+ }
93
+ function refusal(number, size, ceiling) {
94
+ return new RenderRefusedError(`PDF stream object ${number} inflated past the ceiling after ${size} bytes; the ceiling is ${ceiling} bytes. Reduce the page content that Chromium packs into one object or cross-reference stream to clear this refusal.`);
95
+ }
96
+ function assertPdfStreamsBounded(pdf, ceiling = MAX_PDF_STREAM_BYTES) {
97
+ const bytes = Buffer.from(pdf.buffer, pdf.byteOffset, pdf.byteLength);
98
+ for (const stream of streamObjects(pdf)) {
99
+ const length = Number(dictionaryValue(stream.dictionary, "Length", bytes));
100
+ if (!Number.isSafeInteger(length) || length < 0 || stream.start + length > bytes.length) refuseUnknown(stream.number, "declared length");
101
+ const encodings = filters(stream.dictionary);
102
+ if (!encodings || encodings.length > 1 || encodings.length === 1 && encodings[0] !== "FlateDecode") refuseUnknown(stream.number, `decode filter ${encodings?.join(", ") || "value"}`);
103
+ if (encodings.length === 0) {
104
+ if (length > ceiling) throw refusal(stream.number, length, ceiling);
105
+ continue;
106
+ }
107
+ try {
108
+ inflateSync(bytes.subarray(stream.start, stream.start + length), { maxOutputLength: ceiling });
109
+ } catch (error) {
110
+ if (error instanceof Error && "code" in error && error.code === "ERR_BUFFER_TOO_LARGE") throw refusal(stream.number, ceiling, ceiling);
111
+ refuseUnknown(stream.number, "FlateDecode data");
112
+ }
113
+ }
114
+ }
115
+ async function loadPdf(pdf, options) {
116
+ assertPdfStreamsBounded(pdf);
117
+ return PDFDocument.load(pdf, options);
118
+ }
119
+ //#endregion
120
+ export { MAX_PDF_STREAM_BYTES, assertPdfStreamsBounded, loadPdf };
@@ -1,4 +1,5 @@
1
1
  import { RenderRefusedError } from "../errors.js";
2
+ import { loadPdf } from "../pdf-load.js";
2
3
  import { cachedPressColor, destinationColor, newlySnapsToNeutral, pdfMaskLuminosity, pressDestination } from "./color-policy.js";
3
4
  import { groupAction, shadingAction } from "./policy.js";
4
5
  import { rewriteContent } from "./content.js";
@@ -10,7 +11,7 @@ import { readProfile } from "../color/profile.js";
10
11
  import { convertSuppliedImage, convertedImageCount, createImageState } from "./image.js";
11
12
  import { createPaintedBoundsTracker, recordPatternPaintedBounds } from "./pattern-bounds.js";
12
13
  import { inspectPressPdf } from "./report.js";
13
- import { PDFDict, PDFDocument, PDFHexString, PDFName, PDFRef, PDFString } from "pdf-lib";
14
+ import { PDFDict, PDFHexString, PDFName, PDFRef, PDFString } from "pdf-lib";
14
15
  import { deflateSync } from "node:zlib";
15
16
  //#region src/press/adapter.ts
16
17
  const key = (name) => PDFName.of(name);
@@ -319,7 +320,7 @@ async function rewritePressPdf(bytes, profileBytes, tolerance = 3, decodeJpeg) {
319
320
  try {
320
321
  const profile = readProfile(profileBytes);
321
322
  if (profile.colorSpace !== "CMYK" && profile.colorSpace !== "GRAY") refuse(`press profile color space ${profile.colorSpace}`, "CMYK or GRAY");
322
- const document = await PDFDocument.load(bytes, { updateMetadata: false });
323
+ const document = await loadPdf(bytes, { updateMetadata: false });
323
324
  const counts = rewritePressDocument(document, profile, tolerance);
324
325
  let convertedImages = 0;
325
326
  const destination = pressDestination(profile);
@@ -1,9 +1,10 @@
1
1
  import { RenderRefusedError } from "../errors.js";
2
+ import { loadPdf } from "../pdf-load.js";
2
3
  import { groupAction, shadingAction } from "./policy.js";
3
4
  import { inspectContent } from "./content.js";
4
5
  import { decodeContent, dictionaryOf, nameOf, numberOf, rawStreamOf, resolvedColorSpace } from "./pdf-objects.js";
5
6
  import { walkPressGraph } from "./visitor.js";
6
- import { PDFArray, PDFDict, PDFDocument, PDFName, PDFRef } from "pdf-lib";
7
+ import { PDFArray, PDFDict, PDFName, PDFRef } from "pdf-lib";
7
8
  //#region src/press/report.ts
8
9
  const key = (name) => PDFName.of(name);
9
10
  const refuse = (found, supported) => {
@@ -64,7 +65,7 @@ function outputDestination(document) {
64
65
  return profile && numberOf(profile.dict, "N") === 1 ? "gray" : "cmyk";
65
66
  }
66
67
  async function inspectPressPdf(bytes, requested) {
67
- const document = await PDFDocument.load(bytes, { updateMetadata: false });
68
+ const document = await loadPdf(bytes, { updateMetadata: false });
68
69
  const destination = requested ?? outputDestination(document);
69
70
  const state = {
70
71
  budget: { decodedBytes: 0 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segnavia/render",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,9 +26,9 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@segnavia/format": "0.12.0",
30
- "@segnavia/frame": "0.12.0",
31
- "@segnavia/html": "0.12.0",
29
+ "@segnavia/format": "0.13.0",
30
+ "@segnavia/frame": "0.13.0",
31
+ "@segnavia/html": "0.13.0",
32
32
  "playwright-core": "1.55.1",
33
33
  "pdf-lib": "^1.17.1"
34
34
  }