]> git.parisson.com Git - pdf.js.git/commitdiff
Adding type4 postscript function support.
authorBrendan Dahl <brendan.dahl@gmail.com>
Sat, 24 Dec 2011 03:41:12 +0000 (19:41 -0800)
committerBrendan Dahl <brendan.dahl@gmail.com>
Sat, 24 Dec 2011 03:41:12 +0000 (19:41 -0800)
src/function.js
test/unit/function_spec.js [new file with mode: 0644]
test/unit/unit_test.html

index 6b00632180e2b8ef4b2326dfaf9f31c16db97e9c..e9099a68a0e278f6b3d04867e22ad99986d4cd78 100644 (file)
@@ -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 (file)
index 0000000..7c336a6
--- /dev/null
@@ -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
+  });
+});
+
index 1fc28ef83d38efec1633185663bd389ed8cc6287..8d0af03d69d3e76cc8ad20cadfd2914261fcb58b 100644 (file)
 
   <!-- include spec files here... -->
   <script type="text/javascript" src="obj_spec.js"></script>
+  <script type="text/javascript" src="function_spec.js"></script>
 
   <!-- include source files here... -->
+  <script type="text/javascript" src="../../src/core.js"></script>
+  <script type="text/javascript" src="../../src/util.js"></script>
+  <script type="text/javascript" src="../../src/canvas.js"></script>
   <script type="text/javascript" src="../../src/obj.js"></script>
+  <script type="text/javascript" src="../../src/function.js"></script>
+  <script type="text/javascript" src="../../src/charsets.js"></script>
+  <script type="text/javascript" src="../../src/cidmaps.js"></script>
+  <script type="text/javascript" src="../../src/colorspace.js"></script>
+  <script type="text/javascript" src="../../src/crypto.js"></script>
+  <script type="text/javascript" src="../../src/evaluator.js"></script>
+  <script type="text/javascript" src="../../src/fonts.js"></script>
+  <script type="text/javascript" src="../../src/glyphlist.js"></script>
+  <script type="text/javascript" src="../../src/image.js"></script>
+  <script type="text/javascript" src="../../src/metrics.js"></script>
+  <script type="text/javascript" src="../../src/parser.js"></script>
+  <script type="text/javascript" src="../../src/pattern.js"></script>
+  <script type="text/javascript" src="../../src/stream.js"></script>
+  <script type="text/javascript" src="../../src/worker.js"></script>
+  <script type="text/javascript" src="../../external/jpgjs/jpg.js"></script>
 
   <script type="text/javascript">
     'use strict';