@segnavia/render 0.1.0 → 0.3.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/binary.js +27 -0
- package/dist/color/black-point.js +121 -0
- package/dist/color/engine.js +18 -0
- package/dist/color/interpolation.js +74 -0
- package/dist/color/profile.js +98 -0
- package/dist/color/spaces.js +138 -0
- package/dist/color/tables.js +255 -0
- package/dist/color/types.js +21 -0
- package/dist/deck-pdf.js +152 -0
- 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/errors.d.ts +8 -0
- package/dist/errors.js +9 -0
- package/dist/index.d.ts +18 -8
- package/dist/index.js +96 -76
- package/dist/page-paper.js +94 -0
- package/dist/pages-imposition.js +65 -0
- package/dist/pages-pdf.js +67 -0
- package/dist/pdf-compose.js +35 -0
- package/dist/pdf-geometry.js +27 -0
- package/dist/press/adapter.js +275 -0
- 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/color-policy.js +31 -0
- package/dist/press/content.js +292 -0
- package/dist/press/pattern-bounds.js +275 -0
- package/dist/press/pattern-path.js +102 -0
- package/dist/press/pdf-objects.js +88 -0
- package/dist/press/policy.js +126 -0
- package/dist/press/report.js +89 -0
- package/dist/press/sample-shading.js +266 -0
- package/dist/press/types.d.ts +18 -0
- package/dist/press/visitor.js +163 -0
- package/package.json +3 -3
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { applyOperator, calculatorRefuse, classifiedOperatorName, pushStack } from "./calculator-ops.js";
|
|
2
|
+
//#region src/press/calculator.ts
|
|
3
|
+
const MAX_CALCULATOR_PROGRAM_BYTES = 8192;
|
|
4
|
+
const MAX_CALCULATOR_OPERATORS = 4096;
|
|
5
|
+
const NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/;
|
|
6
|
+
const OPERATOR = /^[A-Za-z][A-Za-z0-9]*/;
|
|
7
|
+
const whitespace = (character) => character !== void 0 && /[\0\t\n\f\r ]/.test(character);
|
|
8
|
+
function skipComment(source, at) {
|
|
9
|
+
let cursor = at + 1;
|
|
10
|
+
while (cursor < source.length && source[cursor] !== "\r" && source[cursor] !== "\n") cursor++;
|
|
11
|
+
return cursor;
|
|
12
|
+
}
|
|
13
|
+
function skipSpace(source, at) {
|
|
14
|
+
let cursor = at;
|
|
15
|
+
while (cursor < source.length) if (whitespace(source[cursor])) cursor++;
|
|
16
|
+
else if (source[cursor] === "%") cursor = skipComment(source, cursor);
|
|
17
|
+
else break;
|
|
18
|
+
return cursor;
|
|
19
|
+
}
|
|
20
|
+
function parseNumber(source, at) {
|
|
21
|
+
const match = NUMBER.exec(source.slice(at));
|
|
22
|
+
if (!match) return calculatorRefuse("calculator number", "a PDF calculator number");
|
|
23
|
+
const raw = match[0];
|
|
24
|
+
const value = Number(raw);
|
|
25
|
+
if (!Number.isFinite(value)) return calculatorRefuse("calculator number", "a finite PDF calculator number");
|
|
26
|
+
return {
|
|
27
|
+
end: at + raw.length,
|
|
28
|
+
op: {
|
|
29
|
+
kind: "number",
|
|
30
|
+
value,
|
|
31
|
+
int: !/[.eE]/.test(raw)
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function parseOperator(source, at) {
|
|
36
|
+
const match = OPERATOR.exec(source.slice(at));
|
|
37
|
+
if (!match) return calculatorRefuse(`calculator token ${classifiedOperatorName(source[at] ?? "other")}`, "a PDF calculator operator, number, or procedure");
|
|
38
|
+
return {
|
|
39
|
+
end: at + match[0].length,
|
|
40
|
+
op: {
|
|
41
|
+
kind: "operator",
|
|
42
|
+
name: match[0]
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function parseProcedure(source, at, depth) {
|
|
47
|
+
if (depth > 32) return calculatorRefuse(`calculator procedure nesting over 32`, `at most 32 levels`);
|
|
48
|
+
const body = [];
|
|
49
|
+
let cursor = skipSpace(source, at + 1);
|
|
50
|
+
while (cursor < source.length && source[cursor] !== "}") {
|
|
51
|
+
const parsed = parseToken(source, cursor, depth + 1);
|
|
52
|
+
body.push(parsed.op);
|
|
53
|
+
cursor = skipSpace(source, parsed.end);
|
|
54
|
+
}
|
|
55
|
+
if (cursor >= source.length) return calculatorRefuse("calculator truncated procedure", "a closing }");
|
|
56
|
+
return {
|
|
57
|
+
end: cursor + 1,
|
|
58
|
+
op: {
|
|
59
|
+
kind: "procedure",
|
|
60
|
+
body
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function parseToken(source, at, depth) {
|
|
65
|
+
const character = source[at];
|
|
66
|
+
if (character === "{") return parseProcedure(source, at, depth);
|
|
67
|
+
if (character === "+" || character === "-" || character === "." || character !== void 0 && character >= "0" && character <= "9") return parseNumber(source, at);
|
|
68
|
+
return parseOperator(source, at);
|
|
69
|
+
}
|
|
70
|
+
function parseCalculator(source) {
|
|
71
|
+
if (source.length > 8192) calculatorRefuse(`calculator program length ${source.length}`, `at most ${MAX_CALCULATOR_PROGRAM_BYTES} bytes`);
|
|
72
|
+
const tokens = [];
|
|
73
|
+
let cursor = skipSpace(source, 0);
|
|
74
|
+
while (cursor < source.length) {
|
|
75
|
+
const parsed = parseToken(source, cursor, 1);
|
|
76
|
+
tokens.push(parsed.op);
|
|
77
|
+
cursor = skipSpace(source, parsed.end);
|
|
78
|
+
}
|
|
79
|
+
if (tokens.length === 1 && tokens[0]?.kind === "procedure") return tokens[0].body;
|
|
80
|
+
return tokens;
|
|
81
|
+
}
|
|
82
|
+
function exec(body, state) {
|
|
83
|
+
for (const token of body) {
|
|
84
|
+
if (token.kind === "number") {
|
|
85
|
+
pushStack(state, {
|
|
86
|
+
kind: "n",
|
|
87
|
+
value: token.value,
|
|
88
|
+
int: token.int
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (token.kind === "procedure") {
|
|
93
|
+
pushStack(state, {
|
|
94
|
+
kind: "p",
|
|
95
|
+
body: token.body
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
state.count++;
|
|
100
|
+
if (state.count > 4096) calculatorRefuse(`calculator operators ${state.count}`, `at most ${MAX_CALCULATOR_OPERATORS} operators per evaluation`);
|
|
101
|
+
applyOperator(token.name, state, (inner) => exec(inner, state));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function runCalculator(program, inputs) {
|
|
105
|
+
const state = {
|
|
106
|
+
count: 0,
|
|
107
|
+
stack: []
|
|
108
|
+
};
|
|
109
|
+
for (const value of inputs) pushStack(state, {
|
|
110
|
+
kind: "n",
|
|
111
|
+
value,
|
|
112
|
+
int: Number.isInteger(value)
|
|
113
|
+
});
|
|
114
|
+
exec(program, state);
|
|
115
|
+
return state;
|
|
116
|
+
}
|
|
117
|
+
function staticOperatorCount(program) {
|
|
118
|
+
let count = 0;
|
|
119
|
+
for (const token of program) if (token.kind === "operator") count++;
|
|
120
|
+
else if (token.kind === "procedure") count += staticOperatorCount(token.body);
|
|
121
|
+
return count;
|
|
122
|
+
}
|
|
123
|
+
function evaluateCalculator(program, inputs) {
|
|
124
|
+
return runCalculator(program, inputs).stack.map((value) => {
|
|
125
|
+
if (value.kind !== "n") return calculatorRefuse("calculator output type", "numbers");
|
|
126
|
+
return value.value;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
export { MAX_CALCULATOR_OPERATORS, MAX_CALCULATOR_PROGRAM_BYTES, evaluateCalculator, parseCalculator, staticOperatorCount };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { toPress } from "../color/engine.js";
|
|
2
|
+
//#region src/press/color-policy.ts
|
|
3
|
+
const NEUTRAL_TOLERANCE = .001;
|
|
4
|
+
function pressColor(profile, rgb) {
|
|
5
|
+
const [red, green, blue] = rgb;
|
|
6
|
+
if (Math.max(red, green, blue) - Math.min(red, green, blue) <= NEUTRAL_TOLERANCE) return [
|
|
7
|
+
0,
|
|
8
|
+
0,
|
|
9
|
+
0,
|
|
10
|
+
1 - red
|
|
11
|
+
];
|
|
12
|
+
return toPress(profile, rgb);
|
|
13
|
+
}
|
|
14
|
+
function formatPdfNumber(value) {
|
|
15
|
+
const rounded = Math.round(value * 1e6) / 1e6;
|
|
16
|
+
if (Object.is(rounded, -0)) return "0";
|
|
17
|
+
return rounded.toFixed(6).replace(/(?:\.0+|(?<=\.[0-9]*?)0+)$/, "");
|
|
18
|
+
}
|
|
19
|
+
function cachedPressColor(profile) {
|
|
20
|
+
const cache = /* @__PURE__ */ new Map();
|
|
21
|
+
return (rgb) => {
|
|
22
|
+
const key = rgb.join(",");
|
|
23
|
+
const cached = cache.get(key);
|
|
24
|
+
if (cached) return cached;
|
|
25
|
+
const converted = pressColor(profile, rgb);
|
|
26
|
+
cache.set(key, converted);
|
|
27
|
+
return converted;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { cachedPressColor, formatPdfNumber, pressColor };
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { RenderRefusedError } from "../errors.js";
|
|
2
|
+
import { formatPdfNumber } from "./color-policy.js";
|
|
3
|
+
import { colorSpaceLabel, contentOperatorAction, inlineImageAction, isContentColorOperator } from "./policy.js";
|
|
4
|
+
const decoder = new TextDecoder("latin1");
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
const numberPattern = /^[+-]?(?:\d+\.?\d*|\.\d+)$/;
|
|
7
|
+
const whitespace = (character) => character !== void 0 && /[\0\t\n\f\r ]/.test(character);
|
|
8
|
+
const delimiter = (character) => character !== void 0 && /[()<>{}[\]/%]/.test(character);
|
|
9
|
+
function refusal(found, supported) {
|
|
10
|
+
throw new RenderRefusedError(`The PDF has unsupported ${found}; supported: ${supported}.`);
|
|
11
|
+
}
|
|
12
|
+
function skipComment(source, at) {
|
|
13
|
+
let cursor = at + 1;
|
|
14
|
+
while (cursor < source.length && source[cursor] !== "\r" && source[cursor] !== "\n") cursor++;
|
|
15
|
+
return cursor;
|
|
16
|
+
}
|
|
17
|
+
function skipLiteral(source, at) {
|
|
18
|
+
let cursor = at + 1;
|
|
19
|
+
let depth = 1;
|
|
20
|
+
while (cursor < source.length) {
|
|
21
|
+
const character = source[cursor];
|
|
22
|
+
if (character === "\\") {
|
|
23
|
+
cursor += 2;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (character === "(" && ++depth > 64) refusal(`literal string nesting over 64`, `at most 64 levels`);
|
|
27
|
+
if (character === ")" && --depth === 0) return cursor + 1;
|
|
28
|
+
cursor++;
|
|
29
|
+
}
|
|
30
|
+
return refusal("truncated literal string", "a closing parenthesis");
|
|
31
|
+
}
|
|
32
|
+
function skipHex(source, at) {
|
|
33
|
+
const end = source.indexOf(">", at + 1);
|
|
34
|
+
if (end < 0) return refusal("truncated hex string", "a closing >");
|
|
35
|
+
return end + 1;
|
|
36
|
+
}
|
|
37
|
+
function skipComposite(source, at, dictionary, depth = 1) {
|
|
38
|
+
if (depth > 64) refusal(`dictionary or array nesting over 64`, `at most 64 levels`);
|
|
39
|
+
let cursor = at + (dictionary ? 2 : 1);
|
|
40
|
+
while (cursor < source.length) {
|
|
41
|
+
const character = source[cursor];
|
|
42
|
+
const nested = skipCompositeConstruct(source, cursor, depth);
|
|
43
|
+
if (nested !== void 0) {
|
|
44
|
+
cursor = nested;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (dictionary && character === ">" && source[cursor + 1] === ">") return cursor + 2;
|
|
48
|
+
if (!dictionary && character === "]") return cursor + 1;
|
|
49
|
+
cursor++;
|
|
50
|
+
}
|
|
51
|
+
return refusal(`truncated ${dictionary ? "dictionary" : "array"}`, dictionary ? "a closing >>" : "a closing ]");
|
|
52
|
+
}
|
|
53
|
+
function skipCompositeConstruct(source, cursor, depth) {
|
|
54
|
+
const character = source[cursor];
|
|
55
|
+
if (whitespace(character)) return cursor + 1;
|
|
56
|
+
if (character === "%") return skipComment(source, cursor);
|
|
57
|
+
if (character === "(") return skipLiteral(source, cursor);
|
|
58
|
+
if (character === "<") return source[cursor + 1] === "<" ? skipComposite(source, cursor, true, depth + 1) : skipHex(source, cursor);
|
|
59
|
+
if (character === "[") return skipComposite(source, cursor, false, depth + 1);
|
|
60
|
+
}
|
|
61
|
+
function readToken(source, cursor) {
|
|
62
|
+
const start = cursor;
|
|
63
|
+
if (source[cursor] === "/") {
|
|
64
|
+
cursor++;
|
|
65
|
+
while (cursor < source.length && !whitespace(source[cursor]) && !delimiter(source[cursor])) cursor++;
|
|
66
|
+
return {
|
|
67
|
+
start,
|
|
68
|
+
end: cursor,
|
|
69
|
+
raw: source.slice(start, cursor),
|
|
70
|
+
kind: "name"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (delimiter(source[cursor])) return void 0;
|
|
74
|
+
while (cursor < source.length && !whitespace(source[cursor]) && !delimiter(source[cursor])) cursor++;
|
|
75
|
+
const raw = source.slice(start, cursor);
|
|
76
|
+
return {
|
|
77
|
+
start,
|
|
78
|
+
end: cursor,
|
|
79
|
+
raw,
|
|
80
|
+
kind: numberPattern.test(raw) ? "number" : "operator"
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function inlineHeaderToken(source, at) {
|
|
84
|
+
let cursor = at;
|
|
85
|
+
while (whitespace(source[cursor])) cursor++;
|
|
86
|
+
if (source[cursor] === "[") {
|
|
87
|
+
const end = skipComposite(source, cursor, false);
|
|
88
|
+
return {
|
|
89
|
+
end,
|
|
90
|
+
raw: source.slice(cursor, end)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const token = readToken(source, cursor);
|
|
94
|
+
return token ? {
|
|
95
|
+
end: token.end,
|
|
96
|
+
raw: token.raw
|
|
97
|
+
} : {
|
|
98
|
+
end: cursor + 1,
|
|
99
|
+
raw: ""
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function inlineHeader(source, at) {
|
|
103
|
+
let cursor = at;
|
|
104
|
+
let name = "";
|
|
105
|
+
const values = /* @__PURE__ */ new Map();
|
|
106
|
+
while (cursor < source.length) {
|
|
107
|
+
const token = inlineHeaderToken(source, cursor);
|
|
108
|
+
cursor = token.end;
|
|
109
|
+
if (token.raw === "ID") return {
|
|
110
|
+
colorSpace: values.get("/CS") ?? values.get("/ColorSpace"),
|
|
111
|
+
id: cursor,
|
|
112
|
+
imageMask: values.get("/IM") === "true" || values.get("/ImageMask") === "true"
|
|
113
|
+
};
|
|
114
|
+
if (!token.raw) continue;
|
|
115
|
+
if (!name) name = token.raw;
|
|
116
|
+
else {
|
|
117
|
+
values.set(name, token.raw);
|
|
118
|
+
name = "";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return refusal("truncated BI operator", "an ID and EI pair");
|
|
122
|
+
}
|
|
123
|
+
function inlineImageEnd(source, at) {
|
|
124
|
+
const { colorSpace, id, imageMask } = inlineHeader(source, at);
|
|
125
|
+
const match = /[\0\t\n\f\r ]EI(?=[\0\t\n\f\r ()<>[\]{}/%]|$)/g;
|
|
126
|
+
match.lastIndex = id;
|
|
127
|
+
const end = match.exec(source);
|
|
128
|
+
if (!end) return refusal("truncated BI operator", "an ID and EI pair");
|
|
129
|
+
const name = colorSpace?.startsWith("/") ? colorSpace.slice(1) : void 0;
|
|
130
|
+
const policy = inlineImageAction(name, imageMask);
|
|
131
|
+
if (policy.action === "refuse") refusal(`BI operator with color space ${policy.label}`, policy.supported);
|
|
132
|
+
return end.index + end[0].length;
|
|
133
|
+
}
|
|
134
|
+
function skippedConstruct(source, cursor) {
|
|
135
|
+
const character = source[cursor];
|
|
136
|
+
if (whitespace(character)) return cursor + 1;
|
|
137
|
+
if (character === "%") return skipComment(source, cursor);
|
|
138
|
+
if (character === "(") return skipLiteral(source, cursor);
|
|
139
|
+
if (character === "<") return source[cursor + 1] === "<" ? skipComposite(source, cursor, true) : skipHex(source, cursor);
|
|
140
|
+
if (character === "[") return skipComposite(source, cursor, false);
|
|
141
|
+
}
|
|
142
|
+
function numeric(operands, count, operator) {
|
|
143
|
+
if (operands.length !== count || operands.some((token) => token.kind !== "number")) refusal(`${operator} operator operands`, `${count} numeric operands`);
|
|
144
|
+
return operands.map((token) => Number(token.raw));
|
|
145
|
+
}
|
|
146
|
+
function replacement(token, operands, values) {
|
|
147
|
+
const operator = token.raw === "rg" || token.raw === "g" ? "k" : "K";
|
|
148
|
+
return {
|
|
149
|
+
start: operands[0]?.start ?? token.start,
|
|
150
|
+
end: token.end,
|
|
151
|
+
text: `${values.map(formatPdfNumber).join(" ")} ${operator}`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function replaceRgb(token, operands, convert, counts) {
|
|
155
|
+
const [red = 0, green = 0, blue = 0] = numeric(operands, 3, token.raw);
|
|
156
|
+
counts[token.raw === "rg" ? "fills" : "strokes"]++;
|
|
157
|
+
return replacement(token, operands, convert([
|
|
158
|
+
red,
|
|
159
|
+
green,
|
|
160
|
+
blue
|
|
161
|
+
]));
|
|
162
|
+
}
|
|
163
|
+
function replaceGray(token, operands, convert, counts) {
|
|
164
|
+
const [gray = 0] = numeric(operands, 1, token.raw);
|
|
165
|
+
counts[token.raw === "g" ? "fills" : "strokes"]++;
|
|
166
|
+
return replacement(token, operands, convert([
|
|
167
|
+
gray,
|
|
168
|
+
gray,
|
|
169
|
+
gray
|
|
170
|
+
]));
|
|
171
|
+
}
|
|
172
|
+
function validateSpace(token, operands) {
|
|
173
|
+
if (operands.length !== 1 || operands[0]?.kind !== "name") refusal(`${token.raw} operator operands`, "one color space name");
|
|
174
|
+
const name = operands[0].raw.slice(1);
|
|
175
|
+
const policy = contentOperatorAction(token.raw, false, name);
|
|
176
|
+
if (policy.action === "refuse") refusal(`${token.raw} operator selecting ${colorSpaceLabel({ name })}`, policy.supported);
|
|
177
|
+
}
|
|
178
|
+
function enforceOperatorPolicy(token, operands, policy) {
|
|
179
|
+
if (policy.action !== "refuse") return;
|
|
180
|
+
const operator = token.raw;
|
|
181
|
+
if (operator === "cs" || operator === "CS") {
|
|
182
|
+
validateSpace(token, operands);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
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
|
+
}
|
|
188
|
+
function colorOperator(token, operands, convert, counts, inspect) {
|
|
189
|
+
const operator = token.raw;
|
|
190
|
+
if (!isContentColorOperator(operator)) return void 0;
|
|
191
|
+
const patternName = operands.length === 1 && operands[0]?.kind === "name";
|
|
192
|
+
enforceOperatorPolicy(token, operands, contentOperatorAction(operator, inspect, void 0, patternName));
|
|
193
|
+
if (operator === "rg" || operator === "RG") return replaceRgb(token, operands, convert, counts);
|
|
194
|
+
if (operator === "g" || operator === "G") return replaceGray(token, operands, convert, counts);
|
|
195
|
+
if (operator === "cs" || operator === "CS") validateSpace(token, operands);
|
|
196
|
+
else if (operator === "k" || operator === "K") {
|
|
197
|
+
numeric(operands, 4, operator);
|
|
198
|
+
counts[operator === "k" ? "fills" : "strokes"]++;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function scan(source, convert, inspect, visit, operatorVisit) {
|
|
202
|
+
const counts = {
|
|
203
|
+
fills: 0,
|
|
204
|
+
strokes: 0
|
|
205
|
+
};
|
|
206
|
+
let operands = [];
|
|
207
|
+
let cursor = 0;
|
|
208
|
+
while (cursor < source.length) {
|
|
209
|
+
const skipped = skippedConstruct(source, cursor);
|
|
210
|
+
if (skipped !== void 0) {
|
|
211
|
+
cursor = skipped;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const token = readToken(source, cursor);
|
|
215
|
+
if (!token) {
|
|
216
|
+
cursor++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
cursor = token.end;
|
|
220
|
+
if (token.raw === "BI") {
|
|
221
|
+
cursor = inlineImageEnd(source, cursor);
|
|
222
|
+
operands = [];
|
|
223
|
+
} else if (token.kind === "operator") {
|
|
224
|
+
operatorVisit?.(token, operands);
|
|
225
|
+
const found = colorOperator(token, operands, convert, counts, inspect);
|
|
226
|
+
if (found) visit?.(found);
|
|
227
|
+
operands = [];
|
|
228
|
+
} else {
|
|
229
|
+
if (operands.length >= 32) refusal(`more than 32 operands before an operator`, `at most 32 operands`);
|
|
230
|
+
operands.push(token);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return counts;
|
|
234
|
+
}
|
|
235
|
+
function visitContentTokens(bytes, visit) {
|
|
236
|
+
visitContentOperations(bytes, ({ operator, operands }) => visit(operator, operands));
|
|
237
|
+
}
|
|
238
|
+
function visitContentOperations(bytes, visit) {
|
|
239
|
+
scan(decoder.decode(bytes), () => [
|
|
240
|
+
0,
|
|
241
|
+
0,
|
|
242
|
+
0,
|
|
243
|
+
0
|
|
244
|
+
], false, void 0, (operator, operands) => {
|
|
245
|
+
visit({
|
|
246
|
+
end: operator.end,
|
|
247
|
+
operands: operands.flatMap((operand) => operand.kind === "name" || operand.kind === "number" ? [{
|
|
248
|
+
end: operand.end,
|
|
249
|
+
kind: operand.kind,
|
|
250
|
+
raw: operand.raw,
|
|
251
|
+
start: operand.start
|
|
252
|
+
}] : []),
|
|
253
|
+
operator: operator.raw,
|
|
254
|
+
start: operator.start
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function inspectContent(bytes) {
|
|
259
|
+
return scan(decoder.decode(bytes), () => refusal("RGB color operator after conversion", "CMYK operators"), true);
|
|
260
|
+
}
|
|
261
|
+
function rewriteContent(bytes, convert) {
|
|
262
|
+
const source = decoder.decode(bytes);
|
|
263
|
+
let outputLength = bytes.length;
|
|
264
|
+
let changed = false;
|
|
265
|
+
const counts = scan(source, convert, false, (found) => {
|
|
266
|
+
changed = true;
|
|
267
|
+
outputLength += encoder.encode(found.text).length - (found.end - found.start);
|
|
268
|
+
});
|
|
269
|
+
if (!changed) return {
|
|
270
|
+
bytes,
|
|
271
|
+
counts
|
|
272
|
+
};
|
|
273
|
+
const output = new Uint8Array(outputLength);
|
|
274
|
+
let inputAt = 0;
|
|
275
|
+
let outputAt = 0;
|
|
276
|
+
scan(source, convert, false, (found) => {
|
|
277
|
+
const untouched = bytes.subarray(inputAt, found.start);
|
|
278
|
+
output.set(untouched, outputAt);
|
|
279
|
+
outputAt += untouched.length;
|
|
280
|
+
const inserted = encoder.encode(found.text);
|
|
281
|
+
output.set(inserted, outputAt);
|
|
282
|
+
outputAt += inserted.length;
|
|
283
|
+
inputAt = found.end;
|
|
284
|
+
});
|
|
285
|
+
output.set(bytes.subarray(inputAt), outputAt);
|
|
286
|
+
return {
|
|
287
|
+
bytes: output,
|
|
288
|
+
counts
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
export { inspectContent, rewriteContent, visitContentOperations, visitContentTokens };
|