Bug 878897 - Update pdf.js to version 0.8.229. r=bdahl
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 10 Jun 2013 20:11:41 -0400
changeset 146085 b7637656cc54446ee646a43df732281ea91d239f
parent 146084 63386b71d1b50cc11dd82f1966153e954c9af169
child 146086 172b542f51bae22359cb5c4444f5ceedf0f66590
push id2697
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 18:49:53 +0000
treeherdermozilla-beta@dfec938c7b63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbdahl
bugs878897
milestone24.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 878897 - Update pdf.js to version 0.8.229. r=bdahl
browser/extensions/pdfjs/README.mozilla
browser/extensions/pdfjs/components/PdfStreamConverter.js
browser/extensions/pdfjs/content/build/pdf.js
browser/extensions/pdfjs/content/network.js
browser/extensions/pdfjs/content/web/viewer.css
browser/extensions/pdfjs/content/web/viewer.js
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,4 +1,4 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 0.8.169
+Current extension version is: 0.8.229
 
--- a/browser/extensions/pdfjs/components/PdfStreamConverter.js
+++ b/browser/extensions/pdfjs/components/PdfStreamConverter.js
@@ -193,38 +193,17 @@ PdfDataListener.prototype = {
 // All the priviledged actions.
 function ChromeActions(domWindow, contentDispositionFilename) {
   this.domWindow = domWindow;
   this.contentDispositionFilename = contentDispositionFilename;
 }
 
 ChromeActions.prototype = {
   isInPrivateBrowsing: function() {
-    var docIsPrivate, privateBrowsing;
-    try {
-      docIsPrivate = PrivateBrowsingUtils.isWindowPrivate(this.domWindow);
-    } catch (x) {
-      // unable to use PrivateBrowsingUtils, e.g. FF15
-    }
-    if (typeof docIsPrivate === 'undefined') {
-      // per-window Private Browsing is not supported, trying global service
-      try {
-        privateBrowsing = Cc['@mozilla.org/privatebrowsing;1']
-                            .getService(Ci.nsIPrivateBrowsingService);
-        docIsPrivate = privateBrowsing.privateBrowsingEnabled;
-      } catch (x) {
-        // unable to get nsIPrivateBrowsingService (e.g. not Firefox)
-        docIsPrivate = false;
-      }
-    }
-    // caching the result
-    this.isInPrivateBrowsing = function isInPrivateBrowsingCached() {
-      return docIsPrivate;
-    };
-    return docIsPrivate;
+    return PrivateBrowsingUtils.isWindowPrivate(this.domWindow);
   },
   download: function(data, sendResponse) {
     var self = this;
     var originalUrl = data.originalUrl;
     // The data may not be downloaded so we need just retry getting the pdf with
     // the original url.
     var originalUri = NetUtil.newURI(data.originalUrl);
     var blobUri = data.blobUrl ? NetUtil.newURI(data.blobUrl) : originalUri;
@@ -407,17 +386,17 @@ var RangedChromeActions = (function Rang
   /**
    * This is for range requests
    */
   function RangedChromeActions(
               domWindow, contentDispositionFilename, originalRequest) {
 
     ChromeActions.call(this, domWindow, contentDispositionFilename);
 
-    this.pdfUrl = originalRequest.URI.resolve('');
+    this.pdfUrl = originalRequest.URI.spec;
     this.contentLength = originalRequest.contentLength;
 
     // Pass all the headers from the original request through
     var httpHeaderVisitor = {
       headers: {},
       visitHeader: function(aHeader, aValue) {
         if (aHeader === 'Range') {
           // When loading the PDF from cache, firefox seems to set the Range
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -11,18 +11,18 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 var PDFJS = {};
-PDFJS.version = '0.8.169';
-PDFJS.build = '869b878';
+PDFJS.version = '0.8.229';
+PDFJS.build = 'b996e1b';
 
 (function pdfjsWrapper() {
   // Use strict in our context only - users might not want it
   'use strict';
 
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
@@ -40,25 +40,26 @@ PDFJS.build = '869b878';
  * limitations under the License.
  */
 /* globals assert, MissingDataException, isInt, NetworkManager, Promise,
            isEmptyObj */
 
 'use strict';
 
 var ChunkedStream = (function ChunkedStreamClosure() {
-  function ChunkedStream(length, chunkSize) {
+  function ChunkedStream(length, chunkSize, manager) {
     this.bytes = new Uint8Array(length);
     this.start = 0;
     this.pos = 0;
     this.end = length;
     this.chunkSize = chunkSize;
     this.loadedChunks = [];
     this.numChunksLoaded = 0;
     this.numChunks = Math.ceil(length / chunkSize);
+    this.manager = manager;
   }
 
   // required methods for a stream. if a particular stream does not
   // implement these, an error should be thrown
   ChunkedStream.prototype = {
 
     getMissingChunks: function ChunkedStream_getMissingChunks() {
       var chunks = [];
@@ -198,16 +199,28 @@ var ChunkedStream = (function ChunkedStr
 
     moveStart: function ChunkedStream_moveStart() {
       this.start = this.pos;
     },
 
     makeSubStream: function ChunkedStream_makeSubStream(start, length, dict) {
       function ChunkedStreamSubstream() {}
       ChunkedStreamSubstream.prototype = Object.create(this);
+      ChunkedStreamSubstream.prototype.getMissingChunks = function() {
+        var chunkSize = this.chunkSize;
+        var beginChunk = Math.floor(this.start / chunkSize);
+        var endChunk = Math.floor((this.end - 1) / chunkSize) + 1;
+        var missingChunks = [];
+        for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
+          if (!(chunk in this.loadedChunks)) {
+            missingChunks.push(chunk);
+          }
+        }
+        return missingChunks;
+      };
       var subStream = new ChunkedStreamSubstream();
       subStream.pos = subStream.start = start;
       subStream.end = start + length || this.end;
       subStream.dict = dict;
       return subStream;
     },
 
     isStream: true
@@ -215,17 +228,17 @@ var ChunkedStream = (function ChunkedStr
 
   return ChunkedStream;
 })();
 
 var ChunkedStreamManager = (function ChunkedStreamManagerClosure() {
 
   function ChunkedStreamManager(length, chunkSize, url, args) {
     var self = this;
-    this.stream = new ChunkedStream(length, chunkSize);
+    this.stream = new ChunkedStream(length, chunkSize, this);
     this.length = length;
     this.chunkSize = chunkSize;
     this.url = url;
     this.disableAutoFetch = args.disableAutoFetch;
     var msgHandler = this.msgHandler = args.msgHandler;
 
     if (args.chunkedViewerLoading) {
       msgHandler.on('OnDataRange', this.onReceiveData.bind(this));
@@ -264,60 +277,36 @@ var ChunkedStreamManager = (function Chu
     onLoadedStream: function ChunkedStreamManager_getLoadedStream() {
       return this.loadedStream;
     },
 
     // Get all the chunks that are not yet loaded and groups them into
     // contiguous ranges to load in as few requests as possible
     requestAllChunks: function ChunkedStreamManager_requestAllChunks() {
       var missingChunks = this.stream.getMissingChunks();
-      var chunksToRequest = [];
-      for (var i = 0, n = missingChunks.length; i < n; ++i) {
-        var chunk = missingChunks[i];
-        if (!(chunk in this.requestsByChunk)) {
-          this.requestsByChunk[chunk] = [];
-          chunksToRequest.push(chunk);
-        }
-      }
-      var groupedChunks = this.groupChunks(chunksToRequest);
-      for (var i = 0, n = groupedChunks.length; i < n; ++i) {
-        var groupedChunk = groupedChunks[i];
-        var begin = groupedChunk.beginChunk * this.chunkSize;
-        var end = Math.min(groupedChunk.endChunk * this.chunkSize, this.length);
-        this.sendRequest(begin, end);
-      }
-
+      this.requestChunks(missingChunks);
       return this.loadedStream;
     },
 
-    getStream: function ChunkedStreamManager_getStream() {
-      return this.stream;
-    },
-
-    // Loads any chunks in the requested range that are not yet loaded
-    requestRange: function ChunkedStreamManager_requestRange(
-                      begin, end, callback) {
-
-      end = Math.min(end, this.length);
-
-      var beginChunk = this.getBeginChunk(begin);
-      var endChunk = this.getEndChunk(end);
-
+    requestChunks: function ChunkedStreamManager_requestChunks(chunks,
+                                                               callback) {
       var requestId = this.currRequestId++;
 
       var chunksNeeded;
       this.chunksNeededByRequest[requestId] = chunksNeeded = {};
-      for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
-        if (!this.stream.hasChunk(chunk)) {
-          chunksNeeded[chunk] = true;
+      for (var i = 0, ii = chunks.length; i < ii; i++) {
+        if (!this.stream.hasChunk(chunks[i])) {
+          chunksNeeded[chunks[i]] = true;
         }
       }
 
       if (isEmptyObj(chunksNeeded)) {
-        callback();
+        if (callback) {
+          callback();
+        }
         return;
       }
 
       this.callbacksByRequest[requestId] = callback;
 
       var chunksToRequest = [];
       for (var chunk in chunksNeeded) {
         chunk = chunk | 0;
@@ -337,16 +326,56 @@ var ChunkedStreamManager = (function Chu
       for (var i = 0; i < groupedChunksToRequest.length; ++i) {
         var groupedChunk = groupedChunksToRequest[i];
         var begin = groupedChunk.beginChunk * this.chunkSize;
         var end = Math.min(groupedChunk.endChunk * this.chunkSize, this.length);
         this.sendRequest(begin, end);
       }
     },
 
+    getStream: function ChunkedStreamManager_getStream() {
+      return this.stream;
+    },
+
+    // Loads any chunks in the requested range that are not yet loaded
+    requestRange: function ChunkedStreamManager_requestRange(
+                      begin, end, callback) {
+
+      end = Math.min(end, this.length);
+
+      var beginChunk = this.getBeginChunk(begin);
+      var endChunk = this.getEndChunk(end);
+
+      var chunks = [];
+      for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
+        chunks.push(chunk);
+      }
+
+      this.requestChunks(chunks, callback);
+    },
+
+    requestRanges: function ChunkedStreamManager_requestRanges(ranges,
+                                                               callback) {
+      ranges = ranges || [];
+      var chunksToRequest = [];
+
+      for (var i = 0; i < ranges.length; i++) {
+        var beginChunk = this.getBeginChunk(ranges[i].begin);
+        var endChunk = this.getEndChunk(ranges[i].end);
+        for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
+          if (chunksToRequest.indexOf(chunk) < 0) {
+            chunksToRequest.push(chunk);
+          }
+        }
+      }
+
+      chunksToRequest.sort(function(a, b) { return a - b; });
+      this.requestChunks(chunksToRequest, callback);
+    },
+
     // Groups a sorted array of chunks into as few continguous larger
     // chunks as possible
     groupChunks: function ChunkedStreamManager_groupChunks(chunks) {
       var groupedChunks = [];
       var beginChunk;
       var prevChunk;
       for (var i = 0; i < chunks.length; ++i) {
         var chunk = chunks[i];
@@ -425,27 +454,27 @@ var ChunkedStreamManager = (function Chu
           var lastChunk = this.stream.numChunks - 1;
           if (!this.stream.hasChunk(lastChunk)) {
             nextEmptyChunk = lastChunk;
           }
         } else {
           nextEmptyChunk = this.stream.nextEmptyChunk(endChunk);
         }
         if (isInt(nextEmptyChunk)) {
-          var nextEmptyByte = nextEmptyChunk * this.chunkSize;
-          this.requestRange(nextEmptyByte, nextEmptyByte + this.chunkSize,
-              function() {});
+          this.requestChunks([nextEmptyChunk]);
         }
       }
 
       for (var i = 0; i < loadedRequests.length; ++i) {
         var requestId = loadedRequests[i];
         var callback = this.callbacksByRequest[requestId];
         delete this.callbacksByRequest[requestId];
-        callback();
+        if (callback) {
+          callback();
+        }
       }
 
       this.msgHandler.send('DocProgress', {
         loaded: this.stream.numChunksLoaded * this.chunkSize,
         total: this.length
       });
     },
 
@@ -676,16 +705,17 @@ var Page = (function PageClosure() {
     this.pageIndex = pageIndex;
     this.pageDict = pageDict;
     this.xref = xref;
     this.ref = ref;
     this.idCounters = {
       font: 0,
       obj: 0
     };
+    this.resourcesPromise = null;
   }
 
   Page.prototype = {
     getPageProp: function Page_getPageProp(key) {
       return this.pageDict.get(key);
     },
     inheritPageProp: function Page_inheritPageProp(key) {
       var dict = this.pageDict;
@@ -758,84 +788,93 @@ var Page = (function PageClosure() {
       } else if (isStream(content)) {
         stream = content;
       } else {
         // replacing non-existent page content with empty one
         stream = new NullStream();
       }
       return stream;
     },
+    loadResources: function(keys) {
+      if (!this.resourcesPromise) {
+        // TODO: add async inheritPageProp and remove this.
+        this.resourcesPromise = this.pdfManager.ensure(this, 'resources');
+      }
+      var promise = new Promise();
+      this.resourcesPromise.then(function resourceSuccess() {
+        var objectLoader = new ObjectLoader(this.resources.map,
+                                            keys,
+                                            this.xref);
+        objectLoader.load().then(function objectLoaderSuccess() {
+          promise.resolve();
+        });
+      }.bind(this));
+      return promise;
+    },
     getOperatorList: function Page_getOperatorList(handler) {
       var self = this;
       var promise = new Promise();
 
       function reject(e) {
         promise.reject(e);
       }
 
       var pageListPromise = new Promise();
 
       var pdfManager = this.pdfManager;
       var contentStreamPromise = pdfManager.ensure(this, 'getContentStream',
                                                    []);
-      var resourcesPromise = pdfManager.ensure(this, 'resources');
+      var resourcesPromise = this.loadResources([
+        'ExtGState',
+        'ColorSpace',
+        'Pattern',
+        'Shading',
+        'XObject',
+        'Font',
+        // ProcSet
+        // Properties
+      ]);
 
       var partialEvaluator = new PartialEvaluator(
             pdfManager, this.xref, handler,
             this.pageIndex, 'p' + this.pageIndex + '_',
             this.idCounters);
 
       var dataPromises = Promise.all(
           [contentStreamPromise, resourcesPromise], reject);
       dataPromises.then(function(data) {
         var contentStream = data[0];
-        var resources = data[1];
-
-        pdfManager.ensure(partialEvaluator, 'getOperatorList',
-                          [contentStream, resources]).then(
-          function(opListPromise) {
-            opListPromise.then(function(data) {
-              pageListPromise.resolve(data);
-            });
+
+        partialEvaluator.getOperatorList(contentStream, self.resources).then(
+          function(data) {
+            pageListPromise.resolve(data);
           },
           reject
         );
       });
 
       var annotationsPromise = pdfManager.ensure(this, 'annotations');
       Promise.all([pageListPromise, annotationsPromise]).then(function(datas) {
         var pageData = datas[0];
         var pageQueue = pageData.queue;
         var annotations = datas[1];
 
-        var ensurePromises = [];
-        for (var i = 0, n = annotations.length; i < n; ++i) {
-          var ensurePromise = pdfManager.ensure(annotations[i],
-                                                'getOperatorList',
-                                                [partialEvaluator]);
-          ensurePromises.push(ensurePromise);
-        }
-
-        Promise.all(ensurePromises).then(function(listPromises) {
-          Promise.all(listPromises).then(function(datas) {
-            for (var i = 0, n = datas.length; i < n; ++i) {
-              var annotationData = datas[i];
-              var annotationQueue = annotationData.queue;
-              Util.concatenateToArray(pageQueue.fnArray,
-                                      annotationQueue.fnArray);
-              Util.concatenateToArray(pageQueue.argsArray,
-                                      annotationQueue.argsArray);
-              Util.extendObj(pageData.dependencies,
-                             annotationData.dependencies);
-            }
-
-            PartialEvaluator.optimizeQueue(pageQueue);
-
-            promise.resolve(pageData);
-          }, reject);
+        if (annotations.length === 0) {
+          PartialEvaluator.optimizeQueue(pageQueue);
+          promise.resolve(pageData);
+          return;
+        }
+
+        var dependencies = pageData.dependencies;
+        var annotationsReadyPromise = Annotation.appendToOperatorList(
+          annotations, pageQueue, pdfManager, dependencies, partialEvaluator);
+        annotationsReadyPromise.then(function () {
+          PartialEvaluator.optimizeQueue(pageQueue);
+
+          promise.resolve(pageData);
         }, reject);
       }, reject);
 
       return promise;
     },
     extractTextContent: function Page_extractTextContent() {
       var handler = {
         on: function nullHandlerOn() {},
@@ -844,37 +883,34 @@ var Page = (function PageClosure() {
 
       var self = this;
 
       var textContentPromise = new Promise();
 
       var pdfManager = this.pdfManager;
       var contentStreamPromise = pdfManager.ensure(this, 'getContentStream',
                                                    []);
-      var resourcesPromise = new Promise();
-      pdfManager.ensure(this, 'resources').then(function(resources) {
-        pdfManager.ensure(self.xref, 'fetchIfRef', [resources]).then(
-          function(resources) {
-            resourcesPromise.resolve(resources);
-          }
-        );
-      });
+
+      var resourcesPromise = this.loadResources([
+        'ExtGState',
+        'XObject',
+        'Font'
+      ]);
 
       var dataPromises = Promise.all([contentStreamPromise,
                                       resourcesPromise]);
       dataPromises.then(function(data) {
         var contentStream = data[0];
-        var resources = data[1];
         var partialEvaluator = new PartialEvaluator(
               pdfManager, self.xref, handler,
               self.pageIndex, 'p' + self.pageIndex + '_',
               self.idCounters);
 
         partialEvaluator.getTextContent(
-            contentStream, resources).then(function(bidiTexts) {
+            contentStream, self.resources).then(function(bidiTexts) {
           textContentPromise.resolve({
             bidiTexts: bidiTexts
           });
         });
       });
 
       return textContentPromise;
     },
@@ -921,17 +957,17 @@ var PDFDocument = (function PDFDocumentC
     else
       error('PDFDocument: Unknown argument type');
   }
 
   function init(pdfManager, stream, password) {
     assertWellFormed(stream.length > 0, 'stream must have data');
     this.pdfManager = pdfManager;
     this.stream = stream;
-    var xref = new XRef(this.stream, password);
+    var xref = new XRef(this.stream, password, pdfManager);
     this.xref = xref;
   }
 
   function find(stream, needle, limit, backwards) {
     var pos = stream.pos;
     var end = stream.end;
     var str = '';
     if (pos + limit > end)
@@ -979,18 +1015,18 @@ var PDFDocument = (function PDFDocumentC
           if (linearization.length != length) {
             linearization = false;
           }
         } catch (err) {
           if (err instanceof MissingDataException) {
             throw err;
           }
 
-          warn('The linearization data is not available ' +
-               'or unreadable pdf data is found');
+          info('The linearization data is not available ' +
+               'or unreadable PDF data is found');
           linearization = false;
         }
       }
       // shadow the prototype getter with a data property
       return shadow(this, 'linearization', linearization);
     },
     get startXRef() {
       var stream = this.stream;
@@ -2807,22 +2843,25 @@ var TextRenderingMode = {
   FILL: 0,
   STROKE: 1,
   FILL_STROKE: 2,
   INVISIBLE: 3,
   FILL_ADD_TO_PATH: 4,
   STROKE_ADD_TO_PATH: 5,
   FILL_STROKE_ADD_TO_PATH: 6,
   ADD_TO_PATH: 7,
+  FILL_STROKE_MASK: 3,
   ADD_TO_PATH_FLAG: 4
 };
 
 // Minimal font size that would be used during canvas fillText operations.
 var MIN_FONT_SIZE = 16;
 
+var COMPILE_TYPE3_GLYPHS = true;
+
 function createScratchCanvas(width, height) {
   var canvas = document.createElement('canvas');
   canvas.width = width;
   canvas.height = height;
   return canvas;
 }
 
 function addContextCurrentTransform(ctx) {
@@ -2939,16 +2978,190 @@ function addContextCurrentTransform(ctx)
         m[5]
       ];
 
       this._originalRotate(angle);
     };
   }
 }
 
+var CachedCanvases = (function CachedCanvasesClosure() {
+  var cache = {};
+  return {
+    getCanvas: function CachedCanvases_getCanvas(id, width, height) {
+      var canvas;
+      if (id in cache) {
+        canvas = cache[id];
+        canvas.width = width;
+        canvas.height = height;
+        // reset canvas transform for emulated mozCurrentTransform, if needed
+        canvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);
+      } else {
+        canvas = createScratchCanvas(width, height);
+        cache[id] = canvas;
+      }
+      return canvas;
+    },
+    clear: function () {
+      cache = {};
+    }
+  };
+})();
+
+function compileType3Glyph(imgData) {
+  var POINT_TO_PROCESS_LIMIT = 1000;
+
+  var width = imgData.width, height = imgData.height;
+  var i, j;
+  // we need sparse arrays
+  var points = [];
+  for (i = 0; i <= height; i++) {
+    points.push([]);
+  }
+
+  // finding iteresting points: every point is located between mask pixels,
+  // so there will be points of the (width + 1)x(height + 1) grid. Every point
+  // will have flags assigned based on neighboring mask pixels:
+  //   4 | 8
+  //   --P--
+  //   2 | 1
+  // We are interested only in points with the flags:
+  //   - outside corners: 1, 2, 4, 8;
+  //   - inside corners: 7, 11, 13, 14;
+  //   - and, intersections: 5, 10.
+  var pos = 3, data = imgData.data, lineSize = width * 4, count = 0;
+  if (data[3] !== 0) {
+    points[0][0] = 1;
+    ++count;
+  }
+  for (j = 1; j < width; j++) {
+    if (data[pos] !== data[pos + 4]) {
+      points[0][j] = data[pos] ? 2 : 1;
+      ++count;
+    }
+    pos += 4;
+  }
+  if (data[pos] !== 0) {
+    points[0][j] = 2;
+    ++count;
+  }
+  pos += 4;
+  for (i = 1; i < height; i++) {
+    if (data[pos - lineSize] !== data[pos]) {
+      points[i][0] = data[pos] ? 1 : 8;
+      ++count;
+    }
+    for (j = 1; j < width; j++) {
+      var f1 = data[pos + 4] ? 1 : 0;
+      var f2 = data[pos] ? 1 : 0;
+      var f4 = data[pos - lineSize] ? 1 : 0;
+      var f8 = data[pos - lineSize + 4] ? 1 : 0;
+      var fSum = f1 + f2 + f4 + f8;
+      if (fSum === 1 || fSum === 3 || (fSum === 2 && f1 === f4)) {
+        points[i][j] = f1 | (f2 << 1) | (f4 << 2) | (f8 << 3);
+        ++count;
+      }
+      pos += 4;
+    }
+    if (data[pos - lineSize] !== data[pos]) {
+      points[i][j] = data[pos] ? 2 : 4;
+      ++count;
+    }
+    pos += 4;
+
+    if (count > POINT_TO_PROCESS_LIMIT) {
+      return null;
+    }
+  }
+  pos -= lineSize;
+  if (data[pos] !== 0) {
+    points[i][0] = 8;
+    ++count;
+  }
+  for (j = 1; j < width; j++) {
+    if (data[pos] !== data[pos + 4]) {
+      points[i][j] = data[pos] ? 4 : 8;
+      ++count;
+    }
+    pos += 4;
+  }
+  if (data[pos] !== 0) {
+    points[i][j] = 4;
+    ++count;
+  }
+  if (count > POINT_TO_PROCESS_LIMIT) {
+    return null;
+  }
+
+  // building outlines
+  var outline = [];
+  outline.push('c.save();');
+  // the path shall be painted in [0..1]x[0..1] space
+  outline.push('c.scale(' + (1 / width) + ',' +  (-1 / height) + ');');
+  outline.push('c.translate(0,-' + height + ');');
+  outline.push('c.beginPath();');
+  for (i = 0; i <= height; i++) {
+    if (points[i].length === 0) {
+      continue;
+    }
+    var js = null;
+    for (js in points[i]) {
+      break;
+    }
+    if (js === null) {
+      continue;
+    }
+    var i0 = i, j0 = (j = +js);
+
+    outline.push('c.moveTo(' + j + ',' + i + ');');
+    var type = points[i][j], d = 0;
+    do {
+      if (type === 5 || type === 10) {
+        // line crossed: following dirrection we followed
+        points[i0][j0] = type | (15 ^ d); // changing direction for "future hit"
+        type |= d;
+      }
+
+      switch (type) {
+      case 1:
+      case 13:
+        do { i0++; } while (!points[i0][j0]);
+        d = 9;
+        break;
+      case 4:
+      case 7:
+        do { i0--; } while (!points[i0][j0]);
+        d = 6;
+        break;
+      case 8:
+      case 14:
+        do { j0++; } while (!points[i0][j0]);
+        d = 12;
+        break;
+      case 2:
+      case 11:
+        do { j0--; } while (!points[i0][j0]);
+        d = 3;
+        break;
+      }
+      outline.push('c.lineTo(' + j0 + ',' + i0 + ');');
+
+      type = points[i0][j0];
+      delete points[i0][j0];
+    } while (j0 !== j || i0 !== i);
+    --i;
+  }
+  outline.push('c.fill();');
+  outline.push('c.beginPath();');
+  outline.push('c.restore();');
+
+  /*jshint -W054 */
+  return new Function('c', outline.join('\n'));
+}
+
 var CanvasExtraState = (function CanvasExtraStateClosure() {
   function CanvasExtraState(old) {
     // Are soft masks and alpha values shapes or opacities?
     this.alphaIsShape = false;
     this.fontSize = 0;
     this.fontSizeScale = 1;
     this.textMatrix = IDENTITY_MATRIX;
     this.fontMatrix = FONT_IDENTITY_MATRIX;
@@ -3009,148 +3222,43 @@ var CanvasGraphics = (function CanvasGra
     this.pendingEOFill = false;
     this.res = null;
     this.xobjs = null;
     this.commonObjs = commonObjs;
     this.objs = objs;
     this.textLayer = textLayer;
     this.imageLayer = imageLayer;
     this.groupStack = [];
+    this.processingType3 = null;
     if (canvasCtx) {
       addContextCurrentTransform(canvasCtx);
     }
   }
 
-  function applyStencilMask(imgArray, width, height, inverseDecode, buffer) {
-    var imgArrayPos = 0;
-    var i, j, mask, buf;
-    // removing making non-masked pixels transparent
-    var bufferPos = 3; // alpha component offset
-    for (i = 0; i < height; i++) {
-      mask = 0;
-      for (j = 0; j < width; j++) {
-        if (!mask) {
-          buf = imgArray[imgArrayPos++];
-          mask = 128;
-        }
-        if (!(buf & mask) === inverseDecode) {
-          buffer[bufferPos] = 0;
-        }
-        bufferPos += 4;
-        mask >>= 1;
-      }
-    }
-  }
-
-  function putBinaryImageData(ctx, data, w, h) {
-    var tmpImgData = 'createImageData' in ctx ? ctx.createImageData(w, h) :
-      ctx.getImageData(0, 0, w, h);
-
+  function putBinaryImageData(ctx, imgData) {
+    if (typeof ImageData !== 'undefined' && imgData instanceof ImageData) {
+      ctx.putImageData(imgData, 0, 0);
+      return;
+    }
+
+    var tmpImgData = ctx.createImageData(imgData.width, imgData.height);
+
+    var data = imgData.data;
     var tmpImgDataPixels = tmpImgData.data;
     if ('set' in tmpImgDataPixels)
       tmpImgDataPixels.set(data);
     else {
       // Copy over the imageData pixel by pixel.
       for (var i = 0, ii = tmpImgDataPixels.length; i < ii; i++)
         tmpImgDataPixels[i] = data[i];
     }
 
     ctx.putImageData(tmpImgData, 0, 0);
   }
 
-  function prescaleImage(pixels, width, height, widthScale, heightScale) {
-    pixels = new Uint8Array(pixels); // creating a copy
-    while (widthScale > 2 || heightScale > 2) {
-      if (heightScale > 2) {
-        // scaling image twice vertically
-        var rowSize = width * 4;
-        var k = 0, l = 0;
-        for (var i = 0; i < height - 1; i += 2) {
-          for (var j = 0; j < width; j++) {
-            var alpha1 = pixels[k + 3], alpha2 = pixels[k + 3 + rowSize];
-            if (alpha1 === alpha2) {
-              pixels[l] = (pixels[k] + pixels[k + rowSize]) >> 1;
-              pixels[l + 1] = (pixels[k + 1] + pixels[k + 1 + rowSize]) >> 1;
-              pixels[l + 2] = (pixels[k + 2] + pixels[k + 2 + rowSize]) >> 1;
-              pixels[l + 3] = alpha1;
-            } else if (alpha1 < alpha2) {
-              var d = 256 - alpha2 + alpha1;
-              pixels[l] = (pixels[k] * d + (pixels[k + rowSize] << 8)) >> 9;
-              pixels[l + 1] = (pixels[k + 1] * d +
-                              (pixels[k + 1 + rowSize] << 8)) >> 9;
-              pixels[l + 2] = (pixels[k + 2] * d +
-                              (pixels[k + 2 + rowSize] << 8)) >> 9;
-              pixels[l + 3] = alpha2;
-            } else {
-              var d = 256 - alpha1 + alpha2;
-              pixels[l] = ((pixels[k] << 8) + pixels[k + rowSize] * d) >> 9;
-              pixels[l + 1] = ((pixels[k + 1] << 8) +
-                              pixels[k + 1 + rowSize] * d) >> 9;
-              pixels[l + 2] = ((pixels[k + 2] << 8) +
-                              pixels[k + 2 + rowSize] * d) >> 9;
-              pixels[l + 3] = alpha1;
-            }
-            k += 4; l += 4;
-          }
-          k += rowSize;
-        }
-        if (height & 1) {
-          for (var i = 0; i < rowSize; i++) {
-            pixels[l++] = pixels[k++];
-          }
-        }
-        height = (height + 1) >> 1;
-        heightScale /= 2;
-      }
-      if (widthScale > 2) {
-        // scaling image twice horizontally
-        var k = 0, l = 0;
-        for (var i = 0; i < height; i++) {
-          for (var j = 0; j < width - 1; j += 2) {
-            var alpha1 = pixels[k + 3], alpha2 = pixels[k + 7];
-            if (alpha1 === alpha2) {
-              pixels[l] = (pixels[k] + pixels[k + 4]) >> 1;
-              pixels[l + 1] = (pixels[k + 1] + pixels[k + 5]) >> 1;
-              pixels[l + 2] = (pixels[k + 2] + pixels[k + 6]) >> 1;
-              pixels[l + 3] = alpha1;
-            } else if (alpha1 < alpha2) {
-              var d = 256 - alpha2 + alpha1;
-              pixels[l] = (pixels[k] * d + (pixels[k + 4] << 8)) >> 9;
-              pixels[l + 1] = (pixels[k + 1] * d + (pixels[k + 5] << 8)) >> 9;
-              pixels[l + 2] = (pixels[k + 2] * d + (pixels[k + 6] << 8)) >> 9;
-              pixels[l + 3] = alpha2;
-            } else {
-              var d = 256 - alpha1 + alpha2;
-              pixels[l] = ((pixels[k] << 8) + pixels[k + 4] * d) >> 9;
-              pixels[l + 1] = ((pixels[k + 1] << 8) + pixels[k + 5] * d) >> 9;
-              pixels[l + 2] = ((pixels[k + 2] << 8) + pixels[k + 6] * d) >> 9;
-              pixels[l + 3] = alpha1;
-            }
-            k += 8; l += 4;
-          }
-          if (width & 1) {
-            pixels[l++] = pixels[k++];
-            pixels[l++] = pixels[k++];
-            pixels[l++] = pixels[k++];
-            pixels[l++] = pixels[k++];
-          }
-        }
-        width = (width + 1) >> 1;
-        widthScale /= 2;
-      }
-    }
-
-    var tmpCanvas = createScratchCanvas(width, height);
-    var tmpCtx = tmpCanvas.getContext('2d');
-    putBinaryImageData(tmpCtx, pixels.subarray(0, width * height * 4),
-                               width, height);
-
-    return tmpCanvas;
-  }
-
   function copyCtxState(sourceCtx, destCtx) {
     var properties = ['strokeStyle', 'fillStyle', 'fillRule', 'globalAlpha',
                       'lineWidth', 'lineCap', 'lineJoin', 'miterLimit',
                       'globalCompositeOperation', 'font'];
     for (var i = 0, ii = properties.length; i < ii; i++) {
       var property = properties[i];
       if (property in sourceCtx) {
         destCtx[property] = sourceCtx[property];
@@ -3301,16 +3409,17 @@ var CanvasGraphics = (function CanvasGra
 
         // If the operatorList isn't executed completely yet OR the execution
         // time was short enough, do another execution round.
       }
     },
 
     endDrawing: function CanvasGraphics_endDrawing() {
       this.ctx.restore();
+      CachedCanvases.clear();
 
       if (this.textLayer) {
         this.textLayer.endLayout();
       }
       if (this.imageLayer) {
         this.imageLayer.endLayout();
       }
     },
@@ -3407,20 +3516,16 @@ var CanvasGraphics = (function CanvasGra
     },
     save: function CanvasGraphics_save() {
       this.ctx.save();
       var old = this.current;
       this.stateStack.push(old);
       this.current = old.clone();
     },
     restore: function CanvasGraphics_restore() {
-      if ('textClipLayers' in this) {
-        this.completeTextClipping();
-      }
-
       var prev = this.stateStack.pop();
       if (prev) {
         this.current = prev;
         this.ctx.restore();
       }
     },
     transform: function CanvasGraphics_transform(a, b, c, d, e, f) {
       this.ctx.transform(a, b, c, d, e, f);
@@ -3558,74 +3663,35 @@ var CanvasGraphics = (function CanvasGra
 
     // Text
     beginText: function CanvasGraphics_beginText() {
       this.current.textMatrix = IDENTITY_MATRIX;
       this.current.x = this.current.lineX = 0;
       this.current.y = this.current.lineY = 0;
     },
     endText: function CanvasGraphics_endText() {
-      if ('textClipLayers' in this) {
-        this.swapImageForTextClipping();
-      }
-    },
-    getCurrentTextClipping: function CanvasGraphics_getCurrentTextClipping() {
+      if (!('pendingTextPaths' in this)) {
+        this.ctx.beginPath();
+        return;
+      }
+      var paths = this.pendingTextPaths;
       var ctx = this.ctx;
-      var transform = ctx.mozCurrentTransform;
-      if ('textClipLayers' in this) {
-        // we need to reset only font and transform
-        var maskCtx = this.textClipLayers.maskCtx;
-        maskCtx.setTransform.apply(maskCtx, transform);
-        maskCtx.font = ctx.font;
-        return maskCtx;
-      }
-
-      var canvasWidth = ctx.canvas.width;
-      var canvasHeight = ctx.canvas.height;
-      // keeping track of the text clipping of the separate canvas
-      var maskCanvas = createScratchCanvas(canvasWidth, canvasHeight);
-      var maskCtx = maskCanvas.getContext('2d');
-      maskCtx.setTransform.apply(maskCtx, transform);
-      maskCtx.font = ctx.font;
-      var textClipLayers = {
-        maskCanvas: maskCanvas,
-        maskCtx: maskCtx
-      };
-      this.textClipLayers = textClipLayers;
-      return maskCtx;
-    },
-    swapImageForTextClipping:
-      function CanvasGraphics_swapImageForTextClipping() {
-      var ctx = this.ctx;
-      var canvasWidth = ctx.canvas.width;
-      var canvasHeight = ctx.canvas.height;
-      // saving current image content and clearing whole canvas
+
       ctx.save();
-      ctx.setTransform(1, 0, 0, 1, 0, 0);
-      var data = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
-      this.textClipLayers.imageData = data;
-      ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+      ctx.beginPath();
+      for (var i = 0; i < paths.length; i++) {
+        var path = paths[i];
+        ctx.setTransform.apply(ctx, path.transform);
+        ctx.translate(path.x, path.y);
+        path.addToPath(ctx, path.fontSize);
+      }
       ctx.restore();
-    },
-    completeTextClipping: function CanvasGraphics_completeTextClipping() {
-      var ctx = this.ctx;
-      // applying mask to the image (result is saved in maskCanvas)
-      var maskCtx = this.textClipLayers.maskCtx;
-      maskCtx.setTransform(1, 0, 0, 1, 0, 0);
-      maskCtx.globalCompositeOperation = 'source-in';
-      maskCtx.drawImage(ctx.canvas, 0, 0);
-
-      // restoring image data and applying the result of masked drawing
-      ctx.save();
-      ctx.setTransform(1, 0, 0, 1, 0, 0);
-      ctx.putImageData(this.textClipLayers.imageData, 0, 0);
-      ctx.drawImage(this.textClipLayers.maskCanvas, 0, 0);
-      ctx.restore();
-
-      delete this.textClipLayers;
+      ctx.clip();
+      ctx.beginPath();
+      delete this.pendingTextPaths;
     },
     setCharSpacing: function CanvasGraphics_setCharSpacing(spacing) {
       this.current.charSpacing = spacing;
     },
     setWordSpacing: function CanvasGraphics_setWordSpacing(spacing) {
       this.current.wordSpacing = spacing;
     },
     setHScale: function CanvasGraphics_setHScale(scale) {
@@ -3733,32 +3799,84 @@ var CanvasGraphics = (function CanvasGra
       }
       geometry.spaceWidth = font.spaceWidth;
       geometry.fontName = font.loadedName;
       geometry.fontFamily = font.fallbackName;
       geometry.fontSize = this.current.fontSize;
       return geometry;
     },
 
+    paintChar: function (character, x, y) {
+      var ctx = this.ctx;
+      var current = this.current;
+      var font = current.font;
+      var fontSize = current.fontSize / current.fontSizeScale;
+      var textRenderingMode = current.textRenderingMode;
+      var fillStrokeMode = textRenderingMode &
+        TextRenderingMode.FILL_STROKE_MASK;
+      var isAddToPathSet = !!(textRenderingMode &
+        TextRenderingMode.ADD_TO_PATH_FLAG);
+
+      var addToPath;
+      if (font.disableFontFace || isAddToPathSet) {
+        addToPath = font.renderer.getPathGenerator(character);
+      }
+
+      if (font.disableFontFace) {
+        ctx.save();
+        ctx.translate(x, y);
+        ctx.beginPath();
+        addToPath(ctx, fontSize);
+        if (fillStrokeMode === TextRenderingMode.FILL ||
+            fillStrokeMode === TextRenderingMode.FILL_STROKE) {
+          ctx.fill();
+        }
+        if (fillStrokeMode === TextRenderingMode.STROKE ||
+            fillStrokeMode === TextRenderingMode.FILL_STROKE) {
+          ctx.stroke();
+        }
+        ctx.restore();
+      } else {
+        if (fillStrokeMode === TextRenderingMode.FILL ||
+            fillStrokeMode === TextRenderingMode.FILL_STROKE) {
+          ctx.fillText(character, x, y);
+        }
+        if (fillStrokeMode === TextRenderingMode.STROKE ||
+            fillStrokeMode === TextRenderingMode.FILL_STROKE) {
+          ctx.strokeText(character, x, y);
+        }
+      }
+
+      if (isAddToPathSet) {
+        var paths = this.pendingTextPaths || (this.pendingTextPaths = []);
+        paths.push({
+          transform: ctx.mozCurrentTransform,
+          x: x,
+          y: y,
+          fontSize: fontSize,
+          addToPath: addToPath
+        });
+      }
+    },
+
     showText: function CanvasGraphics_showText(str, skipTextSelection) {
       var ctx = this.ctx;
       var current = this.current;
       var font = current.font;
       var glyphs = font.charsToGlyphs(str);
       var fontSize = current.fontSize;
       var fontSizeScale = current.fontSizeScale;
       var charSpacing = current.charSpacing;
       var wordSpacing = current.wordSpacing;
       var textHScale = current.textHScale * current.fontDirection;
       var fontMatrix = current.fontMatrix || FONT_IDENTITY_MATRIX;
       var glyphsLength = glyphs.length;
       var textLayer = this.textLayer;
       var geom;
       var textSelection = textLayer && !skipTextSelection ? true : false;
-      var textRenderingMode = current.textRenderingMode;
       var canvasWidth = 0.0;
       var vertical = font.vertical;
       var defaultVMetrics = font.defaultVMetrics;
 
       // Type3 fonts - each glyph is a "mini-PDF"
       if (font.coded) {
         ctx.save();
         ctx.transform.apply(ctx, current.textMatrix);
@@ -3777,32 +3895,34 @@ var CanvasGraphics = (function CanvasGra
           var glyph = glyphs[i];
           if (glyph === null) {
             // word break
             this.ctx.translate(wordSpacing, 0);
             current.x += wordSpacing * textHScale;
             continue;
           }
 
+          this.processingType3 = glyph;
           this.save();
           ctx.scale(fontSize, fontSize);
           ctx.transform.apply(ctx, fontMatrix);
           this.executeOperatorList(glyph.operatorList);
           this.restore();
 
           var transformed = Util.applyTransform([glyph.width, 0], fontMatrix);
           var width = (transformed[0] * fontSize + charSpacing) *
                       current.fontDirection;
 
           ctx.translate(width, 0);
           current.x += width * textHScale;
 
           canvasWidth += width;
         }
         ctx.restore();
+        this.processingType3 = null;
       } else {
         ctx.save();
         this.applyTextTransforms();
 
         var lineWidth = current.lineWidth;
         var a1 = current.textMatrix[0], b1 = current.textMatrix[1];
         var scale = Math.sqrt(a1 * a1 + b1 * b1);
         if (scale === 0 || lineWidth === 0)
@@ -3846,71 +3966,37 @@ var CanvasGraphics = (function CanvasGra
           if (!glyph.disabled) {
             if (vertical) {
               scaledX = vx / fontSizeScale;
               scaledY = (x + vy) / fontSizeScale;
             } else {
               scaledX = x / fontSizeScale;
               scaledY = 0;
             }
-            if (accent) {
-              scaledAccentX = scaledX + accent.offset.x / fontSizeScale;
-              scaledAccentY = scaledY - accent.offset.y / fontSizeScale;
-            }
 
             if (font.remeasure && width > 0) {
               // some standard fonts may not have the exact width, trying to
               // rescale per character
               var measuredWidth = ctx.measureText(character).width * 1000 /
                 current.fontSize * current.fontSizeScale;
               var characterScaleX = width / measuredWidth;
               restoreNeeded = true;
               ctx.save();
               ctx.scale(characterScaleX, 1);
               scaledX /= characterScaleX;
               if (accent) {
                 scaledAccentX /= characterScaleX;
               }
             }
 
-            switch (textRenderingMode) {
-              default: // other unsupported rendering modes
-              case TextRenderingMode.FILL:
-              case TextRenderingMode.FILL_ADD_TO_PATH:
-                ctx.fillText(character, scaledX, scaledY);
-                if (accent) {
-                  ctx.fillText(accent.fontChar, scaledAccentX, scaledAccentY);
-                }
-                break;
-              case TextRenderingMode.STROKE:
-              case TextRenderingMode.STROKE_ADD_TO_PATH:
-                ctx.strokeText(character, scaledX, scaledY);
-                if (accent) {
-                  ctx.strokeText(accent.fontChar, scaledAccentX, scaledAccentY);
-                }
-                break;
-              case TextRenderingMode.FILL_STROKE:
-              case TextRenderingMode.FILL_STROKE_ADD_TO_PATH:
-                ctx.fillText(character, scaledX, scaledY);
-                ctx.strokeText(character, scaledX, scaledY);
-                if (accent) {
-                  ctx.fillText(accent.fontChar, scaledAccentX, scaledAccentY);
-                  ctx.strokeText(accent.fontChar, scaledAccentX, scaledAccentY);
-                }
-                break;
-              case TextRenderingMode.INVISIBLE:
-              case TextRenderingMode.ADD_TO_PATH:
-                break;
-            }
-            if (textRenderingMode & TextRenderingMode.ADD_TO_PATH_FLAG) {
-              var clipCtx = this.getCurrentTextClipping();
-              clipCtx.fillText(character, scaledX, scaledY);
-              if (accent) {
-                clipCtx.fillText(accent.fontChar, scaledAccentX, scaledAccentY);
-              }
+            this.paintChar(character, scaledX, scaledY);
+            if (accent) {
+              scaledAccentX = scaledX + accent.offset.x / fontSizeScale;
+              scaledAccentY = scaledY - accent.offset.y / fontSizeScale;
+              this.paintChar(accent.fontChar, scaledAccentX, scaledAccentY);
             }
           }
 
           x += charWidth;
 
           canvasWidth += charWidth;
 
           if (restoreNeeded) {
@@ -4290,16 +4376,25 @@ var CanvasGraphics = (function CanvasGra
         this.ctx.imageSmoothingEnabled = false;
       } else {
         this.ctx.mozImageSmoothingEnabled = false;
       }
       this.ctx.drawImage(groupCtx.canvas, 0, 0);
       this.restore();
     },
 
+    beginAnnotations: function CanvasGraphics_beginAnnotations() {
+      this.save();
+      this.current = new CanvasExtraState();
+    },
+
+    endAnnotations: function CanvasGraphics_endAnnotations() {
+      this.restore();
+    },
+
     beginAnnotation: function CanvasGraphics_beginAnnotation(rect, transform,
                                                              matrix) {
       this.save();
 
       if (rect && isArray(rect) && 4 == rect.length) {
         var width = rect[2] - rect[0];
         var height = rect[3] - rect[1];
         this.rectangle(rect[0], rect[1], width, height);
@@ -4338,67 +4433,84 @@ var CanvasGraphics = (function CanvasGra
           top: position[1],
           width: w / currentTransform[0],
           height: h / currentTransform[3]
         });
       }
       this.restore();
     },
 
-    paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(
-                             imgArray, inverseDecode, width, height) {
+    paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) {
       var ctx = this.ctx;
-      var tmpCanvas = createScratchCanvas(width, height);
-      var tmpCtx = tmpCanvas.getContext('2d');
+      var width = img.width, height = img.height;
+
+      var glyph = this.processingType3;
+
+      if (COMPILE_TYPE3_GLYPHS && glyph && !('compiled' in glyph)) {
+        var MAX_SIZE_TO_COMPILE = 1000;
+        if (width <= MAX_SIZE_TO_COMPILE && height <= MAX_SIZE_TO_COMPILE) {
+          glyph.compiled =
+            compileType3Glyph({data: img.data, width: width, height: height});
+        } else {
+          glyph.compiled = null;
+        }
+      }
+
+      if (glyph && glyph.compiled) {
+        glyph.compiled(ctx);
+        return;
+      }
+
+      var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height);
+      var maskCtx = maskCanvas.getContext('2d');
+      maskCtx.save();
+
+      putBinaryImageData(maskCtx, img);
+
+      maskCtx.globalCompositeOperation = 'source-in';
 
       var fillColor = this.current.fillColor;
-      tmpCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
+      maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
                           fillColor.type === 'Pattern') ?
-                          fillColor.getPattern(tmpCtx) : fillColor;
-      tmpCtx.fillRect(0, 0, width, height);
-
-      var imgData = tmpCtx.getImageData(0, 0, width, height);
-      var pixels = imgData.data;
-
-      applyStencilMask(imgArray, width, height, inverseDecode, pixels);
-
-      this.paintInlineImageXObject(imgData);
+                          fillColor.getPattern(maskCtx) : fillColor;
+      maskCtx.fillRect(0, 0, width, height);
+
+      maskCtx.restore();
+
+      this.paintInlineImageXObject(maskCanvas);
     },
 
     paintImageMaskXObjectGroup:
       function CanvasGraphics_paintImageMaskXObjectGroup(images) {
       var ctx = this.ctx;
-      var tmpCanvasWidth = 0, tmpCanvasHeight = 0, tmpCanvas, tmpCtx;
+
       for (var i = 0, ii = images.length; i < ii; i++) {
         var image = images[i];
-        var w = image.width, h = image.height;
-        if (w > tmpCanvasWidth || h > tmpCanvasHeight) {
-          tmpCanvasWidth = Math.max(w, tmpCanvasWidth);
-          tmpCanvasHeight = Math.max(h, tmpCanvasHeight);
-          tmpCanvas = createScratchCanvas(tmpCanvasWidth, tmpCanvasHeight);
-          tmpCtx = tmpCanvas.getContext('2d');
-
-          var fillColor = this.current.fillColor;
-          tmpCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
-                              fillColor.type === 'Pattern') ?
-                              fillColor.getPattern(tmpCtx) : fillColor;
-        }
-        tmpCtx.fillRect(0, 0, w, h);
-
-        var imgData = tmpCtx.getImageData(0, 0, w, h);
-        var pixels = imgData.data;
-
-        applyStencilMask(image.data, w, h, image.inverseDecode, pixels);
-
-        tmpCtx.putImageData(imgData, 0, 0);
+        var width = image.width, height = image.height;
+
+        var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height);
+        var maskCtx = maskCanvas.getContext('2d');
+        maskCtx.save();
+
+        putBinaryImageData(maskCtx, image);
+
+        maskCtx.globalCompositeOperation = 'source-in';
+
+        var fillColor = this.current.fillColor;
+        maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
+                            fillColor.type === 'Pattern') ?
+                            fillColor.getPattern(maskCtx) : fillColor;
+        maskCtx.fillRect(0, 0, width, height);
+
+        maskCtx.restore();
 
         ctx.save();
         ctx.transform.apply(ctx, image.transform);
         ctx.scale(1, -1);
-        ctx.drawImage(tmpCanvas, 0, 0, w, h,
+        ctx.drawImage(maskCanvas, 0, 0, width, height,
                       0, -1, 1, 1);
         ctx.restore();
       }
     },
 
     paintImageXObject: function CanvasGraphics_paintImageXObject(objId) {
       var imgData = this.objs.get(objId);
       if (!imgData)
@@ -4407,42 +4519,66 @@ var CanvasGraphics = (function CanvasGra
       this.paintInlineImageXObject(imgData);
     },
 
     paintInlineImageXObject:
       function CanvasGraphics_paintInlineImageXObject(imgData) {
       var width = imgData.width;
       var height = imgData.height;
       var ctx = this.ctx;
+
       this.save();
       // scale the image to the unit square
       ctx.scale(1 / width, -1 / height);
 
       var currentTransform = ctx.mozCurrentTransformInverse;
-      var widthScale = Math.max(Math.abs(currentTransform[0]), 1);
-      var heightScale = Math.max(Math.abs(currentTransform[3]), 1);
-      var tmpCanvas = createScratchCanvas(width, height);
-      var tmpCtx = tmpCanvas.getContext('2d');
-
-      if (widthScale > 2 || heightScale > 2) {
-        // canvas does not resize well large images to small -- using simple
-        // algorithm to perform pre-scaling
-        tmpCanvas = prescaleImage(imgData.data,
-                                 width, height,
-                                 widthScale, heightScale);
-        ctx.drawImage(tmpCanvas, 0, 0, tmpCanvas.width, tmpCanvas.height,
-                                 0, -height, width, height);
-      } else {
-        if (typeof ImageData !== 'undefined' && imgData instanceof ImageData) {
-          tmpCtx.putImageData(imgData, 0, 0);
-        } else {
-          putBinaryImageData(tmpCtx, imgData.data, width, height);
-        }
-        ctx.drawImage(tmpCanvas, 0, -height);
-      }
+      var a = currentTransform[0], b = currentTransform[1];
+      var widthScale = Math.max(Math.sqrt(a * a + b * b), 1);
+      var c = currentTransform[2], d = currentTransform[3];
+      var heightScale = Math.max(Math.sqrt(c * c + d * d), 1);
+
+      var imgToPaint;
+      if (imgData instanceof HTMLElement) {
+        imgToPaint = imgData;
+      } else {
+        var tmpCanvas = CachedCanvases.getCanvas('inlineImage', width, height);
+        var tmpCtx = tmpCanvas.getContext('2d');
+        putBinaryImageData(tmpCtx, imgData);
+        imgToPaint = tmpCanvas;
+      }
+
+      var paintWidth = width, paintHeight = height;
+      var tmpCanvasId = 'prescale1';
+      // Vertial or horizontal scaling shall not be more than 2 to not loose the
+      // pixels during drawImage operation, painting on the temporary canvas(es)
+      // that are twice smaller in size
+      while ((widthScale > 2 && paintWidth > 1) ||
+             (heightScale > 2 && paintHeight > 1)) {
+        var newWidth = paintWidth, newHeight = paintHeight;
+        if (widthScale > 2 && paintWidth > 1) {
+          newWidth = Math.ceil(paintWidth / 2);
+          widthScale /= paintWidth / newWidth;
+        }
+        if (heightScale > 2 && paintHeight > 1) {
+          newHeight = Math.ceil(paintHeight / 2);
+          heightScale /= paintHeight / newHeight;
+        }
+        var tmpCanvas = CachedCanvases.getCanvas(tmpCanvasId,
+                                                 newWidth, newHeight);
+        tmpCtx = tmpCanvas.getContext('2d');
+        tmpCtx.clearRect(0, 0, newWidth, newHeight);
+        tmpCtx.drawImage(imgToPaint, 0, 0, paintWidth, paintHeight,
+                                     0, 0, newWidth, newHeight);
+        imgToPaint = tmpCanvas;
+        paintWidth = newWidth;
+        paintHeight = newHeight;
+        tmpCanvasId = tmpCanvasId === 'prescale1' ? 'prescale2' : 'prescale1';
+      }
+      ctx.drawImage(imgToPaint, 0, 0, paintWidth, paintHeight,
+                                0, -height, width, height);
 
       if (this.imageLayer) {
         var position = this.getCanvasPosition(0, -height);
         this.imageLayer.appendImage({
           imgData: imgData,
           left: position[0],
           top: position[1],
           width: width / currentTransform[0],
@@ -4453,19 +4589,19 @@ var CanvasGraphics = (function CanvasGra
     },
 
     paintInlineImageXObjectGroup:
       function CanvasGraphics_paintInlineImageXObjectGroup(imgData, map) {
       var ctx = this.ctx;
       var w = imgData.width;
       var h = imgData.height;
 
-      var tmpCanvas = createScratchCanvas(w, h);
+      var tmpCanvas = CachedCanvases.getCanvas('inlineImage', w, h);
       var tmpCtx = tmpCanvas.getContext('2d');
-      putBinaryImageData(tmpCtx, imgData.data, w, h);
+      putBinaryImageData(tmpCtx, imgData);
 
       for (var i = 0, ii = map.length; i < ii; i++) {
         var entry = map[i];
         ctx.save();
         ctx.transform.apply(ctx, entry.transform);
         ctx.scale(1, -1);
         ctx.drawImage(tmpCanvas, entry.x, entry.y, entry.w, entry.h,
                       0, -1, 1, 1);
@@ -4615,16 +4751,48 @@ var Dict = (function DictClosure() {
       if (typeof (value = this.map[key2]) != 'undefined' || key2 in this.map ||
           typeof key3 == 'undefined') {
         return xref ? xref.fetchIfRef(value) : value;
       }
       value = this.map[key3] || null;
       return xref ? xref.fetchIfRef(value) : value;
     },
 
+    // Same as get(), but returns a promise and uses fetchIfRefAsync().
+    getAsync: function Dict_getAsync(key1, key2, key3) {
+      var value;
+      var promise;
+      var xref = this.xref;
+      if (typeof (value = this.map[key1]) !== undefined || key1 in this.map ||
+          typeof key2 === undefined) {
+        if (xref) {
+          return xref.fetchIfRefAsync(value);
+        }
+        promise = new Promise();
+        promise.resolve(value);
+        return promise;
+      }
+      if (typeof (value = this.map[key2]) !== undefined || key2 in this.map ||
+          typeof key3 === undefined) {
+        if (xref) {
+          return xref.fetchIfRefAsync(value);
+        }
+        promise = new Promise();
+        promise.resolve(value);
+        return promise;
+      }
+      value = this.map[key3] || null;
+      if (xref) {
+        return xref.fetchIfRefAsync(value);
+      }
+      promise = new Promise();
+      promise.resolve(value);
+      return promise;
+    },
+
     // no dereferencing
     getRaw: function Dict_getRaw(key) {
       return this.map[key];
     },
 
     // creates new map and dereferences all Refs
     getAll: function Dict_getAll() {
       var all = {};
@@ -4668,21 +4836,25 @@ var Ref = (function RefClosure() {
 // this structure stores only one instance of the reference.
 var RefSet = (function RefSetClosure() {
   function RefSet() {
     this.dict = {};
   }
 
   RefSet.prototype = {
     has: function RefSet_has(ref) {
-      return !!this.dict['R' + ref.num + '.' + ref.gen];
+      return ('R' + ref.num + '.' + ref.gen) in this.dict;
     },
 
     put: function RefSet_put(ref) {
-      this.dict['R' + ref.num + '.' + ref.gen] = ref;
+      this.dict['R' + ref.num + '.' + ref.gen] = true;
+    },
+
+    remove: function RefSet_remove(ref) {
+      delete this.dict['R' + ref.num + '.' + ref.gen];
     }
   };
 
   return RefSet;
 })();
 
 var Catalog = (function CatalogClosure() {
   function Catalog(pdfManager, xref) {
@@ -5340,17 +5512,16 @@ var XRef = (function XRefClosure() {
           this.startXRefQueue.shift();
         }
 
         return this.topDict;
       } catch (e) {
         if (e instanceof MissingDataException) {
           throw e;
         }
-
         log('(while reading XRef): ' + e);
       }
 
       if (recoveryMode)
         return;
       throw new XRefParseException();
     },
 
@@ -5467,16 +5638,40 @@ var XRef = (function XRefClosure() {
         }
       }
       e = entries[e.gen];
       if (!e) {
         error('bad XRef entry for compressed object');
       }
       return e;
     },
+    fetchIfRefAsync: function XRef_fetchIfRefAsync(obj) {
+      if (!isRef(obj)) {
+        var promise = new Promise();
+        promise.resolve(obj);
+        return promise;
+      }
+      return this.fetchAsync(obj);
+    },
+    fetchAsync: function XRef_fetchAsync(ref, suppressEncryption) {
+      var promise = new Promise();
+      var tryFetch = function (promise) {
+        try {
+          promise.resolve(this.fetch(ref, suppressEncryption));
+        } catch (e) {
+          if (e instanceof MissingDataException) {
+            this.stream.manager.requestRange(e.begin, e.end, tryFetch);
+            return;
+          }
+          promise.reject(e);
+        }
+      }.bind(this, promise);
+      tryFetch();
+      return promise;
+    },
     getCatalogObj: function XRef_getCatalogObj() {
       return this.root;
     }
   };
 
   return XRef;
 })();
 
@@ -5643,16 +5838,151 @@ var PDFObjects = (function PDFObjectsClo
 
     clear: function PDFObjects_clear() {
       this.objs = {};
     }
   };
   return PDFObjects;
 })();
 
+/**
+ * A helper for loading missing data in object graphs. It traverses the graph
+ * depth first and queues up any objects that have missing data. Once it has
+ * has traversed as many objects that are available it attempts to bundle the
+ * missing data requests and then resume from the nodes that weren't ready.
+ *
+ * NOTE: It provides protection from circular references by keeping track of
+ * of loaded references. However, you must be careful not to load any graphs
+ * that have references to the catalog or other pages since that will cause the
+ * entire PDF document object graph to be traversed.
+ */
+var ObjectLoader = (function() {
+
+  function mayHaveChildren(value) {
+    return isRef(value) || isDict(value) || isArray(value) || isStream(value);
+  }
+
+  function addChildren(node, nodesToVisit) {
+    if (isDict(node) || isStream(node)) {
+      var map;
+      if (isDict(node)) {
+        map = node.map;
+      } else {
+        map = node.dict.map;
+      }
+      for (var key in map) {
+        var value = map[key];
+        if (mayHaveChildren(value)) {
+          nodesToVisit.push(value);
+        }
+      }
+    } else if (isArray(node)) {
+      for (var i = 0, ii = node.length; i < ii; i++) {
+        var value = node[i];
+        if (mayHaveChildren(value)) {
+          nodesToVisit.push(value);
+        }
+      }
+    }
+  }
+
+  function ObjectLoader(obj, keys, xref) {
+    this.obj = obj;
+    this.keys = keys;
+    this.xref = xref;
+    this.refSet = null;
+  }
+
+  ObjectLoader.prototype = {
+
+    load: function ObjectLoader_load() {
+      var keys = this.keys;
+      this.promise = new Promise();
+      // Don't walk the graph if all the data is already loaded.
+      if (!(this.xref.stream instanceof ChunkedStream) ||
+          this.xref.stream.getMissingChunks().length === 0) {
+        this.promise.resolve();
+        return this.promise;
+      }
+
+      this.refSet = new RefSet();
+      // Setup the initial nodes to visit.
+      var nodesToVisit = [];
+      for (var i = 0; i < keys.length; i++) {
+        nodesToVisit.push(this.obj[keys[i]]);
+      }
+
+      this.walk(nodesToVisit);
+      return this.promise;
+    },
+
+    walk: function ObjectLoader_walk(nodesToVisit) {
+      var nodesToRevisit = [];
+      var pendingRequests = [];
+      // DFS walk of the object graph.
+      while (nodesToVisit.length) {
+        var currentNode = nodesToVisit.pop();
+
+        // Only references or chunked streams can cause missing data exceptions.
+        if (isRef(currentNode)) {
+          // Skip nodes that have already been visited.
+          if (this.refSet.has(currentNode)) {
+            continue;
+          }
+          try {
+            var ref = currentNode;
+            this.refSet.put(ref);
+            currentNode = this.xref.fetch(currentNode);
+          } catch (e) {
+            if (!(e instanceof MissingDataException)) {
+              throw e;
+            }
+            nodesToRevisit.push(currentNode);
+            pendingRequests.push({ begin: e.begin, end: e.end });
+          }
+        }
+        if (currentNode instanceof ChunkedStream &&
+            currentNode.getMissingChunks().length) {
+          nodesToRevisit.push(currentNode);
+          pendingRequests.push({
+            begin: currentNode.start,
+            end: currentNode.end
+          });
+        }
+
+        addChildren(currentNode, nodesToVisit);
+      }
+
+      if (pendingRequests.length) {
+        this.xref.stream.manager.requestRanges(pendingRequests,
+            function pendingRequestCallback() {
+          nodesToVisit = nodesToRevisit;
+          for (var i = 0; i < nodesToRevisit.length; i++) {
+            var node = nodesToRevisit[i];
+            // Remove any reference nodes from the currrent refset so they
+            // aren't skipped when we revist them.
+            if (isRef(node)) {
+              this.refSet.remove(node);
+            }
+          }
+          this.walk(nodesToVisit);
+        }.bind(this));
+        return;
+      }
+      // Everything is loaded.
+      this.refSet = null;
+      this.promise.resolve();
+    }
+
+  };
+
+  return ObjectLoader;
+})();
+
+
 
 
 var Annotation = (function AnnotationClosure() {
   // 12.5.5: Algorithm: Appearance streams
   function getTransformMatrix(rect, bbox, matrix) {
     var bounds = Util.getAxialAlignedBoundingBox(bbox, matrix);
     var minX = bounds[0];
     var minY = bounds[1];
@@ -5705,24 +6035,33 @@ var Annotation = (function AnnotationClo
     var dict = params.dict;
     var data = this.data = {};
 
     data.subtype = dict.get('Subtype').name;
     var rect = dict.get('Rect');
     data.rect = Util.normalizeRect(rect);
     data.annotationFlags = dict.get('F');
 
-    var border = dict.get('BS');
-    if (isDict(border)) {
-      var borderWidth = border.has('W') ? border.get('W') : 1;
-      data.border = {
-        width: borderWidth,
-        type: border.get('S') || 'S',
-        rgb: dict.get('C') || [0, 0, 1]
-      };
+    var color = dict.get('C');
+    if (isArray(color) && color.length === 3) {
+      // TODO(mack): currently only supporting rgb; need support different
+      // colorspaces
+      data.color = color;
+    } else {
+      data.color = [0, 0, 0];
+    }
+
+    // Some types of annotations have border style dict which has more
+    // info than the border array
+    if (dict.has('BS')) {
+      var borderStyle = dict.get('BS');
+      data.borderWidth = borderStyle.has('W') ? borderStyle.get('W') : 1;
+    } else {
+      var borderArray = dict.get('Border') || [0, 0, 1];
+      data.borderWidth = borderArray[2];
     }
 
     this.appearance = getDefaultAppearance(dict);
   }
 
   Annotation.prototype = {
 
     getData: function Annotation_getData() {
@@ -5733,17 +6072,18 @@ var Annotation = (function AnnotationClo
       return false;
     },
 
     getHtmlElement: function Annotation_getHtmlElement(commonObjs) {
       throw new NotImplementedException(
         'getHtmlElement() should be implemented in subclass');
     },
 
-    getEmptyContainer: function Annotaiton_getEmptyContainer(tagName, rect) {
+    // TODO(mack): Remove this, it's not really that helpful.
+    getEmptyContainer: function Annotation_getEmptyContainer(tagName, rect) {
       assert(!isWorker,
         'getEmptyContainer() should be called from main thread');
 
       rect = rect || this.data.rect;
       var element = document.createElement(tagName);
       element.style.width = Math.ceil(rect[2] - rect[0]) + 'px';
       element.style.height = Math.ceil(rect[3] - rect[1]) + 'px';
       return element;
@@ -5754,17 +6094,31 @@ var Annotation = (function AnnotationClo
       return !!(
         data &&
         (!data.annotationFlags ||
          !(data.annotationFlags & 0x22)) && // Hidden or NoView
         data.rect                            // rectangle is nessessary
       );
     },
 
-    getOperatorList: function Annotation_appendToOperatorList(evaluator) {
+    loadResources: function(keys) {
+      var promise = new Promise();
+      this.appearance.dict.getAsync('Resources').then(function(resources) {
+        var objectLoader = new ObjectLoader(resources.map,
+                                            keys,
+                                            resources.xref);
+        objectLoader.load().then(function() {
+          promise.resolve(resources);
+        });
+      }.bind(this));
+
+      return promise;
+    },
+
+    getOperatorList: function Annotation_getToOperatorList(evaluator) {
 
       var promise = new Promise();
 
       if (!this.appearance) {
         promise.resolve({
           queue: {
             fnArray: [],
             argsArray: []
@@ -5772,36 +6126,47 @@ var Annotation = (function AnnotationClo
           dependency: {}
         });
         return promise;
       }
 
       var data = this.data;
 
       var appearanceDict = this.appearance.dict;
-      var resources = appearanceDict.get('Resources');
+      var resourcesPromise = this.loadResources([
+        'ExtGState',
+        'ColorSpace',
+        'Pattern',
+        'Shading',
+        'XObject',
+        'Font'
+        // ProcSet
+        // Properties
+      ]);
       var bbox = appearanceDict.get('BBox') || [0, 0, 1, 1];
       var matrix = appearanceDict.get('Matrix') || [1, 0, 0, 1, 0 ,0];
       var transform = getTransformMatrix(data.rect, bbox, matrix);
 
       var border = data.border;
 
-      var listPromise = evaluator.getOperatorList(this.appearance, resources);
-      listPromise.then(function(appearanceStreamData) {
-        var fnArray = appearanceStreamData.queue.fnArray;
-        var argsArray = appearanceStreamData.queue.argsArray;
-
-        fnArray.unshift('beginAnnotation');
-        argsArray.unshift([data.rect, transform, matrix]);
-
-        fnArray.push('endAnnotation');
-        argsArray.push([]);
-
-        promise.resolve(appearanceStreamData);
-      });
+      resourcesPromise.then(function(resources) {
+        var listPromise = evaluator.getOperatorList(this.appearance, resources);
+        listPromise.then(function(appearanceStreamData) {
+          var fnArray = appearanceStreamData.queue.fnArray;
+          var argsArray = appearanceStreamData.queue.argsArray;
+
+          fnArray.unshift('beginAnnotation');
+          argsArray.unshift([data.rect, transform, matrix]);
+
+          fnArray.push('endAnnotation');
+          argsArray.push([]);
+
+          promise.resolve(appearanceStreamData);
+        });
+      }.bind(this));
 
       return promise;
     }
   };
 
   Annotation.getConstructor =
       function Annotation_getConstructor(subtype, fieldType) {
 
@@ -5814,17 +6179,21 @@ var Annotation = (function AnnotationClo
       return LinkAnnotation;
     } else if (subtype === 'Text') {
       return TextAnnotation;
     } else if (subtype === 'Widget') {
       if (!fieldType) {
         return;
       }
 
-      return WidgetAnnotation;
+      if (fieldType === 'Tx') {
+        return TextWidgetAnnotation;
+      } else {
+        return WidgetAnnotation;
+      }
     } else {
       return Annotation;
     }
   };
 
   // TODO(mack): Support loading annotation from data
   Annotation.fromData = function Annotation_fromData(data) {
     var subtype = data.subtype;
@@ -5865,16 +6234,51 @@ var Annotation = (function AnnotationClo
 
     if (annotation.isViewable()) {
       return annotation;
     } else {
       TODO('unimplemented annotation type: ' + subtype);
     }
   };
 
+  Annotation.appendToOperatorList = function Annotation_appendToOperatorList(
+      annotations, pageQueue, pdfManager, dependencies, partialEvaluator) {
+
+    function reject(e) {
+      annotationsReadyPromise.reject(e);
+    }
+
+    var annotationsReadyPromise = new Promise();
+
+    var annotationPromises = [];
+    for (var i = 0, n = annotations.length; i < n; ++i) {
+      annotationPromises.push(annotations[i].getOperatorList(partialEvaluator));
+    }
+
+    Promise.all(annotationPromises).then(function(datas) {
+      var fnArray = pageQueue.fnArray;
+      var argsArray = pageQueue.argsArray;
+      fnArray.push('beginAnnotations');
+      argsArray.push([]);
+      for (var i = 0, n = datas.length; i < n; ++i) {
+        var annotationData = datas[i];
+        var annotationQueue = annotationData.queue;
+        Util.concatenateToArray(fnArray, annotationQueue.fnArray);
+        Util.concatenateToArray(argsArray, annotationQueue.argsArray);
+        Util.extendObj(dependencies, annotationData.dependencies);
+      }
+      fnArray.push('endAnnotations');
+      argsArray.push([]);
+
+      annotationsReadyPromise.resolve();
+    }, reject);
+
+    return annotationsReadyPromise;
+  };
+
   return Annotation;
 })();
 PDFJS.Annotation = Annotation;
 
 
 var WidgetAnnotation = (function WidgetAnnotationClosure() {
 
   function WidgetAnnotation(params) {
@@ -5938,16 +6342,158 @@ var WidgetAnnotation = (function WidgetA
 
       return parent.isViewable.call(this);
     }
   });
 
   return WidgetAnnotation;
 })();
 
+var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() {
+  function TextWidgetAnnotation(params) {
+    WidgetAnnotation.call(this, params);
+
+    if (params.data) {
+      return;
+    }
+
+    this.data.textAlignment = Util.getInheritableProperty(params.dict, 'Q');
+  }
+
+  // TODO(mack): This dupes some of the logic in CanvasGraphics.setFont()
+  function setTextStyles(element, item, fontObj) {
+
+    var style = element.style;
+    style.fontSize = item.fontSize + 'px';
+    style.direction = item.fontDirection < 0 ? 'rtl': 'ltr';
+
+    if (!fontObj) {
+      return;
+    }
+
+    style.fontWeight = fontObj.black ?
+                            (fontObj.bold ? 'bolder' : 'bold') :
+                            (fontObj.bold ? 'bold' : 'normal');
+    style.fontStyle = fontObj.italic ? 'italic' : 'normal';
+
+    var fontName = fontObj.loadedName;
+    var fontFamily = fontName ? '"' + fontName + '", ' : '';
+    // Use a reasonable default font if the font doesn't specify a fallback
+    var fallbackName = fontObj.fallbackName || 'Helvetica, sans-serif';
+    style.fontFamily = fontFamily + fallbackName;
+  }
+
+
+  var parent = WidgetAnnotation.prototype;
+  Util.inherit(TextWidgetAnnotation, WidgetAnnotation, {
+    hasHtml: function TextWidgetAnnotation_hasHtml() {
+      return !!this.data.fieldValue;
+    },
+
+    getHtmlElement: function TextWidgetAnnotation_getHtmlElement(commonObjs) {
+      assert(!isWorker, 'getHtmlElement() shall be called from main thread');
+
+      var item = this.data;
+
+      var element = this.getEmptyContainer('div');
+      element.style.display = 'table';
+
+      var content = document.createElement('div');
+      content.textContent = item.fieldValue;
+      var textAlignment = item.textAlignment;
+      content.style.textAlign = ['left', 'center', 'right'][textAlignment];
+      content.style.verticalAlign = 'middle';
+      content.style.display = 'table-cell';
+
+      var fontObj = item.fontRefName ?
+                    commonObjs.getData(item.fontRefName) : null;
+      var cssRules = setTextStyles(content, item, fontObj);
+
+      element.appendChild(content);
+
+      return element;
+    },
+
+    getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator) {
+
+
+      var promise = new Promise();
+      var data = this.data;
+
+      // Even if there is an appearance stream, ignore it. This is the
+      // behaviour used by Adobe Reader.
+
+      var defaultAppearance = data.defaultAppearance;
+      if (!defaultAppearance) {
+        promise.resolve({
+          queue: {
+            fnArray: [],
+            argsArray: []
+          },
+          dependency: {}
+        });
+        return promise;
+      }
+
+      // Include any font resources found in the default appearance
+
+      var stream = new Stream(stringToBytes(defaultAppearance));
+      var listPromise = evaluator.getOperatorList(stream, this.fieldResources);
+      listPromise.then(function(appearanceStreamData) {
+        var appearanceFnArray = appearanceStreamData.queue.fnArray;
+        var appearanceArgsArray = appearanceStreamData.queue.argsArray;
+        var fnArray = [];
+        var argsArray = [];
+
+        // TODO(mack): Add support for stroke color
+        data.rgb = [0, 0, 0];
+        for (var i = 0, n = fnArray.length; i < n; ++i) {
+          var fnName = appearanceFnArray[i];
+          var args = appearanceArgsArray[i];
+          if (fnName === 'dependency') {
+            var dependency = args[i];
+            if (dependency.indexOf('g_font_') === 0) {
+              data.fontRefName = dependency;
+            }
+            fnArray.push(fnName);
+            argsArray.push(args);
+          } else if (fnName === 'setFont') {
+            data.fontRefName = args[0];
+            var size = args[1];
+            if (size < 0) {
+              data.fontDirection = -1;
+              data.fontSize = -size;
+            } else {
+              data.fontDirection = 1;
+              data.fontSize = size;
+            }
+          } else if (fnName === 'setFillRGBColor') {
+            data.rgb = args;
+          } else if (fnName === 'setFillGray') {
+            var rgbValue = args[0] * 255;
+            data.rgb = [rgbValue, rgbValue, rgbValue];
+          }
+        }
+        promise.resolve({
+          queue: {
+            fnArray: fnArray,
+            argsArray: argsArray
+          },
+          dependency: {}
+        });
+
+      });
+
+      return promise;
+    }
+  });
+
+  return TextWidgetAnnotation;
+})();
+
 var TextAnnotation = (function TextAnnotationClosure() {
   function TextAnnotation(params) {
     Annotation.call(this, params);
 
     if (params.data) {
       return;
     }
 
@@ -6123,17 +6669,34 @@ var LinkAnnotation = (function LinkAnnot
       return false;
     },
 
     hasHtml: function LinkAnnotation_hasHtml() {
       return true;
     },
 
     getHtmlElement: function LinkAnnotation_getHtmlElement(commonObjs) {
-      var element = this.getEmptyContainer('a');
+      var rect = this.data.rect;
+      var element = document.createElement('a');
+      var borderWidth = this.data.borderWidth;
+
+      element.style.borderWidth = borderWidth + 'px';
+      var color = this.data.color;
+      var rgb = [];
+      for (var i = 0; i < 3; ++i) {
+        rgb[i] = Math.round(color[i] * 255);
+      }
+      element.style.borderColor = Util.makeCssRgb(rgb);
+      element.style.borderStyle = 'solid';
+
+      var width = rect[2] - rect[0] - 2 * borderWidth;
+      var height = rect[3] - rect[1] - 2 * borderWidth;
+      element.style.width = width + 'px';
+      element.style.height = height + 'px';
+
       element.href = this.data.url || '';
       return element;
     }
   });
 
   return LinkAnnotation;
 })();
 
@@ -14415,17 +14978,17 @@ var IndexedCS = (function IndexedCSClosu
     if (isStream(lookup)) {
       lookupArray = new Uint8Array(length);
       var bytes = lookup.getBytes(length);
       lookupArray.set(bytes);
     } else if (isString(lookup)) {
       lookupArray = new Uint8Array(length);
       for (var i = 0; i < length; ++i)
         lookupArray[i] = lookup.charCodeAt(i);
-    } else if (lookup instanceof Uint8Array) {
+    } else if (lookup instanceof Uint8Array || lookup instanceof Array) {
       lookupArray = lookup;
     } else {
       error('Unrecognized lookup table: ' + lookup);
     }
     this.lookup = lookupArray;
   }
 
   IndexedCS.prototype = {
@@ -15658,17 +16221,18 @@ var PartialEvaluator = (function Partial
         var width = dict.get('Width', 'W');
         var height = dict.get('Height', 'H');
         var bitStrideLength = (width + 7) >> 3;
         var imgArray = image.getBytes(bitStrideLength * height);
         var decode = dict.get('Decode', 'D');
         var inverseDecode = !!decode && decode[0] > 0;
 
         retData.fn = 'paintImageMaskXObject';
-        retData.args = [imgArray, inverseDecode, width, height];
+        retData.args = [PDFImage.createMask(imgArray, width, height,
+                                            inverseDecode)];
         return retData;
       }
 
       var softMask = dict.get('SMask', 'SM') || false;
       var mask = dict.get('Mask') || false;
 
       var SMALL_IMAGE_DIMENSIONS = 200;
       // Inlining small images into the queue as RGB data
@@ -15944,19 +16508,16 @@ var PartialEvaluator = (function Partial
       font.loadedName = 'g_font_' + this.uniquePrefix +
                         (this.idCounters.font + 1);
 
       if (!font.translated) {
         var translated;
         try {
           translated = this.translateFont(font, xref);
         } catch (e) {
-          if (e instanceof MissingDataException) {
-            throw e;
-          }
           translated = new ErrorFont(e instanceof Error ? e.message : e);
         }
         font.translated = translated;
       }
 
       if (font.translated.loadCharProcs) {
         var charProcs = font.get('CharProcs').getAll();
         var fontResources = font.get('Resources') || resources;
@@ -16015,231 +16576,218 @@ var PartialEvaluator = (function Partial
       resources = resources || new Dict();
       var xobjs = resources.get('XObject') || new Dict();
       var patterns = resources.get('Pattern') || new Dict();
       // TODO(mduan): pass array of knownCommands rather than OP_MAP
       // dictionary
       var parser = new Parser(new Lexer(stream, OP_MAP), false, xref);
 
       var promise = new Promise();
-      function parseCommands() {
-        try {
-          parser.restoreState();
-          var args = [];
-          while (true) {
-
-            var obj = parser.getObj();
-
-            if (isEOF(obj)) {
-              break;
-            }
-
-            if (isCmd(obj)) {
-              var cmd = obj.cmd;
-
-              // Check that the command is valid
-              var opSpec = OP_MAP[cmd];
-              if (!opSpec) {
-                warn('Unknown command "' + cmd + '"');
-                continue;
-              }
-
-              var fn = opSpec.fnName;
-
-              // Validate the number of arguments for the command
-              if (opSpec.variableArgs) {
-                if (args.length > opSpec.numArgs) {
-                  info('Command ' + fn + ': expected [0,' + opSpec.numArgs +
-                      '] args, but received ' + args.length + ' args');
-                }
+      var args = [];
+      while (true) {
+
+        var obj = parser.getObj();
+
+        if (isEOF(obj)) {
+          break;
+        }
+
+        if (isCmd(obj)) {
+          var cmd = obj.cmd;
+
+          // Check that the command is valid
+          var opSpec = OP_MAP[cmd];
+          if (!opSpec) {
+            warn('Unknown command "' + cmd + '"');
+            continue;
+          }
+
+          var fn = opSpec.fnName;
+
+          // Validate the number of arguments for the command
+          if (opSpec.variableArgs) {
+            if (args.length > opSpec.numArgs) {
+              info('Command ' + fn + ': expected [0,' + opSpec.numArgs +
+                  '] args, but received ' + args.length + ' args');
+            }
+          } else {
+            if (args.length < opSpec.numArgs) {
+              // If we receive too few args, it's not possible to possible
+              // to execute the command, so skip the command
+              info('Command ' + fn + ': because expected ' +
+                   opSpec.numArgs + ' args, but received ' + args.length +
+                   ' args; skipping');
+              args = [];
+              continue;
+            } else if (args.length > opSpec.numArgs) {
+              info('Command ' + fn + ': expected ' + opSpec.numArgs +
+                  ' args, but received ' + args.length + ' args');
+            }
+          }
+
+          // TODO figure out how to type-check vararg functions
+
+          if ((cmd == 'SCN' || cmd == 'scn') &&
+               !args[args.length - 1].code) {
+            // compile tiling patterns
+            var patternName = args[args.length - 1];
+            // SCN/scn applies patterns along with normal colors
+            var pattern;
+            if (isName(patternName) &&
+                (pattern = patterns.get(patternName.name))) {
+
+              var dict = isStream(pattern) ? pattern.dict : pattern;
+              var typeNum = dict.get('PatternType');
+
+              if (typeNum == TILING_PATTERN) {
+                var patternPromise = self.handleTilingType(
+                    fn, args, resources, pattern, dict);
+                fn = 'promise';
+                args = [patternPromise];
+              } else if (typeNum == SHADING_PATTERN) {
+                var shading = dict.get('Shading');
+                var matrix = dict.get('Matrix');
+                var pattern = Pattern.parseShading(shading, matrix, xref,
+                                                    resources);
+                args = pattern.getIR();
               } else {
-                if (args.length < opSpec.numArgs) {
-                  // If we receive too few args, it's not possible to possible
-                  // to execute the command, so skip the command
-                  info('Command ' + fn + ': because expected ' +
-                       opSpec.numArgs + ' args, but received ' + args.length +
-                       ' args; skipping');
-                  args = [];
-                  continue;
-                } else if (args.length > opSpec.numArgs) {
-                  info('Command ' + fn + ': expected ' + opSpec.numArgs +
-                      ' args, but received ' + args.length + ' args');
-                }
-              }
-
-              // TODO figure out how to type-check vararg functions
-
-              if ((cmd == 'SCN' || cmd == 'scn') &&
-                   !args[args.length - 1].code) {
-                // compile tiling patterns
-                var patternName = args[args.length - 1];
-                // SCN/scn applies patterns along with normal colors
-                var pattern;
-                if (isName(patternName) &&
-                    (pattern = patterns.get(patternName.name))) {
-
-                  var dict = isStream(pattern) ? pattern.dict : pattern;
-                  var typeNum = dict.get('PatternType');
-
-                  if (typeNum == TILING_PATTERN) {
-                    var patternPromise = self.handleTilingType(
-                        fn, args, resources, pattern, dict);
-                    fn = 'promise';
-                    args = [patternPromise];
-                  } else if (typeNum == SHADING_PATTERN) {
-                    var shading = dict.get('Shading');
-                    var matrix = dict.get('Matrix');
-                    var pattern = Pattern.parseShading(shading, matrix, xref,
-                                                        resources);
-                    args = pattern.getIR();
-                  } else {
-                    error('Unkown PatternType ' + typeNum);
-                  }
-                }
-              } else if (cmd == 'Do' && !args[0].code) {
-                // eagerly compile XForm objects
-                var name = args[0].name;
-                var xobj = xobjs.get(name);
-                if (xobj) {
-                  assertWellFormed(
-                      isStream(xobj), 'XObject should be a stream');
-
-                  var type = xobj.dict.get('Subtype');
-                  assertWellFormed(
-                    isName(type),
-                    'XObject should have a Name subtype'
-                  );
-
-                  if ('Form' == type.name) {
-                    fn = 'promise';
-                    args = [self.buildFormXObject(resources, xobj)];
-                  } else if ('Image' == type.name) {
-                    var data = self.buildPaintImageXObject(
-                        resources, xobj, false);
-                    Util.extendObj(dependencies, data.dependencies);
-                    self.insertDependencies(queue, data.dependencies);
-                    fn = data.fn;
-                    args = data.args;
-                  } else {
-                    error('Unhandled XObject subtype ' + type.name);
-                  }
-                }
-              } else if (cmd == 'Tf') { // eagerly collect all fonts
+                error('Unkown PatternType ' + typeNum);
+              }
+            }
+          } else if (cmd == 'Do' && !args[0].code) {
+            // eagerly compile XForm objects
+            var name = args[0].name;
+            var xobj = xobjs.get(name);
+            if (xobj) {
+              assertWellFormed(
+                  isStream(xobj), 'XObject should be a stream');
+
+              var type = xobj.dict.get('Subtype');
+              assertWellFormed(
+                isName(type),
+                'XObject should have a Name subtype'
+              );
+
+              if ('Form' == type.name) {
                 fn = 'promise';
-                args = [self.handleSetFont(resources, args)];
-              } else if (cmd == 'EI') {
+                args = [self.buildFormXObject(resources, xobj)];
+              } else if ('Image' == type.name) {
                 var data = self.buildPaintImageXObject(
-                    resources, args[0], true);
+                    resources, xobj, false);
                 Util.extendObj(dependencies, data.dependencies);
                 self.insertDependencies(queue, data.dependencies);
                 fn = data.fn;
                 args = data.args;
-              }
-
-              switch (fn) {
-                // Parse the ColorSpace data to a raw format.
-                case 'setFillColorSpace':
-                case 'setStrokeColorSpace':
-                  args = [ColorSpace.parseToIR(args[0], xref, resources)];
-                  break;
-                case 'shadingFill':
-                  var shadingRes = resources.get('Shading');
-                  if (!shadingRes)
-                    error('No shading resource found');
-
-                  var shading = shadingRes.get(args[0].name);
-                  if (!shading)
-                    error('No shading object found');
-
-                  var shadingFill = Pattern.parseShading(
-                      shading, null, xref, resources);
-                  var patternIR = shadingFill.getIR();
-                  args = [patternIR];
-                  fn = 'shadingFill';
-                  break;
-                case 'setGState':
-                  var dictName = args[0];
-                  var extGState = resources.get('ExtGState');
-
-                  if (!isDict(extGState) || !extGState.has(dictName.name))
-                    break;
-
-                  var gState = extGState.get(dictName.name);
-                  fn = 'promise';
-                  args = [self.setGState(resources, gState)];
-              } // switch
-
-              fnArray.push(fn);
-              argsArray.push(args);
-              args = [];
-              parser.saveState();
-            } else if (obj !== null && obj !== undefined) {
-              args.push(obj instanceof Dict ? obj.getAll() : obj);
-              assertWellFormed(args.length <= 33, 'Too many arguments');
-            }
-          }
-
-          var subQueuePromises = [];
-          for (var i = 0; i < fnArray.length; ++i) {
-            if (fnArray[i] === 'promise') {
-              subQueuePromises.push(argsArray[i][0]);
-            }
-          }
-          Promise.all(subQueuePromises).then(function(datas) {
-            // TODO(mack): Optimize by using repositioning elements
-            // in original queue rather than creating new queue
-
-            for (var i = 0, n = datas.length; i < n; ++i) {
-              var data = datas[i];
-              var subQueue = data.queue;
-              queue.transparency = subQueue.transparency || queue.transparency;
-              Util.extendObj(dependencies, data.dependencies);
-            }
-
-            var newFnArray = [];
-            var newArgsArray = [];
-            var currOffset = 0;
-            var subQueueIdx = 0;
-            for (var i = 0, n = fnArray.length; i < n; ++i) {
-              var offset = i + currOffset;
-              if (fnArray[i] === 'promise') {
-                var data = datas[subQueueIdx++];
-                var subQueue = data.queue;
-                var subQueueFnArray = subQueue.fnArray;
-                var subQueueArgsArray = subQueue.argsArray;
-                for (var j = 0, nn = subQueueFnArray.length; j < nn; ++j) {
-                  newFnArray[offset + j] = subQueueFnArray[j];
-                  newArgsArray[offset + j] = subQueueArgsArray[j];
-                }
-                currOffset += subQueueFnArray.length - 1;
               } else {
-                newFnArray[offset] = fnArray[i];
-                newArgsArray[offset] = argsArray[i];
-              }
-            }
-
-            promise.resolve({
-              queue: {
-                fnArray: newFnArray,
-                argsArray: newArgsArray,
-                transparency: queue.transparency
-              },
-              dependencies: dependencies
-            });
-          });
-        } catch (e) {
-          if (!(e instanceof MissingDataException)) {
-            throw e;
-          }
-
-          self.pdfManager.requestRange(e.begin, e.end).then(parseCommands);
-        }
-      }
-      parser.saveState();
-      parseCommands();
+                error('Unhandled XObject subtype ' + type.name);
+              }
+            }
+          } else if (cmd == 'Tf') { // eagerly collect all fonts
+            fn = 'promise';
+            args = [self.handleSetFont(resources, args)];
+          } else if (cmd == 'EI') {
+            var data = self.buildPaintImageXObject(
+                resources, args[0], true);
+            Util.extendObj(dependencies, data.dependencies);
+            self.insertDependencies(queue, data.dependencies);
+            fn = data.fn;
+            args = data.args;
+          }
+
+          switch (fn) {
+            // Parse the ColorSpace data to a raw format.
+            case 'setFillColorSpace':
+            case 'setStrokeColorSpace':
+              args = [ColorSpace.parseToIR(args[0], xref, resources)];
+              break;
+            case 'shadingFill':
+              var shadingRes = resources.get('Shading');
+              if (!shadingRes)
+                error('No shading resource found');
+
+              var shading = shadingRes.get(args[0].name);
+              if (!shading)
+                error('No shading object found');
+
+              var shadingFill = Pattern.parseShading(
+                  shading, null, xref, resources);
+              var patternIR = shadingFill.getIR();
+              args = [patternIR];
+              fn = 'shadingFill';
+              break;
+            case 'setGState':
+              var dictName = args[0];
+              var extGState = resources.get('ExtGState');
+
+              if (!isDict(extGState) || !extGState.has(dictName.name))
+                break;
+
+              var gState = extGState.get(dictName.name);
+              fn = 'promise';
+              args = [self.setGState(resources, gState)];
+          } // switch
+
+          fnArray.push(fn);
+          argsArray.push(args);
+          args = [];
+          parser.saveState();
+        } else if (obj !== null && obj !== undefined) {
+          args.push(obj instanceof Dict ? obj.getAll() : obj);
+          assertWellFormed(args.length <= 33, 'Too many arguments');
+        }
+      }
+
+      var subQueuePromises = [];
+      for (var i = 0; i < fnArray.length; ++i) {
+        if (fnArray[i] === 'promise') {
+          subQueuePromises.push(argsArray[i][0]);
+        }
+      }
+      Promise.all(subQueuePromises).then(function(datas) {
+        // TODO(mack): Optimize by using repositioning elements
+        // in original queue rather than creating new queue
+
+        for (var i = 0, n = datas.length; i < n; ++i) {
+          var data = datas[i];
+          var subQueue = data.queue;
+          queue.transparency = subQueue.transparency || queue.transparency;
+          Util.extendObj(dependencies, data.dependencies);
+        }
+
+        var newFnArray = [];
+        var newArgsArray = [];
+        var currOffset = 0;
+        var subQueueIdx = 0;
+        for (var i = 0, n = fnArray.length; i < n; ++i) {
+          var offset = i + currOffset;
+          if (fnArray[i] === 'promise') {
+            var data = datas[subQueueIdx++];
+            var subQueue = data.queue;
+            var subQueueFnArray = subQueue.fnArray;
+            var subQueueArgsArray = subQueue.argsArray;
+            for (var j = 0, nn = subQueueFnArray.length; j < nn; ++j) {
+              newFnArray[offset + j] = subQueueFnArray[j];
+              newArgsArray[offset + j] = subQueueArgsArray[j];
+            }
+            currOffset += subQueueFnArray.length - 1;
+          } else {
+            newFnArray[offset] = fnArray[i];
+            newArgsArray[offset] = argsArray[i];
+          }
+        }
+
+        promise.resolve({
+          queue: {
+            fnArray: newFnArray,
+            argsArray: newArgsArray,
+            transparency: queue.transparency
+          },
+          dependencies: dependencies
+        });
+      });
 
       return promise;
     },
 
     getTextContent: function PartialEvaluator_getTextContent(
                                                     stream, resources) {
 
       var SPACE_FACTOR = 0.35;
@@ -16267,171 +16815,158 @@ var PartialEvaluator = (function Partial
       resources = this.xref.fetchIfRef(resources) || new Dict();
       // The xobj is parsed iff it's needed, e.g. if there is a `DO` cmd.
       var xobjs = null;
 
       var parser = new Parser(new Lexer(stream), false);
 
       var chunkPromises = [];
       var fontPromise;
-      function parseCommands() {
-        try {
-          parser.restoreState();
-          var args = [];
-
-          while (true) {
-            var obj = parser.getObj();
-            if (isEOF(obj)) {
-              break;
-            }
-
-            if (isCmd(obj)) {
-              var cmd = obj.cmd;
-              switch (cmd) {
-                // TODO: Add support for SAVE/RESTORE and XFORM here.
-                case 'Tf':
-                  fontPromise = handleSetFont(args[0].name, null, resources);
-                  //.translated;
-                  break;
-                case 'TJ':
-                  var chunkPromise = new Promise();
-                  chunkPromises.push(chunkPromise);
-                  fontPromise.then(function(items, chunkPromise, font) {
-                    var chunk = '';
-                    for (var j = 0, jj = items.length; j < jj; j++) {
-                      if (typeof items[j] === 'string') {
-                        chunk += fontCharsToUnicode(items[j], font);
-                      } else if (items[j] < 0 && font.spaceWidth > 0) {
-                        var fakeSpaces = -items[j] / font.spaceWidth;
-                        if (fakeSpaces > MULTI_SPACE_FACTOR) {
-                          fakeSpaces = Math.round(fakeSpaces);
-                          while (fakeSpaces--) {
-                            chunk += ' ';
-                          }
-                        } else if (fakeSpaces > SPACE_FACTOR) {
-                          chunk += ' ';
-                        }
+      var args = [];
+
+      while (true) {
+        var obj = parser.getObj();
+        if (isEOF(obj)) {
+          break;
+        }
+
+        if (isCmd(obj)) {
+          var cmd = obj.cmd;
+          switch (cmd) {
+            // TODO: Add support for SAVE/RESTORE and XFORM here.
+            case 'Tf':
+              fontPromise = handleSetFont(args[0].name, null, resources);
+              //.translated;
+              break;
+            case 'TJ':
+              var chunkPromise = new Promise();
+              chunkPromises.push(chunkPromise);
+              fontPromise.then(function(items, chunkPromise, font) {
+                var chunk = '';
+                for (var j = 0, jj = items.length; j < jj; j++) {
+                  if (typeof items[j] === 'string') {
+                    chunk += fontCharsToUnicode(items[j], font);
+                  } else if (items[j] < 0 && font.spaceWidth > 0) {
+                    var fakeSpaces = -items[j] / font.spaceWidth;
+                    if (fakeSpaces > MULTI_SPACE_FACTOR) {
+                      fakeSpaces = Math.round(fakeSpaces);
+                      while (fakeSpaces--) {
+                        chunk += ' ';
                       }
-                    }
-                    chunkPromise.resolve(
-                        getBidiText(chunk, -1, font.vertical));
-                  }.bind(null, args[0], chunkPromise));
-                  break;
-                case 'Tj':
-                  var chunkPromise = new Promise();
-                  chunkPromises.push(chunkPromise);
-                  fontPromise.then(function(charCodes, chunkPromise, font) {
-                    var chunk = fontCharsToUnicode(charCodes, font);
-                    chunkPromise.resolve(
-                        getBidiText(chunk, -1, font.vertical));
-                  }.bind(null, args[0], chunkPromise));
-                  break;
-                case '\'':
-                  // For search, adding a extra white space for line breaks
-                  // would be better here, but that causes too much spaces in
-                  // the text-selection divs.
-                  var chunkPromise = new Promise();
-                  chunkPromises.push(chunkPromise);
-                  fontPromise.then(function(charCodes, chunkPromise, font) {
-                    var chunk = fontCharsToUnicode(charCodes, font);
-                    chunkPromise.resolve(
-                        getBidiText(chunk, -1, font.vertical));
-                  }.bind(null, args[0], chunkPromise));
-                  break;
-                case '"':
-                  // Note comment in "'"
-                  var chunkPromise = new Promise();
-                  chunkPromises.push(chunkPromise);
-                  fontPromise.then(function(charCodes, chunkPromise, font) {
-                    var chunk = fontCharsToUnicode(charCodes, font);
-                    chunkPromise.resolve(
-                        getBidiText(chunk, -1, font.vertical));
-                  }.bind(null, args[2], chunkPromise));
-                  break;
-                case 'Do':
-                  if (args[0].code) {
-                    break;
-                  }
-
-                  if (!xobjs) {
-                    xobjs = resources.get('XObject') || new Dict();
-                  }
-
-                  var name = args[0].name;
-                  var xobj = xobjs.get(name);
-                  if (!xobj)
-                    break;
-                  assertWellFormed(isStream(xobj),
-                                   'XObject should be a stream');
-
-                  var type = xobj.dict.get('Subtype');
-                  assertWellFormed(
-                    isName(type),
-                    'XObject should have a Name subtype'
-                  );
-
-                  if ('Form' !== type.name)
-                    break;
-
-                  var chunkPromise = self.getTextContent(
-                    xobj,
-                    xobj.dict.get('Resources') || resources
-                  );
-                  chunkPromises.push(chunkPromise);
-                  break;
-                case 'gs':
-                  var dictName = args[0];
-                  var extGState = resources.get('ExtGState');
-
-                  if (!isDict(extGState) || !extGState.has(dictName.name))
-                    break;
-
-                  var gsState = extGState.get(dictName.name);
-
-                  for (var i = 0; i < gsState.length; i++) {
-                    if (gsState[i] === 'Font') {
-                      fontPromise = handleSetFont(
-                          args[0].name, null, resources);
+                    } else if (fakeSpaces > SPACE_FACTOR) {
+                      chunk += ' ';
                     }
                   }
-                  break;
-              } // switch
-
-              args = [];
-              parser.saveState();
-            } else if (obj !== null && obj !== undefined) {
-              assertWellFormed(args.length <= 33, 'Too many arguments');
-              args.push(obj);
-            }
-          } // while
-
-          Promise.all(chunkPromises).then(function(datas) {
-            var bidiTexts = [];
-            for (var i = 0, n = datas.length; i < n; ++i) {
-              var bidiText = datas[i];
-              if (!bidiText) {
-                continue;
-              } else if (isArray(bidiText)) {
-                Util.concatenateToArray(bidiTexts, bidiText);
-              } else {
-                bidiTexts.push(bidiText);
-              }
-            }
-            statePromise.resolve(bidiTexts);
-          });
-        } catch (e) {
-          if (!(e instanceof MissingDataException)) {
-            throw e;
-          }
-
-          self.pdfManager.requestRange(e.begin, e.end).then(parseCommands);
-        }
-      }
-      parser.saveState();
-      parseCommands();
+                }
+                chunkPromise.resolve(
+                    getBidiText(chunk, -1, font.vertical));
+              }.bind(null, args[0], chunkPromise));
+              break;
+            case 'Tj':
+              var chunkPromise = new Promise();
+              chunkPromises.push(chunkPromise);
+              fontPromise.then(function(charCodes, chunkPromise, font) {
+                var chunk = fontCharsToUnicode(charCodes, font);
+                chunkPromise.resolve(
+                    getBidiText(chunk, -1, font.vertical));
+              }.bind(null, args[0], chunkPromise));
+              break;
+            case '\'':
+              // For search, adding a extra white space for line breaks
+              // would be better here, but that causes too much spaces in
+              // the text-selection divs.
+              var chunkPromise = new Promise();
+              chunkPromises.push(chunkPromise);
+              fontPromise.then(function(charCodes, chunkPromise, font) {
+                var chunk = fontCharsToUnicode(charCodes, font);
+                chunkPromise.resolve(
+                    getBidiText(chunk, -1, font.vertical));
+              }.bind(null, args[0], chunkPromise));
+              break;
+            case '"':
+              // Note comment in "'"
+              var chunkPromise = new Promise();
+              chunkPromises.push(chunkPromise);
+              fontPromise.then(function(charCodes, chunkPromise, font) {
+                var chunk = fontCharsToUnicode(charCodes, font);
+                chunkPromise.resolve(
+                    getBidiText(chunk, -1, font.vertical));
+              }.bind(null, args[2], chunkPromise));
+              break;
+            case 'Do':
+              if (args[0].code) {
+                break;
+              }
+
+              if (!xobjs) {
+                xobjs = resources.get('XObject') || new Dict();
+              }
+
+              var name = args[0].name;
+              var xobj = xobjs.get(name);
+              if (!xobj)
+                break;
+              assertWellFormed(isStream(xobj),
+                               'XObject should be a stream');
+
+              var type = xobj.dict.get('Subtype');
+              assertWellFormed(
+                isName(type),
+                'XObject should have a Name subtype'
+              );
+
+              if ('Form' !== type.name)
+                break;
+
+              var chunkPromise = self.getTextContent(
+                xobj,
+                xobj.dict.get('Resources') || resources
+              );
+              chunkPromises.push(chunkPromise);
+              break;
+            case 'gs':
+              var dictName = args[0];
+              var extGState = resources.get('ExtGState');
+
+              if (!isDict(extGState) || !extGState.has(dictName.name))
+                break;
+
+              var gsState = extGState.get(dictName.name);
+
+              for (var i = 0; i < gsState.length; i++) {
+                if (gsState[i] === 'Font') {
+                  fontPromise = handleSetFont(
+                      args[0].name, null, resources);
+                }
+              }
+              break;
+          } // switch
+
+          args = [];
+          parser.saveState();
+        } else if (obj !== null && obj !== undefined) {
+          assertWellFormed(args.length <= 33, 'Too many arguments');
+          args.push(obj);
+        }
+      } // while
+
+      Promise.all(chunkPromises).then(function(datas) {
+        var bidiTexts = [];
+        for (var i = 0, n = datas.length; i < n; ++i) {
+          var bidiText = datas[i];
+          if (!bidiText) {
+            continue;
+          } else if (isArray(bidiText)) {
+            Util.concatenateToArray(bidiTexts, bidiText);
+          } else {
+            bidiTexts.push(bidiText);
+          }
+        }
+        statePromise.resolve(bidiTexts);
+      });
 
       return statePromise;
     },
 
     extractDataStructures: function
       partialEvaluatorExtractDataStructures(dict, baseDict,
                                             xref, properties) {
       // 9.10.2
@@ -17036,19 +17571,18 @@ var PartialEvaluator = (function Partial
                              MAX_IMAGES_IN_MASKS_BLOCK);
         if (count < MIN_IMAGES_IN_MASKS_BLOCK) {
           continue;
         }
         var images = [];
         for (var q = 0; q < count; q++) {
           var transform = argsArray[j + (q << 2) + 1];
           var maskParams = argsArray[j + (q << 2) + 2];
-          images.push({data: maskParams[0], width: maskParams[2],
-            height: maskParams[3], transform: transform,
-            inverseDecode: maskParams[1]});
+          images.push({data: maskParams.data, width: maskParams.width,
+            height: maskParams.height, transform: transform});
         }
         // replacing queue items
         fnArray.splice(j, count * 4, ['paintImageMaskXObjectGroup']);
         argsArray.splice(j, count * 4, [images]);
         i = j;
         ii = fnArray.length;
       }
     }
@@ -17097,16 +17631,18 @@ var PDF_GLYPH_SPACE_UNITS = 1000;
 var HINTING_ENABLED = false;
 
 // Accented charactars are not displayed properly on windows, using this flag
 // to control analysis of seac charstrings.
 var SEAC_ANALYSIS_ENABLED = false;
 
 var FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
 
+PDFJS.disableFontFace = false;
+
 var FontFlags = {
   FixedPitch: 1,
   Serif: 2,
   Symbolic: 4,
   Script: 8,
   Nonsymbolic: 32,
   Italic: 64,
   AllCap: 65536,
@@ -19877,16 +20413,21 @@ var Font = (function FontClosure() {
 
 
   Font.prototype = {
     name: null,
     font: null,
     mimetype: null,
     encoding: null,
 
+    get renderer() {
+      var renderer = FontRendererFactory.create(this);
+      return shadow(this, 'renderer', renderer);
+    },
+
     exportData: function Font_exportData() {
       var data = {};
       for (var i in this) {
         if (this.hasOwnProperty(i))
           data[i] = this[i];
       }
       return data;
     },
@@ -20728,16 +21269,19 @@ var Font = (function FontClosure() {
         if (prep) {
           sanitizeTTProgram(prep, ttContext);
         }
         if (fpgm) {
           addTTDummyFunctions(fpgm, ttContext, maxFunctionDefs);
         }
       }
 
+      // The following steps modify the original font data, making copy
+      font = new Stream(new Uint8Array(font.getBytes()));
+
       // Check that required tables are present
       var requiredTables = ['OS/2', 'cmap', 'head', 'hhea',
                              'hmtx', 'maxp', 'name', 'post'];
 
       var header = readOpenTypeHeader(font);
       var numTables = header.numTables;
 
       var cmap, post, maxp, hhea, hmtx, head, os2;
@@ -21423,16 +21967,21 @@ var Font = (function FontClosure() {
         this.unicodeToCID = [];
       }
     },
 
     bindDOM: function Font_bindDOM() {
       if (!this.data)
         return null;
 
+      if (PDFJS.disableFontFace) {
+        this.disableFontFace = true;
+        return null;
+      }
+
       var data = bytesToString(this.data);
       var fontName = this.loadedName;
 
       // Add the font-face rule to the document
       var url = ('url(data:' + this.mimetype + ';base64,' +
                  window.btoa(data) + ');');
       var rule = '@font-face { font-family:"' + fontName + '";src:' + url + '}';
 
@@ -22173,21 +22722,18 @@ var Type1Parser = (function Type1ParserC
                 glyph: glyph,
                 encoded: encoded
               });
             }
             break;
           case 'Subrs':
             var num = this.readInt();
             this.getToken(); // read in 'array'
-            for (var j = 0; j < num; ++j) {
-              token = this.getToken(); // read in 'dup'
+            while ((token = this.getToken()) === 'dup') {
               var index = this.readInt();
-              if (index > j)
-                j = index;
               var length = this.readInt();
               this.getToken(); // read in 'RD' or '-|'
               var data = stream.makeSubStream(stream.pos + 1, length);
               var lenIV = program.properties.privateData['lenIV'];
               var encoded = decrypt(data.getBytes(), CHAR_STRS_ENCRYPT_KEY,
                                     lenIV);
               // Skip past the required space and binary data.
               stream.skip(1 + length);
@@ -24060,16 +24606,709 @@ var CFFCompiler = (function CFFCompilerC
 // https://github.com/mozilla/pdf.js/issues/1689
 (function checkChromeWindows() {
   if (/Windows.*Chrome/.test(navigator.userAgent)) {
     SYMBOLIC_FONT_GLYPH_OFFSET = 0xF100;
   }
 })();
 
 
+var FontRendererFactory = (function FontRendererFactoryClosure() {
+  function getLong(data, offset) {
+    return (data[offset] << 24) | (data[offset + 1] << 16) |
+           (data[offset + 2] << 8) | data[offset + 3];
+  }
+
+  function getUshort(data, offset) {
+    return (data[offset] << 8) | data[offset + 1];
+  }
+
+  function parseCmap(data, start, end) {
+    var offset = getUshort(data, start + 2) === 1 ? getLong(data, start + 8) :
+                                                    getLong(data, start + 16);
+    var format = getUshort(data, start + offset);
+    if (format === 4) {
+      var length = getUshort(data, start + offset + 2);
+      var segCount = getUshort(data, start + offset + 6) >> 1;
+      var p = start + offset + 14;
+      var ranges = [];
+      for (var i = 0; i < segCount; i++, p += 2) {
+        ranges[i] = {end: getUshort(data, p)};
+      }
+      p += 2;
+      for (var i = 0; i < segCount; i++, p += 2) {
+        ranges[i].start = getUshort(data, p);
+      }
+      for (var i = 0; i < segCount; i++, p += 2) {
+        ranges[i].idDelta = getUshort(data, p);
+      }
+      for (var i = 0; i < segCount; i++, p += 2) {
+        var idOffset = getUshort(data, p);
+        if (idOffset === 0) {
+          continue;
+        }
+        ranges[i].ids = [];
+        for (var j = 0, jj = ranges[i].end - ranges[i].start + 1; j < jj; j++) {
+          ranges[i].ids[j] = getUshort(data, p + idOffset);
+          idOffset += 2;
+        }
+      }
+      return ranges;
+    } else if (format === 12) {
+      var length = getLong(data, start + offset + 4);
+      var groups = getLong(data, start + offset + 12);
+      var p = start + offset + 16;
+      var ranges = [];
+      for (var i = 0; i < groups; i++) {
+        ranges.push({
+          start: getLong(data, p),
+          end: getLong(data, p + 4),
+          idDelta: getLong(data, p + 8) - getLong(data, p)
+        });
+        p += 12;
+      }
+      return ranges;
+    }
+    error('not supported cmap: ' + format);
+  }
+
+  function parseCff(data, start, end) {
+    var properties = {};
+    var parser = new CFFParser(
+      new Stream(data, start, end - start), properties);
+    var cff = parser.parse();
+    return {
+      glyphs: cff.charStrings.objects,
+      subrs: cff.topDict.privateDict && cff.topDict.privateDict.subrsIndex &&
+             cff.topDict.privateDict.subrsIndex.objects,
+      gsubrs: cff.globalSubrIndex && cff.globalSubrIndex.objects
+    };
+  }
+
+  function parseGlyfTable(glyf, loca, isGlyphLocationsLong) {
+    var itemSize, itemDecode;
+    if (isGlyphLocationsLong) {
+      itemSize = 4;
+      itemDecode = function fontItemDecodeLong(data, offset) {
+        return (data[offset] << 24) | (data[offset + 1] << 16) |
+               (data[offset + 2] << 8) | data[offset + 3];
+      };
+    } else {
+      itemSize = 2;
+      itemDecode = function fontItemDecode(data, offset) {
+        return (data[offset] << 9) | (data[offset + 1] << 1);
+      };
+    }
+    var glyphs = [];
+    var startOffset = itemDecode(loca, 0);
+    for (var j = itemSize; j < loca.length; j += itemSize) {
+      var endOffset = itemDecode(loca, j);
+      glyphs.push(glyf.subarray(startOffset, endOffset));
+      startOffset = endOffset;
+    }
+    return glyphs;
+  }
+
+  function lookupCmap(ranges, unicode) {
+    var code = unicode.charCodeAt(0);
+    var l = 0, r = ranges.length - 1;
+    while (l < r) {
+      var c = (l + r + 1) >> 1;
+      if (code < ranges[c].start) {
+        r = c - 1;
+      } else {
+        l = c;
+      }
+    }
+    if (ranges[l].start <= code && code <= ranges[l].end) {
+      return (ranges[l].idDelta + (ranges[l].ids ?
+        ranges[l].ids[code - ranges[l].start] : code)) & 0xFFFF;
+    }
+    return 0;
+  }
+
+  function compileGlyf(code, js, font) {
+    function moveTo(x, y) {
+      js.push('c.moveTo(' + x + ',' + y + ');');
+    }
+    function lineTo(x, y) {
+      js.push('c.lineTo(' + x + ',' + y + ');');
+    }
+    function quadraticCurveTo(xa, ya, x, y) {
+      js.push('c.quadraticCurveTo(' + xa + ',' + ya + ',' +
+                                   x + ',' + y + ');');
+    }
+
+    var i = 0;
+    var numberOfContours = ((code[i] << 24) | (code[i + 1] << 16)) >> 16;
+    var xMin = ((code[i + 2] << 24) | (code[i + 3] << 16)) >> 16;
+    var yMin = ((code[i + 4] << 24) | (code[i + 5] << 16)) >> 16;
+    var xMax = ((code[i + 6] << 24) | (code[i + 7] << 16)) >> 16;
+    var yMax = ((code[i + 8] << 24) | (code[i + 9] << 16)) >> 16;
+    i += 10;
+    if (numberOfContours < 0) {
+      // composite glyph
+      var x = 0, y = 0;
+      do {
+        var flags = (code[i] << 8) | code[i + 1];
+        var glyphIndex = (code[i + 2] << 8) | code[i + 3];
+        i += 4;
+        var arg1, arg2;
+        if ((flags & 0x01)) {
+          arg1 = ((code[i] << 24) | (code[i + 1] << 16)) >> 16;
+          arg2 = ((code[i + 2] << 24) | (code[i + 3] << 16)) >> 16;
+          i += 4;
+        } else {
+          arg1 = code[i++]; arg2 = code[i++];
+        }
+        if ((flags & 0x02)) {
+           x = arg1;
+           y = arg2;
+        } else {
+           x = 0; y = 0; // TODO "they are points" ?
+        }
+        var scaleX = 1, scaleY = 1, scale01 = 0, scale10 = 0;
+        if ((flags & 0x08)) {
+          scaleX =
+          scaleY = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824;
+          i += 2;
+        } else if ((flags & 0x40)) {
+          scaleX = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824;
+          scaleY = ((code[i + 2] << 24) | (code[i + 3] << 16)) / 1073741824;
+          i += 4;
+        } else if ((flags & 0x80)) {
+          scaleX = ((code[i] << 24) | (code[i + 1] << 16)) / 1073741824;
+          scale01 = ((code[i + 2] << 24) | (code[i + 3] << 16)) / 1073741824;
+          scale10 = ((code[i + 4] << 24) | (code[i + 5] << 16)) / 1073741824;
+          scaleY = ((code[i + 6] << 24) | (code[i + 7] << 16)) / 1073741824;
+          i += 8;
+        }
+        var subglyph = font.glyphs[glyphIndex];
+        if (subglyph) {
+          js.push('c.save();');
+          js.push('c.transform(' + scaleX + ',' + scale01 + ',' +
+                  scale10 + ',' + scaleY + ',' + x + ',' + y + ');');
+          compileGlyf(subglyph, js, font);
+          js.push('c.restore();');
+        }
+      } while ((flags & 0x20));
+    } else {
+      // simple glyph
+      var endPtsOfContours = [];
+      for (var j = 0; j < numberOfContours; j++) {
+        endPtsOfContours.push((code[i] << 8) | code[i + 1]);
+        i += 2;
+      }
+      var instructionLength = (code[i] << 8) | code[i + 1];
+      i += 2 + instructionLength; // skipping the instructions
+      var numberOfPoints = endPtsOfContours[endPtsOfContours.length - 1] + 1;
+      var points = [];
+      while (points.length < numberOfPoints) {
+        var flags = code[i++], repeat = 1;
+        if ((flags & 0x08)) {
+          repeat += code[i++];
+        }
+        while (repeat-- > 0) {
+          points.push({flags: flags});
+        }
+      }
+      var x = 0, y = 0;
+      for (var j = 0; j < numberOfPoints; j++) {
+        switch (points[j].flags & 0x12) {
+          case 0x00:
+            x += ((code[i] << 24) | (code[i + 1] << 16)) >> 16;
+            i += 2;
+            break;
+          case 0x02:
+            x -= code[i++];
+            break;
+          case 0x12:
+            x += code[i++];
+            break;
+        }
+        points[j].x = x;
+      }
+      for (var j = 0; j < numberOfPoints; j++) {
+        switch (points[j].flags & 0x24) {
+          case 0x00:
+            y += ((code[i] << 24) | (code[i + 1] << 16)) >> 16;
+            i += 2;
+            break;
+          case 0x04:
+            y -= code[i++];
+            break;
+          case 0x24:
+            y += code[i++];
+            break;
+        }
+        points[j].y = y;
+      }
+
+      var startPoint = 0;
+      for (var i = 0; i < numberOfContours; i++) {
+        var endPoint = endPtsOfContours[i];
+        // contours might have implicit points, which is located in the middle
+        // between two neighboring off-curve points
+        var contour = points.slice(startPoint, endPoint + 1);
+        if ((contour[0].flags & 1)) {
+          contour.push(contour[0]); // using start point at the contour end
+        } else if ((contour[contour.length - 1].flags & 1)) {
+          // first is off-curve point, trying to use one from the end
+          contour.unshift(contour[contour.length - 1]);
+        } else {
+          // start and end are off-curve points, creating implicit one
+          var p = {
+            flags: 1,
+            x: (contour[0].x + contour[contour.length - 1].x) / 2,
+            y: (contour[0].y + contour[contour.length - 1].y) / 2
+          };
+          contour.unshift(p);
+          contour.push(p);
+        }
+        moveTo(contour[0].x, contour[0].y);
+        for (var j = 1, jj = contour.length; j < jj; j++) {
+          if ((contour[j].flags & 1)) {
+            lineTo(contour[j].x, contour[j].y);
+          } else if ((contour[j + 1].flags & 1)){
+            quadraticCurveTo(contour[j].x, contour[j].y,
+                             contour[j + 1].x, contour[j + 1].y);
+            j++;
+          } else {
+            quadraticCurveTo(contour[j].x, contour[j].y,
+              (contour[j].x + contour[j + 1].x) / 2,
+              (contour[j].y + contour[j + 1].y) / 2);
+          }
+        }
+        startPoint = endPoint + 1;
+      }
+    }
+  }
+
+  function compileCharString(code, js, font) {
+    var stack = [];
+    var x = 0, y = 0;
+    var stems = 0;
+
+    function moveTo(x, y) {
+      js.push('c.moveTo(' + x + ',' + y + ');');
+    }
+    function lineTo(x, y) {
+      js.push('c.lineTo(' + x + ',' + y + ');');
+    }
+    function bezierCurveTo(x1, y1, x2, y2, x, y) {
+      js.push('c.bezierCurveTo(' + x1 + ',' + y1 + ',' + x2 + ',' + y2 + ',' +
+                                   x + ',' + y + ');');
+    }
+
+    function parse(code) {
+      var i = 0;
+      while (i < code.length) {
+        var stackClean = false;
+        var v = code[i++];
+        switch (v) {
+          case 1: // hstem
+            stems += stack.length >> 1;
+            stackClean = true;
+            break;
+          case 3: // vstem
+            stems += stack.length >> 1;
+            stackClean = true;
+            break;
+          case 4: // vmoveto
+            y += stack.pop();
+            moveTo(x, y);
+            stackClean = true;
+            break;
+          case 5: // rlineto
+            while (stack.length > 0) {
+              x += stack.shift();
+              y += stack.shift();
+              lineTo(x, y);
+            }
+            break;
+          case 6: // hlineto
+            while (stack.length > 0) {
+              x += stack.shift();
+              lineTo(x, y);
+              if (stack.length === 0) {
+                break;
+              }
+              y += stack.shift();
+              lineTo(x, y);
+            }
+            break;
+          case 7: // vlineto
+            while (stack.length > 0) {
+              y += stack.shift();
+              lineTo(x, y);
+              if (stack.length === 0) {
+                break;
+              }
+              x += stack.shift();
+              lineTo(x, y);
+            }
+            break;
+          case 8: // rrcurveto
+            while (stack.length > 0) {
+              var xa = x + stack.shift(), ya = y + stack.shift();
+              var xb = xa + stack.shift(), yb = ya + stack.shift();
+              x = xb + stack.shift(); y = yb + stack.shift();
+              bezierCurveTo(xa, ya, xb, yb, x, y);
+            }
+            break;
+          case 10: // callsubr
+            var n = stack.pop() + font.subrsBias;
+            var subrCode = font.subrs[n];
+            if (subrCode) {
+              parse(subrCode);
+            }
+            break;
+          case 11: // return
+            return;
+          case 12:
+            v = code[i++];
+            switch (v) {
+              case 34: // flex
+                var xa = x + stack.shift();
+                var xb = xa + stack.shift(), y1 = y + stack.shift();
+                x = xb + stack.shift();
+                bezierCurveTo(xa, y, xb, y1, x, y1);
+                var xa = x + stack.shift();
+                var xb = xa + stack.shift();
+                x = xb + stack.shift();
+                bezierCurveTo(xa, y1, xb, y, x, y);
+                break;
+              case 35: // flex
+                var xa = x + stack.shift(), ya = y + stack.shift();
+                var xb = xa + stack.shift(), yb = ya + stack.shift();
+                x = xb + stack.shift(); y = yb + stack.shift();
+                bezierCurveTo(xa, ya, xb, yb, x, y);
+                var xa = x + stack.shift(), ya = y + stack.shift();
+                var xb = xa + stack.shift(), yb = ya + stack.shift();
+                x = xb + stack.shift(); y = yb + stack.shift();
+                bezierCurveTo(xa, ya, xb, yb, x, y);
+                stack.pop(); // fd
+                break;
+              case 36: // hflex1
+                var xa = x + stack.shift(), y1 = y + stack.shift();
+                var xb = xa + stack.shift(), y2 = y1 + stack.shift();
+                x = xb + stack.shift();
+                bezierCurveTo(xa, y1, xb, y2, x, y2);
+                var xa = x + stack.shift();
+                var xb = xa + stack.shift(), y3 = y2 + stack.shift();
+                x = xb + stack.shift();
+                bezierCurveTo(xa, y2, xb, y3, x, y);
+                break;
+              case 37: // flex1
+                var x0 = x, y0 = y;
+                var xa = x + stack.shift(), ya = y + stack.shift();
+                var xb = xa + stack.shift(), yb = ya + stack.shift();
+                x = xb + stack.shift(); y = yb + stack.shift();
+                bezierCurveTo(xa, ya, xb, yb, x, y);
+                var xa = x + stack.shift(), ya = y + stack.shift();
+                var xb = xa + stack.shift(), yb = ya + stack.shift();
+                x = xb; y = yb;
+                if (Math.abs(x - x0) > Math.abs(y - y0))
+                  x += stack.shift();
+                else
+                  y += stack.shift();
+                bezierCurveTo(xa, ya, xb, yb, x, y);
+                break;
+              default:
+                error('unknown operator: 12 ' + v);
+            }
+            break;
+          case 14: // endchar
+            if (stack.length >= 4) {
+              var achar = stack.pop();
+              var bchar = stack.pop();
+              y = stack.pop();
+              x = stack.pop();
+              js.push('c.save();');
+              js.push('c.translate('+ x + ',' + y + ');');
+              var gid = lookupCmap(font.cmap, String.fromCharCode(
+                font.glyphNameMap[Encodings.StandardEncoding[achar]]));
+              compileCharString(font.glyphs[gid], js, font);
+              js.push('c.restore();');
+
+              gid = lookupCmap(font.cmap, String.fromCharCode(
+                font.glyphNameMap[Encodings.StandardEncoding[bchar]]));
+              compileCharString(font.glyphs[gid], js, font);
+            }
+            return;
+          case 18: // hstemhm
+            stems += stack.length >> 1;
+            stackClean = true;
+            break;
+          case 19: // hintmask
+            stems += stack.length >> 1;
+            i += (stems + 7) >> 3;
+            stackClean = true;
+            break;
+          case 20: // cntrmask
+            stems += stack.length >> 1;
+            i += (stems + 7) >> 3;
+            stackClean = true;
+            break;
+          case 21: // rmoveto
+            y += stack.pop();
+            x += stack.pop();
+            moveTo(x, y);
+            stackClean = true;
+            break;
+          case 22: // hmoveto
+            x += stack.pop();
+            moveTo(x, y);
+            stackClean = true;
+            break;
+          case 23: // vstemhm
+            stems += stack.length >> 1;
+            stackClean = true;
+            break;
+          case 24: // rcurveline
+            while (stack.length > 2) {
+              var xa = x + stack.shift(), ya = y + stack.shift();
+              var xb = xa + stack.shift(), yb = ya + stack.shift();
+              x = xb + stack.shift(); y = yb + stack.shift();
+              bezierCurveTo(xa, ya, xb, yb, x, y);
+            }
+            x += stack.shift();
+            y += stack.shift();
+            lineTo(x, y);
+            break;
+          case 25: // rlinecurve
+            while (stack.length > 6) {
+              x += stack.shift();
+              y += stack.shift();
+              lineTo(x, y);
+            }
+            var xa = x + stack.shift(), ya = y + stack.shift();
+            var xb = xa + stack.shift(), yb = ya + stack.shift();
+            x = xb + stack.shift(); y = yb + stack.shift();
+            bezierCurveTo(xa, ya, xb, yb, x, y);
+            break;
+          case 26: // vvcurveto
+            if (stack.length % 2) {
+              x += stack.shift();
+            }
+            while (stack.length > 0) {
+              var xa = x, ya = y + stack.shift();
+              var xb = xa + stack.shift(), yb = ya + stack.shift();
+              x = xb; y = yb + stack.shift();
+              bezierCurveTo(xa, ya, xb, yb, x, y);
+            }
+            break;
+          case 27: // hhcurveto
+            if (stack.length % 2) {
+              y += stack.shift();
+            }
+            while (stack.length > 0) {
+              var xa = x + stack.shift(), ya = y;
+              var xb = xa + stack.shift(), yb = ya + stack.shift();
+              x = xb + stack.shift(); y = yb;
+              bezierCurveTo(xa, ya, xb, yb, x, y);
+            }
+            break;
+          case 28:
+            stack.push(((code[i] << 24) | (code[i + 1] << 16)) >> 16);
+            i += 2;
+            break;
+          case 29: // callgsubr
+            var n = stack.pop() + font.gsubrsBias;
+            var subrCode = font.gsubrs[n];
+            if (subrCode) {
+              parse(subrCode);
+            }
+            break;
+          case 30: // vhcurveto
+            while (stack.length > 0) {
+              var xa = x, ya = y + stack.shift();
+              var xb = xa + stack.shift(), yb = ya + stack.shift();
+              x = xb + stack.shift();
+              y = yb + (stack.length === 1 ? stack.shift() : 0);
+              bezierCurveTo(xa, ya, xb, yb, x, y);
+              if (stack.length === 0) {
+                break;
+              }
+
+              var xa = x + stack.shift(), ya = y;
+              var xb = xa + stack.shift(), yb = ya + stack.shift();
+              y = yb + stack.shift();
+              x = xb + (stack.length === 1 ? stack.shift() : 0);
+              bezierCurveTo(xa, ya, xb, yb, x, y);
+            }
+            break;
+          case 31: // hvcurveto
+            while (stack.length > 0) {
+              var xa = x + stack.shift(), ya = y;
+              var xb = xa + stack.shift(), yb = ya + stack.shift();
+              y = yb + stack.shift();
+              x = xb + (stack.length === 1 ? stack.shift() : 0);
+              bezierCurveTo(xa, ya, xb, yb, x, y);
+              if (stack.length === 0) {
+                break;
+              }
+
+              var xa = x, ya = y + stack.shift();
+              var xb = xa + stack.shift(), yb = ya + stack.shift();
+              x = xb + stack.shift();
+              y = yb + (stack.length === 1 ? stack.shift() : 0);
+              bezierCurveTo(xa, ya, xb, yb, x, y);
+            }
+            break;
+          default:
+            if (v < 32)
+              error('unknown operator: ' + v);
+            if (v < 247)
+              stack.push(v - 139);
+            else if (v < 251)
+              stack.push((v - 247) * 256 + code[i++] + 108);
+            else if (v < 255)
+              stack.push(-(v - 251) * 256 - code[i++] - 108);
+            else {
+              stack.push(((code[i] << 24) | (code[i + 1] << 16) |
+                         (code[i + 2] << 8) | code[i + 3]) / 65536);
+              i += 4;
+            }
+            break;
+        }
+        if (stackClean) {
+          stack.length = 0;
+        }
+      }
+    }
+    parse(code);
+  }
+
+  function TrueTypeCompiled(glyphs, cmap, fontMatrix) {
+    this.glyphs = glyphs;
+    this.cmap = cmap;
+    this.fontMatrix = fontMatrix || [0.000488, 0, 0, 0.000488, 0, 0];
+
+    this.compiledGlyphs = [];
+  }
+
+  var noop = function () {};
+
+  TrueTypeCompiled.prototype = {
+    getPathGenerator: function (unicode) {
+      var gid = lookupCmap(this.cmap, unicode);
+      var fn = this.compiledGlyphs[gid];
+      if (!fn) {
+        this.compiledGlyphs[gid] = fn = this.compileGlyph(this.glyphs[gid]);
+      }
+      return fn;
+    },
+    compileGlyph: function (code) {
+      if (!code || code.length === 0 || code[0] === 14) {
+        return noop;
+      }
+
+      var js = [];
+      js.push('c.save();');
+      js.push('c.transform(' + this.fontMatrix.join(',') + ');');
+      js.push('c.scale(size, -size);');
+
+      var stack = [], x = 0, y = 0;
+      compileGlyf(code, js, this);
+
+      js.push('c.restore();');
+
+      /*jshint -W054 */
+      return new Function('c', 'size', js.join('\n'));
+    }
+  };
+
+  function Type2Compiled(cffInfo, cmap, fontMatrix, glyphNameMap) {
+    this.glyphs = cffInfo.glyphs;
+    this.gsubrs = cffInfo.gsubrs || [];
+    this.subrs = cffInfo.subrs || [];
+    this.cmap = cmap;
+    this.glyphNameMap = glyphNameMap || GlyphsUnicode;
+    this.fontMatrix = fontMatrix || [0.001, 0, 0, 0.001, 0, 0];
+
+    this.compiledGlyphs = [];
+    this.gsubrsBias = this.gsubrs.length < 1240 ? 107 :
+                      this.gsubrs.length < 33900 ? 1131 : 32768;
+    this.subrsBias = this.subrs.length < 1240 ? 107 :
+                     this.subrs.length < 33900 ? 1131 : 32768;
+  }
+
+  Type2Compiled.prototype = {
+    getPathGenerator: function (unicode) {
+      var gid = lookupCmap(this.cmap, unicode);
+      var fn = this.compiledGlyphs[gid];
+      if (!fn) {
+        this.compiledGlyphs[gid] = fn = this.compileGlyph(this.glyphs[gid]);
+      }
+      return fn;
+    },
+    compileGlyph: function (code) {
+      if (!code || code.length === 0 || code[0] === 14) {
+        return noop;
+      }
+
+      var js = [];
+      js.push('c.save();');
+      js.push('c.transform(' + this.fontMatrix.join(',') + ');');
+      js.push('c.scale(size, -size);');
+
+      var stack = [], x = 0, y = 0;
+      compileCharString(code, js, this);
+
+      js.push('c.restore();');
+
+      /*jshint -W054 */
+      return new Function('c', 'size', js.join('\n'));
+    }
+  };
+
+  return {
+    create: function FontRendererFactory_create(font) {
+      var data = new Uint8Array(font.data);
+      var cmap, glyf, loca, cff, indexToLocFormat, unitsPerEm;
+      var numTables = getUshort(data, 4);
+      for (var i = 0, p = 12; i < numTables; i++, p += 16) {
+        var tag = String.fromCharCode.apply(null, data.subarray(p, p + 4));
+        var offset = getLong(data, p + 8);
+        var length = getLong(data, p + 12);
+        switch (tag) {
+          case 'cmap':
+            cmap = parseCmap(data, offset, offset + length);
+            break;
+          case 'glyf':
+            glyf = data.subarray(offset, offset + length);
+            break;
+          case 'loca':
+            loca = data.subarray(offset, offset + length);
+            break;
+          case 'head':
+            unitsPerEm = getUshort(data, offset + 18);
+            indexToLocFormat = getUshort(data, offset + 50);
+            break;
+          case 'CFF ':
+            cff = parseCff(data, offset, offset + length);
+            break;
+        }
+      }
+
+      if (glyf) {
+        var fontMatrix = !unitsPerEm ? font.fontMatrix :
+          [1 / unitsPerEm, 0, 0, 1 / unitsPerEm, 0, 0];
+        return new TrueTypeCompiled(
+          parseGlyfTable(glyf, loca, indexToLocFormat), cmap, fontMatrix);
+      } else {
+        return new Type2Compiled(cff, cmap, font.fontMatrix, font.glyphNameMap);
+      }
+    }
+  };
+})();
+
+
+
 var GlyphsUnicode = {
   A: 0x0041,
   AE: 0x00C6,
   AEacute: 0x01FC,
   AEmacron: 0x01E2,
   AEsmall: 0xF7E6,
   Aacute: 0x00C1,
   Aacutesmall: 0xF7E1,
@@ -28456,16 +29695,40 @@ var PDFImage = (function PDFImageClosure
           temp[newIndex + 1] = pixels[oldIndex + 1];
           temp[newIndex + 2] = pixels[oldIndex + 2];
         }
       }
     }
     return temp;
   };
 
+  PDFImage.createMask = function PDFImage_createMask(imgArray, width, height,
+                                                     inverseDecode) {
+    var buffer = new Uint8Array(width * height * 4);
+    var imgArrayPos = 0;
+    var i, j, mask, buf;
+    // removing making non-masked pixels transparent
+    var bufferPos = 3; // alpha component offset
+    for (i = 0; i < height; i++) {
+      mask = 0;
+      for (j = 0; j < width; j++) {
+        if (!mask) {
+          buf = imgArray[imgArrayPos++];
+          mask = 128;
+        }
+        if (!(buf & mask) !== inverseDecode) {
+          buffer[bufferPos] = 255;
+        }
+        bufferPos += 4;
+        mask >>= 1;
+      }
+    }
+    return {data: buffer, width: width, height: height};
+  };
+
   PDFImage.prototype = {
     get drawWidth() {
       if (!this.smask)
         return this.width;
       return Math.max(this.width, this.smask.width);
     },
     get drawHeight() {
       if (!this.smask)
@@ -28612,40 +29875,16 @@ var PDFImage = (function PDFImageClosure
         }
       } else {
         buf = new Uint8Array(width * height);
         for (var i = 0, ii = width * height; i < ii; ++i)
           buf[i] = 255;
       }
       return buf;
     },
-    applyStencilMask: function PDFImage_applyStencilMask(buffer,
-                                                         inverseDecode) {
-      var width = this.width, height = this.height;
-      var bitStrideLength = (width + 7) >> 3;
-      var imgArray = this.getImageBytes(bitStrideLength * height);
-      var imgArrayPos = 0;
-      var i, j, mask, buf;
-      // removing making non-masked pixels transparent
-      var bufferPos = 3; // alpha component offset
-      for (i = 0; i < height; i++) {
-        mask = 0;
-        for (j = 0; j < width; j++) {
-          if (!mask) {
-            buf = imgArray[imgArrayPos++];
-            mask = 128;
-          }
-          if (!(buf & mask) === inverseDecode) {
-            buffer[bufferPos] = 0;
-          }
-          bufferPos += 4;
-          mask >>= 1;
-        }
-      }
-    },
     fillRgbaBuffer: function PDFImage_fillRgbaBuffer(buffer, width, height) {
       var numComps = this.numComps;
       var originalWidth = this.width;
       var originalHeight = this.height;
       var bpc = this.bpc;
 
       // rows start at byte boundary;
       var rowBytes = (originalWidth * numComps * bpc + 7) >> 3;
@@ -32346,20 +33585,16 @@ var Linearization = (function Linearizat
     }
   };
 
   return Linearization;
 })();
 
 
 
-// This global variable is used to minimize the memory usage when patterns are
-// used.
-var temporaryPatternCanvas = null;
-
 var PatternType = {
   AXIAL: 2,
   RADIAL: 3
 };
 
 var Pattern = (function PatternClosure() {
   // Constructor should define this.getPattern
   function Pattern() {
@@ -32677,19 +33912,16 @@ var TilingPattern = (function TilingPatt
       height = Math.min(Math.ceil(Math.abs(height * combinedScale[1])),
                         MAX_PATTERN_SIZE);
 
       tmpCanvas.width = width;
       tmpCanvas.height = height;
 
       // set the new canvas element context as the graphics context
       var tmpCtx = tmpCanvas.getContext('2d');
-      // for simulated mozCurrentTransform canvas (normaly setting width/height
-      // will reset the matrix)
-      tmpCtx.setTransform(1, 0, 0, 1, 0, 0);
       var graphics = new CanvasGraphics(tmpCtx, commonObjs, objs);
 
       this.setFillAndStrokeStyleToContext(tmpCtx, paintType, color);
 
       this.setScale(width, height, xstep, ystep);
       this.transformToScale(graphics);
 
       // transform coordinates to pattern space
@@ -32741,21 +33973,17 @@ var TilingPattern = (function TilingPatt
           context.strokeStyle = cssColor;
           break;
         default:
           error('Unsupported paint type: ' + paintType);
       }
     },
 
     getPattern: function TilingPattern_getPattern() {
-      // The temporary canvas is created only because the memory is released
-      // more quickly than creating multiple temporary canvases.
-      if (temporaryPatternCanvas === null) {
-        temporaryPatternCanvas = createScratchCanvas(1, 1);
-      }
+      var temporaryPatternCanvas = CachedCanvases.getCanvas('pattern');
       this.createPatternCanvas(temporaryPatternCanvas);
 
       var ctx = this.ctx;
       ctx.setTransform.apply(ctx, this.curMatrix);
       ctx.transform.apply(ctx, this.matrix);
       this.scaleToContext();
 
       return ctx.createPattern(temporaryPatternCanvas, 'repeat');
@@ -35257,22 +36485,30 @@ var WorkerMessageHandler = {
     }
 
     function getPdfManager(data) {
       var pdfManagerPromise = new Promise();
 
       var source = data.source;
       var disableRange = data.disableRange;
       if (source.data) {
-        pdfManager = new LocalPdfManager(source.data, source.password);
-        pdfManagerPromise.resolve();
+        try {
+          pdfManager = new LocalPdfManager(source.data, source.password);
+          pdfManagerPromise.resolve();
+        } catch (ex) {
+          pdfManagerPromise.reject(ex);
+        }
         return pdfManagerPromise;
       } else if (source.chunkedViewerLoading) {
-        pdfManager = new NetworkPdfManager(source, handler);
-        pdfManagerPromise.resolve();
+        try {
+          pdfManager = new NetworkPdfManager(source, handler);
+          pdfManagerPromise.resolve();
+        } catch (ex) {
+          pdfManagerPromise.reject(ex);
+        }
         return pdfManagerPromise;
       }
 
       var networkManager = new NetworkManager(source.url, {
         httpHeaders: source.httpHeaders
       });
       var fullRequestXhrId = networkManager.requestFull({
         onHeadersReceived: function onHeadersReceived() {
@@ -35299,24 +36535,32 @@ var WorkerMessageHandler = {
 
           // NOTE: by cancelling the full request, and then issuing range
           // requests, there will be an issue for sites where you can only
           // request the pdf once. However, if this is the case, then the
           // server should not be returning that it can support range requests.
           networkManager.abortRequest(fullRequestXhrId);
 
           source.length = length;
-          pdfManager = new NetworkPdfManager(source, handler);
-          pdfManagerPromise.resolve(pdfManager);
+          try {
+            pdfManager = new NetworkPdfManager(source, handler);
+            pdfManagerPromise.resolve(pdfManager);
+          } catch (ex) {
+            pdfManagerPromise.reject(ex);
+          }
         },
 
         onDone: function onDone(args) {
           // the data is array, instantiating directly from it
-          pdfManager = new LocalPdfManager(args.chunk, source.password);
-          pdfManagerPromise.resolve();
+          try {
+            pdfManager = new LocalPdfManager(args.chunk, source.password);
+            pdfManagerPromise.resolve();
+          } catch (ex) {
+            pdfManagerPromise.reject(ex);
+          }
         },
 
         onError: function onError(status) {
           if (status == 404) {
             var exception = new MissingPDFException( 'Missing PDF "' +
                 source.url + '".');
             handler.send('MissingPDF', { exception: exception });
           } else {
@@ -35406,18 +36650,18 @@ var WorkerMessageHandler = {
             onFailure(ex);
             return;
           }
 
           pdfManager.requestLoadedStream();
           pdfManager.onLoadedStream().then(function() {
             loadDocument(true).then(onSuccess, onFailure);
           });
-        });
-      });
+        }, onFailure);
+      }, onFailure);
     });
 
     handler.on('GetPageRequest', function wphSetupGetPage(data) {
       var pageIndex = data.pageIndex;
       pdfManager.getPage(pageIndex).then(function(page) {
         var rotatePromise = pdfManager.ensure(page, 'rotate');
         var refPromise = pdfManager.ensure(page, 'ref');
         var viewPromise = pdfManager.ensure(page, 'view');
--- a/browser/extensions/pdfjs/content/network.js
+++ b/browser/extensions/pdfjs/content/network.js
@@ -146,42 +146,44 @@ var NetworkManager = (function NetworkMa
       if (!(xhrId in this.pendingRequests)) {
         // The XHR request might have been aborted in onHeadersReceived()
         // callback, in which case we should abort request
         return;
       }
 
       delete this.pendingRequests[xhrId];
 
-      if (xhr.status === 0) {
+      // success status == 0 can be on ftp, file and other protocols
+      if (xhr.status === 0 && /^https?:/i.test(this.url)) {
         if (pendingRequest.onError) {
           pendingRequest.onError(xhr.status);
         }
         return;
       }
+      var xhrStatus = xhr.status || OK_RESPONSE;
 
       // From http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2:
       // "A server MAY ignore the Range header". This means it's possible to
       // get a 200 rather than a 206 response from a range request.
       var ok_response_on_range_request =
-          xhr.status === OK_RESPONSE &&
+          xhrStatus === OK_RESPONSE &&
           pendingRequest.expectedStatus === PARTIAL_CONTENT_RESPONSE;
 
       if (!ok_response_on_range_request &&
-          xhr.status !== pendingRequest.expectedStatus) {
+          xhrStatus !== pendingRequest.expectedStatus) {
         if (pendingRequest.onError) {
           pendingRequest.onError(xhr.status);
         }
         return;
       }
 
       this.loadedRequests[xhrId] = true;
 
       var chunk = getArrayBuffer(xhr);
-      if (xhr.status === PARTIAL_CONTENT_RESPONSE) {
+      if (xhrStatus === PARTIAL_CONTENT_RESPONSE) {
         var rangeHeader = xhr.getResponseHeader('Content-Range');
         var matches = /bytes (\d+)-(\d+)\/(\d+)/.exec(rangeHeader);
         var begin = parseInt(matches[1], 10);
         pendingRequest.onDone({
           begin: begin,
           chunk: chunk
         });
       } else {
--- a/browser/extensions/pdfjs/content/web/viewer.css
+++ b/browser/extensions/pdfjs/content/web/viewer.css
@@ -1124,16 +1124,20 @@ html[dir='rtl'] .outlineItem > a {
 #sidebarControls {
   position:absolute;
   width: 180px;
   height: 32px;
   left: 15px;
   bottom: 35px;
 }
 
+.canvasWrapper {
+  overflow: hidden;
+}
+
 canvas {
   margin: auto;
   display: block;
 }
 
 .page {
   direction: ltr;
   width: 816px;
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -24,16 +24,19 @@ var DEFAULT_SCALE_DELTA = 1.1;
 var UNKNOWN_SCALE = 0;
 var CACHE_SIZE = 20;
 var CSS_UNITS = 96.0 / 72.0;
 var SCROLLBAR_PADDING = 40;
 var VERTICAL_PADDING = 5;
 var MIN_SCALE = 0.25;
 var MAX_SCALE = 4.0;
 var SETTINGS_MEMORY = 20;
+var HISTORY_DISABLED = false;
+var SCALE_SELECT_CONTAINER_PADDING = 8;
+var SCALE_SELECT_PADDING = 22;
 var RenderingStates = {
   INITIAL: 0,
   RUNNING: 1,
   PAUSED: 2,
   FINISHED: 3
 };
 var FindStates = {
   FIND_FOUND: 0,
@@ -715,16 +718,304 @@ var PDFFindBar = {
     if (this.opened) {
       this.close();
     } else {
       this.open();
     }
   }
 };
 
+var PDFHistory = {
+  initialized: false,
+  initialDestination: null,
+
+  initialize: function pdfHistoryInitialize(fingerprint) {
+    if (HISTORY_DISABLED || window.parent !== window) {
+      // The browsing history is only enabled when the viewer is standalone,
+      // i.e. not when it is embedded in a page.
+      return;
+    }
+    this.initialized = true;
+    this.reInitialized = false;
+    this.allowHashChange = true;
+    this.historyUnlocked = true;
+
+    this.previousHash = window.location.hash.substring(1);
+    this.currentBookmark = '';
+    this.currentPage = 0;
+    this.updatePreviousBookmark = false;
+    this.previousBookmark = '';
+    this.nextHashParam = '';
+
+    this.fingerprint = fingerprint;
+    this.currentUid = this.uid = 0;
+    this.current = {};
+
+    var state = window.history.state;
+    if (this._isStateObjectDefined(state)) {
+      // This case corresponds to navigating back to the document
+      // from another page in the browser history.
+      if (state.target.dest) {
+        this.initialDestination = state.target.dest;
+      } else {
+        PDFView.initialBookmark = state.target.hash;
+      }
+      this.currentUid = state.uid;
+      this.uid = state.uid + 1;
+      this.current = state.target;
+    } else {
+      // This case corresponds to the loading of a new document.
+      if (state && state.fingerprint &&
+          this.fingerprint !== state.fingerprint) {
+        // Reinitialize the browsing history when a new document
+        // is opened in the web viewer.
+        this.reInitialized = true;
+      }
+      window.history.replaceState({ fingerprint: this.fingerprint }, '', '');
+    }
+
+    var self = this;
+    window.addEventListener('popstate', function pdfHistoryPopstate(evt) {
+      evt.preventDefault();
+      evt.stopPropagation();
+
+      if (!self.historyUnlocked) {
+        return;
+      }
+      if (evt.state) {
+        // Move back/forward in the history.
+        self._goTo(evt.state);
+      } else {
+        // Handle the user modifying the hash of a loaded document.
+        self.previousHash = window.location.hash.substring(1);
+        if (self.uid === 0) {
+          var previousParams = (self.previousHash && self.currentBookmark &&
+                                self.previousHash !== self.currentBookmark) ?
+            { hash: self.currentBookmark } : { page: 1 };
+          self.historyUnlocked = false;
+          self.allowHashChange = false;
+          window.history.back();
+          self._pushToHistory(previousParams, false, true);
+          window.history.forward();
+          self.historyUnlocked = true;
+        }
+        self._pushToHistory({ hash: self.previousHash }, false, true);
+        if (self.currentBookmark) {
+          self.previousBookmark = self.currentBookmark;
+        }
+      }
+    }, false);
+
+    window.addEventListener('beforeunload',
+                            function pdfHistoryBeforeunload(evt) {
+      var previousParams = self._getPreviousParams(null, true);
+      if (previousParams) {
+        self._pushToHistory(previousParams, false);
+      }
+      if (PDFView.isPresentationMode) {
+        // Prevent the user from accidentally navigating away from
+        // the document when presentation mode is active.
+        evt.preventDefault();
+      }
+    }, false);
+  },
+
+  _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) {
+    return (state && state.uid >= 0 &&
+            state.fingerprint && this.fingerprint === state.fingerprint &&
+            state.target && state.target.hash) ? true : false;
+  },
+
+  get isHashChangeUnlocked() {
+    if (!this.initialized) {
+      return true;
+    }
+    // If the current hash changes when moving back/forward in the history,
+    // this will trigger a 'popstate' event *as well* as a 'hashchange' event.
+    // Since the hash generally won't correspond to the exact the position
+    // stored in the history's state object, triggering the 'hashchange' event
+    // can thus corrupt the browser history.
+    //
+    // When the hash changes during a 'popstate' event, we *only* prevent the
+    // first 'hashchange' event and immediately reset allowHashChange.
+    // If it is not reset, the user would not be able to change the hash.
+
+    var temp = this.allowHashChange;
+    this.allowHashChange = true;
+    return temp;
+  },
+
+  updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark,
+                                                                  pageNum) {
+    if (this.initialized) {
+      this.currentBookmark = bookmark.substring(1);
+      this.currentPage = pageNum | 0;
+      if (this.updatePreviousBookmark) {
+        this.previousBookmark = this.currentBookmark;
+        this.updatePreviousBookmark = false;
+      }
+    }
+  },
+
+  updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) {
+    if (this.initialized) {
+      this.nextHashParam = param;
+    }
+  },
+
+  push: function pdfHistoryPush(params, isInitialBookmark) {
+    if (!(this.initialized && this.historyUnlocked)) {
+      return;
+    }
+    if (params.dest && !params.hash) {
+      params.hash = (this.current.hash && this.current.dest &&
+                     this.current.dest === params.dest) ?
+        this.current.hash :
+        PDFView.getDestinationHash(params.dest).split('#')[1];
+    }
+    if (params.page) {
+      params.page |= 0;
+    }
+    if (isInitialBookmark && this.uid === 0) {
+      this._pushToHistory(params, false);
+      this.previousHash = window.location.hash.substring(1);
+      this.updatePreviousBookmark = this.nextHashParam ? false : true;
+      return;
+    }
+    if (this.nextHashParam && this.nextHashParam === params.hash) {
+      this.nextHashParam = null;
+      this.updatePreviousBookmark = true;
+      return;
+    }
+
+    if (params.hash) {
+      if (this.current.hash) {
+        if (this.current.hash !== params.hash) {
+          this._pushToHistory(params, true);
+        } else if (!this.current.page && params.page) {
+          this._pushToHistory(params, false, true);
+        }
+      } else {
+        this._pushToHistory(params, true);
+      }
+    } else if (this.current.page && params.page &&
+               this.current.page !== params.page) {
+      this._pushToHistory(params, true);
+    }
+  },
+
+  _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage,
+                                                            beforeUnload) {
+    if (!(this.currentBookmark && this.currentPage)) {
+      return null;
+    }
+    if ((!this.current.dest && !onlyCheckPage) || beforeUnload) {
+      if (this.previousBookmark === this.currentBookmark) {
+        return null;
+      }
+    } else if (this.current.page) {
+      if (this.current.page === this.currentPage) {
+        return null;
+      }
+    } else {
+      return null;
+    }
+    var params = { hash: this.currentBookmark, page: this.currentPage };
+    if (PDFView.isPresentationMode) {
+      params.hash = null;
+    }
+    return params;
+  },
+
+  _stateObj: function pdfHistory_stateObj(params) {
+    return { fingerprint: this.fingerprint, uid: this.uid, target: params };
+  },
+
+  _pushToHistory: function pdfHistory_pushToHistory(params,
+                                                    addPrevious, overwrite) {
+    if (!this.initialized) {
+      return;
+    }
+    if (!params.hash && params.page) {
+      params.hash = ('page=' + params.page);
+    }
+    if (addPrevious && !overwrite) {
+      var previousParams = this._getPreviousParams();
+      if (previousParams) {
+        this._pushToHistory(previousParams, false);
+      }
+    }
+    if (overwrite || this.uid === 0) {
+      window.history.replaceState(this._stateObj(params), '', '');
+    } else {
+      window.history.pushState(this._stateObj(params), '', '');
+    }
+    this.currentUid = this.uid++;
+    this.current = params;
+    this.updatePreviousBookmark = true;
+  },
+
+  _goTo: function pdfHistory_goTo(state) {
+    if (!(this.initialized && this.historyUnlocked &&
+          this._isStateObjectDefined(state))) {
+      return;
+    }
+    if (!this.reInitialized && state.uid < this.currentUid) {
+      var previousParams = this._getPreviousParams(true);
+      if (previousParams) {
+        this._pushToHistory(this.current, false);
+        this._pushToHistory(previousParams, false);
+        this.currentUid = state.uid;
+        window.history.back();
+        return;
+      }
+    }
+    this.historyUnlocked = false;
+
+    if (state.target.dest) {
+      PDFView.navigateTo(state.target.dest);
+    } else {
+      PDFView.setHash(state.target.hash);
+    }
+    this.currentUid = state.uid;
+    if (state.uid > this.uid) {
+      this.uid = state.uid;
+    }
+    this.current = state.target;
+    this.updatePreviousBookmark = true;
+
+    var currentHash = window.location.hash.substring(1);
+    if (this.previousHash !== currentHash) {
+      this.allowHashChange = false;
+    }
+    this.previousHash = currentHash;
+
+    this.historyUnlocked = true;
+  },
+
+  back: function pdfHistoryBack() {
+    this.go(-1);
+  },
+
+  forward: function pdfHistoryForward() {
+    this.go(1);
+  },
+
+  go: function pdfHistoryGo(direction) {
+    if (this.initialized && this.historyUnlocked) {
+      var state = window.history.state;
+      if (direction === -1 && state && state.uid > 0) {
+        window.history.back();
+      } else if (direction === 1 && state && state.uid < (this.uid - 1)) {
+        window.history.forward();
+      }
+    }
+  }
+};
+
 var PDFView = {
   pages: [],
   thumbnails: [],
   currentScale: UNKNOWN_SCALE,
   currentScaleValue: null,
   initialBookmark: document.location.hash.substring(1),
   startedTextExtraction: false,
   pageText: [],
@@ -910,18 +1201,19 @@ var PDFView = {
     return value;
   },
 
   get supportsFullscreen() {
     var doc = document.documentElement;
     var support = doc.requestFullscreen || doc.mozRequestFullScreen ||
                   doc.webkitRequestFullScreen;
 
-    // Disable presentation mode button if we're in an iframe
-    if (window.parent !== window) {
+    if (document.fullscreenEnabled === false ||
+        document.mozFullScreenEnabled === false ||
+        document.webkitFullscreenEnabled === false ) {
       support = false;
     }
 
     Object.defineProperty(this, 'supportsFullscreen', { value: support,
                                                         enumerable: true,
                                                         configurable: true,
                                                         writable: false });
     return support;
@@ -1164,36 +1456,38 @@ var PDFView = {
       PDFView.download();
     });
   },
 
   navigateTo: function pdfViewNavigateTo(dest) {
     var self = this;
     PDFJS.Promise.all([this.pagesPromise,
                        this.destinationsPromise]).then(function() {
+      var destString = '';
       if (typeof dest === 'string') {
+        destString = dest;
         dest = self.destinations[dest];
       }
       if (!(dest instanceof Array)) {
         return; // invalid destination
       }
       // dest array looks like that: <page-ref> </XYZ|FitXXX> <args..>
       var destRef = dest[0];
       var pageNumber = destRef instanceof Object ?
         self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] :
         (destRef + 1);
       if (pageNumber) {
         if (pageNumber > self.pages.length) {
           pageNumber = self.pages.length;
         }
-        self.page = pageNumber;
-        if (!self.isPresentationMode) { // Avoid breaking presentation mode.
-          var currentPage = self.pages[pageNumber - 1];
-          currentPage.scrollIntoView(dest);
-        }
+        var currentPage = self.pages[pageNumber - 1];
+        currentPage.scrollIntoView(dest);
+
+        // Update the browsing history.
+        PDFHistory.push({ dest: dest, hash: destString, page: pageNumber });
       }
     });
   },
 
   getDestinationHash: function pdfViewGetDestinationHash(dest) {
     if (typeof dest === 'string')
       return PDFView.getAnchorUrl('#' + escape(dest));
     if (dest instanceof Array) {
@@ -1201,18 +1495,22 @@ var PDFView = {
       var pageNumber = destRef instanceof Object ?
         this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] :
         (destRef + 1);
       if (pageNumber) {
         var pdfOpenParams = PDFView.getAnchorUrl('#page=' + pageNumber);
         var destKind = dest[1];
         if (typeof destKind === 'object' && 'name' in destKind &&
             destKind.name == 'XYZ') {
-          var scale = (dest[4] || this.currentScale);
-          pdfOpenParams += '&zoom=' + (scale * 100);
+          var scale = (dest[4] || this.currentScaleValue);
+          var scaleNumber = parseFloat(scale);
+          if (scaleNumber) {
+            scale = scaleNumber * 100;
+          }
+          pdfOpenParams += '&zoom=' + scale;
           if (dest[2] || dest[3]) {
             pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0);
           }
         }
         return pdfOpenParams;
       }
     }
     return '';
@@ -1398,18 +1696,21 @@ var PDFView = {
       var storedHash = null;
       if (store.get('exists', false)) {
         var pageNum = store.get('page', '1');
         var zoom = store.get('zoom', PDFView.currentScale);
         var left = store.get('scrollLeft', '0');
         var top = store.get('scrollTop', '0');
 
         storedHash = 'page=' + pageNum + '&zoom=' + zoom + ',' +
-                    left + ',' + top;
+                     left + ',' + top;
       }
+      // Initialize the browsing history.
+      PDFHistory.initialize(self.documentFingerprint);
+
       self.setInitialView(storedHash, scale);
 
       // Make all navigation keys work on document load,
       // unless the viewer is embedded in another page.
       if (window.parent === window) {
         PDFView.container.focus();
         PDFView.container.blur();
       }
@@ -1483,23 +1784,26 @@ var PDFView = {
     });
   },
 
   setInitialView: function pdfViewSetInitialView(storedHash, scale) {
     // Reset the current scale, as otherwise the page's scale might not get
     // updated if the zoom level stayed the same.
     this.currentScale = 0;
     this.currentScaleValue = null;
-    if (this.initialBookmark) {
+    if (PDFHistory.initialDestination) {
+      this.navigateTo(PDFHistory.initialDestination);
+      PDFHistory.initialDestination = null;
+    } else if (this.initialBookmark) {
       this.setHash(this.initialBookmark);
+      PDFHistory.push({ hash: this.initialBookmark }, !!this.initialBookmark);
       this.initialBookmark = null;
-    }
-    else if (storedHash)
+    } else if (storedHash) {
       this.setHash(storedHash);
-    else if (scale) {
+    } else if (scale) {
       this.parseScale(scale, true);
       this.page = 1;
     }
 
     if (PDFView.currentScale === UNKNOWN_SCALE) {
       // Scale was not initialized: invalid bookmark or scale was not specified.
       // Setting the default one.
       this.parseScale(DEFAULT_SCALE, true);
@@ -1591,16 +1895,17 @@ var PDFView = {
   setHash: function pdfViewSetHash(hash) {
     if (!hash)
       return;
 
     if (hash.indexOf('=') >= 0) {
       var params = PDFView.parseQueryString(hash);
       // borrowing syntax from "Parameters for Opening PDF Files"
       if ('nameddest' in params) {
+        PDFHistory.updateNextHashParam(params.nameddest);
         PDFView.navigateTo(params.nameddest);
         return;
       }
       if ('page' in params) {
         var pageNumber = (params.page | 0) || 1;
         if ('zoom' in params) {
           var zoomArgs = params.zoom.split(','); // scale,left,top
           // building destination array
@@ -1629,20 +1934,22 @@ var PDFView = {
             toggle.click();
           }
           this.switchSidebarView(params.pagemode === 'thumbs' ?
                                  'thumbs' : 'outline');
         } else if (params.pagemode === 'none' && this.sidebarOpen) {
           toggle.click();
         }
       }
-    } else if (/^\d+$/.test(hash)) // page number
+    } else if (/^\d+$/.test(hash)) { // page number
       this.page = hash;
-    else // named destination
+    } else { // named destination
+      PDFHistory.updateNextHashParam(unescape(hash));
       PDFView.navigateTo(unescape(hash));
+    }
   },
 
   switchSidebarView: function pdfViewSwitchSidebarView(view) {
     var thumbsView = document.getElementById('thumbnailView');
     var outlineView = document.getElementById('outlineView');
 
     var thumbsButton = document.getElementById('viewThumbnail');
     var outlineButton = document.getElementById('viewOutline');
@@ -1808,29 +2115,31 @@ var PDFView = {
       wrapper.requestFullscreen();
     } else if (document.documentElement.mozRequestFullScreen) {
       wrapper.mozRequestFullScreen();
     } else if (document.documentElement.webkitRequestFullScreen) {
       wrapper.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
     } else {
       return false;
     }
-
+    return true;
+  },
+
+  enterPresentationMode: function pdfViewEnterPresentationMode() {
     this.isPresentationMode = true;
     var currentPage = this.pages[this.page - 1];
     this.previousScale = this.currentScaleValue;
     this.parseScale('page-fit', true);
 
     // Wait for presentation mode to take effect
     setTimeout(function() {
       currentPage.scrollIntoView();
     }, 0);
 
     this.showPresentationControls();
-    return true;
   },
 
   exitPresentationMode: function pdfViewExitPresentationMode() {
     this.isPresentationMode = false;
     this.parseScale(this.previousScale);
     this.page = this.page;
     this.clearMouseScrollState();
     this.hidePresentationControls();
@@ -2102,16 +2411,20 @@ var PageView = function pageView(contain
     return this.viewport.convertToPdfPoint(x, y);
   };
 
   this.scrollIntoView = function pageViewScrollIntoView(dest) {
       if (!dest) {
         scrollIntoView(div);
         return;
       }
+      if (PDFView.isPresentationMode) { // Avoid breaking presentation mode.
+        PDFView.page = id;
+        return;
+      }
 
       var x = 0, y = 0;
       var width = 0, height = 0, widthScale, heightScale;
       var scale = 0;
       switch (dest[1].name) {
         case 'XYZ':
           x = dest[2];
           y = dest[3];
@@ -2199,22 +2512,31 @@ var PageView = function pageView(contain
     }
 
     if (this.renderingState !== RenderingStates.INITIAL) {
       console.error('Must be in new state before drawing');
     }
 
     this.renderingState = RenderingStates.RUNNING;
 
+    var viewport = this.viewport;
+    // Wrap the canvas so if it has a css transform for highdpi the overflow
+    // will be hidden in FF.
+    var canvasWrapper = document.createElement('div');
+    canvasWrapper.style.width = div.style.width;
+    canvasWrapper.style.height = div.style.height;
+    canvasWrapper.classList.add('canvasWrapper');
+
     var canvas = document.createElement('canvas');
     canvas.id = 'page' + this.id;
-    div.appendChild(canvas);
+    canvasWrapper.appendChild(canvas);
+    div.appendChild(canvasWrapper);
     this.canvas = canvas;
 
-    var scale = this.scale, viewport = this.viewport;
+    var scale = this.scale;
     var outputScale = PDFView.getOutputScale();
     canvas.width = Math.floor(viewport.width) * outputScale.sx;
     canvas.height = Math.floor(viewport.height) * outputScale.sy;
 
     var textLayerDiv = null;
     if (!PDFJS.disableTextLayer) {
       textLayerDiv = document.createElement('div');
       textLayerDiv.className = 'textLayer';
@@ -2258,17 +2580,17 @@ var PageView = function pageView(contain
       self.renderingState = RenderingStates.FINISHED;
 
       if (self.loadingIconDiv) {
         div.removeChild(self.loadingIconDiv);
         delete self.loadingIconDiv;
       }
 
       if (checkIfDocumentFontsUsed && PDFView.pdfDocument.embeddedFontsUsed &&
-          !PDFView.supportsDocumentFonts) {
+          PDFJS.disableFontFace) {
         console.error(mozL10n.get('web_fonts_disabled', null,
           'Web fonts are disabled: unable to use embedded PDF fonts.'));
         PDFView.fallback();
       }
       if (self.textLayer && self.textLayer.textDivs &&
           self.textLayer.textDivs.length > 0 &&
           !PDFView.supportsDocumentColors) {
         console.error(mozL10n.get('document_colors_disabled', null,
@@ -3022,16 +3344,23 @@ document.addEventListener('DOMContentLoa
   if ('disableRange' in hashParams) {
     PDFJS.disableRange = (hashParams['disableRange'] === 'true');
   }
 
   if ('disableAutoFetch' in hashParams) {
     PDFJS.disableAutoFetch = (hashParams['disableAutoFetch'] === 'true');
   }
 
+  if ('disableFontFace' in hashParams) {
+    PDFJS.disableFontFace = (hashParams['disableFontFace'] === 'true');
+  }
+
+  if (!PDFView.supportsDocumentFonts) {
+    PDFJS.disableFontFace = true;
+  }
 
   if ('textLayer' in hashParams) {
     switch (hashParams['textLayer']) {
       case 'off':
         PDFJS.disableTextLayer = true;
         break;
       case 'visible':
       case 'shadow':
@@ -3243,29 +3572,34 @@ function updateViewarea() {
     store.set('exists', true);
     store.set('page', pageNumber);
     store.set('zoom', normalizedScaleValue);
     store.set('scrollLeft', Math.round(topLeft[0]));
     store.set('scrollTop', Math.round(topLeft[1]));
   });
   var href = PDFView.getAnchorUrl(pdfOpenParams);
   document.getElementById('viewBookmark').href = href;
+
+  // Update the current bookmark in the browsing history.
+  PDFHistory.updateCurrentBookmark(pdfOpenParams, pageNumber);
 }
 
 window.addEventListener('resize', function webViewerResize(evt) {
   if (PDFView.initialized &&
       (document.getElementById('pageWidthOption').selected ||
       document.getElementById('pageFitOption').selected ||
       document.getElementById('pageAutoOption').selected))
       PDFView.parseScale(document.getElementById('scaleSelect').value);
   updateViewarea();
 });
 
 window.addEventListener('hashchange', function webViewerHashchange(evt) {
-  PDFView.setHash(document.location.hash.substring(1));
+  if (PDFHistory.isHashChangeUnlocked) {
+    PDFView.setHash(document.location.hash.substring(1));
+  }
 });
 
 window.addEventListener('change', function webViewerChange(evt) {
   var files = evt.target.files;
   if (!files || files.length === 0)
     return;
 
   // Read the local file into a Uint8Array.
@@ -3299,25 +3633,29 @@ function selectScaleOption(value) {
   }
   return predefinedValueFound;
 }
 
 window.addEventListener('localized', function localized(evt) {
   document.getElementsByTagName('html')[0].dir = mozL10n.getDirection();
 
   // Adjust the width of the zoom box to fit the content.
-  PDFView.animationStartedPromise.then(
-    function() {
-      var container = document.getElementById('scaleSelectContainer');
+  // Note: This is only done if the zoom box is actually visible,
+  // since otherwise element.clientWidth will return 0.
+  PDFView.animationStartedPromise.then(function() {
+    var container = document.getElementById('scaleSelectContainer');
+    if (container.clientWidth > 0) {
       var select = document.getElementById('scaleSelect');
       select.setAttribute('style', 'min-width: inherit;');
-      var width = select.clientWidth + 8;
-      select.setAttribute('style', 'min-width: ' + (width + 20) + 'px;');
+      var width = select.clientWidth + SCALE_SELECT_CONTAINER_PADDING;
+      select.setAttribute('style', 'min-width: ' +
+                                   (width + SCALE_SELECT_PADDING) + 'px;');
       container.setAttribute('style', 'min-width: ' + width + 'px; ' +
                                       'max-width: ' + width + 'px;');
+    }
   });
 }, true);
 
 window.addEventListener('scalechange', function scalechange(evt) {
   document.getElementById('zoomOut').disabled = (evt.scale === MIN_SCALE);
   document.getElementById('zoomIn').disabled = (evt.scale === MAX_SCALE);
 
   var customScaleOption = document.getElementById('customScaleOption');
@@ -3542,24 +3880,41 @@ window.addEventListener('keydown', funct
         break;
 
       case 82: // 'r'
         PDFView.rotatePages(90);
         break;
     }
   }
 
-  if (cmd == 4) { // shift-key
+  if (cmd === 4) { // shift-key
     switch (evt.keyCode) {
       case 82: // 'r'
         PDFView.rotatePages(-90);
         break;
     }
   }
 
+  if (cmd === 2) { // alt-key
+    switch (evt.keyCode) {
+      case 37: // left arrow
+        if (PDFView.isPresentationMode) {
+          PDFHistory.back();
+          handled = true;
+        }
+        break;
+      case 39: // right arrow
+        if (PDFView.isPresentationMode) {
+          PDFHistory.forward();
+          handled = true;
+        }
+        break;
+    }
+  }
+
   if (handled) {
     evt.preventDefault();
     PDFView.clearMouseScrollState();
   }
 });
 
 window.addEventListener('beforeprint', function beforePrint(evt) {
   PDFView.beforePrint();
@@ -3570,17 +3925,19 @@ window.addEventListener('afterprint', fu
 });
 
 (function presentationModeClosure() {
   function presentationModeChange(e) {
     var isPresentationMode = document.fullscreenElement ||
                              document.mozFullScreen ||
                              document.webkitIsFullScreen;
 
-    if (!isPresentationMode) {
+    if (isPresentationMode) {
+      PDFView.enterPresentationMode();
+    } else {
       PDFView.exitPresentationMode();
     }
   }
 
   window.addEventListener('fullscreenchange', presentationModeChange, false);
   window.addEventListener('mozfullscreenchange', presentationModeChange, false);
   window.addEventListener('webkitfullscreenchange', presentationModeChange,
                           false);