@segnavia/render 0.5.0 → 0.7.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
@@ -5,10 +5,11 @@ import { Book, formatNumber, resolveDirection, sheetMm } from "@segnavia/format"
5
5
  import { PDFDocument } from "pdf-lib";
6
6
  import { bookDocument, bookPrintPart } from "@segnavia/html";
7
7
  //#region src/book-pdf.ts
8
- async function inspect(bytes) {
9
- const document = await PDFDocument.load(bytes);
8
+ async function inspect(output) {
9
+ const document = await PDFDocument.load(output.bytes);
10
10
  return {
11
- bytes,
11
+ bytes: output.bytes,
12
+ overflow: output.overflow,
12
13
  pages: document.getPageCount(),
13
14
  destinations: namedDestinations(document)
14
15
  };
@@ -127,7 +128,7 @@ async function printBody(book, options, adapter) {
127
128
  targets: true
128
129
  });
129
130
  if (firstDocument.issues.length > 0) throw new RenderRefusedError(`The book cannot be printed: ${firstDocument.issues.join("; ")}.`);
130
- let part = await inspect(await adapter.print(firstDocument.html, book, options));
131
+ let part = await inspect(await adapter.print(firstDocument.html, book, options, "body"));
131
132
  requireTargets(part, hashTargets(firstDocument));
132
133
  const plan = book.rectoOpenings ? planBlanks(firstDocument.sections, part.destinations) : {
133
134
  ids: [],
@@ -139,7 +140,7 @@ async function printBody(book, options, adapter) {
139
140
  targets: true,
140
141
  blankBefore: plan.ids
141
142
  });
142
- part = await inspect(await adapter.print(finalDocument.html, book, options));
143
+ part = await inspect(await adapter.print(finalDocument.html, book, options, "body"));
143
144
  requireTargets(part, hashTargets(finalDocument));
144
145
  verifyStarts("body", sectionsFor(firstDocument.sections, "body"), plan.predicted, part.destinations);
145
146
  }
@@ -169,7 +170,7 @@ async function printFront(book, source, body, options, adapter) {
169
170
  crossLinks: true,
170
171
  pageNumbers: numbers
171
172
  });
172
- const printed = await inspect(await adapter.print(document.html, book, options));
173
+ const printed = await inspect(await adapter.print(document.html, book, options, "front"));
173
174
  if (ids.length === 0) ids.push(...hashTargets(document));
174
175
  requireTargets(printed, ids);
175
176
  prints.push(printed);
@@ -219,8 +220,14 @@ async function renderBookPdf(input, options, adapter) {
219
220
  const bodyBlankPages = new Set(printedBody.blankBefore.map((id) => (printedBody.part.destinations.get(id)?.page ?? 1) - 1));
220
221
  const frontBlankPages = new Set(printedFront.trailingBlank ? [front.pages] : []);
221
222
  const marginPages = [...marginsFor(book, sectionsFor(printedBody.document.sections, "front"), front.destinations, front.pages, 0, frontBlankPages, book.numbering.frontMatter), ...marginsFor(book, sectionsFor(printedBody.document.sections, "body"), printedBody.part.destinations, printedBody.part.pages, front.pages, bodyBlankPages, book.numbering.body)];
222
- const marginBytes = await adapter.print(marginDocument(book, marginPages), book, options, true);
223
- return adapter.finish(await join(front, printedBody.part, marginBytes), options);
223
+ const margin = await adapter.print(marginDocument(book, marginPages), book, options, "margin");
224
+ const output = await adapter.finish(await join(front, printedBody.part, margin.bytes), options);
225
+ options.overflow?.report?.({ entries: [
226
+ front.overflow,
227
+ printedBody.part.overflow,
228
+ margin.overflow
229
+ ].filter((entry) => entry !== void 0) });
230
+ return output;
224
231
  }
225
232
  function bookPageCss(book, marginLayer = false) {
226
233
  if (!book.page) return "";
@@ -6,9 +6,10 @@ import { frameDocument, newNonce } from "@segnavia/frame/bridge";
6
6
  //#region src/book-render.ts
7
7
  function renderBrowserBookPdf(input, options, dependencies) {
8
8
  return renderBookPdf(input, options, {
9
- print: (html, book, renderOptions, marginLayer = false) => {
9
+ print: (html, book, renderOptions, part) => {
10
10
  if (!book.page) throw new RenderRefusedError("Printing a continuous book needs page; set page before calling bookPdf.");
11
11
  const size = sheetMm(book.page);
12
+ const marginLayer = part === "margin";
12
13
  const css = `${bookPageCss(book, marginLayer)}${marginLayer ? marginCss(book) : ""}${dependencies.fontFaces(renderOptions)}${renderOptions.css ?? ""}`;
13
14
  return dependencies.render({
14
15
  viewport: {
@@ -39,7 +40,7 @@ function renderBrowserBookPdf(input, options, dependencies) {
39
40
  bottom: "0mm",
40
41
  left: "0mm"
41
42
  }
42
- });
43
+ }, Math.round(size.width * PX_PER_MM), part);
43
44
  },
44
45
  finish: dependencies.finish
45
46
  });
@@ -0,0 +1,42 @@
1
+ //#region src/browser-jpeg.ts
2
+ function browserJpegDecoder(tab) {
3
+ return async (bytes) => {
4
+ const encoded = Buffer.from(bytes).toString("base64");
5
+ const decoded = await tab.evaluate(async (base64) => {
6
+ const binary = atob(base64);
7
+ const source = Uint8Array.from(binary, (value) => value.charCodeAt(0));
8
+ const bitmap = await createImageBitmap(new Blob([source], { type: "image/jpeg" }), {
9
+ colorSpaceConversion: "none",
10
+ premultiplyAlpha: "none"
11
+ });
12
+ try {
13
+ const context = new OffscreenCanvas(bitmap.width, bitmap.height).getContext("2d");
14
+ if (!context) throw new Error("Chromium could not create a 2D canvas for JPEG decoding");
15
+ context.drawImage(bitmap, 0, 0);
16
+ const rgba = context.getImageData(0, 0, bitmap.width, bitmap.height).data;
17
+ const rgb = new Uint8Array(bitmap.width * bitmap.height * 3);
18
+ for (let pixel = 0; pixel < bitmap.width * bitmap.height; pixel++) {
19
+ rgb[pixel * 3] = rgba[pixel * 4] ?? 0;
20
+ rgb[pixel * 3 + 1] = rgba[pixel * 4 + 1] ?? 0;
21
+ rgb[pixel * 3 + 2] = rgba[pixel * 4 + 2] ?? 0;
22
+ }
23
+ let value = "";
24
+ for (let offset = 0; offset < rgb.length; offset += 32768) value += String.fromCharCode(...rgb.subarray(offset, Math.min(rgb.length, offset + 32768)));
25
+ return {
26
+ width: bitmap.width,
27
+ height: bitmap.height,
28
+ rgb: btoa(value)
29
+ };
30
+ } finally {
31
+ bitmap.close();
32
+ }
33
+ }, encoded);
34
+ return {
35
+ width: decoded.width,
36
+ height: decoded.height,
37
+ rgb: new Uint8Array(Buffer.from(decoded.rgb, "base64"))
38
+ };
39
+ };
40
+ }
41
+ //#endregion
42
+ export { browserJpegDecoder };
@@ -2,7 +2,7 @@ import { checked } from "./types.js";
2
2
  import { D50, labToXyz, matrix3, tableDecode, tableEncode, tryInvert3, xyzToLab } from "./spaces.js";
3
3
  //#region src/color/black-point.ts
4
4
  function detectSourceBlackPoint(colorSpace, pcs, major, relativeA2b, perceptualB2a) {
5
- if (colorSpace !== "CMYK") return [
5
+ if (colorSpace !== "CMYK" && colorSpace !== "GRAY") return [
6
6
  0,
7
7
  0,
8
8
  0
@@ -19,11 +19,12 @@ function detectSourceBlackPoint(colorSpace, pcs, major, relativeA2b, perceptualB
19
19
  checked(result, 2, "black-point A2B result")
20
20
  ]);
21
21
  const lab = pcs === "Lab " ? returned : xyzToLab(returned);
22
- return [
22
+ const black = [
23
23
  Math.min(50, lab[0]),
24
24
  0,
25
25
  0
26
26
  ];
27
+ return pcs === "Lab " ? black : labToXyz(black);
27
28
  }
28
29
  function quadraticRoot(x, y) {
29
30
  if (x.length < 4) return 0;
@@ -61,14 +62,15 @@ function quadraticRoot(x, y) {
61
62
  return discriminant <= 0 ? 0 : Math.max(0, Math.min(50, (-b + Math.sqrt(discriminant)) / (2 * a)));
62
63
  }
63
64
  function destinationBlackPoint(colorSpace, pcs, major, relativeA2b, relativeB2a, sourceBlack) {
64
- if (colorSpace !== "CMYK") return [...sourceBlack];
65
+ if (colorSpace !== "CMYK" && colorSpace !== "GRAY") return [...sourceBlack];
66
+ const sourceLab = pcs === "Lab " ? sourceBlack : xyzToLab(sourceBlack);
65
67
  const inputRamp = [], outputRamp = [];
66
68
  for (let index = 0; index < 256; index++) {
67
69
  const lightness = index * 100 / 255;
68
70
  const lab = [
69
71
  lightness,
70
- Math.max(-50, Math.min(50, sourceBlack[1])),
71
- Math.max(-50, Math.min(50, sourceBlack[2]))
72
+ Math.max(-50, Math.min(50, sourceLab[1])),
73
+ Math.max(-50, Math.min(50, sourceLab[2]))
72
74
  ];
73
75
  const value = pcs === "Lab " ? lab : labToXyz(lab);
74
76
  const device = relativeB2a.run(tableEncode(pcs, relativeB2a.encoding, major, value, false));
@@ -97,11 +99,12 @@ function destinationBlackPoint(colorSpace, pcs, major, relativeA2b, relativeB2a,
97
99
  y.push(normalized);
98
100
  }
99
101
  }
100
- return [
102
+ const result = [
101
103
  x.length < 3 ? 0 : quadraticRoot(x, y),
102
- sourceBlack[1],
103
- sourceBlack[2]
104
+ sourceLab[1],
105
+ sourceLab[2]
104
106
  ];
107
+ return pcs === "Lab " ? result : labToXyz(result);
105
108
  }
106
109
  function compensateBlackPoint(profile, lab, reverse) {
107
110
  const point = reverse ? profile.sourceBlackPoint : profile.destinationBlackPoint;
@@ -1,4 +1,4 @@
1
- import { clamp, color4, refuse } from "./types.js";
1
+ import { clamp, color1, color4, refuse } from "./types.js";
2
2
  import { labToXyz, srgbToLab, tableEncode } from "./spaces.js";
3
3
  import { compensateBlackPoint } from "./black-point.js";
4
4
  //#region src/color/engine.ts
@@ -14,5 +14,12 @@ function toPress(profile, rgb) {
14
14
  clamp(black)
15
15
  ];
16
16
  }
17
+ function toGray(profile, rgb) {
18
+ if (profile.colorSpace !== "GRAY") refuse(`destination color space ${profile.colorSpace}`, "GRAY");
19
+ const lab = compensateBlackPoint(profile, srgbToLab(rgb), false);
20
+ const pcs = profile.pcs === "Lab " ? lab : labToXyz(lab);
21
+ const [gray] = color1(profile.b2a.run(tableEncode(profile.pcs, profile.b2a.encoding, profile.majorVersion, pcs, true)), "B2A result");
22
+ return [clamp(gray)];
23
+ }
17
24
  //#endregion
18
- export { toPress };
25
+ export { toGray, toPress };
@@ -34,6 +34,16 @@ function multilinear(grid, points, outputs, input) {
34
34
  }
35
35
  return result;
36
36
  }
37
+ function linearGrid(grid, points, outputs, input) {
38
+ const count = checked(points, 0, "CLUT dimensions");
39
+ const position = clamp(checked(input, 0, "CLUT input")) * (count - 1);
40
+ const low = Math.min(count - 2, Math.floor(position));
41
+ const fraction = position - low;
42
+ return Array.from({ length: outputs }, (_, channel) => {
43
+ const lower = grid.get(low * outputs + channel);
44
+ return lower + (grid.get((low + 1) * outputs + channel) - lower) * fraction;
45
+ });
46
+ }
37
47
  function tetrahedral4(grid, points, outputs, input) {
38
48
  const { base, fractions, stride } = layout(points, outputs, input);
39
49
  const order = [
@@ -71,4 +81,4 @@ function matrixInput(values) {
71
81
  ];
72
82
  }
73
83
  //#endregion
74
- export { interpolate, matrixInput, multilinear, tetrahedral4 };
84
+ export { interpolate, linearGrid, matrixInput, multilinear, tetrahedral4 };
@@ -1,7 +1,7 @@
1
1
  import { REVERSE_LUT, checked, refuse } from "./types.js";
2
2
  import { destinationBlackPoint, detectSourceBlackPoint } from "./black-point.js";
3
3
  import { fixed, need, signature, u32 } from "./binary.js";
4
- import { matrixTables, parseMab, parseMft } from "./tables.js";
4
+ import { grayTables, matrixTables, parseMab, parseMft } from "./tables.js";
5
5
  //#region src/color/profile.ts
6
6
  const MAX_PROFILE_BYTES = 33554432;
7
7
  function header(bytes, view, bounds) {
@@ -12,7 +12,7 @@ function header(bytes, view, bounds) {
12
12
  const profileClass = signature(bytes, 12, bounds);
13
13
  if (profileClass !== "prtr" && profileClass !== "mntr") refuse(`profile class ${JSON.stringify(profileClass)}`, "prtr or mntr");
14
14
  const colorSpace = signature(bytes, 16, bounds);
15
- if (colorSpace !== "CMYK" && colorSpace !== "RGB ") refuse(`color space ${JSON.stringify(colorSpace)}`, "CMYK or RGB");
15
+ if (colorSpace !== "CMYK" && colorSpace !== "GRAY" && colorSpace !== "RGB ") refuse(`color space ${JSON.stringify(colorSpace)}`, "CMYK, GRAY, or RGB");
16
16
  const pcs = signature(bytes, 20, bounds);
17
17
  if (pcs !== "Lab " && pcs !== "XYZ ") refuse(`PCS ${JSON.stringify(pcs)}`, "Lab or XYZ");
18
18
  u32(view, 0, bounds);
@@ -55,6 +55,13 @@ function table(bytes, view, all, names, inputs, outputs) {
55
55
  if (!record) throw new Error(`ICC profile is missing ${name} after its tag set was validated`);
56
56
  return recordTable(bytes, view, record, inputs, outputs);
57
57
  }
58
+ function optionalTable(bytes, view, all, names, inputs, outputs) {
59
+ const name = names.find((candidate) => all[candidate]);
60
+ if (!name) return void 0;
61
+ const record = all[name];
62
+ if (!record) throw new Error(`ICC profile is missing ${name} after its tag set was validated`);
63
+ return recordTable(bytes, view, record, inputs, outputs);
64
+ }
58
65
  function readProfile(bytes) {
59
66
  if (bytes.length > 33554432) refuse(`profile length ${bytes.length}`, `at most ${MAX_PROFILE_BYTES} bytes`);
60
67
  const bounds = {
@@ -63,12 +70,15 @@ function readProfile(bytes) {
63
70
  }, view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
64
71
  const parsed = header(bytes, view, bounds), all = records(bytes, view, bounds);
65
72
  if (all.wtpt?.type !== "XYZ ") refuse(`white point tag type ${JSON.stringify(all.wtpt?.type ?? "missing")}`, "XYZ ");
66
- const channels = parsed.colorSpace === "CMYK" ? 4 : 3;
73
+ const channels = parsed.colorSpace === "CMYK" ? 4 : parsed.colorSpace === "GRAY" ? 1 : 3;
67
74
  const matrix = parsed.colorSpace === "RGB " ? matrixTables(bytes, view, all) : void 0;
68
- const a2b = matrix?.a2b ?? table(bytes, view, all, ["A2B1", "A2B0"], channels, 3);
69
- const b2a = matrix?.b2a ?? table(bytes, view, all, ["B2A1", "B2A0"], 3, channels);
70
- const perceptualA2b = matrix?.a2b ?? table(bytes, view, all, ["A2B0", "A2B1"], channels, 3);
71
- const perceptualB2a = matrix?.b2a ?? table(bytes, view, all, ["B2A0", "B2A1"], 3, channels);
75
+ const trcRecord = all.kTRC;
76
+ const gray = parsed.colorSpace === "GRAY" && trcRecord ? grayTables(bytes, view, trcRecord, parsed.pcs) : void 0;
77
+ const select = (names, inputs, outputs, fallback) => optionalTable(bytes, view, all, names, inputs, outputs) ?? fallback ?? table(bytes, view, all, names, inputs, outputs);
78
+ const a2b = matrix?.a2b ?? select(["A2B1", "A2B0"], channels, 3, gray?.a2b);
79
+ const b2a = matrix?.b2a ?? select(["B2A1", "B2A0"], 3, channels, gray?.b2a);
80
+ const perceptualA2b = matrix?.a2b ?? select(["A2B0", "A2B1"], channels, 3, gray?.a2b);
81
+ const perceptualB2a = matrix?.b2a ?? select(["B2A0", "B2A1"], 3, channels, gray?.b2a);
72
82
  const gamutRecord = all.gamt, gamut = gamutRecord ? recordTable(bytes, view, gamutRecord, 3, 1) : void 0;
73
83
  const white = all.wtpt;
74
84
  if (!white) throw new Error("ICC profile is missing wtpt after its tag set was validated");
@@ -1,7 +1,7 @@
1
1
  import { REVERSE_LUT, checked, clamp, refuse } from "./types.js";
2
2
  import { invert3, matrix3 } from "./spaces.js";
3
3
  import { align4, fixed, need, signature, u16, u32 } from "./binary.js";
4
- import { interpolate, matrixInput, multilinear, tetrahedral4 } from "./interpolation.js";
4
+ import { interpolate, linearGrid, matrixInput, multilinear, tetrahedral4 } from "./interpolation.js";
5
5
  //#region src/color/tables.ts
6
6
  function packed(bytes, view, offset, length, width, bounds) {
7
7
  need(offset, length * width, bounds);
@@ -73,6 +73,16 @@ function parseCurve(bytes, view, offset, bounds) {
73
73
  size: 12 + count * 4
74
74
  };
75
75
  }
76
+ function invertCurve(curve, target) {
77
+ let low = 0, high = 1;
78
+ const descending = curve(0) > curve(1);
79
+ for (let index = 0; index < 24; index++) {
80
+ const middle = (low + high) / 2;
81
+ if (descending ? curve(middle) > target : curve(middle) < target) low = middle;
82
+ else high = middle;
83
+ }
84
+ return (low + high) / 2;
85
+ }
76
86
  function curvesAt(bytes, view, offset, count, bounds) {
77
87
  const curves = [];
78
88
  let cursor = offset;
@@ -132,7 +142,7 @@ function parseMft(bytes, view, record, type, inputs, outputs) {
132
142
  ];
133
143
  current = matrix3(fixedMatrix, matrixInput(current));
134
144
  }
135
- current = inputs === 3 ? multilinear(grid, points, outputs, current) : tetrahedral4(grid, points, outputs, current);
145
+ current = inputs === 1 ? linearGrid(grid, points, outputs, current) : inputs === 3 ? multilinear(grid, points, outputs, current) : tetrahedral4(grid, points, outputs, current);
136
146
  return current.map((value, channel) => interpolate(checked(outputTables, channel, `${type} output tables`), value));
137
147
  }
138
148
  };
@@ -179,7 +189,7 @@ function parseMab(bytes, view, record, type, inputs, outputs) {
179
189
  2
180
190
  ].map((row) => checked(matrix, row * 3, `${type} matrix`) * x + checked(matrix, row * 3 + 1, `${type} matrix`) * y + checked(matrix, row * 3 + 2, `${type} matrix`) * z + checked(matrix, 9 + row, `${type} matrix`));
181
191
  };
182
- const clut = (values) => !grid ? [...values] : inputs === 3 ? multilinear(grid, points, outputs, values) : tetrahedral4(grid, points, outputs, values);
192
+ const clut = (values) => !grid ? [...values] : inputs === 1 ? linearGrid(grid, points, outputs, values) : inputs === 3 ? multilinear(grid, points, outputs, values) : tetrahedral4(grid, points, outputs, values);
183
193
  return {
184
194
  inputChannels: inputs,
185
195
  outputChannels: outputs,
@@ -227,15 +237,6 @@ function matrixTables(bytes, view, records) {
227
237
  checked(checked(columns, 2, "colorants"), 2, "blue")
228
238
  ];
229
239
  const inverse = invert3(matrix);
230
- const invertCurve = (curve, target) => {
231
- let low = 0, high = 1;
232
- for (let index = 0; index < 24; index++) {
233
- const middle = (low + high) / 2;
234
- if (curve(middle) < target) low = middle;
235
- else high = middle;
236
- }
237
- return (low + high) / 2;
238
- };
239
240
  return {
240
241
  a2b: {
241
242
  inputChannels: 3,
@@ -251,5 +252,38 @@ function matrixTables(bytes, view, records) {
251
252
  }
252
253
  };
253
254
  }
255
+ function grayTables(bytes, view, record, pcs) {
256
+ const curve = parseCurve(bytes, view, record.offset, record).curve;
257
+ const scale = 32768 / 65535;
258
+ return {
259
+ a2b: {
260
+ inputChannels: 1,
261
+ outputChannels: 3,
262
+ encoding: "mAB ",
263
+ run(input) {
264
+ const luminance = curve(checked(input, 0, "gray input"));
265
+ if (pcs === "XYZ ") return [
266
+ luminance * .9642 * scale,
267
+ luminance * scale,
268
+ luminance * .8249 * scale
269
+ ];
270
+ return [
271
+ luminance,
272
+ 128 / 255,
273
+ 128 / 255
274
+ ];
275
+ }
276
+ },
277
+ b2a: {
278
+ inputChannels: 3,
279
+ outputChannels: 1,
280
+ encoding: REVERSE_LUT,
281
+ run(input) {
282
+ const luminance = pcs === "XYZ " ? checked(input, 1, "gray PCS input") / scale : checked(input, 0, "gray PCS input");
283
+ return [invertCurve(curve, luminance)];
284
+ }
285
+ }
286
+ };
287
+ }
254
288
  //#endregion
255
- export { matrixTables, parseCurve, parseMab, parseMft };
289
+ export { grayTables, invertCurve, matrixTables, parseCurve, parseMab, parseMft };
@@ -8,6 +8,9 @@ function checked(values, index, structure) {
8
8
  if (value === void 0) throw new Error(`${structure} index ${index} is outside length ${values.length}`);
9
9
  return value;
10
10
  }
11
+ function color1(values, structure) {
12
+ return [checked(values, 0, structure)];
13
+ }
11
14
  function color4(values, structure) {
12
15
  return [
13
16
  checked(values, 0, structure),
@@ -18,4 +21,4 @@ function color4(values, structure) {
18
21
  }
19
22
  const clamp = (value) => Math.max(0, Math.min(1, value));
20
23
  //#endregion
21
- export { REVERSE_LUT, checked, clamp, color4, refuse };
24
+ export { REVERSE_LUT, checked, clamp, color1, color4, refuse };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EffectsReport } from "./effects/types.js";
2
- import { PressImage, PressReport } from "./press/types.js";
3
- import { Font, ImageOptions, RenderLimits, RenderOptions } from "./options.js";
2
+ import { PressDestination, PressImage, PressReport } from "./press/types.js";
3
+ import { Font, ImageOptions, OverflowContribution, OverflowEntry, OverflowPart, OverflowReport, RenderLimits, RenderOptions } from "./options.js";
4
4
  import { RenderRefusedError, RendererUnavailableError } from "./errors.js";
5
5
  import { BookInput, Deck, ImpositionInput, PageInput } from "@segnavia/format";
6
6
  import { PrintOptions } from "@segnavia/frame/print-document";
@@ -33,4 +33,4 @@ export declare function fontFaces(fonts?: Font[]): string;
33
33
  export declare function captureImage(tab: Page$1, options: Pick<ImageOptions, 'format' | 'quality' | 'limits'>, fullPage: boolean): Promise<Uint8Array>;
34
34
  export declare function openRenderer(options: RendererOptions): Promise<Renderer>;
35
35
  //#endregion
36
- export { type EffectsReport, type Font, type ImageOptions, type PressImage, type PressReport, type RenderLimits, type RenderOptions, RenderRefusedError, RendererUnavailableError };
36
+ export { type EffectsReport, type Font, type ImageOptions, type OverflowContribution, type OverflowEntry, type OverflowPart, type OverflowReport, type PressDestination, type PressImage, type PressReport, type RenderLimits, type RenderOptions, RenderRefusedError, RendererUnavailableError };
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { RenderRefusedError, RendererUnavailableError } from "./errors.js";
2
2
  import { renderBrowserBookPdf } from "./book-render.js";
3
+ import { browserJpegDecoder } from "./browser-jpeg.js";
3
4
  import { containFixedPageOverflow } from "./overflow-containment.js";
4
5
  import { renderDeckPdf } from "./deck-pdf.js";
5
6
  import { assertNeutralTolerance } from "./press/color-policy.js";
6
7
  import { rewriteEffectsPdf } from "./effects/adapter.js";
8
+ import { measureOverflow, printWithOverflow } from "./overflow.js";
7
9
  import { assertPrintOptions, printCss, printPage, printViewport } from "./page-paper.js";
8
10
  import { renderPagesPdf } from "./pages-pdf.js";
9
11
  import { rewritePressPdf } from "./press/adapter.js";
@@ -55,7 +57,7 @@ async function enforce(pdf, limits) {
55
57
  }
56
58
  return pdf;
57
59
  }
58
- async function finishPdf(pdf, options) {
60
+ async function finishPdf(pdf, options, decodeJpeg) {
59
61
  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}.`);
60
62
  let output = pdf;
61
63
  if (options.effects !== false) {
@@ -66,7 +68,7 @@ async function finishPdf(pdf, options) {
66
68
  if (options.press) {
67
69
  const neutralTolerance = options.press.neutralTolerance ?? 3;
68
70
  assertNeutralTolerance(neutralTolerance);
69
- const converted = await rewritePressPdf(output, options.press.profile, neutralTolerance);
71
+ const converted = await rewritePressPdf(output, options.press.profile, neutralTolerance, decodeJpeg);
70
72
  output = converted.bytes;
71
73
  options.press.report?.(converted.report);
72
74
  }
@@ -205,6 +207,18 @@ async function openRenderer(options) {
205
207
  reportHeight: false
206
208
  });
207
209
  }
210
+ async function finishWithBrowser(pdf, options) {
211
+ if (!options.press || new TextDecoder().decode(options.press.profile.subarray(16, 20)) !== "GRAY") return finishPdf(pdf, options);
212
+ return withTab({
213
+ viewport: {
214
+ width: 1,
215
+ height: 1
216
+ },
217
+ scripts: true,
218
+ html: "<!doctype html>",
219
+ waitForReady: false
220
+ }, (tab) => finishPdf(pdf, options, browserJpegDecoder(tab)));
221
+ }
208
222
  function pageViewport(page, width, printing = false) {
209
223
  if (page.print) return printViewport(page.print, printing);
210
224
  if (page.size === "fluid") return {
@@ -217,10 +231,10 @@ async function openRenderer(options) {
217
231
  };
218
232
  }
219
233
  const image = captureImage;
220
- async function renderPagePdf(page, opts) {
234
+ async function renderPagePdf(page, opts, inputIndex) {
221
235
  if (page.print) assertPrintOptions(page.print, opts.preferCssPageSize);
222
236
  const viewport = pageViewport(page, void 0, true);
223
- return await withTab({
237
+ return withTab({
224
238
  viewport,
225
239
  scripts: page.scripts === "sandboxed",
226
240
  html: pageDocument(page, opts, true),
@@ -228,15 +242,26 @@ async function openRenderer(options) {
228
242
  boundMs: opts.boundMs,
229
243
  beforeOutput: opts.beforeOutput
230
244
  }, async (tab) => {
231
- if (page.print) return printPage(tab, page.print, opts.preferCssPageSize);
245
+ if (page.print) return printPage(tab, page.print, opts.preferCssPageSize, inputIndex);
232
246
  const fixedHeight = typeof page.size === "object" && page.size.height !== "auto" ? page.size.height : null;
233
247
  const height = fixedHeight ?? await contentHeight(tab);
234
- return tab.pdf({
248
+ if (fixedHeight !== null) {
249
+ const overflow = await measureOverflow(tab, viewport.width, "page", inputIndex);
250
+ return {
251
+ bytes: await tab.pdf({
252
+ width: `${viewport.width}px`,
253
+ height: `${height}px`,
254
+ printBackground: true
255
+ }),
256
+ overflow
257
+ };
258
+ }
259
+ return printWithOverflow(tab, viewport.width, "page", {
235
260
  width: `${viewport.width}px`,
236
261
  height: `${height}px`,
237
262
  printBackground: true,
238
- pageRanges: fixedHeight ? void 0 : "1"
239
- });
263
+ pageRanges: "1"
264
+ }, inputIndex);
240
265
  });
241
266
  }
242
267
  async function renderPiecePdf(page, geometry, css, opts) {
@@ -273,13 +298,16 @@ async function openRenderer(options) {
273
298
  return {
274
299
  bookPdf(input, opts = {}) {
275
300
  return renderBrowserBookPdf(input, opts, {
276
- render: (setup, pdf) => withTab(setup, (tab) => tab.pdf(pdf)),
301
+ render: (setup, pdf, pageWidth, part) => withTab(setup, (tab) => printWithOverflow(tab, pageWidth, part, pdf)),
277
302
  fontFaces: (options) => fontFaces(options.fonts),
278
- finish: finishPdf
303
+ finish: finishWithBrowser
279
304
  });
280
305
  },
281
306
  async pagePdf(input, opts = {}) {
282
- return finishPdf(await renderPagePdf(Page.parse(input), opts), opts);
307
+ const printed = await renderPagePdf(Page.parse(input), opts);
308
+ const output = await finishWithBrowser(printed.bytes, opts);
309
+ if (printed.overflow) opts.overflow?.report?.({ entries: [printed.overflow] });
310
+ return output;
283
311
  },
284
312
  async pageImage(input, opts = {}) {
285
313
  const page = Page.parse(input);
@@ -300,10 +328,10 @@ async function openRenderer(options) {
300
328
  },
301
329
  pagesPdf(inputs, opts = {}) {
302
330
  return renderPagesPdf(inputs, opts, {
303
- pagePdf: renderPagePdf,
331
+ pagePdf: (page, options, inputIndex) => renderPagePdf(page, options, inputIndex),
304
332
  piecePdf: renderPiecePdf,
305
333
  fontFaces: (options) => fontFaces(options.fonts),
306
- finish: finishPdf
334
+ finish: finishWithBrowser
307
335
  });
308
336
  },
309
337
  deckPdf(deck, opts = {}) {
@@ -313,7 +341,7 @@ async function openRenderer(options) {
313
341
  waitForReady: false
314
342
  }, (tab) => tab.pdf(pdf)),
315
343
  fontFaces: (options) => fontFaces(options.fonts),
316
- finish: finishPdf
344
+ finish: finishWithBrowser
317
345
  });
318
346
  },
319
347
  async slideImage(deck, index, opts = {}) {
package/dist/options.d.ts CHANGED
@@ -2,6 +2,37 @@ import { EffectsReport } from "./effects/types.js";
2
2
  import { PressReport } from "./press/types.js";
3
3
  import { Page } from "playwright-core";
4
4
  //#region src/options.d.ts
5
+ export type OverflowPart = 'page' | 'front' | 'body' | 'margin';
6
+ export interface OverflowContribution {
7
+ /** A short id or tag-and-class path locating the element. */
8
+ locator: string;
9
+ /** Border-box pixels clipped beyond the inline start of the page. */
10
+ clippedStart: number;
11
+ /** Border-box pixels clipped beyond the inline end of the page. */
12
+ clippedEnd: number;
13
+ }
14
+ export interface OverflowEntry {
15
+ /** Output part printed by this entry. */
16
+ part: OverflowPart;
17
+ /** Input position when the public render is pagesPdf. */
18
+ inputIndex?: number;
19
+ /** Printable inline width in CSS pixels. */
20
+ pageWidth: number;
21
+ /** Greatest document scroll width in CSS pixels. */
22
+ extent: number;
23
+ /** Pixels clipped beyond the inline start of the page. */
24
+ clippedStart: number;
25
+ /** Pixels clipped beyond the inline end of the page. */
26
+ clippedEnd: number;
27
+ /** PDF scale used to preserve the print's true size. */
28
+ scale: number;
29
+ /** Up to twenty border boxes that extend beyond the page. */
30
+ elements: OverflowContribution[];
31
+ }
32
+ export interface OverflowReport {
33
+ /** Kept prints in output order. */
34
+ entries: OverflowEntry[];
35
+ }
5
36
  export interface Font {
6
37
  family: string;
7
38
  data: Uint8Array;
@@ -24,6 +55,9 @@ export interface RenderOptions {
24
55
  effects?: false | {
25
56
  report?: (report: EffectsReport) => void;
26
57
  };
58
+ overflow?: {
59
+ report?: (report: OverflowReport) => void;
60
+ };
27
61
  press?: {
28
62
  profile: Uint8Array;
29
63
  neutralTolerance?: number;