浏览代码

pdf viewing does not work so far

bscheibel 4 年之前
父节点
当前提交
16282fa887

+ 75 - 0
app/static/css/text_layer_builder.css

@@ -0,0 +1,75 @@
+/* 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.
+ */
+
+.textLayer {
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  overflow: hidden;
+  opacity: 0.2;
+  line-height: 1.0;
+}
+
+.textLayer > span {
+  color: transparent;
+  position: absolute;
+  white-space: pre;
+  cursor: text;
+  transform-origin: 0% 0%;
+}
+
+.textLayer .highlight {
+  margin: -1px;
+  padding: 1px;
+
+  background-color: rgb(180, 0, 170);
+  border-radius: 4px;
+}
+
+.textLayer .highlight.begin {
+  border-radius: 4px 0px 0px 4px;
+}
+
+.textLayer .highlight.end {
+  border-radius: 0px 4px 4px 0px;
+}
+
+.textLayer .highlight.middle {
+  border-radius: 0px;
+}
+
+.textLayer .highlight.selected {
+  background-color: rgb(0, 100, 0);
+}
+
+.textLayer ::selection { background: rgb(0,0,255); }
+
+.textLayer .endOfContent {
+  display: block;
+  position: absolute;
+  left: 0px;
+  top: 100%;
+  right: 0px;
+  bottom: 0px;
+  z-index: -1;
+  cursor: default;
+  user-select: none;
+}
+
+.textLayer .endOfContent.active {
+  top: 0px;
+}

+ 118 - 0
app/static/js/pdf.js

@@ -0,0 +1,118 @@
+/* Copyright 2012 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.
+ */
+/* eslint-disable no-unused-vars */
+
+'use strict';
+
+var pdfjsVersion =
+  typeof PDFJSDev !== 'undefined' ? PDFJSDev.eval('BUNDLE_VERSION') : void 0;
+var pdfjsBuild =
+  typeof PDFJSDev !== 'undefined' ? PDFJSDev.eval('BUNDLE_BUILD') : void 0;
+var pdfjsSharedUtil = require('./shared/util.js');
+var pdfjsDisplayAPI = require('./display/api.js');
+var pdfjsDisplayTextLayer = require('./display/text_layer.js');
+var pdfjsDisplayAnnotationLayer = require('./display/annotation_layer.js');
+var pdfjsDisplayDisplayUtils = require('./display/display_utils.js');
+var pdfjsDisplaySVG = require('./display/svg.js');
+let pdfjsDisplayWorkerOptions = require('./display/worker_options.js');
+let pdfjsDisplayAPICompatibility = require('./display/api_compatibility.js');
+
+if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) {
+  const isNodeJS = require('./shared/is_node.js');
+  if (isNodeJS()) {
+    let PDFNodeStream = require('./display/node_stream.js').PDFNodeStream;
+    pdfjsDisplayAPI.setPDFNetworkStreamFactory((params) => {
+      return new PDFNodeStream(params);
+    });
+  } else {
+    let PDFNetworkStream = require('./display/network.js').PDFNetworkStream;
+    let PDFFetchStream;
+    if (pdfjsDisplayDisplayUtils.isFetchSupported()) {
+      PDFFetchStream = require('./display/fetch_stream.js').PDFFetchStream;
+    }
+    pdfjsDisplayAPI.setPDFNetworkStreamFactory((params) => {
+      if (PDFFetchStream &&
+          pdfjsDisplayDisplayUtils.isValidFetchUrl(params.url)) {
+        return new PDFFetchStream(params);
+      }
+      return new PDFNetworkStream(params);
+    });
+  }
+} else if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME')) {
+  let PDFNetworkStream = require('./display/network.js').PDFNetworkStream;
+  let PDFFetchStream;
+  let isChromeWithFetchCredentials = function() {
+    // fetch does not include credentials until Chrome 61.0.3138.0 and later.
+    // https://chromium.googlesource.com/chromium/src/+/2e231cf052ca5e68e22baf0008ac9e5e29121707
+    try {
+      // Indexed properties on window are read-only in Chrome 61.0.3151.0+
+      // https://chromium.googlesource.com/chromium/src.git/+/58ab4a971b06dec13e4edf9de8382ca6847f6190
+      window[999] = 123; // should throw. Note: JS strict mode MUST be enabled.
+      delete window[999];
+      return false;
+    } catch (e) {
+      return true;
+    }
+  };
+  if (pdfjsDisplayDisplayUtils.isFetchSupported() &&
+      isChromeWithFetchCredentials()) {
+    PDFFetchStream = require('./display/fetch_stream.js').PDFFetchStream;
+  }
+  pdfjsDisplayAPI.setPDFNetworkStreamFactory((params) => {
+    if (PDFFetchStream &&
+        pdfjsDisplayDisplayUtils.isValidFetchUrl(params.url)) {
+      return new PDFFetchStream(params);
+    }
+    return new PDFNetworkStream(params);
+  });
+}
+
+exports.build = pdfjsDisplayAPI.build;
+exports.version = pdfjsDisplayAPI.version;
+exports.getDocument = pdfjsDisplayAPI.getDocument;
+exports.LoopbackPort = pdfjsDisplayAPI.LoopbackPort;
+exports.PDFDataRangeTransport = pdfjsDisplayAPI.PDFDataRangeTransport;
+exports.PDFWorker = pdfjsDisplayAPI.PDFWorker;
+exports.renderTextLayer = pdfjsDisplayTextLayer.renderTextLayer;
+exports.AnnotationLayer = pdfjsDisplayAnnotationLayer.AnnotationLayer;
+exports.createPromiseCapability = pdfjsSharedUtil.createPromiseCapability;
+exports.PasswordResponses = pdfjsSharedUtil.PasswordResponses;
+exports.InvalidPDFException = pdfjsSharedUtil.InvalidPDFException;
+exports.MissingPDFException = pdfjsSharedUtil.MissingPDFException;
+exports.SVGGraphics = pdfjsDisplaySVG.SVGGraphics;
+exports.NativeImageDecoding = pdfjsSharedUtil.NativeImageDecoding;
+exports.CMapCompressionType = pdfjsSharedUtil.CMapCompressionType;
+exports.PermissionFlag = pdfjsSharedUtil.PermissionFlag;
+exports.UnexpectedResponseException =
+  pdfjsSharedUtil.UnexpectedResponseException;
+exports.OPS = pdfjsSharedUtil.OPS;
+exports.VerbosityLevel = pdfjsSharedUtil.VerbosityLevel;
+exports.UNSUPPORTED_FEATURES = pdfjsSharedUtil.UNSUPPORTED_FEATURES;
+exports.createValidAbsoluteUrl = pdfjsSharedUtil.createValidAbsoluteUrl;
+exports.createObjectURL = pdfjsSharedUtil.createObjectURL;
+exports.removeNullCharacters = pdfjsSharedUtil.removeNullCharacters;
+exports.shadow = pdfjsSharedUtil.shadow;
+exports.Util = pdfjsSharedUtil.Util;
+exports.ReadableStream = pdfjsSharedUtil.ReadableStream;
+exports.RenderingCancelledException =
+  pdfjsDisplayDisplayUtils.RenderingCancelledException;
+exports.getFilenameFromUrl = pdfjsDisplayDisplayUtils.getFilenameFromUrl;
+exports.LinkTarget = pdfjsDisplayDisplayUtils.LinkTarget;
+exports.addLinkAttributes = pdfjsDisplayDisplayUtils.addLinkAttributes;
+exports.loadScript = pdfjsDisplayDisplayUtils.loadScript;
+exports.PDFDateString = pdfjsDisplayDisplayUtils.PDFDateString;
+exports.GlobalWorkerOptions = pdfjsDisplayWorkerOptions.GlobalWorkerOptions;
+exports.apiCompatibilityParams =
+  pdfjsDisplayAPICompatibility.apiCompatibilityParams;

+ 431 - 0
app/static/js/text_layer_builder.js

@@ -0,0 +1,431 @@
+/* Copyright 2012 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 { getGlobalEventBus } from './ui_utils';
+import { renderTextLayer } from 'pdfjs-lib';
+
+const EXPAND_DIVS_TIMEOUT = 300; // ms
+
+/**
+ * @typedef {Object} TextLayerBuilderOptions
+ * @property {HTMLDivElement} textLayerDiv - The text layer container.
+ * @property {EventBus} eventBus - The application event bus.
+ * @property {number} pageIndex - The page index.
+ * @property {PageViewport} viewport - The viewport of the text layer.
+ * @property {PDFFindController} findController
+ * @property {boolean} enhanceTextSelection - Option to turn on improved
+ *   text selection.
+ */
+
+/**
+ * The text layer builder provides text selection functionality for the PDF.
+ * It does this by creating overlay divs over the PDF's text. These divs
+ * contain text that matches the PDF text they are overlaying. This object
+ * also provides a way to highlight text that is being searched for.
+ */
+class TextLayerBuilder {
+  constructor({ textLayerDiv, eventBus, pageIndex, viewport,
+                findController = null, enhanceTextSelection = false, }) {
+    this.textLayerDiv = textLayerDiv;
+    this.eventBus = eventBus || getGlobalEventBus();
+    this.textContent = null;
+    this.textContentItemsStr = [];
+    this.textContentStream = null;
+    this.renderingDone = false;
+    this.pageIdx = pageIndex;
+    this.pageNumber = this.pageIdx + 1;
+    this.matches = [];
+    this.viewport = viewport;
+    this.textDivs = [];
+    this.findController = findController;
+    this.textLayerRenderTask = null;
+    this.enhanceTextSelection = enhanceTextSelection;
+
+    this._onUpdateTextLayerMatches = null;
+    this._bindMouse();
+  }
+
+  /**
+   * @private
+   */
+  _finishRendering() {
+    this.renderingDone = true;
+
+    if (!this.enhanceTextSelection) {
+      let endOfContent = document.createElement('div');
+      endOfContent.className = 'endOfContent';
+      this.textLayerDiv.appendChild(endOfContent);
+    }
+
+    this.eventBus.dispatch('textlayerrendered', {
+      source: this,
+      pageNumber: this.pageNumber,
+      numTextDivs: this.textDivs.length,
+    });
+  }
+
+  /**
+   * Renders the text layer.
+   *
+   * @param {number} timeout - (optional) wait for a specified amount of
+   *                           milliseconds before rendering
+   */
+  render(timeout = 0) {
+    if (!(this.textContent || this.textContentStream) || this.renderingDone) {
+      return;
+    }
+    this.cancel();
+
+    this.textDivs = [];
+    let textLayerFrag = document.createDocumentFragment();
+    this.textLayerRenderTask = renderTextLayer({
+      textContent: this.textContent,
+      textContentStream: this.textContentStream,
+      container: textLayerFrag,
+      viewport: this.viewport,
+      textDivs: this.textDivs,
+      textContentItemsStr: this.textContentItemsStr,
+      timeout,
+      enhanceTextSelection: this.enhanceTextSelection,
+    });
+    this.textLayerRenderTask.promise.then(() => {
+      this.textLayerDiv.appendChild(textLayerFrag);
+      this._finishRendering();
+      this._updateMatches();
+    }, function (reason) {
+      // Cancelled or failed to render text layer; skipping errors.
+    });
+
+    if (!this._onUpdateTextLayerMatches) {
+      this._onUpdateTextLayerMatches = (evt) => {
+        if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) {
+          this._updateMatches();
+        }
+      };
+      this.eventBus.on('updatetextlayermatches',
+                       this._onUpdateTextLayerMatches);
+    }
+  }
+
+  /**
+   * Cancel rendering of the text layer.
+   */
+  cancel() {
+    if (this.textLayerRenderTask) {
+      this.textLayerRenderTask.cancel();
+      this.textLayerRenderTask = null;
+    }
+    if (this._onUpdateTextLayerMatches) {
+      this.eventBus.off('updatetextlayermatches',
+                        this._onUpdateTextLayerMatches);
+      this._onUpdateTextLayerMatches = null;
+    }
+  }
+
+  setTextContentStream(readableStream) {
+    this.cancel();
+    this.textContentStream = readableStream;
+  }
+
+  setTextContent(textContent) {
+    this.cancel();
+    this.textContent = textContent;
+  }
+
+  _convertMatches(matches, matchesLength) {
+    // Early exit if there is nothing to convert.
+    if (!matches) {
+      return [];
+    }
+    const { findController, textContentItemsStr, } = this;
+
+    let i = 0, iIndex = 0;
+    const end = textContentItemsStr.length - 1;
+    const queryLen = findController.state.query.length;
+    const result = [];
+
+    for (let m = 0, mm = matches.length; m < mm; m++) {
+      // Calculate the start position.
+      let matchIdx = matches[m];
+
+      // Loop over the divIdxs.
+      while (i !== end &&
+             matchIdx >= (iIndex + textContentItemsStr[i].length)) {
+        iIndex += textContentItemsStr[i].length;
+        i++;
+      }
+
+      if (i === textContentItemsStr.length) {
+        console.error('Could not find a matching mapping');
+      }
+
+      let match = {
+        begin: {
+          divIdx: i,
+          offset: matchIdx - iIndex,
+        },
+      };
+
+      // Calculate the end position.
+      if (matchesLength) { // Multiterm search.
+        matchIdx += matchesLength[m];
+      } else { // Phrase search.
+        matchIdx += queryLen;
+      }
+
+      // Somewhat the same array as above, but use > instead of >= to get
+      // the end position right.
+      while (i !== end &&
+             matchIdx > (iIndex + textContentItemsStr[i].length)) {
+        iIndex += textContentItemsStr[i].length;
+        i++;
+      }
+
+      match.end = {
+        divIdx: i,
+        offset: matchIdx - iIndex,
+      };
+      result.push(match);
+    }
+    return result;
+  }
+
+  _renderMatches(matches) {
+    // Early exit if there is nothing to render.
+    if (matches.length === 0) {
+      return;
+    }
+    const { findController, pageIdx, textContentItemsStr, textDivs, } = this;
+
+    const isSelectedPage = (pageIdx === findController.selected.pageIdx);
+    const selectedMatchIdx = findController.selected.matchIdx;
+    const highlightAll = findController.state.highlightAll;
+    let prevEnd = null;
+    let infinity = {
+      divIdx: -1,
+      offset: undefined,
+    };
+
+    function beginText(begin, className) {
+      let divIdx = begin.divIdx;
+      textDivs[divIdx].textContent = '';
+      appendTextToDiv(divIdx, 0, begin.offset, className);
+    }
+
+    function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
+      let div = textDivs[divIdx];
+      let content = textContentItemsStr[divIdx].substring(fromOffset, toOffset);
+      let node = document.createTextNode(content);
+      if (className) {
+        let span = document.createElement('span');
+        span.className = className;
+        span.appendChild(node);
+        div.appendChild(span);
+        return;
+      }
+      div.appendChild(node);
+    }
+
+    let i0 = selectedMatchIdx, i1 = i0 + 1;
+    if (highlightAll) {
+      i0 = 0;
+      i1 = matches.length;
+    } else if (!isSelectedPage) {
+      // Not highlighting all and this isn't the selected page, so do nothing.
+      return;
+    }
+
+    for (let i = i0; i < i1; i++) {
+      let match = matches[i];
+      let begin = match.begin;
+      let end = match.end;
+      const isSelected = (isSelectedPage && i === selectedMatchIdx);
+      const highlightSuffix = (isSelected ? ' selected' : '');
+
+      if (isSelected) { // Attempt to scroll the selected match into view.
+        findController.scrollMatchIntoView({
+          element: textDivs[begin.divIdx],
+          pageIndex: pageIdx,
+          matchIndex: selectedMatchIdx,
+        });
+      }
+
+      // Match inside new div.
+      if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
+        // If there was a previous div, then add the text at the end.
+        if (prevEnd !== null) {
+          appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
+        }
+        // Clear the divs and set the content until the starting point.
+        beginText(begin);
+      } else {
+        appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset);
+      }
+
+      if (begin.divIdx === end.divIdx) {
+        appendTextToDiv(begin.divIdx, begin.offset, end.offset,
+                        'highlight' + highlightSuffix);
+      } else {
+        appendTextToDiv(begin.divIdx, begin.offset, infinity.offset,
+                        'highlight begin' + highlightSuffix);
+        for (let n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) {
+          textDivs[n0].className = 'highlight middle' + highlightSuffix;
+        }
+        beginText(end, 'highlight end' + highlightSuffix);
+      }
+      prevEnd = end;
+    }
+
+    if (prevEnd) {
+      appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
+    }
+  }
+
+  _updateMatches() {
+    // Only show matches when all rendering is done.
+    if (!this.renderingDone) {
+      return;
+    }
+    const {
+      findController, matches, pageIdx, textContentItemsStr, textDivs,
+    } = this;
+    let clearedUntilDivIdx = -1;
+
+    // Clear all current matches.
+    for (let i = 0, ii = matches.length; i < ii; i++) {
+      let match = matches[i];
+      let begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
+      for (let n = begin, end = match.end.divIdx; n <= end; n++) {
+        let div = textDivs[n];
+        div.textContent = textContentItemsStr[n];
+        div.className = '';
+      }
+      clearedUntilDivIdx = match.end.divIdx + 1;
+    }
+
+    if (!findController || !findController.highlightMatches) {
+      return;
+    }
+    // Convert the matches on the `findController` into the match format
+    // used for the textLayer.
+    const pageMatches = findController.pageMatches[pageIdx] || null;
+    const pageMatchesLength = findController.pageMatchesLength[pageIdx] || null;
+
+    this.matches = this._convertMatches(pageMatches, pageMatchesLength);
+    this._renderMatches(this.matches);
+  }
+
+  /**
+   * Improves text selection by adding an additional div where the mouse was
+   * clicked. This reduces flickering of the content if the mouse is slowly
+   * dragged up or down.
+   *
+   * @private
+   */
+  _bindMouse() {
+    let div = this.textLayerDiv;
+    let expandDivsTimer = null;
+
+    div.addEventListener('mousedown', (evt) => {
+      if (this.enhanceTextSelection && this.textLayerRenderTask) {
+        this.textLayerRenderTask.expandTextDivs(true);
+        if ((typeof PDFJSDev === 'undefined' ||
+             !PDFJSDev.test('FIREFOX || MOZCENTRAL')) &&
+            expandDivsTimer) {
+          clearTimeout(expandDivsTimer);
+          expandDivsTimer = null;
+        }
+        return;
+      }
+
+      let end = div.querySelector('.endOfContent');
+      if (!end) {
+        return;
+      }
+      if (typeof PDFJSDev === 'undefined' ||
+          !PDFJSDev.test('FIREFOX || MOZCENTRAL')) {
+        // On non-Firefox browsers, the selection will feel better if the height
+        // of the `endOfContent` div is adjusted to start at mouse click
+        // location. This avoids flickering when the selection moves up.
+        // However it does not work when selection is started on empty space.
+        let adjustTop = evt.target !== div;
+        if (typeof PDFJSDev === 'undefined' || PDFJSDev.test('GENERIC')) {
+          adjustTop = adjustTop && window.getComputedStyle(end).
+            getPropertyValue('-moz-user-select') !== 'none';
+        }
+        if (adjustTop) {
+          let divBounds = div.getBoundingClientRect();
+          let r = Math.max(0, (evt.pageY - divBounds.top) / divBounds.height);
+          end.style.top = (r * 100).toFixed(2) + '%';
+        }
+      }
+      end.classList.add('active');
+    });
+
+    div.addEventListener('mouseup', () => {
+      if (this.enhanceTextSelection && this.textLayerRenderTask) {
+        if (typeof PDFJSDev === 'undefined' ||
+            !PDFJSDev.test('FIREFOX || MOZCENTRAL')) {
+          expandDivsTimer = setTimeout(() => {
+            if (this.textLayerRenderTask) {
+              this.textLayerRenderTask.expandTextDivs(false);
+            }
+            expandDivsTimer = null;
+          }, EXPAND_DIVS_TIMEOUT);
+        } else {
+          this.textLayerRenderTask.expandTextDivs(false);
+        }
+        return;
+      }
+
+      let end = div.querySelector('.endOfContent');
+      if (!end) {
+        return;
+      }
+      if (typeof PDFJSDev === 'undefined' ||
+          !PDFJSDev.test('FIREFOX || MOZCENTRAL')) {
+        end.style.top = '';
+      }
+      end.classList.remove('active');
+    });
+  }
+}
+
+/**
+ * @implements IPDFTextLayerFactory
+ */
+class DefaultTextLayerFactory {
+  /**
+   * @param {HTMLDivElement} textLayerDiv
+   * @param {number} pageIndex
+   * @param {PageViewport} viewport
+   * @param {boolean} enhanceTextSelection
+   * @returns {TextLayerBuilder}
+   */
+  createTextLayerBuilder(textLayerDiv, pageIndex, viewport,
+                         enhanceTextSelection = false) {
+    return new TextLayerBuilder({
+      textLayerDiv,
+      pageIndex,
+      viewport,
+      enhanceTextSelection,
+    });
+  }
+}
+
+export {
+  TextLayerBuilder,
+  DefaultTextLayerFactory,
+};

+ 919 - 0
app/static/js/ui_utils.js

@@ -0,0 +1,919 @@
+/* Copyright 2012 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.
+ */
+
+const CSS_UNITS = 96.0 / 72.0;
+const DEFAULT_SCALE_VALUE = 'auto';
+const DEFAULT_SCALE = 1.0;
+const MIN_SCALE = 0.10;
+const MAX_SCALE = 10.0;
+const UNKNOWN_SCALE = 0;
+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',
+};
+
+const TextLayerMode = {
+  DISABLE: 0,
+  ENABLE: 1,
+  ENABLE_ENHANCE: 2,
+};
+
+const ScrollMode = {
+  UNKNOWN: -1,
+  VERTICAL: 0, // Default value.
+  HORIZONTAL: 1,
+  WRAPPED: 2,
+};
+
+const SpreadMode = {
+  UNKNOWN: -1,
+  NONE: 0, // Default value.
+  ODD: 1,
+  EVEN: 2,
+};
+
+// Replaces {{arguments}} with their values.
+function formatL10nValue(text, args) {
+  if (!args) {
+    return text;
+  }
+  return text.replace(/\{\{\s*(\w+)\s*\}\}/g, (all, name) => {
+    return (name in args ? args[name] : '{{' + name + '}}');
+  });
+}
+
+/**
+ * No-op implementation of the localization service.
+ * @implements {IL10n}
+ */
+let NullL10n = {
+  async getLanguage() {
+    return 'en-us';
+  },
+
+  async getDirection() {
+    return 'ltr';
+  },
+
+  async get(property, args, fallback) {
+    return formatL10nValue(fallback, args);
+  },
+
+  async translate(element) { },
+};
+
+/**
+ * Returns scale factor for the canvas. It makes sense for the HiDPI displays.
+ * @return {Object} The object with horizontal (sx) and vertical (sy)
+                    scales. The scaled property is set to false if scaling is
+                    not required, true otherwise.
+ */
+function getOutputScale(ctx) {
+  let devicePixelRatio = window.devicePixelRatio || 1;
+  let backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
+                          ctx.mozBackingStorePixelRatio ||
+                          ctx.msBackingStorePixelRatio ||
+                          ctx.oBackingStorePixelRatio ||
+                          ctx.backingStorePixelRatio || 1;
+  let pixelRatio = devicePixelRatio / backingStoreRatio;
+  return {
+    sx: pixelRatio,
+    sy: pixelRatio,
+    scaled: pixelRatio !== 1,
+  };
+}
+
+/**
+ * Scrolls specified element into view of its parent.
+ * @param {Object} element - The element to be visible.
+ * @param {Object} spot - An object with optional top and left properties,
+ *   specifying the offset from the top left edge.
+ * @param {boolean} skipOverflowHiddenElements - Ignore elements that have
+ *   the CSS rule `overflow: hidden;` set. The default is false.
+ */
+function scrollIntoView(element, spot, skipOverflowHiddenElements = false) {
+  // Assuming offsetParent is available (it's not available when viewer is in
+  // hidden iframe or object). We have to scroll: if the offsetParent is not set
+  // producing the error. See also animationStarted.
+  let parent = element.offsetParent;
+  if (!parent) {
+    console.error('offsetParent is not set -- cannot scroll');
+    return;
+  }
+  let offsetY = element.offsetTop + element.clientTop;
+  let offsetX = element.offsetLeft + element.clientLeft;
+  while ((parent.clientHeight === parent.scrollHeight &&
+          parent.clientWidth === parent.scrollWidth) ||
+         (skipOverflowHiddenElements &&
+          getComputedStyle(parent).overflow === 'hidden')) {
+    if (parent.dataset._scaleY) {
+      offsetY /= parent.dataset._scaleY;
+      offsetX /= parent.dataset._scaleX;
+    }
+    offsetY += parent.offsetTop;
+    offsetX += parent.offsetLeft;
+    parent = parent.offsetParent;
+    if (!parent) {
+      return; // no need to scroll
+    }
+  }
+  if (spot) {
+    if (spot.top !== undefined) {
+      offsetY += spot.top;
+    }
+    if (spot.left !== undefined) {
+      offsetX += spot.left;
+      parent.scrollLeft = offsetX;
+    }
+  }
+  parent.scrollTop = offsetY;
+}
+
+/**
+ * Helper function to start monitoring the scroll event and converting them into
+ * PDF.js friendly one: with scroll debounce and scroll direction.
+ */
+function watchScroll(viewAreaElement, callback) {
+  let debounceScroll = function(evt) {
+    if (rAF) {
+      return;
+    }
+    // schedule an invocation of scroll for next animation frame.
+    rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
+      rAF = null;
+
+      let currentX = viewAreaElement.scrollLeft;
+      let lastX = state.lastX;
+      if (currentX !== lastX) {
+        state.right = currentX > lastX;
+      }
+      state.lastX = currentX;
+      let currentY = viewAreaElement.scrollTop;
+      let lastY = state.lastY;
+      if (currentY !== lastY) {
+        state.down = currentY > lastY;
+      }
+      state.lastY = currentY;
+      callback(state);
+    });
+  };
+
+  let state = {
+    right: true,
+    down: true,
+    lastX: viewAreaElement.scrollLeft,
+    lastY: viewAreaElement.scrollTop,
+    _eventHandler: debounceScroll,
+  };
+
+  let rAF = null;
+  viewAreaElement.addEventListener('scroll', debounceScroll, true);
+  return state;
+}
+
+/**
+ * Helper function to parse query string (e.g. ?param1=value&parm2=...).
+ */
+function parseQueryString(query) {
+  let parts = query.split('&');
+  let params = Object.create(null);
+  for (let i = 0, ii = parts.length; i < ii; ++i) {
+    let param = parts[i].split('=');
+    let key = param[0].toLowerCase();
+    let value = param.length > 1 ? param[1] : null;
+    params[decodeURIComponent(key)] = decodeURIComponent(value);
+  }
+  return params;
+}
+
+/**
+ * Use binary search to find the index of the first item in a given array which
+ * passes a given condition. The items are expected to be sorted in the sense
+ * that if the condition is true for one item in the array, then it is also true
+ * for all following items.
+ *
+ * @returns {Number} Index of the first array element to pass the test,
+ *                   or |items.length| if no such element exists.
+ */
+function binarySearchFirstItem(items, condition) {
+  let minIndex = 0;
+  let maxIndex = items.length - 1;
+
+  if (items.length === 0 || !condition(items[maxIndex])) {
+    return items.length;
+  }
+  if (condition(items[minIndex])) {
+    return minIndex;
+  }
+
+  while (minIndex < maxIndex) {
+    let currentIndex = (minIndex + maxIndex) >> 1;
+    let currentItem = items[currentIndex];
+    if (condition(currentItem)) {
+      maxIndex = currentIndex;
+    } else {
+      minIndex = currentIndex + 1;
+    }
+  }
+  return minIndex; /* === maxIndex */
+}
+
+/**
+ *  Approximates float number as a fraction using Farey sequence (max order
+ *  of 8).
+ *  @param {number} x - Positive float number.
+ *  @returns {Array} Estimated fraction: the first array item is a numerator,
+ *                   the second one is a denominator.
+ */
+function approximateFraction(x) {
+  // Fast paths for int numbers or their inversions.
+  if (Math.floor(x) === x) {
+    return [x, 1];
+  }
+  let xinv = 1 / x;
+  let limit = 8;
+  if (xinv > limit) {
+    return [1, limit];
+  } else if (Math.floor(xinv) === xinv) {
+    return [1, xinv];
+  }
+
+  let x_ = x > 1 ? xinv : x;
+  // a/b and c/d are neighbours in Farey sequence.
+  let a = 0, b = 1, c = 1, d = 1;
+  // Limiting search to order 8.
+  while (true) {
+    // Generating next term in sequence (order of q).
+    let p = a + c, q = b + d;
+    if (q > limit) {
+      break;
+    }
+    if (x_ <= p / q) {
+      c = p; d = q;
+    } else {
+      a = p; b = q;
+    }
+  }
+  let result;
+  // Select closest of the neighbours to x.
+  if (x_ - a / b < c / d - x_) {
+    result = x_ === x ? [a, b] : [b, a];
+  } else {
+    result = x_ === x ? [c, d] : [d, c];
+  }
+  return result;
+}
+
+function roundToDivide(x, div) {
+  let r = x % div;
+  return r === 0 ? x : Math.round(x - r + div);
+}
+
+/**
+ * Gets the size of the specified page, converted from PDF units to inches.
+ * @param {Object} An Object containing the properties: {Array} `view`,
+ *   {number} `userUnit`, and {number} `rotate`.
+ * @return {Object} An Object containing the properties: {number} `width`
+ *   and {number} `height`, given in inches.
+ */
+function getPageSizeInches({ view, userUnit, rotate, }) {
+  const [x1, y1, x2, y2] = view;
+  // We need to take the page rotation into account as well.
+  const changeOrientation = rotate % 180 !== 0;
+
+  const width = (x2 - x1) / 72 * userUnit;
+  const height = (y2 - y1) / 72 * userUnit;
+
+  return {
+    width: (changeOrientation ? height : width),
+    height: (changeOrientation ? width : height),
+  };
+}
+
+/**
+ * Helper function for getVisibleElements.
+ *
+ * @param {number} index - initial guess at the first visible element
+ * @param {Array} views - array of pages, into which `index` is an index
+ * @param {number} top - the top of the scroll pane
+ * @returns {number} less than or equal to `index` that is definitely at or
+ *   before the first visible element in `views`, but not by too much. (Usually,
+ *   this will be the first element in the first partially visible row in
+ *   `views`, although sometimes it goes back one row further.)
+ */
+function backtrackBeforeAllVisibleElements(index, views, top) {
+  // binarySearchFirstItem's assumption is that the input is ordered, with only
+  // one index where the conditions flips from false to true: [false ...,
+  // true...]. With vertical scrolling and spreads, it is possible to have
+  // [false ..., true, false, true ...]. With wrapped scrolling we can have a
+  // similar sequence, with many more mixed true and false in the middle.
+  //
+  // So there is no guarantee that the binary search yields the index of the
+  // first visible element. It could have been any of the other visible elements
+  // that were preceded by a hidden element.
+
+  // Of course, if either this element or the previous (hidden) element is also
+  // the first element, there's nothing to worry about.
+  if (index < 2) {
+    return index;
+  }
+
+  // That aside, the possible cases are represented below.
+  //
+  //     ****  = fully hidden
+  //     A*B*  = mix of partially visible and/or hidden pages
+  //     CDEF  = fully visible
+  //
+  // (1) Binary search could have returned A, in which case we can stop.
+  // (2) Binary search could also have returned B, in which case we need to
+  // check the whole row.
+  // (3) Binary search could also have returned C, in which case we need to
+  // check the whole previous row.
+  //
+  // There's one other possibility:
+  //
+  //     ****  = fully hidden
+  //     ABCD  = mix of fully and/or partially visible pages
+  //
+  // (4) Binary search could only have returned A.
+
+  // Initially assume that we need to find the beginning of the current row
+  // (case 1, 2, or 4), which means finding a page that is above the current
+  // page's top. If the found page is partially visible, we're definitely not in
+  // case 3, and this assumption is correct.
+  let elt = views[index].div;
+  let pageTop = elt.offsetTop + elt.clientTop;
+
+  if (pageTop >= top) {
+    // The found page is fully visible, so we're actually either in case 3 or 4,
+    // and unfortunately we can't tell the difference between them without
+    // scanning the entire previous row, so we just conservatively assume that
+    // we do need to backtrack to that row. In both cases, the previous page is
+    // in the previous row, so use its top instead.
+    elt = views[index - 1].div;
+    pageTop = elt.offsetTop + elt.clientTop;
+  }
+
+  // Now we backtrack to the first page that still has its bottom below
+  // `pageTop`, which is the top of a page in the first visible row (unless
+  // we're in case 4, in which case it's the row before that).
+  // `index` is found by binary search, so the page at `index - 1` is
+  // invisible and we can start looking for potentially visible pages from
+  // `index - 2`. (However, if this loop terminates on its first iteration,
+  // which is the case when pages are stacked vertically, `index` should remain
+  // unchanged, so we use a distinct loop variable.)
+  for (let i = index - 2; i >= 0; --i) {
+    elt = views[i].div;
+    if (elt.offsetTop + elt.clientTop + elt.clientHeight <= pageTop) {
+      // We have reached the previous row, so stop now.
+      // This loop is expected to terminate relatively quickly because the
+      // number of pages per row is expected to be small.
+      break;
+    }
+    index = i;
+  }
+  return index;
+}
+
+/**
+ * Generic helper to find out what elements are visible within a scroll pane.
+ *
+ * Well, pretty generic. There are some assumptions placed on the elements
+ * referenced by `views`:
+ *   - If `horizontal`, no left of any earlier element is to the right of the
+ *     left of any later element.
+ *   - Otherwise, `views` can be split into contiguous rows where, within a row,
+ *     no top of any element is below the bottom of any other element, and
+ *     between rows, no bottom of any element in an earlier row is below the
+ *     top of any element in a later row.
+ *
+ * (Here, top, left, etc. all refer to the padding edge of the element in
+ * question. For pages, that ends up being equivalent to the bounding box of the
+ * rendering canvas. Earlier and later refer to index in `views`, not page
+ * layout.)
+ *
+ * @param scrollEl {HTMLElement} - a container that can possibly scroll
+ * @param views {Array} - objects with a `div` property that contains an
+ *   HTMLElement, which should all be descendents of `scrollEl` satisfying the
+ *   above layout assumptions
+ * @param sortByVisibility {boolean} - if true, the returned elements are sorted
+ *   in descending order of the percent of their padding box that is visible
+ * @param horizontal {boolean} - if true, the elements are assumed to be laid
+ *   out horizontally instead of vertically
+ * @returns {Object} `{ first, last, views: [{ id, x, y, view, percent }] }`
+ */
+function getVisibleElements(scrollEl, views, sortByVisibility = false,
+                            horizontal = false) {
+  const top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight;
+  const left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth;
+
+  // Throughout this "generic" function, comments will assume we're working with
+  // PDF document pages, which is the most important and complex case. In this
+  // case, the visible elements we're actually interested is the page canvas,
+  // which is contained in a wrapper which adds no padding/border/margin, which
+  // is itself contained in `view.div` which adds no padding (but does add a
+  // border). So, as specified in this function's doc comment, this function
+  // does all of its work on the padding edge of the provided views, starting at
+  // offsetLeft/Top (which includes margin) and adding clientLeft/Top (which is
+  // the border). Adding clientWidth/Height gets us the bottom-right corner of
+  // the padding edge.
+  function isElementBottomAfterViewTop(view) {
+    const element = view.div;
+    const elementBottom =
+      element.offsetTop + element.clientTop + element.clientHeight;
+    return elementBottom > top;
+  }
+  function isElementRightAfterViewLeft(view) {
+    const element = view.div;
+    const elementRight =
+      element.offsetLeft + element.clientLeft + element.clientWidth;
+    return elementRight > left;
+  }
+
+  const visible = [], numViews = views.length;
+  let firstVisibleElementInd = numViews === 0 ? 0 :
+    binarySearchFirstItem(views, horizontal ? isElementRightAfterViewLeft :
+                                              isElementBottomAfterViewTop);
+
+  // Please note the return value of the `binarySearchFirstItem` function when
+  // no valid element is found (hence the `firstVisibleElementInd` check below).
+  if (firstVisibleElementInd > 0 && firstVisibleElementInd < numViews &&
+      !horizontal) {
+    // In wrapped scrolling (or vertical scrolling with spreads), with some page
+    // sizes, isElementBottomAfterViewTop doesn't satisfy the binary search
+    // condition: there can be pages with bottoms above the view top between
+    // pages with bottoms below. This function detects and corrects that error;
+    // see it for more comments.
+    firstVisibleElementInd =
+      backtrackBeforeAllVisibleElements(firstVisibleElementInd, views, top);
+  }
+
+  // lastEdge acts as a cutoff for us to stop looping, because we know all
+  // subsequent pages will be hidden.
+  //
+  // When using wrapped scrolling or vertical scrolling with spreads, we can't
+  // simply stop the first time we reach a page below the bottom of the view;
+  // the tops of subsequent pages on the same row could still be visible. In
+  // horizontal scrolling, we don't have that issue, so we can stop as soon as
+  // we pass `right`, without needing the code below that handles the -1 case.
+  let lastEdge = horizontal ? right : -1;
+
+  for (let i = firstVisibleElementInd; i < numViews; i++) {
+    const view = views[i], element = view.div;
+    const currentWidth = element.offsetLeft + element.clientLeft;
+    const currentHeight = element.offsetTop + element.clientTop;
+    const viewWidth = element.clientWidth, viewHeight = element.clientHeight;
+    const viewRight = currentWidth + viewWidth;
+    const viewBottom = currentHeight + viewHeight;
+
+    if (lastEdge === -1) {
+      // As commented above, this is only needed in non-horizontal cases.
+      // Setting lastEdge to the bottom of the first page that is partially
+      // visible ensures that the next page fully below lastEdge is on the
+      // next row, which has to be fully hidden along with all subsequent rows.
+      if (viewBottom >= bottom) {
+        lastEdge = viewBottom;
+      }
+    } else if ((horizontal ? currentWidth : currentHeight) > lastEdge) {
+      break;
+    }
+
+    if (viewBottom <= top || currentHeight >= bottom ||
+        viewRight <= left || currentWidth >= right) {
+      continue;
+    }
+
+    const hiddenHeight = Math.max(0, top - currentHeight) +
+                         Math.max(0, viewBottom - bottom);
+    const hiddenWidth = Math.max(0, left - currentWidth) +
+                        Math.max(0, viewRight - right);
+    const percent = ((viewHeight - hiddenHeight) * (viewWidth - hiddenWidth) *
+                     100 / viewHeight / viewWidth) | 0;
+    visible.push({
+      id: view.id,
+      x: currentWidth,
+      y: currentHeight,
+      view,
+      percent,
+    });
+  }
+
+  const first = visible[0], last = visible[visible.length - 1];
+
+  if (sortByVisibility) {
+    visible.sort(function(a, b) {
+      let pc = a.percent - b.percent;
+      if (Math.abs(pc) > 0.001) {
+        return -pc;
+      }
+      return a.id - b.id; // ensure stability
+    });
+  }
+  return { first, last, views: visible, };
+}
+
+/**
+ * Event handler to suppress context menu.
+ */
+function noContextMenuHandler(evt) {
+  evt.preventDefault();
+}
+
+function isDataSchema(url) {
+  let i = 0, ii = url.length;
+  while (i < ii && url[i].trim() === '') {
+    i++;
+  }
+  return url.substring(i, i + 5).toLowerCase() === 'data:';
+}
+
+/**
+ * Returns the filename or guessed filename from the url (see issue 3455).
+ * @param {string} url - The original PDF location.
+ * @param {string} defaultFilename - The value returned if the filename is
+ *   unknown, or the protocol is unsupported.
+ * @returns {string} Guessed PDF filename.
+ */
+function getPDFFileNameFromURL(url, defaultFilename = 'document.pdf') {
+  if (typeof url !== 'string') {
+    return defaultFilename;
+  }
+  if (isDataSchema(url)) {
+    console.warn('getPDFFileNameFromURL: ' +
+                 'ignoring "data:" URL for performance reasons.');
+    return defaultFilename;
+  }
+  const reURI = /^(?:(?:[^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;
+  //            SCHEME        HOST         1.PATH  2.QUERY   3.REF
+  // Pattern to get last matching NAME.pdf
+  const reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
+  let splitURI = reURI.exec(url);
+  let suggestedFilename = reFilename.exec(splitURI[1]) ||
+                          reFilename.exec(splitURI[2]) ||
+                          reFilename.exec(splitURI[3]);
+  if (suggestedFilename) {
+    suggestedFilename = suggestedFilename[0];
+    if (suggestedFilename.includes('%')) {
+      // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf
+      try {
+        suggestedFilename =
+          reFilename.exec(decodeURIComponent(suggestedFilename))[0];
+      } catch (ex) { // Possible (extremely rare) errors:
+        // URIError "Malformed URI", e.g. for "%AA.pdf"
+        // TypeError "null has no properties", e.g. for "%2F.pdf"
+      }
+    }
+  }
+  return suggestedFilename || defaultFilename;
+}
+
+function normalizeWheelEventDelta(evt) {
+  let delta = Math.sqrt(evt.deltaX * evt.deltaX + evt.deltaY * evt.deltaY);
+  let angle = Math.atan2(evt.deltaY, evt.deltaX);
+  if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
+    // All that is left-up oriented has to change the sign.
+    delta = -delta;
+  }
+
+  const MOUSE_DOM_DELTA_PIXEL_MODE = 0;
+  const MOUSE_DOM_DELTA_LINE_MODE = 1;
+  const MOUSE_PIXELS_PER_LINE = 30;
+  const MOUSE_LINES_PER_PAGE = 30;
+
+  // Converts delta to per-page units
+  if (evt.deltaMode === MOUSE_DOM_DELTA_PIXEL_MODE) {
+    delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE;
+  } else if (evt.deltaMode === MOUSE_DOM_DELTA_LINE_MODE) {
+    delta /= MOUSE_LINES_PER_PAGE;
+  }
+  return delta;
+}
+
+function isValidRotation(angle) {
+  return Number.isInteger(angle) && angle % 90 === 0;
+}
+
+function isValidScrollMode(mode) {
+  return (Number.isInteger(mode) && Object.values(ScrollMode).includes(mode) &&
+          mode !== ScrollMode.UNKNOWN);
+}
+
+function isValidSpreadMode(mode) {
+  return (Number.isInteger(mode) && Object.values(SpreadMode).includes(mode) &&
+          mode !== SpreadMode.UNKNOWN);
+}
+
+function isPortraitOrientation(size) {
+  return size.width <= size.height;
+}
+
+const WaitOnType = {
+  EVENT: 'event',
+  TIMEOUT: 'timeout',
+};
+
+/**
+ * @typedef {Object} WaitOnEventOrTimeoutParameters
+ * @property {Object} target - The event target, can for example be:
+ *   `window`, `document`, a DOM element, or an {EventBus} instance.
+ * @property {string} name - The name of the event.
+ * @property {number} delay - The delay, in milliseconds, after which the
+ *   timeout occurs (if the event wasn't already dispatched).
+ */
+
+/**
+ * Allows waiting for an event or a timeout, whichever occurs first.
+ * Can be used to ensure that an action always occurs, even when an event
+ * arrives late or not at all.
+ *
+ * @param {WaitOnEventOrTimeoutParameters}
+ * @returns {Promise} A promise that is resolved with a {WaitOnType} value.
+ */
+function waitOnEventOrTimeout({ target, name, delay = 0, }) {
+  return new Promise(function(resolve, reject) {
+    if (typeof target !== 'object' || !(name && typeof name === 'string') ||
+        !(Number.isInteger(delay) && delay >= 0)) {
+      throw new Error('waitOnEventOrTimeout - invalid parameters.');
+    }
+
+    function handler(type) {
+      if (target instanceof EventBus) {
+        target.off(name, eventHandler);
+      } else {
+        target.removeEventListener(name, eventHandler);
+      }
+
+      if (timeout) {
+        clearTimeout(timeout);
+      }
+      resolve(type);
+    }
+
+    const eventHandler = handler.bind(null, WaitOnType.EVENT);
+    if (target instanceof EventBus) {
+      target.on(name, eventHandler);
+    } else {
+      target.addEventListener(name, eventHandler);
+    }
+
+    const timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT);
+    let timeout = setTimeout(timeoutHandler, delay);
+  });
+}
+
+/**
+ * Promise that is resolved when DOM window becomes visible.
+ */
+let animationStarted = new Promise(function (resolve) {
+  if ((typeof PDFJSDev !== 'undefined' && PDFJSDev.test('LIB')) &&
+      typeof window === 'undefined') {
+    // Prevent "ReferenceError: window is not defined" errors when running the
+    // unit-tests in Node.js/Travis.
+    setTimeout(resolve, 20);
+    return;
+  }
+  window.requestAnimationFrame(resolve);
+});
+
+/**
+ * Simple event bus for an application. Listeners are attached using the
+ * `on` and `off` methods. To raise an event, the `dispatch` method shall be
+ * used.
+ */
+class EventBus {
+  constructor({ dispatchToDOM = false, } = {}) {
+    this._listeners = Object.create(null);
+    this._dispatchToDOM = dispatchToDOM === true;
+  }
+
+  on(eventName, listener) {
+    let eventListeners = this._listeners[eventName];
+    if (!eventListeners) {
+      eventListeners = [];
+      this._listeners[eventName] = eventListeners;
+    }
+    eventListeners.push(listener);
+  }
+
+  off(eventName, listener) {
+    let eventListeners = this._listeners[eventName];
+    let i;
+    if (!eventListeners || ((i = eventListeners.indexOf(listener)) < 0)) {
+      return;
+    }
+    eventListeners.splice(i, 1);
+  }
+
+  dispatch(eventName) {
+    let eventListeners = this._listeners[eventName];
+    if (!eventListeners || eventListeners.length === 0) {
+      if (this._dispatchToDOM) {
+        const args = Array.prototype.slice.call(arguments, 1);
+        this._dispatchDOMEvent(eventName, args);
+      }
+      return;
+    }
+    // Passing all arguments after the eventName to the listeners.
+    const args = Array.prototype.slice.call(arguments, 1);
+    // Making copy of the listeners array in case if it will be modified
+    // during dispatch.
+    eventListeners.slice(0).forEach(function (listener) {
+      listener.apply(null, args);
+    });
+    if (this._dispatchToDOM) {
+      this._dispatchDOMEvent(eventName, args);
+    }
+  }
+
+  /**
+   * @private
+   */
+  _dispatchDOMEvent(eventName, args = null) {
+    const details = Object.create(null);
+    if (args && args.length > 0) {
+      const obj = args[0];
+      for (let key in obj) {
+        const value = obj[key];
+        if (key === 'source') {
+          if (value === window || value === document) {
+            return; // No need to re-dispatch (already) global events.
+          }
+          continue; // Ignore the `source` property.
+        }
+        details[key] = value;
+      }
+    }
+    const event = document.createEvent('CustomEvent');
+    event.initCustomEvent(eventName, true, true, details);
+    document.dispatchEvent(event);
+  }
+}
+
+let globalEventBus = null;
+function getGlobalEventBus(dispatchToDOM = false) {
+  if (!globalEventBus) {
+    globalEventBus = new EventBus({ dispatchToDOM, });
+  }
+  return globalEventBus;
+}
+
+function clamp(v, min, max) {
+  return Math.min(Math.max(v, min), max);
+}
+
+class ProgressBar {
+  constructor(id, { height, width, units, } = {}) {
+    this.visible = true;
+
+    // Fetch the sub-elements for later.
+    this.div = document.querySelector(id + ' .progress');
+    // Get the loading bar element, so it can be resized to fit the viewer.
+    this.bar = this.div.parentNode;
+
+    // Get options, with sensible defaults.
+    this.height = height || 100;
+    this.width = width || 100;
+    this.units = units || '%';
+
+    // Initialize heights.
+    this.div.style.height = this.height + this.units;
+    this.percent = 0;
+  }
+
+  _updateBar() {
+    if (this._indeterminate) {
+      this.div.classList.add('indeterminate');
+      this.div.style.width = this.width + this.units;
+      return;
+    }
+
+    this.div.classList.remove('indeterminate');
+    let progressSize = this.width * this._percent / 100;
+    this.div.style.width = progressSize + this.units;
+  }
+
+  get percent() {
+    return this._percent;
+  }
+
+  set percent(val) {
+    this._indeterminate = isNaN(val);
+    this._percent = clamp(val, 0, 100);
+    this._updateBar();
+  }
+
+  setWidth(viewer) {
+    if (!viewer) {
+      return;
+    }
+    let container = viewer.parentNode;
+    let scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
+    if (scrollbarWidth > 0) {
+      this.bar.setAttribute('style', 'width: calc(100% - ' +
+                                     scrollbarWidth + 'px);');
+    }
+  }
+
+  hide() {
+    if (!this.visible) {
+      return;
+    }
+    this.visible = false;
+    this.bar.classList.add('hidden');
+    document.body.classList.remove('loadingInProgress');
+  }
+
+  show() {
+    if (this.visible) {
+      return;
+    }
+    this.visible = true;
+    document.body.classList.add('loadingInProgress');
+    this.bar.classList.remove('hidden');
+  }
+}
+
+/**
+ * Moves all elements of an array that satisfy condition to the end of the
+ * array, preserving the order of the rest.
+ */
+function moveToEndOfArray(arr, condition) {
+  const moved = [], len = arr.length;
+  let write = 0;
+  for (let read = 0; read < len; ++read) {
+    if (condition(arr[read])) {
+      moved.push(arr[read]);
+    } else {
+      arr[write] = arr[read];
+      ++write;
+    }
+  }
+  for (let read = 0; write < len; ++read, ++write) {
+    arr[write] = moved[read];
+  }
+}
+
+export {
+  CSS_UNITS,
+  DEFAULT_SCALE_VALUE,
+  DEFAULT_SCALE,
+  MIN_SCALE,
+  MAX_SCALE,
+  UNKNOWN_SCALE,
+  MAX_AUTO_SCALE,
+  SCROLLBAR_PADDING,
+  VERTICAL_PADDING,
+  isValidRotation,
+  isValidScrollMode,
+  isValidSpreadMode,
+  isPortraitOrientation,
+  PresentationModeState,
+  RendererType,
+  TextLayerMode,
+  ScrollMode,
+  SpreadMode,
+  NullL10n,
+  EventBus,
+  getGlobalEventBus,
+  ProgressBar,
+  getPDFFileNameFromURL,
+  noContextMenuHandler,
+  parseQueryString,
+  backtrackBeforeAllVisibleElements, // only exported for testing
+  getVisibleElements,
+  roundToDivide,
+  getPageSizeInches,
+  approximateFraction,
+  getOutputScale,
+  scrollIntoView,
+  watchScroll,
+  binarySearchFirstItem,
+  normalizeWheelEventDelta,
+  animationStarted,
+  WaitOnType,
+  waitOnEventOrTimeout,
+  moveToEndOfArray,
+};

app/templates/show_image.html → app/templates/show_image_old_working.html


+ 62 - 0
app/templates/show_pdf.html

@@ -0,0 +1,62 @@
+<!DOCTYPE html><meta charset="utf-8">
+<link rel="stylesheet" href="{{ url_for('static', filename='css/text_layer_builder.css') }}" />
+<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
+<!--
+<script src="//mozilla.github.io/pdf.js/build/pdf.js"></script>
+ -->
+<!-- <script src="https://mozilla.github.io/pdf.js/build/pdf.js"></script>das funktioniert!!!!!
+<script type="javascript" src="{{ url_for('static', filename='js/pdf.js') }}"></script>-->
+
+
+<script type="javascript" src="https://unpkg.com/pdfjs-dist@latest/build/pdf.js" ></script>
+https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.3.200/pdf.js
+<script type="javascript" src="{{ url_for('static', filename='js/text_layer_builder.js') }}"></script>
+<script type="javascript" src="{{ url_for('static', filename='js/ui_utils.js') }}"></script>
+<body>
+<canvas id="the-canvas"></canvas>
+<script>
+
+var url = "{{ url_for('send_file', filename=og_filename) }}";
+
+var pdfjsLib = window['pdfjs-dist/build/pdf'];
+pdfjsLib.disableWorker = true;
+
+pdfjsLib.getDocument(url).then(function(pdf){
+    var page_num = 1;
+    pdf.getPage(page_num).then(function(page){
+        var scale = 1.5;
+        var viewport = page.getViewport(scale);
+        var canvas = $('#the-canvas')[0];
+        var context = canvas.getContext('2d');
+        canvas.height = viewport.height;
+        canvas.width = viewport.width;
+
+        var canvasOffset = $(canvas).offset();
+        var $textLayerDiv = $('#text-layer').css({
+            height : viewport.height+'px',
+            width : viewport.width+'px',
+            top : canvasOffset.top,
+            left : canvasOffset.left
+        });
+
+        page.render({
+            canvasContext : context,
+            viewport : viewport
+        });
+
+        page.getTextContent().then(function(textContent){
+           console.log( textContent );
+            var textLayer = new TextLayerBuilder({
+                textLayerDiv : $textLayerDiv.get(0),
+                pageIndex : page_num - 1,
+                viewport : viewport
+            });
+
+            textLayer.setTextContent(textContent);
+            textLayer.render();
+        });
+    });
+});
+</script>
+</body>
+</html>

+ 35 - 118
app/test.html

@@ -1,94 +1,49 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title>Title</title>
-</head>
-<body>
-
-
-
-</body>
-</html>
-
-    <script>
-    function set_redis(key, value) {
-        $.ajax({
-            url: "http://127.0.0.1:7379/SET/" + key + "/" + value,
-            data: "format=json",
-            dataType: "json",
-            success: function(data)
-            {
-                $("#result").text(data.GET);
-            }
-        });
-    }
-
-    </script>
-    <script>
-
-
-
-    </script>
-
-
-
-var url = "{{ url_for('send_file', filename=filename) }}";
-console.log(url);
-var file = url;
-var file_reader = new FileReader();
-typedarray = new Uint8Array(file);
-
-
-// Loaded via <script> tag, create shortcut to access PDF.js exports.
 var pdfjsLib = window['pdfjs-dist/build/pdf'];
 
-var loadingTask = pdfjsLib.getDocument({data : file});
-loadingTask.promise.then(function(pdf) {
-  console.log('PDF loaded');
+//pdfjsLib.disableWorker = true;
+
+pdfjsLib.getDocument(url).then(function(pdf){
+    var page_num = 1;
+    pdf.getPage(page_num).then(function(page){
+        var scale = 1.5;
+        var viewport = page.getViewport(scale);
+        var canvas = $('#the-canvas')[0];
+        var context = canvas.getContext('2d');
+        canvas.height = viewport.height;
+        canvas.width = viewport.width;
+
+        var canvasOffset = $(canvas).offset();
+        var $textLayerDiv = $('#text-layer').css({
+            height : viewport.height+'px',
+            width : viewport.width+'px',
+            top : canvasOffset.top,
+            left : canvasOffset.left
+        });
 
-  // Fetch the first page
-  var pageNumber = 1;
-  pdf.getPage(pageNumber).then(function(page) {
-    console.log('Page loaded');
+        page.render({
+            canvasContext : context,
+            viewport : viewport
+        });
 
-    var scale = 1.5;
-    var viewport = page.getViewport({scale: scale});
+        page.getTextContent().then(function(textContent){
+           console.log( textContent );
+            var textLayer = new TextLayerBuilder({
+                textLayerDiv : $textLayerDiv.get(0),
+                pageIndex : page_num - 1,
+                viewport : viewport
+            });
 
-    // Prepare canvas using PDF page dimensions
-    var canvas = document.getElementById('the-canvas');
-    var context = canvas.getContext('2d');
-    canvas.height = viewport.height;
-    canvas.width = viewport.width;
-
-    // Render PDF page into canvas context
-    var renderContext = {
-      canvasContext: context,
-      viewport: viewport
-    };
-    var renderTask = page.render(renderContext);
-    renderTask.promise.then(function () {
-      console.log('Page rendered');
+            textLayer.setTextContent(textContent);
+            textLayer.render();
+        });
     });
-  });
-}, function (reason) {
-  // PDF loading error
-  console.error(reason);
 });
-            <img src="{{ url_for('send_file', filename=filename) }}" id="drawing"/>
 
-</script>
 
 
-<script>
-var encoded_data = '{{ encoded_data }}';
-console.log(encoded_data);
-var pdf_data = atob(encoded_data);
-console_log(pdf_data);
-var pdfjsLib = window['pdfjs-dist/build/pdf'];
 
 
-var loadingTask = pdfjsLib.getDocument({pdf_data});
+var loadingTask = pdfjsLib.getDocument(url);
 loadingTask.promise.then(function(pdf) {
   console.log('PDF loaded');
 
@@ -101,7 +56,7 @@ loadingTask.promise.then(function(pdf) {
     var viewport = page.getViewport({scale: scale});
 
     // Prepare canvas using PDF page dimensions
-    var canvas = document.getElementById('canvas');
+    var canvas = document.getElementById('the-canvas');
     var context = canvas.getContext('2d');
     canvas.height = viewport.height;
     canvas.width = viewport.width;
@@ -120,41 +75,3 @@ loadingTask.promise.then(function(pdf) {
   // PDF loading error
   console.error(reason);
 });
-
-
-
-</script>
- document.querySelector("#pdf-upload").addEventListener("change", function(e){
-	var canvasElement = document.querySelector("canvas")
-	var file = e.target.files[0]
-	if(file.type != "application/pdf"){
-		console.error(file.name, "is not a pdf file.")
-		return
-	}
-
-	var fileReader = new FileReader();
-
-	fileReader.onload = function() {
-		var typedarray = new Uint8Array(this.result);
-
-		pdfjsLib.getDocument(typedarray).then(function(pdf) {
-			// you can now use *pdf* here
-			console.log("the pdf has ",pdf.numPages, "page(s).")
-			pdf.getPage(pdf.numPages).then(function(page) {
-				// you can now use *page* here
-				var viewport = page.getViewport(2.0);
-				var canvas = document.querySelector("canvas")
-				canvas.height = viewport.height;
-				canvas.width = viewport.width;
-
-
-				page.render({
-					canvasContext: canvas.getContext('2d'),
-					viewport: viewport
-				});
-			});
-
-		});
-	};
-
-	fileReader.readAsArrayBuffer(file);

+ 7 - 6
app/views.py

@@ -12,8 +12,7 @@ import base64
 #https://medium.com/@emerico/convert-pdf-to-image-using-python-flask-2864fb655e01
 
 
-#UPLOAD_FOLDER = '/Users/beatescheibel/Desktop/flask/uploads'
-UPLOAD_FOLDER = "./static/"
+UPLOAD_FOLDER = "/home/bscheibel/app/app/temporary"
 app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
 ALLOWED_EXTENSIONS = set(['pdf', 'png', 'jpg', 'jpeg', 'PDF'])
 
@@ -25,7 +24,7 @@ def allowed_file(filename):
 def convert_pdf_img(filename):
     PDFFILE = UPLOAD_FOLDER +"/" + filename
     subprocess.call(['pdftoppm', '-jpeg', '-singlefile',
-                     PDFFILE, '/home/bscheibel/PycharmProjects/app/app/temporary/out'])
+                     PDFFILE, '/home/bscheibel//app/app/temporary/out'])
 
 def extract_all(uuid, filename, db):
     #order_bounding_boxes_in_each_block.main(uuid, UPLOAD_FOLDER + "/" + filename)
@@ -37,7 +36,8 @@ def upload_file():
         file = request.files['file']
         if file and allowed_file(file.filename):
             filename = file.filename
-            file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename))
+            basedir = os.path.abspath(os.path.dirname(__file__))
+            file.save(os.path.join(basedir,app.config["UPLOAD_FOLDER"], filename))
             uuid = random.randint(100,10000000)
             extract_all(uuid, filename, 'localhost')
             return redirect(url_for('uploaded_file', filename=filename, uuid=uuid))
@@ -53,7 +53,7 @@ def upload_file():
 
 @app.route('/show/<filename>&<uuid>')
 def uploaded_file(filename, uuid):
-    file_out = "temporary/out.jpg"
+    file_out = "out.jpg"
     #file_out = filename
     if request.method == 'POST':
         uuid = 433
@@ -87,12 +87,13 @@ def uploaded_file(filename, uuid):
                         steps = 0.1
                 except:
                     number = d
+                    steps = 0.1
                 coords = ",".join(str(e) for e in dims[dim][d])
                 html_code += "<tr><td style='text-align:center'> <input type='checkbox' name='relevant." + d + "' value='checked'> </td>" + \
                              "<td style='text-align:center'>" + d + "</td>" + \
                              "<td style='text-align:center'> <input type='number' step='" + str(steps) + "' data-coords='" + coords + "' name='" + d + "' value='" + number + "'  size='10'> </td></tr>"
                 #print(html_code)
-        return render_template('show_image.html', filename=file_out, isos=isos, dims=dims, text=html_code, number=number_blocks, og_filename=filename)
+        return render_template('show_pdf.html', filename=file_out, isos=isos, dims=dims, text=html_code, number=number_blocks, og_filename=filename)
 
     else:
         filename = filename