/* Copyright 2015 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. */ 'use strict'; (function (root, factory) { if (typeof define === 'function' && define.amd) { define('pdfjs/display/text_layer', ['exports', 'pdfjs/shared/util', 'pdfjs/display/dom_utils'], factory); } else if (typeof exports !== 'undefined') { factory(exports, require('../shared/util.js'), require('./dom_utils.js')); } else { factory((root.pdfjsDisplayTextLayer = {}), root.pdfjsSharedUtil, root.pdfjsDisplayDOMUtils); } }(this, function (exports, sharedUtil, displayDOMUtils) { var Util = sharedUtil.Util; var createPromiseCapability = sharedUtil.createPromiseCapability; var CustomStyle = displayDOMUtils.CustomStyle; var getDefaultSetting = displayDOMUtils.getDefaultSetting; /** * Text layer render parameters. * * @typedef {Object} TextLayerRenderParameters * @property {TextContent} textContent - Text content to render (the object is * returned by the page's getTextContent() method). * @property {HTMLElement} container - HTML element that will contain text runs. * @property {PageViewport} viewport - The target viewport to properly * layout the text runs. * @property {Array} textDivs - (optional) HTML elements that are correspond * the text items of the textContent input. This is output and shall be * initially be set to empty array. * @property {number} timeout - (optional) Delay in milliseconds before * rendering of the text runs occurs. */ var renderTextLayer = (function renderTextLayerClosure() { var MAX_TEXT_DIVS_TO_RENDER = 100000; var NonWhitespaceRegexp = /\S/; function isAllWhitespace(str) { return !NonWhitespaceRegexp.test(str); } function appendText(textDivs, viewport, geom, styles) { var style = styles[geom.fontName]; var textDiv = document.createElement('div'); textDivs.push(textDiv); if (isAllWhitespace(geom.str)) { textDiv.dataset.isWhitespace = true; return; } var tx = Util.transform(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 (getDefaultSetting('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 (geom.str.length > 1) { if (style.vertical) { textDiv.dataset.canvasWidth = geom.height * viewport.scale; } else { textDiv.dataset.canvasWidth = geom.width * viewport.scale; } } } function render(task) { if (task._canceled) { return; } var textLayerFrag = task._container; var textDivs = task._textDivs; var capability = task._capability; var textDivsLength = textDivs.length; // 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) { capability.resolve(); return; } var canvas = document.createElement('canvas'); //#if MOZCENTRAL || FIREFOX || GENERIC canvas.mozOpaque = true; //#endif var ctx = canvas.getContext('2d', {alpha: false}); 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; textLayerFrag.appendChild(textDiv); var transform; if (textDiv.dataset.canvasWidth !== undefined && width > 0) { // 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); } } capability.resolve(); } /** * Text layer rendering task. * * @param {TextContent} textContent * @param {HTMLElement} container * @param {PageViewport} viewport * @param {Array} textDivs * @private */ function TextLayerRenderTask(textContent, container, viewport, textDivs) { this._textContent = textContent; this._container = container; this._viewport = viewport; textDivs = textDivs || []; this._textDivs = textDivs; this._canceled = false; this._capability = createPromiseCapability(); this._renderTimer = null; } TextLayerRenderTask.prototype = { get promise() { return this._capability.promise; }, cancel: function TextLayer_cancel() { this._canceled = true; if (this._renderTimer !== null) { clearTimeout(this._renderTimer); this._renderTimer = null; } this._capability.reject('canceled'); }, _render: function TextLayer_render(timeout) { var textItems = this._textContent.items; var styles = this._textContent.styles; var textDivs = this._textDivs; var viewport = this._viewport; for (var i = 0, len = textItems.length; i < len; i++) { appendText(textDivs, viewport, textItems[i], styles); } if (!timeout) { // Render right away render(this); } else { // Schedule var self = this; this._renderTimer = setTimeout(function() { render(self); self._renderTimer = null; }, timeout); } } }; /** * Starts rendering of the text layer. * * @param {TextLayerRenderParameters} renderParameters * @returns {TextLayerRenderTask} */ function renderTextLayer(renderParameters) { var task = new TextLayerRenderTask(renderParameters.textContent, renderParameters.container, renderParameters.viewport, renderParameters.textDivs); task._render(renderParameters.timeout); return task; } return renderTextLayer; })(); exports.renderTextLayer = renderTextLayer; }));