From 2f34fd46cbcbb0d91f7201a10f9e73263908dfa4 Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Mon, 9 Nov 2015 19:24:15 -0600 Subject: [PATCH 1/3] Move CustomStyle. --- examples/acroforms/index.html | 1 + examples/helloworld/index.html | 1 + make.js | 1 + src/display/dom_utils.js | 73 ++++++++++++++++++++++++++++++++ test/test_slave.html | 1 + test/unit/unit_test.html | 1 + web/annotations_layer_builder.js | 4 +- web/pdf_page_view.js | 4 +- web/text_layer_builder.js | 4 +- web/ui_utils.js | 51 ---------------------- web/viewer.html | 1 + 11 files changed, 87 insertions(+), 55 deletions(-) create mode 100644 src/display/dom_utils.js diff --git a/examples/acroforms/index.html b/examples/acroforms/index.html index 3011e016a..1936ae1e1 100644 --- a/examples/acroforms/index.html +++ b/examples/acroforms/index.html @@ -11,6 +11,7 @@ <script src="../../src/display/webgl.js"></script> <script src="../../src/display/pattern_helper.js"></script> <script src="../../src/display/font_loader.js"></script> + <script src="../../src/display/dom_utils.js"></script> <script src="../../src/display/annotation_helper.js"></script> <script> diff --git a/examples/helloworld/index.html b/examples/helloworld/index.html index 748d60612..9f79d54c3 100644 --- a/examples/helloworld/index.html +++ b/examples/helloworld/index.html @@ -11,6 +11,7 @@ <script src="../../src/display/webgl.js"></script> <script src="../../src/display/pattern_helper.js"></script> <script src="../../src/display/font_loader.js"></script> + <script src="../../src/display/dom_utils.js"></script> <script src="../../src/display/annotation_helper.js"></script> <script> diff --git a/make.js b/make.js index 2b15f3c34..5b4e206f3 100644 --- a/make.js +++ b/make.js @@ -530,6 +530,7 @@ target.bundle = function(args) { 'display/webgl.js', 'display/pattern_helper.js', 'display/font_loader.js', + 'display/dom_utils.js', 'display/annotation_helper.js', 'display/svg.js' ]); diff --git a/src/display/dom_utils.js b/src/display/dom_utils.js new file mode 100644 index 000000000..5b76bc48f --- /dev/null +++ b/src/display/dom_utils.js @@ -0,0 +1,73 @@ +/* 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. + */ +/* globals PDFJS */ + +'use strict'; + +/** + * Optimised CSS custom property getter/setter. + * @class + */ +var CustomStyle = (function CustomStyleClosure() { + + // As noted on: http://www.zachstronaut.com/posts/2009/02/17/ + // animate-css-transforms-firefox-webkit.html + // in some versions of IE9 it is critical that ms appear in this list + // before Moz + var prefixes = ['ms', 'Moz', 'Webkit', 'O']; + var _cache = {}; + + function CustomStyle() {} + + CustomStyle.getProp = function get(propName, element) { + // check cache only when no element is given + if (arguments.length === 1 && typeof _cache[propName] === 'string') { + return _cache[propName]; + } + + element = element || document.documentElement; + var style = element.style, prefixed, uPropName; + + // test standard property first + if (typeof style[propName] === 'string') { + return (_cache[propName] = propName); + } + + // capitalize + uPropName = propName.charAt(0).toUpperCase() + propName.slice(1); + + // test vendor specific properties + for (var i = 0, l = prefixes.length; i < l; i++) { + prefixed = prefixes[i] + uPropName; + if (typeof style[prefixed] === 'string') { + return (_cache[propName] = prefixed); + } + } + + //if all fails then set to undefined + return (_cache[propName] = 'undefined'); + }; + + CustomStyle.setProp = function set(propName, element, str) { + var prop = this.getProp(propName); + if (prop !== 'undefined') { + element.style[prop] = str; + } + }; + + return CustomStyle; +})(); + +PDFJS.CustomStyle = CustomStyle; diff --git a/test/test_slave.html b/test/test_slave.html index 8a78e978b..3677b4e06 100644 --- a/test/test_slave.html +++ b/test/test_slave.html @@ -25,6 +25,7 @@ limitations under the License. <script src="../src/display/webgl.js"></script> <script src="../src/display/pattern_helper.js"></script> <script src="../src/display/font_loader.js"></script> + <script src="../src/display/dom_utils.js"></script> <script src="../src/display/annotation_helper.js"></script> <script src="driver.js"></script> </head> diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html index 5fe1f254b..687bd5201 100644 --- a/test/unit/unit_test.html +++ b/test/unit/unit_test.html @@ -36,6 +36,7 @@ <script src="../../src/core/ps_parser.js"></script> <script src="../../src/display/pattern_helper.js"></script> <script src="../../src/display/font_loader.js"></script> + <script src="../../src/display/dom_utils.js"></script> <script src="../../src/display/annotation_helper.js"></script> <script src="../../src/core/stream.js"></script> <script src="../../src/core/worker.js"></script> diff --git a/web/annotations_layer_builder.js b/web/annotations_layer_builder.js index 7e81c7e0e..752e43134 100644 --- a/web/annotations_layer_builder.js +++ b/web/annotations_layer_builder.js @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/*globals PDFJS, CustomStyle, mozL10n, SimpleLinkService */ +/*globals PDFJS, mozL10n, SimpleLinkService */ 'use strict'; @@ -27,6 +27,8 @@ * @class */ var AnnotationsLayerBuilder = (function AnnotationsLayerBuilderClosure() { + var CustomStyle = PDFJS.CustomStyle; + /** * @param {AnnotationsLayerBuilderOptions} options * @constructs AnnotationsLayerBuilder diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 52b3f59df..021fe7f4a 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals RenderingStates, PDFJS, CustomStyle, CSS_UNITS, getOutputScale, +/* globals RenderingStates, PDFJS, CSS_UNITS, getOutputScale, TextLayerBuilder, AnnotationsLayerBuilder, Promise, approximateFraction, roundToDivide */ @@ -36,6 +36,8 @@ var TEXT_LAYER_RENDER_DELAY = 200; // ms * @implements {IRenderableView} */ var PDFPageView = (function PDFPageViewClosure() { + var CustomStyle = PDFJS.CustomStyle; + /** * @constructs PDFPageView * @param {PDFPageViewOptions} options diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index 0691c8c14..805498de5 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals CustomStyle, PDFJS */ +/* globals PDFJS */ 'use strict'; @@ -119,7 +119,7 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { transform = 'rotate(' + rotation + 'deg) ' + transform; } if (transform) { - CustomStyle.setProp('transform' , textDiv, transform); + PDFJS.CustomStyle.setProp('transform' , textDiv, transform); } } } diff --git a/web/ui_utils.js b/web/ui_utils.js index 6f5aa26c7..5f4b4fdc7 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -23,57 +23,6 @@ var MAX_AUTO_SCALE = 1.25; var SCROLLBAR_PADDING = 40; var VERTICAL_PADDING = 5; -// optimised CSS custom property getter/setter -var CustomStyle = (function CustomStyleClosure() { - - // As noted on: http://www.zachstronaut.com/posts/2009/02/17/ - // animate-css-transforms-firefox-webkit.html - // in some versions of IE9 it is critical that ms appear in this list - // before Moz - var prefixes = ['ms', 'Moz', 'Webkit', 'O']; - var _cache = {}; - - function CustomStyle() {} - - CustomStyle.getProp = function get(propName, element) { - // check cache only when no element is given - if (arguments.length === 1 && typeof _cache[propName] === 'string') { - return _cache[propName]; - } - - element = element || document.documentElement; - var style = element.style, prefixed, uPropName; - - // test standard property first - if (typeof style[propName] === 'string') { - return (_cache[propName] = propName); - } - - // capitalize - uPropName = propName.charAt(0).toUpperCase() + propName.slice(1); - - // test vendor specific properties - for (var i = 0, l = prefixes.length; i < l; i++) { - prefixed = prefixes[i] + uPropName; - if (typeof style[prefixed] === 'string') { - return (_cache[propName] = prefixed); - } - } - - //if all fails then set to undefined - return (_cache[propName] = 'undefined'); - }; - - CustomStyle.setProp = function set(propName, element, str) { - var prop = this.getProp(propName); - if (prop !== 'undefined') { - element.style[prop] = str; - } - }; - - return CustomStyle; -})(); - var NullCharactersRegExp = /\x00/g; function removeNullCharacters(str) { diff --git a/web/viewer.html b/web/viewer.html index 8a65736ee..217d3f2a3 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -60,6 +60,7 @@ See https://github.com/adobe-type-tools/cmap-resources <script src="../src/display/webgl.js"></script> <script src="../src/display/pattern_helper.js"></script> <script src="../src/display/font_loader.js"></script> + <script src="../src/display/dom_utils.js"></script> <script src="../src/display/annotation_helper.js"></script> <script>PDFJS.workerSrc = '../src/worker_loader.js';</script> <!--#endif--> From 56ccaea99b966e788793eacfac4acae42bf3141c Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Tue, 10 Nov 2015 09:45:03 -0600 Subject: [PATCH 2/3] Move text layer building logic into src/display/text_layer.js --- examples/acroforms/index.html | 1 + examples/helloworld/index.html | 1 + make.js | 1 + src/display/text_layer.js | 237 +++++++++++++++++++++++++++++++++ test/font/font_test.html | 1 + test/test_slave.html | 1 + test/unit/unit_test.html | 1 + web/text_layer_builder.js | 168 ++++------------------- web/viewer.html | 1 + 9 files changed, 268 insertions(+), 144 deletions(-) create mode 100644 src/display/text_layer.js diff --git a/examples/acroforms/index.html b/examples/acroforms/index.html index 1936ae1e1..d6c644f44 100644 --- a/examples/acroforms/index.html +++ b/examples/acroforms/index.html @@ -13,6 +13,7 @@ <script src="../../src/display/font_loader.js"></script> <script src="../../src/display/dom_utils.js"></script> <script src="../../src/display/annotation_helper.js"></script> + <script src="../../src/display/text_layer.js"></script> <script> // Specify the main script used to create a new PDF.JS web worker. diff --git a/examples/helloworld/index.html b/examples/helloworld/index.html index 9f79d54c3..b113164f1 100644 --- a/examples/helloworld/index.html +++ b/examples/helloworld/index.html @@ -13,6 +13,7 @@ <script src="../../src/display/font_loader.js"></script> <script src="../../src/display/dom_utils.js"></script> <script src="../../src/display/annotation_helper.js"></script> + <script src="../../src/display/text_layer.js"></script> <script> // Specify the main script used to create a new PDF.JS web worker. diff --git a/make.js b/make.js index 5b4e206f3..72f0f3fc0 100644 --- a/make.js +++ b/make.js @@ -532,6 +532,7 @@ target.bundle = function(args) { 'display/font_loader.js', 'display/dom_utils.js', 'display/annotation_helper.js', + 'display/text_layer.js', 'display/svg.js' ]); diff --git a/src/display/text_layer.js b/src/display/text_layer.js new file mode 100644 index 000000000..576853eed --- /dev/null +++ b/src/display/text_layer.js @@ -0,0 +1,237 @@ +/* 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. + */ +/* globals PDFJS, createPromiseCapability */ + +'use strict'; + +/** + * 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 {PDFJS.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 = PDFJS.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 (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 (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; + 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) { + PDFJS.CustomStyle.setProp('transform' , textDiv, transform); + } + } + } + capability.resolve(); + } + + /** + * Text layer rendering task. + * + * @param {TextContent} textContent + * @param {HTMLElement} container + * @param {PDFJS.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; +})(); + +PDFJS.renderTextLayer = renderTextLayer; diff --git a/test/font/font_test.html b/test/font/font_test.html index 4869002ce..0f52cbd35 100644 --- a/test/font/font_test.html +++ b/test/font/font_test.html @@ -37,6 +37,7 @@ <script src="../../src/core/ps_parser.js"></script> <script src="../../src/display/pattern_helper.js"></script> <script src="../../src/display/annotation_helper.js"></script> + <script src="../../src/display/text_layer.js"></script> <script src="../../src/core/stream.js"></script> <script src="../../src/core/worker.js"></script> <script src="../../src/display/metadata.js"></script> diff --git a/test/test_slave.html b/test/test_slave.html index 3677b4e06..7236914d5 100644 --- a/test/test_slave.html +++ b/test/test_slave.html @@ -27,6 +27,7 @@ limitations under the License. <script src="../src/display/font_loader.js"></script> <script src="../src/display/dom_utils.js"></script> <script src="../src/display/annotation_helper.js"></script> + <script src="../src/display/text_layer.js"></script> <script src="driver.js"></script> </head> <body> diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html index 687bd5201..fcce65d19 100644 --- a/test/unit/unit_test.html +++ b/test/unit/unit_test.html @@ -38,6 +38,7 @@ <script src="../../src/display/font_loader.js"></script> <script src="../../src/display/dom_utils.js"></script> <script src="../../src/display/annotation_helper.js"></script> + <script src="../../src/display/text_layer.js"></script> <script src="../../src/core/stream.js"></script> <script src="../../src/core/worker.js"></script> <script src="../../src/display/metadata.js"></script> diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index 805498de5..6ef873684 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -16,14 +16,6 @@ '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. @@ -50,6 +42,7 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { this.viewport = options.viewport; this.textDivs = []; this.findController = options.findController || null; + this.textLayerRenderTask = null; this._bindMouse(); } @@ -68,67 +61,6 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { 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'); -//#if MOZCENTRAL || FIREFOX || GENERIC - canvas.mozOpaque = true; -//#endif - var ctx = canvas.getContext('2d', {alpha: false}); - - // 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) { - PDFJS.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 @@ -139,87 +71,35 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { return; } - if (this.renderTimer) { - clearTimeout(this.renderTimer); - this.renderTimer = null; + if (this.textLayerRenderTask) { + this.textLayerRenderTask.cancel(); + this.textLayerRenderTask = 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 (geom.str.length > 1) { - if (style.vertical) { - textDiv.dataset.canvasWidth = geom.height * this.viewport.scale; - } else { - textDiv.dataset.canvasWidth = geom.width * this.viewport.scale; - } - } + this.textDivs = []; + var textLayerFrag = document.createDocumentFragment(); + this.textLayerRenderTask = PDFJS.renderTextLayer({ + textContent: this.textContent, + container: textLayerFrag, + viewport: this.viewport, + textDivs: this.textDivs, + timeout: timeout + }); + this.textLayerRenderTask.promise.then(function () { + this.textLayerDiv.appendChild(textLayerFrag); + this._finishRendering(); + this.updateMatches(); + }.bind(this), function (reason) { + // canceled or failed to render text layer -- skipping errors + }); }, 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); + if (this.textLayerRenderTask) { + this.textLayerRenderTask.cancel(); + this.textLayerRenderTask = null; } + this.textContent = textContent; this.divContentDone = true; }, diff --git a/web/viewer.html b/web/viewer.html index 217d3f2a3..dbbaece08 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -62,6 +62,7 @@ See https://github.com/adobe-type-tools/cmap-resources <script src="../src/display/font_loader.js"></script> <script src="../src/display/dom_utils.js"></script> <script src="../src/display/annotation_helper.js"></script> + <script src="../src/display/text_layer.js"></script> <script>PDFJS.workerSrc = '../src/worker_loader.js';</script> <!--#endif--> From bd7f121c8300b75aba4a21d5ef6022baa00b4eac Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Thu, 19 Nov 2015 11:03:52 -0600 Subject: [PATCH 3/3] Better "text" testing. --- test/driver.js | 170 +++++++++++++++++++-------------------- test/test_manifest.json | 6 -- test/text_layer_test.css | 38 +++++++++ web/viewer.css | 22 ++++- 4 files changed, 144 insertions(+), 92 deletions(-) create mode 100644 test/text_layer_test.css diff --git a/test/driver.js b/test/driver.js index c6c295f88..c41ec7012 100644 --- a/test/driver.js +++ b/test/driver.js @@ -22,80 +22,71 @@ var PDF_TO_CSS_UNITS = 96.0 / 72.0; /** * @class */ -var NullTextLayerBuilder = (function NullTextLayerBuilderClosure() { - /** - * @constructs NullTextLayerBuilder - */ - function NullTextLayerBuilder() {} +var rasterizeTextLayer = (function rasterizeTextLayerClosure() { + var SVG_NS = 'http://www.w3.org/2000/svg'; - NullTextLayerBuilder.prototype = { - beginLayout: function NullTextLayerBuilder_BeginLayout() {}, - endLayout: function NullTextLayerBuilder_EndLayout() {}, - appendText: function NullTextLayerBuilder_AppendText() {} - }; - - return NullTextLayerBuilder; -})(); - -/** - * @class - */ -var SimpleTextLayerBuilder = (function SimpleTextLayerBuilderClosure() { - /** - * @constructs SimpleTextLayerBuilder - */ - function SimpleTextLayerBuilder(ctx, viewport) { - this.ctx = ctx; - this.viewport = viewport; - this.textCounter = 0; - - // clear canvas - ctx.save(); - ctx.fillStyle = 'rgb(255,255,255)'; - ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); - ctx.restore(); + var textLayerStylePromise = null; + function getTextLayerStyle() { + if (textLayerStylePromise) { + return textLayerStylePromise; + } + textLayerStylePromise = new Promise(function (resolve) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', './text_layer_test.css'); + xhr.onload = function () { + resolve(xhr.responseText); + }; + xhr.send(null); + }); + return textLayerStylePromise; } - SimpleTextLayerBuilder.prototype = { - appendText: function SimpleTextLayerBuilder_AppendText(geom, styles) { - var style = styles[geom.fontName]; - var ctx = this.ctx, viewport = this.viewport; - var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform); - var angle = Math.atan2(tx[1], tx[0]); - var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])); - var fontAscent = (style.ascent ? style.ascent * fontHeight : - (style.descent ? (1 + style.descent) * fontHeight : fontHeight)); + function rasterizeTextLayer(ctx, viewport, textContent) { + return new Promise(function (resolve) { + // Building SVG with size of the viewport. + var svg = document.createElementNS(SVG_NS, 'svg:svg'); + svg.setAttribute('width', viewport.width + 'px'); + svg.setAttribute('height', viewport.height + 'px'); + // items are transformed to have 1px font size + svg.setAttribute('font-size', 1); - ctx.save(); - ctx.beginPath(); - ctx.strokeStyle = 'red'; - ctx.fillStyle = 'yellow'; - ctx.translate(tx[4] + (fontAscent * Math.sin(angle)), - tx[5] - (fontAscent * Math.cos(angle))); - ctx.rotate(angle); - ctx.rect(0, 0, geom.width * viewport.scale, geom.height * viewport.scale); - ctx.stroke(); - ctx.fill(); - ctx.restore(); - ctx.font = fontHeight + 'px ' + style.fontFamily; - ctx.fillStyle = 'black'; - ctx.fillText(geom.str, tx[4], tx[5]); + // Adding element to host our HTML (style + text layer div). + var foreignObject = document.createElementNS(SVG_NS, 'svg:foreignObject'); + foreignObject.setAttribute('x', '0'); + foreignObject.setAttribute('y', '0'); + foreignObject.setAttribute('width', viewport.width + 'px'); + foreignObject.setAttribute('height', viewport.height + 'px'); + var style = document.createElement('style'); + var stylePromise = getTextLayerStyle(); + foreignObject.appendChild(style); + var div = document.createElement('div'); + div.className = 'textLayer'; + foreignObject.appendChild(div); - this.textCounter++; - }, + // Rendering text layer as HTML. + var task = PDFJS.renderTextLayer({ + textContent: textContent, + container: div, + viewport: viewport + }); + Promise.all([stylePromise, task.promise]).then(function (results) { + style.textContent = results[0]; + svg.appendChild(foreignObject); - setTextContent: - function SimpleTextLayerBuilder_SetTextContent(textContent) { - this.ctx.save(); - var textItems = textContent.items; - for (var i = 0, ii = textItems.length; i < ii; i++) { - this.appendText(textItems[i], textContent.styles); - } - this.ctx.restore(); - } - }; + // We need to have UTF-8 encoded XML. + var svg_xml = unescape(encodeURIComponent( + (new XMLSerializer()).serializeToString(svg))); + var img = new Image(); + img.src = 'data:image/svg+xml;base64,' + btoa(svg_xml); + img.onload = function () { + ctx.drawImage(img, 0, 0); + resolve(); + }; + }); + }); + } - return SimpleTextLayerBuilder; + return rasterizeTextLayer; })(); /** @@ -328,35 +319,44 @@ var Driver = (function DriverClosure() { self.canvas.height = viewport.height; self._clearCanvas(); - var drawContext, textLayerBuilder; - var resolveInitPromise; - var initPromise = new Promise(function (resolve) { - resolveInitPromise = resolve; - }); + var textLayerCanvas; + var initPromise; if (task.type === 'text') { // Using a dummy canvas for PDF context drawing operations - if (!self.dummyCanvas) { - self.dummyCanvas = document.createElement('canvas'); + textLayerCanvas = self.textLayerCanvas; + if (!textLayerCanvas) { + textLayerCanvas = document.createElement('canvas'); + self.textLayerCanvas = textLayerCanvas; } - drawContext = self.dummyCanvas.getContext('2d'); + textLayerCanvas.width = viewport.width; + textLayerCanvas.height = viewport.height; + var textLayerContext = textLayerCanvas.getContext('2d'); + textLayerContext.clearRect(0, 0, + textLayerCanvas.width, textLayerCanvas.height); // The text builder will draw its content on the test canvas - textLayerBuilder = new SimpleTextLayerBuilder(ctx, viewport); - - page.getTextContent().then(function(textContent) { - textLayerBuilder.setTextContent(textContent); - resolveInitPromise(); + initPromise = page.getTextContent().then(function(textContent) { + return rasterizeTextLayer(textLayerContext, viewport, + textContent); }); } else { - drawContext = ctx; - textLayerBuilder = new NullTextLayerBuilder(); - resolveInitPromise(); + textLayerCanvas = null; + initPromise = Promise.resolve(); } var renderContext = { - canvasContext: drawContext, + canvasContext: ctx, viewport: viewport }; var completeRender = (function(error) { - page.destroy(); + // if text layer is present, compose it on top of the page + if (textLayerCanvas) { + ctx.save(); + ctx.globalCompositeOperation = 'screen'; + ctx.fillStyle = 'rgb(128, 255, 128)'; // making it green + ctx.fillRect(0, 0, viewport.width, viewport.height); + ctx.restore(); + ctx.drawImage(textLayerCanvas, 0, 0); + } + page.cleanup(); task.stats = page.stats; page.stats = new StatTimer(); self._snapshot(task, error); diff --git a/test/test_manifest.json b/test/test_manifest.json index d478031c1..1bb299cee 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -323,12 +323,6 @@ "lastPage": 1, "about": "The same file as issue2337." }, - { "id": "thuluthfont-text", - "file": "pdfs/ThuluthFeatures.pdf", - "md5": "b7e18bf7a3d6a9c82aefa12d721072fc", - "rounds": 1, - "type": "text" - }, { "id": "freeculture", "file": "pdfs/freeculture.pdf", "md5": "dcdf3a8268e6a18938a42d5149efcfca", diff --git a/test/text_layer_test.css b/test/text_layer_test.css new file mode 100644 index 000000000..ed62e6c99 --- /dev/null +++ b/test/text_layer_test.css @@ -0,0 +1,38 @@ +/* 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. + */ + +/* Used in 'text' tests */ + +.textLayer { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; +} +.textLayer > div { + position: absolute; + white-space: pre; + -webkit-transform-origin: 0% 0%; + -moz-transform-origin: 0% 0%; + -o-transform-origin: 0% 0%; + -ms-transform-origin: 0% 0%; + transform-origin: 0% 0%; + border: solid 1px rgba(255, 0, 0, 0.5); + background-color: rgba(255, 255, 32, 0.1); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} diff --git a/web/viewer.css b/web/viewer.css index b012631e6..b7826849f 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -1515,7 +1515,27 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { font-size: 10px; } -#viewer.textLayer-visible .textLayer > div, +#viewer.textLayer-visible .textLayer { + opacity: 1.0; +} + +#viewer.textLayer-visible .canvasWrapper { + background-color: rgb(128,255,128); +} + +#viewer.textLayer-visible .canvasWrapper canvas { + mix-blend-mode: screen; +} + +#viewer.textLayer-visible .textLayer > div { + background-color: rgba(255, 255, 0, 0.1); + color: black; + border: solid 1px rgba(255, 0, 0, 0.5); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + #viewer.textLayer-hover .textLayer > div:hover { background-color: white; color: black;