@segnavia/render 0.2.0 → 0.4.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.
@@ -0,0 +1,354 @@
1
+ import { IDENTITY_MATRIX, applyAffine } from "../press/affine.js";
2
+ import { visitContentOperations } from "../press/content.js";
3
+ import { decodeContent, dictionaryOf, nameOf, numberOf, resolvedColorSpace } from "../press/pdf-objects.js";
4
+ import { GENERATED_EFFECT_IMAGE_KEY } from "./types.js";
5
+ import { SHADING_SAMPLE_SPACING_PT, chargeShadingSamples } from "../press/sample-shading.js";
6
+ import { functionEvaluator } from "./function-evaluator.js";
7
+ import { EMPTY_EFFECT_GRAPHICS_STATE, advanceEffectGraphicsState, effectFormStructure, effectGraphicsStateOpacity, effectGraphicsStateStructure, effectGraphicsStateValue } from "./structure.js";
8
+ import { PDFArray, PDFDict, PDFName, PDFNumber, PDFRawStream, PDFRef } from "pdf-lib";
9
+ import { deflateSync } from "node:zlib";
10
+ //#region src/effects/rule-c.ts
11
+ const key = (name) => PDFName.of(name);
12
+ const resolved = (document, object) => object instanceof PDFRef ? document.context.lookup(object) : object;
13
+ const MAX_EFFECT_COVERAGE_LAYERS = 4096;
14
+ function arrayNumbers(dictionary, label) {
15
+ const array = dictionary.lookupMaybe(key(label), PDFArray);
16
+ if (!array) return void 0;
17
+ const values = [];
18
+ for (let index = 0; index < array.size(); index++) {
19
+ const value = array.lookupMaybe(index, PDFNumber)?.asNumber();
20
+ if (value === void 0 || !Number.isFinite(value)) return void 0;
21
+ values.push(value);
22
+ }
23
+ return values;
24
+ }
25
+ function clamp(value) {
26
+ return Math.max(0, Math.min(1, value));
27
+ }
28
+ function axialLayer(document, pattern) {
29
+ if (numberOf(pattern, "PatternType") !== 2) return void 0;
30
+ const shading = dictionaryOf(resolved(document, pattern.get(key("Shading"))));
31
+ if (!shading || numberOf(shading, "ShadingType") !== 2) return void 0;
32
+ const space = resolvedColorSpace(document, resolved(document, shading.get(key("ColorSpace")))).kind;
33
+ const channels = space === "rgb" ? 3 : space === "gray" ? 1 : 0;
34
+ const coords = arrayNumbers(shading, "Coords");
35
+ const matrix = arrayNumbers(pattern, "Matrix") ?? IDENTITY_MATRIX;
36
+ const domain = arrayNumbers(shading, "Domain") ?? [0, 1];
37
+ const evaluate = functionEvaluator(document, shading.get(key("Function")), channels);
38
+ if (!channels || coords?.length !== 4 || matrix.length !== 6 || domain.length !== 2 || !evaluate) return void 0;
39
+ const start = applyAffine(matrix, coords[0] ?? 0, coords[1] ?? 0);
40
+ const end = applyAffine(matrix, coords[2] ?? 0, coords[3] ?? 0);
41
+ const dx = end.x - start.x;
42
+ const dy = end.y - start.y;
43
+ if (!(dx * dx + dy * dy > 0)) return void 0;
44
+ return {
45
+ axis: {
46
+ start,
47
+ end
48
+ },
49
+ channels,
50
+ cost: evaluate.cost,
51
+ evaluate: (position) => evaluate((domain[0] ?? 0) + position * ((domain[1] ?? 1) - (domain[0] ?? 0))).map(clamp)
52
+ };
53
+ }
54
+ function operations(stream, budget) {
55
+ const found = [];
56
+ visitContentOperations(decodeContent(stream, budget), (operation) => found.push(operation));
57
+ return found;
58
+ }
59
+ function simplePaintStream(stream, budget) {
60
+ const source = new TextDecoder("latin1").decode(decodeContent(stream, budget));
61
+ return !/[()]/.test(source) && !/(?:^|\s)(?:BT|ET|Tj|TJ)(?:\s|$)/.test(source);
62
+ }
63
+ function paints(document, stream, inherited, budget) {
64
+ const resources = stream.dict.lookupMaybe(key("Resources"), PDFDict) ?? inherited;
65
+ const states = resources?.lookupMaybe(key("ExtGState"), PDFDict);
66
+ const patterns = resources?.lookupMaybe(key("Pattern"), PDFDict);
67
+ const xObjects = resources?.lookupMaybe(key("XObject"), PDFDict);
68
+ const stack = [];
69
+ const graphicsStack = [];
70
+ let graphicsState = EMPTY_EFFECT_GRAPHICS_STATE;
71
+ let pattern;
72
+ const result = [];
73
+ for (const operation of operations(stream, budget)) {
74
+ const name = operation.operands.at(-1)?.kind === "name" ? operation.operands.at(-1)?.raw.slice(1) : void 0;
75
+ graphicsState = advanceEffectGraphicsState(document, operation, states, graphicsState, graphicsStack);
76
+ if (operation.operator === "q") stack.push(pattern);
77
+ else if (operation.operator === "Q") pattern = stack.pop();
78
+ else if (["scn", "SCN"].includes(operation.operator) && name) pattern = resolved(document, patterns?.get(key(name)));
79
+ else if (operation.operator === "Do" && name) result.push({
80
+ graphicsState,
81
+ operation,
82
+ xObject: resolved(document, xObjects?.get(key(name)))
83
+ });
84
+ else if ([
85
+ "f",
86
+ "f*",
87
+ "F"
88
+ ].includes(operation.operator)) result.push({
89
+ graphicsState,
90
+ operation,
91
+ pattern
92
+ });
93
+ }
94
+ return result;
95
+ }
96
+ function inspectForm(context, stream) {
97
+ if (nameOf(stream.dict.get(key("Subtype"))) === "Form" && effectFormStructure(stream.dict) === "other") context.structure = "other";
98
+ }
99
+ function inspectGraphicsState(context, graphicsState) {
100
+ if (graphicsState.structure === "other") context.structure = "other";
101
+ }
102
+ function singleLayer(document, stream, budget, context, depth = 0) {
103
+ inspectForm(context, stream);
104
+ if (depth > 12 || !simplePaintStream(stream, budget)) return void 0;
105
+ const items = paints(document, stream, void 0, budget);
106
+ for (const item of items) inspectGraphicsState(context, item.graphicsState);
107
+ if (items.length !== 1 || effectGraphicsStateValue(items[0]?.graphicsState ?? EMPTY_EFFECT_GRAPHICS_STATE, "SMask")) return void 0;
108
+ const item = items[0];
109
+ if (!item) return void 0;
110
+ if (effectGraphicsStateOpacity(item.graphicsState) !== 1) return void 0;
111
+ if (item.pattern instanceof PDFDict) {
112
+ const axial = axialLayer(document, item.pattern);
113
+ if (axial) return axial;
114
+ if (item.pattern instanceof PDFRawStream) return singleLayer(document, item.pattern, budget, context, depth + 1);
115
+ }
116
+ return item.xObject instanceof PDFRawStream ? singleLayer(document, item.xObject, budget, context, depth + 1) : void 0;
117
+ }
118
+ function transfer(document, mask) {
119
+ const object = resolved(document, mask.get(key("TR")));
120
+ if (!object || nameOf(object) === "Identity") return "identity";
121
+ if (!(object instanceof PDFRawStream) || numberOf(object.dict, "FunctionType") !== 4) return void 0;
122
+ return new TextDecoder("latin1").decode(object.contents).replace(/\s+/g, " ").trim() === "{1 exch sub}" ? "invert" : void 0;
123
+ }
124
+ function mappedCoverage(coverage, mapped) {
125
+ if (mapped === "identity") return coverage;
126
+ return coverage.kind === "constant" ? {
127
+ kind: "constant",
128
+ value: 1 - coverage.value
129
+ } : {
130
+ ...coverage,
131
+ transfer: coverage.transfer === "identity" ? "invert" : "identity"
132
+ };
133
+ }
134
+ function scaledCoverage(coverage, opacity) {
135
+ return coverage.kind === "constant" ? {
136
+ kind: "constant",
137
+ value: coverage.value * opacity
138
+ } : {
139
+ ...coverage,
140
+ opacity: coverage.opacity * opacity
141
+ };
142
+ }
143
+ function maskCoverage(document, mask, budget, masks, context, depth) {
144
+ masks.push(mask);
145
+ const source = resolved(document, mask.get(key("G")));
146
+ if (!(source instanceof PDFRawStream)) return void 0;
147
+ const mode = nameOf(mask.get(key("S")));
148
+ const mapped = transfer(document, mask);
149
+ if (!mapped) return void 0;
150
+ if (mode === "Alpha") {
151
+ const coverage = alphaCoverage(document, source, budget, masks, context, depth + 1);
152
+ return coverage ? mappedCoverage(coverage, mapped) : void 0;
153
+ }
154
+ if (mode !== "Luminosity") return void 0;
155
+ const layer = singleLayer(document, source, budget, context, depth + 1);
156
+ const gray = layer?.channels === 1 || layer?.channels === 3 && [
157
+ 0,
158
+ .5,
159
+ 1
160
+ ].every((position) => {
161
+ const sample = layer.evaluate(position);
162
+ return sample.every((value) => Math.abs(value - (sample[0] ?? 0)) < .001);
163
+ });
164
+ return layer && gray ? {
165
+ kind: "layer",
166
+ layer,
167
+ opacity: 1,
168
+ transfer: mapped
169
+ } : void 0;
170
+ }
171
+ function alphaCoverage(document, stream, budget, masks, context, depth = 0) {
172
+ inspectForm(context, stream);
173
+ if (depth > 12 || !simplePaintStream(stream, budget)) return void 0;
174
+ const items = paints(document, stream, void 0, budget);
175
+ for (const found of items) inspectGraphicsState(context, found.graphicsState);
176
+ if (items.length !== 1) return void 0;
177
+ const item = items[0];
178
+ if (!item) return void 0;
179
+ const opacity = effectGraphicsStateOpacity(item.graphicsState);
180
+ if (opacity === void 0) return void 0;
181
+ const mask = dictionaryOf(resolved(document, effectGraphicsStateValue(item.graphicsState, "SMask")));
182
+ if (mask) {
183
+ const found = maskCoverage(document, mask, budget, masks, context, depth);
184
+ return found ? scaledCoverage(found, opacity) : void 0;
185
+ }
186
+ if (item.xObject instanceof PDFRawStream) {
187
+ const found = alphaCoverage(document, item.xObject, budget, masks, context, depth + 1);
188
+ return found ? scaledCoverage(found, opacity) : void 0;
189
+ }
190
+ if (item.pattern instanceof PDFRawStream) {
191
+ const found = alphaCoverage(document, item.pattern, budget, masks, context, depth + 1);
192
+ return found ? scaledCoverage(found, opacity) : void 0;
193
+ }
194
+ return item.pattern instanceof PDFDict || [
195
+ "f",
196
+ "f*",
197
+ "F"
198
+ ].includes(item.operation.operator) ? {
199
+ kind: "constant",
200
+ value: opacity
201
+ } : void 0;
202
+ }
203
+ function sameAxis(left, right) {
204
+ const lx = left.end.x - left.start.x;
205
+ const ly = left.end.y - left.start.y;
206
+ const rx = right.end.x - right.start.x;
207
+ const ry = right.end.y - right.start.y;
208
+ const scale = Math.hypot(lx, ly) * Math.hypot(rx, ry);
209
+ if (!(scale > 0) || Math.abs(lx * ry - ly * rx) > scale * 1e-4) return false;
210
+ return Math.abs((right.start.x - left.start.x) * ly - (right.start.y - left.start.y) * lx) <= scale * 1e-4;
211
+ }
212
+ function compositeCoverage(document, items, budget, context) {
213
+ if (items.length > 4096) return void 0;
214
+ const masks = [];
215
+ const coverage = [];
216
+ for (const item of items) {
217
+ inspectGraphicsState(context, item.graphicsState);
218
+ const opacity = effectGraphicsStateOpacity(item.graphicsState);
219
+ const mask = dictionaryOf(resolved(document, effectGraphicsStateValue(item.graphicsState, "SMask")));
220
+ const source = resolved(document, mask?.get(key("G")));
221
+ if (opacity === void 0 || !mask || !(source instanceof PDFRawStream) || nameOf(mask.get(key("S"))) !== "Alpha") return void 0;
222
+ masks.push(mask);
223
+ const found = alphaCoverage(document, source, budget, masks, context);
224
+ const mapped = transfer(document, mask);
225
+ if (!found || !mapped) return void 0;
226
+ coverage.push(scaledCoverage(mappedCoverage(found, mapped), opacity));
227
+ }
228
+ return {
229
+ coverage,
230
+ masks
231
+ };
232
+ }
233
+ function candidate(context, nested, oneAxis, plan) {
234
+ return {
235
+ nested,
236
+ oneAxis,
237
+ plan,
238
+ structure: context.structure
239
+ };
240
+ }
241
+ function ruleCPlan(document, form, outerGraphicsState, budget) {
242
+ const context = { structure: "allowed" };
243
+ if (effectGraphicsStateStructure(outerGraphicsState) === "other") context.structure = "other";
244
+ inspectForm(context, form);
245
+ if (!simplePaintStream(form, budget)) return candidate(context, false, "non-shading");
246
+ const bbox = arrayNumbers(form.dict, "BBox");
247
+ const items = paints(document, form, void 0, budget);
248
+ const nested = items.some((item) => dictionaryOf(resolved(document, effectGraphicsStateValue(item.graphicsState, "SMask"))) !== void 0);
249
+ if (bbox?.length !== 4 || items.length < 1 || items.some((item) => !(item.xObject instanceof PDFRawStream))) return candidate(context, nested, "non-shading");
250
+ const colorObject = items[0]?.xObject;
251
+ if (!(colorObject instanceof PDFRawStream) || nameOf(colorObject.dict.get(key("Subtype"))) !== "Form") return candidate(context, nested, "non-shading");
252
+ const color = singleLayer(document, colorObject, budget, context);
253
+ if (color?.channels !== 3 || items.some((item) => item.xObject !== colorObject)) return candidate(context, nested, "non-shading");
254
+ const combined = compositeCoverage(document, items, budget, context);
255
+ if (!combined) return candidate(context, nested, "non-shading");
256
+ const { coverage, masks } = combined;
257
+ const layers = coverage.filter((value) => value.kind === "layer");
258
+ if (layers.length === 0) return candidate(context, nested, "non-shading");
259
+ if (layers.some(({ layer }) => !sameAxis(color.axis, layer.axis))) return candidate(context, nested, "different");
260
+ return candidate(context, nested, "shared", {
261
+ bbox,
262
+ color,
263
+ coverage,
264
+ masks
265
+ });
266
+ }
267
+ function positionOnAxis(axis, x, y) {
268
+ const dx = axis.end.x - axis.start.x;
269
+ const dy = axis.end.y - axis.start.y;
270
+ return ((x - axis.start.x) * dx + (y - axis.start.y) * dy) / (dx * dx + dy * dy);
271
+ }
272
+ function buildRuleCImage(document, plan, opacity, budget, printMatrix) {
273
+ const dx = plan.color.axis.end.x - plan.color.axis.start.x;
274
+ const dy = plan.color.axis.end.y - plan.color.axis.start.y;
275
+ const length = Math.hypot(dx, dy);
276
+ if (!(length > 0)) return void 0;
277
+ const ux = dx / length;
278
+ const uy = dy / length;
279
+ const vx = -uy;
280
+ const vy = ux;
281
+ const [left = 0, bottom = 0, right = 0, top = 0] = plan.bbox;
282
+ const corners = [
283
+ [left, bottom],
284
+ [left, top],
285
+ [right, bottom],
286
+ [right, top]
287
+ ];
288
+ const along = corners.map(([x = 0, y = 0]) => x * ux + y * uy);
289
+ const across = corners.map(([x = 0, y = 0]) => x * vx + y * vy);
290
+ const amin = Math.min(...along);
291
+ const amax = Math.max(...along);
292
+ const vmin = Math.min(...across);
293
+ const vmax = Math.max(...across);
294
+ const printStart = applyAffine(printMatrix, plan.color.axis.start.x, plan.color.axis.start.y);
295
+ const printEnd = applyAffine(printMatrix, plan.color.axis.end.x, plan.color.axis.end.y);
296
+ const printScale = Math.hypot(printEnd.x - printStart.x, printEnd.y - printStart.y) / length;
297
+ const rows = Math.ceil((amax - amin) * printScale / SHADING_SAMPLE_SPACING_PT) + 1;
298
+ if (rows > 1048576) return void 0;
299
+ const operations = rows * (plan.color.cost + plan.coverage.reduce((sum, coverage) => sum + (coverage.kind === "layer" ? coverage.layer.cost : 0), 0));
300
+ if ((budget.calculatorOperations ?? 0) + operations > 436207616) return void 0;
301
+ chargeShadingSamples(budget, rows, operations);
302
+ const rgb = new Uint8Array(rows * 3);
303
+ const gray = new Uint8Array(rows);
304
+ for (let row = 0; row < rows; row++) {
305
+ const distance = amin + (amax - amin) * row / Math.max(1, rows - 1);
306
+ const x = ux * distance + vx * ((vmin + vmax) / 2);
307
+ const y = uy * distance + vy * ((vmin + vmax) / 2);
308
+ const colorPosition = positionOnAxis(plan.color.axis, x, y);
309
+ const color = plan.color.evaluate(colorPosition);
310
+ rgb.set(color.map((value) => Math.round(clamp(value) * 255)), (rows - row - 1) * 3);
311
+ let alpha = 0;
312
+ for (const coverage of plan.coverage) {
313
+ let value = coverage.kind === "constant" ? coverage.value : coverage.layer.evaluate(positionOnAxis(coverage.layer.axis, x, y))[0] ?? 0;
314
+ if (coverage.kind === "layer" && coverage.transfer === "invert") value = 1 - value;
315
+ if (coverage.kind === "layer") value *= coverage.opacity;
316
+ alpha = alpha + clamp(value) * (1 - alpha);
317
+ }
318
+ gray[rows - row - 1] = Math.round(clamp(alpha * opacity) * 255);
319
+ }
320
+ const mask = document.context.register(document.context.stream(deflateSync(gray), {
321
+ Type: "XObject",
322
+ Subtype: "Image",
323
+ Width: 1,
324
+ Height: rows,
325
+ ColorSpace: "DeviceGray",
326
+ BitsPerComponent: 8,
327
+ Interpolate: true,
328
+ Filter: "FlateDecode"
329
+ }));
330
+ return {
331
+ image: document.context.register(document.context.stream(deflateSync(rgb), {
332
+ Type: "XObject",
333
+ Subtype: "Image",
334
+ Width: 1,
335
+ Height: rows,
336
+ ColorSpace: "DeviceRGB",
337
+ BitsPerComponent: 8,
338
+ Interpolate: true,
339
+ Filter: "FlateDecode",
340
+ SMask: mask,
341
+ [GENERATED_EFFECT_IMAGE_KEY]: true
342
+ })),
343
+ matrix: [
344
+ vx * (vmax - vmin),
345
+ vy * (vmax - vmin),
346
+ ux * (amax - amin),
347
+ uy * (amax - amin),
348
+ vx * vmin + ux * amin,
349
+ vy * vmin + uy * amin
350
+ ]
351
+ };
352
+ }
353
+ //#endregion
354
+ export { MAX_EFFECT_COVERAGE_LAYERS, buildRuleCImage, ruleCPlan };
@@ -0,0 +1,86 @@
1
+ import { dictionaryOf, nameOf } from "../press/pdf-objects.js";
2
+ import { PDFArray, PDFName, PDFNumber, PDFRef } from "pdf-lib";
3
+ //#region src/effects/structure.ts
4
+ const key = (name) => PDFName.of(name);
5
+ const resolved = (document, object) => object instanceof PDFRef ? document.context.lookup(object) : object;
6
+ const EMPTY_EFFECT_GRAPHICS_STATE = {
7
+ dictionaries: [],
8
+ structure: "allowed"
9
+ };
10
+ function finiteNumber(dictionary, label) {
11
+ const object = dictionary.get(key(label));
12
+ const value = object instanceof PDFRef ? dictionary.context.lookup(object) : object;
13
+ const number = value instanceof PDFNumber ? value.asNumber() : void 0;
14
+ return number !== void 0 && Number.isFinite(number) ? number : void 0;
15
+ }
16
+ function effectOpacity(graphicsState) {
17
+ if (!graphicsState?.has(key("ca"))) return 1;
18
+ return finiteNumber(graphicsState, "ca");
19
+ }
20
+ function effectGraphicsStateStructure(graphicsState) {
21
+ if (!graphicsState) return "allowed";
22
+ for (const [name] of graphicsState.entries()) {
23
+ const label = name.toString().slice(1);
24
+ if (label === "Type" || label === "SMask") continue;
25
+ if (label === "ca" && finiteNumber(graphicsState, label) !== void 0) continue;
26
+ if (label === "CA" && finiteNumber(graphicsState, label) === 1) continue;
27
+ if (label === "BM" && nameOf(graphicsState.lookup(key(label))) === "Normal") continue;
28
+ return "other";
29
+ }
30
+ return "allowed";
31
+ }
32
+ function advanceEffectGraphicsState(document, operation, states, current, stack) {
33
+ if (operation.operator === "q") {
34
+ stack.push(current);
35
+ return current;
36
+ }
37
+ if (operation.operator === "Q") return stack.pop() ?? EMPTY_EFFECT_GRAPHICS_STATE;
38
+ const operand = operation.operands[0];
39
+ if (operation.operator !== "gs") return current;
40
+ if (operation.operands.length !== 1 || operand?.kind !== "name") return {
41
+ ...current,
42
+ structure: "other"
43
+ };
44
+ const dictionary = dictionaryOf(resolved(document, states?.get(key(operand.raw.slice(1)))));
45
+ if (!dictionary) return {
46
+ ...current,
47
+ structure: "other"
48
+ };
49
+ return {
50
+ dictionaries: [...current.dictionaries, dictionary],
51
+ structure: current.structure === "other" || effectGraphicsStateStructure(dictionary) === "other" ? "other" : "allowed"
52
+ };
53
+ }
54
+ function effectGraphicsStateValue(state, label) {
55
+ for (let index = state.dictionaries.length - 1; index >= 0; index--) {
56
+ const dictionary = state.dictionaries[index];
57
+ if (dictionary?.has(key(label))) return dictionary.get(key(label));
58
+ }
59
+ }
60
+ function effectGraphicsStateOpacity(state) {
61
+ for (let index = state.dictionaries.length - 1; index >= 0; index--) {
62
+ const dictionary = state.dictionaries[index];
63
+ if (dictionary?.has(key("ca"))) return effectOpacity(dictionary);
64
+ }
65
+ return 1;
66
+ }
67
+ function effectFormStructure(form) {
68
+ if (!form.has(key("Matrix"))) return "allowed";
69
+ const matrix = form.lookupMaybe(key("Matrix"), PDFArray);
70
+ if (matrix?.size() !== 6) return "other";
71
+ const identity = [
72
+ 1,
73
+ 0,
74
+ 0,
75
+ 1,
76
+ 0,
77
+ 0
78
+ ];
79
+ for (let index = 0; index < identity.length; index++) {
80
+ const value = matrix.lookupMaybe(index, PDFNumber)?.asNumber();
81
+ if (value === void 0 || !Number.isFinite(value) || Math.abs(value - (identity[index] ?? 0)) > 1e-9) return "other";
82
+ }
83
+ return "allowed";
84
+ }
85
+ //#endregion
86
+ export { EMPTY_EFFECT_GRAPHICS_STATE, advanceEffectGraphicsState, effectFormStructure, effectGraphicsStateOpacity, effectGraphicsStateStructure, effectGraphicsStateValue, effectOpacity };
@@ -0,0 +1,15 @@
1
+ //#region src/effects/types.d.ts
2
+ /**
3
+ * Counts the implemented rewrites. Rule 2 is unbuilt, Rule 3 is left out
4
+ * because measurements show it is ineffective, and Rule 4 is unbuilt and
5
+ * will be opt-in and off by default when implemented.
6
+ */
7
+ export interface EffectsReport {
8
+ /** Luminosity masks rewritten from a grayscale image group to a direct image alpha mask. */
9
+ rule1: number;
10
+ /** Nested one-axis shading composites rewritten as an interpolated image and direct alpha mask. */
11
+ ruleC: number;
12
+ /** Soft masks left in the form Chromium wrote. */
13
+ kept: number;
14
+ }
15
+ //#endregion
@@ -0,0 +1,4 @@
1
+ //#region src/effects/types.ts
2
+ const GENERATED_EFFECT_IMAGE_KEY = "SegnaviaGeneratedEffect";
3
+ //#endregion
4
+ export { GENERATED_EFFECT_IMAGE_KEY };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { EffectsReport } from "./effects/types.js";
1
2
  import { PressImage, PressReport } from "./press/types.js";
2
3
  import { RenderRefusedError, RendererUnavailableError } from "./errors.js";
3
4
  import { Deck, ImpositionInput, PageInput } from "@segnavia/format";
@@ -29,6 +30,9 @@ export interface RenderOptions {
29
30
  waitForPageReady?: boolean;
30
31
  preferCssPageSize?: boolean;
31
32
  beforeOutput?: (tab: Page$1) => Promise<void>;
33
+ effects?: false | {
34
+ report?: (report: EffectsReport) => void;
35
+ };
32
36
  press?: {
33
37
  /** The ICC profile of the press, as bytes. The library ships none. */
34
38
  profile: Uint8Array;
@@ -62,4 +66,4 @@ export declare function fontFaces(fonts?: Font[]): string;
62
66
  export declare function captureImage(tab: Page$1, options: Pick<ImageOptions, 'format' | 'quality' | 'limits'>, fullPage: boolean): Promise<Uint8Array>;
63
67
  export declare function openRenderer(options: RendererOptions): Promise<Renderer>;
64
68
  //#endregion
65
- export { type PressImage, type PressReport, RenderRefusedError, RendererUnavailableError };
69
+ export { type EffectsReport, type PressImage, type PressReport, RenderRefusedError, RendererUnavailableError };
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { RenderRefusedError, RendererUnavailableError } from "./errors.js";
2
+ import { containFixedPageOverflow } from "./overflow-containment.js";
2
3
  import { renderDeckPdf } from "./deck-pdf.js";
4
+ import { rewriteEffectsPdf } from "./effects/adapter.js";
3
5
  import { assertPrintOptions, printCss, printPage, printViewport } from "./page-paper.js";
4
6
  import { renderPagesPdf } from "./pages-pdf.js";
5
7
  import { rewritePressPdf } from "./press/adapter.js";
@@ -52,11 +54,19 @@ async function enforce(pdf, limits) {
52
54
  return pdf;
53
55
  }
54
56
  async function finishPdf(pdf, options) {
55
- if (!options.press) return enforce(pdf, options.limits);
56
57
  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);
58
+ let output = pdf;
59
+ if (options.effects !== false) {
60
+ const rewritten = await rewriteEffectsPdf(output);
61
+ output = rewritten.bytes;
62
+ options.effects?.report?.(rewritten.report);
63
+ }
64
+ if (options.press) {
65
+ const converted = await rewritePressPdf(output, options.press.profile);
66
+ output = converted.bytes;
67
+ options.press.report?.(converted.report);
68
+ }
69
+ return enforce(output, options.limits);
60
70
  }
61
71
  async function captureImage(tab, options, fullPage) {
62
72
  const format = options.format ?? "png";
@@ -178,7 +188,9 @@ async function openRenderer(options) {
178
188
  }
179
189
  }
180
190
  function pageDocument(page, options, printing = false) {
181
- const css = `${page.print ? printing ? printCss(page.print) : printProperties(page.print) : ""}${fontFaces(options.fonts)}${options.css ?? ""}`;
191
+ const hostCss = `${page.print ? printing ? printCss(page.print) : printProperties(page.print) : ""}${fontFaces(options.fonts)}${options.css ?? ""}`;
192
+ const fixedSize = typeof page.size === "object" && page.size.height !== "auto";
193
+ const css = printing && !page.print && fixedSize ? containFixedPageOverflow(hostCss) : hostCss;
182
194
  return frameDocument(page.html, {
183
195
  scripts: page.scripts,
184
196
  nonce: newNonce(),
@@ -229,7 +241,7 @@ async function openRenderer(options) {
229
241
  nonce: newNonce(),
230
242
  dir: page.dir,
231
243
  lang: page.lang,
232
- css,
244
+ css: containFixedPageOverflow(css),
233
245
  relayKeys: false,
234
246
  reportHeight: false
235
247
  });
@@ -0,0 +1,7 @@
1
+ //#region src/overflow-containment.ts
2
+ const CSS = ":where(:root){contain:paint;min-height:100%}";
3
+ function containFixedPageOverflow(css) {
4
+ return `${CSS}${css}`;
5
+ }
6
+ //#endregion
7
+ export { containFixedPageOverflow };
package/dist/pages-pdf.js CHANGED
@@ -53,6 +53,7 @@ async function renderPagesPdf(inputs, opts, services) {
53
53
  if (!opts.imposition) {
54
54
  const rendered = await Promise.all(pages.map((page) => services.pagePdf(page, {
55
55
  ...opts,
56
+ effects: false,
56
57
  limits: void 0,
57
58
  press: void 0
58
59
  })));