123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- /* -*- 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, PDFFindController, scrollIntoView */
- 'use strict';
- var FIND_SCROLL_OFFSET_TOP = -50;
- var FIND_SCROLL_OFFSET_LEFT = -400;
- /**
- * TextLayerBuilder provides text-selection
- * functionality for the PDF. It does this
- * by creating overlay divs over the PDF
- * text. This divs contain text that matches
- * the PDF text they are overlaying. This
- * object also provides for a way to highlight
- * text that is being searched for.
- */
- var TextLayerBuilder = function textLayerBuilder(options) {
- var textLayerFrag = document.createDocumentFragment();
- this.textLayerDiv = options.textLayerDiv;
- this.layoutDone = false;
- this.divContentDone = false;
- this.pageIdx = options.pageIndex;
- this.matches = [];
- this.lastScrollSource = options.lastScrollSource;
- this.viewport = options.viewport;
- this.isViewerInPresentationMode = options.isViewerInPresentationMode;
- if(typeof PDFFindController === 'undefined') {
- window.PDFFindController = null;
- }
- if(typeof this.lastScrollSource === 'undefined') {
- this.lastScrollSource = null;
- }
- this.beginLayout = function textLayerBuilderBeginLayout() {
- this.textDivs = [];
- this.renderingDone = false;
- };
- this.endLayout = function textLayerBuilderEndLayout() {
- this.layoutDone = true;
- this.insertDivContent();
- };
- this.renderLayer = function textLayerBuilderRenderLayer() {
- var self = this;
- var textDivs = this.textDivs;
- var bidiTexts = this.textContent;
- var textLayerDiv = this.textLayerDiv;
- var canvas = document.createElement('canvas');
- var ctx = canvas.getContext('2d');
- // No point in rendering so many divs as it'd make the browser unusable
- // even after the divs are rendered
- var MAX_TEXT_DIVS_TO_RENDER = 100000;
- if (textDivs.length > MAX_TEXT_DIVS_TO_RENDER)
- return;
- for (var i = 0, ii = textDivs.length; i < ii; i++) {
- var textDiv = textDivs[i];
- if ('isWhitespace' in textDiv.dataset) {
- continue;
- }
- ctx.font = textDiv.style.fontSize + ' ' + textDiv.style.fontFamily;
- var width = ctx.measureText(textDiv.textContent).width;
- if (width > 0) {
- textLayerFrag.appendChild(textDiv);
- var textScale = textDiv.dataset.canvasWidth / width;
- var rotation = textDiv.dataset.angle;
- var transform = 'scale(' + textScale + ', 1)';
- transform = 'rotate(' + rotation + 'deg) ' + transform;
- CustomStyle.setProp('transform' , textDiv, transform);
- CustomStyle.setProp('transformOrigin' , textDiv, '0% 0%');
- }
- }
- textLayerDiv.appendChild(textLayerFrag);
- this.renderingDone = true;
- this.updateMatches();
- };
- this.setupRenderLayoutTimer = function textLayerSetupRenderLayoutTimer() {
- // Schedule renderLayout() if user has been scrolling, otherwise
- // run it right away
- var RENDER_DELAY = 200; // in ms
- var self = this;
- var lastScroll = this.lastScrollSource === null ?
- 0 : this.lastScrollSource.lastScroll;
- if (Date.now() - lastScroll > RENDER_DELAY) {
- // Render right away
- this.renderLayer();
- } else {
- // Schedule
- if (this.renderTimer)
- clearTimeout(this.renderTimer);
- this.renderTimer = setTimeout(function() {
- self.setupRenderLayoutTimer();
- }, RENDER_DELAY);
- }
- };
- this.appendText = function textLayerBuilderAppendText(geom) {
- var textDiv = document.createElement('div');
- // vScale and hScale already contain the scaling to pixel units
- var fontHeight = geom.fontSize * Math.abs(geom.vScale);
- textDiv.dataset.canvasWidth = geom.canvasWidth * Math.abs(geom.hScale);
- textDiv.dataset.fontName = geom.fontName;
- textDiv.dataset.angle = geom.angle * (180 / Math.PI);
- textDiv.style.fontSize = fontHeight + 'px';
- textDiv.style.fontFamily = geom.fontFamily;
- var fontAscent = geom.ascent ? geom.ascent * fontHeight :
- geom.descent ? (1 + geom.descent) * fontHeight : fontHeight;
- textDiv.style.left = (geom.x + (fontAscent * Math.sin(geom.angle))) + 'px';
- textDiv.style.top = (geom.y - (fontAscent * Math.cos(geom.angle))) + 'px';
- // The content of the div is set in the `setTextContent` function.
- this.textDivs.push(textDiv);
- };
- this.insertDivContent = function textLayerUpdateTextContent() {
- // Only set the content of the divs once layout has finished, the content
- // for the divs is available and content is not yet set on the divs.
- if (!this.layoutDone || this.divContentDone || !this.textContent)
- return;
- this.divContentDone = true;
- var textDivs = this.textDivs;
- var bidiTexts = this.textContent;
- for (var i = 0; i < bidiTexts.length; i++) {
- var bidiText = bidiTexts[i];
- var textDiv = textDivs[i];
- if (!/\S/.test(bidiText.str)) {
- textDiv.dataset.isWhitespace = true;
- continue;
- }
- textDiv.textContent = bidiText.str;
- // TODO refactor text layer to use text content position
- /**
- * var arr = this.viewport.convertToViewportPoint(bidiText.x, bidiText.y);
- * textDiv.style.left = arr[0] + 'px';
- * textDiv.style.top = arr[1] + 'px';
- */
- // bidiText.dir may be 'ttb' for vertical texts.
- textDiv.dir = bidiText.dir;
- }
- this.setupRenderLayoutTimer();
- };
- this.setTextContent = function textLayerBuilderSetTextContent(textContent) {
- this.textContent = textContent;
- this.insertDivContent();
- };
- this.convertMatches = function textLayerBuilderConvertMatches(matches) {
- var i = 0;
- var iIndex = 0;
- var bidiTexts = this.textContent;
- var end = bidiTexts.length - 1;
- var queryLen = PDFFindController === null ?
- 0 : PDFFindController.state.query.length;
- var lastDivIdx = -1;
- var pos;
- var ret = [];
- // Loop over all the matches.
- for (var m = 0; m < matches.length; m++) {
- var matchIdx = matches[m];
- // # Calculate the begin position.
- // Loop over the divIdxs.
- while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) {
- iIndex += bidiTexts[i].str.length;
- i++;
- }
- // TODO: Do proper handling here if something goes wrong.
- if (i == bidiTexts.length) {
- console.error('Could not find matching mapping');
- }
- var match = {
- begin: {
- divIdx: i,
- offset: matchIdx - iIndex
- }
- };
- // # Calculate the end position.
- matchIdx += queryLen;
- // Somewhat same array as above, but use a > 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;
- };
- this.renderMatches = function textLayerBuilder_renderMatches(matches) {
- // Early exit if there is nothing to render.
- if (matches.length === 0) {
- return;
- }
- var bidiTexts = this.textContent;
- var textDivs = this.textDivs;
- var prevEnd = null;
- var isSelectedPage = PDFFindController === null ?
- false : (this.pageIdx === PDFFindController.selected.pageIdx);
- var selectedMatchIdx = PDFFindController === null ?
- -1 : PDFFindController.selected.matchIdx;
- var highlightAll = PDFFindController === null ?
- false : PDFFindController.state.highlightAll;
- var infty = {
- divIdx: -1,
- offset: undefined
- };
- function beginText(begin, className) {
- var divIdx = begin.divIdx;
- var div = textDivs[divIdx];
- div.textContent = '';
- var content = bidiTexts[divIdx].str.substring(0, begin.offset);
- var node = document.createTextNode(content);
- if (className) {
- var isSelected = isSelectedPage &&
- divIdx === selectedMatchIdx;
- var span = document.createElement('span');
- span.className = className + (isSelected ? ' selected' : '');
- span.appendChild(node);
- div.appendChild(span);
- return;
- }
- div.appendChild(node);
- }
- function appendText(from, to, className) {
- var divIdx = from.divIdx;
- var div = textDivs[divIdx];
- var content = bidiTexts[divIdx].str.substring(from.offset, to.offset);
- 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);
- }
- function highlightDiv(divIdx, className) {
- textDivs[divIdx].className = className;
- }
- var i0 = selectedMatchIdx, i1 = i0 + 1, i;
- 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 (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 (isSelected && !this.isViewerInPresentationMode) {
- scrollIntoView(textDivs[begin.divIdx], { top: FIND_SCROLL_OFFSET_TOP,
- left: FIND_SCROLL_OFFSET_LEFT });
- }
- // 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) {
- appendText(prevEnd, infty);
- }
- // clears the divs and set the content until the begin point.
- beginText(begin);
- } else {
- appendText(prevEnd, begin);
- }
- if (begin.divIdx === end.divIdx) {
- appendText(begin, end, 'highlight' + highlightSuffix);
- } else {
- appendText(begin, infty, 'highlight begin' + highlightSuffix);
- for (var n = begin.divIdx + 1; n < end.divIdx; n++) {
- highlightDiv(n, 'highlight middle' + highlightSuffix);
- }
- beginText(end, 'highlight end' + highlightSuffix);
- }
- prevEnd = end;
- }
- if (prevEnd) {
- appendText(prevEnd, infty);
- }
- };
- this.updateMatches = function textLayerUpdateMatches() {
- // Only show matches, once all rendering is done.
- if (!this.renderingDone)
- return;
- // Clear out all matches.
- var matches = this.matches;
- var textDivs = this.textDivs;
- var bidiTexts = this.textContent;
- var clearedUntilDivIdx = -1;
- // Clear out all current matches.
- for (var i = 0; i < matches.length; i++) {
- var match = matches[i];
- var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
- for (var n = begin; n <= match.end.divIdx; n++) {
- var div = textDivs[n];
- div.textContent = bidiTexts[n].str;
- div.className = '';
- }
- clearedUntilDivIdx = match.end.divIdx + 1;
- }
- if (PDFFindController === null || !PDFFindController.active)
- return;
- // Convert the matches on the page controller into the match format used
- // for the textLayer.
- this.matches = matches =
- this.convertMatches(PDFFindController === null ?
- [] : (PDFFindController.pageMatches[this.pageIdx] || []));
- this.renderMatches(this.matches);
- };
- };
|