From 2f34fd46cbcbb0d91f7201a10f9e73263908dfa4 Mon Sep 17 00:00:00 2001 From: Yury Delendik 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 @@ + + + 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 @@ + 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 + From 56ccaea99b966e788793eacfac4acae42bf3141c Mon Sep 17 00:00:00 2001 From: Yury Delendik 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 @@ + + + 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. + 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 @@ + 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 (!timeout) { // Render right away - this.renderLayer(); - } else { // Schedule - var self = this; - this.renderTimer = setTimeout(function() { - self.renderLayer(); - self.renderTimer = null; - }, timeout); + if (this.textLayerRenderTask) { + this.textLayerRenderTask.cancel(); + this.textLayerRenderTask = null; } - }, - 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 + From bd7f121c8300b75aba4a21d5ef6022baa00b4eac Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Thu, 19 Nov 2015 11:03:52 -0600 Subject: [PATCH 3/3] Better "text" testing. --- test/driver.js | 176 +++++++++++++++++++-------------------- test/test_manifest.json | 6 -- test/text_layer_test.css | 38 +++++++++ web/viewer.css | 22 ++++- 4 files changed, 147 insertions(+), 95 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() {} - - NullTextLayerBuilder.prototype = { - beginLayout: function NullTextLayerBuilder_BeginLayout() {}, - endLayout: function NullTextLayerBuilder_EndLayout() {}, - appendText: function NullTextLayerBuilder_AppendText() {} - }; +var rasterizeTextLayer = (function rasterizeTextLayerClosure() { + var SVG_NS = 'http://www.w3.org/2000/svg'; - 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)); - - 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]); - - this.textCounter++; - }, - - 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(); - } - }; + 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); + + // 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); + + // 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); + + // 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;