Bug 1253498 - Update pdf.js to version 1.4.121. r=bdahl
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 03 Mar 2016 22:47:13 -0500
changeset 322958 ed5cef025e088fdfb4a9ba1fda6eab8f0dbc3bab
parent 322957 6b7cac27bb8397f223af0ea20950f657328fbb4b
child 322959 996d83c26f6fdca95b877de4f1226bc660dc3418
push id5913
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 16:57:49 +0000
treeherdermozilla-beta@dcaf0a6fa115 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbdahl
bugs1253498
milestone47.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 1253498 - Update pdf.js to version 1.4.121. r=bdahl
browser/extensions/pdfjs/README.mozilla
browser/extensions/pdfjs/content/build/pdf.js
browser/extensions/pdfjs/content/build/pdf.worker.js
browser/extensions/pdfjs/content/web/viewer.js
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,3 +1,3 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.4.95
+Current extension version is: 1.4.121
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf', ['exports
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdf = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.4.95';
-var pdfjsBuild = '2b813c0';
+var pdfjsVersion = '1.4.121';
+var pdfjsBuild = '51f6aba';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -685,16 +685,65 @@ function stringToBytes(str) {
   var length = str.length;
   var bytes = new Uint8Array(length);
   for (var i = 0; i < length; ++i) {
     bytes[i] = str.charCodeAt(i) & 0xFF;
   }
   return bytes;
 }
 
+/**
+ * Gets length of the array (Array, Uint8Array, or string) in bytes.
+ * @param {Array|Uint8Array|string} arr
+ * @returns {number}
+ */
+function arrayByteLength(arr) {
+  if (arr.length !== undefined) {
+    return arr.length;
+  }
+  assert(arr.byteLength !== undefined);
+  return arr.byteLength;
+}
+
+/**
+ * Combines array items (arrays) into single Uint8Array object.
+ * @param {Array} arr - the array of the arrays (Array, Uint8Array, or string).
+ * @returns {Uint8Array}
+ */
+function arraysToBytes(arr) {
+  // Shortcut: if first and only item is Uint8Array, return it.
+  if (arr.length === 1 && (arr[0] instanceof Uint8Array)) {
+    return arr[0];
+  }
+  var resultLength = 0;
+  var i, ii = arr.length;
+  var item, itemLength ;
+  for (i = 0; i < ii; i++) {
+    item = arr[i];
+    itemLength = arrayByteLength(item);
+    resultLength += itemLength;
+  }
+  var pos = 0;
+  var data = new Uint8Array(resultLength);
+  for (i = 0; i < ii; i++) {
+    item = arr[i];
+    if (!(item instanceof Uint8Array)) {
+      if (typeof item === 'string') {
+        item = stringToBytes(item);
+      } else {
+        item = new Uint8Array(item);
+      }
+    }
+    itemLength = item.byteLength;
+    data.set(item, pos);
+    pos += itemLength;
+  }
+  return data;
+}
+
 function string32(value) {
   return String.fromCharCode((value >> 24) & 0xff, (value >> 16) & 0xff,
                              (value >> 8) & 0xff, value & 0xff);
 }
 
 function log2(x) {
   var n = 1, i = 0;
   while (x > n) {
@@ -1537,16 +1586,18 @@ exports.PasswordException = PasswordExce
 exports.PasswordResponses = PasswordResponses;
 exports.StatTimer = StatTimer;
 exports.StreamType = StreamType;
 exports.TextRenderingMode = TextRenderingMode;
 exports.UnexpectedResponseException = UnexpectedResponseException;
 exports.UnknownErrorException = UnknownErrorException;
 exports.Util = Util;
 exports.XRefParseException = XRefParseException;
+exports.arrayByteLength = arrayByteLength;
+exports.arraysToBytes = arraysToBytes;
 exports.assert = assert;
 exports.bytesToString = bytesToString;
 exports.combineUrl = combineUrl;
 exports.createPromiseCapability = createPromiseCapability;
 exports.deprecated = deprecated;
 exports.error = error;
 exports.getFilenameFromUrl = getFilenameFromUrl;
 exports.getLookupTableFactory = getLookupTableFactory;
@@ -5276,25 +5327,29 @@ var CanvasGraphics = (function CanvasGra
             ctx.scale(characterScaleX, 1);
             scaledX /= characterScaleX;
           } else if (width !== measuredWidth) {
             scaledX += (width - measuredWidth) / 2000 *
               fontSize / fontSizeScale;
           }
         }
 
-        if (simpleFillText && !accent) {
-          // common case
-          ctx.fillText(character, scaledX, scaledY);
-        } else {
-          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);
+        // Only attempt to draw the glyph if it is actually in the embedded font
+        // file or if there isn't a font file so the fallback font is shown.
+        if (glyph.isInFont || font.missingFile) {
+          if (simpleFillText && !accent) {
+            // common case
+            ctx.fillText(character, scaledX, scaledY);
+          } else {
+            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);
+            }
           }
         }
 
         var charWidth = width * widthAdvanceScale + spacing * fontDirection;
         x += charWidth;
 
         if (restoreNeeded) {
           ctx.restore();
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf.worker', ['
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdfWorker = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.4.95';
-var pdfjsBuild = '2b813c0';
+var pdfjsVersion = '1.4.121';
+var pdfjsBuild = '51f6aba';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -2644,16 +2644,65 @@ function stringToBytes(str) {
   var length = str.length;
   var bytes = new Uint8Array(length);
   for (var i = 0; i < length; ++i) {
     bytes[i] = str.charCodeAt(i) & 0xFF;
   }
   return bytes;
 }
 
+/**
+ * Gets length of the array (Array, Uint8Array, or string) in bytes.
+ * @param {Array|Uint8Array|string} arr
+ * @returns {number}
+ */
+function arrayByteLength(arr) {
+  if (arr.length !== undefined) {
+    return arr.length;
+  }
+  assert(arr.byteLength !== undefined);
+  return arr.byteLength;
+}
+
+/**
+ * Combines array items (arrays) into single Uint8Array object.
+ * @param {Array} arr - the array of the arrays (Array, Uint8Array, or string).
+ * @returns {Uint8Array}
+ */
+function arraysToBytes(arr) {
+  // Shortcut: if first and only item is Uint8Array, return it.
+  if (arr.length === 1 && (arr[0] instanceof Uint8Array)) {
+    return arr[0];
+  }
+  var resultLength = 0;
+  var i, ii = arr.length;
+  var item, itemLength ;
+  for (i = 0; i < ii; i++) {
+    item = arr[i];
+    itemLength = arrayByteLength(item);
+    resultLength += itemLength;
+  }
+  var pos = 0;
+  var data = new Uint8Array(resultLength);
+  for (i = 0; i < ii; i++) {
+    item = arr[i];
+    if (!(item instanceof Uint8Array)) {
+      if (typeof item === 'string') {
+        item = stringToBytes(item);
+      } else {
+        item = new Uint8Array(item);
+      }
+    }
+    itemLength = item.byteLength;
+    data.set(item, pos);
+    pos += itemLength;
+  }
+  return data;
+}
+
 function string32(value) {
   return String.fromCharCode((value >> 24) & 0xff, (value >> 16) & 0xff,
                              (value >> 8) & 0xff, value & 0xff);
 }
 
 function log2(x) {
   var n = 1, i = 0;
   while (x > n) {
@@ -3496,16 +3545,18 @@ exports.PasswordException = PasswordExce
 exports.PasswordResponses = PasswordResponses;
 exports.StatTimer = StatTimer;
 exports.StreamType = StreamType;
 exports.TextRenderingMode = TextRenderingMode;
 exports.UnexpectedResponseException = UnexpectedResponseException;
 exports.UnknownErrorException = UnknownErrorException;
 exports.Util = Util;
 exports.XRefParseException = XRefParseException;
+exports.arrayByteLength = arrayByteLength;
+exports.arraysToBytes = arraysToBytes;
 exports.assert = assert;
 exports.bytesToString = bytesToString;
 exports.combineUrl = combineUrl;
 exports.createPromiseCapability = createPromiseCapability;
 exports.deprecated = deprecated;
 exports.error = error;
 exports.getFilenameFromUrl = getFilenameFromUrl;
 exports.getLookupTableFactory = getLookupTableFactory;
@@ -3539,16 +3590,18 @@ exports.warn = warn;
 
 (function (root, factory) {
   {
     factory((root.pdfjsCoreChunkedStream = {}), root.pdfjsSharedUtil);
   }
 }(this, function (exports, sharedUtil) {
 
 var MissingDataException = sharedUtil.MissingDataException;
+var arrayByteLength = sharedUtil.arrayByteLength;
+var arraysToBytes = sharedUtil.arraysToBytes;
 var assert = sharedUtil.assert;
 var createPromiseCapability = sharedUtil.createPromiseCapability;
 var isInt = sharedUtil.isInt;
 var isEmptyObj = sharedUtil.isEmptyObj;
 
 var ChunkedStream = (function ChunkedStreamClosure() {
   function ChunkedStream(length, chunkSize, manager) {
     this.bytes = new Uint8Array(length);
@@ -3790,67 +3843,81 @@ var ChunkedStream = (function ChunkedStr
     isStream: true
   };
 
   return ChunkedStream;
 })();
 
 var ChunkedStreamManager = (function ChunkedStreamManagerClosure() {
 
-  function ChunkedStreamManager(length, chunkSize, url, args) {
+  function ChunkedStreamManager(pdfNetworkStream, args) {
+    var chunkSize = args.rangeChunkSize;
+    var length = args.length;
     this.stream = new ChunkedStream(length, chunkSize, this);
     this.length = length;
     this.chunkSize = chunkSize;
-    this.url = url;
+    this.pdfNetworkStream = pdfNetworkStream;
+    this.url = args.url;
     this.disableAutoFetch = args.disableAutoFetch;
-    var msgHandler = this.msgHandler = args.msgHandler;
-
-    if (args.chunkedViewerLoading) {
-      msgHandler.on('OnDataRange', this.onReceiveData.bind(this));
-      msgHandler.on('OnDataProgress', this.onProgress.bind(this));
-      this.sendRequest = function ChunkedStreamManager_sendRequest(begin, end) {
-        msgHandler.send('RequestDataRange', { begin: begin, end: end });
-      };
-    } else {
-
-      var getXhr = function getXhr() {
-        return new XMLHttpRequest();
-      };
-      this.networkManager = new NetworkManager(this.url, {
-        getXhr: getXhr,
-        httpHeaders: args.httpHeaders,
-        withCredentials: args.withCredentials
-      });
-      this.sendRequest = function ChunkedStreamManager_sendRequest(begin, end) {
-        this.networkManager.requestRange(begin, end, {
-          onDone: this.onReceiveData.bind(this),
-          onProgress: this.onProgress.bind(this)
-        });
-      };
-    }
+    this.msgHandler = args.msgHandler;
 
     this.currRequestId = 0;
 
     this.chunksNeededByRequest = Object.create(null);
     this.requestsByChunk = Object.create(null);
     this.promisesByRequest = Object.create(null);
     this.progressiveDataLength = 0;
+    this.aborted = false;
 
     this._loadedStreamCapability = createPromiseCapability();
-
-    if (args.initialData) {
-      this.onReceiveData({chunk: args.initialData});
-    }
   }
 
   ChunkedStreamManager.prototype = {
     onLoadedStream: function ChunkedStreamManager_getLoadedStream() {
       return this._loadedStreamCapability.promise;
     },
 
+    sendRequest: function ChunkedStreamManager_sendRequest(begin, end) {
+      var rangeReader = this.pdfNetworkStream.getRangeReader(begin, end);
+      if (!rangeReader.isStreamingSupported) {
+        rangeReader.onProgress = this.onProgress.bind(this);
+      }
+      var chunks = [], loaded = 0;
+      var manager = this;
+      var promise = new Promise(function (resolve, reject) {
+        var readChunk = function (chunk) {
+          try {
+            if (!chunk.done) {
+              var data = chunk.value;
+              chunks.push(data);
+              loaded += arrayByteLength(data);
+              if (rangeReader.isStreamingSupported) {
+                manager.onProgress({loaded: loaded});
+              }
+              rangeReader.read().then(readChunk, reject);
+              return;
+            }
+            var chunkData = arraysToBytes(chunks);
+            chunks = null;
+            resolve(chunkData);
+          } catch (e) {
+            reject(e);
+          }
+        };
+        rangeReader.read().then(readChunk, reject);
+      });
+      promise.then(function (data) {
+        if (this.aborted) {
+          return; // ignoring any data after abort
+        }
+        this.onReceiveData({chunk: data, begin: begin});
+      }.bind(this));
+      // TODO check errors
+    },
+
     // 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();
       this._requestChunks(missingChunks);
       return this._loadedStreamCapability.promise;
     },
 
@@ -4060,18 +4127,19 @@ var ChunkedStreamManager = (function Chu
     },
 
     getEndChunk: function ChunkedStreamManager_getEndChunk(end) {
       var chunk = Math.floor((end - 1) / this.chunkSize) + 1;
       return chunk;
     },
 
     abort: function ChunkedStreamManager_abort() {
-      if (this.networkManager) {
-        this.networkManager.abortAllRequests();
+      this.aborted = true;
+      if (this.pdfNetworkStream) {
+        this.pdfNetworkStream.cancelAllRequests('abort');
       }
       for(var requestId in this.promisesByRequest) {
         var capability = this.promisesByRequest[requestId];
         capability.reject(new Error('Request was aborted'));
       }
     }
   };
 
@@ -24933,35 +25001,38 @@ function getFontType(type, subtype) {
       return FontType.TYPE0;
     default:
       return FontType.UNKNOWN;
   }
 }
 
 var Glyph = (function GlyphClosure() {
   function Glyph(fontChar, unicode, accent, width, vmetric, operatorListId,
-                 isSpace) {
+                 isSpace, isInFont) {
     this.fontChar = fontChar;
     this.unicode = unicode;
     this.accent = accent;
     this.width = width;
     this.vmetric = vmetric;
     this.operatorListId = operatorListId;
     this.isSpace = isSpace;
+    this.isInFont = isInFont;
   }
 
   Glyph.prototype.matchesForCache = function(fontChar, unicode, accent, width,
-                                             vmetric, operatorListId, isSpace) {
+                                             vmetric, operatorListId, isSpace,
+                                             isInFont) {
     return this.fontChar === fontChar &&
            this.unicode === unicode &&
            this.accent === accent &&
            this.width === width &&
            this.vmetric === vmetric &&
            this.operatorListId === operatorListId &&
-           this.isSpace === isSpace;
+           this.isSpace === isSpace &&
+           this.isInFont === isInFont;
   };
 
   return Glyph;
 })();
 
 var ToUnicodeMap = (function ToUnicodeMapClosure() {
   function ToUnicodeMap(cmap) {
     // The elements of this._map can be integers or strings, depending on how
@@ -25210,16 +25281,17 @@ var ProblematicCharRanges = new Int32Arr
 var Font = (function FontClosure() {
   function Font(name, file, properties) {
     var charCode, glyphName, fontChar;
 
     this.name = name;
     this.loadedName = properties.loadedName;
     this.isType3Font = properties.isType3Font;
     this.sizes = [];
+    this.missingFile = false;
 
     this.glyphCache = Object.create(null);
 
     var names = name.split('+');
     names = names.length > 1 ? names[1] : names[0];
     names = names.split(/[-,_]/g)[0];
     this.isSerifFont = !!(properties.flags & FontFlags.Serif);
     this.isSymbolicFont = !!(properties.flags & FontFlags.Symbolic);
@@ -26574,16 +26646,19 @@ var Font = (function FontClosure() {
           // using only Macintosh and Windows platform/encoding names
           if ((r.platform === 1 && r.encoding === 0 && r.language === 0) ||
               (r.platform === 3 && r.encoding === 1 && r.language === 0x409)) {
             records.push(r);
           }
         }
         for (i = 0, ii = records.length; i < ii; i++) {
           var record = records[i];
+          if (record.length <= 0) {
+            continue; // Nothing to process, ignoring.
+          }
           var pos = start + stringsStart + record.offset;
           if (pos + record.length > end) {
             continue; // outside of name table, ignoring
           }
           font.pos = pos;
           var nameIndex = record.name;
           if (record.encoding) {
             // unicode
@@ -27021,17 +27096,17 @@ var Font = (function FontClosure() {
       if (properties.type === 'CIDFontType2') {
         var cidToGidMap = properties.cidToGidMap || [];
         var isCidToGidMapEmpty = cidToGidMap.length === 0;
 
         properties.cMap.forEach(function(charCode, cid) {
           assert(cid <= 0xffff, 'Max size of CID is 65,535');
           var glyphId = -1;
           if (isCidToGidMapEmpty) {
-            glyphId = charCode;
+            glyphId = cid;
           } else if (cidToGidMap[cid] !== undefined) {
             glyphId = cidToGidMap[cid];
           }
 
           if (glyphId >= 0 && glyphId < numGlyphs &&
               hasGlyph(glyphId, charCode, cid)) {
             charCodeToGlyphId[charCode] = glyphId;
           }
@@ -27549,46 +27624,48 @@ var Font = (function FontClosure() {
       width = isNum(width) ? width : this.defaultWidth;
       var vmetric = this.vmetrics && this.vmetrics[widthCode];
 
       var unicode = this.toUnicode.get(charcode) || charcode;
       if (typeof unicode === 'number') {
         unicode = String.fromCharCode(unicode);
       }
 
+      var isInFont = charcode in this.toFontChar;
       // First try the toFontChar map, if it's not there then try falling
       // back to the char code.
       fontCharCode = this.toFontChar[charcode] || charcode;
       if (this.missingFile) {
         fontCharCode = mapSpecialUnicodeValues(fontCharCode);
       }
 
       if (this.isType3Font) {
         // Font char code in this case is actually a glyph name.
         operatorListId = fontCharCode;
       }
 
       var accent = null;
       if (this.seacMap && this.seacMap[charcode]) {
+        isInFont = true;
         var seac = this.seacMap[charcode];
         fontCharCode = seac.baseFontCharCode;
         accent = {
           fontChar: String.fromCharCode(seac.accentFontCharCode),
           offset: seac.accentOffset
         };
       }
 
       var fontChar = String.fromCharCode(fontCharCode);
 
       var glyph = this.glyphCache[charcode];
       if (!glyph ||
           !glyph.matchesForCache(fontChar, unicode, accent, width, vmetric,
-                                 operatorListId, isSpace)) {
+                                 operatorListId, isSpace, isInFont)) {
         glyph = new Glyph(fontChar, unicode, accent, width, vmetric,
-                          operatorListId, isSpace);
+                          operatorListId, isSpace, isInFont);
         this.glyphCache[charcode] = glyph;
       }
       return glyph;
     },
 
     charsToGlyphs: function Font_charsToGlyphs(chars) {
       var charsCache = this.charsCache;
       var glyphs, glyph, charcode;
@@ -40357,31 +40434,28 @@ var LocalPdfManager = (function LocalPdf
       return;
     }
   });
 
   return LocalPdfManager;
 })();
 
 var NetworkPdfManager = (function NetworkPdfManagerClosure() {
-  function NetworkPdfManager(docId, args, msgHandler) {
+  function NetworkPdfManager(docId, pdfNetworkStream, args) {
     this._docId = docId;
-    this.msgHandler = msgHandler;
+    this.msgHandler = args.msgHandler;
 
     var params = {
-      msgHandler: msgHandler,
-      httpHeaders: args.httpHeaders,
-      withCredentials: args.withCredentials,
-      chunkedViewerLoading: args.chunkedViewerLoading,
+      msgHandler: args.msgHandler,
+      url: args.url,
+      length: args.length,
       disableAutoFetch: args.disableAutoFetch,
-      initialData: args.initialData
+      rangeChunkSize: args.rangeChunkSize
     };
-    this.streamManager = new ChunkedStreamManager(args.length,
-                                                  args.rangeChunkSize,
-                                                  args.url, params);
+    this.streamManager = new ChunkedStreamManager(pdfNetworkStream, params);
     this.pdfDocument = new PDFDocument(this, this.streamManager.getStream(),
                                        args.password);
   }
 
   Util.inherit(NetworkPdfManager, BasePdfManager, {
     ensure: function NetworkPdfManager_ensure(obj, prop, args) {
       var pdfManager = this;
 
@@ -40453,20 +40527,22 @@ var UNSUPPORTED_FEATURES = sharedUtil.UN
 var InvalidPDFException = sharedUtil.InvalidPDFException;
 var MessageHandler = sharedUtil.MessageHandler;
 var MissingPDFException = sharedUtil.MissingPDFException;
 var UnexpectedResponseException = sharedUtil.UnexpectedResponseException;
 var PasswordException = sharedUtil.PasswordException;
 var PasswordResponses = sharedUtil.PasswordResponses;
 var UnknownErrorException = sharedUtil.UnknownErrorException;
 var XRefParseException = sharedUtil.XRefParseException;
+var arrayByteLength = sharedUtil.arrayByteLength;
+var arraysToBytes = sharedUtil.arraysToBytes;
+var assert = sharedUtil.assert;
 var createPromiseCapability = sharedUtil.createPromiseCapability;
 var error = sharedUtil.error;
 var info = sharedUtil.info;
-var isInt = sharedUtil.isInt;
 var warn = sharedUtil.warn;
 var Ref = corePrimitives.Ref;
 var LocalPdfManager = corePdfManager.LocalPdfManager;
 var NetworkPdfManager = corePdfManager.NetworkPdfManager;
 var globalScope = sharedGlobal.globalScope;
 var PDFJS = sharedGlobal.PDFJS;
 
 var WorkerTask = (function WorkerTaskClosure() {
@@ -40494,16 +40570,231 @@ var WorkerTask = (function WorkerTaskClo
         throw new Error('Worker task was terminated');
       }
     }
   };
 
   return WorkerTask;
 })();
 
+
+/** @implements {IPDFStream} */
+var PDFWorkerStream = (function PDFWorkerStreamClosure() {
+  function PDFWorkerStream(params, msgHandler) {
+    this._queuedChunks = [];
+    var initialData = params.initialData;
+    if (initialData && initialData.length > 0) {
+      this._queuedChunks.push(initialData);
+    }
+    this._msgHandler = msgHandler;
+
+    this._isRangeSupported = !(params.disableRange);
+    this._isStreamingSupported = !(params.disableStream);
+    this._contentLength = params.length;
+
+    this._fullRequestReader = null;
+    this._rangeReaders = [];
+
+    msgHandler.on('OnDataRange', this._onReceiveData.bind(this));
+    msgHandler.on('OnDataProgress', this._onProgress.bind(this));
+  }
+  PDFWorkerStream.prototype = {
+    _onReceiveData: function PDFWorkerStream_onReceiveData(args) {
+       if (args.begin === undefined) {
+         if (this._fullRequestReader) {
+           this._fullRequestReader._enqueue(args.chunk);
+         } else {
+           this._queuedChunks.push(args.chunk);
+         }
+       } else {
+         var found = this._rangeReaders.some(function (rangeReader) {
+           if (rangeReader._begin !== args.begin) {
+             return false;
+           }
+           rangeReader._enqueue(args.chunk);
+           return true;
+         });
+         assert(found);
+       }
+    },
+
+    _onProgress: function PDFWorkerStream_onProgress(evt) {
+       if (this._rangeReaders.length > 0) {
+         // Reporting to first range reader.
+         var firstReader = this._rangeReaders[0];
+         if (firstReader.onProgress) {
+           firstReader.onProgress({loaded: evt.loaded});
+         }
+       }
+    },
+
+    _removeRangeReader: function PDFWorkerStream_removeRangeReader(reader) {
+      var i = this._rangeReaders.indexOf(reader);
+      if (i >= 0) {
+        this._rangeReaders.splice(i, 1);
+      }
+    },
+
+    getFullReader: function PDFWorkerStream_getFullReader() {
+      assert(!this._fullRequestReader);
+      var queuedChunks = this._queuedChunks;
+      this._queuedChunks = null;
+      return new PDFWorkerStreamReader(this, queuedChunks);
+    },
+
+    getRangeReader: function PDFWorkerStream_getRangeReader(begin, end) {
+      var reader = new PDFWorkerStreamRangeReader(this, begin, end);
+      this._msgHandler.send('RequestDataRange', { begin: begin, end: end });
+      this._rangeReaders.push(reader);
+      return reader;
+    },
+
+    cancelAllRequests: function PDFWorkerStream_cancelAllRequests(reason) {
+      if (this._fullRequestReader) {
+        this._fullRequestReader.cancel(reason);
+      }
+      var readers = this._rangeReaders.slice(0);
+      readers.forEach(function (rangeReader) {
+        rangeReader.cancel(reason);
+      });
+    }
+  };
+
+  /** @implements {IPDFStreamReader} */
+  function PDFWorkerStreamReader(stream, queuedChunks) {
+    this._stream = stream;
+    this._done = false;
+    this._queuedChunks = queuedChunks || [];
+    this._requests = [];
+    this._headersReady = Promise.resolve();
+    stream._fullRequestReader = this;
+
+    this.onProgress = null; // not used
+  }
+  PDFWorkerStreamReader.prototype = {
+    _enqueue: function PDFWorkerStreamReader_enqueue(chunk) {
+      if (this._done) {
+        return; // ignore new data
+      }
+      if (this._requests.length > 0) {
+        var requestCapability = this._requests.shift();
+        requestCapability.resolve({value: chunk, done: false});
+        return;
+      }
+      this._queuedChunks.push(chunk);
+    },
+
+    get headersReady() {
+      return this._headersReady;
+    },
+
+    get isRangeSupported() {
+      return this._stream._isRangeSupported;
+    },
+
+    get isStreamingSupported() {
+      return this._stream._isStreamingSupported;
+    },
+
+    get contentLength() {
+      return this._stream._contentLength;
+    },
+
+    read: function PDFWorkerStreamReader_read() {
+      if (this._queuedChunks.length > 0) {
+        var chunk = this._queuedChunks.shift();
+        return Promise.resolve({value: chunk, done: false});
+      }
+      if (this._done) {
+        return Promise.resolve({value: undefined, done: true});
+      }
+      var requestCapability = createPromiseCapability();
+      this._requests.push(requestCapability);
+      return requestCapability.promise;
+    },
+
+    cancel: function PDFWorkerStreamReader_cancel(reason) {
+      this._done = true;
+      this._requests.forEach(function (requestCapability) {
+        requestCapability.resolve({value: undefined, done: true});
+      });
+      this._requests = [];
+    }
+  };
+
+  /** @implements {IPDFStreamRangeReader} */
+  function PDFWorkerStreamRangeReader(stream, begin, end) {
+    this._stream = stream;
+    this._begin = begin;
+    this._end = end;
+    this._queuedChunk = null;
+    this._requests = [];
+    this._done = false;
+
+    this.onProgress = null;
+  }
+  PDFWorkerStreamRangeReader.prototype = {
+    _enqueue: function PDFWorkerStreamRangeReader_enqueue(chunk) {
+      if (this._done) {
+        return; // ignore new data
+      }
+      if (this._requests.length === 0) {
+        this._queuedChunk = chunk;
+      } else {
+        var requestsCapability = this._requests.shift();
+        requestsCapability.resolve({value: chunk, done: false});
+        this._requests.forEach(function (requestCapability) {
+          requestCapability.resolve({value: undefined, done: true});
+        });
+        this._requests = [];
+      }
+      this._done = true;
+      this._stream._removeRangeReader(this);
+    },
+
+    get isStreamingSupported() {
+      return false;
+    },
+
+    read: function PDFWorkerStreamRangeReader_read() {
+      if (this._queuedChunk) {
+        return Promise.resolve({value: this._queuedChunk, done: false});
+      }
+      if (this._done) {
+        return Promise.resolve({value: undefined, done: true});
+      }
+      var requestCapability = createPromiseCapability();
+      this._requests.push(requestCapability);
+      return requestCapability.promise;
+    },
+
+    cancel: function PDFWorkerStreamRangeReader_cancel(reason) {
+      this._done = true;
+      this._requests.forEach(function (requestCapability) {
+        requestCapability.resolve({value: undefined, done: true});
+      });
+      this._requests = [];
+      this._stream._removeRangeReader(this);
+    }
+  };
+
+  return PDFWorkerStream;
+})();
+
+/** @type IPDFStream */
+var PDFNetworkStream;
+
+/**
+ * Sets PDFNetworkStream class to be used as alternative PDF data transport.
+ * @param {IPDFStream} cls - the PDF data transport.
+ */
+function setPDFNetworkStreamClass(cls) {
+  PDFNetworkStream = cls;
+}
+
 var WorkerMessageHandler = PDFJS.WorkerMessageHandler = {
   setup: function wphSetup(handler, port) {
     var testMessageProcessed = false;
     handler.on('test', function wphSetupTest(data) {
       if (testMessageProcessed) {
         return; // we already processed 'test' message once
       }
       testMessageProcessed = true;
@@ -40600,162 +40891,131 @@ var WorkerMessageHandler = PDFJS.WorkerM
       return loadDocumentCapability.promise;
     }
 
     function getPdfManager(data) {
       var pdfManagerCapability = createPromiseCapability();
       var pdfManager;
 
       var source = data.source;
-      var disableRange = data.disableRange;
       if (source.data) {
         try {
           pdfManager = new LocalPdfManager(docId, source.data, source.password);
           pdfManagerCapability.resolve(pdfManager);
         } catch (ex) {
           pdfManagerCapability.reject(ex);
         }
         return pdfManagerCapability.promise;
-      } else if (source.chunkedViewerLoading) {
+      }
+
+      var pdfStream;
+      try {
+        if (source.chunkedViewerLoading) {
+          pdfStream = new PDFWorkerStream(source, handler);
+        } else {
+          assert(PDFNetworkStream, 'pdfjs/core/network module is not loaded');
+          pdfStream = new PDFNetworkStream(data);
+        }
+      } catch (ex) {
+        pdfManagerCapability.reject(ex);
+        return pdfManagerCapability.promise;
+      }
+
+      var fullRequest = pdfStream.getFullReader();
+      fullRequest.headersReady.then(function () {
+        if (!fullRequest.isStreamingSupported ||
+            !fullRequest.isRangeSupported) {
+          // If stream or range are disabled, it's our only way to report
+          // loading progress.
+          fullRequest.onProgress = function (evt) {
+            handler.send('DocProgress', {
+              loaded: evt.loaded,
+              total: evt.total
+            });
+          };
+        }
+
+        if (!fullRequest.isRangeSupported) {
+          return;
+        }
+
+        // We don't need auto-fetch when streaming is enabled.
+        var disableAutoFetch = source.disableAutoFetch ||
+                               fullRequest.isStreamingSupported;
+        pdfManager = new NetworkPdfManager(docId, pdfStream, {
+          msgHandler: handler,
+          url: source.url,
+          password: source.password,
+          length: fullRequest.contentLength,
+          disableAutoFetch: disableAutoFetch,
+          rangeChunkSize: source.rangeChunkSize
+        });
+        pdfManagerCapability.resolve(pdfManager);
+        cancelXHRs = null;
+      }).catch(function (reason) {
+        pdfManagerCapability.reject(reason);
+        cancelXHRs = null;
+      });
+
+      var cachedChunks = [], loaded = 0;
+      var flushChunks = function () {
+        var pdfFile = arraysToBytes(cachedChunks);
+        if (source.length && pdfFile.length !== source.length) {
+          warn('reported HTTP length is different from actual');
+        }
+        // the data is array, instantiating directly from it
         try {
-          pdfManager = new NetworkPdfManager(docId, source, handler);
+          pdfManager = new LocalPdfManager(docId, pdfFile, source.password);
           pdfManagerCapability.resolve(pdfManager);
         } catch (ex) {
           pdfManagerCapability.reject(ex);
         }
-        return pdfManagerCapability.promise;
-      }
-
-      var networkManager = new NetworkManager(source.url, {
-        httpHeaders: source.httpHeaders,
-        withCredentials: source.withCredentials
-      });
-      var cachedChunks = [];
-      var fullRequestXhrId = networkManager.requestFull({
-        onHeadersReceived: function onHeadersReceived() {
-          if (disableRange) {
-            return;
-          }
-
-          var fullRequestXhr = networkManager.getRequestXhr(fullRequestXhrId);
-          if (fullRequestXhr.getResponseHeader('Accept-Ranges') !== 'bytes') {
-            return;
-          }
-
-          var contentEncoding =
-            fullRequestXhr.getResponseHeader('Content-Encoding') || 'identity';
-          if (contentEncoding !== 'identity') {
-            return;
-          }
-
-          var length = fullRequestXhr.getResponseHeader('Content-Length');
-          length = parseInt(length, 10);
-          if (!isInt(length)) {
-            return;
-          }
-          source.length = length;
-          if (length <= 2 * source.rangeChunkSize) {
-            // The file size is smaller than the size of two chunks, so it does
-            // not make any sense to abort the request and retry with a range
-            // request.
-            return;
-          }
-
-          if (networkManager.isStreamingRequest(fullRequestXhrId)) {
-            // We can continue fetching when progressive loading is enabled,
-            // and we don't need the autoFetch feature.
-            source.disableAutoFetch = true;
-          } else {
-            // 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);
-          }
-
+        cachedChunks = [];
+      };
+      var readPromise = new Promise(function (resolve, reject) {
+        var readChunk = function (chunk) {
           try {
-            pdfManager = new NetworkPdfManager(docId, source, handler);
-            pdfManagerCapability.resolve(pdfManager);
-          } catch (ex) {
-            pdfManagerCapability.reject(ex);
-          }
-          cancelXHRs = null;
-        },
-
-        onProgressiveData: source.disableStream ? null :
-            function onProgressiveData(chunk) {
-          if (!pdfManager) {
-            cachedChunks.push(chunk);
-            return;
-          }
-          pdfManager.sendProgressiveData(chunk);
-        },
-
-        onDone: function onDone(args) {
-          if (pdfManager) {
-            return; // already processed
-          }
-
-          var pdfFile;
-          if (args === null) {
-            // TODO add some streaming manager, e.g. for unknown length files.
-            // The data was returned in the onProgressiveData, combining...
-            var pdfFileLength = 0, pos = 0;
-            cachedChunks.forEach(function (chunk) {
-              pdfFileLength += chunk.byteLength;
-            });
-            if (source.length && pdfFileLength !== source.length) {
-              warn('reported HTTP length is different from actual');
-            }
-            var pdfFileArray = new Uint8Array(pdfFileLength);
-            cachedChunks.forEach(function (chunk) {
-              pdfFileArray.set(new Uint8Array(chunk), pos);
-              pos += chunk.byteLength;
-            });
-            pdfFile = pdfFileArray.buffer;
-          } else {
-            pdfFile = args.chunk;
-          }
-
-          // the data is array, instantiating directly from it
-          try {
-            pdfManager = new LocalPdfManager(docId, pdfFile, source.password);
-            pdfManagerCapability.resolve(pdfManager);
-          } catch (ex) {
-            pdfManagerCapability.reject(ex);
-          }
-          cancelXHRs = null;
-        },
-
-        onError: function onError(status) {
-          var exception;
-          if (status === 404 || status === 0 && /^file:/.test(source.url)) {
-            exception = new MissingPDFException('Missing PDF "' +
-                                                source.url + '".');
-            handler.send('MissingPDF', exception);
-          } else {
-            exception = new UnexpectedResponseException(
-              'Unexpected server response (' + status +
-              ') while retrieving PDF "' + source.url + '".', status);
-            handler.send('UnexpectedResponse', exception);
-          }
-          cancelXHRs = null;
-        },
-
-        onProgress: function onProgress(evt) {
-          handler.send('DocProgress', {
-            loaded: evt.loaded,
-            total: evt.lengthComputable ? evt.total : source.length
-          });
-        }
+            ensureNotTerminated();
+            if (chunk.done) {
+              if (!pdfManager) {
+                flushChunks();
+              }
+              cancelXHRs = null;
+              return;
+            }
+
+            var data = chunk.value;
+            loaded += arrayByteLength(data);
+            if (!fullRequest.isStreamingSupported) {
+              handler.send('DocProgress', {
+                loaded: loaded,
+                total: Math.max(loaded, fullRequest.contentLength || 0)
+              });
+            }
+
+            if (pdfManager) {
+              pdfManager.sendProgressiveData(data);
+            } else {
+              cachedChunks.push(data);
+            }
+
+            fullRequest.read().then(readChunk, reject);
+          } catch (e) {
+            reject(e);
+          }
+        };
+        fullRequest.read().then(readChunk, reject);
+      });
+      readPromise.catch(function (e) {
+        pdfManagerCapability.reject(e);
+        cancelXHRs = null;
       });
 
       cancelXHRs = function () {
-        networkManager.abortRequest(fullRequestXhrId);
+        pdfStream.cancelAllRequests('abort');
       };
 
       return pdfManagerCapability.promise;
     }
 
     var setupDoc = function(data) {
       var onSuccess = function(doc) {
         ensureNotTerminated();
@@ -41048,16 +41308,17 @@ function initializeWorker() {
 }
 
 // Worker thread (and not node.js)?
 if (typeof window === 'undefined' &&
     !(typeof module !== 'undefined' && module.require)) {
   initializeWorker();
 }
 
+exports.setPDFNetworkStreamClass = setPDFNetworkStreamClass;
 exports.WorkerTask = WorkerTask;
 exports.WorkerMessageHandler = WorkerMessageHandler;
 }));
 
 
   }).call(pdfjsLibs);
 
   exports.PDFJS = pdfjsLibs.pdfjsSharedGlobal.PDFJS;
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -10,19 +10,19 @@
  * 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.
  */
 /* globals PDFJS, PDFBug, FirefoxCom, Stats, ProgressBar, DownloadManager,
            getPDFFileNameFromURL, PDFHistory, Preferences, SidebarView,
            ViewHistory, Stats, PDFThumbnailViewer, URL, noContextMenuHandler,
-           SecondaryToolbar, PasswordPrompt, PDFPresentationMode,
+           SecondaryToolbar, PasswordPrompt, PDFPresentationMode, PDFSidebar,
            PDFDocumentProperties, HandTool, Promise, PDFLinkService,
-           PDFOutlineView, PDFAttachmentView, OverlayManager,
+           PDFOutlineViewer, PDFAttachmentViewer, OverlayManager,
            PDFFindController, PDFFindBar, PDFViewer, PDFRenderingQueue,
            PresentationModeState, parseQueryString, RenderingStates,
            UNKNOWN_SCALE, DEFAULT_SCALE_VALUE,
            IGNORE_CURRENT_POSITION_ON_ZOOM: true */
 
 'use strict';
 
 var DEFAULT_URL = 'compressed.tracemonkey-pldi-09.pdf';
@@ -444,23 +444,16 @@ var DEFAULT_PREFERENCES = {
   disableAutoFetch: false,
   disableFontFace: false,
   disableTextLayer: false,
   useOnlyCssZoom: false,
   externalLinkTarget: 0,
 };
 
 
-var SidebarView = {
-  NONE: 0,
-  THUMBS: 1,
-  OUTLINE: 2,
-  ATTACHMENTS: 3
-};
-
 /**
  * Preferences - Utility for storing persistent settings.
  *   Used for settings that should be applied to all opened documents,
  *   or every time the viewer is loaded.
  */
 var Preferences = {
   prefs: Object.create(DEFAULT_PREFERENCES),
   isInitializedPromiseResolved: false,
@@ -868,16 +861,20 @@ var PDFFindBar = (function PDFFindBarClo
     });
 
     this.caseSensitive.addEventListener('click', function() {
       self.dispatchEvent('casesensitivitychange');
     });
   }
 
   PDFFindBar.prototype = {
+    reset: function PDFFindBar_reset() {
+      this.updateUIState();
+    },
+
     dispatchEvent: function PDFFindBar_dispatchEvent(type, findPrev) {
       var event = document.createEvent('CustomEvent');
       event.initCustomEvent('find' + type, true, true, {
         query: this.findField.value,
         caseSensitive: this.caseSensitive.checked,
         highlightAll: this.highlightAll.checked,
         findPrevious: findPrev
       });
@@ -981,95 +978,94 @@ var FindStates = {
   FIND_NOTFOUND: 1,
   FIND_WRAPPED: 2,
   FIND_PENDING: 3
 };
 
 var FIND_SCROLL_OFFSET_TOP = -50;
 var FIND_SCROLL_OFFSET_LEFT = -400;
 
+var CHARACTERS_TO_NORMALIZE = {
+  '\u2018': '\'', // Left single quotation mark
+  '\u2019': '\'', // Right single quotation mark
+  '\u201A': '\'', // Single low-9 quotation mark
+  '\u201B': '\'', // Single high-reversed-9 quotation mark
+  '\u201C': '"', // Left double quotation mark
+  '\u201D': '"', // Right double quotation mark
+  '\u201E': '"', // Double low-9 quotation mark
+  '\u201F': '"', // Double high-reversed-9 quotation mark
+  '\u00BC': '1/4', // Vulgar fraction one quarter
+  '\u00BD': '1/2', // Vulgar fraction one half
+  '\u00BE': '3/4', // Vulgar fraction three quarters
+};
+
 /**
  * Provides "search" or "find" functionality for the PDF.
  * This object actually performs the search for a given string.
  */
 var PDFFindController = (function PDFFindControllerClosure() {
   function PDFFindController(options) {
-    this.startedTextExtraction = false;
-    this.extractTextPromises = [];
-    this.pendingFindMatches = Object.create(null);
-    this.active = false; // If active, find results will be highlighted.
-    this.pageContents = []; // Stores the text for each page.
-    this.pageMatches = [];
-    this.matchCount = 0;
-    this.selected = { // Currently selected match.
-      pageIdx: -1,
-      matchIdx: -1
-    };
-    this.offset = { // Where the find algorithm currently is in the document.
-      pageIdx: null,
-      matchIdx: null
-    };
-    this.pagesToSearch = null;
-    this.resumePageIdx = null;
-    this.state = null;
-    this.dirtyMatch = false;
-    this.findTimeout = null;
     this.pdfViewer = options.pdfViewer || null;
     this.integratedFind = options.integratedFind || false;
-    this.charactersToNormalize = {
-      '\u2018': '\'', // Left single quotation mark
-      '\u2019': '\'', // Right single quotation mark
-      '\u201A': '\'', // Single low-9 quotation mark
-      '\u201B': '\'', // Single high-reversed-9 quotation mark
-      '\u201C': '"', // Left double quotation mark
-      '\u201D': '"', // Right double quotation mark
-      '\u201E': '"', // Double low-9 quotation mark
-      '\u201F': '"', // Double high-reversed-9 quotation mark
-      '\u00BC': '1/4', // Vulgar fraction one quarter
-      '\u00BD': '1/2', // Vulgar fraction one half
-      '\u00BE': '3/4', // Vulgar fraction three quarters
-    };
     this.findBar = options.findBar || null;
 
-    // Compile the regular expression for text normalization once
-    var replace = Object.keys(this.charactersToNormalize).join('');
+    this.reset();
+
+    // Compile the regular expression for text normalization once.
+    var replace = Object.keys(CHARACTERS_TO_NORMALIZE).join('');
     this.normalizationRegex = new RegExp('[' + replace + ']', 'g');
 
     var events = [
       'find',
       'findagain',
       'findhighlightallchange',
       'findcasesensitivitychange'
     ];
-
-    this.firstPagePromise = new Promise(function (resolve) {
-      this.resolveFirstPage = resolve;
-    }.bind(this));
     this.handleEvent = this.handleEvent.bind(this);
 
     for (var i = 0, len = events.length; i < len; i++) {
       window.addEventListener(events[i], this.handleEvent);
     }
   }
 
   PDFFindController.prototype = {
     setFindBar: function PDFFindController_setFindBar(findBar) {
       this.findBar = findBar;
     },
 
     reset: function PDFFindController_reset() {
       this.startedTextExtraction = false;
       this.extractTextPromises = [];
-      this.active = false;
+      this.pendingFindMatches = Object.create(null);
+      this.active = false; // If active, find results will be highlighted.
+      this.pageContents = []; // Stores the text for each page.
+      this.pageMatches = [];
+      this.matchCount = 0;
+      this.selected = { // Currently selected match.
+        pageIdx: -1,
+        matchIdx: -1
+      };
+      this.offset = { // Where the find algorithm currently is in the document.
+        pageIdx: null,
+        matchIdx: null
+      };
+      this.pagesToSearch = null;
+      this.resumePageIdx = null;
+      this.state = null;
+      this.dirtyMatch = false;
+      this.findTimeout = null;
+
+      this.firstPagePromise = new Promise(function (resolve) {
+        this.resolveFirstPage = resolve;
+      }.bind(this));
     },
 
     normalize: function PDFFindController_normalize(text) {
-      var self = this;
       return text.replace(this.normalizationRegex, function (ch) {
-        return self.charactersToNormalize[ch];
+        return CHARACTERS_TO_NORMALIZE[ch];
       });
     },
 
     calcFindMatch: function PDFFindController_calcFindMatch(pageIndex) {
       var pageContent = this.normalize(this.pageContents[pageIndex]);
       var query = this.normalize(this.state.query);
       var caseSensitive = this.state.caseSensitive;
       var queryLen = query.length;
@@ -2205,35 +2201,32 @@ var DELAY_BEFORE_HIDING_CONTROLS = 3000;
 var ACTIVE_SELECTOR = 'pdfPresentationMode';
 var CONTROLS_SELECTOR = 'pdfPresentationModeControls';
 
 /**
  * @typedef {Object} PDFPresentationModeOptions
  * @property {HTMLDivElement} container - The container for the viewer element.
  * @property {HTMLDivElement} viewer - (optional) The viewer element.
  * @property {PDFViewer} pdfViewer - The document viewer.
- * @property {PDFThumbnailViewer} pdfThumbnailViewer - (optional) The thumbnail
- *   viewer.
  * @property {Array} contextMenuItems - (optional) The menuitems that are added
  *   to the context menu in Presentation Mode.
  */
 
 /**
  * @class
  */
 var PDFPresentationMode = (function PDFPresentationModeClosure() {
   /**
    * @constructs PDFPresentationMode
    * @param {PDFPresentationModeOptions} options
    */
   function PDFPresentationMode(options) {
     this.container = options.container;
     this.viewer = options.viewer || options.container.firstElementChild;
     this.pdfViewer = options.pdfViewer;
-    this.pdfThumbnailViewer = options.pdfThumbnailViewer || null;
     var contextMenuItems = options.contextMenuItems || null;
 
     this.active = false;
     this.args = null;
     this.contextMenuOpen = false;
     this.mouseScrollTimeStamp = 0;
     this.mouseScrollDelta = 0;
 
@@ -2425,20 +2418,16 @@ var PDFPresentationMode = (function PDFP
         this.args = null;
       }.bind(this), 0);
 
       this._removeWindowListeners();
       this._hideControls();
       this._resetMouseScrollState();
       this.container.removeAttribute('contextmenu');
       this.contextMenuOpen = false;
-
-      if (this.pdfThumbnailViewer) {
-        this.pdfThumbnailViewer.ensureThumbnailVisible(page);
-      }
     },
 
     /**
      * @private
      */
     _mouseDown: function PDFPresentationMode_mouseDown(evt) {
       if (this.contextMenuOpen) {
         this.contextMenuOpen = false;
@@ -5670,23 +5659,16 @@ var PDFThumbnailViewer = (function PDFTh
           thumbView.setPdfPage(pdfPage);
           this._pagesRequests[pageNumber] = null;
           return pdfPage;
         }.bind(this));
       this._pagesRequests[pageNumber] = promise;
       return promise;
     },
 
-    ensureThumbnailVisible:
-        function PDFThumbnailViewer_ensureThumbnailVisible(page) {
-      // Ensure that the thumbnail of the current page is visible
-      // when switching from another view.
-      scrollIntoView(document.getElementById('thumbnailContainer' + page));
-    },
-
     forceRendering: function () {
       var visibleThumbs = this._getVisibleThumbs();
       var thumbView = this.renderingQueue.getHighestPriority(visibleThumbs,
                                                              this.thumbnails,
                                                              this.scroll.down);
       if (thumbView) {
         this._ensurePdfPageLoaded(thumbView).then(function () {
           this.renderingQueue.renderView(thumbView);
@@ -5696,81 +5678,422 @@ var PDFThumbnailViewer = (function PDFTh
       return false;
     }
   };
 
   return PDFThumbnailViewer;
 })();
 
 
+var SidebarView = {
+  NONE: 0,
+  THUMBS: 1,
+  OUTLINE: 2,
+  ATTACHMENTS: 3
+};
+
 /**
- * @typedef {Object} PDFOutlineViewOptions
- * @property {HTMLDivElement} container - The viewer element.
- * @property {Array} outline - An array of outline objects.
- * @property {IPDFLinkService} linkService - The navigation/linking service.
+ * @typedef {Object} PDFSidebarOptions
+ * @property {PDFViewer} - The document viewer.
+ * @property {PDFThumbnailViewer} - The thumbnail viewer.
+ * @property {PDFOutlineViewer} - The outline viewer.
+ * @property {HTMLDivElement} mainContainer - The main container
+ *   (in which the viewer element is placed).
+ * @property {HTMLDivElement} outerContainer - The outer container
+ *   (encasing both the viewer and sidebar elements).
+ * @property {HTMLButtonElement} toggleButton - The button used for
+ *   opening/closing the sidebar.
+ * @property {HTMLButtonElement} thumbnailButton - The button used to show
+ *   the thumbnail view.
+ * @property {HTMLButtonElement} outlineButton - The button used to show
+ *   the outline view.
+ * @property {HTMLButtonElement} attachmentsButton - The button used to show
+ *   the attachments view.
+ * @property {HTMLDivElement} thumbnailView - The container in which
+ *   the thumbnails are placed.
+ * @property {HTMLDivElement} outlineView - The container in which
+ *   the outline is placed.
+ * @property {HTMLDivElement} attachmentsView - The container in which
+ *   the attachments are placed.
  */
 
 /**
  * @class
  */
-var PDFOutlineView = (function PDFOutlineViewClosure() {
+var PDFSidebar = (function PDFSidebarClosure() {
   /**
-   * @constructs PDFOutlineView
-   * @param {PDFOutlineViewOptions} options
+   * @constructs PDFSidebar
+   * @param {PDFSidebarOptions} options
    */
-  function PDFOutlineView(options) {
-    this.container = options.container;
-    this.outline = options.outline;
-    this.linkService = options.linkService;
-    this.lastToggleIsShow = true;
+  function PDFSidebar(options) {
+    this.isOpen = false;
+    this.active = SidebarView.THUMBS;
+    this.isInitialViewSet = false;
+
+    /**
+     * Callback used when the sidebar has been opened/closed, to ensure that
+     * the viewers (PDFViewer/PDFThumbnailViewer) are updated correctly.
+     */
+    this.onToggled = null;
+
+    this.pdfViewer = options.pdfViewer;
+    this.pdfThumbnailViewer = options.pdfThumbnailViewer;
+    this.pdfOutlineViewer = options.pdfOutlineViewer;
+
+    this.mainContainer = options.mainContainer;
+    this.outerContainer = options.outerContainer;
+    this.toggleButton = options.toggleButton;
+
+    this.thumbnailButton = options.thumbnailButton;
+    this.outlineButton = options.outlineButton;
+    this.attachmentsButton = options.attachmentsButton;
+
+    this.thumbnailView = options.thumbnailView;
+    this.outlineView = options.outlineView;
+    this.attachmentsView = options.attachmentsView;
+
+    this._addEventListeners();
   }
 
-  PDFOutlineView.prototype = {
-    reset: function PDFOutlineView_reset() {
-      var container = this.container;
-      while (container.firstChild) {
-        container.removeChild(container.firstChild);
-      }
-      this.lastToggleIsShow = true;
+  PDFSidebar.prototype = {
+    reset: function PDFSidebar_reset() {
+      this.isInitialViewSet = false;
+
+      this.close();
+      this.switchView(SidebarView.THUMBS);
+
+      this.outlineButton.disabled = false;
+      this.attachmentsButton.disabled = false;
+    },
+
+    /**
+     * @returns {number} One of the values in {SidebarView}.
+     */
+    get visibleView() {
+      return (this.isOpen ? this.active : SidebarView.NONE);
+    },
+
+    get isThumbnailViewVisible() {
+      return (this.isOpen && this.active === SidebarView.THUMBS);
+    },
+
+    get isOutlineViewVisible() {
+      return (this.isOpen && this.active === SidebarView.OUTLINE);
+    },
+
+    get isAttachmentsViewVisible() {
+      return (this.isOpen && this.active === SidebarView.ATTACHMENTS);
+    },
+
+    /**
+     * @param {number} view - The sidebar view that should become visible,
+     *                        must be one of the values in {SidebarView}.
+     */
+    setInitialView: function PDFSidebar_setInitialView(view) {
+      if (this.isInitialViewSet) {
+        return;
+      }
+      this.isInitialViewSet = true;
+
+      if (this.isOpen && view === SidebarView.NONE) {
+        // If the user has already manually opened the sidebar,
+        // immediately closing it would be bad UX.
+        return;
+      }
+      this.switchView(view, true);
+    },
+
+    /**
+     * @param {number} view - The sidebar view that should be switched to,
+     *                        must be one of the values in {SidebarView}.
+     * @param {boolean} forceOpen - Ensure that the sidebar is opened.
+     *                              The default value is false.
+     */
+    switchView: function PDFSidebar_switchView(view, forceOpen) {
+      if (view === SidebarView.NONE) {
+        this.close();
+        return;
+      }
+      if (forceOpen) {
+        this.open();
+      }
+      var shouldForceRendering = false;
+
+      switch (view) {
+        case SidebarView.THUMBS:
+          this.thumbnailButton.classList.add('toggled');
+          this.outlineButton.classList.remove('toggled');
+          this.attachmentsButton.classList.remove('toggled');
+
+          this.thumbnailView.classList.remove('hidden');
+          this.outlineView.classList.add('hidden');
+          this.attachmentsView.classList.add('hidden');
+
+          if (this.isOpen && view !== this.active) {
+            this._updateThumbnailViewer();
+            shouldForceRendering = true;
+          }
+          break;
+        case SidebarView.OUTLINE:
+          if (this.outlineButton.disabled) {
+            return;
+          }
+          this.thumbnailButton.classList.remove('toggled');
+          this.outlineButton.classList.add('toggled');
+          this.attachmentsButton.classList.remove('toggled');
+
+          this.thumbnailView.classList.add('hidden');
+          this.outlineView.classList.remove('hidden');
+          this.attachmentsView.classList.add('hidden');
+          break;
+        case SidebarView.ATTACHMENTS:
+          if (this.attachmentsButton.disabled) {
+            return;
+          }
+          this.thumbnailButton.classList.remove('toggled');
+          this.outlineButton.classList.remove('toggled');
+          this.attachmentsButton.classList.add('toggled');
+
+          this.thumbnailView.classList.add('hidden');
+          this.outlineView.classList.add('hidden');
+          this.attachmentsView.classList.remove('hidden');
+          break;
+        default:
+          console.error('PDFSidebar_switchView: "' + view +
+                        '" is an unsupported value.');
+          return;
+      }
+      // Update the active view *after* it has been validated above,
+      // in order to prevent setting it to an invalid state.
+      this.active = view | 0;
+
+      if (shouldForceRendering) {
+        this._forceRendering();
+      }
+    },
+
+    open: function PDFSidebar_open() {
+      if (this.isOpen) {
+        return;
+      }
+      this.isOpen = true;
+      this.toggleButton.classList.add('toggled');
+
+      this.outerContainer.classList.add('sidebarMoving');
+      this.outerContainer.classList.add('sidebarOpen');
+
+      if (this.active === SidebarView.THUMBS) {
+        this._updateThumbnailViewer();
+      }
+      this._forceRendering();
+    },
+
+    close: function PDFSidebar_close() {
+      if (!this.isOpen) {
+        return;
+      }
+      this.isOpen = false;
+      this.toggleButton.classList.remove('toggled');
+
+      this.outerContainer.classList.add('sidebarMoving');
+      this.outerContainer.classList.remove('sidebarOpen');
+
+      this._forceRendering();
+    },
+
+    toggle: function PDFSidebar_toggle() {
+      if (this.isOpen) {
+        this.close();
+      } else {
+        this.open();
+      }
     },
 
     /**
      * @private
      */
-    _dispatchEvent: function PDFOutlineView_dispatchEvent(outlineCount) {
+    _forceRendering: function PDFSidebar_forceRendering() {
+      if (this.onToggled) {
+        this.onToggled();
+      } else { // Fallback
+        this.pdfViewer.forceRendering();
+        this.pdfThumbnailViewer.forceRendering();
+      }
+    },
+
+    /**
+     * @private
+     */
+    _updateThumbnailViewer: function PDFSidebar_updateThumbnailViewer() {
+      var pdfViewer = this.pdfViewer;
+      var thumbnailViewer = this.pdfThumbnailViewer;
+
+      // Use the rendered pages to set the corresponding thumbnail images.
+      var pagesCount = pdfViewer.pagesCount;
+      for (var pageIndex = 0; pageIndex < pagesCount; pageIndex++) {
+        var pageView = pdfViewer.getPageView(pageIndex);
+        if (pageView && pageView.renderingState === RenderingStates.FINISHED) {
+          var thumbnailView = thumbnailViewer.getThumbnail(pageIndex);
+          thumbnailView.setImage(pageView);
+        }
+      }
+      thumbnailViewer.scrollThumbnailIntoView(pdfViewer.currentPageNumber);
+    },
+
+    /**
+     * @private
+     */
+    _addEventListeners: function PDFSidebar_addEventListeners() {
+      var self = this;
+
+      self.mainContainer.addEventListener('transitionend', function(evt) {
+        if (evt.target === /* mainContainer */ this) {
+          self.outerContainer.classList.remove('sidebarMoving');
+        }
+      });
+
+      // Buttons for switching views.
+      self.thumbnailButton.addEventListener('click', function() {
+        self.switchView(SidebarView.THUMBS);
+      });
+
+      self.outlineButton.addEventListener('click', function() {
+        self.switchView(SidebarView.OUTLINE);
+      });
+      self.outlineButton.addEventListener('dblclick', function() {
+        self.pdfOutlineViewer.toggleOutlineTree();
+      });
+
+      self.attachmentsButton.addEventListener('click', function() {
+        self.switchView(SidebarView.ATTACHMENTS);
+      });
+
+      // Disable/enable views.
+      self.outlineView.addEventListener('outlineloaded', function(evt) {
+        var outlineCount = evt.detail.outlineCount;
+
+        self.outlineButton.disabled = !outlineCount;
+        if (!outlineCount && self.active === SidebarView.OUTLINE) {
+          self.switchView(SidebarView.THUMBS);
+        }
+      });
+
+      self.attachmentsView.addEventListener('attachmentsloaded', function(evt) {
+        var attachmentsCount = evt.detail.attachmentsCount;
+
+        self.attachmentsButton.disabled = !attachmentsCount;
+        if (!attachmentsCount && self.active === SidebarView.ATTACHMENTS) {
+          self.switchView(SidebarView.THUMBS);
+        }
+      });
+
+      // Update the thumbnailViewer, if visible, when exiting presentation mode.
+      window.addEventListener('presentationmodechanged', function(evt) {
+        if (!evt.detail.active && !evt.detail.switchInProgress &&
+            self.isThumbnailViewVisible) {
+          self._updateThumbnailViewer();
+        }
+      });
+    },
+  };
+
+  return PDFSidebar;
+})();
+
+
+var DEFAULT_TITLE = '\u2013';
+
+/**
+ * @typedef {Object} PDFOutlineViewerOptions
+ * @property {HTMLDivElement} container - The viewer element.
+ * @property {IPDFLinkService} linkService - The navigation/linking service.
+ */
+
+/**
+ * @typedef {Object} PDFOutlineViewerRenderParameters
+ * @property {Array|null} outline - An array of outline objects.
+ */
+
+/**
+ * @class
+ */
+var PDFOutlineViewer = (function PDFOutlineViewerClosure() {
+  /**
+   * @constructs PDFOutlineViewer
+   * @param {PDFOutlineViewerOptions} options
+   */
+  function PDFOutlineViewer(options) {
+    this.outline = null;
+    this.lastToggleIsShow = true;
+    this.container = options.container;
+    this.linkService = options.linkService;
+  }
+
+  PDFOutlineViewer.prototype = {
+    reset: function PDFOutlineViewer_reset() {
+      this.outline = null;
+      this.lastToggleIsShow = true;
+
+      var container = this.container;
+      while (container.firstChild) {
+        container.removeChild(container.firstChild);
+      }
+    },
+
+    /**
+     * @private
+     */
+    _dispatchEvent: function PDFOutlineViewer_dispatchEvent(outlineCount) {
       var event = document.createEvent('CustomEvent');
       event.initCustomEvent('outlineloaded', true, true, {
         outlineCount: outlineCount
       });
       this.container.dispatchEvent(event);
     },
 
     /**
      * @private
      */
-    _bindLink: function PDFOutlineView_bindLink(element, item) {
+    _bindLink: function PDFOutlineViewer_bindLink(element, item) {
       if (item.url) {
         PDFJS.addLinkAttributes(element, { url: item.url });
         return;
       }
       var linkService = this.linkService;
       element.href = linkService.getDestinationHash(item.dest);
       element.onclick = function goToDestination(e) {
         linkService.navigateTo(item.dest);
         return false;
       };
     },
 
     /**
+     * @private
+     */
+    _setStyles: function PDFOutlineViewer_setStyles(element, item) {
+      var styleStr = '';
+      if (item.bold) {
+        styleStr += 'font-weight: bold;';
+      }
+      if (item.italic) {
+        styleStr += 'font-style: italic;';
+      }
+
+      if (styleStr) {
+        element.setAttribute('style', styleStr);
+      }
+    },
+
+    /**
      * Prepend a button before an outline item which allows the user to toggle
      * the visibility of all outline items at that level.
      *
      * @private
      */
-    _addToggleButton: function PDFOutlineView_addToggleButton(div) {
+    _addToggleButton: function PDFOutlineViewer_addToggleButton(div) {
       var toggler = document.createElement('div');
       toggler.className = 'outlineItemToggler';
       toggler.onclick = function(event) {
         event.stopPropagation();
         toggler.classList.toggle('outlineItemsHidden');
 
         if (event.shiftKey) {
           var shouldShowAll = !toggler.classList.contains('outlineItemsHidden');
@@ -5784,54 +6107,69 @@ var PDFOutlineView = (function PDFOutlin
      * Toggle the visibility of the subtree of an outline item.
      *
      * @param {Element} root - the root of the outline (sub)tree.
      * @param {boolean} state - whether to show the outline (sub)tree. If false,
      *   the outline subtree rooted at |root| will be collapsed.
      *
      * @private
      */
-    _toggleOutlineItem: function PDFOutlineView_toggleOutlineItem(root, show) {
+    _toggleOutlineItem:
+        function PDFOutlineViewer_toggleOutlineItem(root, show) {
       this.lastToggleIsShow = show;
       var togglers = root.querySelectorAll('.outlineItemToggler');
       for (var i = 0, ii = togglers.length; i < ii; ++i) {
         togglers[i].classList[show ? 'remove' : 'add']('outlineItemsHidden');
       }
     },
 
     /**
      * Collapse or expand all subtrees of the outline.
      */
-    toggleOutlineTree: function PDFOutlineView_toggleOutlineTree() {
+    toggleOutlineTree: function PDFOutlineViewer_toggleOutlineTree() {
+      if (!this.outline) {
+        return;
+      }
       this._toggleOutlineItem(this.container, !this.lastToggleIsShow);
     },
 
-    render: function PDFOutlineView_render() {
-      var outline = this.outline;
+    /**
+     * @param {PDFOutlineViewerRenderParameters} params
+     */
+    render: function PDFOutlineViewer_render(params) {
+      var outline = (params && params.outline) || null;
       var outlineCount = 0;
 
-      this.reset();
+      if (this.outline) {
+        this.reset();
+      }
+      this.outline = outline;
 
       if (!outline) {
         this._dispatchEvent(outlineCount);
         return;
       }
 
       var fragment = document.createDocumentFragment();
       var queue = [{ parent: fragment, items: this.outline }];
       var hasAnyNesting = false;
       while (queue.length > 0) {
         var levelData = queue.shift();
         for (var i = 0, len = levelData.items.length; i < len; i++) {
           var item = levelData.items[i];
+
           var div = document.createElement('div');
           div.className = 'outlineItem';
+
           var element = document.createElement('a');
           this._bindLink(element, item);
-          element.textContent = PDFJS.removeNullCharacters(item.title);
+          this._setStyles(element, item);
+          element.textContent =
+            PDFJS.removeNullCharacters(item.title) || DEFAULT_TITLE;
+
           div.appendChild(element);
 
           if (item.items.length > 0) {
             hasAnyNesting = true;
             this._addToggleButton(div);
 
             var itemsDiv = document.createElement('div');
             itemsDiv.className = 'outlineItems';
@@ -5848,75 +6186,89 @@ var PDFOutlineView = (function PDFOutlin
       }
 
       this.container.appendChild(fragment);
 
       this._dispatchEvent(outlineCount);
     }
   };
 
-  return PDFOutlineView;
+  return PDFOutlineViewer;
 })();
 
 
 /**
- * @typedef {Object} PDFAttachmentViewOptions
+ * @typedef {Object} PDFAttachmentViewerOptions
  * @property {HTMLDivElement} container - The viewer element.
- * @property {Array} attachments - An array of attachment objects.
  * @property {DownloadManager} downloadManager - The download manager.
  */
 
 /**
+ * @typedef {Object} PDFAttachmentViewerRenderParameters
+ * @property {Array|null} attachments - An array of attachment objects.
+ */
+
+/**
  * @class
  */
-var PDFAttachmentView = (function PDFAttachmentViewClosure() {
+var PDFAttachmentViewer = (function PDFAttachmentViewerClosure() {
   /**
-   * @constructs PDFAttachmentView
-   * @param {PDFAttachmentViewOptions} options
+   * @constructs PDFAttachmentViewer
+   * @param {PDFAttachmentViewerOptions} options
    */
-  function PDFAttachmentView(options) {
+  function PDFAttachmentViewer(options) {
+    this.attachments = null;
     this.container = options.container;
-    this.attachments = options.attachments;
     this.downloadManager = options.downloadManager;
   }
 
-  PDFAttachmentView.prototype = {
-    reset: function PDFAttachmentView_reset() {
+  PDFAttachmentViewer.prototype = {
+    reset: function PDFAttachmentViewer_reset() {
+      this.attachments = null;
+
       var container = this.container;
       while (container.firstChild) {
         container.removeChild(container.firstChild);
       }
     },
 
     /**
      * @private
      */
-    _dispatchEvent: function PDFAttachmentView_dispatchEvent(attachmentsCount) {
+    _dispatchEvent:
+        function PDFAttachmentViewer_dispatchEvent(attachmentsCount) {
       var event = document.createEvent('CustomEvent');
       event.initCustomEvent('attachmentsloaded', true, true, {
         attachmentsCount: attachmentsCount
       });
       this.container.dispatchEvent(event);
     },
 
     /**
      * @private
      */
-    _bindLink: function PDFAttachmentView_bindLink(button, content, filename) {
+    _bindLink:
+        function PDFAttachmentViewer_bindLink(button, content, filename) {
       button.onclick = function downloadFile(e) {
         this.downloadManager.downloadData(content, filename, '');
         return false;
       }.bind(this);
     },
 
-    render: function PDFAttachmentView_render() {
-      var attachments = this.attachments;
+    /**
+     * @param {PDFAttachmentViewerRenderParameters} params
+     */
+    render: function PDFAttachmentViewer_render(params) {
+      var attachments = (params && params.attachments) || null;
       var attachmentsCount = 0;
 
-      this.reset();
+      if (this.attachments) {
+        this.reset();
+      }
+      this.attachments = attachments;
 
       if (!attachments) {
         this._dispatchEvent(attachmentsCount);
         return;
       }
 
       var names = Object.keys(attachments).sort(function(a, b) {
         return a.toLowerCase().localeCompare(b.toLowerCase());
@@ -5934,43 +6286,48 @@ var PDFAttachmentView = (function PDFAtt
         div.appendChild(button);
         this.container.appendChild(div);
       }
 
       this._dispatchEvent(attachmentsCount);
     }
   };
 
-  return PDFAttachmentView;
+  return PDFAttachmentViewer;
 })();
 
 
 var PDFViewerApplication = {
   initialBookmark: document.location.hash.substring(1),
   initialDestination: null,
   initialized: false,
   fellback: false,
   pdfDocument: null,
   pdfLoadingTask: null,
-  sidebarOpen: false,
   printing: false,
   /** @type {PDFViewer} */
   pdfViewer: null,
   /** @type {PDFThumbnailViewer} */
   pdfThumbnailViewer: null,
   /** @type {PDFRenderingQueue} */
   pdfRenderingQueue: null,
   /** @type {PDFPresentationMode} */
   pdfPresentationMode: null,
   /** @type {PDFDocumentProperties} */
   pdfDocumentProperties: null,
   /** @type {PDFLinkService} */
   pdfLinkService: null,
   /** @type {PDFHistory} */
   pdfHistory: null,
+  /** @type {PDFSidebar} */
+  pdfSidebar: null,
+  /** @type {PDFOutlineViewer} */
+  pdfOutlineViewer: null,
+  /** @type {PDFAttachmentViewer} */
+  pdfAttachmentViewer: null,
   pageRotation: 0,
   isInitialViewSet: false,
   animationStartedPromise: null,
   preferenceSidebarViewOnLoad: SidebarView.NONE,
   preferencePdfBugEnabled: false,
   preferenceShowPreviousViewOnLoad: true,
   preferenceDefaultZoomValue: '',
   isViewerEmbedded: (window.parent !== window),
@@ -6075,17 +6432,16 @@ var PDFViewerApplication = {
     });
 
     if (this.supportsFullscreen) {
       var toolbar = SecondaryToolbar;
       this.pdfPresentationMode = new PDFPresentationMode({
         container: container,
         viewer: viewer,
         pdfViewer: this.pdfViewer,
-        pdfThumbnailViewer: this.pdfThumbnailViewer,
         contextMenuItems: [
           { element: document.getElementById('contextFirstPage'),
             handler: toolbar.firstPageClick.bind(toolbar) },
           { element: document.getElementById('contextLastPage'),
             handler: toolbar.lastPageClick.bind(toolbar) },
           { element: document.getElementById('contextPageRotateCw'),
             handler: toolbar.pageRotateCwClick.bind(toolbar) },
           { element: document.getElementById('contextPageRotateCcw'),
@@ -6097,16 +6453,45 @@ var PDFViewerApplication = {
     PasswordPrompt.initialize({
       overlayName: 'passwordOverlay',
       passwordField: document.getElementById('password'),
       passwordText: document.getElementById('passwordText'),
       passwordSubmit: document.getElementById('passwordSubmit'),
       passwordCancel: document.getElementById('passwordCancel')
     });
 
+    this.pdfOutlineViewer = new PDFOutlineViewer({
+      container: document.getElementById('outlineView'),
+      linkService: pdfLinkService,
+    });
+
+    this.pdfAttachmentViewer = new PDFAttachmentViewer({
+      container: document.getElementById('attachmentsView'),
+      downloadManager: new DownloadManager(),
+    });
+
+    this.pdfSidebar = new PDFSidebar({
+      pdfViewer: this.pdfViewer,
+      pdfThumbnailViewer: this.pdfThumbnailViewer,
+      pdfOutlineViewer: this.pdfOutlineViewer,
+      // Divs (and sidebar button)
+      mainContainer: document.getElementById('mainContainer'),
+      outerContainer: document.getElementById('outerContainer'),
+      toggleButton: document.getElementById('sidebarToggle'),
+      // Buttons
+      thumbnailButton: document.getElementById('viewThumbnail'),
+      outlineButton: document.getElementById('viewOutline'),
+      attachmentsButton: document.getElementById('viewAttachments'),
+      // Views
+      thumbnailView: document.getElementById('thumbnailView'),
+      outlineView: document.getElementById('outlineView'),
+      attachmentsView: document.getElementById('attachmentsView'),
+    });
+    this.pdfSidebar.onToggled = this.forceRendering.bind(this);
+
     var self = this;
     var initializedPromise = Promise.all([
       Preferences.get('enableWebGL').then(function resolved(value) {
         PDFJS.disableWebGL = !value;
       }),
       Preferences.get('sidebarViewOnLoad').then(function resolved(value) {
         self.preferenceSidebarViewOnLoad = value;
       }),
@@ -6368,16 +6753,23 @@ var PDFViewerApplication = {
     if (this.pdfDocument) {
       this.pdfDocument = null;
 
       this.pdfThumbnailViewer.setDocument(null);
       this.pdfViewer.setDocument(null);
       this.pdfLinkService.setDocument(null, null);
     }
 
+    this.pdfSidebar.reset();
+    this.pdfOutlineViewer.reset();
+    this.pdfAttachmentViewer.reset();
+
+    this.findController.reset();
+    this.findBar.reset();
+
     if (typeof PDFBug !== 'undefined') {
       PDFBug.cleanup();
     }
     return promise;
   },
 
   /**
    * Opens PDF document specified by URL or array with additional arguments.
@@ -6605,18 +6997,16 @@ var PDFViewerApplication = {
       }
     }
   },
 
   load: function pdfViewLoad(pdfDocument, scale) {
     var self = this;
     scale = scale || UNKNOWN_SCALE;
 
-    this.findController.reset();
-
     this.pdfDocument = pdfDocument;
 
     this.pdfDocumentProperties.setDocumentAndUrl(pdfDocument, this.url);
 
     var downloadedPromise = pdfDocument.getDownloadInfo().then(function() {
       self.downloadComplete = true;
       self.loadingBar.hide();
     });
@@ -6742,59 +7132,23 @@ var PDFViewerApplication = {
         });
       }
     });
 
     // outline depends on pagesRefMap
     var promises = [pagesPromise, this.animationStartedPromise];
     Promise.all(promises).then(function() {
       pdfDocument.getOutline().then(function(outline) {
-        var container = document.getElementById('outlineView');
-        self.outline = new PDFOutlineView({
-          container: container,
-          outline: outline,
-          linkService: self.pdfLinkService
-        });
-        self.outline.render();
-        document.getElementById('viewOutline').disabled = !outline;
-
-        if (!outline && !container.classList.contains('hidden')) {
-          self.switchSidebarView('thumbs');
-        }
-        if (outline &&
-            self.preferenceSidebarViewOnLoad === SidebarView.OUTLINE) {
-          self.switchSidebarView('outline', true);
-        }
+        self.pdfOutlineViewer.render({ outline: outline });
       });
       pdfDocument.getAttachments().then(function(attachments) {
-        var container = document.getElementById('attachmentsView');
-        self.attachments = new PDFAttachmentView({
-          container: container,
-          attachments: attachments,
-          downloadManager: new DownloadManager()
-        });
-        self.attachments.render();
-        document.getElementById('viewAttachments').disabled = !attachments;
-
-        if (!attachments && !container.classList.contains('hidden')) {
-          self.switchSidebarView('thumbs');
-        }
-        if (attachments &&
-            self.preferenceSidebarViewOnLoad === SidebarView.ATTACHMENTS) {
-          self.switchSidebarView('attachments', true);
-        }
+        self.pdfAttachmentViewer.render({ attachments: attachments });
       });
     });
 
-    if (self.preferenceSidebarViewOnLoad === SidebarView.THUMBS) {
-      Promise.all([firstPagePromise, onePageRendered]).then(function () {
-        self.switchSidebarView('thumbs', true);
-      });
-    }
-
     pdfDocument.getMetadata().then(function(data) {
       var info = data.info, metadata = data.metadata;
       self.documentInfo = info;
       self.metadata = metadata;
 
       // Provides some basic debug information
       console.log('PDF ' + pdfDocument.fingerprint + ' [' +
                   info.PDFFormatVersion + ' ' + (info.Producer || '-').trim() +
@@ -6856,16 +7210,18 @@ var PDFViewerApplication = {
   setInitialView: function pdfViewSetInitialView(storedHash, scale) {
     this.isInitialViewSet = true;
 
     // When opening a new file, when one is already loaded in the viewer,
     // ensure that the 'pageNumber' element displays the correct value.
     document.getElementById('pageNumber').value =
       this.pdfViewer.currentPageNumber;
 
+    this.pdfSidebar.setInitialView(this.preferenceSidebarViewOnLoad);
+
     if (this.initialDestination) {
       this.pdfLinkService.navigateTo(this.initialDestination);
       this.initialDestination = null;
     } else if (this.initialBookmark) {
       this.pdfLinkService.setHash(this.initialBookmark);
       this.pdfHistory.push({ hash: this.initialBookmark }, true);
       this.initialBookmark = null;
     } else if (storedHash) {
@@ -6888,93 +7244,21 @@ var PDFViewerApplication = {
     }
     this.pdfViewer.cleanup();
     this.pdfThumbnailViewer.cleanup();
     this.pdfDocument.cleanup();
   },
 
   forceRendering: function pdfViewForceRendering() {
     this.pdfRenderingQueue.printing = this.printing;
-    this.pdfRenderingQueue.isThumbnailViewEnabled = this.sidebarOpen;
+    this.pdfRenderingQueue.isThumbnailViewEnabled =
+      this.pdfSidebar.isThumbnailViewVisible;
     this.pdfRenderingQueue.renderHighestPriority();
   },
 
-  refreshThumbnailViewer: function pdfViewRefreshThumbnailViewer() {
-    var pdfViewer = this.pdfViewer;
-    var thumbnailViewer = this.pdfThumbnailViewer;
-
-    // set thumbnail images of rendered pages
-    var pagesCount = pdfViewer.pagesCount;
-    for (var pageIndex = 0; pageIndex < pagesCount; pageIndex++) {
-      var pageView = pdfViewer.getPageView(pageIndex);
-      if (pageView && pageView.renderingState === RenderingStates.FINISHED) {
-        var thumbnailView = thumbnailViewer.getThumbnail(pageIndex);
-        thumbnailView.setImage(pageView);
-      }
-    }
-
-    thumbnailViewer.scrollThumbnailIntoView(this.page);
-  },
-
-  switchSidebarView: function pdfViewSwitchSidebarView(view, openSidebar) {
-    if (openSidebar && !this.sidebarOpen) {
-      document.getElementById('sidebarToggle').click();
-    }
-    var thumbsView = document.getElementById('thumbnailView');
-    var outlineView = document.getElementById('outlineView');
-    var attachmentsView = document.getElementById('attachmentsView');
-
-    var thumbsButton = document.getElementById('viewThumbnail');
-    var outlineButton = document.getElementById('viewOutline');
-    var attachmentsButton = document.getElementById('viewAttachments');
-
-    switch (view) {
-      case 'thumbs':
-        var wasAnotherViewVisible = thumbsView.classList.contains('hidden');
-
-        thumbsButton.classList.add('toggled');
-        outlineButton.classList.remove('toggled');
-        attachmentsButton.classList.remove('toggled');
-        thumbsView.classList.remove('hidden');
-        outlineView.classList.add('hidden');
-        attachmentsView.classList.add('hidden');
-
-        this.forceRendering();
-
-        if (wasAnotherViewVisible) {
-          this.pdfThumbnailViewer.ensureThumbnailVisible(this.page);
-        }
-        break;
-
-      case 'outline':
-        if (outlineButton.disabled) {
-          return;
-        }
-        thumbsButton.classList.remove('toggled');
-        outlineButton.classList.add('toggled');
-        attachmentsButton.classList.remove('toggled');
-        thumbsView.classList.add('hidden');
-        outlineView.classList.remove('hidden');
-        attachmentsView.classList.add('hidden');
-        break;
-
-      case 'attachments':
-        if (attachmentsButton.disabled) {
-          return;
-        }
-        thumbsButton.classList.remove('toggled');
-        outlineButton.classList.remove('toggled');
-        attachmentsButton.classList.add('toggled');
-        thumbsView.classList.add('hidden');
-        outlineView.classList.add('hidden');
-        attachmentsView.classList.remove('hidden');
-        break;
-    }
-  },
-
   beforePrint: function pdfViewSetupBeforePrint() {
     if (!this.supportsPrinting) {
       var printMessage = mozL10n.get('printing_not_supported', null,
           'Warning: Printing is not fully supported by this browser.');
       this.error(printMessage);
       return;
     }
 
@@ -7188,58 +7472,28 @@ function webViewerInitialized() {
 
   if (PDFViewerApplication.supportsIntegratedFind) {
     document.getElementById('viewFind').classList.add('hidden');
   }
 
   // Suppress context menus for some controls
   document.getElementById('scaleSelect').oncontextmenu = noContextMenuHandler;
 
-  var mainContainer = document.getElementById('mainContainer');
-  var outerContainer = document.getElementById('outerContainer');
-  mainContainer.addEventListener('transitionend', function(e) {
-    if (e.target === mainContainer) {
-      var event = document.createEvent('UIEvents');
-      event.initUIEvent('resize', false, false, window, 0);
-      window.dispatchEvent(event);
-      outerContainer.classList.remove('sidebarMoving');
-    }
-  }, true);
+  document.getElementById('mainContainer').addEventListener('transitionend',
+    function(e) {
+      if (e.target === /* mainContainer */ this) {
+        var event = document.createEvent('UIEvents');
+        event.initUIEvent('resize', false, false, window, 0);
+        window.dispatchEvent(event);
+      }
+    }, true);
 
   document.getElementById('sidebarToggle').addEventListener('click',
     function() {
-      this.classList.toggle('toggled');
-      outerContainer.classList.add('sidebarMoving');
-      outerContainer.classList.toggle('sidebarOpen');
-      PDFViewerApplication.sidebarOpen =
-        outerContainer.classList.contains('sidebarOpen');
-      if (PDFViewerApplication.sidebarOpen) {
-        PDFViewerApplication.refreshThumbnailViewer();
-      }
-      PDFViewerApplication.forceRendering();
-    });
-
-  document.getElementById('viewThumbnail').addEventListener('click',
-    function() {
-      PDFViewerApplication.switchSidebarView('thumbs');
-    });
-
-  document.getElementById('viewOutline').addEventListener('click',
-    function() {
-      PDFViewerApplication.switchSidebarView('outline');
-    });
-
-  document.getElementById('viewOutline').addEventListener('dblclick',
-    function() {
-      PDFViewerApplication.outline.toggleOutlineTree();
-    });
-
-  document.getElementById('viewAttachments').addEventListener('click',
-    function() {
-      PDFViewerApplication.switchSidebarView('attachments');
+      PDFViewerApplication.pdfSidebar.toggle();
     });
 
   document.getElementById('previous').addEventListener('click',
     function() {
       PDFViewerApplication.page--;
     });
 
   document.getElementById('next').addEventListener('click',
@@ -7297,17 +7551,18 @@ function webViewerInitialized() {
 
 document.addEventListener('DOMContentLoaded', webViewerLoad, true);
 
 document.addEventListener('pagerendered', function (e) {
   var pageNumber = e.detail.pageNumber;
   var pageIndex = pageNumber - 1;
   var pageView = PDFViewerApplication.pdfViewer.getPageView(pageIndex);
 
-  if (PDFViewerApplication.sidebarOpen) {
+  // Use the rendered page to set the corresponding thumbnail image.
+  if (PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
     var thumbnailView = PDFViewerApplication.pdfThumbnailViewer.
                         getThumbnail(pageIndex);
     thumbnailView.setImage(pageView);
   }
 
   if (PDFJS.pdfBug && Stats.enabled && pageView.stats) {
     Stats.add(pageNumber, pageView.stats);
   }
@@ -7351,33 +7606,36 @@ document.addEventListener('textlayerrend
   }
 }, true);
 
 document.addEventListener('pagemode', function (evt) {
   if (!PDFViewerApplication.initialized) {
     return;
   }
   // Handle the 'pagemode' hash parameter, see also `PDFLinkService_setHash`.
-  var mode = evt.detail.mode;
+  var mode = evt.detail.mode, view;
   switch (mode) {
+    case 'thumbs':
+      view = SidebarView.THUMBS;
+      break;
     case 'bookmarks':
-      // Note: Our code calls this property 'outline', even though the
-      //       Open Parameter specification calls it 'bookmarks'.
-      mode = 'outline';
-      /* falls through */
-    case 'thumbs':
+    case 'outline':
+      view = SidebarView.OUTLINE;
+      break;
     case 'attachments':
-      PDFViewerApplication.switchSidebarView(mode, true);
+      view = SidebarView.ATTACHMENTS;
       break;
     case 'none':
-      if (PDFViewerApplication.sidebarOpen) {
-        document.getElementById('sidebarToggle').click();
-      }
+      view = SidebarView.NONE;
       break;
+    default:
+      console.error('Invalid "pagemode" hash parameter: ' + mode);
+      return;
   }
+  PDFViewerApplication.pdfSidebar.switchView(view, /* forceOpen = */ true);
 }, true);
 
 document.addEventListener('namedaction', function (e) {
   if (!PDFViewerApplication.initialized) {
     return;
   }
   // Processing couple of named actions that might be useful.
   // See also PDFLinkService.executeNamedAction
@@ -7537,17 +7795,18 @@ window.addEventListener('scalechange', f
   }
   PDFViewerApplication.pdfViewer.update();
 }, true);
 
 window.addEventListener('pagechange', function pagechange(evt) {
   var page = evt.pageNumber;
   if (evt.previousPageNumber !== page) {
     document.getElementById('pageNumber').value = page;
-    if (PDFViewerApplication.sidebarOpen) {
+
+    if (PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
       PDFViewerApplication.pdfThumbnailViewer.scrollThumbnailIntoView(page);
     }
   }
   var numPages = PDFViewerApplication.pagesCount;
 
   document.getElementById('previous').disabled = (page <= 1);
   document.getElementById('next').disabled = (page >= numPages);