@segnavia/render 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/color/profile.js +1 -1
- package/dist/color/tables.js +1 -1
- package/dist/deck-pdf.js +4 -2
- package/dist/effects/adapter.js +336 -0
- package/dist/effects/content-replacement.js +35 -0
- package/dist/effects/function-evaluator.js +72 -0
- package/dist/effects/policy.js +10 -0
- package/dist/effects/rule-1.js +40 -0
- package/dist/effects/rule-c.js +354 -0
- package/dist/effects/structure.js +86 -0
- package/dist/effects/types.d.ts +15 -0
- package/dist/effects/types.js +4 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +18 -6
- package/dist/overflow-containment.js +7 -0
- package/dist/pages-pdf.js +1 -0
- package/dist/press/adapter.js +141 -11
- package/dist/press/affine.js +60 -0
- package/dist/press/calculator-ops.js +277 -0
- package/dist/press/calculator.js +130 -0
- package/dist/press/content.js +24 -1
- package/dist/press/pattern-bounds.js +275 -0
- package/dist/press/pattern-path.js +102 -0
- package/dist/press/policy.js +7 -1
- package/dist/press/report.js +13 -3
- package/dist/press/sample-shading.js +266 -0
- package/dist/press/types.d.ts +2 -0
- package/dist/press/visitor.js +55 -32
- package/package.json +3 -3
package/dist/press/adapter.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { RenderRefusedError } from "../errors.js";
|
|
2
|
-
import {
|
|
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
|
|
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")
|
|
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
|
|
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: {
|
|
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: () =>
|
|
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 };
|