@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.
@@ -1,12 +1,15 @@
1
1
  import { RenderRefusedError } from "../errors.js";
2
- import { readProfile } from "../color/profile.js";
3
- import { cachedPressColor } from "./color-policy.js";
2
+ import { cachedPressColor, pressColor } from "./color-policy.js";
4
3
  import { groupAction, shadingAction } from "./policy.js";
5
4
  import { rewriteContent } from "./content.js";
6
- import { arrayNumbers, decodeContent, nameOf, numberOf, replaceArray, resolvedColorSpace } from "./pdf-objects.js";
5
+ import { arrayNumbers, decodeContent, dictionaryOf, nameOf, numberOf, replaceArray, resolvedColorSpace } from "./pdf-objects.js";
7
6
  import { walkPressGraph } from "./visitor.js";
7
+ import "../effects/types.js";
8
+ import { replaceType1Function, sampleDomainForUses, shadingDeviceMatrix, shadingDomainOf, shadingSampleGridForUses } from "./sample-shading.js";
9
+ import { readProfile } from "../color/profile.js";
10
+ import { createPaintedBoundsTracker, recordPatternPaintedBounds } from "./pattern-bounds.js";
8
11
  import { inspectPressPdf } from "./report.js";
9
- import { PDFDict, PDFDocument, PDFName, PDFRef, PDFString } from "pdf-lib";
12
+ import { PDFDict, PDFDocument, PDFHexString, PDFName, PDFRef, PDFString } from "pdf-lib";
10
13
  import { deflateSync } from "node:zlib";
11
14
  //#region src/press/adapter.ts
12
15
  const key = (name) => PDFName.of(name);
@@ -43,16 +46,57 @@ function convertFunction(state, fn, policy) {
43
46
  1
44
47
  ]);
45
48
  }
46
- function convertShading(state, shading) {
49
+ function functionTypeOf(state, shading) {
50
+ const fn = dictionaryOf(resolved(state, shading.get(key("Function"))));
51
+ return fn ? numberOf(fn, "FunctionType") : void 0;
52
+ }
53
+ function indexShadingPattern(state, shading, pattern) {
54
+ if (!pattern) return false;
55
+ const list = state.shadingPatterns.get(shading) ?? [];
56
+ if (!list.includes(pattern)) list.push(pattern);
57
+ state.shadingPatterns.set(shading, list);
58
+ return false;
59
+ }
60
+ function shadingUses(state, shading, pattern) {
61
+ const patterns = [...state.shadingPatterns.get(shading) ?? []];
62
+ if (pattern && !patterns.includes(pattern)) patterns.push(pattern);
63
+ const uses = patterns.map((found) => ({
64
+ matrix: shadingDeviceMatrix(shading, found),
65
+ painted: state.painted.get(found),
66
+ pattern: found
67
+ }));
68
+ const shPaint = state.shadingPainted.get(shading);
69
+ if (shPaint !== void 0) uses.push({
70
+ matrix: shadingDeviceMatrix(shading, void 0),
71
+ painted: shPaint
72
+ });
73
+ return uses.filter((use) => use.painted !== void 0);
74
+ }
75
+ function convertType1Shading(state, shading, pattern) {
76
+ const uses = shadingUses(state, shading, pattern);
77
+ const domain = sampleDomainForUses(shadingDomainOf(shading), uses);
78
+ const matrices = uses.length > 0 ? uses.map((use) => use.matrix) : [shadingDeviceMatrix(shading, pattern)];
79
+ const size = shadingSampleGridForUses(domain, matrices);
80
+ replaceType1Function(state.document, shading, pattern, (rgb) => pressColor(state.profile, rgb), state.budget, void 0, domain, size);
81
+ }
82
+ function convertShading(state, shading, pattern) {
47
83
  if (state.seenShadings.has(shading)) return false;
48
84
  state.seenShadings.add(shading);
49
85
  const type = numberOf(shading, "ShadingType");
86
+ const functionType = functionTypeOf(state, shading);
50
87
  const space = resolvedColorSpace(state.document, resolved(state, shading.get(key("ColorSpace"))));
51
- const policy = shadingAction(space.kind, type);
52
- if (policy.action === "refuse") refuse(`ShadingType ${type ?? "missing"} color space ${space.label}`, policy.supported);
88
+ const policy = shadingAction(space.kind, type, functionType);
89
+ if (policy.action === "refuse") {
90
+ if (type === 1) refuse(`ShadingType 1 FunctionType ${functionType ?? "missing"}`, policy.supported);
91
+ refuse(`ShadingType ${type ?? "missing"} color space ${space.label}`, policy.supported);
92
+ }
53
93
  if (policy.action === "keep") return false;
54
94
  if (shading.has(key("Background"))) replaceArray(shading, "Background", state.convert(arrayNumbers(shading, "Background", 3)));
55
95
  shading.set(key("ColorSpace"), key("DeviceCMYK"));
96
+ if (type === 1) {
97
+ convertType1Shading(state, shading, pattern);
98
+ return false;
99
+ }
56
100
  return true;
57
101
  }
58
102
  function convertGroup(state, owner, maskSource) {
@@ -67,6 +111,47 @@ function convertGroup(state, owner, maskSource) {
67
111
  state.totals.groups++;
68
112
  }
69
113
  }
114
+ function convertRuleCImage(state, image, width, height, dictionary) {
115
+ const source = decodeContent(image, state.budget);
116
+ if (source.length !== width * height * 3) refuse("effect image sample length", "three 8-bit RGB samples per pixel");
117
+ const converted = new Uint8Array(width * height * 4);
118
+ for (let pixel = 0; pixel < width * height; pixel++) {
119
+ const color = state.convert([
120
+ (source[pixel * 3] ?? 0) / 255,
121
+ (source[pixel * 3 + 1] ?? 0) / 255,
122
+ (source[pixel * 3 + 2] ?? 0) / 255
123
+ ]);
124
+ for (let channel = 0; channel < 4; channel++) converted[pixel * 4 + channel] = Math.round(Math.max(0, Math.min(1, color[channel] ?? 0)) * 255);
125
+ }
126
+ dictionary.ColorSpace = key("DeviceCMYK");
127
+ const replacement = state.document.context.stream(deflateSync(converted), dictionary);
128
+ const reference = state.document.context.getObjectRef(image);
129
+ if (reference) state.document.context.assign(reference, replacement);
130
+ }
131
+ function convertEffectImage(state, image) {
132
+ if (!image.dict.has(key("SegnaviaGeneratedEffect")) || resolvedColorSpace(state.document, resolved(state, image.dict.get(key("ColorSpace")))).kind !== "rgb") return;
133
+ const width = numberOf(image.dict, "Width") ?? refuse("effect image Width missing", "a positive Width number");
134
+ const height = numberOf(image.dict, "Height") ?? refuse("effect image Height missing", "a positive Height number");
135
+ if (width <= 0 || height <= 0) refuse("effect image dimensions", "positive Width and Height numbers");
136
+ const dictionary = streamDictionary(image);
137
+ if (!image.dict.has(key("SegnaviaEffectColor"))) {
138
+ convertRuleCImage(state, image, width, height, dictionary);
139
+ return;
140
+ }
141
+ const source = arrayNumbers(image.dict, "SegnaviaEffectColor", 3);
142
+ const color = state.convert(source);
143
+ const palette = color.map((value) => Math.round(Math.max(0, Math.min(1, value)) * 255));
144
+ dictionary.ColorSpace = state.document.context.obj([
145
+ "Indexed",
146
+ "DeviceCMYK",
147
+ 0,
148
+ PDFHexString.of(Buffer.from(palette).toString("hex"))
149
+ ]);
150
+ dictionary.SegnaviaEffectColor = state.document.context.obj(color);
151
+ const replacement = state.document.context.stream(image.contents, dictionary);
152
+ const reference = state.document.context.getObjectRef(image);
153
+ if (reference) state.document.context.assign(reference, replacement);
154
+ }
70
155
  function streamDictionary(stream) {
71
156
  const dictionary = { Filter: key("FlateDecode") };
72
157
  for (const [name, value] of stream.dict.entries()) {
@@ -79,8 +164,30 @@ function streamDictionary(stream) {
79
164
  }
80
165
  return dictionary;
81
166
  }
167
+ function resolveShadingPattern(state, resources, name) {
168
+ const patterns = resources?.lookupMaybe(key("Pattern"), PDFDict);
169
+ const found = dictionaryOf(resolved(state, patterns?.get(PDFName.of(name))));
170
+ if (found && numberOf(found, "PatternType") === 2) return found;
171
+ }
172
+ function resolveGraphicsState(state, resources, name) {
173
+ const states = resources?.lookupMaybe(key("ExtGState"), PDFDict);
174
+ return dictionaryOf(resolved(state, states?.get(PDFName.of(name))));
175
+ }
176
+ function resolveShadingResource(state, resources, name) {
177
+ const shadings = resources?.lookupMaybe(key("Shading"), PDFDict);
178
+ return dictionaryOf(resolved(state, shadings?.get(PDFName.of(name))));
179
+ }
180
+ function recordContentBounds(state, stream, resources, budget) {
181
+ const bytes = decodeContent(stream, budget);
182
+ const resolve = (name) => resolveShadingPattern(state, resources, name);
183
+ const resolveGs = (name) => resolveGraphicsState(state, resources, name);
184
+ const resolveShading = (name) => resolveShadingResource(state, resources, name);
185
+ if (state.sequence) state.sequence.feed(bytes, resolve, resolveGs, resolveShading);
186
+ else recordPatternPaintedBounds(bytes, resolve, state.painted, resolveGs, resolveShading, state.shadingPainted);
187
+ }
82
188
  function convertContent(state, object, stream, setDirect) {
83
- const rewritten = rewriteContent(decodeContent(stream, state.budget), state.convert);
189
+ const bytes = decodeContent(stream, state.budget);
190
+ const rewritten = rewriteContent(bytes, state.convert);
84
191
  state.totals.fills += rewritten.counts.fills;
85
192
  state.totals.strokes += rewritten.counts.strokes;
86
193
  const replacement = state.document.context.stream(deflateSync(rewritten.bytes), streamDictionary(stream));
@@ -90,11 +197,19 @@ function convertContent(state, object, stream, setDirect) {
90
197
  }
91
198
  function newState(document, profile) {
92
199
  return {
93
- budget: { decodedBytes: 0 },
200
+ budget: {
201
+ calculatorOperations: 0,
202
+ decodedBytes: 0,
203
+ shadingSamples: 0
204
+ },
94
205
  convert: cachedPressColor(profile),
95
206
  document,
207
+ painted: /* @__PURE__ */ new WeakMap(),
208
+ profile,
96
209
  seenGroups: /* @__PURE__ */ new WeakSet(),
97
210
  seenShadings: /* @__PURE__ */ new WeakSet(),
211
+ shadingPainted: /* @__PURE__ */ new WeakMap(),
212
+ shadingPatterns: /* @__PURE__ */ new WeakMap(),
98
213
  totals: {
99
214
  fills: 0,
100
215
  strokes: 0,
@@ -105,12 +220,27 @@ function newState(document, profile) {
105
220
  }
106
221
  function rewritePressDocument(document, profile) {
107
222
  const state = newState(document, profile);
223
+ const boundsBudget = { decodedBytes: 0 };
224
+ walkPressGraph(document, {
225
+ beginContentsSequence: () => {
226
+ state.sequence = createPaintedBoundsTracker(state.painted, state.shadingPainted);
227
+ },
228
+ content: (_object, stream, _replace, resources) => recordContentBounds(state, stream, resources, boundsBudget),
229
+ endContentsSequence: () => {
230
+ state.sequence = void 0;
231
+ },
232
+ function: () => void 0,
233
+ group: () => void 0,
234
+ image: () => void 0,
235
+ replayPageContents: true,
236
+ shading: (shading, pattern) => indexShadingPattern(state, shading, pattern)
237
+ });
108
238
  walkPressGraph(document, {
109
239
  content: (object, stream, replace) => convertContent(state, object, stream, replace),
110
240
  function: (fn, decision) => convertFunction(state, fn, decision),
111
241
  group: (owner, _stream, maskSource) => convertGroup(state, owner, maskSource),
112
- image: () => void 0,
113
- shading: (shading) => convertShading(state, shading)
242
+ image: (image) => convertEffectImage(state, image),
243
+ shading: (shading, pattern) => convertShading(state, shading, pattern)
114
244
  });
115
245
  return state.totals;
116
246
  }
@@ -0,0 +1,60 @@
1
+ //#region src/press/affine.ts
2
+ const IDENTITY_MATRIX = [
3
+ 1,
4
+ 0,
5
+ 0,
6
+ 1,
7
+ 0,
8
+ 0
9
+ ];
10
+ function affineComponent(values, index) {
11
+ return values[index] ?? 0;
12
+ }
13
+ function multiplyAffine(left, right) {
14
+ const a0 = affineComponent(left, 0);
15
+ const b0 = affineComponent(left, 1);
16
+ const c0 = affineComponent(left, 2);
17
+ const d0 = affineComponent(left, 3);
18
+ const e0 = affineComponent(left, 4);
19
+ const f0 = affineComponent(left, 5);
20
+ const a1 = affineComponent(right, 0);
21
+ const b1 = affineComponent(right, 1);
22
+ const c1 = affineComponent(right, 2);
23
+ const d1 = affineComponent(right, 3);
24
+ const e1 = affineComponent(right, 4);
25
+ const f1 = affineComponent(right, 5);
26
+ return [
27
+ a0 * a1 + c0 * b1,
28
+ b0 * a1 + d0 * b1,
29
+ a0 * c1 + c0 * d1,
30
+ b0 * c1 + d0 * d1,
31
+ a0 * e1 + c0 * f1 + e0,
32
+ b0 * e1 + d0 * f1 + f0
33
+ ];
34
+ }
35
+ function invertAffine(matrix) {
36
+ const a = affineComponent(matrix, 0);
37
+ const b = affineComponent(matrix, 1);
38
+ const c = affineComponent(matrix, 2);
39
+ const d = affineComponent(matrix, 3);
40
+ const e = affineComponent(matrix, 4);
41
+ const f = affineComponent(matrix, 5);
42
+ const det = a * d - b * c;
43
+ if (det === 0 || !Number.isFinite(det)) return void 0;
44
+ return [
45
+ d / det,
46
+ -b / det,
47
+ -c / det,
48
+ a / det,
49
+ (c * f - d * e) / det,
50
+ (b * e - a * f) / det
51
+ ];
52
+ }
53
+ function applyAffine(matrix, x, y) {
54
+ return {
55
+ x: affineComponent(matrix, 0) * x + affineComponent(matrix, 2) * y + affineComponent(matrix, 4),
56
+ y: affineComponent(matrix, 1) * x + affineComponent(matrix, 3) * y + affineComponent(matrix, 5)
57
+ };
58
+ }
59
+ //#endregion
60
+ export { IDENTITY_MATRIX, applyAffine, invertAffine, multiplyAffine };
@@ -0,0 +1,277 @@
1
+ import { RenderRefusedError } from "../errors.js";
2
+ function calculatorRefuse(found, supported) {
3
+ throw new RenderRefusedError(`The PDF has unsupported ${found}; supported: ${supported}.`);
4
+ }
5
+ function pop(state, operator) {
6
+ const value = state.stack.pop();
7
+ if (!value) return calculatorRefuse(`calculator ${operator} stack underflow`, "enough operands");
8
+ return value;
9
+ }
10
+ function classifiedOperatorName(name) {
11
+ return /^[A-Za-z][A-Za-z0-9]{0,15}$/.test(name) ? name : "other";
12
+ }
13
+ function pushStack(state, value) {
14
+ state.stack.push(value);
15
+ if (state.stack.length > 32) calculatorRefuse(`calculator stack depth ${state.stack.length}`, `at most 32 values`);
16
+ }
17
+ function popNumber(state, operator) {
18
+ const value = pop(state, operator);
19
+ if (value.kind !== "n") return calculatorRefuse(`calculator ${operator} operand type`, "a number");
20
+ return value;
21
+ }
22
+ function popInt(state, operator) {
23
+ const value = popNumber(state, operator);
24
+ if (!value.int || !Number.isInteger(value.value)) return calculatorRefuse(`calculator ${operator} operand type`, "an integer");
25
+ return value.value;
26
+ }
27
+ function popBool(state, operator) {
28
+ const value = pop(state, operator);
29
+ if (value.kind !== "b") return calculatorRefuse(`calculator ${operator} operand type`, "a boolean");
30
+ return value.value;
31
+ }
32
+ function popProc(state, operator) {
33
+ const value = pop(state, operator);
34
+ if (value.kind !== "p") return calculatorRefuse(`calculator ${operator} operand type`, "a procedure");
35
+ return value.body;
36
+ }
37
+ function pushNumber(state, value, int) {
38
+ if (!Number.isFinite(value)) calculatorRefuse("calculator numeric result", "a finite number");
39
+ pushStack(state, {
40
+ kind: "n",
41
+ value,
42
+ int: int && Number.isInteger(value)
43
+ });
44
+ }
45
+ function pushBool(state, value) {
46
+ pushStack(state, {
47
+ kind: "b",
48
+ value
49
+ });
50
+ }
51
+ function unary(operator, apply) {
52
+ return (state) => {
53
+ const operand = popNumber(state, operator);
54
+ const result = apply(operand.value, operand.int);
55
+ pushNumber(state, result.value, result.int);
56
+ };
57
+ }
58
+ function binary(operator, apply) {
59
+ return (state) => {
60
+ const right = popNumber(state, operator);
61
+ const left = popNumber(state, operator);
62
+ const result = apply(left.value, right.value, left.int && right.int);
63
+ pushNumber(state, result.value, result.int);
64
+ };
65
+ }
66
+ function compare(operator, apply) {
67
+ return (state) => {
68
+ const right = popNumber(state, operator);
69
+ pushBool(state, apply(popNumber(state, operator).value, right.value));
70
+ };
71
+ }
72
+ function equal(state, operator, negate) {
73
+ const right = pop(state, operator);
74
+ const left = pop(state, operator);
75
+ let same = false;
76
+ if (left.kind === "n" && right.kind === "n") same = left.value === right.value;
77
+ else if (left.kind === "b" && right.kind === "b") same = left.value === right.value;
78
+ else calculatorRefuse(`calculator ${operator} operand type`, "two numbers or two booleans");
79
+ pushBool(state, negate ? !same : same);
80
+ }
81
+ function psRound(value) {
82
+ return value < 0 ? -Math.round(-value) : Math.round(value);
83
+ }
84
+ function logicalOrBitwise(state, operator, bools, ints) {
85
+ const right = pop(state, operator);
86
+ const left = pop(state, operator);
87
+ if (left.kind === "b" && right.kind === "b") {
88
+ pushBool(state, bools(left.value, right.value));
89
+ return;
90
+ }
91
+ if (left.kind === "n" && right.kind === "n" && left.int && right.int) {
92
+ pushNumber(state, ints(left.value, right.value) | 0, true);
93
+ return;
94
+ }
95
+ calculatorRefuse(`calculator ${operator} operand type`, "two booleans or two integers");
96
+ }
97
+ const operators = {
98
+ abs: unary("abs", (value, int) => ({
99
+ value: Math.abs(value),
100
+ int
101
+ })),
102
+ add: binary("add", (left, right, ints) => ({
103
+ value: left + right,
104
+ int: ints
105
+ })),
106
+ atan: (state) => {
107
+ const den = popNumber(state, "atan");
108
+ const num = popNumber(state, "atan");
109
+ if (num.value === 0 && den.value === 0) {
110
+ pushNumber(state, 0, false);
111
+ return;
112
+ }
113
+ let angle = Math.atan2(num.value, den.value) * 180 / Math.PI;
114
+ if (angle < 0) angle += 360;
115
+ pushNumber(state, angle, false);
116
+ },
117
+ ceiling: unary("ceiling", (value, int) => ({
118
+ value: Math.ceil(value),
119
+ int
120
+ })),
121
+ cos: unary("cos", (value) => ({
122
+ value: Math.cos(value * Math.PI / 180),
123
+ int: false
124
+ })),
125
+ cvi: unary("cvi", (value) => ({
126
+ value: Math.trunc(value),
127
+ int: true
128
+ })),
129
+ cvr: unary("cvr", (value) => ({
130
+ value,
131
+ int: false
132
+ })),
133
+ div: binary("div", (left, right) => ({
134
+ value: left / right,
135
+ int: false
136
+ })),
137
+ exp: binary("exp", (left, right) => ({
138
+ value: left ** right,
139
+ int: false
140
+ })),
141
+ floor: unary("floor", (value, int) => ({
142
+ value: Math.floor(value),
143
+ int
144
+ })),
145
+ idiv: (state) => {
146
+ const right = popInt(state, "idiv");
147
+ const left = popInt(state, "idiv");
148
+ if (right === 0) calculatorRefuse("calculator idiv divisor 0", "a non-zero divisor");
149
+ pushNumber(state, Math.trunc(left / right), true);
150
+ },
151
+ ln: unary("ln", (value) => ({
152
+ value: Math.log(value),
153
+ int: false
154
+ })),
155
+ log: unary("log", (value) => ({
156
+ value: Math.log10(value),
157
+ int: false
158
+ })),
159
+ mod: (state) => {
160
+ const right = popInt(state, "mod");
161
+ const left = popInt(state, "mod");
162
+ if (right === 0) calculatorRefuse("calculator mod divisor 0", "a non-zero divisor");
163
+ pushNumber(state, left % right, true);
164
+ },
165
+ mul: binary("mul", (left, right, ints) => ({
166
+ value: left * right,
167
+ int: ints
168
+ })),
169
+ neg: unary("neg", (value, int) => ({
170
+ value: -value,
171
+ int
172
+ })),
173
+ round: unary("round", (value, int) => ({
174
+ value: psRound(value),
175
+ int
176
+ })),
177
+ sin: unary("sin", (value) => ({
178
+ value: Math.sin(value * Math.PI / 180),
179
+ int: false
180
+ })),
181
+ sqrt: unary("sqrt", (value) => ({
182
+ value: Math.sqrt(value),
183
+ int: false
184
+ })),
185
+ sub: binary("sub", (left, right, ints) => ({
186
+ value: left - right,
187
+ int: ints
188
+ })),
189
+ truncate: unary("truncate", (value, int) => ({
190
+ value: Math.trunc(value),
191
+ int
192
+ })),
193
+ and: (state) => logicalOrBitwise(state, "and", (left, right) => left && right, (left, right) => left & right),
194
+ bitshift: (state) => {
195
+ const shift = popInt(state, "bitshift");
196
+ const bits = popInt(state, "bitshift") | 0;
197
+ if (shift >= 32 || shift <= -32) {
198
+ pushNumber(state, 0, true);
199
+ return;
200
+ }
201
+ pushNumber(state, shift >= 0 ? bits << shift | 0 : bits >>> -shift | 0, true);
202
+ },
203
+ eq: (state) => equal(state, "eq", false),
204
+ false: (state) => pushBool(state, false),
205
+ ge: compare("ge", (left, right) => left >= right),
206
+ gt: compare("gt", (left, right) => left > right),
207
+ le: compare("le", (left, right) => left <= right),
208
+ lt: compare("lt", (left, right) => left < right),
209
+ ne: (state) => equal(state, "ne", true),
210
+ not: (state) => {
211
+ const value = pop(state, "not");
212
+ if (value.kind === "b") {
213
+ pushBool(state, !value.value);
214
+ return;
215
+ }
216
+ if (value.kind === "n" && value.int) {
217
+ pushNumber(state, ~value.value | 0, true);
218
+ return;
219
+ }
220
+ calculatorRefuse("calculator not operand type", "a boolean or an integer");
221
+ },
222
+ or: (state) => logicalOrBitwise(state, "or", (left, right) => left || right, (left, right) => left | right),
223
+ true: (state) => pushBool(state, true),
224
+ xor: (state) => logicalOrBitwise(state, "xor", (left, right) => left !== right, (left, right) => left ^ right),
225
+ if: (state, exec) => {
226
+ const body = popProc(state, "if");
227
+ if (popBool(state, "if")) exec(body);
228
+ },
229
+ ifelse: (state, exec) => {
230
+ const otherwise = popProc(state, "ifelse");
231
+ const thenBody = popProc(state, "ifelse");
232
+ exec(popBool(state, "ifelse") ? thenBody : otherwise);
233
+ },
234
+ copy: (state) => {
235
+ const count = popInt(state, "copy");
236
+ if (count < 0 || state.stack.length < count) calculatorRefuse("calculator copy count", "a non-negative integer within the stack");
237
+ const copies = state.stack.slice(state.stack.length - count);
238
+ for (const value of copies) pushStack(state, value);
239
+ },
240
+ dup: (state) => {
241
+ const value = pop(state, "dup");
242
+ pushStack(state, value);
243
+ pushStack(state, value);
244
+ },
245
+ exch: (state) => {
246
+ const top = pop(state, "exch");
247
+ const below = pop(state, "exch");
248
+ pushStack(state, top);
249
+ pushStack(state, below);
250
+ },
251
+ index: (state) => {
252
+ const n = popInt(state, "index");
253
+ if (n < 0 || n >= state.stack.length) calculatorRefuse("calculator index", "a non-negative integer within the stack");
254
+ const value = state.stack[state.stack.length - 1 - n];
255
+ if (value) pushStack(state, value);
256
+ },
257
+ pop: (state) => {
258
+ pop(state, "pop");
259
+ },
260
+ roll: (state) => {
261
+ const j = popInt(state, "roll");
262
+ const n = popInt(state, "roll");
263
+ if (n < 0 || state.stack.length < n) calculatorRefuse("calculator roll count", "a non-negative integer within the stack");
264
+ if (n === 0) return;
265
+ const shift = (j % n + n) % n;
266
+ const start = state.stack.length - n;
267
+ const slice = state.stack.splice(start, n);
268
+ state.stack.push(...slice.slice(n - shift), ...slice.slice(0, n - shift));
269
+ }
270
+ };
271
+ function applyOperator(name, state, exec) {
272
+ const handler = operators[name];
273
+ if (!handler) calculatorRefuse(`calculator operator ${classifiedOperatorName(name)}`, "the PDF calculator operators");
274
+ handler(state, exec);
275
+ }
276
+ //#endregion
277
+ export { applyOperator, calculatorRefuse, classifiedOperatorName, pushStack };
@@ -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 };