@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,275 @@
|
|
|
1
|
+
import { RenderRefusedError } from "../errors.js";
|
|
2
|
+
import { cachedPressColor, pressColor } from "./color-policy.js";
|
|
3
|
+
import { groupAction, shadingAction } from "./policy.js";
|
|
4
|
+
import { rewriteContent } from "./content.js";
|
|
5
|
+
import { arrayNumbers, decodeContent, dictionaryOf, nameOf, numberOf, replaceArray, resolvedColorSpace } from "./pdf-objects.js";
|
|
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";
|
|
11
|
+
import { inspectPressPdf } from "./report.js";
|
|
12
|
+
import { PDFDict, PDFDocument, PDFHexString, PDFName, PDFRef, PDFString } from "pdf-lib";
|
|
13
|
+
import { deflateSync } from "node:zlib";
|
|
14
|
+
//#region src/press/adapter.ts
|
|
15
|
+
const key = (name) => PDFName.of(name);
|
|
16
|
+
const refuse = (found, supported) => {
|
|
17
|
+
throw new RenderRefusedError(`The PDF has unsupported ${found}; supported: ${supported}.`);
|
|
18
|
+
};
|
|
19
|
+
const resolved = (state, object) => object instanceof PDFRef ? state.document.context.lookup(object) : object;
|
|
20
|
+
function convertFunction(state, fn, policy) {
|
|
21
|
+
if (policy.action === "refuse") refuse(policy.found, policy.supported);
|
|
22
|
+
if (!fn) return;
|
|
23
|
+
if (numberOf(fn, "FunctionType") === 2) {
|
|
24
|
+
for (const name of ["C0", "C1"]) replaceArray(fn, name, state.convert(arrayNumbers(fn, name, 3)));
|
|
25
|
+
if (fn.has(key("Range"))) replaceArray(fn, "Range", [
|
|
26
|
+
0,
|
|
27
|
+
1,
|
|
28
|
+
0,
|
|
29
|
+
1,
|
|
30
|
+
0,
|
|
31
|
+
1,
|
|
32
|
+
0,
|
|
33
|
+
1
|
|
34
|
+
]);
|
|
35
|
+
state.totals.gradientStops += 2;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (fn.has(key("Range"))) replaceArray(fn, "Range", [
|
|
39
|
+
0,
|
|
40
|
+
1,
|
|
41
|
+
0,
|
|
42
|
+
1,
|
|
43
|
+
0,
|
|
44
|
+
1,
|
|
45
|
+
0,
|
|
46
|
+
1
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
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) {
|
|
83
|
+
if (state.seenShadings.has(shading)) return false;
|
|
84
|
+
state.seenShadings.add(shading);
|
|
85
|
+
const type = numberOf(shading, "ShadingType");
|
|
86
|
+
const functionType = functionTypeOf(state, shading);
|
|
87
|
+
const space = resolvedColorSpace(state.document, resolved(state, shading.get(key("ColorSpace"))));
|
|
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
|
+
}
|
|
93
|
+
if (policy.action === "keep") return false;
|
|
94
|
+
if (shading.has(key("Background"))) replaceArray(shading, "Background", state.convert(arrayNumbers(shading, "Background", 3)));
|
|
95
|
+
shading.set(key("ColorSpace"), key("DeviceCMYK"));
|
|
96
|
+
if (type === 1) {
|
|
97
|
+
convertType1Shading(state, shading, pattern);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
function convertGroup(state, owner, maskSource) {
|
|
103
|
+
const group = owner.lookupMaybe(key("Group"), PDFDict);
|
|
104
|
+
if (!group || nameOf(group.get(key("S"))) !== "Transparency" || state.seenGroups.has(group)) return;
|
|
105
|
+
state.seenGroups.add(group);
|
|
106
|
+
const space = resolvedColorSpace(state.document, resolved(state, group.get(key("CS"))));
|
|
107
|
+
const policy = groupAction(space.kind, space.label, maskSource);
|
|
108
|
+
if (policy.action === "refuse") refuse(`transparency group color space ${space.label}`, policy.supported);
|
|
109
|
+
if (policy.action === "convert") {
|
|
110
|
+
group.set(key("CS"), key("DeviceCMYK"));
|
|
111
|
+
state.totals.groups++;
|
|
112
|
+
}
|
|
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
|
+
}
|
|
155
|
+
function streamDictionary(stream) {
|
|
156
|
+
const dictionary = { Filter: key("FlateDecode") };
|
|
157
|
+
for (const [name, value] of stream.dict.entries()) {
|
|
158
|
+
const label = name.toString().slice(1);
|
|
159
|
+
if (![
|
|
160
|
+
"Filter",
|
|
161
|
+
"DecodeParms",
|
|
162
|
+
"Length"
|
|
163
|
+
].includes(label)) dictionary[label] = value;
|
|
164
|
+
}
|
|
165
|
+
return dictionary;
|
|
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
|
+
}
|
|
188
|
+
function convertContent(state, object, stream, setDirect) {
|
|
189
|
+
const bytes = decodeContent(stream, state.budget);
|
|
190
|
+
const rewritten = rewriteContent(bytes, state.convert);
|
|
191
|
+
state.totals.fills += rewritten.counts.fills;
|
|
192
|
+
state.totals.strokes += rewritten.counts.strokes;
|
|
193
|
+
const replacement = state.document.context.stream(deflateSync(rewritten.bytes), streamDictionary(stream));
|
|
194
|
+
const reference = object instanceof PDFRef ? object : state.document.context.getObjectRef(object);
|
|
195
|
+
if (reference) state.document.context.assign(reference, replacement);
|
|
196
|
+
else setDirect(replacement);
|
|
197
|
+
}
|
|
198
|
+
function newState(document, profile) {
|
|
199
|
+
return {
|
|
200
|
+
budget: {
|
|
201
|
+
calculatorOperations: 0,
|
|
202
|
+
decodedBytes: 0,
|
|
203
|
+
shadingSamples: 0
|
|
204
|
+
},
|
|
205
|
+
convert: cachedPressColor(profile),
|
|
206
|
+
document,
|
|
207
|
+
painted: /* @__PURE__ */ new WeakMap(),
|
|
208
|
+
profile,
|
|
209
|
+
seenGroups: /* @__PURE__ */ new WeakSet(),
|
|
210
|
+
seenShadings: /* @__PURE__ */ new WeakSet(),
|
|
211
|
+
shadingPainted: /* @__PURE__ */ new WeakMap(),
|
|
212
|
+
shadingPatterns: /* @__PURE__ */ new WeakMap(),
|
|
213
|
+
totals: {
|
|
214
|
+
fills: 0,
|
|
215
|
+
strokes: 0,
|
|
216
|
+
gradientStops: 0,
|
|
217
|
+
groups: 0
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function rewritePressDocument(document, profile) {
|
|
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
|
+
});
|
|
238
|
+
walkPressGraph(document, {
|
|
239
|
+
content: (object, stream, replace) => convertContent(state, object, stream, replace),
|
|
240
|
+
function: (fn, decision) => convertFunction(state, fn, decision),
|
|
241
|
+
group: (owner, _stream, maskSource) => convertGroup(state, owner, maskSource),
|
|
242
|
+
image: (image) => convertEffectImage(state, image),
|
|
243
|
+
shading: (shading, pattern) => convertShading(state, shading, pattern)
|
|
244
|
+
});
|
|
245
|
+
return state.totals;
|
|
246
|
+
}
|
|
247
|
+
function addOutputIntent(document, profile) {
|
|
248
|
+
const profileRef = document.context.register(document.context.stream(profile, { N: 4 }));
|
|
249
|
+
const intentRef = document.context.register(document.context.obj({
|
|
250
|
+
Type: "OutputIntent",
|
|
251
|
+
S: "GTS_PDFX",
|
|
252
|
+
OutputConditionIdentifier: PDFString.of("Custom"),
|
|
253
|
+
DestOutputProfile: profileRef
|
|
254
|
+
}));
|
|
255
|
+
document.catalog.set(key("OutputIntents"), document.context.obj([intentRef]));
|
|
256
|
+
}
|
|
257
|
+
async function rewritePressPdf(bytes, profileBytes) {
|
|
258
|
+
try {
|
|
259
|
+
const profile = readProfile(profileBytes);
|
|
260
|
+
if (profile.colorSpace !== "CMYK") refuse(`press profile color space ${profile.colorSpace}`, "CMYK");
|
|
261
|
+
const document = await PDFDocument.load(bytes, { updateMetadata: false });
|
|
262
|
+
rewritePressDocument(document, profile);
|
|
263
|
+
addOutputIntent(document, profileBytes);
|
|
264
|
+
const output = await document.save({ useObjectStreams: false });
|
|
265
|
+
return {
|
|
266
|
+
bytes: output,
|
|
267
|
+
report: await inspectPressPdf(output)
|
|
268
|
+
};
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error instanceof RenderRefusedError) throw error;
|
|
271
|
+
throw new RenderRefusedError("The PDF has unsupported malformed structure; supported: a valid PDF produced by the renderer.");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
export { rewritePressDocument, rewritePressPdf };
|
|
@@ -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 };
|