diff --git a/src/core/function.js b/src/core/function.js index 909ef3b20..9b3f5f7dd 100644 --- a/src/core/function.js +++ b/src/core/function.js @@ -815,439 +815,434 @@ class PostScriptEvaluator { } } +class AstNode { + constructor(type) { + this.type = type; + } + + visit(visitor) { + unreachable("abstract method"); + } +} + +class AstArgument extends AstNode { + constructor(index, min, max) { + super("args"); + this.index = index; + this.min = min; + this.max = max; + } + + visit(visitor) { + visitor.visitArgument(this); + } +} + +class AstLiteral extends AstNode { + constructor(number) { + super("literal"); + this.number = number; + this.min = number; + this.max = number; + } + + visit(visitor) { + visitor.visitLiteral(this); + } +} + +class AstBinaryOperation extends AstNode { + constructor(op, arg1, arg2, min, max) { + super("binary"); + this.op = op; + this.arg1 = arg1; + this.arg2 = arg2; + this.min = min; + this.max = max; + } + + visit(visitor) { + visitor.visitBinaryOperation(this); + } +} + +class AstMin extends AstNode { + constructor(arg, max) { + super("max"); + this.arg = arg; + this.min = arg.min; + this.max = max; + } + + visit(visitor) { + visitor.visitMin(this); + } +} + +class AstVariable extends AstNode { + constructor(index, min, max) { + super("var"); + this.index = index; + this.min = min; + this.max = max; + } + + visit(visitor) { + visitor.visitVariable(this); + } +} + +class AstVariableDefinition extends AstNode { + constructor(variable, arg) { + super("definition"); + this.variable = variable; + this.arg = arg; + } + + visit(visitor) { + visitor.visitVariableDefinition(this); + } +} + +class ExpressionBuilderVisitor { + constructor() { + this.parts = []; + } + + visitArgument(arg) { + this.parts.push( + "Math.max(", + arg.min, + ", Math.min(", + arg.max, + ", src[srcOffset + ", + arg.index, + "]))" + ); + } + + visitVariable(variable) { + this.parts.push("v", variable.index); + } + + visitLiteral(literal) { + this.parts.push(literal.number); + } + + visitBinaryOperation(operation) { + this.parts.push("("); + operation.arg1.visit(this); + this.parts.push(" ", operation.op, " "); + operation.arg2.visit(this); + this.parts.push(")"); + } + + visitVariableDefinition(definition) { + this.parts.push("var "); + definition.variable.visit(this); + this.parts.push(" = "); + definition.arg.visit(this); + this.parts.push(";"); + } + + visitMin(max) { + this.parts.push("Math.min("); + max.arg.visit(this); + this.parts.push(", ", max.max, ")"); + } + + toString() { + return this.parts.join(""); + } +} + +function buildAddOperation(num1, num2) { + if (num2.type === "literal" && num2.number === 0) { + // optimization: second operand is 0 + return num1; + } + if (num1.type === "literal" && num1.number === 0) { + // optimization: first operand is 0 + return num2; + } + if (num2.type === "literal" && num1.type === "literal") { + // optimization: operands operand are literals + return new AstLiteral(num1.number + num2.number); + } + return new AstBinaryOperation( + "+", + num1, + num2, + num1.min + num2.min, + num1.max + num2.max + ); +} + +function buildMulOperation(num1, num2) { + if (num2.type === "literal") { + // optimization: second operands is a literal... + if (num2.number === 0) { + return new AstLiteral(0); // and it's 0 + } else if (num2.number === 1) { + return num1; // and it's 1 + } else if (num1.type === "literal") { + // ... and first operands is a literal too + return new AstLiteral(num1.number * num2.number); + } + } + if (num1.type === "literal") { + // optimization: first operands is a literal... + if (num1.number === 0) { + return new AstLiteral(0); // and it's 0 + } else if (num1.number === 1) { + return num2; // and it's 1 + } + } + const min = Math.min( + num1.min * num2.min, + num1.min * num2.max, + num1.max * num2.min, + num1.max * num2.max + ); + const max = Math.max( + num1.min * num2.min, + num1.min * num2.max, + num1.max * num2.min, + num1.max * num2.max + ); + return new AstBinaryOperation("*", num1, num2, min, max); +} + +function buildSubOperation(num1, num2) { + if (num2.type === "literal") { + // optimization: second operands is a literal... + if (num2.number === 0) { + return num1; // ... and it's 0 + } else if (num1.type === "literal") { + // ... and first operands is a literal too + return new AstLiteral(num1.number - num2.number); + } + } + if ( + num2.type === "binary" && + num2.op === "-" && + num1.type === "literal" && + num1.number === 1 && + num2.arg1.type === "literal" && + num2.arg1.number === 1 + ) { + // optimization for case: 1 - (1 - x) + return num2.arg2; + } + return new AstBinaryOperation( + "-", + num1, + num2, + num1.min - num2.max, + num1.max - num2.min + ); +} + +function buildMinOperation(num1, max) { + if (num1.min >= max) { + // optimization: num1 min value is not less than required max + return new AstLiteral(max); // just returning max + } else if (num1.max <= max) { + // optimization: num1 max value is not greater than required max + return num1; // just returning an argument + } + return new AstMin(num1, max); +} + // Most of the PDFs functions consist of simple operations such as: // roll, exch, sub, cvr, pop, index, dup, mul, if, gt, add. // // We can compile most of such programs, and at the same moment, we can // optimize some expressions using basic math properties. Keeping track of // min/max values will allow us to avoid extra Math.min/Math.max calls. -const PostScriptCompiler = (function PostScriptCompilerClosure() { - class AstNode { - constructor(type) { - this.type = type; +class PostScriptCompiler { + compile(code, domain, range) { + const stack = []; + const instructions = []; + const inputSize = domain.length >> 1, + outputSize = range.length >> 1; + let lastRegister = 0; + let n, j; + let num1, num2, ast1, ast2, tmpVar, item; + for (let i = 0; i < inputSize; i++) { + stack.push(new AstArgument(i, domain[i * 2], domain[i * 2 + 1])); } - visit(visitor) { - unreachable("abstract method"); - } - } - - class AstArgument extends AstNode { - constructor(index, min, max) { - super("args"); - this.index = index; - this.min = min; - this.max = max; - } - - visit(visitor) { - visitor.visitArgument(this); - } - } - - class AstLiteral extends AstNode { - constructor(number) { - super("literal"); - this.number = number; - this.min = number; - this.max = number; - } - - visit(visitor) { - visitor.visitLiteral(this); - } - } - - class AstBinaryOperation extends AstNode { - constructor(op, arg1, arg2, min, max) { - super("binary"); - this.op = op; - this.arg1 = arg1; - this.arg2 = arg2; - this.min = min; - this.max = max; - } - - visit(visitor) { - visitor.visitBinaryOperation(this); - } - } - - class AstMin extends AstNode { - constructor(arg, max) { - super("max"); - this.arg = arg; - this.min = arg.min; - this.max = max; - } - - visit(visitor) { - visitor.visitMin(this); - } - } - - class AstVariable extends AstNode { - constructor(index, min, max) { - super("var"); - this.index = index; - this.min = min; - this.max = max; - } - - visit(visitor) { - visitor.visitVariable(this); - } - } - - class AstVariableDefinition extends AstNode { - constructor(variable, arg) { - super("definition"); - this.variable = variable; - this.arg = arg; - } - - visit(visitor) { - visitor.visitVariableDefinition(this); - } - } - - class ExpressionBuilderVisitor { - constructor() { - this.parts = []; - } - - visitArgument(arg) { - this.parts.push( - "Math.max(", - arg.min, - ", Math.min(", - arg.max, - ", src[srcOffset + ", - arg.index, - "]))" - ); - } - - visitVariable(variable) { - this.parts.push("v", variable.index); - } - - visitLiteral(literal) { - this.parts.push(literal.number); - } - - visitBinaryOperation(operation) { - this.parts.push("("); - operation.arg1.visit(this); - this.parts.push(" ", operation.op, " "); - operation.arg2.visit(this); - this.parts.push(")"); - } - - visitVariableDefinition(definition) { - this.parts.push("var "); - definition.variable.visit(this); - this.parts.push(" = "); - definition.arg.visit(this); - this.parts.push(";"); - } - - visitMin(max) { - this.parts.push("Math.min("); - max.arg.visit(this); - this.parts.push(", ", max.max, ")"); - } - - toString() { - return this.parts.join(""); - } - } - - function buildAddOperation(num1, num2) { - if (num2.type === "literal" && num2.number === 0) { - // optimization: second operand is 0 - return num1; - } - if (num1.type === "literal" && num1.number === 0) { - // optimization: first operand is 0 - return num2; - } - if (num2.type === "literal" && num1.type === "literal") { - // optimization: operands operand are literals - return new AstLiteral(num1.number + num2.number); - } - return new AstBinaryOperation( - "+", - num1, - num2, - num1.min + num2.min, - num1.max + num2.max - ); - } - - function buildMulOperation(num1, num2) { - if (num2.type === "literal") { - // optimization: second operands is a literal... - if (num2.number === 0) { - return new AstLiteral(0); // and it's 0 - } else if (num2.number === 1) { - return num1; // and it's 1 - } else if (num1.type === "literal") { - // ... and first operands is a literal too - return new AstLiteral(num1.number * num2.number); - } - } - if (num1.type === "literal") { - // optimization: first operands is a literal... - if (num1.number === 0) { - return new AstLiteral(0); // and it's 0 - } else if (num1.number === 1) { - return num2; // and it's 1 - } - } - const min = Math.min( - num1.min * num2.min, - num1.min * num2.max, - num1.max * num2.min, - num1.max * num2.max - ); - const max = Math.max( - num1.min * num2.min, - num1.min * num2.max, - num1.max * num2.min, - num1.max * num2.max - ); - return new AstBinaryOperation("*", num1, num2, min, max); - } - - function buildSubOperation(num1, num2) { - if (num2.type === "literal") { - // optimization: second operands is a literal... - if (num2.number === 0) { - return num1; // ... and it's 0 - } else if (num1.type === "literal") { - // ... and first operands is a literal too - return new AstLiteral(num1.number - num2.number); - } - } - if ( - num2.type === "binary" && - num2.op === "-" && - num1.type === "literal" && - num1.number === 1 && - num2.arg1.type === "literal" && - num2.arg1.number === 1 - ) { - // optimization for case: 1 - (1 - x) - return num2.arg2; - } - return new AstBinaryOperation( - "-", - num1, - num2, - num1.min - num2.max, - num1.max - num2.min - ); - } - - function buildMinOperation(num1, max) { - if (num1.min >= max) { - // optimization: num1 min value is not less than required max - return new AstLiteral(max); // just returning max - } else if (num1.max <= max) { - // optimization: num1 max value is not greater than required max - return num1; // just returning an argument - } - return new AstMin(num1, max); - } - - // eslint-disable-next-line no-shadow - class PostScriptCompiler { - compile(code, domain, range) { - const stack = []; - const instructions = []; - const inputSize = domain.length >> 1, - outputSize = range.length >> 1; - let lastRegister = 0; - let n, j; - let num1, num2, ast1, ast2, tmpVar, item; - for (let i = 0; i < inputSize; i++) { - stack.push(new AstArgument(i, domain[i * 2], domain[i * 2 + 1])); + for (let i = 0, ii = code.length; i < ii; i++) { + item = code[i]; + if (typeof item === "number") { + stack.push(new AstLiteral(item)); + continue; } - for (let i = 0, ii = code.length; i < ii; i++) { - item = code[i]; - if (typeof item === "number") { - stack.push(new AstLiteral(item)); - continue; - } - - switch (item) { - case "add": - if (stack.length < 2) { - return null; - } - num2 = stack.pop(); + switch (item) { + case "add": + if (stack.length < 2) { + return null; + } + num2 = stack.pop(); + num1 = stack.pop(); + stack.push(buildAddOperation(num1, num2)); + break; + case "cvr": + if (stack.length < 1) { + return null; + } + break; + case "mul": + if (stack.length < 2) { + return null; + } + num2 = stack.pop(); + num1 = stack.pop(); + stack.push(buildMulOperation(num1, num2)); + break; + case "sub": + if (stack.length < 2) { + return null; + } + num2 = stack.pop(); + num1 = stack.pop(); + stack.push(buildSubOperation(num1, num2)); + break; + case "exch": + if (stack.length < 2) { + return null; + } + ast1 = stack.pop(); + ast2 = stack.pop(); + stack.push(ast1, ast2); + break; + case "pop": + if (stack.length < 1) { + return null; + } + stack.pop(); + break; + case "index": + if (stack.length < 1) { + return null; + } + num1 = stack.pop(); + if (num1.type !== "literal") { + return null; + } + n = num1.number; + if (n < 0 || !Number.isInteger(n) || stack.length < n) { + return null; + } + ast1 = stack[stack.length - n - 1]; + if (ast1.type === "literal" || ast1.type === "var") { + stack.push(ast1); + break; + } + tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max); + stack[stack.length - n - 1] = tmpVar; + stack.push(tmpVar); + instructions.push(new AstVariableDefinition(tmpVar, ast1)); + break; + case "dup": + if (stack.length < 1) { + return null; + } + if ( + typeof code[i + 1] === "number" && + code[i + 2] === "gt" && + code[i + 3] === i + 7 && + code[i + 4] === "jz" && + code[i + 5] === "pop" && + code[i + 6] === code[i + 1] + ) { + // special case of the commands sequence for the min operation num1 = stack.pop(); - stack.push(buildAddOperation(num1, num2)); + stack.push(buildMinOperation(num1, code[i + 1])); + i += 6; break; - case "cvr": - if (stack.length < 1) { - return null; - } + } + ast1 = stack.at(-1); + if (ast1.type === "literal" || ast1.type === "var") { + // we don't have to save into intermediate variable a literal or + // variable. + stack.push(ast1); break; - case "mul": - if (stack.length < 2) { - return null; - } - num2 = stack.pop(); - num1 = stack.pop(); - stack.push(buildMulOperation(num1, num2)); - break; - case "sub": - if (stack.length < 2) { - return null; - } - num2 = stack.pop(); - num1 = stack.pop(); - stack.push(buildSubOperation(num1, num2)); - break; - case "exch": - if (stack.length < 2) { - return null; - } - ast1 = stack.pop(); - ast2 = stack.pop(); - stack.push(ast1, ast2); - break; - case "pop": - if (stack.length < 1) { - return null; - } - stack.pop(); - break; - case "index": - if (stack.length < 1) { - return null; - } - num1 = stack.pop(); - if (num1.type !== "literal") { - return null; - } - n = num1.number; - if (n < 0 || !Number.isInteger(n) || stack.length < n) { - return null; - } - ast1 = stack[stack.length - n - 1]; - if (ast1.type === "literal" || ast1.type === "var") { - stack.push(ast1); - break; - } - tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max); - stack[stack.length - n - 1] = tmpVar; - stack.push(tmpVar); - instructions.push(new AstVariableDefinition(tmpVar, ast1)); - break; - case "dup": - if (stack.length < 1) { - return null; - } - if ( - typeof code[i + 1] === "number" && - code[i + 2] === "gt" && - code[i + 3] === i + 7 && - code[i + 4] === "jz" && - code[i + 5] === "pop" && - code[i + 6] === code[i + 1] - ) { - // special case of the commands sequence for the min operation - num1 = stack.pop(); - stack.push(buildMinOperation(num1, code[i + 1])); - i += 6; - break; - } - ast1 = stack.at(-1); - if (ast1.type === "literal" || ast1.type === "var") { - // we don't have to save into intermediate variable a literal or - // variable. - stack.push(ast1); - break; - } - tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max); - stack[stack.length - 1] = tmpVar; - stack.push(tmpVar); - instructions.push(new AstVariableDefinition(tmpVar, ast1)); - break; - case "roll": - if (stack.length < 2) { - return null; - } - num2 = stack.pop(); - num1 = stack.pop(); - if (num2.type !== "literal" || num1.type !== "literal") { - // both roll operands must be numbers - return null; - } - j = num2.number; - n = num1.number; - if ( - n <= 0 || - !Number.isInteger(n) || - !Number.isInteger(j) || - stack.length < n - ) { - // ... and integers - return null; - } - j = ((j % n) + n) % n; - if (j === 0) { - break; // just skipping -- there are nothing to rotate - } - Array.prototype.push.apply( - stack, - stack.splice(stack.length - n, n - j) - ); - break; - default: - return null; // unsupported operator - } + } + tmpVar = new AstVariable(lastRegister++, ast1.min, ast1.max); + stack[stack.length - 1] = tmpVar; + stack.push(tmpVar); + instructions.push(new AstVariableDefinition(tmpVar, ast1)); + break; + case "roll": + if (stack.length < 2) { + return null; + } + num2 = stack.pop(); + num1 = stack.pop(); + if (num2.type !== "literal" || num1.type !== "literal") { + // both roll operands must be numbers + return null; + } + j = num2.number; + n = num1.number; + if ( + n <= 0 || + !Number.isInteger(n) || + !Number.isInteger(j) || + stack.length < n + ) { + // ... and integers + return null; + } + j = ((j % n) + n) % n; + if (j === 0) { + break; // just skipping -- there are nothing to rotate + } + Array.prototype.push.apply( + stack, + stack.splice(stack.length - n, n - j) + ); + break; + default: + return null; // unsupported operator } - - if (stack.length !== outputSize) { - return null; - } - - const result = []; - for (const instruction of instructions) { - const statementBuilder = new ExpressionBuilderVisitor(); - instruction.visit(statementBuilder); - result.push(statementBuilder.toString()); - } - for (let i = 0, ii = stack.length; i < ii; i++) { - const expr = stack[i], - statementBuilder = new ExpressionBuilderVisitor(); - expr.visit(statementBuilder); - const min = range[i * 2], - max = range[i * 2 + 1]; - const out = [statementBuilder.toString()]; - if (min > expr.min) { - out.unshift("Math.max(", min, ", "); - out.push(")"); - } - if (max < expr.max) { - out.unshift("Math.min(", max, ", "); - out.push(")"); - } - out.unshift("dest[destOffset + ", i, "] = "); - out.push(";"); - result.push(out.join("")); - } - return result.join("\n"); } - } - return PostScriptCompiler; -})(); + if (stack.length !== outputSize) { + return null; + } + + const result = []; + for (const instruction of instructions) { + const statementBuilder = new ExpressionBuilderVisitor(); + instruction.visit(statementBuilder); + result.push(statementBuilder.toString()); + } + for (let i = 0, ii = stack.length; i < ii; i++) { + const expr = stack[i], + statementBuilder = new ExpressionBuilderVisitor(); + expr.visit(statementBuilder); + const min = range[i * 2], + max = range[i * 2 + 1]; + const out = [statementBuilder.toString()]; + if (min > expr.min) { + out.unshift("Math.max(", min, ", "); + out.push(")"); + } + if (max < expr.max) { + out.unshift("Math.min(", max, ", "); + out.push(")"); + } + out.unshift("dest[destOffset + ", i, "] = "); + out.push(";"); + result.push(out.join("")); + } + return result.join("\n"); + } +} export { isPDFFunction,