diff --git a/web/app.js b/web/app.js
index dc3c6044b..ed5e6fbe2 100644
--- a/web/app.js
+++ b/web/app.js
@@ -17,7 +17,7 @@
 import {
   animationStarted, DEFAULT_SCALE_VALUE, getPDFFileNameFromURL, isValidRotation,
   MAX_SCALE, MIN_SCALE, noContextMenuHandler, normalizeWheelEventDelta,
-  parseQueryString, ProgressBar, RendererType
+  parseQueryString, PresentationModeState, ProgressBar, RendererType
 } from './ui_utils';
 import {
   build, createBlob, getDocument, getFilenameFromUrl, InvalidPDFException,
@@ -27,7 +27,6 @@ import {
 import { CursorTool, PDFCursorTools } from './pdf_cursor_tools';
 import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue';
 import { PDFSidebar, SidebarView } from './pdf_sidebar';
-import { PDFViewer, PresentationModeState } from './pdf_viewer';
 import { getGlobalEventBus } from './dom_events';
 import { OverlayManager } from './overlay_manager';
 import { PasswordPrompt } from './password_prompt';
@@ -40,6 +39,7 @@ import { PDFLinkService } from './pdf_link_service';
 import { PDFOutlineViewer } from './pdf_outline_viewer';
 import { PDFPresentationMode } from './pdf_presentation_mode';
 import { PDFThumbnailViewer } from './pdf_thumbnail_viewer';
+import { PDFViewer } from './pdf_viewer';
 import { SecondaryToolbar } from './secondary_toolbar';
 import { Toolbar } from './toolbar';
 import { ViewHistory } from './view_history';
diff --git a/web/base_viewer.js b/web/base_viewer.js
new file mode 100644
index 000000000..dcb7125de
--- /dev/null
+++ b/web/base_viewer.js
@@ -0,0 +1,948 @@
+/* Copyright 2014 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.
+ */
+
+import { createPromiseCapability, PDFJS } from 'pdfjs-lib';
+import {
+  CSS_UNITS, DEFAULT_SCALE, DEFAULT_SCALE_VALUE, isValidRotation,
+  MAX_AUTO_SCALE, NullL10n, PresentationModeState, RendererType,
+  SCROLLBAR_PADDING, UNKNOWN_SCALE, VERTICAL_PADDING, watchScroll
+} from './ui_utils';
+import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue';
+import { AnnotationLayerBuilder } from './annotation_layer_builder';
+import { getGlobalEventBus } from './dom_events';
+import { PDFPageView } from './pdf_page_view';
+import { SimpleLinkService } from './pdf_link_service';
+import { TextLayerBuilder } from './text_layer_builder';
+
+const DEFAULT_CACHE_SIZE = 10;
+
+/**
+ * @typedef {Object} PDFViewerOptions
+ * @property {HTMLDivElement} container - The container for the viewer element.
+ * @property {HTMLDivElement} viewer - (optional) The viewer element.
+ * @property {EventBus} eventBus - The application event bus.
+ * @property {IPDFLinkService} linkService - The navigation/linking service.
+ * @property {DownloadManager} downloadManager - (optional) The download
+ *   manager component.
+ * @property {PDFRenderingQueue} renderingQueue - (optional) The rendering
+ *   queue object.
+ * @property {boolean} removePageBorders - (optional) Removes the border shadow
+ *   around the pages. The default is false.
+ * @property {boolean} enhanceTextSelection - (optional) Enables the improved
+ *   text selection behaviour. The default is `false`.
+ * @property {boolean} renderInteractiveForms - (optional) Enables rendering of
+ *   interactive form elements. The default is `false`.
+ * @property {boolean} enablePrintAutoRotate - (optional) Enables automatic
+ *   rotation of pages whose orientation differ from the first page upon
+ *   printing. The default is `false`.
+ * @property {string} renderer - 'canvas' or 'svg'. The default is 'canvas'.
+ * @property {IL10n} l10n - Localization service.
+ */
+
+function PDFPageViewBuffer(size) {
+  let data = [];
+  this.push = function(view) {
+    let i = data.indexOf(view);
+    if (i >= 0) {
+      data.splice(i, 1);
+    }
+    data.push(view);
+    if (data.length > size) {
+      data.shift().destroy();
+    }
+  };
+  this.resize = function(newSize) {
+    size = newSize;
+    while (data.length > size) {
+      data.shift().destroy();
+    }
+  };
+}
+
+function isSameScale(oldScale, newScale) {
+  if (newScale === oldScale) {
+    return true;
+  }
+  if (Math.abs(newScale - oldScale) < 1e-15) {
+    // Prevent unnecessary re-rendering of all pages when the scale
+    // changes only because of limited numerical precision.
+    return true;
+  }
+  return false;
+}
+
+function isPortraitOrientation(size) {
+  return size.width <= size.height;
+}
+
+/**
+ * Simple viewer control to display PDF content/pages.
+ * @implements {IRenderableView}
+ */
+class BaseViewer {
+  /**
+   * @param {PDFViewerOptions} options
+   */
+  constructor(options) {
+    if (this.constructor === BaseViewer) {
+      throw new Error('Cannot initialize BaseViewer.');
+    }
+    this._name = this.constructor.name;
+
+    this.container = options.container;
+    this.viewer = options.viewer || options.container.firstElementChild;
+    this.eventBus = options.eventBus || getGlobalEventBus();
+    this.linkService = options.linkService || new SimpleLinkService();
+    this.downloadManager = options.downloadManager || null;
+    this.removePageBorders = options.removePageBorders || false;
+    this.enhanceTextSelection = options.enhanceTextSelection || false;
+    this.renderInteractiveForms = options.renderInteractiveForms || false;
+    this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
+    this.renderer = options.renderer || RendererType.CANVAS;
+    this.l10n = options.l10n || NullL10n;
+
+    this.defaultRenderingQueue = !options.renderingQueue;
+    if (this.defaultRenderingQueue) {
+      // Custom rendering queue is not specified, using default one
+      this.renderingQueue = new PDFRenderingQueue();
+      this.renderingQueue.setViewer(this);
+    } else {
+      this.renderingQueue = options.renderingQueue;
+    }
+
+    this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this));
+    this.presentationModeState = PresentationModeState.UNKNOWN;
+    this._resetView();
+
+    if (this.removePageBorders) {
+      this.viewer.classList.add('removePageBorders');
+    }
+  }
+
+  get pagesCount() {
+    return this._pages.length;
+  }
+
+  getPageView(index) {
+    return this._pages[index];
+  }
+
+  /**
+   * @returns {boolean} true if all {PDFPageView} objects are initialized.
+   */
+  get pageViewsReady() {
+    return this._pageViewsReady;
+  }
+
+  /**
+   * @returns {number}
+   */
+  get currentPageNumber() {
+    return this._currentPageNumber;
+  }
+
+  /**
+   * @param {number} val - The page number.
+   */
+  set currentPageNumber(val) {
+    if (!Number.isInteger(val)) {
+      throw new Error('Invalid page number.');
+    }
+    if (!this.pdfDocument) {
+      return;
+    }
+    // The intent can be to just reset a scroll position and/or scale.
+    this._setCurrentPageNumber(val, /* resetCurrentPageView = */ true);
+  }
+
+  /**
+   * @private
+   */
+  _setCurrentPageNumber(val, resetCurrentPageView = false) {
+    if (this._currentPageNumber === val) {
+      if (resetCurrentPageView) {
+        this._resetCurrentPageView();
+      }
+      return;
+    }
+
+    if (!(0 < val && val <= this.pagesCount)) {
+      console.error(
+        `${this._name}._setCurrentPageNumber: "${val}" is out of bounds.`);
+      return;
+    }
+
+    let arg = {
+      source: this,
+      pageNumber: val,
+      pageLabel: this._pageLabels && this._pageLabels[val - 1],
+    };
+    this._currentPageNumber = val;
+    this.eventBus.dispatch('pagechanging', arg);
+    this.eventBus.dispatch('pagechange', arg);
+
+    if (resetCurrentPageView) {
+      this._resetCurrentPageView();
+    }
+  }
+
+  /**
+   * @returns {string|null} Returns the current page label,
+   *                        or `null` if no page labels exist.
+   */
+  get currentPageLabel() {
+    return this._pageLabels && this._pageLabels[this._currentPageNumber - 1];
+  }
+
+  /**
+   * @param {string} val - The page label.
+   */
+  set currentPageLabel(val) {
+    let pageNumber = val | 0; // Fallback page number.
+    if (this._pageLabels) {
+      let i = this._pageLabels.indexOf(val);
+      if (i >= 0) {
+        pageNumber = i + 1;
+      }
+    }
+    this.currentPageNumber = pageNumber;
+  }
+
+  /**
+   * @returns {number}
+   */
+  get currentScale() {
+    return this._currentScale !== UNKNOWN_SCALE ? this._currentScale :
+                                                  DEFAULT_SCALE;
+  }
+
+  /**
+   * @param {number} val - Scale of the pages in percents.
+   */
+  set currentScale(val) {
+    if (isNaN(val)) {
+      throw new Error('Invalid numeric scale');
+    }
+    if (!this.pdfDocument) {
+      return;
+    }
+    this._setScale(val, false);
+  }
+
+  /**
+   * @returns {string}
+   */
+  get currentScaleValue() {
+    return this._currentScaleValue;
+  }
+
+  /**
+   * @param val - The scale of the pages (in percent or predefined value).
+   */
+  set currentScaleValue(val) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    this._setScale(val, false);
+  }
+
+  /**
+   * @returns {number}
+   */
+  get pagesRotation() {
+    return this._pagesRotation;
+  }
+
+  /**
+   * @param {number} rotation - The rotation of the pages (0, 90, 180, 270).
+   */
+  set pagesRotation(rotation) {
+    if (!isValidRotation(rotation)) {
+      throw new Error('Invalid pages rotation angle.');
+    }
+    if (!this.pdfDocument) {
+      return;
+    }
+    if (this._pagesRotation === rotation) {
+      return; // The rotation didn't change.
+    }
+    this._pagesRotation = rotation;
+
+    let pageNumber = this._currentPageNumber;
+
+    for (let i = 0, ii = this._pages.length; i < ii; i++) {
+      let pageView = this._pages[i];
+      pageView.update(pageView.scale, rotation);
+    }
+    // Prevent errors in case the rotation changes *before* the scale has been
+    // set to a non-default value.
+    if (this._currentScaleValue) {
+      this._setScale(this._currentScaleValue, true);
+    }
+
+    this.eventBus.dispatch('rotationchanging', {
+      source: this,
+      pagesRotation: rotation,
+      pageNumber,
+    });
+
+    if (this.defaultRenderingQueue) {
+      this.update();
+    }
+  }
+
+  get _setDocumentViewerElement() {
+    throw new Error('Not implemented: _setDocumentViewerElement');
+  }
+
+  /**
+   * @param pdfDocument {PDFDocument}
+   */
+  setDocument(pdfDocument) {
+    if (this.pdfDocument) {
+      this._cancelRendering();
+      this._resetView();
+    }
+
+    this.pdfDocument = pdfDocument;
+    if (!pdfDocument) {
+      return;
+    }
+    let pagesCount = pdfDocument.numPages;
+
+    let pagesCapability = createPromiseCapability();
+    this.pagesPromise = pagesCapability.promise;
+
+    pagesCapability.promise.then(() => {
+      this._pageViewsReady = true;
+      this.eventBus.dispatch('pagesloaded', {
+        source: this,
+        pagesCount,
+      });
+    });
+
+    let isOnePageRenderedResolved = false;
+    let onePageRenderedCapability = createPromiseCapability();
+    this.onePageRendered = onePageRenderedCapability.promise;
+
+    let bindOnAfterAndBeforeDraw = (pageView) => {
+      pageView.onBeforeDraw = () => {
+        // Add the page to the buffer at the start of drawing. That way it can
+        // be evicted from the buffer and destroyed even if we pause its
+        // rendering.
+        this._buffer.push(pageView);
+      };
+      pageView.onAfterDraw = () => {
+        if (!isOnePageRenderedResolved) {
+          isOnePageRenderedResolved = true;
+          onePageRenderedCapability.resolve();
+        }
+      };
+    };
+
+    let firstPagePromise = pdfDocument.getPage(1);
+    this.firstPagePromise = firstPagePromise;
+
+    // Fetch a single page so we can get a viewport that will be the default
+    // viewport for all pages
+    firstPagePromise.then((pdfPage) => {
+      let scale = this.currentScale;
+      let viewport = pdfPage.getViewport(scale * CSS_UNITS);
+      for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
+        let textLayerFactory = null;
+        if (!PDFJS.disableTextLayer) {
+          textLayerFactory = this;
+        }
+        let pageView = new PDFPageView({
+          container: this._setDocumentViewerElement,
+          eventBus: this.eventBus,
+          id: pageNum,
+          scale,
+          defaultViewport: viewport.clone(),
+          renderingQueue: this.renderingQueue,
+          textLayerFactory,
+          annotationLayerFactory: this,
+          enhanceTextSelection: this.enhanceTextSelection,
+          renderInteractiveForms: this.renderInteractiveForms,
+          renderer: this.renderer,
+          l10n: this.l10n,
+        });
+        bindOnAfterAndBeforeDraw(pageView);
+        this._pages.push(pageView);
+      }
+
+      // Fetch all the pages since the viewport is needed before printing
+      // starts to create the correct size canvas. Wait until one page is
+      // rendered so we don't tie up too many resources early on.
+      onePageRenderedCapability.promise.then(() => {
+        if (PDFJS.disableAutoFetch) {
+          // XXX: Printing is semi-broken with auto fetch disabled.
+          pagesCapability.resolve();
+          return;
+        }
+        let getPagesLeft = pagesCount;
+        for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
+          pdfDocument.getPage(pageNum).then((pdfPage) => {
+            let pageView = this._pages[pageNum - 1];
+            if (!pageView.pdfPage) {
+              pageView.setPdfPage(pdfPage);
+            }
+            this.linkService.cachePageRef(pageNum, pdfPage.ref);
+            if (--getPagesLeft === 0) {
+              pagesCapability.resolve();
+            }
+          }, (reason) => {
+            console.error(`Unable to get page ${pageNum} to initialize viewer`,
+                          reason);
+            if (--getPagesLeft === 0) {
+              pagesCapability.resolve();
+            }
+          });
+        }
+      });
+
+      this.eventBus.dispatch('pagesinit', { source: this, });
+
+      if (this.defaultRenderingQueue) {
+        this.update();
+      }
+
+      if (this.findController) {
+        this.findController.resolveFirstPage();
+      }
+    }).catch((reason) => {
+      console.error('Unable to initialize viewer', reason);
+    });
+  }
+
+  /**
+   * @param {Array|null} labels
+   */
+  setPageLabels(labels) {
+    if (!this.pdfDocument) {
+      return;
+    }
+    if (!labels) {
+      this._pageLabels = null;
+    } else if (!(labels instanceof Array &&
+                 this.pdfDocument.numPages === labels.length)) {
+      this._pageLabels = null;
+      console.error(`${this._name}.setPageLabels: Invalid page labels.`);
+    } else {
+      this._pageLabels = labels;
+    }
+    // Update all the `PDFPageView` instances.
+    for (let i = 0, ii = this._pages.length; i < ii; i++) {
+      let pageView = this._pages[i];
+      let label = this._pageLabels && this._pageLabels[i];
+      pageView.setPageLabel(label);
+    }
+  }
+
+  _resetView() {
+    this._pages = [];
+    this._currentPageNumber = 1;
+    this._currentScale = UNKNOWN_SCALE;
+    this._currentScaleValue = null;
+    this._pageLabels = null;
+    this._buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE);
+    this._location = null;
+    this._pagesRotation = 0;
+    this._pagesRequests = [];
+    this._pageViewsReady = false;
+
+    // Remove the pages from the DOM.
+    this.viewer.textContent = '';
+  }
+
+  _scrollUpdate() {
+    if (this.pagesCount === 0) {
+      return;
+    }
+    this.update();
+  }
+
+  _scrollIntoView({ pageDiv, pageSpot = null, pageNumber = null, }) {
+    throw new Error('Not implemented: _scrollIntoView');
+  }
+
+  _setScaleDispatchEvent(newScale, newValue, preset = false) {
+    let arg = {
+      source: this,
+      scale: newScale,
+      presetValue: preset ? newValue : undefined,
+    };
+    this.eventBus.dispatch('scalechanging', arg);
+    this.eventBus.dispatch('scalechange', arg);
+  }
+
+  _setScaleUpdatePages(newScale, newValue, noScroll = false, preset = false) {
+    this._currentScaleValue = newValue.toString();
+
+    if (isSameScale(this._currentScale, newScale)) {
+      if (preset) {
+        this._setScaleDispatchEvent(newScale, newValue, true);
+      }
+      return;
+    }
+
+    for (let i = 0, ii = this._pages.length; i < ii; i++) {
+      this._pages[i].update(newScale);
+    }
+    this._currentScale = newScale;
+
+    if (!noScroll) {
+      let page = this._currentPageNumber, dest;
+      if (this._location && !PDFJS.ignoreCurrentPositionOnZoom &&
+          !(this.isInPresentationMode || this.isChangingPresentationMode)) {
+        page = this._location.pageNumber;
+        dest = [null, { name: 'XYZ', }, this._location.left,
+                this._location.top, null];
+      }
+      this.scrollPageIntoView({
+        pageNumber: page,
+        destArray: dest,
+        allowNegativeOffset: true,
+      });
+    }
+
+    this._setScaleDispatchEvent(newScale, newValue, preset);
+
+    if (this.defaultRenderingQueue) {
+      this.update();
+    }
+  }
+
+  _setScale(value, noScroll = false) {
+    let scale = parseFloat(value);
+
+    if (scale > 0) {
+      this._setScaleUpdatePages(scale, value, noScroll, /* preset = */ false);
+    } else {
+      let currentPage = this._pages[this._currentPageNumber - 1];
+      if (!currentPage) {
+        return;
+      }
+      let hPadding = (this.isInPresentationMode || this.removePageBorders) ?
+        0 : SCROLLBAR_PADDING;
+      let vPadding = (this.isInPresentationMode || this.removePageBorders) ?
+        0 : VERTICAL_PADDING;
+      let pageWidthScale = (this.container.clientWidth - hPadding) /
+                           currentPage.width * currentPage.scale;
+      let pageHeightScale = (this.container.clientHeight - vPadding) /
+                            currentPage.height * currentPage.scale;
+      switch (value) {
+        case 'page-actual':
+          scale = 1;
+          break;
+        case 'page-width':
+          scale = pageWidthScale;
+          break;
+        case 'page-height':
+          scale = pageHeightScale;
+          break;
+        case 'page-fit':
+          scale = Math.min(pageWidthScale, pageHeightScale);
+          break;
+        case 'auto':
+          let isLandscape = (currentPage.width > currentPage.height);
+          // For pages in landscape mode, fit the page height to the viewer
+          // *unless* the page would thus become too wide to fit horizontally.
+          let horizontalScale = isLandscape ?
+            Math.min(pageHeightScale, pageWidthScale) : pageWidthScale;
+          scale = Math.min(MAX_AUTO_SCALE, horizontalScale);
+          break;
+        default:
+          console.error(
+            `${this._name}._setScale: "${value}" is an unknown zoom value.`);
+          return;
+      }
+      this._setScaleUpdatePages(scale, value, noScroll, /* preset = */ true);
+    }
+  }
+
+  /**
+   * Refreshes page view: scrolls to the current page and updates the scale.
+   * @private
+   */
+  _resetCurrentPageView() {
+    if (this.isInPresentationMode) {
+      // Fixes the case when PDF has different page sizes.
+      this._setScale(this._currentScaleValue, true);
+    }
+
+    let pageView = this._pages[this._currentPageNumber - 1];
+    this._scrollIntoView({ pageDiv: pageView.div, });
+  }
+
+  /**
+   * @typedef ScrollPageIntoViewParameters
+   * @property {number} pageNumber - The page number.
+   * @property {Array} destArray - (optional) The original PDF destination
+   *   array, in the format: <page-ref> </XYZ|/FitXXX> <args..>
+   * @property {boolean} allowNegativeOffset - (optional) Allow negative page
+   *   offsets. The default value is `false`.
+   */
+
+  /**
+   * Scrolls page into view.
+   * @param {ScrollPageIntoViewParameters} params
+   */
+  scrollPageIntoView(params) {
+    if ((typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) &&
+        (arguments.length > 1 || typeof params === 'number')) {
+      console.error('Call of scrollPageIntoView() with obsolete signature.');
+      return;
+    }
+    if (!this.pdfDocument) {
+      return;
+    }
+    let pageNumber = params.pageNumber || 0;
+    let dest = params.destArray || null;
+    let allowNegativeOffset = params.allowNegativeOffset || false;
+
+    if (this.isInPresentationMode || !dest) {
+      this._setCurrentPageNumber(pageNumber, /* resetCurrentPageView = */ true);
+      return;
+    }
+
+    let pageView = this._pages[pageNumber - 1];
+    if (!pageView) {
+      console.error(
+        `${this._name}.scrollPageIntoView: Invalid "pageNumber" parameter.`);
+      return;
+    }
+    let x = 0, y = 0;
+    let width = 0, height = 0, widthScale, heightScale;
+    let changeOrientation = (pageView.rotation % 180 === 0 ? false : true);
+    let pageWidth = (changeOrientation ? pageView.height : pageView.width) /
+      pageView.scale / CSS_UNITS;
+    let pageHeight = (changeOrientation ? pageView.width : pageView.height) /
+      pageView.scale / CSS_UNITS;
+    let scale = 0;
+    switch (dest[1].name) {
+      case 'XYZ':
+        x = dest[2];
+        y = dest[3];
+        scale = dest[4];
+        // If x and/or y coordinates are not supplied, default to
+        // _top_ left of the page (not the obvious bottom left,
+        // since aligning the bottom of the intended page with the
+        // top of the window is rarely helpful).
+        x = x !== null ? x : 0;
+        y = y !== null ? y : pageHeight;
+        break;
+      case 'Fit':
+      case 'FitB':
+        scale = 'page-fit';
+        break;
+      case 'FitH':
+      case 'FitBH':
+        y = dest[2];
+        scale = 'page-width';
+        // According to the PDF spec, section 12.3.2.2, a `null` value in the
+        // parameter should maintain the position relative to the new page.
+        if (y === null && this._location) {
+          x = this._location.left;
+          y = this._location.top;
+        }
+        break;
+      case 'FitV':
+      case 'FitBV':
+        x = dest[2];
+        width = pageWidth;
+        height = pageHeight;
+        scale = 'page-height';
+        break;
+      case 'FitR':
+        x = dest[2];
+        y = dest[3];
+        width = dest[4] - x;
+        height = dest[5] - y;
+        let hPadding = this.removePageBorders ? 0 : SCROLLBAR_PADDING;
+        let vPadding = this.removePageBorders ? 0 : VERTICAL_PADDING;
+
+        widthScale = (this.container.clientWidth - hPadding) /
+          width / CSS_UNITS;
+        heightScale = (this.container.clientHeight - vPadding) /
+          height / CSS_UNITS;
+        scale = Math.min(Math.abs(widthScale), Math.abs(heightScale));
+        break;
+      default:
+        console.error(`${this._name}.scrollPageIntoView: "${dest[1].name}" ` +
+                      'is not a valid destination type.');
+        return;
+    }
+
+    if (scale && scale !== this._currentScale) {
+      this.currentScaleValue = scale;
+    } else if (this._currentScale === UNKNOWN_SCALE) {
+      this.currentScaleValue = DEFAULT_SCALE_VALUE;
+    }
+
+    if (scale === 'page-fit' && !dest[4]) {
+      this._scrollIntoView({
+        pageDiv: pageView.div,
+        pageNumber,
+      });
+      return;
+    }
+
+    let boundingRect = [
+      pageView.viewport.convertToViewportPoint(x, y),
+      pageView.viewport.convertToViewportPoint(x + width, y + height)
+    ];
+    let left = Math.min(boundingRect[0][0], boundingRect[1][0]);
+    let top = Math.min(boundingRect[0][1], boundingRect[1][1]);
+
+    if (!allowNegativeOffset) {
+      // Some bad PDF generators will create destinations with e.g. top values
+      // that exceeds the page height. Ensure that offsets are not negative,
+      // to prevent a previous page from becoming visible (fixes bug 874482).
+      left = Math.max(left, 0);
+      top = Math.max(top, 0);
+    }
+    this._scrollIntoView({
+      pageDiv: pageView.div,
+      pageSpot: { left, top, },
+      pageNumber,
+    });
+  }
+
+  _resizeBuffer(numVisiblePages) {
+    let suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE,
+                                      2 * numVisiblePages + 1);
+    this._buffer.resize(suggestedCacheSize);
+  }
+
+  _updateLocation(firstPage) {
+    let currentScale = this._currentScale;
+    let currentScaleValue = this._currentScaleValue;
+    let normalizedScaleValue =
+      parseFloat(currentScaleValue) === currentScale ?
+      Math.round(currentScale * 10000) / 100 : currentScaleValue;
+
+    let pageNumber = firstPage.id;
+    let pdfOpenParams = '#page=' + pageNumber;
+    pdfOpenParams += '&zoom=' + normalizedScaleValue;
+    let currentPageView = this._pages[pageNumber - 1];
+    let container = this.container;
+    let topLeft = currentPageView.getPagePoint(
+      (container.scrollLeft - firstPage.x),
+      (container.scrollTop - firstPage.y));
+    let intLeft = Math.round(topLeft[0]);
+    let intTop = Math.round(topLeft[1]);
+    pdfOpenParams += ',' + intLeft + ',' + intTop;
+
+    this._location = {
+      pageNumber,
+      scale: normalizedScaleValue,
+      top: intTop,
+      left: intLeft,
+      rotation: this._pagesRotation,
+      pdfOpenParams,
+    };
+  }
+
+  update() {
+    throw new Error('Not implemented: update');
+  }
+
+  containsElement(element) {
+    return this.container.contains(element);
+  }
+
+  focus() {
+    this.container.focus();
+  }
+
+  get isInPresentationMode() {
+    return this.presentationModeState === PresentationModeState.FULLSCREEN;
+  }
+
+  get isChangingPresentationMode() {
+    return this.presentationModeState === PresentationModeState.CHANGING;
+  }
+
+  get isHorizontalScrollbarEnabled() {
+    return (this.isInPresentationMode ?
+      false : (this.container.scrollWidth > this.container.clientWidth));
+  }
+
+  _getVisiblePages() {
+    throw new Error('Not implemented: _getVisiblePages');
+  }
+
+  cleanup() {
+    for (let i = 0, ii = this._pages.length; i < ii; i++) {
+      if (this._pages[i] &&
+          this._pages[i].renderingState !== RenderingStates.FINISHED) {
+        this._pages[i].reset();
+      }
+    }
+  }
+
+  /**
+   * @private
+   */
+  _cancelRendering() {
+    for (let i = 0, ii = this._pages.length; i < ii; i++) {
+      if (this._pages[i]) {
+        this._pages[i].cancelRendering();
+      }
+    }
+  }
+
+  /**
+   * @param {PDFPageView} pageView
+   * @returns {Promise} Returns a promise containing a {PDFPageProxy} object.
+   * @private
+   */
+  _ensurePdfPageLoaded(pageView) {
+    if (pageView.pdfPage) {
+      return Promise.resolve(pageView.pdfPage);
+    }
+    let pageNumber = pageView.id;
+    if (this._pagesRequests[pageNumber]) {
+      return this._pagesRequests[pageNumber];
+    }
+    let promise = this.pdfDocument.getPage(pageNumber).then((pdfPage) => {
+      if (!pageView.pdfPage) {
+        pageView.setPdfPage(pdfPage);
+      }
+      this._pagesRequests[pageNumber] = null;
+      return pdfPage;
+    }).catch((reason) => {
+      console.error('Unable to get page for page view', reason);
+      // Page error -- there is nothing can be done.
+      this._pagesRequests[pageNumber] = null;
+    });
+    this._pagesRequests[pageNumber] = promise;
+    return promise;
+  }
+
+  forceRendering(currentlyVisiblePages) {
+    let visiblePages = currentlyVisiblePages || this._getVisiblePages();
+    let pageView = this.renderingQueue.getHighestPriority(visiblePages,
+                                                          this._pages,
+                                                          this.scroll.down);
+    if (pageView) {
+      this._ensurePdfPageLoaded(pageView).then(() => {
+        this.renderingQueue.renderView(pageView);
+      });
+      return true;
+    }
+    return false;
+  }
+
+  getPageTextContent(pageIndex) {
+    return this.pdfDocument.getPage(pageIndex + 1).then(function(page) {
+      return page.getTextContent({
+        normalizeWhitespace: true,
+      });
+    });
+  }
+
+  /**
+   * @param {HTMLDivElement} textLayerDiv
+   * @param {number} pageIndex
+   * @param {PageViewport} viewport
+   * @returns {TextLayerBuilder}
+   */
+  createTextLayerBuilder(textLayerDiv, pageIndex, viewport,
+                         enhanceTextSelection = false) {
+    return new TextLayerBuilder({
+      textLayerDiv,
+      eventBus: this.eventBus,
+      pageIndex,
+      viewport,
+      findController: this.isInPresentationMode ? null : this.findController,
+      enhanceTextSelection: this.isInPresentationMode ? false :
+                                                        enhanceTextSelection,
+    });
+  }
+
+  /**
+   * @param {HTMLDivElement} pageDiv
+   * @param {PDFPage} pdfPage
+   * @param {boolean} renderInteractiveForms
+   * @param {IL10n} l10n
+   * @returns {AnnotationLayerBuilder}
+   */
+  createAnnotationLayerBuilder(pageDiv, pdfPage, renderInteractiveForms = false,
+                               l10n = NullL10n) {
+    return new AnnotationLayerBuilder({
+      pageDiv,
+      pdfPage,
+      renderInteractiveForms,
+      linkService: this.linkService,
+      downloadManager: this.downloadManager,
+      l10n,
+    });
+  }
+
+  setFindController(findController) {
+    this.findController = findController;
+  }
+
+  /**
+   * @returns {boolean} Whether all pages of the PDF document have identical
+   *                    widths and heights.
+   */
+  get hasEqualPageSizes() {
+    let firstPageView = this._pages[0];
+    for (let i = 1, ii = this._pages.length; i < ii; ++i) {
+      let pageView = this._pages[i];
+      if (pageView.width !== firstPageView.width ||
+          pageView.height !== firstPageView.height) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Returns sizes of the pages.
+   * @returns {Array} Array of objects with width/height/rotation fields.
+   */
+  getPagesOverview() {
+    let pagesOverview = this._pages.map(function(pageView) {
+      let viewport = pageView.pdfPage.getViewport(1);
+      return {
+        width: viewport.width,
+        height: viewport.height,
+        rotation: viewport.rotation,
+      };
+    });
+    if (!this.enablePrintAutoRotate) {
+      return pagesOverview;
+    }
+    let isFirstPagePortrait = isPortraitOrientation(pagesOverview[0]);
+    return pagesOverview.map(function (size) {
+      if (isFirstPagePortrait === isPortraitOrientation(size)) {
+        return size;
+      }
+      return {
+        width: size.height,
+        height: size.width,
+        rotation: (size.rotation + 90) % 360,
+      };
+    });
+  }
+}
+
+export {
+  BaseViewer,
+};
diff --git a/web/pdf_single_page_viewer.js b/web/pdf_single_page_viewer.js
new file mode 100644
index 000000000..94fb7823e
--- /dev/null
+++ b/web/pdf_single_page_viewer.js
@@ -0,0 +1,149 @@
+/* Copyright 2017 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.
+ */
+
+import { BaseViewer } from './base_viewer';
+import { scrollIntoView } from './ui_utils';
+import { shadow } from 'pdfjs-lib';
+
+class PDFSinglePageViewer extends BaseViewer {
+  constructor(options) {
+    super(options);
+
+    this.eventBus.on('pagesinit', (evt) => {
+      // Since the pages are placed in a `DocumentFragment`, make sure that
+      // the current page becomes visible upon loading of the document.
+      this._ensurePageViewVisible();
+    });
+  }
+
+  get _setDocumentViewerElement() {
+    // Since we only want to display *one* page at a time when using the
+    // `PDFSinglePageViewer`, we cannot append them to the `viewer` DOM element.
+    // Instead, they are placed in a `DocumentFragment`, and only the current
+    // page is displayed in the viewer (refer to `this._ensurePageViewVisible`).
+    return shadow(this, '_setDocumentViewerElement', this._shadowViewer);
+  }
+
+  _resetView() {
+    super._resetView();
+    this._previousPageNumber = 1;
+    this._shadowViewer = document.createDocumentFragment();
+  }
+
+  _ensurePageViewVisible() {
+    let pageView = this._pages[this._currentPageNumber - 1];
+    let previousPageView = this._pages[this._previousPageNumber - 1];
+
+    let viewerNodes = this.viewer.childNodes;
+    switch (viewerNodes.length) {
+      case 0: // Should *only* occur on initial loading.
+        this.viewer.appendChild(pageView.div);
+        break;
+      case 1: // The normal page-switching case.
+        if (viewerNodes[0] !== previousPageView.div) {
+          throw new Error(
+            '_ensurePageViewVisible: Unexpected previously visible page.');
+        }
+        if (pageView === previousPageView) {
+          break; // The correct page is already visible.
+        }
+        // Switch visible pages, and reset the viewerContainer scroll position.
+        this._shadowViewer.appendChild(previousPageView.div);
+        this.viewer.appendChild(pageView.div);
+
+        this.container.scrollTop = 0;
+        break;
+      default:
+        throw new Error(
+          '_ensurePageViewVisible: Only one page should be visible at a time.');
+    }
+    this._previousPageNumber = this._currentPageNumber;
+  }
+
+  _scrollUpdate() {
+    if (this._updateScrollDown) {
+      this._updateScrollDown();
+    }
+    super._scrollUpdate();
+  }
+
+  _scrollIntoView({ pageDiv, pageSpot = null, pageNumber = null, }) {
+    if (pageNumber) { // Ensure that `this._currentPageNumber` is correct.
+      this._setCurrentPageNumber(pageNumber);
+    }
+    let scrolledDown = this._currentPageNumber >= this._previousPageNumber;
+    let previousLocation = this._location;
+    this._ensurePageViewVisible();
+
+    scrollIntoView(pageDiv, pageSpot);
+
+    // Since scrolling is tracked using `requestAnimationFrame`, update the
+    // scroll direction during the next `this._scrollUpdate` invocation.
+    this._updateScrollDown = () => {
+      this.scroll.down = scrolledDown;
+      delete this._updateScrollDown;
+    };
+    // If the scroll position doesn't change as a result of the `scrollIntoView`
+    // call, ensure that rendering always occurs to avoid showing a blank page.
+    setTimeout(() => {
+      if (this._location === previousLocation) {
+        if (this._updateScrollDown) {
+          this._updateScrollDown();
+        }
+        this.update();
+      }
+    }, 0);
+  }
+
+  _getVisiblePages() {
+    if (!this.pagesCount) {
+      return { views: [], };
+    }
+    let pageView = this._pages[this._currentPageNumber - 1];
+    // NOTE: Compute the `x` and `y` properties of the current view,
+    // since `this._updateLocation` depends of them being available.
+    let element = pageView.div;
+
+    let view = {
+      id: pageView.id,
+      x: element.offsetLeft + element.clientLeft,
+      y: element.offsetTop + element.clientTop,
+      view: pageView,
+    };
+    return { first: view, last: view, views: [view], };
+  }
+
+  update() {
+    let visible = this._getVisiblePages();
+    let visiblePages = visible.views, numVisiblePages = visiblePages.length;
+
+    if (numVisiblePages === 0) {
+      return;
+    }
+    this._resizeBuffer(numVisiblePages);
+
+    this.renderingQueue.renderHighestPriority(visible);
+
+    this._updateLocation(visible.first);
+    this.eventBus.dispatch('updateviewarea', {
+      source: this,
+      location: this._location,
+    });
+  }
+}
+
+export {
+  PDFSinglePageViewer,
+};
diff --git a/web/pdf_viewer.component.js b/web/pdf_viewer.component.js
index 584aa9ed6..8c9f50ef4 100644
--- a/web/pdf_viewer.component.js
+++ b/web/pdf_viewer.component.js
@@ -17,6 +17,7 @@
 
 var pdfjsLib = require('./pdfjs.js');
 var pdfjsWebPDFViewer = require('./pdf_viewer.js');
+var pdfjsWebPDFSinglePageViewer = require('./pdf_single_page_viewer');
 var pdfjsWebPDFPageView = require('./pdf_page_view.js');
 var pdfjsWebPDFLinkService = require('./pdf_link_service.js');
 var pdfjsWebTextLayerBuilder = require('./text_layer_builder.js');
@@ -30,6 +31,7 @@ var pdfjsWebGenericL10n = require('./genericl10n.js');
 var PDFJS = pdfjsLib.PDFJS;
 
 PDFJS.PDFViewer = pdfjsWebPDFViewer.PDFViewer;
+PDFJS.PDFSinglePageViewer = pdfjsWebPDFSinglePageViewer.PDFSinglePageViewer;
 PDFJS.PDFPageView = pdfjsWebPDFPageView.PDFPageView;
 PDFJS.PDFLinkService = pdfjsWebPDFLinkService.PDFLinkService;
 PDFJS.SimpleLinkService = pdfjsWebPDFLinkService.SimpleLinkService;
diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js
index b4f9e3b38..c32065ff6 100644
--- a/web/pdf_viewer.js
+++ b/web/pdf_viewer.js
@@ -13,748 +13,45 @@
  * limitations under the License.
  */
 
-import { createPromiseCapability, PDFJS } from 'pdfjs-lib';
-import {
-  CSS_UNITS, DEFAULT_SCALE, DEFAULT_SCALE_VALUE, getVisibleElements,
-  isValidRotation, MAX_AUTO_SCALE, NullL10n, RendererType, SCROLLBAR_PADDING,
-  scrollIntoView, UNKNOWN_SCALE, VERTICAL_PADDING, watchScroll
-} from './ui_utils';
-import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue';
-import { AnnotationLayerBuilder } from './annotation_layer_builder';
-import { getGlobalEventBus } from './dom_events';
-import { PDFPageView } from './pdf_page_view';
-import { SimpleLinkService } from './pdf_link_service';
-import { TextLayerBuilder } from './text_layer_builder';
+import { getVisibleElements, scrollIntoView } from './ui_utils';
+import { BaseViewer } from './base_viewer';
+import { shadow } from 'pdfjs-lib';
 
-const PresentationModeState = {
-  UNKNOWN: 0,
-  NORMAL: 1,
-  CHANGING: 2,
-  FULLSCREEN: 3,
-};
-
-const DEFAULT_CACHE_SIZE = 10;
-
-/**
- * @typedef {Object} PDFViewerOptions
- * @property {HTMLDivElement} container - The container for the viewer element.
- * @property {HTMLDivElement} viewer - (optional) The viewer element.
- * @property {EventBus} eventBus - The application event bus.
- * @property {IPDFLinkService} linkService - The navigation/linking service.
- * @property {DownloadManager} downloadManager - (optional) The download
- *   manager component.
- * @property {PDFRenderingQueue} renderingQueue - (optional) The rendering
- *   queue object.
- * @property {boolean} removePageBorders - (optional) Removes the border shadow
- *   around the pages. The default is false.
- * @property {boolean} enhanceTextSelection - (optional) Enables the improved
- *   text selection behaviour. The default is `false`.
- * @property {boolean} renderInteractiveForms - (optional) Enables rendering of
- *   interactive form elements. The default is `false`.
- * @property {boolean} enablePrintAutoRotate - (optional) Enables automatic
- *   rotation of pages whose orientation differ from the first page upon
- *   printing. The default is `false`.
- * @property {string} renderer - 'canvas' or 'svg'. The default is 'canvas'.
- * @property {IL10n} l10n - Localization service.
- */
-
-function PDFPageViewBuffer(size) {
-  let data = [];
-  this.push = function cachePush(view) {
-    let i = data.indexOf(view);
-    if (i >= 0) {
-      data.splice(i, 1);
-    }
-    data.push(view);
-    if (data.length > size) {
-      data.shift().destroy();
-    }
-  };
-  this.resize = function (newSize) {
-    size = newSize;
-    while (data.length > size) {
-      data.shift().destroy();
-    }
-  };
-}
-
-function isSameScale(oldScale, newScale) {
-  if (newScale === oldScale) {
-    return true;
-  }
-  if (Math.abs(newScale - oldScale) < 1e-15) {
-    // Prevent unnecessary re-rendering of all pages when the scale
-    // changes only because of limited numerical precision.
-    return true;
-  }
-  return false;
-}
-
-function isPortraitOrientation(size) {
-  return size.width <= size.height;
-}
-
-/**
- * Simple viewer control to display PDF content/pages.
- * @implements {IRenderableView}
- */
-class PDFViewer {
-  /**
-   * @param {PDFViewerOptions} options
-   */
-  constructor(options) {
-    this.container = options.container;
-    this.viewer = options.viewer || options.container.firstElementChild;
-    this.eventBus = options.eventBus || getGlobalEventBus();
-    this.linkService = options.linkService || new SimpleLinkService();
-    this.downloadManager = options.downloadManager || null;
-    this.removePageBorders = options.removePageBorders || false;
-    this.enhanceTextSelection = options.enhanceTextSelection || false;
-    this.renderInteractiveForms = options.renderInteractiveForms || false;
-    this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
-    this.renderer = options.renderer || RendererType.CANVAS;
-    this.l10n = options.l10n || NullL10n;
-
-    this.defaultRenderingQueue = !options.renderingQueue;
-    if (this.defaultRenderingQueue) {
-      // Custom rendering queue is not specified, using default one
-      this.renderingQueue = new PDFRenderingQueue();
-      this.renderingQueue.setViewer(this);
-    } else {
-      this.renderingQueue = options.renderingQueue;
-    }
-
-    this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this));
-    this.presentationModeState = PresentationModeState.UNKNOWN;
-    this._resetView();
-
-    if (this.removePageBorders) {
-      this.viewer.classList.add('removePageBorders');
-    }
-  }
-
-  get pagesCount() {
-    return this._pages.length;
-  }
-
-  getPageView(index) {
-    return this._pages[index];
-  }
-
-  /**
-   * @returns {boolean} true if all {PDFPageView} objects are initialized.
-   */
-  get pageViewsReady() {
-    return this._pageViewsReady;
-  }
-
-  /**
-   * @returns {number}
-   */
-  get currentPageNumber() {
-    return this._currentPageNumber;
-  }
-
-  /**
-   * @param {number} val - The page number.
-   */
-  set currentPageNumber(val) {
-    if (!Number.isInteger(val)) {
-      throw new Error('Invalid page number.');
-    }
-    if (!this.pdfDocument) {
-      return;
-    }
-    // The intent can be to just reset a scroll position and/or scale.
-    this._setCurrentPageNumber(val, /* resetCurrentPageView = */ true);
-  }
-
-  /**
-   * @private
-   */
-  _setCurrentPageNumber(val, resetCurrentPageView = false) {
-    if (this._currentPageNumber === val) {
-      if (resetCurrentPageView) {
-        this._resetCurrentPageView();
-      }
-      return;
-    }
-
-    if (!(0 < val && val <= this.pagesCount)) {
-      console.error(
-        `PDFViewer._setCurrentPageNumber: "${val}" is out of bounds.`);
-      return;
-    }
-
-    let arg = {
-      source: this,
-      pageNumber: val,
-      pageLabel: this._pageLabels && this._pageLabels[val - 1],
-    };
-    this._currentPageNumber = val;
-    this.eventBus.dispatch('pagechanging', arg);
-    this.eventBus.dispatch('pagechange', arg);
-
-    if (resetCurrentPageView) {
-      this._resetCurrentPageView();
-    }
-  }
-
-  /**
-   * @returns {string|null} Returns the current page label,
-   *                        or `null` if no page labels exist.
-   */
-  get currentPageLabel() {
-    return this._pageLabels && this._pageLabels[this._currentPageNumber - 1];
-  }
-
-  /**
-   * @param {string} val - The page label.
-   */
-  set currentPageLabel(val) {
-    let pageNumber = val | 0; // Fallback page number.
-    if (this._pageLabels) {
-      let i = this._pageLabels.indexOf(val);
-      if (i >= 0) {
-        pageNumber = i + 1;
-      }
-    }
-    this.currentPageNumber = pageNumber;
-  }
-
-  /**
-   * @returns {number}
-   */
-  get currentScale() {
-    return this._currentScale !== UNKNOWN_SCALE ? this._currentScale :
-                                                  DEFAULT_SCALE;
-  }
-
-  /**
-   * @param {number} val - Scale of the pages in percents.
-   */
-  set currentScale(val) {
-    if (isNaN(val)) {
-      throw new Error('Invalid numeric scale');
-    }
-    if (!this.pdfDocument) {
-      return;
-    }
-    this._setScale(val, false);
-  }
-
-  /**
-   * @returns {string}
-   */
-  get currentScaleValue() {
-    return this._currentScaleValue;
-  }
-
-  /**
-   * @param val - The scale of the pages (in percent or predefined value).
-   */
-  set currentScaleValue(val) {
-    if (!this.pdfDocument) {
-      return;
-    }
-    this._setScale(val, false);
-  }
-
-  /**
-   * @returns {number}
-   */
-  get pagesRotation() {
-    return this._pagesRotation;
-  }
-
-  /**
-   * @param {number} rotation - The rotation of the pages (0, 90, 180, 270).
-   */
-  set pagesRotation(rotation) {
-    if (!isValidRotation(rotation)) {
-      throw new Error('Invalid pages rotation angle.');
-    }
-    if (!this.pdfDocument) {
-      return;
-    }
-    if (this._pagesRotation === rotation) {
-      return; // The rotation didn't change.
-    }
-    this._pagesRotation = rotation;
-
-    let pageNumber = this._currentPageNumber;
-
-    for (let i = 0, ii = this._pages.length; i < ii; i++) {
-      let pageView = this._pages[i];
-      pageView.update(pageView.scale, rotation);
-    }
-    // Prevent errors in case the rotation changes *before* the scale has been
-    // set to a non-default value.
-    if (this._currentScaleValue) {
-      this._setScale(this._currentScaleValue, true);
-    }
-
-    this.eventBus.dispatch('rotationchanging', {
-      source: this,
-      pagesRotation: rotation,
-      pageNumber,
-    });
-
-    if (this.defaultRenderingQueue) {
-      this.update();
-    }
-  }
-
-  /**
-   * @param pdfDocument {PDFDocument}
-   */
-  setDocument(pdfDocument) {
-    if (this.pdfDocument) {
-      this._cancelRendering();
-      this._resetView();
-    }
-
-    this.pdfDocument = pdfDocument;
-    if (!pdfDocument) {
-      return;
-    }
-    let pagesCount = pdfDocument.numPages;
-
-    let pagesCapability = createPromiseCapability();
-    this.pagesPromise = pagesCapability.promise;
-
-    pagesCapability.promise.then(() => {
-      this._pageViewsReady = true;
-      this.eventBus.dispatch('pagesloaded', {
-        source: this,
-        pagesCount,
-      });
-    });
-
-    let isOnePageRenderedResolved = false;
-    let onePageRenderedCapability = createPromiseCapability();
-    this.onePageRendered = onePageRenderedCapability.promise;
-
-    let bindOnAfterAndBeforeDraw = (pageView) => {
-      pageView.onBeforeDraw = () => {
-        // Add the page to the buffer at the start of drawing. That way it can
-        // be evicted from the buffer and destroyed even if we pause its
-        // rendering.
-        this._buffer.push(pageView);
-      };
-      pageView.onAfterDraw = () => {
-        if (!isOnePageRenderedResolved) {
-          isOnePageRenderedResolved = true;
-          onePageRenderedCapability.resolve();
-        }
-      };
-    };
-
-    let firstPagePromise = pdfDocument.getPage(1);
-    this.firstPagePromise = firstPagePromise;
-
-    // Fetch a single page so we can get a viewport that will be the default
-    // viewport for all pages
-    firstPagePromise.then((pdfPage) => {
-      let scale = this.currentScale;
-      let viewport = pdfPage.getViewport(scale * CSS_UNITS);
-      for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
-        let textLayerFactory = null;
-        if (!PDFJS.disableTextLayer) {
-          textLayerFactory = this;
-        }
-        let pageView = new PDFPageView({
-          container: this.viewer,
-          eventBus: this.eventBus,
-          id: pageNum,
-          scale,
-          defaultViewport: viewport.clone(),
-          renderingQueue: this.renderingQueue,
-          textLayerFactory,
-          annotationLayerFactory: this,
-          enhanceTextSelection: this.enhanceTextSelection,
-          renderInteractiveForms: this.renderInteractiveForms,
-          renderer: this.renderer,
-          l10n: this.l10n,
-        });
-        bindOnAfterAndBeforeDraw(pageView);
-        this._pages.push(pageView);
-      }
-
-      // Fetch all the pages since the viewport is needed before printing
-      // starts to create the correct size canvas. Wait until one page is
-      // rendered so we don't tie up too many resources early on.
-      onePageRenderedCapability.promise.then(() => {
-        if (PDFJS.disableAutoFetch) {
-          // XXX: Printing is semi-broken with auto fetch disabled.
-          pagesCapability.resolve();
-          return;
-        }
-        let getPagesLeft = pagesCount;
-        for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
-          pdfDocument.getPage(pageNum).then((pdfPage) => {
-            let pageView = this._pages[pageNum - 1];
-            if (!pageView.pdfPage) {
-              pageView.setPdfPage(pdfPage);
-            }
-            this.linkService.cachePageRef(pageNum, pdfPage.ref);
-            if (--getPagesLeft === 0) {
-              pagesCapability.resolve();
-            }
-          }, (reason) => {
-            console.error(`Unable to get page ${pageNum} to initialize viewer`,
-                          reason);
-            if (--getPagesLeft === 0) {
-              pagesCapability.resolve();
-            }
-          });
-        }
-      });
-
-      this.eventBus.dispatch('pagesinit', { source: this, });
-
-      if (this.defaultRenderingQueue) {
-        this.update();
-      }
-
-      if (this.findController) {
-        this.findController.resolveFirstPage();
-      }
-    }).catch((reason) => {
-      console.error('Unable to initialize viewer', reason);
-    });
-  }
-
-  /**
-   * @param {Array|null} labels
-   */
-  setPageLabels(labels) {
-    if (!this.pdfDocument) {
-      return;
-    }
-    if (!labels) {
-      this._pageLabels = null;
-    } else if (!(labels instanceof Array &&
-                 this.pdfDocument.numPages === labels.length)) {
-      this._pageLabels = null;
-      console.error('PDFViewer.setPageLabels: Invalid page labels.');
-    } else {
-      this._pageLabels = labels;
-    }
-    // Update all the `PDFPageView` instances.
-    for (let i = 0, ii = this._pages.length; i < ii; i++) {
-      let pageView = this._pages[i];
-      let label = this._pageLabels && this._pageLabels[i];
-      pageView.setPageLabel(label);
-    }
-  }
-
-  _resetView() {
-    this._pages = [];
-    this._currentPageNumber = 1;
-    this._currentScale = UNKNOWN_SCALE;
-    this._currentScaleValue = null;
-    this._pageLabels = null;
-    this._buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE);
-    this._location = null;
-    this._pagesRotation = 0;
-    this._pagesRequests = [];
-    this._pageViewsReady = false;
-
-    // Remove the pages from the DOM.
-    this.viewer.textContent = '';
-  }
-
-  _scrollUpdate() {
-    if (this.pagesCount === 0) {
-      return;
-    }
-    this.update();
-  }
-
-  _setScaleDispatchEvent(newScale, newValue, preset = false) {
-    let arg = {
-      source: this,
-      scale: newScale,
-      presetValue: preset ? newValue : undefined,
-    };
-    this.eventBus.dispatch('scalechanging', arg);
-    this.eventBus.dispatch('scalechange', arg);
+class PDFViewer extends BaseViewer {
+  get _setDocumentViewerElement() {
+    return shadow(this, '_setDocumentViewerElement', this.viewer);
   }
 
-  _setScaleUpdatePages(newScale, newValue, noScroll = false, preset = false) {
-    this._currentScaleValue = newValue.toString();
-
-    if (isSameScale(this._currentScale, newScale)) {
-      if (preset) {
-        this._setScaleDispatchEvent(newScale, newValue, true);
-      }
-      return;
-    }
-
-    for (let i = 0, ii = this._pages.length; i < ii; i++) {
-      this._pages[i].update(newScale);
-    }
-    this._currentScale = newScale;
-
-    if (!noScroll) {
-      let page = this._currentPageNumber, dest;
-      if (this._location && !PDFJS.ignoreCurrentPositionOnZoom &&
-          !(this.isInPresentationMode || this.isChangingPresentationMode)) {
-        page = this._location.pageNumber;
-        dest = [null, { name: 'XYZ', }, this._location.left,
-                this._location.top, null];
-      }
-      this.scrollPageIntoView({
-        pageNumber: page,
-        destArray: dest,
-        allowNegativeOffset: true,
-      });
-    }
-
-    this._setScaleDispatchEvent(newScale, newValue, preset);
-
-    if (this.defaultRenderingQueue) {
-      this.update();
-    }
-  }
-
-  _setScale(value, noScroll = false) {
-    let scale = parseFloat(value);
-
-    if (scale > 0) {
-      this._setScaleUpdatePages(scale, value, noScroll, /* preset = */ false);
-    } else {
-      let currentPage = this._pages[this._currentPageNumber - 1];
-      if (!currentPage) {
-        return;
-      }
-      let hPadding = (this.isInPresentationMode || this.removePageBorders) ?
-        0 : SCROLLBAR_PADDING;
-      let vPadding = (this.isInPresentationMode || this.removePageBorders) ?
-        0 : VERTICAL_PADDING;
-      let pageWidthScale = (this.container.clientWidth - hPadding) /
-                           currentPage.width * currentPage.scale;
-      let pageHeightScale = (this.container.clientHeight - vPadding) /
-                            currentPage.height * currentPage.scale;
-      switch (value) {
-        case 'page-actual':
-          scale = 1;
-          break;
-        case 'page-width':
-          scale = pageWidthScale;
-          break;
-        case 'page-height':
-          scale = pageHeightScale;
-          break;
-        case 'page-fit':
-          scale = Math.min(pageWidthScale, pageHeightScale);
-          break;
-        case 'auto':
-          let isLandscape = (currentPage.width > currentPage.height);
-          // For pages in landscape mode, fit the page height to the viewer
-          // *unless* the page would thus become too wide to fit horizontally.
-          let horizontalScale = isLandscape ?
-            Math.min(pageHeightScale, pageWidthScale) : pageWidthScale;
-          scale = Math.min(MAX_AUTO_SCALE, horizontalScale);
-          break;
-        default:
-          console.error(
-            `PDFViewer._setScale: "${value}" is an unknown zoom value.`);
-          return;
-      }
-      this._setScaleUpdatePages(scale, value, noScroll, /* preset = */ true);
-    }
-  }
-
-  /**
-   * Refreshes page view: scrolls to the current page and updates the scale.
-   * @private
-   */
-  _resetCurrentPageView() {
-    if (this.isInPresentationMode) {
-      // Fixes the case when PDF has different page sizes.
-      this._setScale(this._currentScaleValue, true);
-    }
-
-    let pageView = this._pages[this._currentPageNumber - 1];
-    scrollIntoView(pageView.div);
+  _scrollIntoView({ pageDiv, pageSpot = null, }) {
+    scrollIntoView(pageDiv, pageSpot);
   }
 
-  /**
-   * @typedef ScrollPageIntoViewParameters
-   * @property {number} pageNumber - The page number.
-   * @property {Array} destArray - (optional) The original PDF destination
-   *   array, in the format: <page-ref> </XYZ|/FitXXX> <args..>
-   * @property {boolean} allowNegativeOffset - (optional) Allow negative page
-   *   offsets. The default value is `false`.
-   */
-
-  /**
-   * Scrolls page into view.
-   * @param {ScrollPageIntoViewParameters} params
-   */
-  scrollPageIntoView(params) {
-    if ((typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) &&
-        (arguments.length > 1 || typeof params === 'number')) {
-      console.error('Call of scrollPageIntoView() with obsolete signature.');
-      return;
-    }
-    if (!this.pdfDocument) {
-      return;
-    }
-    let pageNumber = params.pageNumber || 0;
-    let dest = params.destArray || null;
-    let allowNegativeOffset = params.allowNegativeOffset || false;
-
-    if (this.isInPresentationMode || !dest) {
-      this._setCurrentPageNumber(pageNumber, /* resetCurrentPageView = */ true);
-      return;
-    }
-
-    let pageView = this._pages[pageNumber - 1];
-    if (!pageView) {
-      console.error(
-        'PDFViewer.scrollPageIntoView: Invalid "pageNumber" parameter.');
-      return;
-    }
-    let x = 0, y = 0;
-    let width = 0, height = 0, widthScale, heightScale;
-    let changeOrientation = (pageView.rotation % 180 === 0 ? false : true);
-    let pageWidth = (changeOrientation ? pageView.height : pageView.width) /
-      pageView.scale / CSS_UNITS;
-    let pageHeight = (changeOrientation ? pageView.width : pageView.height) /
-      pageView.scale / CSS_UNITS;
-    let scale = 0;
-    switch (dest[1].name) {
-      case 'XYZ':
-        x = dest[2];
-        y = dest[3];
-        scale = dest[4];
-        // If x and/or y coordinates are not supplied, default to
-        // _top_ left of the page (not the obvious bottom left,
-        // since aligning the bottom of the intended page with the
-        // top of the window is rarely helpful).
-        x = x !== null ? x : 0;
-        y = y !== null ? y : pageHeight;
-        break;
-      case 'Fit':
-      case 'FitB':
-        scale = 'page-fit';
-        break;
-      case 'FitH':
-      case 'FitBH':
-        y = dest[2];
-        scale = 'page-width';
-        // According to the PDF spec, section 12.3.2.2, a `null` value in the
-        // parameter should maintain the position relative to the new page.
-        if (y === null && this._location) {
-          x = this._location.left;
-          y = this._location.top;
-        }
-        break;
-      case 'FitV':
-      case 'FitBV':
-        x = dest[2];
-        width = pageWidth;
-        height = pageHeight;
-        scale = 'page-height';
-        break;
-      case 'FitR':
-        x = dest[2];
-        y = dest[3];
-        width = dest[4] - x;
-        height = dest[5] - y;
-        let hPadding = this.removePageBorders ? 0 : SCROLLBAR_PADDING;
-        let vPadding = this.removePageBorders ? 0 : VERTICAL_PADDING;
-
-        widthScale = (this.container.clientWidth - hPadding) /
-          width / CSS_UNITS;
-        heightScale = (this.container.clientHeight - vPadding) /
-          height / CSS_UNITS;
-        scale = Math.min(Math.abs(widthScale), Math.abs(heightScale));
-        break;
-      default:
-        console.error(`PDFViewer.scrollPageIntoView: "${dest[1].name}" ` +
-                      'is not a valid destination type.');
-        return;
-    }
-
-    if (scale && scale !== this._currentScale) {
-      this.currentScaleValue = scale;
-    } else if (this._currentScale === UNKNOWN_SCALE) {
-      this.currentScaleValue = DEFAULT_SCALE_VALUE;
-    }
-
-    if (scale === 'page-fit' && !dest[4]) {
-      scrollIntoView(pageView.div);
-      return;
-    }
-
-    let boundingRect = [
-      pageView.viewport.convertToViewportPoint(x, y),
-      pageView.viewport.convertToViewportPoint(x + width, y + height)
-    ];
-    let left = Math.min(boundingRect[0][0], boundingRect[1][0]);
-    let top = Math.min(boundingRect[0][1], boundingRect[1][1]);
-
-    if (!allowNegativeOffset) {
-      // Some bad PDF generators will create destinations with e.g. top values
-      // that exceeds the page height. Ensure that offsets are not negative,
-      // to prevent a previous page from becoming visible (fixes bug 874482).
-      left = Math.max(left, 0);
-      top = Math.max(top, 0);
+  _getVisiblePages() {
+    if (!this.isInPresentationMode) {
+      return getVisibleElements(this.container, this._pages, true);
     }
-    scrollIntoView(pageView.div, { left, top, });
-  }
-
-  _updateLocation(firstPage) {
-    let currentScale = this._currentScale;
-    let currentScaleValue = this._currentScaleValue;
-    let normalizedScaleValue =
-      parseFloat(currentScaleValue) === currentScale ?
-      Math.round(currentScale * 10000) / 100 : currentScaleValue;
-
-    let pageNumber = firstPage.id;
-    let pdfOpenParams = '#page=' + pageNumber;
-    pdfOpenParams += '&zoom=' + normalizedScaleValue;
-    let currentPageView = this._pages[pageNumber - 1];
-    let container = this.container;
-    let topLeft = currentPageView.getPagePoint(
-      (container.scrollLeft - firstPage.x),
-      (container.scrollTop - firstPage.y));
-    let intLeft = Math.round(topLeft[0]);
-    let intTop = Math.round(topLeft[1]);
-    pdfOpenParams += ',' + intLeft + ',' + intTop;
-
-    this._location = {
-      pageNumber,
-      scale: normalizedScaleValue,
-      top: intTop,
-      left: intLeft,
-      rotation: this._pagesRotation,
-      pdfOpenParams,
-    };
+    // The algorithm in getVisibleElements doesn't work in all browsers and
+    // configurations when presentation mode is active.
+    let currentPage = this._pages[this._currentPageNumber - 1];
+    let visible = [{ id: currentPage.id, view: currentPage, }];
+    return { first: currentPage, last: currentPage, views: visible, };
   }
 
   update() {
     let visible = this._getVisiblePages();
-    let visiblePages = visible.views;
-    if (visiblePages.length === 0) {
+    let visiblePages = visible.views, numVisiblePages = visiblePages.length;
+
+    if (numVisiblePages === 0) {
       return;
     }
-
-    let suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE,
-                                      2 * visiblePages.length + 1);
-    this._buffer.resize(suggestedCacheSize);
+    this._resizeBuffer(numVisiblePages);
 
     this.renderingQueue.renderHighestPriority(visible);
 
     let currentId = this._currentPageNumber;
-    let firstPage = visible.first;
     let stillFullyVisible = false;
 
-    for (let i = 0, ii = visiblePages.length; i < ii; ++i) {
+    for (let i = 0; i < numVisiblePages; ++i) {
       let page = visiblePages[i];
 
       if (page.percent < 100) {
@@ -769,211 +66,18 @@ class PDFViewer {
     if (!stillFullyVisible) {
       currentId = visiblePages[0].id;
     }
-
     if (!this.isInPresentationMode) {
       this._setCurrentPageNumber(currentId);
     }
 
-    this._updateLocation(firstPage);
-
+    this._updateLocation(visible.first);
     this.eventBus.dispatch('updateviewarea', {
       source: this,
       location: this._location,
     });
   }
-
-  containsElement(element) {
-    return this.container.contains(element);
-  }
-
-  focus() {
-    this.container.focus();
-  }
-
-  get isInPresentationMode() {
-    return this.presentationModeState === PresentationModeState.FULLSCREEN;
-  }
-
-  get isChangingPresentationMode() {
-    return this.presentationModeState === PresentationModeState.CHANGING;
-  }
-
-  get isHorizontalScrollbarEnabled() {
-    return (this.isInPresentationMode ?
-      false : (this.container.scrollWidth > this.container.clientWidth));
-  }
-
-  _getVisiblePages() {
-    if (!this.isInPresentationMode) {
-      return getVisibleElements(this.container, this._pages, true);
-    }
-    // The algorithm in getVisibleElements doesn't work in all browsers and
-    // configurations when presentation mode is active.
-    let visible = [];
-    let currentPage = this._pages[this._currentPageNumber - 1];
-    visible.push({ id: currentPage.id, view: currentPage, });
-    return { first: currentPage, last: currentPage, views: visible, };
-  }
-
-  cleanup() {
-    for (let i = 0, ii = this._pages.length; i < ii; i++) {
-      if (this._pages[i] &&
-          this._pages[i].renderingState !== RenderingStates.FINISHED) {
-        this._pages[i].reset();
-      }
-    }
-  }
-
-  /**
-   * @private
-   */
-  _cancelRendering() {
-    for (let i = 0, ii = this._pages.length; i < ii; i++) {
-      if (this._pages[i]) {
-        this._pages[i].cancelRendering();
-      }
-    }
-  }
-
-  /**
-   * @param {PDFPageView} pageView
-   * @returns {Promise} Returns a promise containing a {PDFPageProxy} object.
-   * @private
-   */
-  _ensurePdfPageLoaded(pageView) {
-    if (pageView.pdfPage) {
-      return Promise.resolve(pageView.pdfPage);
-    }
-    let pageNumber = pageView.id;
-    if (this._pagesRequests[pageNumber]) {
-      return this._pagesRequests[pageNumber];
-    }
-    let promise = this.pdfDocument.getPage(pageNumber).then((pdfPage) => {
-      if (!pageView.pdfPage) {
-        pageView.setPdfPage(pdfPage);
-      }
-      this._pagesRequests[pageNumber] = null;
-      return pdfPage;
-    }).catch((reason) => {
-      console.error('Unable to get page for page view', reason);
-      // Page error -- there is nothing can be done.
-      this._pagesRequests[pageNumber] = null;
-    });
-    this._pagesRequests[pageNumber] = promise;
-    return promise;
-  }
-
-  forceRendering(currentlyVisiblePages) {
-    let visiblePages = currentlyVisiblePages || this._getVisiblePages();
-    let pageView = this.renderingQueue.getHighestPriority(visiblePages,
-                                                          this._pages,
-                                                          this.scroll.down);
-    if (pageView) {
-      this._ensurePdfPageLoaded(pageView).then(() => {
-        this.renderingQueue.renderView(pageView);
-      });
-      return true;
-    }
-    return false;
-  }
-
-  getPageTextContent(pageIndex) {
-    return this.pdfDocument.getPage(pageIndex + 1).then(function(page) {
-      return page.getTextContent({
-        normalizeWhitespace: true,
-      });
-    });
-  }
-
-  /**
-   * @param {HTMLDivElement} textLayerDiv
-   * @param {number} pageIndex
-   * @param {PageViewport} viewport
-   * @returns {TextLayerBuilder}
-   */
-  createTextLayerBuilder(textLayerDiv, pageIndex, viewport,
-                         enhanceTextSelection = false) {
-    return new TextLayerBuilder({
-      textLayerDiv,
-      eventBus: this.eventBus,
-      pageIndex,
-      viewport,
-      findController: this.isInPresentationMode ? null : this.findController,
-      enhanceTextSelection: this.isInPresentationMode ? false :
-                                                        enhanceTextSelection,
-    });
-  }
-
-  /**
-   * @param {HTMLDivElement} pageDiv
-   * @param {PDFPage} pdfPage
-   * @param {boolean} renderInteractiveForms
-   * @param {IL10n} l10n
-   * @returns {AnnotationLayerBuilder}
-   */
-  createAnnotationLayerBuilder(pageDiv, pdfPage, renderInteractiveForms = false,
-                               l10n = NullL10n) {
-    return new AnnotationLayerBuilder({
-      pageDiv,
-      pdfPage,
-      renderInteractiveForms,
-      linkService: this.linkService,
-      downloadManager: this.downloadManager,
-      l10n,
-    });
-  }
-
-  setFindController(findController) {
-    this.findController = findController;
-  }
-
-  /**
-   * @returns {boolean} Whether all pages of the PDF document have identical
-   *                    widths and heights.
-   */
-  get hasEqualPageSizes() {
-    let firstPageView = this._pages[0];
-    for (let i = 1, ii = this._pages.length; i < ii; ++i) {
-      let pageView = this._pages[i];
-      if (pageView.width !== firstPageView.width ||
-          pageView.height !== firstPageView.height) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  /**
-   * Returns sizes of the pages.
-   * @returns {Array} Array of objects with width/height/rotation fields.
-   */
-  getPagesOverview() {
-    let pagesOverview = this._pages.map(function(pageView) {
-      let viewport = pageView.pdfPage.getViewport(1);
-      return {
-        width: viewport.width,
-        height: viewport.height,
-        rotation: viewport.rotation,
-      };
-    });
-    if (!this.enablePrintAutoRotate) {
-      return pagesOverview;
-    }
-    let isFirstPagePortrait = isPortraitOrientation(pagesOverview[0]);
-    return pagesOverview.map(function (size) {
-      if (isFirstPagePortrait === isPortraitOrientation(size)) {
-        return size;
-      }
-      return {
-        width: size.height,
-        height: size.width,
-        rotation: (size.rotation + 90) % 360,
-      };
-    });
-  }
 }
 
 export {
-  PresentationModeState,
   PDFViewer,
 };
diff --git a/web/ui_utils.js b/web/ui_utils.js
index aec8a5793..394e1e625 100644
--- a/web/ui_utils.js
+++ b/web/ui_utils.js
@@ -25,6 +25,13 @@ const MAX_AUTO_SCALE = 1.25;
 const SCROLLBAR_PADDING = 40;
 const VERTICAL_PADDING = 5;
 
+const PresentationModeState = {
+  UNKNOWN: 0,
+  NORMAL: 1,
+  CHANGING: 2,
+  FULLSCREEN: 3,
+};
+
 const RendererType = {
   CANVAS: 'canvas',
   SVG: 'svg',
@@ -661,6 +668,7 @@ export {
   VERTICAL_PADDING,
   isValidRotation,
   cloneObj,
+  PresentationModeState,
   RendererType,
   mozL10n,
   NullL10n,