mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-12 08:14:02 +02:00
Add a js fallback for interpreting ps code
It's a basic stack based interpreter. A wasm version will come soon.
This commit is contained in:
parent
399fce6471
commit
8c7a5f3500
@ -15,7 +15,6 @@
|
||||
|
||||
import { Dict, Ref } from "./primitives.js";
|
||||
import {
|
||||
FeatureTest,
|
||||
FormatError,
|
||||
info,
|
||||
MathClamp,
|
||||
@ -23,7 +22,6 @@ import {
|
||||
unreachable,
|
||||
warn,
|
||||
} from "../shared/util.js";
|
||||
import { PostScriptLexer, PostScriptParser } from "./ps_parser.js";
|
||||
import { BaseStream } from "./base_stream.js";
|
||||
import { buildPostScriptJsFunction } from "./postscript/js_evaluator.js";
|
||||
import { buildPostScriptWasmFunction } from "./postscript/wasm_compiler.js";
|
||||
@ -371,89 +369,20 @@ class PDFFunction {
|
||||
throw new FormatError("No range.");
|
||||
}
|
||||
|
||||
const psCode = fn.getString();
|
||||
|
||||
try {
|
||||
if (factory.useWasm) {
|
||||
const wasmFn = buildPostScriptWasmFunction(
|
||||
fn.getString(),
|
||||
domain,
|
||||
range
|
||||
);
|
||||
const wasmFn = buildPostScriptWasmFunction(psCode, domain, range);
|
||||
if (wasmFn) {
|
||||
return wasmFn; // (src, srcOffset, dest, destOffset) → void
|
||||
}
|
||||
} else {
|
||||
const jsFn = buildPostScriptJsFunction(fn.getString(), domain, range);
|
||||
if (jsFn) {
|
||||
return jsFn; // (src, srcOffset, dest, destOffset) → void
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fall through to the existing interpreter-based path.
|
||||
}
|
||||
} catch {}
|
||||
|
||||
warn("Unable to compile PS function, using interpreter");
|
||||
fn.reset();
|
||||
warn("Failed to compile PostScript function to wasm, falling back to JS");
|
||||
|
||||
const lexer = new PostScriptLexer(fn);
|
||||
const parser = new PostScriptParser(lexer);
|
||||
const code = parser.parse();
|
||||
|
||||
if (factory.isEvalSupported && FeatureTest.isEvalSupported) {
|
||||
const compiled = new PostScriptCompiler().compile(code, domain, range);
|
||||
if (compiled) {
|
||||
// Compiled function consists of simple expressions such as addition,
|
||||
// subtraction, Math.max, and also contains 'var' and 'return'
|
||||
// statements. See the generation in the PostScriptCompiler below.
|
||||
// eslint-disable-next-line no-new-func
|
||||
return new Function("src", "srcOffset", "dest", "destOffset", compiled);
|
||||
}
|
||||
}
|
||||
info("Unable to compile PS function");
|
||||
|
||||
const numOutputs = range.length >> 1;
|
||||
const numInputs = domain.length >> 1;
|
||||
const evaluator = new PostScriptEvaluator(code);
|
||||
// Cache the values for a big speed up, the cache size is limited though
|
||||
// since the number of possible values can be huge from a PS function.
|
||||
const cache = Object.create(null);
|
||||
// The MAX_CACHE_SIZE is set to ~4x the maximum number of distinct values
|
||||
// seen in our tests.
|
||||
const MAX_CACHE_SIZE = 2048 * 4;
|
||||
let cache_available = MAX_CACHE_SIZE;
|
||||
const tmpBuf = new Float32Array(numInputs);
|
||||
|
||||
return function constructPostScriptFn(src, srcOffset, dest, destOffset) {
|
||||
let i, value;
|
||||
let key = "";
|
||||
const input = tmpBuf;
|
||||
for (i = 0; i < numInputs; i++) {
|
||||
value = src[srcOffset + i];
|
||||
input[i] = value;
|
||||
key += value + "_";
|
||||
}
|
||||
|
||||
const cachedValue = cache[key];
|
||||
if (cachedValue !== undefined) {
|
||||
dest.set(cachedValue, destOffset);
|
||||
return;
|
||||
}
|
||||
|
||||
const output = new Float32Array(numOutputs);
|
||||
const stack = evaluator.execute(input);
|
||||
const stackIndex = stack.length - numOutputs;
|
||||
for (i = 0; i < numOutputs; i++) {
|
||||
output[i] = MathClamp(
|
||||
stack[stackIndex + i],
|
||||
range[i * 2],
|
||||
range[i * 2 + 1]
|
||||
);
|
||||
}
|
||||
if (cache_available > 0) {
|
||||
cache_available--;
|
||||
cache[key] = output;
|
||||
}
|
||||
dest.set(output, destOffset);
|
||||
};
|
||||
return buildPostScriptJsFunction(psCode, domain, range);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -528,23 +528,311 @@ function compilePostScriptToIR(source, domain, range) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Same calling convention as the Wasm wrapper:
|
||||
* fn(src, srcOffset, dest, destOffset)
|
||||
* Direct stack-based interpreter for a parsed PsProgram.
|
||||
* Used when PSStackToTree fails to optimize the AST.
|
||||
*/
|
||||
class PSStackBasedInterpreter {
|
||||
// Safe: JS is single-threaded.
|
||||
static #stack = new Float64Array(100);
|
||||
|
||||
static #sp = 0;
|
||||
|
||||
static #push(v) {
|
||||
if (this.#sp < this.#stack.length) {
|
||||
this.#stack[this.#sp++] = v;
|
||||
}
|
||||
}
|
||||
|
||||
static #execOp(op) {
|
||||
const stack = this.#stack;
|
||||
switch (op) {
|
||||
case TOKEN.true:
|
||||
this.#push(1);
|
||||
break;
|
||||
case TOKEN.false:
|
||||
this.#push(0);
|
||||
break;
|
||||
case TOKEN.abs:
|
||||
stack[this.#sp - 1] = Math.abs(stack[this.#sp - 1]);
|
||||
break;
|
||||
case TOKEN.neg:
|
||||
stack[this.#sp - 1] = -stack[this.#sp - 1];
|
||||
break;
|
||||
case TOKEN.ceiling:
|
||||
stack[this.#sp - 1] = Math.ceil(stack[this.#sp - 1]);
|
||||
break;
|
||||
case TOKEN.floor:
|
||||
stack[this.#sp - 1] = Math.floor(stack[this.#sp - 1]);
|
||||
break;
|
||||
case TOKEN.round:
|
||||
stack[this.#sp - 1] = Math.floor(stack[this.#sp - 1] + 0.5);
|
||||
break;
|
||||
case TOKEN.truncate:
|
||||
stack[this.#sp - 1] = Math.trunc(stack[this.#sp - 1]);
|
||||
break;
|
||||
case TOKEN.sqrt:
|
||||
stack[this.#sp - 1] = Math.sqrt(stack[this.#sp - 1]);
|
||||
break;
|
||||
case TOKEN.sin:
|
||||
stack[this.#sp - 1] = Math.sin(
|
||||
(stack[this.#sp - 1] % 360) * _DEG_TO_RAD
|
||||
);
|
||||
break;
|
||||
case TOKEN.cos:
|
||||
stack[this.#sp - 1] = Math.cos(
|
||||
(stack[this.#sp - 1] % 360) * _DEG_TO_RAD
|
||||
);
|
||||
break;
|
||||
case TOKEN.ln:
|
||||
stack[this.#sp - 1] = Math.log(stack[this.#sp - 1]);
|
||||
break;
|
||||
case TOKEN.log:
|
||||
stack[this.#sp - 1] = Math.log10(stack[this.#sp - 1]);
|
||||
break;
|
||||
case TOKEN.cvi:
|
||||
stack[this.#sp - 1] = Math.trunc(stack[this.#sp - 1]) | 0;
|
||||
break;
|
||||
case TOKEN.cvr:
|
||||
break; // values are already f64
|
||||
case TOKEN.not: {
|
||||
const v = stack[this.#sp - 1];
|
||||
stack[this.#sp - 1] = v === 0 || v === 1 ? 1 - v : ~(v | 0);
|
||||
break;
|
||||
}
|
||||
case TOKEN.add: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] += b;
|
||||
break;
|
||||
}
|
||||
case TOKEN.sub: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] -= b;
|
||||
break;
|
||||
}
|
||||
case TOKEN.mul: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] *= b;
|
||||
break;
|
||||
}
|
||||
case TOKEN.div: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] = b !== 0 ? stack[this.#sp - 1] / b : 0;
|
||||
break;
|
||||
}
|
||||
case TOKEN.idiv: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] = b !== 0 ? Math.trunc(stack[this.#sp - 1] / b) : 0;
|
||||
break;
|
||||
}
|
||||
case TOKEN.mod: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] = b !== 0 ? stack[this.#sp - 1] % b : 0;
|
||||
break;
|
||||
}
|
||||
case TOKEN.exp: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] **= b;
|
||||
break;
|
||||
}
|
||||
case TOKEN.atan: {
|
||||
// Stack: [..., dy, dx] — dx on top.
|
||||
const dx = stack[--this.#sp];
|
||||
const deg = Math.atan2(stack[this.#sp - 1], dx) * _RAD_TO_DEG;
|
||||
stack[this.#sp - 1] = deg < 0 ? deg + 360 : deg;
|
||||
break;
|
||||
}
|
||||
case TOKEN.eq: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] = stack[this.#sp - 1] === b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case TOKEN.ne: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] = stack[this.#sp - 1] !== b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case TOKEN.gt: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] = stack[this.#sp - 1] > b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case TOKEN.ge: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] = stack[this.#sp - 1] >= b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case TOKEN.lt: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] = stack[this.#sp - 1] < b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case TOKEN.le: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] = stack[this.#sp - 1] <= b ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
case TOKEN.and: {
|
||||
const b = stack[--this.#sp] | 0;
|
||||
stack[this.#sp - 1] = (stack[this.#sp - 1] | 0) & b;
|
||||
break;
|
||||
}
|
||||
case TOKEN.or: {
|
||||
const b = stack[--this.#sp] | 0;
|
||||
stack[this.#sp - 1] = stack[this.#sp - 1] | 0 | b;
|
||||
break;
|
||||
}
|
||||
case TOKEN.xor: {
|
||||
const b = stack[--this.#sp] | 0;
|
||||
stack[this.#sp - 1] = (stack[this.#sp - 1] | 0) ^ b;
|
||||
break;
|
||||
}
|
||||
case TOKEN.bitshift: {
|
||||
const amt = stack[--this.#sp] | 0;
|
||||
const v = stack[this.#sp - 1] | 0;
|
||||
stack[this.#sp - 1] = amt > 0 ? v << amt : v >> -amt;
|
||||
break;
|
||||
}
|
||||
case TOKEN.min: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] = Math.min(stack[this.#sp - 1], b);
|
||||
break;
|
||||
}
|
||||
case TOKEN.max: {
|
||||
const b = stack[--this.#sp];
|
||||
stack[this.#sp - 1] = Math.max(stack[this.#sp - 1], b);
|
||||
break;
|
||||
}
|
||||
case TOKEN.dup:
|
||||
this.#push(stack[this.#sp - 1]);
|
||||
break;
|
||||
case TOKEN.exch: {
|
||||
const a = stack[--this.#sp];
|
||||
const b = stack[--this.#sp];
|
||||
this.#push(a);
|
||||
this.#push(b);
|
||||
break;
|
||||
}
|
||||
case TOKEN.pop:
|
||||
this.#sp--;
|
||||
break;
|
||||
case TOKEN.copy: {
|
||||
const n = Math.trunc(stack[--this.#sp]);
|
||||
const base = this.#sp - n;
|
||||
for (let k = 0; k < n; k++) {
|
||||
this.#push(stack[base + k]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TOKEN.index: {
|
||||
const i = Math.trunc(stack[--this.#sp]);
|
||||
this.#push(stack[this.#sp - 1 - i]);
|
||||
break;
|
||||
}
|
||||
case TOKEN.roll: {
|
||||
// Rotate top n elements by j positions toward the top.
|
||||
const j = Math.trunc(stack[--this.#sp]);
|
||||
const n = Math.trunc(stack[--this.#sp]);
|
||||
if (n > 1 && j !== 0) {
|
||||
const mod = ((j % n) + n) % n;
|
||||
if (mod !== 0) {
|
||||
const base = this.#sp - n;
|
||||
const sub = stack.slice(base, this.#sp);
|
||||
for (let k = 0; k < n; k++) {
|
||||
stack[base + k] = sub[(k - mod + n) % n];
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static #execBlock(instructions) {
|
||||
for (const instr of instructions) {
|
||||
switch (instr.type) {
|
||||
case PS_NODE.number:
|
||||
this.#push(instr.value);
|
||||
break;
|
||||
case PS_NODE.operator:
|
||||
this.#execOp(instr.op);
|
||||
break;
|
||||
case PS_NODE.if:
|
||||
if (this.#stack[--this.#sp] !== 0) {
|
||||
this.#execBlock(instr.then.instructions);
|
||||
}
|
||||
break;
|
||||
case PS_NODE.ifelse:
|
||||
if (this.#stack[--this.#sp] !== 0) {
|
||||
this.#execBlock(instr.then.instructions);
|
||||
} else {
|
||||
this.#execBlock(instr.otherwise.instructions);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("./ast.js").PsProgram} program
|
||||
* @param {number[]} domain – flat [min0,max0, …]
|
||||
* @param {number[]} range – flat [min0,max0, …]
|
||||
* @returns {Function} – `(src, srcOffset, dest, destOffset) => void`
|
||||
*/
|
||||
static build(program, domain, range) {
|
||||
const nIn = domain.length >> 1;
|
||||
const nOut = range.length >> 1;
|
||||
const { instructions } = program.body;
|
||||
return (src, srcOffset, dest, destOffset) => {
|
||||
this.#sp = 0;
|
||||
for (let i = 0; i < nIn; i++) {
|
||||
this.#push(src[srcOffset + i]);
|
||||
}
|
||||
this.#execBlock(instructions);
|
||||
// Outputs: first at bottom, last at top.
|
||||
const base = this.#sp - nOut;
|
||||
for (let i = 0; i < nOut; i++) {
|
||||
const v = base + i >= 0 ? this.#stack[base + i] : 0;
|
||||
dest[destOffset + i] = Math.max(
|
||||
range[i * 2],
|
||||
Math.min(range[i * 2 + 1], v)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries PSStackToTree-optimized IR first; falls back to direct interpreter.
|
||||
*
|
||||
* @param {string} source
|
||||
* @param {number[]} domain – flat [min0,max0, …]
|
||||
* @param {number[]} range – flat [min0,max0, …]
|
||||
* @returns {Function|null}
|
||||
* @returns {Function} – `(src, srcOffset, dest, destOffset) => void`
|
||||
*/
|
||||
function buildPostScriptJsFunction(source, domain, range) {
|
||||
const ir = compilePostScriptToIR(source, domain, range);
|
||||
if (!ir) {
|
||||
return null;
|
||||
const program = parsePostScriptFunction(source);
|
||||
const ir = new PsJsCompiler(domain, range).compile(program);
|
||||
if (ir) {
|
||||
return (src, srcOffset, dest, destOffset) => {
|
||||
PsJsCompiler.execute(ir, src, srcOffset, dest, destOffset);
|
||||
};
|
||||
}
|
||||
|
||||
return (src, srcOffset, dest, destOffset) => {
|
||||
PsJsCompiler.execute(ir, src, srcOffset, dest, destOffset);
|
||||
};
|
||||
// Fall back to direct interpreter.
|
||||
return PSStackBasedInterpreter.build(program, domain, range);
|
||||
}
|
||||
|
||||
export { buildPostScriptJsFunction, compilePostScriptToIR };
|
||||
/**
|
||||
* @param {import("./ast.js").PsProgram} program
|
||||
* @param {number[]} domain – flat [min0,max0, …]
|
||||
* @param {number[]} range – flat [min0,max0, …]
|
||||
* @returns {Function} – `(src, srcOffset, dest, destOffset) => void`
|
||||
*/
|
||||
function buildPostScriptProgramFunction(program, domain, range) {
|
||||
return PSStackBasedInterpreter.build(program, domain, range);
|
||||
}
|
||||
|
||||
export {
|
||||
buildPostScriptJsFunction,
|
||||
buildPostScriptProgramFunction,
|
||||
compilePostScriptToIR,
|
||||
};
|
||||
|
||||
@ -13,6 +13,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
buildPostScriptJsFunction,
|
||||
buildPostScriptProgramFunction,
|
||||
} from "../../src/core/postscript/js_evaluator.js";
|
||||
import {
|
||||
buildPostScriptWasmFunction,
|
||||
compilePostScriptToWasm,
|
||||
@ -35,7 +39,6 @@ import {
|
||||
PsTernaryNode,
|
||||
PsUnaryNode,
|
||||
} from "../../src/core/postscript/ast.js";
|
||||
import { buildPostScriptJsFunction } from "../../src/core/postscript/js_evaluator.js";
|
||||
|
||||
// Precision argument for toBeCloseTo() in trigonometric tests.
|
||||
const TRIGONOMETRY_EPS = 1e-10;
|
||||
@ -192,25 +195,41 @@ describe("PostScript Type 4 lexer, parser, and Wasm compiler", function () {
|
||||
describe("PostScript Type 4 Wasm compiler", function () {
|
||||
/**
|
||||
* Compile and instantiate a PostScript Type 4 function, then call it.
|
||||
* Returns null if compilation returns null (unsupported program).
|
||||
* Returns null if Wasm compilation returns null (unsupported program).
|
||||
* For single-output functions returns a scalar; for multi-output an Array.
|
||||
*
|
||||
* Validates three implementations against each other:
|
||||
* - Wasm compiler (PSStackToTree → Wasm binary)
|
||||
* - JS IR compiler (PSStackToTree → flat IR interpreted in JS)
|
||||
* - Direct program interpreter (raw PsProgram stack-machine interpreter)
|
||||
*/
|
||||
function compileAndRun(src, domain, range, args) {
|
||||
const wasmFn = buildPostScriptWasmFunction(src, domain, range);
|
||||
// jsFn now always returns a function: PSStackToTree IR when possible,
|
||||
// direct program interpreter otherwise.
|
||||
const jsFn = buildPostScriptJsFunction(src, domain, range);
|
||||
// Direct interpreter: always available, never uses PSStackToTree.
|
||||
const interpFn = buildPostScriptProgramFunction(
|
||||
parsePostScriptFunction(src),
|
||||
domain,
|
||||
range
|
||||
);
|
||||
|
||||
if (!wasmFn) {
|
||||
expect(jsFn).toBeNull();
|
||||
return null;
|
||||
}
|
||||
expect(jsFn).not.toBeNull();
|
||||
|
||||
const nOut = range.length >> 1;
|
||||
const srcBuf = new Float64Array(args);
|
||||
const wasmDest = new Float64Array(nOut);
|
||||
const jsDest = new Float64Array(nOut);
|
||||
const interpDest = new Float64Array(nOut);
|
||||
wasmFn(srcBuf, 0, wasmDest, 0);
|
||||
jsFn(srcBuf, 0, jsDest, 0);
|
||||
interpFn(srcBuf, 0, interpDest, 0);
|
||||
for (let i = 0; i < nOut; i++) {
|
||||
expect(jsDest[i]).toBeCloseTo(wasmDest[i], 10);
|
||||
expect(interpDest[i]).toBeCloseTo(wasmDest[i], 10);
|
||||
}
|
||||
return nOut === 1 ? wasmDest[0] : Array.from(wasmDest);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user