diff --git a/src/core/colorspace.js b/src/core/colorspace.js index 0f1dc36e2..0189368e3 100644 --- a/src/core/colorspace.js +++ b/src/core/colorspace.js @@ -380,23 +380,20 @@ var AlternateCS = (function AlternateCSClosure() { } this.base = base; this.tintFn = tintFn; + this.tmpBuf = new Float32Array(base.numComps); } AlternateCS.prototype = { getRgb: ColorSpace.prototype.getRgb, getRgbItem: function AlternateCS_getRgbItem(src, srcOffset, dest, destOffset) { - var baseNumComps = this.base.numComps; - var input = 'subarray' in src ? - src.subarray(srcOffset, srcOffset + this.numComps) : - Array.prototype.slice.call(src, srcOffset, srcOffset + this.numComps); - var tinted = this.tintFn(input); - this.base.getRgbItem(tinted, 0, dest, destOffset); + var tmpBuf = this.tmpBuf; + this.tintFn(src, srcOffset, tmpBuf, 0); + this.base.getRgbItem(tmpBuf, 0, dest, destOffset); }, getRgbBuffer: function AlternateCS_getRgbBuffer(src, srcOffset, count, dest, destOffset, bits, alpha01) { - var tinted; var tintFn = this.tintFn; var base = this.base; var scale = 1 / ((1 << bits) - 1); @@ -409,13 +406,14 @@ var AlternateCS = (function AlternateCSClosure() { var numComps = this.numComps; var scaled = new Float32Array(numComps); + var tinted = new Float32Array(baseNumComps); var i, j; if (usesZeroToOneRange) { for (i = 0; i < count; i++) { for (j = 0; j < numComps; j++) { scaled[j] = src[srcOffset++] * scale; } - tinted = tintFn(scaled); + tintFn(scaled, 0, tinted, 0); for (j = 0; j < baseNumComps; j++) { baseBuf[pos++] = tinted[j] * 255; } @@ -425,7 +423,7 @@ var AlternateCS = (function AlternateCSClosure() { for (j = 0; j < numComps; j++) { scaled[j] = src[srcOffset++] * scale; } - tinted = tintFn(scaled); + tintFn(scaled, 0, tinted, 0); base.getRgbItem(tinted, 0, baseBuf, pos); pos += baseNumComps; } diff --git a/src/core/function.js b/src/core/function.js index e5e2922f8..296757dd8 100644 --- a/src/core/function.js +++ b/src/core/function.js @@ -97,6 +97,24 @@ var PDFFunction = (function PDFFunctionClosure() { return this.fromIR(IR); }, + parseArray: function PDFFunction_parseArray(xref, fnObj) { + if (!isArray(fnObj)) { + // not an array -- parsing as regular function + return this.parse(xref, fnObj); + } + + var fnArray = []; + for (var j = 0, jj = fnObj.length; j < jj; j++) { + var obj = xref.fetchIfRef(fnObj[j]); + fnArray.push(PDFFunction.parse(xref, obj)); + } + return function (src, srcOffset, dest, destOffset) { + for (var i = 0, ii = fnArray.length; i < ii; i++) { + fnArray[i](src, srcOffset, dest, destOffset + i); + } + }; + }, + constructSampled: function PDFFunction_constructSampled(str, dict) { function toMultiArray(arr) { var inputLength = arr.length; @@ -161,7 +179,8 @@ var PDFFunction = (function PDFFunctionClosure() { return ymin + ((x - xmin) * ((ymax - ymin) / (xmax - xmin))); } - return function constructSampledFromIRResult(args) { + return function constructSampledFromIRResult(src, srcOffset, + dest, destOffset) { // See chapter 3, page 110 of the PDF reference. var m = IR[1]; var domain = IR[2]; @@ -173,13 +192,6 @@ var PDFFunction = (function PDFFunctionClosure() { //var mask = IR[8]; var range = IR[9]; - if (m !== args.length) { - error('Incorrect number of arguments: ' + m + ' != ' + - args.length); - } - - var x = args; - // Building the cube vertices: its part and sample index // http://rjwagner49.com/Mathematics/Interpolation.pdf var cubeVertices = 1 << m; @@ -196,7 +208,8 @@ var PDFFunction = (function PDFFunctionClosure() { // x_i' = min(max(x_i, Domain_2i), Domain_2i+1) var domain_2i = domain[i][0]; var domain_2i_1 = domain[i][1]; - var xi = Math.min(Math.max(x[i], domain_2i), domain_2i_1); + var xi = Math.min(Math.max(src[srcOffset +i], domain_2i), + domain_2i_1); // e_i = Interpolate(x_i', Domain_2i, Domain_2i+1, // Encode_2i, Encode_2i+1) @@ -227,7 +240,6 @@ var PDFFunction = (function PDFFunctionClosure() { pos <<= 1; } - var y = new Float64Array(n); for (j = 0; j < n; ++j) { // Sum all cube vertices' samples portions var rj = 0; @@ -240,10 +252,9 @@ var PDFFunction = (function PDFFunctionClosure() { rj = interpolate(rj, 0, 1, decode[j][0], decode[j][1]); // y_j = min(max(r_j, range_2j), range_2j+1) - y[j] = Math.min(Math.max(rj, range[j][0]), range[j][1]); + dest[destOffset + j] = Math.min(Math.max(rj, range[j][0]), + range[j][1]); } - - return y; }; }, @@ -274,16 +285,13 @@ var PDFFunction = (function PDFFunctionClosure() { var length = diff.length; - return function constructInterpolatedFromIRResult(args) { - var x = (n === 1 ? args[0] : Math.pow(args[0], n)); + return function constructInterpolatedFromIRResult(src, srcOffset, + dest, destOffset) { + var x = n === 1 ? src[srcOffset] : Math.pow(src[srcOffset], n); - var out = []; for (var j = 0; j < length; ++j) { - out.push(c0[j] + (x * diff[j])); + dest[destOffset + j] = c0[j] + (x * diff[j]); } - - return out; - }; }, @@ -317,12 +325,14 @@ var PDFFunction = (function PDFFunctionClosure() { var encode = IR[3]; var fnsIR = IR[4]; var fns = []; + var tmpBuf = new Float32Array(1); for (var i = 0, ii = fnsIR.length; i < ii; i++) { fns.push(PDFFunction.fromIR(fnsIR[i])); } - return function constructStichedFromIRResult(args) { + return function constructStichedFromIRResult(src, srcOffset, + dest, destOffset) { var clip = function constructStichedFromIRClip(v, min, max) { if (v > max) { v = max; @@ -333,7 +343,7 @@ var PDFFunction = (function PDFFunctionClosure() { }; // clip to domain - var v = clip(args[0], domain[0], domain[1]); + var v = clip(src[srcOffset], domain[0], domain[1]); // calulate which bound the value is in for (var i = 0, ii = bounds.length; i < ii; ++i) { if (v < bounds[i]) { @@ -354,10 +364,10 @@ var PDFFunction = (function PDFFunctionClosure() { var rmin = encode[2 * i]; var rmax = encode[2 * i + 1]; - var v2 = rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin); + tmpBuf[0] = rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin); // call the appropriate function - return fns[i]([v2]); + fns[i](tmpBuf, 0, dest, destOffset); }; }, @@ -386,6 +396,18 @@ var PDFFunction = (function PDFFunctionClosure() { var domain = IR[1]; var range = IR[2]; var code = IR[3]; + + var 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. + /*jshint -W054 */ + return new Function('src', 'srcOffset', 'dest', 'destOffset', compiled); + } + + info('Unable to compile PS function'); + var numOutputs = range.length >> 1; var numInputs = domain.length >> 1; var evaluator = new PostScriptEvaluator(code); @@ -396,22 +418,26 @@ var PDFFunction = (function PDFFunctionClosure() { // seen in our tests. var MAX_CACHE_SIZE = 2048 * 4; var cache_available = MAX_CACHE_SIZE; - return function constructPostScriptFromIRResult(args) { + var tmpBuf = new Float32Array(numInputs); + + return function constructPostScriptFromIRResult(src, srcOffset, + dest, destOffset) { var i, value; var key = ''; - var input = new Array(numInputs); + var input = tmpBuf; for (i = 0; i < numInputs; i++) { - value = args[i]; + value = src[srcOffset + i]; input[i] = value; key += value + '_'; } var cachedValue = cache[key]; if (cachedValue !== undefined) { - return cachedValue; + cachedValue.set(dest, destOffset); + return; } - var output = new Array(numOutputs); + var output = new Float32Array(numOutputs); var stack = evaluator.execute(input); var stackIndex = stack.length - numOutputs; for (i = 0; i < numOutputs; i++) { @@ -431,7 +457,7 @@ var PDFFunction = (function PDFFunctionClosure() { cache_available--; cache[key] = output; } - return output; + output.set(dest, destOffset); }; } }; @@ -454,7 +480,8 @@ function isPDFFunction(v) { var PostScriptStack = (function PostScriptStackClosure() { var MAX_STACK_SIZE = 100; function PostScriptStack(initialStack) { - this.stack = initialStack || []; + this.stack = !initialStack ? [] : + Array.prototype.slice.call(initialStack, 0); } PostScriptStack.prototype = { @@ -736,3 +763,370 @@ var PostScriptEvaluator = (function PostScriptEvaluatorClosure() { }; return PostScriptEvaluator; })(); + +// 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. +var PostScriptCompiler = (function PostScriptCompilerClosure() { + function AstNode(type) { + this.type = type; + } + AstNode.prototype.visit = function (visitor) { + throw new Error('abstract method'); + }; + + function AstArgument(index, min, max) { + AstNode.call(this, 'args'); + this.index = index; + this.min = min; + this.max = max; + } + AstArgument.prototype = Object.create(AstNode.prototype); + AstArgument.prototype.visit = function (visitor) { + visitor.visitArgument(this); + }; + + function AstLiteral(number) { + AstNode.call(this, 'literal'); + this.number = number; + this.min = number; + this.max = number; + } + AstLiteral.prototype = Object.create(AstNode.prototype); + AstLiteral.prototype.visit = function (visitor) { + visitor.visitLiteral(this); + }; + + function AstBinaryOperation(op, arg1, arg2, min, max) { + AstNode.call(this, 'binary'); + this.op = op; + this.arg1 = arg1; + this.arg2 = arg2; + this.min = min; + this.max = max; + } + AstBinaryOperation.prototype = Object.create(AstNode.prototype); + AstBinaryOperation.prototype.visit = function (visitor) { + visitor.visitBinaryOperation(this); + }; + + function AstMin(arg, max) { + AstNode.call(this, 'max'); + this.arg = arg; + this.min = arg.min; + this.max = max; + } + AstMin.prototype = Object.create(AstNode.prototype); + AstMin.prototype.visit = function (visitor) { + visitor.visitMin(this); + }; + + function AstVariable(index, min, max) { + AstNode.call(this, 'var'); + this.index = index; + this.min = min; + this.max = max; + } + AstVariable.prototype = Object.create(AstNode.prototype); + AstVariable.prototype.visit = function (visitor) { + visitor.visitVariable(this); + }; + + function AstVariableDefinition(variable, arg) { + AstNode.call(this, 'definition'); + this.variable = variable; + this.arg = arg; + } + AstVariableDefinition.prototype = Object.create(AstNode.prototype); + AstVariableDefinition.prototype.visit = function (visitor) { + visitor.visitVariableDefinition(this); + }; + + function ExpressionBuilderVisitor() { + this.parts = []; + } + ExpressionBuilderVisitor.prototype = { + visitArgument: function (arg) { + this.parts.push('Math.max(', arg.min, ', Math.min(', + arg.max, ', src[srcOffset + ', arg.index, ']))'); + }, + visitVariable: function (variable) { + this.parts.push('v', variable.index); + }, + visitLiteral: function (literal) { + this.parts.push(literal.number); + }, + visitBinaryOperation: function (operation) { + this.parts.push('('); + operation.arg1.visit(this); + this.parts.push(' ', operation.op, ' '); + operation.arg2.visit(this); + this.parts.push(')'); + }, + visitVariableDefinition: function (definition) { + this.parts.push('var '); + definition.variable.visit(this); + this.parts.push(' = '); + definition.arg.visit(this); + this.parts.push(';'); + }, + visitMin: function (max) { + this.parts.push('Math.min('); + max.arg.visit(this); + this.parts.push(', ', max.max, ')'); + }, + toString: function () { + 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 + } + } + var min = Math.min(num1.min * num2.min, num1.min * num2.max, + num1.max * num2.min, num1.max * num2.max); + var 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); + } + + function PostScriptCompiler() {} + PostScriptCompiler.prototype = { + compile: function PostScriptCompiler_compile(code, domain, range) { + var stack = []; + var i, ii; + var instructions = []; + var inputSize = domain.length >> 1, outputSize = range.length >> 1; + var lastRegister = 0; + var n, j, min, max; + var num1, num2, ast1, ast2, tmpVar, item; + for (i = 0; i < inputSize; i++) { + stack.push(new AstArgument(i, domain[i * 2], domain[i * 2 + 1])); + } + + for (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(); + 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 || (n|0) !== 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[stack.length - 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 || (n|0) !== n || (j|0) !== 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; + } + + var result = []; + instructions.forEach(function (instruction) { + var statementBuilder = new ExpressionBuilderVisitor(); + instruction.visit(statementBuilder); + result.push(statementBuilder.toString()); + }); + stack.forEach(function (expr, i) { + var statementBuilder = new ExpressionBuilderVisitor(); + expr.visit(statementBuilder); + var min = range[i * 2], max = range[i * 2 + 1]; + var 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; +})(); diff --git a/src/core/pattern.js b/src/core/pattern.js index 2a320dfd8..2bb9c72b8 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -122,29 +122,7 @@ Shadings.RadialAxial = (function RadialAxialClosure() { this.extendEnd = extendEnd; var fnObj = dict.get('Function'); - var fn; - if (isArray(fnObj)) { - var fnArray = []; - for (var j = 0, jj = fnObj.length; j < jj; j++) { - var obj = xref.fetchIfRef(fnObj[j]); - if (!isPDFFunction(obj)) { - error('Invalid function'); - } - fnArray.push(PDFFunction.parse(xref, obj)); - } - fn = function radialAxialColorFunction(arg) { - var out = []; - for (var i = 0, ii = fnArray.length; i < ii; i++) { - out.push(fnArray[i](arg)[0]); - } - return out; - }; - } else { - if (!isPDFFunction(fnObj)) { - error('Invalid function'); - } - fn = PDFFunction.parse(xref, fnObj); - } + var fn = PDFFunction.parseArray(xref, fnObj); // 10 samples seems good enough for now, but probably won't work // if there are sharp color changes. Ideally, we would implement @@ -162,9 +140,12 @@ Shadings.RadialAxial = (function RadialAxialClosure() { return; } + var color = new Float32Array(cs.numComps), ratio = new Float32Array(1); var rgbColor; for (var i = t0; i <= t1; i += step) { - rgbColor = cs.getRgb(fn([i]), 0); + ratio[0] = i; + fn(ratio, 0, color, 0); + rgbColor = cs.getRgb(color, 0); var cssColor = Util.makeCssRgb(rgbColor); colorStops.push([(i - t0) / diff, cssColor]); } @@ -232,6 +213,12 @@ Shadings.Mesh = (function MeshClosure() { this.context = context; this.buffer = 0; this.bufferLength = 0; + + var numComps = context.numComps; + this.tmpCompsBuf = new Float32Array(numComps); + var csNumComps = context.colorSpace; + this.tmpCsCompsBuf = context.colorFn ? new Float32Array(csNumComps) : + this.tmpCompsBuf; } MeshStreamReader.prototype = { get hasData() { @@ -302,15 +289,16 @@ Shadings.Mesh = (function MeshClosure() { var scale = bitsPerComponent < 32 ? 1 / ((1 << bitsPerComponent) - 1) : 2.3283064365386963e-10; // 2 ^ -32 var decode = this.context.decode; - var components = []; + var components = this.tmpCompsBuf; for (var i = 0, j = 4; i < numComps; i++, j += 2) { var ci = this.readBits(bitsPerComponent); - components.push(ci * scale * (decode[j + 1] - decode[j]) + decode[j]); + components[i] = ci * scale * (decode[j + 1] - decode[j]) + decode[j]; } + var color = this.tmpCsCompsBuf; if (this.context.colorFn) { - components = this.context.colorFn(components); + this.context.colorFn(components, 0, color, 0); } - return this.context.colorSpace.getRgb(components, 0); + return this.context.colorSpace.getRgb(color, 0); } }; @@ -716,31 +704,7 @@ Shadings.Mesh = (function MeshClosure() { cs.getRgb(dict.get('Background'), 0) : null; var fnObj = dict.get('Function'); - var fn; - if (!fnObj) { - fn = null; - } else if (isArray(fnObj)) { - var fnArray = []; - for (var j = 0, jj = fnObj.length; j < jj; j++) { - var obj = xref.fetchIfRef(fnObj[j]); - if (!isPDFFunction(obj)) { - error('Invalid function'); - } - fnArray.push(PDFFunction.parse(xref, obj)); - } - fn = function radialAxialColorFunction(arg) { - var out = []; - for (var i = 0, ii = fnArray.length; i < ii; i++) { - out.push(fnArray[i](arg)[0]); - } - return out; - }; - } else { - if (!isPDFFunction(fnObj)) { - error('Invalid function'); - } - fn = PDFFunction.parse(xref, fnObj); - } + var fn = fnObj ? PDFFunction.parseArray(xref, fnObj) : null; this.coords = []; this.colors = []; diff --git a/test/unit/function_spec.js b/test/unit/function_spec.js index d3e6f8180..77b7ba7b2 100644 --- a/test/unit/function_spec.js +++ b/test/unit/function_spec.js @@ -1,7 +1,8 @@ /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* globals expect, it, describe, beforeEach, isArray, StringStream, - PostScriptParser, PostScriptLexer, PostScriptEvaluator */ + PostScriptParser, PostScriptLexer, PostScriptEvaluator, + PostScriptCompiler*/ 'use strict'; @@ -413,5 +414,110 @@ describe('function', function() { expect(stack).toMatchArray(expectedStack); }); }); + + + describe('PostScriptCompiler', function() { + function check(code, domain, range, samples) { + var compiler = new PostScriptCompiler(); + var compiledCode = compiler.compile(code, domain, range); + if (samples === null) { + expect(compiledCode).toBeNull(); + } else { + expect(compiledCode).not.toBeNull(); + /*jshint -W054 */ + var fn = new Function('src', 'srcOffset', 'dest', 'destOffset', + compiledCode); + for (var i = 0; i < samples.length; i++) { + var out = new Float32Array(samples[i].output.length); + fn(samples[i].input, 0, out, 0); + expect(Array.prototype.slice.call(out, 0)). + toMatchArray(samples[i].output); + } + } + } + + it('check compiled add', function() { + check([0.25, 0.5, 'add'], [], [0, 1], [{input: [], output: [0.75]}]); + check([0, 'add'], [0, 1], [0, 1], [{input: [0.25], output: [0.25]}]); + check([0.5, 'add'], [0, 1], [0, 1], [{input: [0.25], output: [0.75]}]); + check([0, 'exch', 'add'], [0, 1], [0, 1], + [{input: [0.25], output: [0.25]}]); + check([0.5, 'exch', 'add'], [0, 1], [0, 1], + [{input: [0.25], output: [0.75]}]); + check(['add'], [0, 1, 0, 1], [0, 1], + [{input: [0.25, 0.5], output: [0.75]}]); + check(['add'], [0, 1], [0, 1], null); + }); + it('check compiled sub', function() { + check([0.5, 0.25, 'sub'], [], [0, 1], [{input: [], output: [0.25]}]); + check([0, 'sub'], [0, 1], [0, 1], [{input: [0.25], output: [0.25]}]); + check([0.5, 'sub'], [0, 1], [0, 1], [{input: [0.75], output: [0.25]}]); + check([0, 'exch', 'sub'], [0, 1], [-1, 1], + [{input: [0.25], output: [-0.25]}]); + check([0.75, 'exch', 'sub'], [0, 1], [-1, 1], + [{input: [0.25], output: [0.5]}]); + check(['sub'], [0, 1, 0, 1], [-1, 1], + [{input: [0.25, 0.5], output: [-0.25]}]); + check(['sub'], [0, 1], [0, 1], null); + + check([1, 'dup', 3, 2, 'roll', 'sub', 'sub'], [0, 1], [0, 1], + [{input: [0.75], output: [0.75]}]); + }); + it('check compiled mul', function() { + check([0.25, 0.5, 'mul'], [], [0, 1], [{input: [], output: [0.125]}]); + check([0, 'mul'], [0, 1], [0, 1], [{input: [0.25], output: [0]}]); + check([0.5, 'mul'], [0, 1], [0, 1], [{input: [0.25], output: [0.125]}]); + check([1, 'mul'], [0, 1], [0, 1], [{input: [0.25], output: [0.25]}]); + check([0, 'exch', 'mul'], [0, 1], [0, 1], [{input: [0.25], output: [0]}]); + check([0.5, 'exch', 'mul'], [0, 1], [0, 1], + [{input: [0.25], output: [0.125]}]); + check([1, 'exch', 'mul'], [0, 1], [0, 1], + [{input: [0.25], output: [0.25]}]); + check(['mul'], [0, 1, 0, 1], [0, 1], + [{input: [0.25, 0.5], output: [0.125]}]); + check(['mul'], [0, 1], [0, 1], null); + }); + it('check compiled max', function() { + check(['dup', 0.75, 'gt', 7, 'jz', 'pop', 0.75], [0, 1], [0, 1], + [{input: [0.5], output: [0.5]}]); + check(['dup', 0.75, 'gt', 7, 'jz', 'pop', 0.75], [0, 1], [0, 1], + [{input: [1], output: [0.75]}]); + check(['dup', 0.75, 'gt', 5, 'jz', 'pop', 0.75], [0, 1], [0, 1], null); + }); + it('check pop/roll/index', function() { + check([1, 'pop'], [0, 1], [0, 1], [{input: [0.5], output: [0.5]}]); + check([1, 3, -1, 'roll'], [0, 1, 0, 1], [0, 1, 0, 1, 0, 1], + [{input: [0.25, 0.5], output: [0.5, 1, 0.25]}]); + check([1, 3, 1, 'roll'], [0, 1, 0, 1], [0, 1, 0, 1, 0, 1], + [{input: [0.25, 0.5], output: [1, 0.25, 0.5]}]); + check([1, 3, 1.5, 'roll'], [0, 1, 0, 1], [0, 1, 0, 1, 0, 1], null); + check([1, 1, 'index'], [0, 1], [0, 1, 0, 1, 0, 1], + [{input: [0.5], output: [0.5, 1, 0.5]}]); + check([1, 3, 'index', 'pop'], [0, 1], [0, 1], null); + check([1, 0.5, 'index', 'pop'], [0, 1], [0, 1], null); + }); + it('check input boundaries', function () { + check([], [0, 0.5], [0, 1], [{input: [1], output: [0.5]}]); + check([], [0.5, 1], [0, 1], [{input: [0], output: [0.5]}]); + check(['dup'], [0.5, 0.75], [0, 1, 0, 1], + [{input: [0], output: [0.5, 0.5]}]); + check([], [100, 1001], [0, 10000], [{input: [1000], output: [1000]}]); + }); + it('check output boundaries', function () { + check([], [0, 1], [0, 0.5], [{input: [1], output: [0.5]}]); + check([], [0, 1], [0.5, 1], [{input: [0], output: [0.5]}]); + check(['dup'], [0, 1], [0.5, 1, 0.75, 1], + [{input: [0], output: [0.5, 0.75]}]); + check([], [0, 10000], [100, 1001], [{input: [1000], output: [1000]}]); + }); + it('compile optimized', function () { + var compiler = new PostScriptCompiler(); + var code = [0, 'add', 1, 1, 3, -1, 'roll', 'sub', 'sub', 1, 'mul']; + var compiledCode = compiler.compile(code, [0, 1], [0, 1]); + expect(compiledCode).toEqual( + 'dest[destOffset + 0] = Math.max(0, Math.min(1, src[srcOffset + 0]));'); + + }); + }); });