@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.
- package/dist/color/profile.js +1 -1
- package/dist/color/tables.js +1 -1
- package/dist/deck-pdf.js +4 -2
- package/dist/effects/adapter.js +336 -0
- package/dist/effects/content-replacement.js +35 -0
- package/dist/effects/function-evaluator.js +72 -0
- package/dist/effects/policy.js +10 -0
- package/dist/effects/rule-1.js +40 -0
- package/dist/effects/rule-c.js +354 -0
- package/dist/effects/structure.js +86 -0
- package/dist/effects/types.d.ts +15 -0
- package/dist/effects/types.js +4 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +18 -6
- package/dist/overflow-containment.js +7 -0
- package/dist/pages-pdf.js +1 -0
- package/dist/press/adapter.js +141 -11
- package/dist/press/affine.js +60 -0
- package/dist/press/calculator-ops.js +277 -0
- package/dist/press/calculator.js +130 -0
- package/dist/press/content.js +24 -1
- package/dist/press/pattern-bounds.js +275 -0
- package/dist/press/pattern-path.js +102 -0
- package/dist/press/policy.js +7 -1
- package/dist/press/report.js +13 -3
- package/dist/press/sample-shading.js +266 -0
- package/dist/press/types.d.ts +2 -0
- package/dist/press/visitor.js +55 -32
- package/package.json +3 -3
package/dist/color/profile.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { REVERSE_LUT, checked, refuse } from "./types.js";
|
|
2
|
-
import { fixed, need, signature, u32 } from "./binary.js";
|
|
3
2
|
import { destinationBlackPoint, detectSourceBlackPoint } from "./black-point.js";
|
|
3
|
+
import { fixed, need, signature, u32 } from "./binary.js";
|
|
4
4
|
import { matrixTables, parseMab, parseMft } from "./tables.js";
|
|
5
5
|
//#region src/color/profile.ts
|
|
6
6
|
const MAX_PROFILE_BYTES = 33554432;
|
package/dist/color/tables.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { REVERSE_LUT, checked, clamp, refuse } from "./types.js";
|
|
2
|
-
import { align4, fixed, need, signature, u16, u32 } from "./binary.js";
|
|
3
2
|
import { invert3, matrix3 } from "./spaces.js";
|
|
3
|
+
import { align4, fixed, need, signature, u16, u32 } from "./binary.js";
|
|
4
4
|
import { interpolate, matrixInput, multilinear, tetrahedral4 } from "./interpolation.js";
|
|
5
5
|
//#region src/color/tables.ts
|
|
6
6
|
function packed(bytes, view, offset, length, width, bounds) {
|
package/dist/deck-pdf.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RenderRefusedError } from "./errors.js";
|
|
2
|
+
import { containFixedPageOverflow } from "./overflow-containment.js";
|
|
2
3
|
import { pdfPixelBox } from "./pdf-geometry.js";
|
|
3
4
|
import { composeImposedPdf, drawClippedPage, embedOnePage } from "./pdf-compose.js";
|
|
4
5
|
import { Imposition, fitPage, impose, pagePlacementLayout } from "@segnavia/format";
|
|
@@ -20,6 +21,7 @@ async function regularDeckPdf(deck, opts, services) {
|
|
|
20
21
|
html: slide.content.html
|
|
21
22
|
}] : []);
|
|
22
23
|
const css = `${services.fontFaces(opts)}${opts.css ?? ""}`;
|
|
24
|
+
const slideCss = containFixedPageOverflow(css);
|
|
23
25
|
const notesGeometry = opts.layout === "notes" ? notesLayoutGeometry(deck.size, opts.notesPaper) : void 0;
|
|
24
26
|
const [printable, rendered] = await Promise.all([services.renderPdf({
|
|
25
27
|
viewport: notesGeometry ? {
|
|
@@ -46,7 +48,7 @@ async function regularDeckPdf(deck, opts, services) {
|
|
|
46
48
|
scripts: deck.scripts === "sandboxed",
|
|
47
49
|
html: htmlSlideDocument(deck, html, {
|
|
48
50
|
nonce: newNonce(),
|
|
49
|
-
css
|
|
51
|
+
css: slideCss
|
|
50
52
|
}),
|
|
51
53
|
boundMs: opts.boundMs
|
|
52
54
|
}, {
|
|
@@ -84,7 +86,7 @@ async function regularDeckPdf(deck, opts, services) {
|
|
|
84
86
|
return services.finish(await output.save(), opts);
|
|
85
87
|
}
|
|
86
88
|
async function imposedDeckPdf(deck, imposition, opts, services) {
|
|
87
|
-
const css = `${services.fontFaces(opts)}${opts.css ?? ""}
|
|
89
|
+
const css = containFixedPageOverflow(`${services.fontFaces(opts)}${opts.css ?? ""}`);
|
|
88
90
|
const imposed = impose(imposition, deck.slides.length);
|
|
89
91
|
const rendered = await Promise.all(deck.slides.map(async (slide, index) => {
|
|
90
92
|
const single = {
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { RenderRefusedError } from "../errors.js";
|
|
2
|
+
import { IDENTITY_MATRIX, multiplyAffine } from "../press/affine.js";
|
|
3
|
+
import { visitContentOperations } from "../press/content.js";
|
|
4
|
+
import { decodeContent, dictionaryOf, nameOf, resolvedColorSpace } from "../press/pdf-objects.js";
|
|
5
|
+
import { walkPressGraph } from "../press/visitor.js";
|
|
6
|
+
import { replaceRanges, streamDictionary } from "./content-replacement.js";
|
|
7
|
+
import { softMaskAction } from "./policy.js";
|
|
8
|
+
import { rule1EffectImage } from "./rule-1.js";
|
|
9
|
+
import { EMPTY_EFFECT_GRAPHICS_STATE, advanceEffectGraphicsState, effectFormStructure, effectGraphicsStateOpacity, effectGraphicsStateStructure, effectOpacity } from "./structure.js";
|
|
10
|
+
import { buildRuleCImage, ruleCPlan } from "./rule-c.js";
|
|
11
|
+
import { PDFArray, PDFDict, PDFDocument, PDFName, PDFNumber, PDFRawStream, PDFRef } from "pdf-lib";
|
|
12
|
+
import { deflateSync } from "node:zlib";
|
|
13
|
+
//#region src/effects/adapter.ts
|
|
14
|
+
const key = (name) => PDFName.of(name);
|
|
15
|
+
const resolved = (document, object) => object instanceof PDFRef ? document.context.lookup(object) : object;
|
|
16
|
+
function numbers(array) {
|
|
17
|
+
if (!array) return void 0;
|
|
18
|
+
const values = [];
|
|
19
|
+
for (let index = 0; index < array.size(); index++) {
|
|
20
|
+
const value = array.lookupMaybe(index, PDFNumber)?.asNumber();
|
|
21
|
+
if (value === void 0) return void 0;
|
|
22
|
+
values.push(value);
|
|
23
|
+
}
|
|
24
|
+
return values;
|
|
25
|
+
}
|
|
26
|
+
function blackBackdrop(mask) {
|
|
27
|
+
const backdrop = numbers(mask.lookupMaybe(key("BC"), PDFArray));
|
|
28
|
+
return backdrop === void 0 || backdrop.length === 1 && backdrop[0] === 0;
|
|
29
|
+
}
|
|
30
|
+
function hasNestedMask(document, resources) {
|
|
31
|
+
const states = resources?.lookupMaybe(key("ExtGState"), PDFDict);
|
|
32
|
+
for (const [, object] of states?.entries() ?? []) {
|
|
33
|
+
const state = dictionaryOf(resolved(document, object));
|
|
34
|
+
if (dictionaryOf(resolved(document, state?.get(key("SMask"))))) return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
function groupContents(document, source, resources, budget) {
|
|
39
|
+
const operations = [];
|
|
40
|
+
visitContentOperations(decodeContent(source, budget), (operation) => operations.push(operation));
|
|
41
|
+
const states = resources?.lookupMaybe(key("ExtGState"), PDFDict);
|
|
42
|
+
const stack = [];
|
|
43
|
+
let graphicsState = EMPTY_EFFECT_GRAPHICS_STATE;
|
|
44
|
+
let structure = "allowed";
|
|
45
|
+
let paintState = EMPTY_EFFECT_GRAPHICS_STATE;
|
|
46
|
+
for (const operation of operations) {
|
|
47
|
+
graphicsState = advanceEffectGraphicsState(document, operation, states, graphicsState, stack);
|
|
48
|
+
if (graphicsState.structure === "other") structure = "other";
|
|
49
|
+
if (operation.operator === "Do") paintState = graphicsState;
|
|
50
|
+
}
|
|
51
|
+
const paints = operations.filter(({ operator }) => [
|
|
52
|
+
"Do",
|
|
53
|
+
"sh",
|
|
54
|
+
"f",
|
|
55
|
+
"f*",
|
|
56
|
+
"F",
|
|
57
|
+
"S",
|
|
58
|
+
"B",
|
|
59
|
+
"B*",
|
|
60
|
+
"b",
|
|
61
|
+
"b*"
|
|
62
|
+
].includes(operator));
|
|
63
|
+
if (paints.length !== 1 || paints[0]?.operator !== "Do" || paints[0].operands.length !== 1 || paints[0].operands[0]?.kind !== "name") return {
|
|
64
|
+
kind: paints.some(({ operator }) => operator !== "Do") ? "vector" : "other",
|
|
65
|
+
structure: "other"
|
|
66
|
+
};
|
|
67
|
+
const xObjects = resources?.lookupMaybe(key("XObject"), PDFDict);
|
|
68
|
+
const image = resolved(document, xObjects?.get(PDFName.of(paints[0].operands[0].raw.slice(1))));
|
|
69
|
+
if (!(image instanceof PDFRawStream) || nameOf(image.dict.get(key("Subtype"))) !== "Image") return {
|
|
70
|
+
kind: "vector",
|
|
71
|
+
structure: "other"
|
|
72
|
+
};
|
|
73
|
+
if (resolvedColorSpace(document, resolved(document, image.dict.get(key("ColorSpace")))).kind !== "gray" || image.dict.has(key("SMask"))) return {
|
|
74
|
+
kind: "other",
|
|
75
|
+
structure: "other"
|
|
76
|
+
};
|
|
77
|
+
if (effectGraphicsStateOpacity(paintState) !== 1) structure = "other";
|
|
78
|
+
return {
|
|
79
|
+
image,
|
|
80
|
+
kind: "single-image",
|
|
81
|
+
structure
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function classifyMask(state, graphicsState, mask, source, resources) {
|
|
85
|
+
state.masks.add(mask);
|
|
86
|
+
const group = source.dict.lookupMaybe(key("Group"), PDFDict);
|
|
87
|
+
const contents = groupContents(state.document, source, resources, state.budget);
|
|
88
|
+
const image = contents.image;
|
|
89
|
+
const nested = hasNestedMask(state.document, resources);
|
|
90
|
+
const opacity = effectOpacity(graphicsState);
|
|
91
|
+
const structure = rule1Structure(state.document, graphicsState, mask, source, contents.structure, nested);
|
|
92
|
+
const candidate = {
|
|
93
|
+
backdrop: blackBackdrop(mask) ? "black" : "other",
|
|
94
|
+
colorSpace: group && resolvedColorSpace(state.document, resolved(state.document, group.get(key("CS")))).kind === "gray" ? "gray" : "other",
|
|
95
|
+
contents: contents.kind,
|
|
96
|
+
structure,
|
|
97
|
+
subtype: nameOf(mask.get(key("S"))) === "Luminosity" ? "luminosity" : "other"
|
|
98
|
+
};
|
|
99
|
+
const bbox = numbers(source.dict.lookupMaybe(key("BBox"), PDFArray));
|
|
100
|
+
const plan = image && bbox?.length === 4 && opacity !== void 0 ? {
|
|
101
|
+
bbox,
|
|
102
|
+
image,
|
|
103
|
+
mask,
|
|
104
|
+
opacity
|
|
105
|
+
} : void 0;
|
|
106
|
+
state.rule1Candidates.set(graphicsState, {
|
|
107
|
+
...candidate,
|
|
108
|
+
plan
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function rule1Structure(document, graphicsState, mask, source, contents, nested) {
|
|
112
|
+
return (!mask.has(key("TR")) || nameOf(resolved(document, mask.get(key("TR")))) === "Identity") && !nested && contents === "allowed" && effectGraphicsStateStructure(graphicsState) === "allowed" && effectFormStructure(source.dict) === "allowed" ? "allowed" : "other";
|
|
113
|
+
}
|
|
114
|
+
function finiteNumberEntry(dictionary, label) {
|
|
115
|
+
const object = dictionary.get(key(label));
|
|
116
|
+
const value = object instanceof PDFRef ? dictionary.context.lookup(object) : object;
|
|
117
|
+
const number = value instanceof PDFNumber ? value.asNumber() : void 0;
|
|
118
|
+
return number !== void 0 && Number.isFinite(number) ? number : void 0;
|
|
119
|
+
}
|
|
120
|
+
function numericOperands(operation, count) {
|
|
121
|
+
if (operation.operands.length !== count || operation.operands.some(({ kind }) => kind !== "number")) return void 0;
|
|
122
|
+
return operation.operands.map(({ raw }) => Number(raw));
|
|
123
|
+
}
|
|
124
|
+
const close = (left, right) => Math.abs(left - right) <= .01;
|
|
125
|
+
function matchingRectangle(operation, bbox) {
|
|
126
|
+
const rectangle = numericOperands(operation, 4);
|
|
127
|
+
if (!rectangle) return false;
|
|
128
|
+
const [x = 0, y = 0, width = 0, height = 0] = rectangle;
|
|
129
|
+
return close(x, bbox[0] ?? 0) && close(y, bbox[1] ?? 0) && close(width, (bbox[2] ?? 0) - (bbox[0] ?? 0)) && close(height, (bbox[3] ?? 0) - (bbox[1] ?? 0));
|
|
130
|
+
}
|
|
131
|
+
function colorFrom(operation) {
|
|
132
|
+
const definition = {
|
|
133
|
+
g: ["DeviceGray", 1],
|
|
134
|
+
rg: ["DeviceRGB", 3],
|
|
135
|
+
k: ["DeviceCMYK", 4]
|
|
136
|
+
}[operation.operator];
|
|
137
|
+
if (!definition) return void 0;
|
|
138
|
+
const values = numericOperands(operation, definition[1]);
|
|
139
|
+
return values ? {
|
|
140
|
+
space: definition[0],
|
|
141
|
+
values
|
|
142
|
+
} : void 0;
|
|
143
|
+
}
|
|
144
|
+
function xObjectName(resources, image) {
|
|
145
|
+
let xObjects = resources.lookupMaybe(key("XObject"), PDFDict);
|
|
146
|
+
if (!xObjects) {
|
|
147
|
+
xObjects = resources.context.obj({});
|
|
148
|
+
resources.set(key("XObject"), xObjects);
|
|
149
|
+
}
|
|
150
|
+
let index = 1;
|
|
151
|
+
while (xObjects.has(key(`SegnaviaEffect${index}`))) index++;
|
|
152
|
+
const name = `SegnaviaEffect${index}`;
|
|
153
|
+
xObjects.set(key(name), image);
|
|
154
|
+
return name;
|
|
155
|
+
}
|
|
156
|
+
function opacityGraphicsStateName(resources, opacity) {
|
|
157
|
+
if (opacity === 1) return void 0;
|
|
158
|
+
let states = resources.lookupMaybe(key("ExtGState"), PDFDict);
|
|
159
|
+
if (!states) {
|
|
160
|
+
states = resources.context.obj({});
|
|
161
|
+
resources.set(key("ExtGState"), states);
|
|
162
|
+
}
|
|
163
|
+
let index = 1;
|
|
164
|
+
while (states.has(key(`SegnaviaEffectOpacity${index}`))) index++;
|
|
165
|
+
const name = `SegnaviaEffectOpacity${index}`;
|
|
166
|
+
states.set(key(name), resources.context.obj({
|
|
167
|
+
Type: "ExtGState",
|
|
168
|
+
ca: opacity
|
|
169
|
+
}));
|
|
170
|
+
return name;
|
|
171
|
+
}
|
|
172
|
+
function ruleCCandidate(state, next, graphicsState, resources) {
|
|
173
|
+
if (!graphicsState || next?.operator !== "Do" || next.operands.length !== 1 || next.operands[0]?.kind !== "name") return void 0;
|
|
174
|
+
const xObjects = resources.lookupMaybe(key("XObject"), PDFDict);
|
|
175
|
+
const form = resolved(state.document, xObjects?.get(PDFName.of(next.operands[0].raw.slice(1))));
|
|
176
|
+
if (!(form instanceof PDFRawStream) || nameOf(form.dict.get(key("Subtype"))) !== "Form") return void 0;
|
|
177
|
+
return ruleCPlan(state.document, form, graphicsState, state.budget);
|
|
178
|
+
}
|
|
179
|
+
function ruleCReplacement(state, next, graphicsState, resources, printMatrix, plan) {
|
|
180
|
+
const opacity = graphicsState.has(key("ca")) ? finiteNumberEntry(graphicsState, "ca") : 1;
|
|
181
|
+
const built = opacity !== void 0 ? buildRuleCImage(state.document, plan, opacity, state.budget, printMatrix) : void 0;
|
|
182
|
+
if (!built) return void 0;
|
|
183
|
+
const name = xObjectName(resources, built.image);
|
|
184
|
+
const [left = 0, bottom = 0, right = 0, top = 0] = plan.bbox;
|
|
185
|
+
for (const mask of plan.masks) state.ruleCMasks.add(mask);
|
|
186
|
+
state.appliedRuleC++;
|
|
187
|
+
return {
|
|
188
|
+
end: next.end,
|
|
189
|
+
text: `q ${left} ${bottom} ${right - left} ${top - bottom} re W n ${built.matrix.join(" ")} cm /${name} Do Q`
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function namedOperand(operation, operator) {
|
|
193
|
+
const operand = operation.operands[0];
|
|
194
|
+
return operation.operator === operator && operation.operands.length === 1 && operand?.kind === "name" ? operand : void 0;
|
|
195
|
+
}
|
|
196
|
+
function advanceGraphics(operation, current, stack) {
|
|
197
|
+
if (operation.operator === "q") {
|
|
198
|
+
stack.push(current);
|
|
199
|
+
return current;
|
|
200
|
+
}
|
|
201
|
+
if (operation.operator === "Q") return stack.pop() ?? {
|
|
202
|
+
color: void 0,
|
|
203
|
+
matrix: IDENTITY_MATRIX
|
|
204
|
+
};
|
|
205
|
+
if (operation.operator === "cm") return {
|
|
206
|
+
...current,
|
|
207
|
+
matrix: multiplyAffine(current.matrix, numericOperands(operation, 6) ?? IDENTITY_MATRIX)
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
...current,
|
|
211
|
+
color: colorFrom(operation) ?? current.color
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function effectReplacement(state, next, following, graphicsState, resources, graphics) {
|
|
215
|
+
const ruleC = ruleCCandidate(state, next, graphicsState, resources);
|
|
216
|
+
const rule1 = state.rule1Candidates.get(graphicsState);
|
|
217
|
+
if (!rule1 && !ruleC?.nested) return void 0;
|
|
218
|
+
const facts = {
|
|
219
|
+
backdrop: rule1?.backdrop ?? "other",
|
|
220
|
+
colorSpace: rule1?.colorSpace ?? "other",
|
|
221
|
+
contents: rule1?.contents ?? "other",
|
|
222
|
+
nested: ruleC?.nested ?? false,
|
|
223
|
+
oneAxis: ruleC?.nested ? ruleC.oneAxis : "unknown",
|
|
224
|
+
structure: ruleC?.nested ? ruleC.structure : rule1?.structure ?? "other",
|
|
225
|
+
subtype: rule1?.subtype ?? "other"
|
|
226
|
+
};
|
|
227
|
+
const decision = softMaskAction(facts);
|
|
228
|
+
if (decision === "ruleC" && ruleC?.plan && next) {
|
|
229
|
+
const replacement = ruleCReplacement(state, next, graphicsState, resources, graphics.matrix, ruleC.plan);
|
|
230
|
+
return replacement ? {
|
|
231
|
+
consumed: 1,
|
|
232
|
+
...replacement
|
|
233
|
+
} : void 0;
|
|
234
|
+
}
|
|
235
|
+
const plan = rule1?.plan;
|
|
236
|
+
if (decision !== "rule1" || !plan || !graphics.color || !next || !following || next.operator !== "re" || ![
|
|
237
|
+
"f",
|
|
238
|
+
"f*",
|
|
239
|
+
"F"
|
|
240
|
+
].includes(following.operator) || !matchingRectangle(next, plan.bbox)) return void 0;
|
|
241
|
+
const name = xObjectName(resources, rule1EffectImage(state.document, state.budget, plan, graphics.color));
|
|
242
|
+
const opacityName = opacityGraphicsStateName(resources, plan.opacity);
|
|
243
|
+
const [left = 0, bottom = 0, right = 0, top = 0] = plan.bbox;
|
|
244
|
+
state.applied.add(plan.mask);
|
|
245
|
+
const opacity = opacityName ? `/${opacityName} gs ` : "";
|
|
246
|
+
return {
|
|
247
|
+
consumed: 2,
|
|
248
|
+
end: following.end,
|
|
249
|
+
text: `q ${opacity}${right - left} 0 0 ${bottom - top} ${left} ${top} cm /${name} Do Q`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function rewriteContent(state, object, stream, setDirect, resources) {
|
|
253
|
+
const bytes = decodeContent(stream, state.budget);
|
|
254
|
+
if (!resources) return;
|
|
255
|
+
const operations = [];
|
|
256
|
+
visitContentOperations(bytes, (operation) => operations.push(operation));
|
|
257
|
+
const graphicsStates = resources.lookupMaybe(key("ExtGState"), PDFDict);
|
|
258
|
+
const replacements = [];
|
|
259
|
+
const stack = [];
|
|
260
|
+
let graphics = {
|
|
261
|
+
color: void 0,
|
|
262
|
+
matrix: IDENTITY_MATRIX
|
|
263
|
+
};
|
|
264
|
+
for (let index = 0; index < operations.length; index++) {
|
|
265
|
+
const operation = operations[index];
|
|
266
|
+
if (!operation) continue;
|
|
267
|
+
graphics = advanceGraphics(operation, graphics, stack);
|
|
268
|
+
const operand = namedOperand(operation, "gs");
|
|
269
|
+
if (!operand) continue;
|
|
270
|
+
const graphicsState = dictionaryOf(resolved(state.document, graphicsStates?.get(PDFName.of(operand.raw.slice(1)))));
|
|
271
|
+
if (!graphicsState) continue;
|
|
272
|
+
const replacement = effectReplacement(state, operations[index + 1], operations[index + 2], graphicsState, resources, graphics);
|
|
273
|
+
if (!replacement) continue;
|
|
274
|
+
replacements.push({
|
|
275
|
+
start: operand.start,
|
|
276
|
+
...replacement
|
|
277
|
+
});
|
|
278
|
+
index += replacement.consumed;
|
|
279
|
+
}
|
|
280
|
+
if (replacements.length === 0) return;
|
|
281
|
+
const replacement = state.document.context.stream(deflateSync(replaceRanges(bytes, replacements)), streamDictionary(stream));
|
|
282
|
+
const reference = object instanceof PDFRef ? object : state.document.context.getObjectRef(object);
|
|
283
|
+
if (reference) state.document.context.assign(reference, replacement);
|
|
284
|
+
else setDirect(replacement);
|
|
285
|
+
}
|
|
286
|
+
function rewriteEffectsDocument(document) {
|
|
287
|
+
const state = {
|
|
288
|
+
applied: /* @__PURE__ */ new WeakSet(),
|
|
289
|
+
appliedRuleC: 0,
|
|
290
|
+
budget: { decodedBytes: 0 },
|
|
291
|
+
document,
|
|
292
|
+
masks: /* @__PURE__ */ new Set(),
|
|
293
|
+
rule1Candidates: /* @__PURE__ */ new WeakMap(),
|
|
294
|
+
ruleCMasks: /* @__PURE__ */ new WeakSet()
|
|
295
|
+
};
|
|
296
|
+
walkPressGraph(document, {
|
|
297
|
+
content: () => void 0,
|
|
298
|
+
function: () => void 0,
|
|
299
|
+
group: () => void 0,
|
|
300
|
+
image: () => void 0,
|
|
301
|
+
shading: () => false,
|
|
302
|
+
softMask: (graphicsState, mask, source, resources) => classifyMask(state, graphicsState, mask, source, resources)
|
|
303
|
+
});
|
|
304
|
+
state.budget = { decodedBytes: 0 };
|
|
305
|
+
walkPressGraph(document, {
|
|
306
|
+
content: (object, stream, replace, resources) => rewriteContent(state, object, stream, replace, resources),
|
|
307
|
+
function: () => void 0,
|
|
308
|
+
group: () => void 0,
|
|
309
|
+
image: () => void 0,
|
|
310
|
+
shading: () => false
|
|
311
|
+
});
|
|
312
|
+
let rule1 = 0;
|
|
313
|
+
for (const mask of state.masks) if (state.applied.has(mask)) rule1++;
|
|
314
|
+
let kept = 0;
|
|
315
|
+
for (const mask of state.masks) if (!state.applied.has(mask) && !state.ruleCMasks.has(mask)) kept++;
|
|
316
|
+
return {
|
|
317
|
+
rule1,
|
|
318
|
+
ruleC: state.appliedRuleC,
|
|
319
|
+
kept
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
async function rewriteEffectsPdf(bytes) {
|
|
323
|
+
try {
|
|
324
|
+
const document = await PDFDocument.load(bytes, { updateMetadata: false });
|
|
325
|
+
const report = rewriteEffectsDocument(document);
|
|
326
|
+
return {
|
|
327
|
+
bytes: report.rule1 === 0 && report.ruleC === 0 ? bytes : await document.save(),
|
|
328
|
+
report
|
|
329
|
+
};
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (error instanceof RenderRefusedError) throw error;
|
|
332
|
+
throw new RenderRefusedError("The PDF has unsupported malformed structure; supported: a valid PDF produced by the renderer.");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
336
|
+
export { rewriteEffectsDocument, rewriteEffectsPdf };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { PDFName } from "pdf-lib";
|
|
2
|
+
//#region src/effects/content-replacement.ts
|
|
3
|
+
const key = (name) => PDFName.of(name);
|
|
4
|
+
function streamDictionary(stream) {
|
|
5
|
+
const dictionary = { Filter: key("FlateDecode") };
|
|
6
|
+
for (const [name, value] of stream.dict.entries()) {
|
|
7
|
+
const label = name.toString().slice(1);
|
|
8
|
+
if (![
|
|
9
|
+
"Filter",
|
|
10
|
+
"DecodeParms",
|
|
11
|
+
"Length"
|
|
12
|
+
].includes(label)) dictionary[label] = value;
|
|
13
|
+
}
|
|
14
|
+
return dictionary;
|
|
15
|
+
}
|
|
16
|
+
function replaceRanges(bytes, replacements) {
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
let length = bytes.length;
|
|
19
|
+
for (const item of replacements) length += encoder.encode(item.text).length - (item.end - item.start);
|
|
20
|
+
const output = new Uint8Array(length);
|
|
21
|
+
let inputAt = 0;
|
|
22
|
+
let outputAt = 0;
|
|
23
|
+
for (const item of replacements) {
|
|
24
|
+
output.set(bytes.subarray(inputAt, item.start), outputAt);
|
|
25
|
+
outputAt += item.start - inputAt;
|
|
26
|
+
const inserted = encoder.encode(item.text);
|
|
27
|
+
output.set(inserted, outputAt);
|
|
28
|
+
outputAt += inserted.length;
|
|
29
|
+
inputAt = item.end;
|
|
30
|
+
}
|
|
31
|
+
output.set(bytes.subarray(inputAt), outputAt);
|
|
32
|
+
return output;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { replaceRanges, streamDictionary };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { dictionaryOf, numberOf } from "../press/pdf-objects.js";
|
|
2
|
+
import { PDFArray, PDFName, PDFNumber, PDFRef } from "pdf-lib";
|
|
3
|
+
const MAX_EFFECT_FUNCTION_CHILDREN = 4096;
|
|
4
|
+
const key = (name) => PDFName.of(name);
|
|
5
|
+
const resolved = (document, object) => object instanceof PDFRef ? document.context.lookup(object) : object;
|
|
6
|
+
function numbers(dictionary, label) {
|
|
7
|
+
const array = dictionary.lookupMaybe(key(label), PDFArray);
|
|
8
|
+
if (!array) return void 0;
|
|
9
|
+
const values = [];
|
|
10
|
+
for (let index = 0; index < array.size(); index++) {
|
|
11
|
+
const value = array.lookupMaybe(index, PDFNumber)?.asNumber();
|
|
12
|
+
if (value === void 0 || !Number.isFinite(value)) return void 0;
|
|
13
|
+
values.push(value);
|
|
14
|
+
}
|
|
15
|
+
return values;
|
|
16
|
+
}
|
|
17
|
+
const clamp = (value) => Math.max(0, Math.min(1, value));
|
|
18
|
+
function exponentialEvaluator(fn, channels, domain) {
|
|
19
|
+
const from = numbers(fn, "C0") ?? [0];
|
|
20
|
+
const to = numbers(fn, "C1") ?? [1];
|
|
21
|
+
const exponent = numberOf(fn, "N");
|
|
22
|
+
if (from.length !== channels || to.length !== channels || exponent === void 0) return void 0;
|
|
23
|
+
return Object.assign((input) => {
|
|
24
|
+
const value = clamp((input - (domain[0] ?? 0)) / ((domain[1] ?? 1) - (domain[0] ?? 0))) ** exponent;
|
|
25
|
+
return from.map((start, index) => start + ((to[index] ?? start) - start) * value);
|
|
26
|
+
}, { cost: 1 });
|
|
27
|
+
}
|
|
28
|
+
function stitchingEvaluator(context, fn, domain, depth) {
|
|
29
|
+
const functions = fn.lookupMaybe(key("Functions"), PDFArray);
|
|
30
|
+
const bounds = numbers(fn, "Bounds") ?? [];
|
|
31
|
+
const encode = numbers(fn, "Encode") ?? [];
|
|
32
|
+
if (!functions || functions.size() > MAX_EFFECT_FUNCTION_CHILDREN || functions.size() !== bounds.length + 1 || encode.length !== functions.size() * 2) return void 0;
|
|
33
|
+
context.path.add(fn);
|
|
34
|
+
const children = [];
|
|
35
|
+
for (let index = 0; index < functions.size(); index++) children.push(buildEvaluator(context, functions.get(index), depth + 1));
|
|
36
|
+
context.path.delete(fn);
|
|
37
|
+
if (children.some((child) => !child)) return void 0;
|
|
38
|
+
const evaluators = children;
|
|
39
|
+
const cost = bounds.length + evaluators.reduce((sum, child) => sum + child.cost, 0);
|
|
40
|
+
return Object.assign((input) => {
|
|
41
|
+
const value = Math.max(domain[0] ?? 0, Math.min(domain[1] ?? 1, input));
|
|
42
|
+
let index = bounds.findIndex((bound) => value < bound);
|
|
43
|
+
if (index < 0) index = bounds.length;
|
|
44
|
+
const low = index === 0 ? domain[0] ?? 0 : bounds[index - 1] ?? 0;
|
|
45
|
+
const high = index === bounds.length ? domain[1] ?? 1 : bounds[index] ?? 1;
|
|
46
|
+
const encodedLow = encode[index * 2] ?? 0;
|
|
47
|
+
const encodedHigh = encode[index * 2 + 1] ?? 1;
|
|
48
|
+
const mapped = high === low ? encodedLow : encodedLow + (value - low) / (high - low) * (encodedHigh - encodedLow);
|
|
49
|
+
return evaluators[index](mapped);
|
|
50
|
+
}, { cost });
|
|
51
|
+
}
|
|
52
|
+
function buildEvaluator(context, object, depth) {
|
|
53
|
+
const fn = dictionaryOf(resolved(context.document, object));
|
|
54
|
+
if (!fn || depth > 12 || context.path.has(fn)) return void 0;
|
|
55
|
+
if (context.memo.has(fn)) return context.memo.get(fn);
|
|
56
|
+
const domain = numbers(fn, "Domain") ?? [0, 1];
|
|
57
|
+
if (domain.length !== 2 || domain[0] === domain[1]) return void 0;
|
|
58
|
+
const type = numberOf(fn, "FunctionType");
|
|
59
|
+
const evaluator = type === 2 ? exponentialEvaluator(fn, context.channels, domain) : type === 3 ? stitchingEvaluator(context, fn, domain, depth) : void 0;
|
|
60
|
+
context.memo.set(fn, evaluator);
|
|
61
|
+
return evaluator;
|
|
62
|
+
}
|
|
63
|
+
function functionEvaluator(document, object, channels) {
|
|
64
|
+
return buildEvaluator({
|
|
65
|
+
channels,
|
|
66
|
+
document,
|
|
67
|
+
memo: /* @__PURE__ */ new WeakMap(),
|
|
68
|
+
path: /* @__PURE__ */ new WeakSet()
|
|
69
|
+
}, object, 0);
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { functionEvaluator };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/effects/policy.ts
|
|
2
|
+
function softMaskAction(facts) {
|
|
3
|
+
if (facts.structure === "other") return "keep";
|
|
4
|
+
if (facts.nested) return facts.oneAxis === "shared" ? "ruleC" : "keep";
|
|
5
|
+
if (facts.subtype !== "luminosity") return "keep";
|
|
6
|
+
if (facts.contents === "single-image" && facts.backdrop === "black" && facts.colorSpace === "gray") return "rule1";
|
|
7
|
+
return "keep";
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { softMaskAction };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { RenderRefusedError } from "../errors.js";
|
|
2
|
+
import { MAX_CONTENT_BYTES, MAX_DOCUMENT_CONTENT_BYTES, numberOf } from "../press/pdf-objects.js";
|
|
3
|
+
import { GENERATED_EFFECT_IMAGE_KEY } from "./types.js";
|
|
4
|
+
import { PDFHexString } from "pdf-lib";
|
|
5
|
+
import { deflateSync } from "node:zlib";
|
|
6
|
+
//#region src/effects/rule-1.ts
|
|
7
|
+
function chargePixels(budget, bytes) {
|
|
8
|
+
if (bytes > 33554432) throw new RenderRefusedError(`The PDF has unsupported effect image longer than ${MAX_CONTENT_BYTES} bytes; supported: at most ${MAX_CONTENT_BYTES} bytes.`);
|
|
9
|
+
budget.decodedBytes += bytes;
|
|
10
|
+
if (budget.decodedBytes > 134217728) throw new RenderRefusedError(`The PDF has unsupported total decoded content length ${budget.decodedBytes}; supported: at most ${MAX_DOCUMENT_CONTENT_BYTES} bytes.`);
|
|
11
|
+
}
|
|
12
|
+
function rule1EffectImage(document, budget, plan, color) {
|
|
13
|
+
const width = numberOf(plan.image.dict, "Width");
|
|
14
|
+
const height = numberOf(plan.image.dict, "Height");
|
|
15
|
+
if (!width || !height) throw new RenderRefusedError("The PDF has unsupported grayscale mask image dimensions; supported: positive Width and Height numbers.");
|
|
16
|
+
const palette = color.values.map((value) => Math.round(Math.max(0, Math.min(1, value)) * 255));
|
|
17
|
+
const length = Math.ceil(width / 8) * height;
|
|
18
|
+
chargePixels(budget, length);
|
|
19
|
+
const bytes = new Uint8Array(length);
|
|
20
|
+
const softMask = document.context.getObjectRef(plan.image) ?? plan.image;
|
|
21
|
+
return document.context.register(document.context.stream(deflateSync(bytes), {
|
|
22
|
+
Type: "XObject",
|
|
23
|
+
Subtype: "Image",
|
|
24
|
+
Width: width,
|
|
25
|
+
Height: height,
|
|
26
|
+
ColorSpace: [
|
|
27
|
+
"Indexed",
|
|
28
|
+
color.space,
|
|
29
|
+
0,
|
|
30
|
+
PDFHexString.of(Buffer.from(palette).toString("hex"))
|
|
31
|
+
],
|
|
32
|
+
BitsPerComponent: 1,
|
|
33
|
+
Filter: "FlateDecode",
|
|
34
|
+
SMask: softMask,
|
|
35
|
+
[GENERATED_EFFECT_IMAGE_KEY]: true,
|
|
36
|
+
SegnaviaEffectColor: color.values
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { rule1EffectImage };
|