420 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			420 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 | 
						|
/* 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.
 | 
						|
 */
 | 
						|
/* globals CustomStyle, PDFJS */
 | 
						|
 | 
						|
'use strict';
 | 
						|
 | 
						|
var MAX_TEXT_DIVS_TO_RENDER = 100000;
 | 
						|
 | 
						|
var NonWhitespaceRegexp = /\S/;
 | 
						|
 | 
						|
function isAllWhitespace(str) {
 | 
						|
  return !NonWhitespaceRegexp.test(str);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {Object} TextLayerBuilderOptions
 | 
						|
 * @property {HTMLDivElement} textLayerDiv - The text layer container.
 | 
						|
 * @property {number} pageIndex - The page index.
 | 
						|
 * @property {PageViewport} viewport - The viewport of the text layer.
 | 
						|
 * @property {PDFFindController} findController
 | 
						|
 */
 | 
						|
 | 
						|
/**
 | 
						|
 * TextLayerBuilder provides text-selection functionality for the PDF.
 | 
						|
 * It does this by creating overlay divs over the PDF 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
 | 
						|
 */
 | 
						|
var TextLayerBuilder = (function TextLayerBuilderClosure() {
 | 
						|
  function TextLayerBuilder(options) {
 | 
						|
    this.textLayerDiv = options.textLayerDiv;
 | 
						|
    this.renderingDone = false;
 | 
						|
    this.divContentDone = false;
 | 
						|
    this.pageIdx = options.pageIndex;
 | 
						|
    this.pageNumber = this.pageIdx + 1;
 | 
						|
    this.matches = [];
 | 
						|
    this.viewport = options.viewport;
 | 
						|
    this.textDivs = [];
 | 
						|
    this.findController = options.findController || null;
 | 
						|
  }
 | 
						|
 | 
						|
  TextLayerBuilder.prototype = {
 | 
						|
    _finishRendering: function TextLayerBuilder_finishRendering() {
 | 
						|
      this.renderingDone = true;
 | 
						|
 | 
						|
      var event = document.createEvent('CustomEvent');
 | 
						|
      event.initCustomEvent('textlayerrendered', true, true, {
 | 
						|
        pageNumber: this.pageNumber
 | 
						|
      });
 | 
						|
      this.textLayerDiv.dispatchEvent(event);
 | 
						|
    },
 | 
						|
 | 
						|
    renderLayer: function TextLayerBuilder_renderLayer() {
 | 
						|
      var textLayerFrag = document.createDocumentFragment();
 | 
						|
      var textDivs = this.textDivs;
 | 
						|
      var textDivsLength = textDivs.length;
 | 
						|
      var canvas = document.createElement('canvas');
 | 
						|
      var ctx = canvas.getContext('2d');
 | 
						|
 | 
						|
      // No point in rendering many divs as it would make the browser
 | 
						|
      // unusable even after the divs are rendered.
 | 
						|
      if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) {
 | 
						|
        this._finishRendering();
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      var lastFontSize;
 | 
						|
      var lastFontFamily;
 | 
						|
      for (var i = 0; i < textDivsLength; i++) {
 | 
						|
        var textDiv = textDivs[i];
 | 
						|
        if (textDiv.dataset.isWhitespace !== undefined) {
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
 | 
						|
        var fontSize = textDiv.style.fontSize;
 | 
						|
        var fontFamily = textDiv.style.fontFamily;
 | 
						|
 | 
						|
        // Only build font string and set to context if different from last.
 | 
						|
        if (fontSize !== lastFontSize || fontFamily !== lastFontFamily) {
 | 
						|
          ctx.font = fontSize + ' ' + fontFamily;
 | 
						|
          lastFontSize = fontSize;
 | 
						|
          lastFontFamily = fontFamily;
 | 
						|
        }
 | 
						|
 | 
						|
        var width = ctx.measureText(textDiv.textContent).width;
 | 
						|
        if (width > 0) {
 | 
						|
          textLayerFrag.appendChild(textDiv);
 | 
						|
          var transform;
 | 
						|
          if (textDiv.dataset.canvasWidth !== undefined) {
 | 
						|
            // Dataset values come of type string.
 | 
						|
            var textScale = textDiv.dataset.canvasWidth / width;
 | 
						|
            transform = 'scaleX(' + textScale + ')';
 | 
						|
          } else {
 | 
						|
            transform = '';
 | 
						|
          }
 | 
						|
          var rotation = textDiv.dataset.angle;
 | 
						|
          if (rotation) {
 | 
						|
            transform = 'rotate(' + rotation + 'deg) ' + transform;
 | 
						|
          }
 | 
						|
          if (transform) {
 | 
						|
            CustomStyle.setProp('transform' , textDiv, transform);
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      this.textLayerDiv.appendChild(textLayerFrag);
 | 
						|
      this._finishRendering();
 | 
						|
      this.updateMatches();
 | 
						|
    },
 | 
						|
 | 
						|
    /**
 | 
						|
     * Renders the text layer.
 | 
						|
     * @param {number} timeout (optional) if specified, the rendering waits
 | 
						|
     *   for specified amount of ms.
 | 
						|
     */
 | 
						|
    render: function TextLayerBuilder_render(timeout) {
 | 
						|
      if (!this.divContentDone || this.renderingDone) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      if (this.renderTimer) {
 | 
						|
        clearTimeout(this.renderTimer);
 | 
						|
        this.renderTimer = null;
 | 
						|
      }
 | 
						|
 | 
						|
      if (!timeout) { // Render right away
 | 
						|
        this.renderLayer();
 | 
						|
      } else { // Schedule
 | 
						|
        var self = this;
 | 
						|
        this.renderTimer = setTimeout(function() {
 | 
						|
          self.renderLayer();
 | 
						|
          self.renderTimer = null;
 | 
						|
        }, timeout);
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    appendText: function TextLayerBuilder_appendText(geom, styles) {
 | 
						|
      var style = styles[geom.fontName];
 | 
						|
      var textDiv = document.createElement('div');
 | 
						|
      this.textDivs.push(textDiv);
 | 
						|
      if (isAllWhitespace(geom.str)) {
 | 
						|
        textDiv.dataset.isWhitespace = true;
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform);
 | 
						|
      var angle = Math.atan2(tx[1], tx[0]);
 | 
						|
      if (style.vertical) {
 | 
						|
        angle += Math.PI / 2;
 | 
						|
      }
 | 
						|
      var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3]));
 | 
						|
      var fontAscent = fontHeight;
 | 
						|
      if (style.ascent) {
 | 
						|
        fontAscent = style.ascent * fontAscent;
 | 
						|
      } else if (style.descent) {
 | 
						|
        fontAscent = (1 + style.descent) * fontAscent;
 | 
						|
      }
 | 
						|
 | 
						|
      var left;
 | 
						|
      var top;
 | 
						|
      if (angle === 0) {
 | 
						|
        left = tx[4];
 | 
						|
        top = tx[5] - fontAscent;
 | 
						|
      } else {
 | 
						|
        left = tx[4] + (fontAscent * Math.sin(angle));
 | 
						|
        top = tx[5] - (fontAscent * Math.cos(angle));
 | 
						|
      }
 | 
						|
      textDiv.style.left = left + 'px';
 | 
						|
      textDiv.style.top = top + 'px';
 | 
						|
      textDiv.style.fontSize = fontHeight + 'px';
 | 
						|
      textDiv.style.fontFamily = style.fontFamily;
 | 
						|
 | 
						|
      textDiv.textContent = geom.str;
 | 
						|
      // |fontName| is only used by the Font Inspector. This test will succeed
 | 
						|
      // when e.g. the Font Inspector is off but the Stepper is on, but it's
 | 
						|
      // not worth the effort to do a more accurate test.
 | 
						|
      if (PDFJS.pdfBug) {
 | 
						|
        textDiv.dataset.fontName = geom.fontName;
 | 
						|
      }
 | 
						|
      // Storing into dataset will convert number into string.
 | 
						|
      if (angle !== 0) {
 | 
						|
        textDiv.dataset.angle = angle * (180 / Math.PI);
 | 
						|
      }
 | 
						|
      // We don't bother scaling single-char text divs, because it has very
 | 
						|
      // little effect on text highlighting. This makes scrolling on docs with
 | 
						|
      // lots of such divs a lot faster.
 | 
						|
      if (textDiv.textContent.length > 1) {
 | 
						|
        if (style.vertical) {
 | 
						|
          textDiv.dataset.canvasWidth = geom.height * this.viewport.scale;
 | 
						|
        } else {
 | 
						|
          textDiv.dataset.canvasWidth = geom.width * this.viewport.scale;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    },
 | 
						|
 | 
						|
    setTextContent: function TextLayerBuilder_setTextContent(textContent) {
 | 
						|
      this.textContent = textContent;
 | 
						|
 | 
						|
      var textItems = textContent.items;
 | 
						|
      for (var i = 0, len = textItems.length; i < len; i++) {
 | 
						|
        this.appendText(textItems[i], textContent.styles);
 | 
						|
      }
 | 
						|
      this.divContentDone = true;
 | 
						|
    },
 | 
						|
 | 
						|
    convertMatches: function TextLayerBuilder_convertMatches(matches) {
 | 
						|
      var i = 0;
 | 
						|
      var iIndex = 0;
 | 
						|
      var bidiTexts = this.textContent.items;
 | 
						|
      var end = bidiTexts.length - 1;
 | 
						|
      var queryLen = (this.findController === null ?
 | 
						|
                      0 : this.findController.state.query.length);
 | 
						|
      var ret = [];
 | 
						|
 | 
						|
      for (var m = 0, len = matches.length; m < len; m++) {
 | 
						|
        // Calculate the start position.
 | 
						|
        var matchIdx = matches[m];
 | 
						|
 | 
						|
        // Loop over the divIdxs.
 | 
						|
        while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) {
 | 
						|
          iIndex += bidiTexts[i].str.length;
 | 
						|
          i++;
 | 
						|
        }
 | 
						|
 | 
						|
        if (i === bidiTexts.length) {
 | 
						|
          console.error('Could not find a matching mapping');
 | 
						|
        }
 | 
						|
 | 
						|
        var match = {
 | 
						|
          begin: {
 | 
						|
            divIdx: i,
 | 
						|
            offset: matchIdx - iIndex
 | 
						|
          }
 | 
						|
        };
 | 
						|
 | 
						|
        // Calculate the end position.
 | 
						|
        matchIdx += queryLen;
 | 
						|
 | 
						|
        // Somewhat the same array as above, but use > instead of >= to get
 | 
						|
        // the end position right.
 | 
						|
        while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) {
 | 
						|
          iIndex += bidiTexts[i].str.length;
 | 
						|
          i++;
 | 
						|
        }
 | 
						|
 | 
						|
        match.end = {
 | 
						|
          divIdx: i,
 | 
						|
          offset: matchIdx - iIndex
 | 
						|
        };
 | 
						|
        ret.push(match);
 | 
						|
      }
 | 
						|
 | 
						|
      return ret;
 | 
						|
    },
 | 
						|
 | 
						|
    renderMatches: function TextLayerBuilder_renderMatches(matches) {
 | 
						|
      // Early exit if there is nothing to render.
 | 
						|
      if (matches.length === 0) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      var bidiTexts = this.textContent.items;
 | 
						|
      var textDivs = this.textDivs;
 | 
						|
      var prevEnd = null;
 | 
						|
      var pageIdx = this.pageIdx;
 | 
						|
      var isSelectedPage = (this.findController === null ?
 | 
						|
        false : (pageIdx === this.findController.selected.pageIdx));
 | 
						|
      var selectedMatchIdx = (this.findController === null ?
 | 
						|
                              -1 : this.findController.selected.matchIdx);
 | 
						|
      var highlightAll = (this.findController === null ?
 | 
						|
                          false : this.findController.state.highlightAll);
 | 
						|
      var infinity = {
 | 
						|
        divIdx: -1,
 | 
						|
        offset: undefined
 | 
						|
      };
 | 
						|
 | 
						|
      function beginText(begin, className) {
 | 
						|
        var divIdx = begin.divIdx;
 | 
						|
        textDivs[divIdx].textContent = '';
 | 
						|
        appendTextToDiv(divIdx, 0, begin.offset, className);
 | 
						|
      }
 | 
						|
 | 
						|
      function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
 | 
						|
        var div = textDivs[divIdx];
 | 
						|
        var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset);
 | 
						|
        var node = document.createTextNode(content);
 | 
						|
        if (className) {
 | 
						|
          var span = document.createElement('span');
 | 
						|
          span.className = className;
 | 
						|
          span.appendChild(node);
 | 
						|
          div.appendChild(span);
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        div.appendChild(node);
 | 
						|
      }
 | 
						|
 | 
						|
      var 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 (var i = i0; i < i1; i++) {
 | 
						|
        var match = matches[i];
 | 
						|
        var begin = match.begin;
 | 
						|
        var end = match.end;
 | 
						|
        var isSelected = (isSelectedPage && i === selectedMatchIdx);
 | 
						|
        var highlightSuffix = (isSelected ? ' selected' : '');
 | 
						|
 | 
						|
        if (this.findController) {
 | 
						|
          this.findController.updateMatchPosition(pageIdx, i, textDivs,
 | 
						|
                                                  begin.divIdx, end.divIdx);
 | 
						|
        }
 | 
						|
 | 
						|
        // 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 (var 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: function TextLayerBuilder_updateMatches() {
 | 
						|
      // Only show matches when all rendering is done.
 | 
						|
      if (!this.renderingDone) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      // Clear all matches.
 | 
						|
      var matches = this.matches;
 | 
						|
      var textDivs = this.textDivs;
 | 
						|
      var bidiTexts = this.textContent.items;
 | 
						|
      var clearedUntilDivIdx = -1;
 | 
						|
 | 
						|
      // Clear all current matches.
 | 
						|
      for (var i = 0, len = matches.length; i < len; i++) {
 | 
						|
        var match = matches[i];
 | 
						|
        var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
 | 
						|
        for (var n = begin, end = match.end.divIdx; n <= end; n++) {
 | 
						|
          var div = textDivs[n];
 | 
						|
          div.textContent = bidiTexts[n].str;
 | 
						|
          div.className = '';
 | 
						|
        }
 | 
						|
        clearedUntilDivIdx = match.end.divIdx + 1;
 | 
						|
      }
 | 
						|
 | 
						|
      if (this.findController === null || !this.findController.active) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      // Convert the matches on the page controller into the match format
 | 
						|
      // used for the textLayer.
 | 
						|
      this.matches = this.convertMatches(this.findController === null ?
 | 
						|
        [] : (this.findController.pageMatches[this.pageIdx] || []));
 | 
						|
      this.renderMatches(this.matches);
 | 
						|
    }
 | 
						|
  };
 | 
						|
  return TextLayerBuilder;
 | 
						|
})();
 | 
						|
 | 
						|
/**
 | 
						|
 * @constructor
 | 
						|
 * @implements IPDFTextLayerFactory
 | 
						|
 */
 | 
						|
function DefaultTextLayerFactory() {}
 | 
						|
DefaultTextLayerFactory.prototype = {
 | 
						|
  /**
 | 
						|
   * @param {HTMLDivElement} textLayerDiv
 | 
						|
   * @param {number} pageIndex
 | 
						|
   * @param {PageViewport} viewport
 | 
						|
   * @returns {TextLayerBuilder}
 | 
						|
   */
 | 
						|
  createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) {
 | 
						|
    return new TextLayerBuilder({
 | 
						|
      textLayerDiv: textLayerDiv,
 | 
						|
      pageIndex: pageIndex,
 | 
						|
      viewport: viewport
 | 
						|
    });
 | 
						|
  }
 | 
						|
};
 |