@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.
Files changed (42) hide show
  1. package/dist/color/binary.js +27 -0
  2. package/dist/color/black-point.js +121 -0
  3. package/dist/color/engine.js +18 -0
  4. package/dist/color/interpolation.js +74 -0
  5. package/dist/color/profile.js +98 -0
  6. package/dist/color/spaces.js +138 -0
  7. package/dist/color/tables.js +255 -0
  8. package/dist/color/types.js +21 -0
  9. package/dist/deck-pdf.js +152 -0
  10. package/dist/effects/adapter.js +336 -0
  11. package/dist/effects/content-replacement.js +35 -0
  12. package/dist/effects/function-evaluator.js +72 -0
  13. package/dist/effects/policy.js +10 -0
  14. package/dist/effects/rule-1.js +40 -0
  15. package/dist/effects/rule-c.js +354 -0
  16. package/dist/effects/structure.js +86 -0
  17. package/dist/effects/types.d.ts +15 -0
  18. package/dist/effects/types.js +4 -0
  19. package/dist/errors.d.ts +8 -0
  20. package/dist/errors.js +9 -0
  21. package/dist/index.d.ts +18 -8
  22. package/dist/index.js +96 -76
  23. package/dist/page-paper.js +94 -0
  24. package/dist/pages-imposition.js +65 -0
  25. package/dist/pages-pdf.js +67 -0
  26. package/dist/pdf-compose.js +35 -0
  27. package/dist/pdf-geometry.js +27 -0
  28. package/dist/press/adapter.js +275 -0
  29. package/dist/press/affine.js +60 -0
  30. package/dist/press/calculator-ops.js +277 -0
  31. package/dist/press/calculator.js +130 -0
  32. package/dist/press/color-policy.js +31 -0
  33. package/dist/press/content.js +292 -0
  34. package/dist/press/pattern-bounds.js +275 -0
  35. package/dist/press/pattern-path.js +102 -0
  36. package/dist/press/pdf-objects.js +88 -0
  37. package/dist/press/policy.js +126 -0
  38. package/dist/press/report.js +89 -0
  39. package/dist/press/sample-shading.js +266 -0
  40. package/dist/press/types.d.ts +18 -0
  41. package/dist/press/visitor.js +163 -0
  42. 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 };