@segnavia/render 0.4.0 → 0.6.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-destinations.js +101 -0
- package/dist/book-margins.js +20 -0
- package/dist/book-pdf.js +233 -0
- package/dist/book-render.js +48 -0
- package/dist/browser-jpeg.js +42 -0
- package/dist/color/black-point.js +11 -8
- package/dist/color/engine.js +9 -2
- package/dist/color/interpolation.js +11 -1
- package/dist/color/profile.js +17 -7
- package/dist/color/tables.js +47 -13
- package/dist/color/types.js +4 -1
- package/dist/index.d.ts +5 -38
- package/dist/index.js +29 -5
- package/dist/options.d.ts +39 -0
- package/dist/press/adapter.js +117 -50
- package/dist/press/color-policy.js +27 -8
- package/dist/press/content.js +28 -20
- package/dist/press/image.js +327 -0
- package/dist/press/policy.js +27 -10
- package/dist/press/report.js +26 -14
- package/dist/press/sample-shading.js +9 -19
- package/dist/press/types.d.ts +9 -2
- package/dist/press/visitor.js +32 -30
- package/package.json +4 -3
|
@@ -1,31 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderRefusedError } from "../errors.js";
|
|
2
|
+
import { toGray, toPress } from "../color/engine.js";
|
|
2
3
|
//#region src/press/color-policy.ts
|
|
3
4
|
const NEUTRAL_TOLERANCE = .001;
|
|
4
|
-
function
|
|
5
|
+
function pressDestination(profile) {
|
|
6
|
+
return profile.colorSpace === "GRAY" ? "gray" : "cmyk";
|
|
7
|
+
}
|
|
8
|
+
function pdfMaskLuminosity([red, green, blue]) {
|
|
9
|
+
return [.3 * red + .59 * green + .11 * blue];
|
|
10
|
+
}
|
|
11
|
+
function assertNeutralTolerance(value) {
|
|
12
|
+
if (!Number.isFinite(value) || value < 0 || value > 255) throw new RenderRefusedError(`The press neutral tolerance is ${String(value)}; supported: a finite number from 0 to 255.`);
|
|
13
|
+
}
|
|
14
|
+
function snapsToNeutral(rgb, tolerance) {
|
|
15
|
+
return Math.max(...rgb) - Math.min(...rgb) <= Math.max(NEUTRAL_TOLERANCE, tolerance / 255) + Number.EPSILON;
|
|
16
|
+
}
|
|
17
|
+
function newlySnapsToNeutral(rgb, tolerance) {
|
|
18
|
+
return snapsToNeutral(rgb, tolerance) && !snapsToNeutral(rgb, 0);
|
|
19
|
+
}
|
|
20
|
+
function pressColor(profile, rgb, tolerance = 3) {
|
|
5
21
|
const [red, green, blue] = rgb;
|
|
6
|
-
if (
|
|
22
|
+
if (snapsToNeutral(rgb, tolerance)) return [
|
|
7
23
|
0,
|
|
8
24
|
0,
|
|
9
25
|
0,
|
|
10
|
-
1 - red
|
|
26
|
+
1 - (red === green && green === blue ? red : (red + green + blue) / 3)
|
|
11
27
|
];
|
|
12
28
|
return toPress(profile, rgb);
|
|
13
29
|
}
|
|
30
|
+
function destinationColor(profile, rgb, tolerance = 3) {
|
|
31
|
+
return profile.colorSpace === "GRAY" ? toGray(profile, rgb) : pressColor(profile, rgb, tolerance);
|
|
32
|
+
}
|
|
14
33
|
function formatPdfNumber(value) {
|
|
15
34
|
const rounded = Math.round(value * 1e6) / 1e6;
|
|
16
35
|
if (Object.is(rounded, -0)) return "0";
|
|
17
36
|
return rounded.toFixed(6).replace(/(?:\.0+|(?<=\.[0-9]*?)0+)$/, "");
|
|
18
37
|
}
|
|
19
|
-
function cachedPressColor(profile) {
|
|
38
|
+
function cachedPressColor(profile, tolerance = 3) {
|
|
20
39
|
const cache = /* @__PURE__ */ new Map();
|
|
21
40
|
return (rgb) => {
|
|
22
|
-
const key = rgb.join(",")
|
|
41
|
+
const key = `${tolerance}:${rgb.join(",")}`;
|
|
23
42
|
const cached = cache.get(key);
|
|
24
43
|
if (cached) return cached;
|
|
25
|
-
const converted =
|
|
44
|
+
const converted = destinationColor(profile, rgb, tolerance);
|
|
26
45
|
cache.set(key, converted);
|
|
27
46
|
return converted;
|
|
28
47
|
};
|
|
29
48
|
}
|
|
30
49
|
//#endregion
|
|
31
|
-
export { cachedPressColor, formatPdfNumber, pressColor };
|
|
50
|
+
export { assertNeutralTolerance, cachedPressColor, destinationColor, formatPdfNumber, newlySnapsToNeutral, pdfMaskLuminosity, pressColor, pressDestination, snapsToNeutral };
|
package/dist/press/content.js
CHANGED
|
@@ -120,7 +120,7 @@ function inlineHeader(source, at) {
|
|
|
120
120
|
}
|
|
121
121
|
return refusal("truncated BI operator", "an ID and EI pair");
|
|
122
122
|
}
|
|
123
|
-
function inlineImageEnd(source, at) {
|
|
123
|
+
function inlineImageEnd(source, at, destination) {
|
|
124
124
|
const { colorSpace, id, imageMask } = inlineHeader(source, at);
|
|
125
125
|
const match = /[\0\t\n\f\r ]EI(?=[\0\t\n\f\r ()<>[\]{}/%]|$)/g;
|
|
126
126
|
match.lastIndex = id;
|
|
@@ -129,6 +129,7 @@ function inlineImageEnd(source, at) {
|
|
|
129
129
|
const name = colorSpace?.startsWith("/") ? colorSpace.slice(1) : void 0;
|
|
130
130
|
const policy = inlineImageAction(name, imageMask);
|
|
131
131
|
if (policy.action === "refuse") refusal(`BI operator with color space ${policy.label}`, policy.supported);
|
|
132
|
+
if (destination === "gray" && policy.kind === "cmyk") refusal(`BI operator with color space ${policy.label}`, "RGB or gray input for a greyscale press");
|
|
132
133
|
return end.index + end[0].length;
|
|
133
134
|
}
|
|
134
135
|
function skippedConstruct(source, cursor) {
|
|
@@ -144,7 +145,7 @@ function numeric(operands, count, operator) {
|
|
|
144
145
|
return operands.map((token) => Number(token.raw));
|
|
145
146
|
}
|
|
146
147
|
function replacement(token, operands, values) {
|
|
147
|
-
const operator = token.raw === "
|
|
148
|
+
const operator = values.length === 1 ? token.raw === token.raw.toLowerCase() ? "g" : "G" : token.raw === token.raw.toLowerCase() ? "k" : "K";
|
|
148
149
|
return {
|
|
149
150
|
start: operands[0]?.start ?? token.start,
|
|
150
151
|
end: token.end,
|
|
@@ -169,36 +170,43 @@ function replaceGray(token, operands, convert, counts) {
|
|
|
169
170
|
gray
|
|
170
171
|
]));
|
|
171
172
|
}
|
|
172
|
-
function validateSpace(token, operands) {
|
|
173
|
+
function validateSpace(token, operands, destination) {
|
|
173
174
|
if (operands.length !== 1 || operands[0]?.kind !== "name") refusal(`${token.raw} operator operands`, "one color space name");
|
|
174
175
|
const name = operands[0].raw.slice(1);
|
|
175
|
-
const policy = contentOperatorAction(token.raw, false, name);
|
|
176
|
+
const policy = contentOperatorAction(token.raw, false, name, false, destination);
|
|
176
177
|
if (policy.action === "refuse") refusal(`${token.raw} operator selecting ${colorSpaceLabel({ name })}`, policy.supported);
|
|
177
178
|
}
|
|
178
|
-
function enforceOperatorPolicy(token, operands, policy) {
|
|
179
|
+
function enforceOperatorPolicy(token, operands, policy, destination) {
|
|
179
180
|
if (policy.action !== "refuse") return;
|
|
180
181
|
const operator = token.raw;
|
|
181
182
|
if (operator === "cs" || operator === "CS") {
|
|
182
|
-
validateSpace(token, operands);
|
|
183
|
+
validateSpace(token, operands, destination);
|
|
183
184
|
return;
|
|
184
185
|
}
|
|
185
186
|
if (operator === "sc" || operator === "SC" || operator === "scn" || operator === "SCN") refusal(`${operator} operator with numeric operands`, policy.supported);
|
|
186
|
-
refusal(`${operator === "g" || operator === "G" ? "gray" : "RGB"} color operator ${operator} after conversion`, policy.supported);
|
|
187
|
+
refusal(`${operator === "g" || operator === "G" ? "gray" : operator === "k" || operator === "K" ? "CMYK" : "RGB"} color operator ${operator} after conversion`, policy.supported);
|
|
187
188
|
}
|
|
188
|
-
function colorOperator(token, operands, convert, counts, inspect) {
|
|
189
|
+
function colorOperator(token, operands, convert, counts, inspect, destination) {
|
|
189
190
|
const operator = token.raw;
|
|
190
191
|
if (!isContentColorOperator(operator)) return void 0;
|
|
191
192
|
const patternName = operands.length === 1 && operands[0]?.kind === "name";
|
|
192
|
-
enforceOperatorPolicy(token, operands, contentOperatorAction(operator, inspect, void 0, patternName));
|
|
193
|
+
enforceOperatorPolicy(token, operands, contentOperatorAction(operator, inspect, void 0, patternName, destination), destination);
|
|
193
194
|
if (operator === "rg" || operator === "RG") return replaceRgb(token, operands, convert, counts);
|
|
194
|
-
if (operator === "g" || operator === "G")
|
|
195
|
-
|
|
195
|
+
if (operator === "g" || operator === "G") {
|
|
196
|
+
if (inspect && destination === "gray") {
|
|
197
|
+
numeric(operands, 1, operator);
|
|
198
|
+
counts[operator === "g" ? "fills" : "strokes"]++;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
return replaceGray(token, operands, convert, counts);
|
|
202
|
+
}
|
|
203
|
+
if (operator === "cs" || operator === "CS") validateSpace(token, operands, destination);
|
|
196
204
|
else if (operator === "k" || operator === "K") {
|
|
197
205
|
numeric(operands, 4, operator);
|
|
198
206
|
counts[operator === "k" ? "fills" : "strokes"]++;
|
|
199
207
|
}
|
|
200
208
|
}
|
|
201
|
-
function scan(source, convert, inspect, visit, operatorVisit) {
|
|
209
|
+
function scan(source, convert, inspect, destination, visit, operatorVisit) {
|
|
202
210
|
const counts = {
|
|
203
211
|
fills: 0,
|
|
204
212
|
strokes: 0
|
|
@@ -218,11 +226,11 @@ function scan(source, convert, inspect, visit, operatorVisit) {
|
|
|
218
226
|
}
|
|
219
227
|
cursor = token.end;
|
|
220
228
|
if (token.raw === "BI") {
|
|
221
|
-
cursor = inlineImageEnd(source, cursor);
|
|
229
|
+
cursor = inlineImageEnd(source, cursor, destination);
|
|
222
230
|
operands = [];
|
|
223
231
|
} else if (token.kind === "operator") {
|
|
224
232
|
operatorVisit?.(token, operands);
|
|
225
|
-
const found = colorOperator(token, operands, convert, counts, inspect);
|
|
233
|
+
const found = colorOperator(token, operands, convert, counts, inspect, destination);
|
|
226
234
|
if (found) visit?.(found);
|
|
227
235
|
operands = [];
|
|
228
236
|
} else {
|
|
@@ -241,7 +249,7 @@ function visitContentOperations(bytes, visit) {
|
|
|
241
249
|
0,
|
|
242
250
|
0,
|
|
243
251
|
0
|
|
244
|
-
], false, void 0, (operator, operands) => {
|
|
252
|
+
], false, "cmyk", void 0, (operator, operands) => {
|
|
245
253
|
visit({
|
|
246
254
|
end: operator.end,
|
|
247
255
|
operands: operands.flatMap((operand) => operand.kind === "name" || operand.kind === "number" ? [{
|
|
@@ -255,14 +263,14 @@ function visitContentOperations(bytes, visit) {
|
|
|
255
263
|
});
|
|
256
264
|
});
|
|
257
265
|
}
|
|
258
|
-
function inspectContent(bytes) {
|
|
259
|
-
return scan(decoder.decode(bytes), () => refusal("RGB color operator after conversion", "CMYK operators
|
|
266
|
+
function inspectContent(bytes, destination = "cmyk") {
|
|
267
|
+
return scan(decoder.decode(bytes), () => refusal("RGB color operator after conversion", `${destination === "gray" ? "gray" : "CMYK"} operators`), true, destination);
|
|
260
268
|
}
|
|
261
|
-
function rewriteContent(bytes, convert) {
|
|
269
|
+
function rewriteContent(bytes, convert, destination = "cmyk") {
|
|
262
270
|
const source = decoder.decode(bytes);
|
|
263
271
|
let outputLength = bytes.length;
|
|
264
272
|
let changed = false;
|
|
265
|
-
const counts = scan(source, convert, false, (found) => {
|
|
273
|
+
const counts = scan(source, convert, false, destination, (found) => {
|
|
266
274
|
changed = true;
|
|
267
275
|
outputLength += encoder.encode(found.text).length - (found.end - found.start);
|
|
268
276
|
});
|
|
@@ -273,7 +281,7 @@ function rewriteContent(bytes, convert) {
|
|
|
273
281
|
const output = new Uint8Array(outputLength);
|
|
274
282
|
let inputAt = 0;
|
|
275
283
|
let outputAt = 0;
|
|
276
|
-
scan(source, convert, false, (found) => {
|
|
284
|
+
scan(source, convert, false, destination, (found) => {
|
|
277
285
|
const untouched = bytes.subarray(inputAt, found.start);
|
|
278
286
|
output.set(untouched, outputAt);
|
|
279
287
|
outputAt += untouched.length;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { RenderRefusedError } from "../errors.js";
|
|
2
|
+
import { destinationColor, pdfMaskLuminosity } from "./color-policy.js";
|
|
3
|
+
import { nameOf, numberOf, rawStreamOf } from "./pdf-objects.js";
|
|
4
|
+
import { PDFArray, PDFDict, PDFHexString, PDFName, PDFNumber, PDFRef, PDFString } from "pdf-lib";
|
|
5
|
+
import { deflateSync, inflateSync } from "node:zlib";
|
|
6
|
+
//#region src/press/image.ts
|
|
7
|
+
const MAX_IMAGE_PIXELS = 5e7;
|
|
8
|
+
const MAX_TOTAL_IMAGE_PIXELS = 4e8;
|
|
9
|
+
const key = (value) => PDFName.of(value);
|
|
10
|
+
const refuse = (found, supported) => {
|
|
11
|
+
throw new RenderRefusedError(`The PDF has unsupported ${found}; supported: ${supported}.`);
|
|
12
|
+
};
|
|
13
|
+
const resolved = (document, object) => object instanceof PDFRef ? document.context.lookup(object) : object;
|
|
14
|
+
function bytesOf(document, object) {
|
|
15
|
+
const value = resolved(document, object);
|
|
16
|
+
if (value instanceof PDFString || value instanceof PDFHexString) return value.asBytes();
|
|
17
|
+
const stream = rawStreamOf(value);
|
|
18
|
+
if (!stream) return void 0;
|
|
19
|
+
const filter = nameOf(resolved(document, stream.dict.get(key("Filter"))));
|
|
20
|
+
if (!filter) return stream.contents;
|
|
21
|
+
if (filter === "FlateDecode" || filter === "Fl") return inflate(stream.contents, "Indexed lookup");
|
|
22
|
+
}
|
|
23
|
+
function baseSpace(document, object) {
|
|
24
|
+
const value = resolved(document, object);
|
|
25
|
+
const name = nameOf(value);
|
|
26
|
+
if (name === "DeviceRGB") return {
|
|
27
|
+
channels: 3,
|
|
28
|
+
kind: "rgb",
|
|
29
|
+
label: name
|
|
30
|
+
};
|
|
31
|
+
if (name === "DeviceGray") return {
|
|
32
|
+
channels: 1,
|
|
33
|
+
kind: "gray",
|
|
34
|
+
label: name
|
|
35
|
+
};
|
|
36
|
+
if (!(value instanceof PDFArray) || nameOf(resolved(document, value.get(0))) !== "ICCBased") return void 0;
|
|
37
|
+
const profile = rawStreamOf(resolved(document, value.get(1)));
|
|
38
|
+
const channels = profile ? numberOf(profile.dict, "N") : void 0;
|
|
39
|
+
if (channels === 1) return {
|
|
40
|
+
channels,
|
|
41
|
+
kind: "gray",
|
|
42
|
+
label: "ICCBased N 1"
|
|
43
|
+
};
|
|
44
|
+
if (channels === 3) return {
|
|
45
|
+
channels,
|
|
46
|
+
kind: "rgb",
|
|
47
|
+
label: "ICCBased N 3"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function imageSpace(document, object) {
|
|
51
|
+
const base = baseSpace(document, object);
|
|
52
|
+
if (base) return base;
|
|
53
|
+
const value = resolved(document, object);
|
|
54
|
+
if (value instanceof PDFArray && nameOf(resolved(document, value.get(0))) === "Indexed") {
|
|
55
|
+
const indexedBase = baseSpace(document, value.get(1));
|
|
56
|
+
const high = resolved(document, value.get(2));
|
|
57
|
+
const lookup = bytesOf(document, value.get(3));
|
|
58
|
+
if (indexedBase && high instanceof PDFNumber && lookup) return {
|
|
59
|
+
base: indexedBase,
|
|
60
|
+
high: high.asNumber(),
|
|
61
|
+
kind: "indexed",
|
|
62
|
+
label: `Indexed over ${indexedBase.label}`,
|
|
63
|
+
lookup
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const label = nameOf(value) ?? (value instanceof PDFArray ? nameOf(resolved(document, value.get(0))) ?? "array" : "missing");
|
|
67
|
+
return refuse(`image color space ${label}`, "DeviceRGB, DeviceGray, ICCBased with N 1 or 3, or Indexed over one of those");
|
|
68
|
+
}
|
|
69
|
+
function filterName(document, image) {
|
|
70
|
+
const value = resolved(document, image.dict.get(key("Filter")));
|
|
71
|
+
if (!value) return "none";
|
|
72
|
+
if (value instanceof PDFArray) refuse("image filter chain", "no filter, FlateDecode, or DCTDecode");
|
|
73
|
+
const name = nameOf(value);
|
|
74
|
+
if (name === "FlateDecode" || name === "Fl") return "flate";
|
|
75
|
+
if (name === "DCTDecode" || name === "DCT") return "jpeg";
|
|
76
|
+
return refuse(`image filter ${name ?? "other"}`, "no filter, FlateDecode, or DCTDecode");
|
|
77
|
+
}
|
|
78
|
+
function inflate(bytes, structure, maximum) {
|
|
79
|
+
try {
|
|
80
|
+
return inflateSync(bytes, maximum === void 0 ? void 0 : { maxOutputLength: maximum });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (maximum !== void 0 && error instanceof Error && "code" in error && error.code === "ERR_BUFFER_TOO_LARGE") return refuse(`${structure} data length greater than ${maximum}`, `${maximum} bytes`);
|
|
83
|
+
return refuse(`${structure} FlateDecode data`, "valid FlateDecode data");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const paeth = (left, above, upperLeft) => {
|
|
87
|
+
const estimate = left + above - upperLeft;
|
|
88
|
+
const leftDistance = Math.abs(estimate - left);
|
|
89
|
+
const aboveDistance = Math.abs(estimate - above);
|
|
90
|
+
const upperLeftDistance = Math.abs(estimate - upperLeft);
|
|
91
|
+
return leftDistance <= aboveDistance && leftDistance <= upperLeftDistance ? left : aboveDistance <= upperLeftDistance ? above : upperLeft;
|
|
92
|
+
};
|
|
93
|
+
const predictorAddition = (filter, left, above, upperLeft) => {
|
|
94
|
+
if (filter === 1) return left;
|
|
95
|
+
if (filter === 2) return above;
|
|
96
|
+
if (filter === 3) return Math.floor((left + above) / 2);
|
|
97
|
+
if (filter === 4) return paeth(left, above, upperLeft);
|
|
98
|
+
return 0;
|
|
99
|
+
};
|
|
100
|
+
function undoPngPredictor(bytes, width, height, colors, bits) {
|
|
101
|
+
const rowBytes = Math.ceil(width * colors * bits / 8);
|
|
102
|
+
const pixelBytes = Math.max(1, Math.ceil(colors * bits / 8));
|
|
103
|
+
if (bytes.length !== (rowBytes + 1) * height) refuse(`image predictor data length ${bytes.length}`, `${height} rows of ${rowBytes + 1} bytes`);
|
|
104
|
+
const output = new Uint8Array(rowBytes * height);
|
|
105
|
+
let source = 0;
|
|
106
|
+
for (let row = 0; row < height; row++) {
|
|
107
|
+
const filter = bytes[source++] ?? 0;
|
|
108
|
+
if (filter > 4) refuse(`image PNG predictor filter ${filter}`, "filters 0 through 4");
|
|
109
|
+
const target = row * rowBytes;
|
|
110
|
+
for (let column = 0; column < rowBytes; column++) {
|
|
111
|
+
const raw = bytes[source++] ?? 0;
|
|
112
|
+
const left = output[target + column - pixelBytes] ?? 0;
|
|
113
|
+
const above = output[target + column - rowBytes] ?? 0;
|
|
114
|
+
const upperLeft = output[target + column - rowBytes - pixelBytes] ?? 0;
|
|
115
|
+
const add = predictorAddition(filter, left, above, upperLeft);
|
|
116
|
+
output[target + column] = raw + add & 255;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return output;
|
|
120
|
+
}
|
|
121
|
+
function decodeFlate(state, image, width, height, channels, bits) {
|
|
122
|
+
const rawParms = resolved(state.document, image.dict.get(key("DecodeParms")));
|
|
123
|
+
const rowBytes = Math.ceil(width * channels * bits / 8);
|
|
124
|
+
if (!rawParms) return inflate(image.contents, "image", rowBytes * height);
|
|
125
|
+
if (!(rawParms instanceof PDFDict)) refuse("image DecodeParms", "no DecodeParms or a PNG predictor 10 through 15");
|
|
126
|
+
const parms = rawParms;
|
|
127
|
+
const predictor = numberOf(parms, "Predictor") ?? 1;
|
|
128
|
+
if (predictor < 10 || predictor > 15) refuse(`image predictor ${predictor}`, "a PNG predictor 10 through 15");
|
|
129
|
+
const colors = numberOf(parms, "Colors") ?? 1;
|
|
130
|
+
const columns = numberOf(parms, "Columns") ?? 1;
|
|
131
|
+
const predictorBits = numberOf(parms, "BitsPerComponent") ?? 8;
|
|
132
|
+
if (columns !== width || colors !== channels || predictorBits !== bits) refuse("image PNG predictor parameters", `Colors ${channels}, Columns ${width}, and BitsPerComponent ${bits}`);
|
|
133
|
+
const expected = (rowBytes + 1) * height;
|
|
134
|
+
return undoPngPredictor(inflate(image.contents, "image predictor", expected), width, height, colors, predictorBits);
|
|
135
|
+
}
|
|
136
|
+
function isDefaultDecode(image, channels, bits, indexed) {
|
|
137
|
+
const decode = image.dict.lookupMaybe(key("Decode"), PDFArray);
|
|
138
|
+
if (!decode) return true;
|
|
139
|
+
if (decode.size() !== channels * 2) return false;
|
|
140
|
+
const high = 2 ** bits - 1;
|
|
141
|
+
for (let channel = 0; channel < channels; channel++) {
|
|
142
|
+
const lowValue = decode.lookupMaybe(channel * 2, PDFNumber)?.asNumber();
|
|
143
|
+
const highValue = decode.lookupMaybe(channel * 2 + 1, PDFNumber)?.asNumber();
|
|
144
|
+
if (lowValue !== 0 || highValue !== (indexed ? high : 1)) return false;
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
function grayTable(profile) {
|
|
149
|
+
const table = /* @__PURE__ */ new Uint8Array(256);
|
|
150
|
+
for (let value = 0; value < 256; value++) table[value] = Math.round(destinationColor(profile, [
|
|
151
|
+
value / 255,
|
|
152
|
+
value / 255,
|
|
153
|
+
value / 255
|
|
154
|
+
], 0)[0] * 255);
|
|
155
|
+
return table;
|
|
156
|
+
}
|
|
157
|
+
function buildGrayImageLookup(profile, size = 33) {
|
|
158
|
+
const table = new Uint8Array(size ** 3);
|
|
159
|
+
const at = (red, green, blue) => (red * size + green) * size + blue;
|
|
160
|
+
for (let red = 0; red < size; red++) for (let green = 0; green < size; green++) for (let blue = 0; blue < size; blue++) {
|
|
161
|
+
const value = destinationColor(profile, [
|
|
162
|
+
red / (size - 1),
|
|
163
|
+
green / (size - 1),
|
|
164
|
+
blue / (size - 1)
|
|
165
|
+
], 0)[0];
|
|
166
|
+
table[at(red, green, blue)] = Math.round(Math.max(0, Math.min(1, value)) * 255);
|
|
167
|
+
}
|
|
168
|
+
return (red, green, blue) => {
|
|
169
|
+
const scale = (size - 1) / 255;
|
|
170
|
+
const r = red * scale, g = green * scale, b = blue * scale;
|
|
171
|
+
const r0 = Math.floor(r), g0 = Math.floor(g), b0 = Math.floor(b);
|
|
172
|
+
const r1 = Math.min(size - 1, r0 + 1), g1 = Math.min(size - 1, g0 + 1), b1 = Math.min(size - 1, b0 + 1);
|
|
173
|
+
const rf = r - r0, gf = g - g0, bf = b - b0;
|
|
174
|
+
const mix = (first, second, amount) => first + (second - first) * amount;
|
|
175
|
+
const low = mix(mix(table[at(r0, g0, b0)] ?? 0, table[at(r1, g0, b0)] ?? 0, rf), mix(table[at(r0, g1, b0)] ?? 0, table[at(r1, g1, b0)] ?? 0, rf), gf);
|
|
176
|
+
const high = mix(mix(table[at(r0, g0, b1)] ?? 0, table[at(r1, g0, b1)] ?? 0, rf), mix(table[at(r0, g1, b1)] ?? 0, table[at(r1, g1, b1)] ?? 0, rf), gf);
|
|
177
|
+
return Math.round(mix(low, high, bf));
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function streamDictionary(stream) {
|
|
181
|
+
const dictionary = { Filter: key("FlateDecode") };
|
|
182
|
+
for (const [name, value] of stream.dict.entries()) {
|
|
183
|
+
const label = name.toString().slice(1);
|
|
184
|
+
if (![
|
|
185
|
+
"Filter",
|
|
186
|
+
"DecodeParms",
|
|
187
|
+
"Length",
|
|
188
|
+
"ColorSpace",
|
|
189
|
+
"BitsPerComponent",
|
|
190
|
+
"Decode"
|
|
191
|
+
].includes(label)) dictionary[label] = value;
|
|
192
|
+
}
|
|
193
|
+
return dictionary;
|
|
194
|
+
}
|
|
195
|
+
function profileRgb(state, source, space, mask, pixels) {
|
|
196
|
+
const expected = pixels * space.channels;
|
|
197
|
+
if (source.length !== expected) refuse(`image sample length ${source.length}`, `${expected} bytes for its dimensions and color space`);
|
|
198
|
+
const output = new Uint8Array(pixels);
|
|
199
|
+
for (let pixel = 0; pixel < pixels; pixel++) {
|
|
200
|
+
const first = source[pixel * space.channels] ?? 0;
|
|
201
|
+
const red = first, green = space.channels === 1 ? first : source[pixel * 3 + 1] ?? 0, blue = space.channels === 1 ? first : source[pixel * 3 + 2] ?? 0;
|
|
202
|
+
output[pixel] = mask === "none" ? space.kind === "gray" ? state.grayLookup[first] ?? 0 : state.rgbLookup(red, green, blue) : Math.round(pdfMaskLuminosity([
|
|
203
|
+
red / 255,
|
|
204
|
+
green / 255,
|
|
205
|
+
blue / 255
|
|
206
|
+
])[0] * 255);
|
|
207
|
+
}
|
|
208
|
+
return output;
|
|
209
|
+
}
|
|
210
|
+
function paletteToGray(state, space, mask) {
|
|
211
|
+
const count = space.high + 1;
|
|
212
|
+
const expected = count * space.base.channels;
|
|
213
|
+
if (!Number.isInteger(space.high) || space.high < 0 || space.high > 255 || space.lookup.length < expected) refuse(`Indexed image lookup length ${space.lookup.length}`, `${expected} bytes for entries 0 through ${space.high}`);
|
|
214
|
+
return profileRgb(state, space.lookup.subarray(0, expected), space.base, mask, count);
|
|
215
|
+
}
|
|
216
|
+
async function sourceBytes(state, image, filter, width, height, channels, bits) {
|
|
217
|
+
if (filter === "none") return image.contents;
|
|
218
|
+
if (filter === "flate") return decodeFlate(state, image, width, height, channels, bits);
|
|
219
|
+
if (!state.decodeJpeg) refuse("DCTDecode image without a JPEG decoder", "a JPEG needs the renderer's decoder");
|
|
220
|
+
const decoder = state.decodeJpeg;
|
|
221
|
+
const decoded = await decodeJpeg(decoder, image.contents);
|
|
222
|
+
if (decoded.width !== width || decoded.height !== height) refuse(`decoded JPEG dimensions ${decoded.width}x${decoded.height}`, `the image Width and Height ${width}x${height}`);
|
|
223
|
+
if (decoded.rgb.length !== width * height * 3) refuse(`decoded JPEG sample length ${decoded.rgb.length}`, `${width * height * 3} RGB bytes`);
|
|
224
|
+
return decoded.rgb;
|
|
225
|
+
}
|
|
226
|
+
async function decodeJpeg(decoder, bytes) {
|
|
227
|
+
try {
|
|
228
|
+
return await decoder(bytes);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
231
|
+
return refuse(`JPEG decoding failure: ${message}`, "a JPEG the renderer's browser can decode");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function createImageState(document, profile, decodeJpeg) {
|
|
235
|
+
return {
|
|
236
|
+
counted: /* @__PURE__ */ new WeakSet(),
|
|
237
|
+
converted: /* @__PURE__ */ new WeakMap(),
|
|
238
|
+
decodeJpeg,
|
|
239
|
+
document,
|
|
240
|
+
profile,
|
|
241
|
+
rgbLookup: buildGrayImageLookup(profile),
|
|
242
|
+
grayLookup: grayTable(profile),
|
|
243
|
+
pixels: 0,
|
|
244
|
+
total: 0
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function countSuppliedImage(state, image, mask) {
|
|
248
|
+
if (mask !== "none" || state.counted.has(image)) return;
|
|
249
|
+
state.counted.add(image);
|
|
250
|
+
state.total++;
|
|
251
|
+
}
|
|
252
|
+
function imageGeometry(image) {
|
|
253
|
+
const width = numberOf(image.dict, "Width") ?? refuse("image Width missing", "a positive Width number");
|
|
254
|
+
const height = numberOf(image.dict, "Height") ?? refuse("image Height missing", "a positive Height number");
|
|
255
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) refuse(`image dimensions ${width}x${height}`, "positive integer Width and Height");
|
|
256
|
+
const pixels = width * height;
|
|
257
|
+
if (pixels > 5e7) refuse(`image pixel count ${pixels}`, `at most ${MAX_IMAGE_PIXELS} pixels in one image`);
|
|
258
|
+
return {
|
|
259
|
+
height,
|
|
260
|
+
pixels,
|
|
261
|
+
width
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function chargePixels(state, pixels) {
|
|
265
|
+
state.pixels += pixels;
|
|
266
|
+
if (state.pixels > 4e8) refuse(`total image pixel count ${state.pixels}`, `at most ${MAX_TOTAL_IMAGE_PIXELS} pixels in one render`);
|
|
267
|
+
}
|
|
268
|
+
async function convertSuppliedImage(state, image, mask) {
|
|
269
|
+
const context = mask === "none" ? "profile" : "luminosity";
|
|
270
|
+
const variants = state.converted.get(image);
|
|
271
|
+
const existing = variants?.[context];
|
|
272
|
+
if (existing) {
|
|
273
|
+
countSuppliedImage(state, image, mask);
|
|
274
|
+
return existing;
|
|
275
|
+
}
|
|
276
|
+
if (image.dict.has(key("SegnaviaGeneratedEffect")) || image.dict.get(key("ImageMask"))?.toString() === "true") return void 0;
|
|
277
|
+
const { width, height, pixels } = imageGeometry(image);
|
|
278
|
+
chargePixels(state, pixels);
|
|
279
|
+
const space = imageSpace(state.document, image.dict.get(key("ColorSpace")));
|
|
280
|
+
const bits = numberOf(image.dict, "BitsPerComponent");
|
|
281
|
+
const allowedBits = space.kind === "indexed" ? [
|
|
282
|
+
1,
|
|
283
|
+
2,
|
|
284
|
+
4,
|
|
285
|
+
8
|
|
286
|
+
] : [8];
|
|
287
|
+
if (bits === void 0 || !allowedBits.includes(bits)) refuse(`image BitsPerComponent ${bits ?? "missing"}`, allowedBits.join(", "));
|
|
288
|
+
const componentBits = bits;
|
|
289
|
+
const channels = space.kind === "indexed" ? 1 : space.channels;
|
|
290
|
+
if (!isDefaultDecode(image, channels, componentBits, space.kind === "indexed")) refuse("image Decode array", "the default Decode array");
|
|
291
|
+
const filter = filterName(state.document, image);
|
|
292
|
+
if (space.kind === "indexed" && filter === "jpeg") refuse("Indexed image filter DCTDecode", "no filter or FlateDecode");
|
|
293
|
+
const source = await sourceBytes(state, image, filter, width, height, filter === "jpeg" ? 3 : channels, componentBits);
|
|
294
|
+
const dictionary = streamDictionary(image);
|
|
295
|
+
let output;
|
|
296
|
+
if (space.kind === "indexed") {
|
|
297
|
+
const expected = Math.ceil(width * componentBits / 8) * height;
|
|
298
|
+
if (source.length !== expected) refuse(`Indexed image sample length ${source.length}`, `${expected} bytes for its dimensions`);
|
|
299
|
+
output = source;
|
|
300
|
+
const palette = paletteToGray(state, space, mask);
|
|
301
|
+
dictionary.ColorSpace = state.document.context.obj([
|
|
302
|
+
"Indexed",
|
|
303
|
+
"DeviceGray",
|
|
304
|
+
space.high,
|
|
305
|
+
PDFHexString.of(Buffer.from(palette).toString("hex"))
|
|
306
|
+
]);
|
|
307
|
+
dictionary.BitsPerComponent = state.document.context.obj(componentBits);
|
|
308
|
+
} else {
|
|
309
|
+
output = profileRgb(state, source, filter === "jpeg" ? {
|
|
310
|
+
channels: 3,
|
|
311
|
+
kind: "rgb",
|
|
312
|
+
label: "decoded JPEG"
|
|
313
|
+
} : space, mask, pixels);
|
|
314
|
+
dictionary.ColorSpace = key("DeviceGray");
|
|
315
|
+
dictionary.BitsPerComponent = state.document.context.obj(8);
|
|
316
|
+
}
|
|
317
|
+
const replacement = state.document.context.stream(deflateSync(output), dictionary);
|
|
318
|
+
state.converted.set(image, {
|
|
319
|
+
...variants,
|
|
320
|
+
[context]: replacement
|
|
321
|
+
});
|
|
322
|
+
countSuppliedImage(state, image, mask);
|
|
323
|
+
return replacement;
|
|
324
|
+
}
|
|
325
|
+
const convertedImageCount = (state) => state.total;
|
|
326
|
+
//#endregion
|
|
327
|
+
export { MAX_IMAGE_PIXELS, MAX_TOTAL_IMAGE_PIXELS, buildGrayImageLookup, convertSuppliedImage, convertedImageCount, createImageState };
|
package/dist/press/policy.js
CHANGED
|
@@ -39,23 +39,29 @@ function contentFilter(names) {
|
|
|
39
39
|
encoding: "unsupported"
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
function groupAction(kind, label, maskSource) {
|
|
42
|
+
function groupAction(kind, label, maskSource, destination = "cmyk") {
|
|
43
|
+
if (destination === "gray") {
|
|
44
|
+
const supported = "DeviceGray, DeviceRGB, or no declared space";
|
|
45
|
+
if (kind === "gray") return decision("keep", supported);
|
|
46
|
+
if (label === "missing" || label === "DeviceRGB") return decision("convert", supported);
|
|
47
|
+
return decision("refuse", supported);
|
|
48
|
+
}
|
|
43
49
|
const supported = "DeviceCMYK, DeviceGray, DeviceRGB on a mask source, or no declared space";
|
|
44
50
|
if (kind === "gray" || kind === "cmyk" || maskSource && (label === "DeviceRGB" || label === "missing")) return decision("keep", supported);
|
|
45
51
|
if (label === "missing" || label === "DeviceRGB") return decision("convert", supported);
|
|
46
52
|
return decision("refuse", supported);
|
|
47
53
|
}
|
|
48
|
-
function shadingAction(kind, type, functionType) {
|
|
54
|
+
function shadingAction(kind, type, functionType, destination = "cmyk") {
|
|
49
55
|
if (type === 1) {
|
|
50
56
|
const supported = "ShadingType 1 in an RGB space with FunctionType 4, or DeviceCMYK";
|
|
51
|
-
if (kind ===
|
|
57
|
+
if (kind === destination) return decision("keep", supported);
|
|
52
58
|
if (kind === "rgb" && functionType === 4) return decision("convert", supported);
|
|
53
59
|
return decision("refuse", supported);
|
|
54
60
|
}
|
|
55
61
|
const supported = "ShadingType 2 or 3 in an RGB or DeviceCMYK color space";
|
|
56
62
|
if (type !== 2 && type !== 3) return decision("refuse", supported);
|
|
57
63
|
if (kind === "rgb") return decision("convert", supported);
|
|
58
|
-
if (kind ===
|
|
64
|
+
if (kind === destination) return decision("keep", supported);
|
|
59
65
|
return decision("refuse", supported);
|
|
60
66
|
}
|
|
61
67
|
function shadingFunctionAction(type, dictionary, functions) {
|
|
@@ -96,8 +102,10 @@ function inlineImageAction(name, imageMask) {
|
|
|
96
102
|
label: colorSpaceLabel(value)
|
|
97
103
|
};
|
|
98
104
|
}
|
|
99
|
-
function contentColorSpaceAction(name) {
|
|
100
|
-
|
|
105
|
+
function contentColorSpaceAction(name, destination = "cmyk") {
|
|
106
|
+
const device = destination === "gray" ? "DeviceGray" : "DeviceCMYK";
|
|
107
|
+
const supported = `Pattern or ${device}`;
|
|
108
|
+
return decision(name === "Pattern" || name === device ? "keep" : "refuse", supported);
|
|
101
109
|
}
|
|
102
110
|
const colorOperators = [
|
|
103
111
|
"rg",
|
|
@@ -116,10 +124,19 @@ const colorOperators = [
|
|
|
116
124
|
function isContentColorOperator(operator) {
|
|
117
125
|
return colorOperators.includes(operator);
|
|
118
126
|
}
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
function deviceColorAction(operator, inspect, destination) {
|
|
128
|
+
const rgb = operator === "rg" || operator === "RG";
|
|
129
|
+
const gray = operator === "g" || operator === "G";
|
|
130
|
+
const cmyk = operator === "k" || operator === "K";
|
|
131
|
+
if (!rgb && !gray && !cmyk) return void 0;
|
|
132
|
+
if (!inspect && (rgb || gray)) return decision("convert", "RGB or DeviceGray color operands");
|
|
133
|
+
if (destination === "gray") return decision(gray ? "keep" : "refuse", "RGB or gray input for a greyscale press");
|
|
134
|
+
return decision(cmyk ? "keep" : "refuse", "CMYK operators");
|
|
135
|
+
}
|
|
136
|
+
function contentOperatorAction(operator, inspect, selectedSpace, patternName = false, destination = "cmyk") {
|
|
137
|
+
const device = deviceColorAction(operator, inspect, destination);
|
|
138
|
+
if (device) return device;
|
|
139
|
+
if (operator === "cs" || operator === "CS") return contentColorSpaceAction(selectedSpace, destination);
|
|
123
140
|
return decision((operator === "scn" || operator === "SCN") && patternName ? "keep" : "refuse", "a pattern name with scn or SCN");
|
|
124
141
|
}
|
|
125
142
|
//#endregion
|