From 1089c30b56659d46dee2fd570e3c4957028cab19 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Fri, 23 Dec 2011 19:41:12 -0800 Subject: [PATCH] Adding type4 postscript function support. --- src/function.js | 515 ++++++++++++++++++++++++++++++++++++- test/unit/function_spec.js | 223 ++++++++++++++++ test/unit/unit_test.html | 19 ++ 3 files changed, 751 insertions(+), 6 deletions(-) create mode 100644 test/unit/function_spec.js diff --git a/src/function.js b/src/function.js index 6b0063218..e9099a68a 100644 --- a/src/function.js +++ b/src/function.js @@ -336,16 +336,519 @@ var PDFFunction = (function PDFFunctionClosure() { }; }, - constructPostScript: function pdfFunctionConstructPostScript() { - return [CONSTRUCT_POSTSCRIPT]; + constructPostScript: function pdfFunctionConstructPostScript(fn, dict, xref) { + var domain = dict.get('Domain'); + var range = dict.get('Range'); + + if (!domain) + error('No domain.'); + + if(!range) + error('No range.') + + var lexer = new PostScriptLexer(fn); + var parser = new PostScriptParser(lexer); + var code = parser.parse(); + + return [CONSTRUCT_POSTSCRIPT, domain, range, code]; }, - constructPostScriptFromIR: function pdfFunctionConstructPostScriptFromIR() { - TODO('unhandled type of function'); - return function constructPostScriptFromIRResult() { - return [255, 105, 180]; + constructPostScriptFromIR: + function pdfFunctionConstructPostScriptFromIR(IR) { + var domain = IR[1]; + var range = IR[2]; + var code = IR[3]; + var numOutputs = range.length / 2; + var 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. + var cache = new FunctionCache(); + return function constructPostScriptFromIRResult(args) { + var initialStack = []; + for (var i = 0, ii = (domain.length / 2); i < ii; ++i) { + initialStack.push(args[i]); + } + + var key = initialStack.join('_'); + if (cache.has(key)) + return cache.get(key); + + var stack = evaluator.execute(initialStack); + var transformed = new Array(numOutputs); + for (i = numOutputs - 1; i >= 0; --i) { + var out = stack.pop(); + var rangeIndex = 2 * i; + if (out < range[rangeIndex]) + out = range[rangeIndex]; + else if (out > range[rangeIndex + 1]) + out = range[rangeIndex + 1]; + transformed[i] = out; + } + cache.set(key, transformed); + return transformed; }; } }; })(); +var FunctionCache = (function FunctionCache() { + var MAX_CACHE_SIZE = 1024; + function FunctionCache() { + this.cache = {}; + this.total = 0; + } + FunctionCache.prototype = { + has: function(key) { + return key in this.cache + }, + get: function(key) { + return this.cache[key]; + }, + set: function(key, value) { + if (this.total < MAX_CACHE_SIZE) { + this.cache[key] = value; + this.total++; + } + } + }; + return FunctionCache; +})(); + +var PostScriptStack = (function PostScriptStack() { + var MAX_STACK_SIZE = 100; + function PostScriptStack(initialStack) { + this.stack = initialStack || []; + } + + PostScriptStack.prototype = { + push: function push(value) { + if (this.stack.length >= MAX_STACK_SIZE) + error('PostScript function stack overflow.'); + this.stack.push(value); + }, + pop: function pop() { + if (this.stack.length <= 0) + error('PostScript function stack underflow.'); + return this.stack.pop(); + }, + copy: function copy(n) { + if (this.stack.length + n >= MAX_STACK_SIZE) + error('PostScript function stack overflow.'); + var part = this.stack.slice(this.stack.length - n); + this.stack = this.stack.concat(part); + }, + index: function index(n) { + this.push(this.stack[this.stack.length - n - 1]); + }, + roll: function roll(n, p) { + // rotate the last n stack elements p times + var a = this.stack.splice(this.stack.length - n, n); + // algorithm from http://jsfromhell.com/array/rotate + var l = a.length, p = (Math.abs(p) >= l && (p %= l), + p < 0 && (p += l), p), i, x; + for(; p; p = (Math.ceil(l / p) - 1) * p - l + (l = p)) + for(i = l; i > p; x = a[--i], a[i] = a[i - p], a[i - p] = x); + this.stack = this.stack.concat(a); + } + }; + return PostScriptStack; +})(); +var PostScriptEvaluator = (function PostScriptEvaluator() { + function PostScriptEvaluator(code) { + this.code = code; + console.log(code); + } + PostScriptEvaluator.prototype = { + execute: function(initialStack) { + var stack = new PostScriptStack(initialStack); + var counter = 0; + var code = this.code; + var a, b; + while (counter < this.code.length) { + var instruction = this.code[counter++]; + var operator = instruction[0]; + switch (operator) { + // non standard ps operators + case 'push': + stack.push(instruction[1]); + break; + case 'jz': // jump if false + a = stack.pop(); + if (!a) + counter = instruction[1]; + break; + case 'j': // jump + counter = instruction[1]; + break; + + // all ps operators in alphabetical order (excluding if/ifelse) + case 'abs': + a = stack.pop(); + stack.push(Math.abs(a)); + break; + case 'add': + b = stack.pop(); + a = stack.pop(); + stack.push(a + b); + break; + case 'and': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a && b); + else + stack.push(a & b); + break; + case 'atan': + a = stack.pop(); + stack.push(Math.atan(a)); + break; + case 'bitshift': + b = stack.pop(); + a = stack.pop(); + if (a > 0) + stack.push(a << b); + else + stack.push(a >> b); + break; + case 'ceiling': + a = stack.pop(); + stack.push(Math.ceil(a)); + break; + case 'copy': + a = stack.pop(); + stack.copy(a); + break; + case 'cos': + a = stack.pop(); + stack.push(Math.cos(a)); + break; + case 'cvi': + a = stack.pop(); + if (a >= 0) + stack.push(Math.floor(a)); + else + stack.push(Math.ceil(a)); + break; + case 'cvr': + // noop + break; + case 'div': + b = stack.pop(); + a = stack.pop(); + stack.push(a / b); + break; + case 'dup': + stack.copy(1); + break; + case 'eq': + b = stack.pop(); + a = stack.pop(); + stack.push(a == b); + break; + case 'exch': + stack.roll(2, 1); + break; + case 'exp': + b = stack.pop(); + a = stack.pop(); + stack.push(Math.pow(a, b)); + break; + case 'false': + stack.push(false); + break; + case 'floor': + a = stack.pop(); + stack.push(Math.floor(a)); + break; + case 'ge': + b = stack.pop(); + a = stack.pop(); + stack.push(a >= b); + break; + case 'gt': + b = stack.pop(); + a = stack.pop(); + stack.push(a > b); + break; + case 'idiv': + b = stack.pop(); + a = stack.pop(); + stack.push(Math.floor(a / b)); + break; + case 'index': + a = stack.pop(); + stack.index(a); + break; + case 'le': + b = stack.pop(); + a = stack.pop(); + stack.push(a <= b); + break; + case 'ln': + a = stack.pop(); + stack.push(Math.log(a)); + break; + case 'log': + a = stack.pop(); + stack.push(Math.log(a) / Math.LN10); + break; + case 'lt': + b = stack.pop(); + a = stack.pop(); + stack.push(a < b); + break; + case 'mod': + b = stack.pop(); + a = stack.pop(); + stack.push(a % b); + break; + case 'mul': + b = stack.pop(); + a = stack.pop(); + stack.push(a * b); + break; + case 'ne': + b = stack.pop(); + a = stack.pop(); + stack.push(a != b); + break; + case 'neg': + a = stack.pop(); + stack.push(-1 * b); + break; + case 'not': + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a && b); + else + stack.push(a & b); + break; + case 'or': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a || b); + else + stack.push(a | b); + break; + case 'pop': + stack.pop(); + break; + case 'roll': + b = stack.pop(); + a = stack.pop(); + stack.roll(a, b); + break; + case 'round': + a = stack.pop(); + stack.push(Math.round(a)); + break; + case 'sin': + a = stack.pop(); + stack.push(Math.sin(a)); + break; + case 'sqrt': + a = stack.pop(); + stack.push(Math.sqrt(a)); + break; + case 'sub': + b = stack.pop(); + a = stack.pop(); + stack.push(a - b); + break; + case 'true': + stack.push(true); + break; + case 'truncate': + a = stack.pop(); + if (a >= 0) + stack.push(Math.floor(a)); + else + stack.push(Math.ceil(a)); + break; + case 'xor': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push((a ^ b) ? true : false); + else + stack.push(a ^ b); + break; + default: + error('Unknown operator ' + operator); + break + } + } + return stack.stack; + } + } + return PostScriptEvaluator; +})(); + +var PostScriptParser = (function PostScriptParser() { + function PostScriptParser(lexer) { + this.lexer = lexer; + this.code = []; + this.token; + this.prev; + } + PostScriptParser.prototype = { + nextToken: function nextToken() { + this.prev = this.token; + this.token = this.lexer.getToken(); + }, + accept: function accept(type) { + if (this.token.type == type) { + this.nextToken(); + return true; + } + return false; + }, + expect: function expect(type) { + if (this.accept(type)) + return true; + error('Unexpected symbol: found ' + this.token.type + ' expected ' + + type + '.'); + }, + parse: function parse() { + this.nextToken(); + this.expect(PostScriptTokenTypes.LBRACE); + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + return this.code; + }, + parseBlock: function parseBlock() { + while (true) { + if (this.accept(PostScriptTokenTypes.NUMBER)) { + this.code.push(['push', this.prev.value]); + } else if (this.accept(PostScriptTokenTypes.OPERATOR)) { + this.code.push([this.prev.value]); + } else if (this.accept(PostScriptTokenTypes.LBRACE)) { + this.parseCondition(); + } else { + return; + } + } + }, + parseCondition: function parseCondition() { + var counter = this.code.length - 1; + var condition = []; + this.code.push(condition); + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + if (this.accept(PostScriptTokenTypes.IF)) { + // The true block is right after the 'if' so it just falls through on + // true else it jumps and skips the true block. + condition.push('jz', this.code.length); + } else if(this.accept(PostScriptTokenTypes.LBRACE)) { + var jump = []; + this.code.push(jump); + var endOfTrue = this.code.length; + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + this.expect(PostScriptTokenTypes.IFELSE); + // The jump is added at the end of the true block to skip the false + // block. + jump.push('j', this.code.length); + condition.push('jz', endOfTrue); + } else { + error('PS Function: error parsing conditional.'); + } + } + }; + return PostScriptParser; +})(); + +var PostScriptTokenTypes = { + LBRACE: 0, + RBRACE: 1, + NUMBER: 2, + OPERATOR: 3, + IF: 4, + IFELSE: 5 +}; + +var PostScriptToken = (function PostScriptToken() { + function PostScriptToken(type, value) { + this.type = type; + this.value = value; + } + return PostScriptToken; +})(); + +var PostScriptLexer = (function PostScriptLexer() { + function PostScriptLexer(stream) { + this.stream = stream; + } + PostScriptLexer.prototype = { + getToken: function getToken() { + var s = ''; + var ch; + var comment = false; + var stream = this.stream; + + // skip comments + while (true) { + if (!(ch = stream.getChar())) + return EOF; + + if (comment) { + if (ch == '\x0a' || ch == '\x0d') + comment = false; + } else if (ch == '%') { + comment = true; + } else if (!Lexer.isSpace(ch)) { + break; + } + } + switch (ch) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case '+': case '-': case '.': + return new PostScriptToken(PostScriptTokenTypes.NUMBER, + this.getNumber(ch)); + case '{': + return new PostScriptToken(PostScriptTokenTypes.LBRACE, '{'); + case '}': + return new PostScriptToken(PostScriptTokenTypes.RBRACE, '}'); + } + // operator + var str = ch.toLowerCase(); + while (true) { + ch = stream.lookChar().toLowerCase(); + if (ch >= 'a' && ch <= 'z') + str += ch; + else + break; + stream.skip(); + } + switch (str) { + case 'if': + return new PostScriptToken(PostScriptTokenTypes.IF, str); + case 'ifelse': + return new PostScriptToken(PostScriptTokenTypes.IFELSE, str); + default: + return new PostScriptToken(PostScriptTokenTypes.OPERATOR, str); + } + }, + getNumber: function getNumber(ch) { + var str = ch; + var stream = this.stream; + while (true) { + ch = stream.lookChar(); + if ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.') + str += ch; + else + break; + stream.skip(); + } + var value = parseFloat(str); + if (isNaN(value)) + error('Invalid floating point number: ' + value); + return value; + } + }; + return PostScriptLexer; +})(); + diff --git a/test/unit/function_spec.js b/test/unit/function_spec.js new file mode 100644 index 000000000..7c336a65d --- /dev/null +++ b/test/unit/function_spec.js @@ -0,0 +1,223 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +describe('function', function() { + beforeEach(function () { + this.addMatchers({ + toMatchArray: function(expected) { + var actual = this.actual; + if (actual.length != expected.length) + return false; + for (var i = 0; i < expected.length; i++) { + var a = actual[i], b = expected[i]; + if (isArray(b)) { + if (a.length != b.length) + return false; + for (var j = 0; j < a.length; j++) { + var suba = a[j], subb = b[j]; + if (suba !== subb) + return false + } + } else { + if (a !== b) + return false; + } + } + return true; + } + }); + }); + + describe('PostScriptParser', function() { + function parse(program) { + var stream = new StringStream(program); + var parser = new PostScriptParser(new PostScriptLexer(stream)); + return parser.parse(); + } + it('parses empty programs', function() { + var output = parse('{}'); + expect(output.length).toEqual(0); + }); + it('parses positive numbers', function() { + var number = 999; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [ + ['push', number] + ]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses negative numbers', function() { + var number = -999; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [ + ['push', number] + ]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses negative floats', function() { + var number = 3.3; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [ + ['push', number] + ]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses operators', function() { + var program = parse('{ sub }'); + var expectedProgram = [ + ['sub'] + ]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses if statements', function() { + var program = parse('{ { 99 } if }'); + var expectedProgram = [ + ['jz', 2], + ['push', 99] + ]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses ifelse statements', function() { + var program = parse('{ { 99 } { 44 } ifelse }'); + var expectedProgram = [ + ['jz', 3], + ['push', 99], + ['j', 4], + ['push', 44], + ]; + expect(program).toMatchArray(expectedProgram); + }); + it('handles missing brackets', function() { + expect(function() { parse('{'); }).toThrow( + new Error('Unexpected symbol: found undefined expected 1.')); + }); + }); + + describe('PostScriptEvaluator', function() { + function evaluate(program) { + var stream = new StringStream(program); + var parser = new PostScriptParser(new PostScriptLexer(stream)); + var code = parser.parse(); + var evaluator = new PostScriptEvaluator(code); + var output = evaluator.execute(); + console.log(output); + return output; + } + it('pushes stack', function() { + var stack = evaluate('{ 99 }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles if with true', function() { + var stack = evaluate('{ 1 {99} if }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles if with false', function() { + var stack = evaluate('{ 0 {99} if }'); + var expectedStack = []; + expect(stack).toMatchArray(expectedStack); + }); + it('handles ifelse with true', function() { + var stack = evaluate('{ 1 {99} {77} ifelse }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles ifelse with false', function() { + var stack = evaluate('{ 0 {99} {77} ifelse }'); + var expectedStack = [77]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles nested if', function() { + var stack = evaluate('{ 1 {1 {77} if} if }'); + var expectedStack = [77]; + expect(stack).toMatchArray(expectedStack); + }); + + it('abs', function() { + var stack = evaluate('{ -2 abs }'); + var expectedStack = [2]; + expect(stack).toMatchArray(expectedStack); + }); + it('adds', function() { + var stack = evaluate('{ 1 2 add }'); + var expectedStack = [3]; + expect(stack).toMatchArray(expectedStack); + }); + it('boolean ands', function() { + var stack = evaluate('{ true false and }'); + var expectedStack = [false]; + expect(stack).toMatchArray(expectedStack); + }); + it('bitwise ands', function() { + var stack = evaluate('{ 254 1 and }'); + var expectedStack = [254 & 1]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO atan + // TODO bitshift + // TODO ceiling + // TODO copy + // TODO cos + // TODO cvi + // TODO cvr + // TODO div + it('duplicates', function() { + var stack = evaluate('{ 99 dup }'); + var expectedStack = [99, 99]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO eq + it('exchanges', function() { + var stack = evaluate('{ 44 99 exch }'); + var expectedStack = [99, 44]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO exp + // TODO false + // TODO floor + // TODO ge + // TODO gt + // TODO idiv + it('duplicates index', function() { + var stack = evaluate('{ 4 3 2 1 2 index }'); + var expectedStack = [4, 3, 2, 1, 3]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO le + // TODO ln + // TODO log + // TODO lt + // TODO mod + // TODO mul + // TODO ne + // TODO neg + // TODO not + // TODO or + it('pops stack', function() { + var stack = evaluate('{ 1 2 pop }'); + var expectedStack = [1]; + expect(stack).toMatchArray(expectedStack); + }); + it('rolls stack right', function() { + var stack = evaluate('{ 1 3 2 2 4 1 roll }'); + var expectedStack = [2, 1, 3, 2]; + expect(stack).toMatchArray(expectedStack); + }); + it('rolls stack left', function() { + var stack = evaluate('{ 1 3 2 2 4 -1 roll }'); + var expectedStack = [3, 2, 2, 1]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO round + // TODO sin + // TODO sqrt + // TODO sub + // TODO true + // TODO truncate + // TODO xor + }); +}); + diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html index 1fc28ef83..8d0af03d6 100644 --- a/test/unit/unit_test.html +++ b/test/unit/unit_test.html @@ -11,9 +11,28 @@ + + + + + + + + + + + + + + + + + + +