From 863d583ae1e5234382c0d13c2c9f8cb88f864d06 Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Sat, 27 Sep 2014 11:58:42 -0500 Subject: [PATCH 01/10] Renames page_view.js file. --- web/{page_view.js => pdf_page_view.js} | 0 web/pdf_viewer.js | 2 +- web/viewer.html | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename web/{page_view.js => pdf_page_view.js} (100%) diff --git a/web/page_view.js b/web/pdf_page_view.js similarity index 100% rename from web/page_view.js rename to web/pdf_page_view.js diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 47e109fab..190c37ed4 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -31,7 +31,7 @@ var PresentationModeState = { var IGNORE_CURRENT_POSITION_ON_ZOOM = false; //#include pdf_rendering_queue.js -//#include page_view.js +//#include pdf_page_view.js //#include text_layer_builder.js /** diff --git a/web/viewer.html b/web/viewer.html index f562dbd66..c2d3dff69 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -69,7 +69,7 @@ http://sourceforge.net/adobe/cmap/wiki/License/ <script src="download_manager.js"></script> <script src="view_history.js"></script> <script src="pdf_rendering_queue.js"></script> - <script src="page_view.js"></script> + <script src="pdf_page_view.js"></script> <script src="text_layer_builder.js"></script> <script src="pdf_viewer.js"></script> <script src="thumbnail_view.js"></script> From f68678086dbbc0214eed883905a830f6fc14c1ff Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Sat, 27 Sep 2014 13:03:28 -0500 Subject: [PATCH 02/10] Simple restructuring PageView into PDFPageView --- web/pdf_find_controller.js | 3 +- web/pdf_page_view.js | 1053 ++++++++++++++++++------------------ web/pdf_viewer.js | 24 +- web/viewer.js | 3 +- 4 files changed, 556 insertions(+), 527 deletions(-) diff --git a/web/pdf_find_controller.js b/web/pdf_find_controller.js index 1aa912109..c54fc8ade 100644 --- a/web/pdf_find_controller.js +++ b/web/pdf_find_controller.js @@ -197,8 +197,6 @@ var PDFFindController = (function PDFFindControllerClosure() { }, updatePage: function PDFFindController_updatePage(index) { - var page = this.pdfViewer.getPageView(index); - if (this.selected.pageIdx === index) { // If the page is selected, scroll the page into view, which triggers // rendering the page, which adds the textLayer. Once the textLayer is @@ -206,6 +204,7 @@ var PDFFindController = (function PDFFindControllerClosure() { this.pdfViewer.scrollPageIntoView(index + 1); } + var page = this.pdfViewer.getPageView(index); if (page.textLayer) { page.textLayer.updateMatches(); } diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 92ed39b7d..7e6aa17b3 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -20,591 +20,616 @@ 'use strict'; /** - * @constructor - * @param {HTMLDivElement} container - The viewer element. - * @param {number} id - The page unique ID (normally its number). - * @param {number} scale - The page scale display. - * @param {PageViewport} defaultViewport - The page viewport. - * @param {IPDFLinkService} linkService - The navigation/linking service. - * @param {PDFRenderingQueue} renderingQueue - The rendering queue object. - * @param {Cache} cache - The page cache. - * @param {PDFPageSource} pageSource - * @param {PDFViewer} viewer - * + * @typedef {Object} PDFPageViewOptions + * @property {HTMLDivElement} container - The viewer element. + * @property {number} id - The page unique ID (normally its number). + * @property {number} scale - The page scale display. + * @property {PageViewport} defaultViewport - The page viewport. + * @property {IPDFLinkService} linkService - The navigation/linking service. + * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. + * @property {Cache} cache - The page cache. + * @property {PDFPageSource} pageSource + * @property {PDFViewer} viewer + */ + +/** + * @class * @implements {IRenderableView} */ -var PageView = function pageView(container, id, scale, defaultViewport, - linkService, renderingQueue, cache, - pageSource, viewer) { - this.id = id; - this.renderingId = 'page' + id; - - this.rotation = 0; - this.scale = scale || 1.0; - this.viewport = defaultViewport; - this.pdfPageRotate = defaultViewport.rotation; - this.hasRestrictedScaling = false; - - this.linkService = linkService; - this.renderingQueue = renderingQueue; - this.cache = cache; - this.pageSource = pageSource; - this.viewer = viewer; - - this.renderingState = RenderingStates.INITIAL; - this.resume = null; - - this.textLayer = null; - - this.zoomLayer = null; - - this.annotationLayer = null; - - var anchor = document.createElement('a'); - anchor.name = '' + this.id; - - var div = this.el = document.createElement('div'); - div.id = 'pageContainer' + this.id; - div.className = 'page'; - div.style.width = Math.floor(this.viewport.width) + 'px'; - div.style.height = Math.floor(this.viewport.height) + 'px'; - - container.appendChild(anchor); - container.appendChild(div); - - this.setPdfPage = function pageViewSetPdfPage(pdfPage) { - this.pdfPage = pdfPage; - this.pdfPageRotate = pdfPage.rotate; - var totalRotation = (this.rotation + this.pdfPageRotate) % 360; - this.viewport = pdfPage.getViewport(this.scale * CSS_UNITS, totalRotation); - this.stats = pdfPage.stats; - this.reset(); - }; +var PDFPageView = (function PDFPageViewClosure() { + /** + * @constructs PDFPageView + * @param {PDFPageViewOptions} options + */ + function PDFPageView(options) { + var container = options.container; + var id = options.id; + var scale = options.scale; + var defaultViewport = options.defaultViewport; + var linkService = options.linkService; + var renderingQueue = options.renderingQueue; + var cache = options.cache; + var pageSource = options.pageSource; + var viewer = options.viewer; + + this.id = id; + this.renderingId = 'page' + id; + + this.rotation = 0; + this.scale = scale || 1.0; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + this.hasRestrictedScaling = false; + + this.linkService = linkService; + this.renderingQueue = renderingQueue; + this.cache = cache; + this.pageSource = pageSource; + this.viewer = viewer; + + this.renderingState = RenderingStates.INITIAL; + this.resume = null; + + this.textLayer = null; - this.destroy = function pageViewDestroy() { this.zoomLayer = null; - this.reset(); - if (this.pdfPage) { - this.pdfPage.destroy(); - } - }; - this.reset = function pageViewReset(keepAnnotations) { - if (this.renderTask) { - this.renderTask.cancel(); - } - this.resume = null; - this.renderingState = RenderingStates.INITIAL; + this.annotationLayer = null; + var anchor = document.createElement('a'); + anchor.name = '' + this.id; + + var div = document.createElement('div'); + div.id = 'pageContainer' + this.id; + div.className = 'page'; div.style.width = Math.floor(this.viewport.width) + 'px'; div.style.height = Math.floor(this.viewport.height) + 'px'; + this.el = div; // TODO replace 'el' property usage + this.div = div; + + container.appendChild(anchor); + container.appendChild(div); + } + + PDFPageView.prototype = { + setPdfPage: function PDFPageView_setPdfPage(pdfPage) { + this.pdfPage = pdfPage; + this.pdfPageRotate = pdfPage.rotate; + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = pdfPage.getViewport(this.scale * CSS_UNITS, + totalRotation); + this.stats = pdfPage.stats; + this.reset(); + }, - var childNodes = div.childNodes; - for (var i = div.childNodes.length - 1; i >= 0; i--) { - var node = childNodes[i]; - if ((this.zoomLayer && this.zoomLayer === node) || - (keepAnnotations && this.annotationLayer === node)) { - continue; + destroy: function PDFPageView_destroy() { + this.zoomLayer = null; + this.reset(); + if (this.pdfPage) { + this.pdfPage.destroy(); } - div.removeChild(node); - } - div.removeAttribute('data-loaded'); + }, - if (keepAnnotations) { - if (this.annotationLayer) { - // Hide annotationLayer until all elements are resized - // so they are not displayed on the already-resized page - this.annotationLayer.setAttribute('hidden', 'true'); + reset: function PDFPageView_reset(keepAnnotations) { + if (this.renderTask) { + this.renderTask.cancel(); + } + this.resume = null; + this.renderingState = RenderingStates.INITIAL; + + var div = this.div; + div.style.width = Math.floor(this.viewport.width) + 'px'; + div.style.height = Math.floor(this.viewport.height) + 'px'; + + var childNodes = div.childNodes; + for (var i = div.childNodes.length - 1; i >= 0; i--) { + var node = childNodes[i]; + if ((this.zoomLayer && this.zoomLayer === node) || + (keepAnnotations && this.annotationLayer === node)) { + continue; + } + div.removeChild(node); + } + div.removeAttribute('data-loaded'); + + if (keepAnnotations) { + if (this.annotationLayer) { + // Hide annotationLayer until all elements are resized + // so they are not displayed on the already-resized page + this.annotationLayer.setAttribute('hidden', 'true'); + } + } else { + this.annotationLayer = null; } - } else { - this.annotationLayer = null; - } - - if (this.canvas) { - // Zeroing the width and height causes Firefox to release graphics - // resources immediately, which can greatly reduce memory consumption. - this.canvas.width = 0; - this.canvas.height = 0; - delete this.canvas; - } - - this.loadingIconDiv = document.createElement('div'); - this.loadingIconDiv.className = 'loadingIcon'; - div.appendChild(this.loadingIconDiv); - }; - this.update = function pageViewUpdate(scale, rotation) { - this.scale = scale || this.scale; + if (this.canvas) { + // Zeroing the width and height causes Firefox to release graphics + // resources immediately, which can greatly reduce memory consumption. + this.canvas.width = 0; + this.canvas.height = 0; + delete this.canvas; + } - if (typeof rotation !== 'undefined') { - this.rotation = rotation; - } + this.loadingIconDiv = document.createElement('div'); + this.loadingIconDiv.className = 'loadingIcon'; + div.appendChild(this.loadingIconDiv); + }, - var totalRotation = (this.rotation + this.pdfPageRotate) % 360; - this.viewport = this.viewport.clone({ - scale: this.scale * CSS_UNITS, - rotation: totalRotation - }); + update: function PDFPageView_update(scale, rotation) { + this.scale = scale || this.scale; - var isScalingRestricted = false; - if (this.canvas && PDFJS.maxCanvasPixels > 0) { - var ctx = this.canvas.getContext('2d'); - var outputScale = getOutputScale(ctx); - var pixelsInViewport = this.viewport.width * this.viewport.height; - var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport); - if (((Math.floor(this.viewport.width) * outputScale.sx) | 0) * - ((Math.floor(this.viewport.height) * outputScale.sy) | 0) > - PDFJS.maxCanvasPixels) { - isScalingRestricted = true; + if (typeof rotation !== 'undefined') { + this.rotation = rotation; } - } - - if (this.canvas && - (PDFJS.useOnlyCssZoom || - (this.hasRestrictedScaling && isScalingRestricted))) { - this.cssTransform(this.canvas, true); - return; - } else if (this.canvas && !this.zoomLayer) { - this.zoomLayer = this.canvas.parentNode; - this.zoomLayer.style.position = 'absolute'; - } - if (this.zoomLayer) { - this.cssTransform(this.zoomLayer.firstChild); - } - this.reset(true); - }; - this.cssTransform = function pageCssTransform(canvas, redrawAnnotations) { - // Scale canvas, canvas wrapper, and page container. - var width = this.viewport.width; - var height = this.viewport.height; - canvas.style.width = canvas.parentNode.style.width = div.style.width = + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = this.viewport.clone({ + scale: this.scale * CSS_UNITS, + rotation: totalRotation + }); + + var isScalingRestricted = false; + if (this.canvas && PDFJS.maxCanvasPixels > 0) { + var ctx = this.canvas.getContext('2d'); + var outputScale = getOutputScale(ctx); + var pixelsInViewport = this.viewport.width * this.viewport.height; + var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport); + if (((Math.floor(this.viewport.width) * outputScale.sx) | 0) * + ((Math.floor(this.viewport.height) * outputScale.sy) | 0) > + PDFJS.maxCanvasPixels) { + isScalingRestricted = true; + } + } + + if (this.canvas && + (PDFJS.useOnlyCssZoom || + (this.hasRestrictedScaling && isScalingRestricted))) { + this.cssTransform(this.canvas, true); + return; + } else if (this.canvas && !this.zoomLayer) { + this.zoomLayer = this.canvas.parentNode; + this.zoomLayer.style.position = 'absolute'; + } + if (this.zoomLayer) { + this.cssTransform(this.zoomLayer.firstChild); + } + this.reset(true); + }, + + cssTransform: function PDFPageView_transform(canvas, redrawAnnotations) { + // Scale canvas, canvas wrapper, and page container. + var width = this.viewport.width; + var height = this.viewport.height; + var div = this.div; + canvas.style.width = canvas.parentNode.style.width = div.style.width = Math.floor(width) + 'px'; - canvas.style.height = canvas.parentNode.style.height = div.style.height = + canvas.style.height = canvas.parentNode.style.height = div.style.height = Math.floor(height) + 'px'; - // The canvas may have been originally rotated, so rotate relative to that. - var relativeRotation = this.viewport.rotation - canvas._viewport.rotation; - var absRotation = Math.abs(relativeRotation); - var scaleX = 1, scaleY = 1; - if (absRotation === 90 || absRotation === 270) { - // Scale x and y because of the rotation. - scaleX = height / width; - scaleY = width / height; - } - var cssTransform = 'rotate(' + relativeRotation + 'deg) ' + - 'scale(' + scaleX + ',' + scaleY + ')'; - CustomStyle.setProp('transform', canvas, cssTransform); - - if (this.textLayer) { - // Rotating the text layer is more complicated since the divs inside the - // the text layer are rotated. - // TODO: This could probably be simplified by drawing the text layer in - // one orientation then rotating overall. - var textLayerViewport = this.textLayer.viewport; - var textRelativeRotation = this.viewport.rotation - - textLayerViewport.rotation; - var textAbsRotation = Math.abs(textRelativeRotation); - var scale = width / textLayerViewport.width; - if (textAbsRotation === 90 || textAbsRotation === 270) { - scale = width / textLayerViewport.height; + // The canvas may have been originally rotated, rotate relative to that. + var relativeRotation = this.viewport.rotation - canvas._viewport.rotation; + var absRotation = Math.abs(relativeRotation); + var scaleX = 1, scaleY = 1; + if (absRotation === 90 || absRotation === 270) { + // Scale x and y because of the rotation. + scaleX = height / width; + scaleY = width / height; } - var textLayerDiv = this.textLayer.textLayerDiv; - var transX, transY; - switch (textAbsRotation) { - case 0: - transX = transY = 0; - break; - case 90: - transX = 0; - transY = '-' + textLayerDiv.style.height; - break; - case 180: - transX = '-' + textLayerDiv.style.width; - transY = '-' + textLayerDiv.style.height; - break; - case 270: - transX = '-' + textLayerDiv.style.width; - transY = 0; - break; - default: - console.error('Bad rotation value.'); - break; + var cssTransform = 'rotate(' + relativeRotation + 'deg) ' + + 'scale(' + scaleX + ',' + scaleY + ')'; + CustomStyle.setProp('transform', canvas, cssTransform); + + if (this.textLayer) { + // Rotating the text layer is more complicated since the divs inside the + // the text layer are rotated. + // TODO: This could probably be simplified by drawing the text layer in + // one orientation then rotating overall. + var textLayerViewport = this.textLayer.viewport; + var textRelativeRotation = this.viewport.rotation - + textLayerViewport.rotation; + var textAbsRotation = Math.abs(textRelativeRotation); + var scale = width / textLayerViewport.width; + if (textAbsRotation === 90 || textAbsRotation === 270) { + scale = width / textLayerViewport.height; + } + var textLayerDiv = this.textLayer.textLayerDiv; + var transX, transY; + switch (textAbsRotation) { + case 0: + transX = transY = 0; + break; + case 90: + transX = 0; + transY = '-' + textLayerDiv.style.height; + break; + case 180: + transX = '-' + textLayerDiv.style.width; + transY = '-' + textLayerDiv.style.height; + break; + case 270: + transX = '-' + textLayerDiv.style.width; + transY = 0; + break; + default: + console.error('Bad rotation value.'); + break; + } + CustomStyle.setProp('transform', textLayerDiv, + 'rotate(' + textAbsRotation + 'deg) ' + + 'scale(' + scale + ', ' + scale + ') ' + + 'translate(' + transX + ', ' + transY + ')'); + CustomStyle.setProp('transformOrigin', textLayerDiv, '0% 0%'); } - CustomStyle.setProp('transform', textLayerDiv, - 'rotate(' + textAbsRotation + 'deg) ' + - 'scale(' + scale + ', ' + scale + ') ' + - 'translate(' + transX + ', ' + transY + ')'); - CustomStyle.setProp('transformOrigin', textLayerDiv, '0% 0%'); - } - - if (redrawAnnotations && this.annotationLayer) { - setupAnnotations(div, this.pdfPage, this.viewport); - } - }; - Object.defineProperty(this, 'width', { - get: function PageView_getWidth() { + if (redrawAnnotations && this.annotationLayer) { + this.setupAnnotations(); + } + }, + + get width() { return this.viewport.width; }, - enumerable: true - }); - Object.defineProperty(this, 'height', { - get: function PageView_getHeight() { + get height() { return this.viewport.height; }, - enumerable: true - }); - - var self = this; - - function setupAnnotations(pageDiv, pdfPage, viewport) { - function bindLink(link, dest) { - link.href = linkService.getDestinationHash(dest); - link.onclick = function pageViewSetupLinksOnclick() { + setupAnnotations: function PDFPageView_setupAnnotations() { + function bindLink(link, dest) { + link.href = linkService.getDestinationHash(dest); + link.onclick = function pageViewSetupLinksOnclick() { + if (dest) { + linkService.navigateTo(dest); + } + return false; + }; if (dest) { - linkService.navigateTo(dest); + link.className = 'internalLink'; } - return false; - }; - if (dest) { + } + + function bindNamedAction(link, action) { + link.href = linkService.getAnchorUrl(''); + link.onclick = function pageViewSetupNamedActionOnClick() { + linkService.executeNamedAction(action); + return false; + }; link.className = 'internalLink'; } - } - function bindNamedAction(link, action) { - link.href = linkService.getAnchorUrl(''); - link.onclick = function pageViewSetupNamedActionOnClick() { - linkService.executeNamedAction(action); - return false; - }; - link.className = 'internalLink'; - } - - pdfPage.getAnnotations().then(function(annotationsData) { - viewport = viewport.clone({ dontFlip: true }); - var transform = viewport.transform; - var transformStr = 'matrix(' + transform.join(',') + ')'; - var data, element, i, ii; - - if (self.annotationLayer) { - // If an annotationLayer already exists, refresh its children's - // transformation matrices - for (i = 0, ii = annotationsData.length; i < ii; i++) { - data = annotationsData[i]; - element = self.annotationLayer.querySelector( - '[data-annotation-id="' + data.id + '"]'); - if (element) { - CustomStyle.setProp('transform', element, transformStr); - } - } - // See this.reset() - self.annotationLayer.removeAttribute('hidden'); - } else { - for (i = 0, ii = annotationsData.length; i < ii; i++) { - data = annotationsData[i]; - if (!data || !data.hasHtml) { - continue; + var linkService = this.linkService; + var pageDiv = this.div; + var pdfPage = this.pdfPage; + var viewport = this.viewport; + var self = this; + + pdfPage.getAnnotations().then(function(annotationsData) { + viewport = viewport.clone({ dontFlip: true }); + var transform = viewport.transform; + var transformStr = 'matrix(' + transform.join(',') + ')'; + var data, element, i, ii; + + if (self.annotationLayer) { + // If an annotationLayer already exists, refresh its children's + // transformation matrices + for (i = 0, ii = annotationsData.length; i < ii; i++) { + data = annotationsData[i]; + element = self.annotationLayer.querySelector( + '[data-annotation-id="' + data.id + '"]'); + if (element) { + CustomStyle.setProp('transform', element, transformStr); + } } + // See this.reset() + self.annotationLayer.removeAttribute('hidden'); + } else { + for (i = 0, ii = annotationsData.length; i < ii; i++) { + data = annotationsData[i]; + if (!data || !data.hasHtml) { + continue; + } - element = PDFJS.AnnotationUtils.getHtmlElement(data, - pdfPage.commonObjs); - element.setAttribute('data-annotation-id', data.id); - mozL10n.translate(element); - - var rect = data.rect; - var view = pdfPage.view; - rect = PDFJS.Util.normalizeRect([ - rect[0], - view[3] - rect[1] + view[1], - rect[2], - view[3] - rect[3] + view[1] - ]); - element.style.left = rect[0] + 'px'; - element.style.top = rect[1] + 'px'; - element.style.position = 'absolute'; - - CustomStyle.setProp('transform', element, transformStr); - var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px'; - CustomStyle.setProp('transformOrigin', element, transformOriginStr); - - if (data.subtype === 'Link' && !data.url) { - var link = element.getElementsByTagName('a')[0]; - if (link) { - if (data.action) { - bindNamedAction(link, data.action); - } else { - bindLink(link, ('dest' in data) ? data.dest : null); + element = PDFJS.AnnotationUtils.getHtmlElement(data, + pdfPage.commonObjs); + element.setAttribute('data-annotation-id', data.id); + mozL10n.translate(element); + + var rect = data.rect; + var view = pdfPage.view; + rect = PDFJS.Util.normalizeRect([ + rect[0], + view[3] - rect[1] + view[1], + rect[2], + view[3] - rect[3] + view[1] + ]); + element.style.left = rect[0] + 'px'; + element.style.top = rect[1] + 'px'; + element.style.position = 'absolute'; + + CustomStyle.setProp('transform', element, transformStr); + var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px'; + CustomStyle.setProp('transformOrigin', element, transformOriginStr); + + if (data.subtype === 'Link' && !data.url) { + var link = element.getElementsByTagName('a')[0]; + if (link) { + if (data.action) { + bindNamedAction(link, data.action); + } else { + bindLink(link, ('dest' in data) ? data.dest : null); + } } } - } - if (!self.annotationLayer) { - var annotationLayerDiv = document.createElement('div'); - annotationLayerDiv.className = 'annotationLayer'; - pageDiv.appendChild(annotationLayerDiv); - self.annotationLayer = annotationLayerDiv; - } + if (!self.annotationLayer) { + var annotationLayerDiv = document.createElement('div'); + annotationLayerDiv.className = 'annotationLayer'; + pageDiv.appendChild(annotationLayerDiv); + self.annotationLayer = annotationLayerDiv; + } - self.annotationLayer.appendChild(element); + self.annotationLayer.appendChild(element); + } } - } - }); - } + }); + }, - this.getPagePoint = function pageViewGetPagePoint(x, y) { - return this.viewport.convertToPdfPoint(x, y); - }; + getPagePoint: function PDFPageView_getPagePoint(x, y) { + return this.viewport.convertToPdfPoint(x, y); + }, - this.draw = function pageviewDraw(callback) { - var pdfPage = this.pdfPage; - - if (this.pagePdfPromise) { - return; - } - if (!pdfPage) { - var promise = this.pageSource.getPage(); - promise.then(function(pdfPage) { - delete this.pagePdfPromise; - this.setPdfPage(pdfPage); - this.draw(callback); - }.bind(this)); - this.pagePdfPromise = promise; - return; - } - - if (this.renderingState !== RenderingStates.INITIAL) { - console.error('Must be in new state before drawing'); - } - - this.renderingState = RenderingStates.RUNNING; - - var viewport = this.viewport; - // Wrap the canvas so if it has a css transform for highdpi the overflow - // will be hidden in FF. - var canvasWrapper = document.createElement('div'); - canvasWrapper.style.width = div.style.width; - canvasWrapper.style.height = div.style.height; - canvasWrapper.classList.add('canvasWrapper'); - - var canvas = document.createElement('canvas'); - canvas.id = 'page' + this.id; - canvasWrapper.appendChild(canvas); - if (this.annotationLayer) { - // annotationLayer needs to stay on top - div.insertBefore(canvasWrapper, this.annotationLayer); - } else { - div.appendChild(canvasWrapper); - } - this.canvas = canvas; - - var ctx = canvas.getContext('2d'); - var outputScale = getOutputScale(ctx); - - if (PDFJS.useOnlyCssZoom) { - var actualSizeViewport = viewport.clone({ scale: CSS_UNITS }); - // Use a scale that will make the canvas be the original intended size - // of the page. - outputScale.sx *= actualSizeViewport.width / viewport.width; - outputScale.sy *= actualSizeViewport.height / viewport.height; - outputScale.scaled = true; - } - - if (PDFJS.maxCanvasPixels > 0) { - var pixelsInViewport = viewport.width * viewport.height; - var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport); - if (outputScale.sx > maxScale || outputScale.sy > maxScale) { - outputScale.sx = maxScale; - outputScale.sy = maxScale; - outputScale.scaled = true; - this.hasRestrictedScaling = true; - } else { - this.hasRestrictedScaling = false; + draw: function PDFPageView_draw(callback) { + var pdfPage = this.pdfPage; + + if (this.pagePdfPromise) { + return; + } + if (!pdfPage) { + var promise = this.pageSource.getPage(); + promise.then(function(pdfPage) { + delete this.pagePdfPromise; + this.setPdfPage(pdfPage); + this.draw(callback); + }.bind(this)); + this.pagePdfPromise = promise; + return; } - } - - canvas.width = (Math.floor(viewport.width) * outputScale.sx) | 0; - canvas.height = (Math.floor(viewport.height) * outputScale.sy) | 0; - canvas.style.width = Math.floor(viewport.width) + 'px'; - canvas.style.height = Math.floor(viewport.height) + 'px'; - // Add the viewport so it's known what it was originally drawn with. - canvas._viewport = viewport; - - var textLayerDiv = null; - var textLayer = null; - if (!PDFJS.disableTextLayer) { - textLayerDiv = document.createElement('div'); - textLayerDiv.className = 'textLayer'; - textLayerDiv.style.width = canvas.style.width; - textLayerDiv.style.height = canvas.style.height; + + if (this.renderingState !== RenderingStates.INITIAL) { + console.error('Must be in new state before drawing'); + } + + this.renderingState = RenderingStates.RUNNING; + + var viewport = this.viewport; + var div = this.div; + // Wrap the canvas so if it has a css transform for highdpi the overflow + // will be hidden in FF. + var canvasWrapper = document.createElement('div'); + canvasWrapper.style.width = div.style.width; + canvasWrapper.style.height = div.style.height; + canvasWrapper.classList.add('canvasWrapper'); + + var canvas = document.createElement('canvas'); + canvas.id = 'page' + this.id; + canvasWrapper.appendChild(canvas); if (this.annotationLayer) { // annotationLayer needs to stay on top - div.insertBefore(textLayerDiv, this.annotationLayer); + div.insertBefore(canvasWrapper, this.annotationLayer); } else { - div.appendChild(textLayerDiv); + div.appendChild(canvasWrapper); } + this.canvas = canvas; - textLayer = this.viewer.createTextLayerBuilder(textLayerDiv, this.id - 1, - this.viewport); - } - this.textLayer = textLayer; - - // TODO(mack): use data attributes to store these - ctx._scaleX = outputScale.sx; - ctx._scaleY = outputScale.sy; - if (outputScale.scaled) { - ctx.scale(outputScale.sx, outputScale.sy); - } - - // Rendering area - - var self = this; - function pageViewDrawCallback(error) { - // The renderTask may have been replaced by a new one, so only remove the - // reference to the renderTask if it matches the one that is triggering - // this callback. - if (renderTask === self.renderTask) { - self.renderTask = null; + var ctx = canvas.getContext('2d'); + var outputScale = getOutputScale(ctx); + + if (PDFJS.useOnlyCssZoom) { + var actualSizeViewport = viewport.clone({ scale: CSS_UNITS }); + // Use a scale that will make the canvas be the original intended size + // of the page. + outputScale.sx *= actualSizeViewport.width / viewport.width; + outputScale.sy *= actualSizeViewport.height / viewport.height; + outputScale.scaled = true; } - if (error === 'cancelled') { - return; + if (PDFJS.maxCanvasPixels > 0) { + var pixelsInViewport = viewport.width * viewport.height; + var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport); + if (outputScale.sx > maxScale || outputScale.sy > maxScale) { + outputScale.sx = maxScale; + outputScale.sy = maxScale; + outputScale.scaled = true; + this.hasRestrictedScaling = true; + } else { + this.hasRestrictedScaling = false; + } } - self.renderingState = RenderingStates.FINISHED; + canvas.width = (Math.floor(viewport.width) * outputScale.sx) | 0; + canvas.height = (Math.floor(viewport.height) * outputScale.sy) | 0; + canvas.style.width = Math.floor(viewport.width) + 'px'; + canvas.style.height = Math.floor(viewport.height) + 'px'; + // Add the viewport so it's known what it was originally drawn with. + canvas._viewport = viewport; + + var textLayerDiv = null; + var textLayer = null; + if (!PDFJS.disableTextLayer) { + textLayerDiv = document.createElement('div'); + textLayerDiv.className = 'textLayer'; + textLayerDiv.style.width = canvas.style.width; + textLayerDiv.style.height = canvas.style.height; + if (this.annotationLayer) { + // annotationLayer needs to stay on top + div.insertBefore(textLayerDiv, this.annotationLayer); + } else { + div.appendChild(textLayerDiv); + } - if (self.loadingIconDiv) { - div.removeChild(self.loadingIconDiv); - delete self.loadingIconDiv; + textLayer = this.viewer.createTextLayerBuilder(textLayerDiv, + this.id - 1, + this.viewport); } + this.textLayer = textLayer; - if (self.zoomLayer) { - div.removeChild(self.zoomLayer); - self.zoomLayer = null; + // TODO(mack): use data attributes to store these + ctx._scaleX = outputScale.sx; + ctx._scaleY = outputScale.sy; + if (outputScale.scaled) { + ctx.scale(outputScale.sx, outputScale.sy); } - self.error = error; - self.stats = pdfPage.stats; - self.updateStats(); - if (self.onAfterDraw) { - self.onAfterDraw(); - } + // Rendering area - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('pagerender', true, true, { - pageNumber: pdfPage.pageNumber - }); - div.dispatchEvent(event); - - callback(); - } - - var renderContext = { - canvasContext: ctx, - viewport: this.viewport, - // intent: 'default', // === 'display' - continueCallback: function pdfViewcContinueCallback(cont) { - if (!self.renderingQueue.isHighestPriority(self)) { - self.renderingState = RenderingStates.PAUSED; - self.resume = function resumeCallback() { - self.renderingState = RenderingStates.RUNNING; - cont(); - }; + var self = this; + function pageViewDrawCallback(error) { + // The renderTask may have been replaced by a new one, so only remove + // the reference to the renderTask if it matches the one that is + // triggering this callback. + if (renderTask === self.renderTask) { + self.renderTask = null; + } + + if (error === 'cancelled') { return; } - cont(); - } - }; - var renderTask = this.renderTask = this.pdfPage.render(renderContext); - - this.renderTask.promise.then( - function pdfPageRenderCallback() { - pageViewDrawCallback(null); - if (textLayer) { - self.pdfPage.getTextContent().then( - function textContentResolved(textContent) { - textLayer.setTextContent(textContent); - } - ); + + self.renderingState = RenderingStates.FINISHED; + + if (self.loadingIconDiv) { + div.removeChild(self.loadingIconDiv); + delete self.loadingIconDiv; } - }, - function pdfPageRenderError(error) { - pageViewDrawCallback(error); - } - ); - setupAnnotations(div, pdfPage, this.viewport); - div.setAttribute('data-loaded', true); + if (self.zoomLayer) { + div.removeChild(self.zoomLayer); + self.zoomLayer = null; + } - // Add the page to the cache at the start of drawing. That way it can be - // evicted from the cache and destroyed even if we pause its rendering. - cache.push(this); - }; + self.error = error; + self.stats = pdfPage.stats; + self.updateStats(); + if (self.onAfterDraw) { + self.onAfterDraw(); + } + + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('pagerender', true, true, { + pageNumber: pdfPage.pageNumber + }); + div.dispatchEvent(event); - this.beforePrint = function pageViewBeforePrint() { - var pdfPage = this.pdfPage; - - var viewport = pdfPage.getViewport(1); - // Use the same hack we use for high dpi displays for printing to get better - // output until bug 811002 is fixed in FF. - var PRINT_OUTPUT_SCALE = 2; - var canvas = document.createElement('canvas'); - canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE; - canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE; - canvas.style.width = (PRINT_OUTPUT_SCALE * viewport.width) + 'pt'; - canvas.style.height = (PRINT_OUTPUT_SCALE * viewport.height) + 'pt'; - var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' + - (1 / PRINT_OUTPUT_SCALE) + ')'; - CustomStyle.setProp('transform' , canvas, cssScale); - CustomStyle.setProp('transformOrigin' , canvas, '0% 0%'); - - var printContainer = document.getElementById('printContainer'); - var canvasWrapper = document.createElement('div'); - canvasWrapper.style.width = viewport.width + 'pt'; - canvasWrapper.style.height = viewport.height + 'pt'; - canvasWrapper.appendChild(canvas); - printContainer.appendChild(canvasWrapper); - - canvas.mozPrintCallback = function(obj) { - var ctx = obj.context; - - ctx.save(); - ctx.fillStyle = 'rgb(255, 255, 255)'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.restore(); - ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE); + callback(); + } var renderContext = { canvasContext: ctx, - viewport: viewport, - intent: 'print' + viewport: this.viewport, + // intent: 'default', // === 'display' + continueCallback: function pdfViewcContinueCallback(cont) { + if (!self.renderingQueue.isHighestPriority(self)) { + self.renderingState = RenderingStates.PAUSED; + self.resume = function resumeCallback() { + self.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + } }; + var renderTask = this.renderTask = this.pdfPage.render(renderContext); + + this.renderTask.promise.then( + function pdfPageRenderCallback() { + pageViewDrawCallback(null); + if (textLayer) { + self.pdfPage.getTextContent().then( + function textContentResolved(textContent) { + textLayer.setTextContent(textContent); + } + ); + } + }, + function pdfPageRenderError(error) { + pageViewDrawCallback(error); + } + ); - pdfPage.render(renderContext).promise.then(function() { - // Tell the printEngine that rendering this canvas/page has finished. - obj.done(); - }, function(error) { - console.error(error); - // Tell the printEngine that rendering this canvas/page has failed. - // This will make the print proces stop. - if ('abort' in obj) { - obj.abort(); - } else { + this.setupAnnotations(); + div.setAttribute('data-loaded', true); + + // Add the page to the cache at the start of drawing. That way it can be + // evicted from the cache and destroyed even if we pause its rendering. + this.cache.push(this); + }, + + beforePrint: function PDFPageView_beforePrint() { + var pdfPage = this.pdfPage; + + var viewport = pdfPage.getViewport(1); + // Use the same hack we use for high dpi displays for printing to get + // better output until bug 811002 is fixed in FF. + var PRINT_OUTPUT_SCALE = 2; + var canvas = document.createElement('canvas'); + canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE; + canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE; + canvas.style.width = (PRINT_OUTPUT_SCALE * viewport.width) + 'pt'; + canvas.style.height = (PRINT_OUTPUT_SCALE * viewport.height) + 'pt'; + var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' + + (1 / PRINT_OUTPUT_SCALE) + ')'; + CustomStyle.setProp('transform' , canvas, cssScale); + CustomStyle.setProp('transformOrigin' , canvas, '0% 0%'); + + var printContainer = document.getElementById('printContainer'); + var canvasWrapper = document.createElement('div'); + canvasWrapper.style.width = viewport.width + 'pt'; + canvasWrapper.style.height = viewport.height + 'pt'; + canvasWrapper.appendChild(canvas); + printContainer.appendChild(canvasWrapper); + + canvas.mozPrintCallback = function(obj) { + var ctx = obj.context; + + ctx.save(); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE); + + var renderContext = { + canvasContext: ctx, + viewport: viewport, + intent: 'print' + }; + + pdfPage.render(renderContext).promise.then(function() { + // Tell the printEngine that rendering this canvas/page has finished. obj.done(); - } - }); - }; - }; + }, function(error) { + console.error(error); + // Tell the printEngine that rendering this canvas/page has failed. + // This will make the print proces stop. + if ('abort' in obj) { + obj.abort(); + } else { + obj.done(); + } + }); + }; + }, - this.updateStats = function pageViewUpdateStats() { - if (!this.stats) { - return; - } + updateStats: function PDFPageView_updateStats() { + if (!this.stats) { + return; + } - if (PDFJS.pdfBug && Stats.enabled) { - var stats = this.stats; - Stats.add(this.id, stats); - } + if (PDFJS.pdfBug && Stats.enabled) { + var stats = this.stats; + Stats.add(this.id, stats); + } + }, }; -}; + + return PDFPageView; +})(); diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 190c37ed4..bd87a941a 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - /*globals watchScroll, Cache, DEFAULT_CACHE_SIZE, PageView, UNKNOWN_SCALE, + /*globals watchScroll, Cache, DEFAULT_CACHE_SIZE, PDFPageView, UNKNOWN_SCALE, SCROLLBAR_PADDING, VERTICAL_PADDING, MAX_AUTO_SCALE, CSS_UNITS, DEFAULT_SCALE, scrollIntoView, getVisibleElements, RenderingStates, PDFJS, Promise, TextLayerBuilder, PDFRenderingQueue */ @@ -236,10 +236,17 @@ var PDFViewer = (function pdfViewer() { var viewport = pdfPage.getViewport(scale * CSS_UNITS); for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { var pageSource = new PDFPageSource(pdfDocument, pageNum); - var pageView = new PageView(this.viewer, pageNum, scale, - viewport.clone(), this.linkService, - this.renderingQueue, this.cache, - pageSource, this); + var pageView = new PDFPageView({ + container: this.viewer, + id: pageNum, + scale: scale, + defaultViewport: viewport.clone(), + linkService: this.linkService, + renderingQueue: this.renderingQueue, + cache: this.cache, + pageSource: pageSource, + viewer: this + }); bindOnAfterDraw(pageView); this.pages.push(pageView); } @@ -398,7 +405,6 @@ var PDFViewer = (function pdfViewer() { scrollPageIntoView: function PDFViewer_scrollPageIntoView(pageNumber, dest) { var pageView = this.pages[pageNumber - 1]; - var pageViewDiv = pageView.el; if (this.presentationModeState === PresentationModeState.FULLSCREEN) { @@ -412,7 +418,7 @@ var PDFViewer = (function pdfViewer() { this._setScale(this.currentScaleValue, true); } if (!dest) { - scrollIntoView(pageViewDiv); + scrollIntoView(pageView.div); return; } @@ -475,7 +481,7 @@ var PDFViewer = (function pdfViewer() { } if (scale === 'page-fit' && !dest[4]) { - scrollIntoView(pageViewDiv); + scrollIntoView(pageView.div); return; } @@ -486,7 +492,7 @@ var PDFViewer = (function pdfViewer() { var left = Math.min(boundingRect[0][0], boundingRect[1][0]); var top = Math.min(boundingRect[0][1], boundingRect[1][1]); - scrollIntoView(pageViewDiv, { left: left, top: top }); + scrollIntoView(pageView.div, { left: left, top: top }); }, _updateLocation: function (firstPage) { diff --git a/web/viewer.js b/web/viewer.js index 439fb0106..5528dd36c 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -16,7 +16,7 @@ */ /* globals PDFJS, PDFBug, FirefoxCom, Stats, Cache, ProgressBar, DownloadManager, getFileName, scrollIntoView, getPDFFileNameFromURL, - PDFHistory, Preferences, SidebarView, ViewHistory, PageView, + PDFHistory, Preferences, SidebarView, ViewHistory, PDFThumbnailViewer, URL, noContextMenuHandler, SecondaryToolbar, PasswordPrompt, PresentationMode, HandTool, Promise, DocumentProperties, DocumentOutlineView, DocumentAttachmentsView, @@ -1353,7 +1353,6 @@ var PDFViewerApplication = { rotatePages: function pdfViewRotatePages(delta) { var pageNumber = this.page; - this.pageRotation = (this.pageRotation + 360 + delta) % 360; this.pdfViewer.pagesRotation = this.pageRotation; this.pdfThumbnailViewer.pagesRotation = this.pageRotation; From 7663942ee5ac344326b4b32a1e6e333209ab1e34 Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Sun, 28 Sep 2014 09:35:33 -0500 Subject: [PATCH 03/10] Creates IPDFTextLayerFactory interface --- web/interfaces.js | 14 ++++++++++++++ web/pdf_page_view.js | 14 +++++++------- web/pdf_viewer.js | 12 ++++++++---- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/web/interfaces.js b/web/interfaces.js index 5f0ad04c6..91ac65d9b 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -83,3 +83,17 @@ ILastScrollSource.prototype = { */ get lastScroll() {}, }; + +/** + * @interface + */ +function IPDFTextLayerFactory() {} +IPDFTextLayerFactory.prototype = { + /** + * @param {HTMLDivElement} textLayerDiv + * @param {number} pageIndex + * @param {PageViewport} viewport + * @returns {TextLayerBuilder} + */ + createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) {} +}; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 7e6aa17b3..d0a081827 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -29,7 +29,7 @@ * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. * @property {Cache} cache - The page cache. * @property {PDFPageSource} pageSource - * @property {PDFViewer} viewer + * @property {IPDFTextLayerFactory} textLayerFactory */ /** @@ -50,7 +50,7 @@ var PDFPageView = (function PDFPageViewClosure() { var renderingQueue = options.renderingQueue; var cache = options.cache; var pageSource = options.pageSource; - var viewer = options.viewer; + var textLayerFactory = options.textLayerFactory; this.id = id; this.renderingId = 'page' + id; @@ -65,7 +65,7 @@ var PDFPageView = (function PDFPageViewClosure() { this.renderingQueue = renderingQueue; this.cache = cache; this.pageSource = pageSource; - this.viewer = viewer; + this.textLayerFactory = textLayerFactory; this.renderingState = RenderingStates.INITIAL; this.resume = null; @@ -454,7 +454,7 @@ var PDFPageView = (function PDFPageViewClosure() { var textLayerDiv = null; var textLayer = null; - if (!PDFJS.disableTextLayer) { + if (this.textLayerFactory) { textLayerDiv = document.createElement('div'); textLayerDiv.className = 'textLayer'; textLayerDiv.style.width = canvas.style.width; @@ -466,9 +466,9 @@ var PDFPageView = (function PDFPageViewClosure() { div.appendChild(textLayerDiv); } - textLayer = this.viewer.createTextLayerBuilder(textLayerDiv, - this.id - 1, - this.viewport); + textLayer = this.textLayerFactory.createTextLayerBuilder(textLayerDiv, + this.id - 1, + this.viewport); } this.textLayer = textLayer; diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index bd87a941a..292d765cd 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -236,6 +236,10 @@ var PDFViewer = (function pdfViewer() { var viewport = pdfPage.getViewport(scale * CSS_UNITS); for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { var pageSource = new PDFPageSource(pdfDocument, pageNum); + var textLayerFactory = null; + if (!PDFJS.disableTextLayer) { + textLayerFactory = this; + } var pageView = new PDFPageView({ container: this.viewer, id: pageNum, @@ -245,7 +249,7 @@ var PDFViewer = (function pdfViewer() { renderingQueue: this.renderingQueue, cache: this.cache, pageSource: pageSource, - viewer: this + textLayerFactory: textLayerFactory }); bindOnAfterDraw(pageView); this.pages.push(pageView); @@ -629,9 +633,9 @@ var PDFViewer = (function pdfViewer() { }, /** - * @param textLayerDiv {HTMLDivElement} - * @param pageIndex {number} - * @param viewport {PageViewport} + * @param {HTMLDivElement} textLayerDiv + * @param {number} pageIndex + * @param {PageViewport} viewport * @returns {TextLayerBuilder} */ createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) { From fe4ac8678198937aca49c02fb7d2e83de183cd0e Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Mon, 29 Sep 2014 08:11:46 -0500 Subject: [PATCH 04/10] Removes PDFPageSource --- web/pdf_page_view.js | 20 +------------- web/pdf_viewer.js | 58 +++++++++++++++++++--------------------- web/thumbnail_view.js | 61 +++++++++++++++++++++++++------------------ web/viewer.js | 2 +- 4 files changed, 64 insertions(+), 77 deletions(-) diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index d0a081827..38556efc6 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -28,7 +28,6 @@ * @property {IPDFLinkService} linkService - The navigation/linking service. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. * @property {Cache} cache - The page cache. - * @property {PDFPageSource} pageSource * @property {IPDFTextLayerFactory} textLayerFactory */ @@ -49,7 +48,6 @@ var PDFPageView = (function PDFPageViewClosure() { var linkService = options.linkService; var renderingQueue = options.renderingQueue; var cache = options.cache; - var pageSource = options.pageSource; var textLayerFactory = options.textLayerFactory; this.id = id; @@ -64,7 +62,6 @@ var PDFPageView = (function PDFPageViewClosure() { this.linkService = linkService; this.renderingQueue = renderingQueue; this.cache = cache; - this.pageSource = pageSource; this.textLayerFactory = textLayerFactory; this.renderingState = RenderingStates.INITIAL; @@ -378,28 +375,13 @@ var PDFPageView = (function PDFPageViewClosure() { }, draw: function PDFPageView_draw(callback) { - var pdfPage = this.pdfPage; - - if (this.pagePdfPromise) { - return; - } - if (!pdfPage) { - var promise = this.pageSource.getPage(); - promise.then(function(pdfPage) { - delete this.pagePdfPromise; - this.setPdfPage(pdfPage); - this.draw(callback); - }.bind(this)); - this.pagePdfPromise = promise; - return; - } - if (this.renderingState !== RenderingStates.INITIAL) { console.error('Must be in new state before drawing'); } this.renderingState = RenderingStates.RUNNING; + var pdfPage = this.pdfPage; var viewport = this.viewport; var div = this.div; // Wrap the canvas so if it has a css transform for highdpi the overflow diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 292d765cd..6441eae9e 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -235,7 +235,6 @@ var PDFViewer = (function pdfViewer() { var scale = this._currentScale || 1.0; var viewport = pdfPage.getViewport(scale * CSS_UNITS); for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { - var pageSource = new PDFPageSource(pdfDocument, pageNum); var textLayerFactory = null; if (!PDFJS.disableTextLayer) { textLayerFactory = this; @@ -248,7 +247,6 @@ var PDFViewer = (function pdfViewer() { linkService: this.linkService, renderingQueue: this.renderingQueue, cache: this.cache, - pageSource: pageSource, textLayerFactory: textLayerFactory }); bindOnAfterDraw(pageView); @@ -299,6 +297,7 @@ var PDFViewer = (function pdfViewer() { this._currentScaleValue = null; this.location = null; this._pagesRotation = 0; + this._pagesRequests = []; var container = this.viewer; while (container.hasChildNodes()) { @@ -614,13 +613,38 @@ var PDFViewer = (function pdfViewer() { } }, + /** + * @param {PDFPageView} pageView + * @returns {PDFPage} + * @private + */ + _ensurePdfPageLoaded: function (pageView) { + if (pageView.pdfPage) { + return Promise.resolve(pageView.pdfPage); + } + var pageNumber = pageView.id; + if (this._pagesRequests[pageNumber]) { + return this._pagesRequests[pageNumber]; + } + var promise = this.pdfDocument.getPage(pageNumber).then( + function (pdfPage) { + pageView.setPdfPage(pdfPage); + this._pagesRequests[pageNumber] = null; + return pdfPage; + }.bind(this)); + this._pagesRequests[pageNumber] = promise; + return promise; + }, + forceRendering: function (currentlyVisiblePages) { var visiblePages = currentlyVisiblePages || this._getVisiblePages(); var pageView = this.renderingQueue.getHighestPriority(visiblePages, this.pages, this.scroll.down); if (pageView) { - this.renderingQueue.renderView(pageView); + this._ensurePdfPageLoaded(pageView).then(function () { + this.renderingQueue.renderView(pageView); + }.bind(this)); return true; } return false; @@ -705,31 +729,3 @@ var SimpleLinkService = (function SimpleLinkServiceClosure() { }; return SimpleLinkService; })(); - -/** - * PDFPage object source. - * @class - */ -var PDFPageSource = (function PDFPageSourceClosure() { - /** - * @constructs - * @param {PDFDocument} pdfDocument - * @param {number} pageNumber - * @constructor - */ - function PDFPageSource(pdfDocument, pageNumber) { - this.pdfDocument = pdfDocument; - this.pageNumber = pageNumber; - } - - PDFPageSource.prototype = /** @lends PDFPageSource.prototype */ { - /** - * @returns {Promise<PDFPage>} - */ - getPage: function () { - return this.pdfDocument.getPage(this.pageNumber); - } - }; - - return PDFPageSource; -})(); diff --git a/web/thumbnail_view.js b/web/thumbnail_view.js index 1e16a7de4..f8c207ee9 100644 --- a/web/thumbnail_view.js +++ b/web/thumbnail_view.js @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals mozL10n, RenderingStates, Promise, scrollIntoView, PDFPageSource, +/* globals mozL10n, RenderingStates, Promise, scrollIntoView, watchScroll, getVisibleElements */ 'use strict'; @@ -28,13 +28,11 @@ var THUMBNAIL_SCROLL_MARGIN = -19; * @param defaultViewport * @param linkService * @param renderingQueue - * @param pageSource * * @implements {IRenderableView} */ var ThumbnailView = function thumbnailView(container, id, defaultViewport, - linkService, renderingQueue, - pageSource) { + linkService, renderingQueue) { var anchor = document.createElement('a'); anchor.href = linkService.getAnchorUrl('#page=' + id); anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}'); @@ -80,7 +78,6 @@ var ThumbnailView = function thumbnailView(container, id, defaultViewport, this.hasImage = false; this.renderingState = RenderingStates.INITIAL; this.renderingQueue = renderingQueue; - this.pageSource = pageSource; this.setPdfPage = function thumbnailViewSetPdfPage(pdfPage) { this.pdfPage = pdfPage; @@ -143,15 +140,6 @@ var ThumbnailView = function thumbnailView(container, id, defaultViewport, }; this.draw = function thumbnailViewDraw(callback) { - if (!this.pdfPage) { - var promise = this.pageSource.getPage(this.id); - promise.then(function(pdfPage) { - this.setPdfPage(pdfPage); - this.draw(callback); - }.bind(this)); - return; - } - if (this.renderingState !== RenderingStates.INITIAL) { console.error('Must be in new state before drawing'); } @@ -204,18 +192,14 @@ var ThumbnailView = function thumbnailView(container, id, defaultViewport, return tempCanvas; } - this.setImage = function thumbnailViewSetImage(img) { - if (!this.pdfPage) { - var promise = this.pageSource.getPage(); - promise.then(function(pdfPage) { - this.setPdfPage(pdfPage); - this.setImage(img); - }.bind(this)); - return; - } + this.setImage = function thumbnailViewSetImage(pageView) { + var img = pageView.canvas; if (this.hasImage || !img) { return; } + if (this.pdfPage) { + this.setPdfPage(pageView.pdfPage); + } this.renderingState = RenderingStates.FINISHED; var ctx = this.getPageDrawContext(); @@ -330,6 +314,7 @@ var PDFThumbnailViewer = (function pdfThumbnailViewer() { _resetView: function () { this.thumbnails = []; this._pagesRotation = 0; + this._pagesRequests = []; }, setDocument: function (pdfDocument) { @@ -351,15 +336,37 @@ var PDFThumbnailViewer = (function pdfThumbnailViewer() { var pagesCount = pdfDocument.numPages; var viewport = firstPage.getViewport(1.0); for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { - var pageSource = new PDFPageSource(pdfDocument, pageNum); var thumbnail = new ThumbnailView(this.container, pageNum, viewport.clone(), this.linkService, - this.renderingQueue, pageSource); + this.renderingQueue); this.thumbnails.push(thumbnail); } }.bind(this)); }, + /** + * @param {PDFPageView} pageView + * @returns {PDFPage} + * @private + */ + _ensurePdfPageLoaded: function (thumbView) { + if (thumbView.pdfPage) { + return Promise.resolve(thumbView.pdfPage); + } + var pageNumber = thumbView.id; + if (this._pagesRequests[pageNumber]) { + return this._pagesRequests[pageNumber]; + } + var promise = this.pdfDocument.getPage(pageNumber).then( + function (pdfPage) { + thumbView.setPdfPage(pdfPage); + this._pagesRequests[pageNumber] = null; + return pdfPage; + }.bind(this)); + this._pagesRequests[pageNumber] = promise; + return promise; + }, + ensureThumbnailVisible: function PDFThumbnailViewer_ensureThumbnailVisible(page) { // Ensure that the thumbnail of the current page is visible @@ -373,7 +380,9 @@ var PDFThumbnailViewer = (function pdfThumbnailViewer() { this.thumbnails, this.scroll.down); if (thumbView) { - this.renderingQueue.renderView(thumbView); + this._ensurePdfPageLoaded(thumbView).then(function () { + this.renderingQueue.renderView(thumbView); + }.bind(this)); return true; } return false; diff --git a/web/viewer.js b/web/viewer.js index 5528dd36c..802d554dd 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -1734,7 +1734,7 @@ document.addEventListener('pagerendered', function (e) { var pageView = PDFViewerApplication.pdfViewer.getPageView(pageIndex); var thumbnailView = PDFViewerApplication.pdfThumbnailViewer. getThumbnail(pageIndex); - thumbnailView.setImage(pageView.canvas); + thumbnailView.setImage(pageView); //#if (FIREFOX || MOZCENTRAL) //if (pageView.textLayer && pageView.textLayer.textDivs && From 9f384bbb41f780b258d832d1c7170b570aedac95 Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Mon, 29 Sep 2014 11:05:28 -0500 Subject: [PATCH 05/10] Creates AnnotationsLayerBuilder. --- web/annotations_layer_builder.js | 156 +++++++++++++++++++++++++++++++ web/interfaces.js | 13 +++ web/pdf_page_view.js | 132 ++++---------------------- web/pdf_viewer.js | 21 ++++- web/viewer.html | 1 + 5 files changed, 208 insertions(+), 115 deletions(-) create mode 100644 web/annotations_layer_builder.js diff --git a/web/annotations_layer_builder.js b/web/annotations_layer_builder.js new file mode 100644 index 000000000..309f7c033 --- /dev/null +++ b/web/annotations_layer_builder.js @@ -0,0 +1,156 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* Copyright 2014 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, CustomStyle, mozL10n */ + +'use strict'; + +/** + * @typedef {Object} AnnotationsLayerBuilderOptions + * @property {HTMLDivElement} pageDiv + * @property {PDFPage} pdfPage + * @property {IPDFLinkService} linkService + */ + +/** + * @class + */ +var AnnotationsLayerBuilder = (function AnnotationsLayerBuilderClosure() { + /** + * @param {AnnotationsLayerBuilderOptions} options + * @constructs AnnotationsLayerBuilder + */ + function AnnotationsLayerBuilder(options) { + this.pageDiv = options.pageDiv; + this.pdfPage = options.pdfPage; + this.linkService = options.linkService; + + this.div = null; + } + AnnotationsLayerBuilder.prototype = + /** @lends AnnotationsLayerBuilder.prototype */ { + + /** + * @param {PageViewport} viewport + */ + setupAnnotations: + function AnnotationsLayerBuilder_setupAnnotations(viewport) { + function bindLink(link, dest) { + link.href = linkService.getDestinationHash(dest); + link.onclick = function annotationsLayerBuilderLinksOnclick() { + if (dest) { + linkService.navigateTo(dest); + } + return false; + }; + if (dest) { + link.className = 'internalLink'; + } + } + + function bindNamedAction(link, action) { + link.href = linkService.getAnchorUrl(''); + link.onclick = function annotationsLayerBuilderNamedActionOnClick() { + linkService.executeNamedAction(action); + return false; + }; + link.className = 'internalLink'; + } + + var linkService = this.linkService; + var pdfPage = this.pdfPage; + var self = this; + + pdfPage.getAnnotations().then(function (annotationsData) { + viewport = viewport.clone({ dontFlip: true }); + var transform = viewport.transform; + var transformStr = 'matrix(' + transform.join(',') + ')'; + var data, element, i, ii; + + if (self.div) { + // If an annotationLayer already exists, refresh its children's + // transformation matrices + for (i = 0, ii = annotationsData.length; i < ii; i++) { + data = annotationsData[i]; + element = self.div.querySelector( + '[data-annotation-id="' + data.id + '"]'); + if (element) { + CustomStyle.setProp('transform', element, transformStr); + } + } + // See PDFPageView.reset() + self.div.removeAttribute('hidden'); + } else { + for (i = 0, ii = annotationsData.length; i < ii; i++) { + data = annotationsData[i]; + if (!data || !data.hasHtml) { + continue; + } + + element = PDFJS.AnnotationUtils.getHtmlElement(data, + pdfPage.commonObjs); + element.setAttribute('data-annotation-id', data.id); + mozL10n.translate(element); + + var rect = data.rect; + var view = pdfPage.view; + rect = PDFJS.Util.normalizeRect([ + rect[0], + view[3] - rect[1] + view[1], + rect[2], + view[3] - rect[3] + view[1] + ]); + element.style.left = rect[0] + 'px'; + element.style.top = rect[1] + 'px'; + element.style.position = 'absolute'; + + CustomStyle.setProp('transform', element, transformStr); + var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px'; + CustomStyle.setProp('transformOrigin', element, transformOriginStr); + + if (data.subtype === 'Link' && !data.url) { + var link = element.getElementsByTagName('a')[0]; + if (link) { + if (data.action) { + bindNamedAction(link, data.action); + } else { + bindLink(link, ('dest' in data) ? data.dest : null); + } + } + } + + if (!self.div) { + var annotationLayerDiv = document.createElement('div'); + annotationLayerDiv.className = 'annotationLayer'; + self.pageDiv.appendChild(annotationLayerDiv); + self.div = annotationLayerDiv; + } + + self.div.appendChild(element); + } + } + }); + }, + + hide: function () { + if (!this.div) { + return; + } + this.div.setAttribute('hidden', 'true'); + } + }; + return AnnotationsLayerBuilder; +})(); diff --git a/web/interfaces.js b/web/interfaces.js index 91ac65d9b..1cf9ee8d7 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -97,3 +97,16 @@ IPDFTextLayerFactory.prototype = { */ createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) {} }; + +/** + * @interface + */ +function IPDFAnnotationsLayerFactory() {} +IPDFAnnotationsLayerFactory.prototype = { + /** + * @param {HTMLDivElement} pageDiv + * @param {PDFPage} pdfPage + * @returns {AnnotationsLayerBuilder} + */ + createAnnotationsLayerBuilder: function (pageDiv, pdfPage) {} +}; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 38556efc6..87228c3e5 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -14,8 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals RenderingStates, PDFJS, mozL10n, CustomStyle, getOutputScale, Stats, - CSS_UNITS */ +/* globals RenderingStates, PDFJS, CustomStyle, CSS_UNITS, getOutputScale, + Stats */ 'use strict'; @@ -25,10 +25,10 @@ * @property {number} id - The page unique ID (normally its number). * @property {number} scale - The page scale display. * @property {PageViewport} defaultViewport - The page viewport. - * @property {IPDFLinkService} linkService - The navigation/linking service. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. * @property {Cache} cache - The page cache. * @property {IPDFTextLayerFactory} textLayerFactory + * @property {IPDFAnnotationsLayerFactory} annotationsLayerFactory */ /** @@ -45,10 +45,10 @@ var PDFPageView = (function PDFPageViewClosure() { var id = options.id; var scale = options.scale; var defaultViewport = options.defaultViewport; - var linkService = options.linkService; var renderingQueue = options.renderingQueue; var cache = options.cache; var textLayerFactory = options.textLayerFactory; + var annotationsLayerFactory = options.annotationsLayerFactory; this.id = id; this.renderingId = 'page' + id; @@ -59,10 +59,10 @@ var PDFPageView = (function PDFPageViewClosure() { this.pdfPageRotate = defaultViewport.rotation; this.hasRestrictedScaling = false; - this.linkService = linkService; this.renderingQueue = renderingQueue; this.cache = cache; this.textLayerFactory = textLayerFactory; + this.annotationsLayerFactory = annotationsLayerFactory; this.renderingState = RenderingStates.INITIAL; this.resume = null; @@ -119,10 +119,12 @@ var PDFPageView = (function PDFPageViewClosure() { div.style.height = Math.floor(this.viewport.height) + 'px'; var childNodes = div.childNodes; + var currentZoomLayer = this.zoomLayer || null; + var currentAnnotationNode = (keepAnnotations && this.annotationLayer && + this.annotationLayer.div) || null; for (var i = div.childNodes.length - 1; i >= 0; i--) { var node = childNodes[i]; - if ((this.zoomLayer && this.zoomLayer === node) || - (keepAnnotations && this.annotationLayer === node)) { + if (currentZoomLayer === node || currentAnnotationNode === node) { continue; } div.removeChild(node); @@ -133,7 +135,7 @@ var PDFPageView = (function PDFPageViewClosure() { if (this.annotationLayer) { // Hide annotationLayer until all elements are resized // so they are not displayed on the already-resized page - this.annotationLayer.setAttribute('hidden', 'true'); + this.annotationLayer.hide(); } } else { this.annotationLayer = null; @@ -258,7 +260,7 @@ var PDFPageView = (function PDFPageViewClosure() { } if (redrawAnnotations && this.annotationLayer) { - this.setupAnnotations(); + this.annotationLayer.setupAnnotations(this.viewport); } }, @@ -270,106 +272,6 @@ var PDFPageView = (function PDFPageViewClosure() { return this.viewport.height; }, - setupAnnotations: function PDFPageView_setupAnnotations() { - function bindLink(link, dest) { - link.href = linkService.getDestinationHash(dest); - link.onclick = function pageViewSetupLinksOnclick() { - if (dest) { - linkService.navigateTo(dest); - } - return false; - }; - if (dest) { - link.className = 'internalLink'; - } - } - - function bindNamedAction(link, action) { - link.href = linkService.getAnchorUrl(''); - link.onclick = function pageViewSetupNamedActionOnClick() { - linkService.executeNamedAction(action); - return false; - }; - link.className = 'internalLink'; - } - - var linkService = this.linkService; - var pageDiv = this.div; - var pdfPage = this.pdfPage; - var viewport = this.viewport; - var self = this; - - pdfPage.getAnnotations().then(function(annotationsData) { - viewport = viewport.clone({ dontFlip: true }); - var transform = viewport.transform; - var transformStr = 'matrix(' + transform.join(',') + ')'; - var data, element, i, ii; - - if (self.annotationLayer) { - // If an annotationLayer already exists, refresh its children's - // transformation matrices - for (i = 0, ii = annotationsData.length; i < ii; i++) { - data = annotationsData[i]; - element = self.annotationLayer.querySelector( - '[data-annotation-id="' + data.id + '"]'); - if (element) { - CustomStyle.setProp('transform', element, transformStr); - } - } - // See this.reset() - self.annotationLayer.removeAttribute('hidden'); - } else { - for (i = 0, ii = annotationsData.length; i < ii; i++) { - data = annotationsData[i]; - if (!data || !data.hasHtml) { - continue; - } - - element = PDFJS.AnnotationUtils.getHtmlElement(data, - pdfPage.commonObjs); - element.setAttribute('data-annotation-id', data.id); - mozL10n.translate(element); - - var rect = data.rect; - var view = pdfPage.view; - rect = PDFJS.Util.normalizeRect([ - rect[0], - view[3] - rect[1] + view[1], - rect[2], - view[3] - rect[3] + view[1] - ]); - element.style.left = rect[0] + 'px'; - element.style.top = rect[1] + 'px'; - element.style.position = 'absolute'; - - CustomStyle.setProp('transform', element, transformStr); - var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px'; - CustomStyle.setProp('transformOrigin', element, transformOriginStr); - - if (data.subtype === 'Link' && !data.url) { - var link = element.getElementsByTagName('a')[0]; - if (link) { - if (data.action) { - bindNamedAction(link, data.action); - } else { - bindLink(link, ('dest' in data) ? data.dest : null); - } - } - } - - if (!self.annotationLayer) { - var annotationLayerDiv = document.createElement('div'); - annotationLayerDiv.className = 'annotationLayer'; - pageDiv.appendChild(annotationLayerDiv); - self.annotationLayer = annotationLayerDiv; - } - - self.annotationLayer.appendChild(element); - } - } - }); - }, - getPagePoint: function PDFPageView_getPagePoint(x, y) { return this.viewport.convertToPdfPoint(x, y); }, @@ -396,7 +298,7 @@ var PDFPageView = (function PDFPageViewClosure() { canvasWrapper.appendChild(canvas); if (this.annotationLayer) { // annotationLayer needs to stay on top - div.insertBefore(canvasWrapper, this.annotationLayer); + div.insertBefore(canvasWrapper, this.annotationLayer.div); } else { div.appendChild(canvasWrapper); } @@ -443,7 +345,7 @@ var PDFPageView = (function PDFPageViewClosure() { textLayerDiv.style.height = canvas.style.height; if (this.annotationLayer) { // annotationLayer needs to stay on top - div.insertBefore(textLayerDiv, this.annotationLayer); + div.insertBefore(textLayerDiv, this.annotationLayer.div); } else { div.appendChild(textLayerDiv); } @@ -538,7 +440,13 @@ var PDFPageView = (function PDFPageViewClosure() { } ); - this.setupAnnotations(); + if (this.annotationsLayerFactory) { + if (!this.annotationLayer) { + this.annotationLayer = this.annotationsLayerFactory. + createAnnotationsLayerBuilder(div, this.pdfPage); + } + this.annotationLayer.setupAnnotations(this.viewport); + } div.setAttribute('data-loaded', true); // Add the page to the cache at the start of drawing. That way it can be diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 6441eae9e..84483224c 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -17,7 +17,8 @@ /*globals watchScroll, Cache, DEFAULT_CACHE_SIZE, PDFPageView, UNKNOWN_SCALE, SCROLLBAR_PADDING, VERTICAL_PADDING, MAX_AUTO_SCALE, CSS_UNITS, DEFAULT_SCALE, scrollIntoView, getVisibleElements, RenderingStates, - PDFJS, Promise, TextLayerBuilder, PDFRenderingQueue */ + PDFJS, Promise, TextLayerBuilder, PDFRenderingQueue, + AnnotationsLayerBuilder */ 'use strict'; @@ -33,6 +34,7 @@ var IGNORE_CURRENT_POSITION_ON_ZOOM = false; //#include pdf_rendering_queue.js //#include pdf_page_view.js //#include text_layer_builder.js +//#include annotations_layer_builder.js /** * @typedef {Object} PDFViewerOptions @@ -244,10 +246,10 @@ var PDFViewer = (function pdfViewer() { id: pageNum, scale: scale, defaultViewport: viewport.clone(), - linkService: this.linkService, renderingQueue: this.renderingQueue, cache: this.cache, - textLayerFactory: textLayerFactory + textLayerFactory: textLayerFactory, + annotationsLayerFactory: this }); bindOnAfterDraw(pageView); this.pages.push(pageView); @@ -675,6 +677,19 @@ var PDFViewer = (function pdfViewer() { }); }, + /** + * @param {HTMLDivElement} pageDiv + * @param {PDFPage} pdfPage + * @returns {AnnotationsLayerBuilder} + */ + createAnnotationsLayerBuilder: function (pageDiv, pdfPage) { + return new AnnotationsLayerBuilder({ + pageDiv: pageDiv, + pdfPage: pdfPage, + linkService: this.linkService + }); + }, + setFindController: function (findController) { this.findController = findController; }, diff --git a/web/viewer.html b/web/viewer.html index c2d3dff69..d6588d940 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -71,6 +71,7 @@ http://sourceforge.net/adobe/cmap/wiki/License/ <script src="pdf_rendering_queue.js"></script> <script src="pdf_page_view.js"></script> <script src="text_layer_builder.js"></script> + <script src="annotations_layer_builder.js"></script> <script src="pdf_viewer.js"></script> <script src="thumbnail_view.js"></script> <script src="document_outline_view.js"></script> From 22c62685b0f7e6d2009f3a6d0419b51817851c99 Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Mon, 29 Sep 2014 11:15:06 -0500 Subject: [PATCH 06/10] Removes Stats dependency from PDFPageView. --- web/pdf_page_view.js | 15 +-------------- web/pdf_viewer.js | 1 - web/viewer.js | 19 ++++++++++++++++--- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 87228c3e5..3ebe41427 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -14,8 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals RenderingStates, PDFJS, CustomStyle, CSS_UNITS, getOutputScale, - Stats */ +/* globals RenderingStates, PDFJS, CustomStyle, CSS_UNITS, getOutputScale */ 'use strict'; @@ -392,7 +391,6 @@ var PDFPageView = (function PDFPageViewClosure() { self.error = error; self.stats = pdfPage.stats; - self.updateStats(); if (self.onAfterDraw) { self.onAfterDraw(); } @@ -508,17 +506,6 @@ var PDFPageView = (function PDFPageViewClosure() { }); }; }, - - updateStats: function PDFPageView_updateStats() { - if (!this.stats) { - return; - } - - if (PDFJS.pdfBug && Stats.enabled) { - var stats = this.stats; - Stats.add(this.id, stats); - } - }, }; return PDFPageView; diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 84483224c..42e9efaf2 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -107,7 +107,6 @@ var PDFViewer = (function pdfViewer() { return; } - this.pages[val - 1].updateStats(); event.previousPageNumber = this._currentPageNumber; this._currentPageNumber = val; event.pageNumber = val; diff --git a/web/viewer.js b/web/viewer.js index 802d554dd..c00a43bcd 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -16,7 +16,7 @@ */ /* globals PDFJS, PDFBug, FirefoxCom, Stats, Cache, ProgressBar, DownloadManager, getFileName, scrollIntoView, getPDFFileNameFromURL, - PDFHistory, Preferences, SidebarView, ViewHistory, + PDFHistory, Preferences, SidebarView, ViewHistory, Stats, PDFThumbnailViewer, URL, noContextMenuHandler, SecondaryToolbar, PasswordPrompt, PresentationMode, HandTool, Promise, DocumentProperties, DocumentOutlineView, DocumentAttachmentsView, @@ -1730,12 +1730,17 @@ function webViewerInitialized() { document.addEventListener('DOMContentLoaded', webViewerLoad, true); document.addEventListener('pagerendered', function (e) { - var pageIndex = e.detail.pageNumber - 1; + var pageNumber = e.detail.pageNumber; + var pageIndex = pageNumber - 1; var pageView = PDFViewerApplication.pdfViewer.getPageView(pageIndex); var thumbnailView = PDFViewerApplication.pdfThumbnailViewer. getThumbnail(pageIndex); thumbnailView.setImage(pageView); + if (PDFJS.pdfBug && Stats.enabled && pageView.stats) { + Stats.add(pageNumber, pageView.stats); + } + //#if (FIREFOX || MOZCENTRAL) //if (pageView.textLayer && pageView.textLayer.textDivs && // pageView.textLayer.textDivs.length > 0 && @@ -1768,7 +1773,7 @@ document.addEventListener('pagerendered', function (e) { // If the page is still visible when it has finished rendering, // ensure that the page number input loading indicator is hidden. - if ((pageIndex + 1) === PDFViewerApplication.page) { + if (pageNumber === PDFViewerApplication.page) { var pageNumberInput = document.getElementById('pageNumber'); pageNumberInput.classList.remove(PAGE_NUMBER_LOADING_INDICATOR); } @@ -1965,6 +1970,14 @@ window.addEventListener('pagechange', function pagechange(evt) { document.getElementById('firstPage').disabled = (page <= 1); document.getElementById('lastPage').disabled = (page >= numPages); + // we need to update stats + if (PDFJS.pdfBug && Stats.enabled) { + var pageView = PDFViewerApplication.pdfViewer.getPageView(page - 1); + if (pageView.stats) { + Stats.add(page, pageView.stats); + } + } + // checking if the this.page was called from the updateViewarea function if (evt.updateInProgress) { return; From b930228788c64414d6040bca6941cc734158df31 Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Mon, 29 Sep 2014 11:32:45 -0500 Subject: [PATCH 07/10] Refactors Cache into PDFPageViewBuffer --- web/pdf_page_view.js | 12 ++++++------ web/pdf_viewer.js | 38 ++++++++++++++++++++++++++++++++------ web/ui_utils.js | 21 --------------------- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 3ebe41427..6c8ea8c29 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -25,7 +25,6 @@ * @property {number} scale - The page scale display. * @property {PageViewport} defaultViewport - The page viewport. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. - * @property {Cache} cache - The page cache. * @property {IPDFTextLayerFactory} textLayerFactory * @property {IPDFAnnotationsLayerFactory} annotationsLayerFactory */ @@ -45,7 +44,6 @@ var PDFPageView = (function PDFPageViewClosure() { var scale = options.scale; var defaultViewport = options.defaultViewport; var renderingQueue = options.renderingQueue; - var cache = options.cache; var textLayerFactory = options.textLayerFactory; var annotationsLayerFactory = options.annotationsLayerFactory; @@ -59,13 +57,15 @@ var PDFPageView = (function PDFPageViewClosure() { this.hasRestrictedScaling = false; this.renderingQueue = renderingQueue; - this.cache = cache; this.textLayerFactory = textLayerFactory; this.annotationsLayerFactory = annotationsLayerFactory; this.renderingState = RenderingStates.INITIAL; this.resume = null; + this.onBeforeDraw = null; + this.onAfterDraw = null; + this.textLayer = null; this.zoomLayer = null; @@ -447,9 +447,9 @@ var PDFPageView = (function PDFPageViewClosure() { } div.setAttribute('data-loaded', true); - // Add the page to the cache at the start of drawing. That way it can be - // evicted from the cache and destroyed even if we pause its rendering. - this.cache.push(this); + if (self.onBeforeDraw) { + self.onBeforeDraw(); + } }, beforePrint: function PDFPageView_beforePrint() { diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 42e9efaf2..1ad1c0c7e 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - /*globals watchScroll, Cache, DEFAULT_CACHE_SIZE, PDFPageView, UNKNOWN_SCALE, + /*globals watchScroll, PDFPageView, UNKNOWN_SCALE, SCROLLBAR_PADDING, VERTICAL_PADDING, MAX_AUTO_SCALE, CSS_UNITS, DEFAULT_SCALE, scrollIntoView, getVisibleElements, RenderingStates, PDFJS, Promise, TextLayerBuilder, PDFRenderingQueue, @@ -30,6 +30,7 @@ var PresentationModeState = { }; var IGNORE_CURRENT_POSITION_ON_ZOOM = false; +var DEFAULT_CACHE_SIZE = 10; //#include pdf_rendering_queue.js //#include pdf_page_view.js @@ -52,6 +53,26 @@ var IGNORE_CURRENT_POSITION_ON_ZOOM = false; * @implements {IRenderableView} */ var PDFViewer = (function pdfViewer() { + function PDFPageViewBuffer(size) { + var data = []; + this.push = function cachePush(view) { + var i = data.indexOf(view); + if (i >= 0) { + data.splice(i, 1); + } + data.push(view); + if (data.length > size) { + data.shift().destroy(); + } + }; + this.resize = function (newSize) { + size = newSize; + while (data.length > size) { + data.shift().destroy(); + } + }; + } + /** * @constructs PDFViewer * @param {PDFViewerOptions} options @@ -212,7 +233,13 @@ var PDFViewer = (function pdfViewer() { }); this.onePageRendered = onePageRendered; - var bindOnAfterDraw = function (pageView) { + var bindOnAfterAndBeforeDraw = function (pageView) { + pageView.onBeforeDraw = function pdfViewLoadOnBeforeDraw() { + // Add the page to the buffer at the start of drawing. That way it can + // be evicted from the buffer and destroyed even if we pause its + // rendering. + self._buffer.push(this); + }; // when page is painted, using the image as thumbnail base pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() { if (!isOnePageRenderedResolved) { @@ -246,11 +273,10 @@ var PDFViewer = (function pdfViewer() { scale: scale, defaultViewport: viewport.clone(), renderingQueue: this.renderingQueue, - cache: this.cache, textLayerFactory: textLayerFactory, annotationsLayerFactory: this }); - bindOnAfterDraw(pageView); + bindOnAfterAndBeforeDraw(pageView); this.pages.push(pageView); } @@ -291,11 +317,11 @@ var PDFViewer = (function pdfViewer() { }, _resetView: function () { - this.cache = new Cache(DEFAULT_CACHE_SIZE); this.pages = []; this._currentPageNumber = 1; this._currentScale = UNKNOWN_SCALE; this._currentScaleValue = null; + this._buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE); this.location = null; this._pagesRotation = 0; this._pagesRequests = []; @@ -538,7 +564,7 @@ var PDFViewer = (function pdfViewer() { var suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * visiblePages.length + 1); - this.cache.resize(suggestedCacheSize); + this._buffer.resize(suggestedCacheSize); this.renderingQueue.renderHighestPriority(visible); diff --git a/web/ui_utils.js b/web/ui_utils.js index b4f2d7365..3c32c5939 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -22,7 +22,6 @@ var UNKNOWN_SCALE = 0; var MAX_AUTO_SCALE = 1.25; var SCROLLBAR_PADDING = 40; var VERTICAL_PADDING = 5; -var DEFAULT_CACHE_SIZE = 10; // optimised CSS custom property getter/setter var CustomStyle = (function CustomStyleClosure() { @@ -349,23 +348,3 @@ var ProgressBar = (function ProgressBarClosure() { return ProgressBar; })(); - -var Cache = function cacheCache(size) { - var data = []; - this.push = function cachePush(view) { - var i = data.indexOf(view); - if (i >= 0) { - data.splice(i, 1); - } - data.push(view); - if (data.length > size) { - data.shift().destroy(); - } - }; - this.resize = function (newSize) { - size = newSize; - while (data.length > size) { - data.shift().destroy(); - } - }; -}; From 2ac7ac4678519d8fcfa0f5e703531ba71bf0859d Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Wed, 17 Dec 2014 14:12:51 -0600 Subject: [PATCH 08/10] Removes lastScrollSource and isViewerInPresentationMode from TextLayerBuilderOptions --- web/interfaces.js | 11 -------- web/pdf_find_controller.js | 25 ++++++++++++++++- web/pdf_page_view.js | 12 ++++++++ web/pdf_viewer.js | 11 +++----- web/text_layer_builder.js | 56 ++++++++++++++++++-------------------- 5 files changed, 67 insertions(+), 48 deletions(-) diff --git a/web/interfaces.js b/web/interfaces.js index 1cf9ee8d7..b99338c49 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -73,17 +73,6 @@ IRenderableView.prototype = { resume: function () {}, }; -/** - * @interface - */ -function ILastScrollSource() {} -ILastScrollSource.prototype = { - /** - * @returns {number} - */ - get lastScroll() {}, -}; - /** * @interface */ diff --git a/web/pdf_find_controller.js b/web/pdf_find_controller.js index c54fc8ade..0d3d1d5ae 100644 --- a/web/pdf_find_controller.js +++ b/web/pdf_find_controller.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals PDFJS, FirefoxCom, Promise */ +/* globals PDFJS, FirefoxCom, Promise, scrollIntoView */ 'use strict'; @@ -24,6 +24,9 @@ var FindStates = { FIND_PENDING: 3 }; +var FIND_SCROLL_OFFSET_TOP = -50; +var FIND_SCROLL_OFFSET_LEFT = -400; + /** * Provides "search" or "find" functionality for the PDF. * This object actually performs the search for a given string. @@ -308,6 +311,26 @@ var PDFFindController = (function PDFFindControllerClosure() { } }, + /** + * The method is called back from the text layer when match presentation + * is updated. + * @param {number} pageIndex - page index. + * @param {number} index - match index. + * @param {Array} elements - text layer div elements array. + * @param {number} beginIdx - start index of the div array for the match. + * @param {number} endIdx - end index of the div array for the match. + */ + updateMatchPosition: function PDFFindController_updateMatchPosition( + pageIndex, index, elements, beginIdx, endIdx) { + if (this.selected.matchIdx === index && + this.selected.pageIdx === pageIndex) { + scrollIntoView(elements[beginIdx], { + top: FIND_SCROLL_OFFSET_TOP, + left: FIND_SCROLL_OFFSET_LEFT + }); + } + }, + nextPageMatch: function PDFFindController_nextPageMatch() { if (this.resumePageIdx !== null) { console.error('There can only be one pending page.'); diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 6c8ea8c29..50d3c2b9b 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -18,6 +18,8 @@ 'use strict'; +var TEXT_LAYER_RENDER_DELAY = 200; // ms + /** * @typedef {Object} PDFPageViewOptions * @property {HTMLDivElement} container - The viewer element. @@ -194,6 +196,15 @@ var PDFPageView = (function PDFPageViewClosure() { this.reset(true); }, + /** + * Called when moved in the parent's container. + */ + updatePosition: function PDFPageView_updatePosition() { + if (this.textLayer) { + this.textLayer.render(TEXT_LAYER_RENDER_DELAY); + } + }, + cssTransform: function PDFPageView_transform(canvas, redrawAnnotations) { // Scale canvas, canvas wrapper, and page container. var width = this.viewport.width; @@ -429,6 +440,7 @@ var PDFPageView = (function PDFPageViewClosure() { self.pdfPage.getTextContent().then( function textContentResolved(textContent) { textLayer.setTextContent(textContent); + textLayer.render(TEXT_LAYER_RENDER_DELAY); } ); } diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 1ad1c0c7e..e68130ce5 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -49,7 +49,6 @@ var DEFAULT_CACHE_SIZE = 10; /** * Simple viewer control to display PDF content/pages. * @class - * @implements {ILastScrollSource} * @implements {IRenderableView} */ var PDFViewer = (function pdfViewer() { @@ -92,7 +91,6 @@ var PDFViewer = (function pdfViewer() { } this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this)); - this.lastScroll = 0; this.updateInProgress = false; this.presentationModeState = PresentationModeState.UNKNOWN; this._resetView(); @@ -333,12 +331,13 @@ var PDFViewer = (function pdfViewer() { }, _scrollUpdate: function () { - this.lastScroll = Date.now(); - if (this.pagesCount === 0) { return; } this.update(); + for (var i = 0, ii = this.pages.length; i < ii; i++) { + this.pages[i].updatePosition(); + } }, _setScaleUpdatePages: function pdfViewer_setScaleUpdatePages( @@ -696,9 +695,7 @@ var PDFViewer = (function pdfViewer() { textLayerDiv: textLayerDiv, pageIndex: pageIndex, viewport: viewport, - lastScrollSource: this, - isViewerInPresentationMode: isViewerInPresentationMode, - findController: this.findController + findController: isViewerInPresentationMode ? null : this.findController }); }, diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index 5fac1c493..d300f6354 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -13,14 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals CustomStyle, scrollIntoView, PDFJS */ +/* globals CustomStyle, PDFJS */ 'use strict'; -var FIND_SCROLL_OFFSET_TOP = -50; -var FIND_SCROLL_OFFSET_LEFT = -400; var MAX_TEXT_DIVS_TO_RENDER = 100000; -var RENDER_DELAY = 200; // ms var NonWhitespaceRegexp = /\S/; @@ -33,9 +30,6 @@ function isAllWhitespace(str) { * @property {HTMLDivElement} textLayerDiv - The text layer container. * @property {number} pageIndex - The page index. * @property {PageViewport} viewport - The viewport of the text layer. - * @property {ILastScrollSource} lastScrollSource - The object that records when - * last time scroll happened. - * @property {boolean} isViewerInPresentationMode * @property {PDFFindController} findController */ @@ -49,13 +43,11 @@ function isAllWhitespace(str) { var TextLayerBuilder = (function TextLayerBuilderClosure() { function TextLayerBuilder(options) { this.textLayerDiv = options.textLayerDiv; - this.layoutDone = false; + this.renderingDone = false; this.divContentDone = false; this.pageIdx = options.pageIndex; this.matches = []; - this.lastScrollSource = options.lastScrollSource || null; this.viewport = options.viewport; - this.isViewerInPresentationMode = options.isViewerInPresentationMode; this.textDivs = []; this.findController = options.findController || null; } @@ -71,6 +63,7 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { // 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.renderingDone = true; return; } @@ -118,23 +111,29 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { this.updateMatches(); }, - setupRenderLayoutTimer: - function TextLayerBuilder_setupRenderLayoutTimer() { - // Schedule renderLayout() if the user has been scrolling, - // otherwise run it right away. - var self = this; - var lastScroll = (this.lastScrollSource === null ? - 0 : this.lastScrollSource.lastScroll); + /** + * 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 (Date.now() - lastScroll > RENDER_DELAY) { // Render right away + if (this.renderTimer) { + clearTimeout(this.renderTimer); + this.renderTimer = null; + } + + if (!timeout) { // Render right away this.renderLayer(); } else { // Schedule - if (this.renderTimer) { - clearTimeout(this.renderTimer); - } + var self = this; this.renderTimer = setTimeout(function() { - self.setupRenderLayoutTimer(); - }, RENDER_DELAY); + self.renderLayer(); + self.renderTimer = null; + }, timeout); } }, @@ -204,7 +203,6 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { this.appendText(textItems[i], textContent.styles); } this.divContentDone = true; - this.setupRenderLayoutTimer(); }, convertMatches: function TextLayerBuilder_convertMatches(matches) { @@ -266,8 +264,9 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { var bidiTexts = this.textContent.items; var textDivs = this.textDivs; var prevEnd = null; + var pageIdx = this.pageIdx; var isSelectedPage = (this.findController === null ? - false : (this.pageIdx === this.findController.selected.pageIdx)); + false : (pageIdx === this.findController.selected.pageIdx)); var selectedMatchIdx = (this.findController === null ? -1 : this.findController.selected.matchIdx); var highlightAll = (this.findController === null ? @@ -313,10 +312,9 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { 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 }); + if (this.findController) { + this.findController.updateMatchPosition(pageIdx, i, textDivs, + begin.divIdx, end.divIdx); } // Match inside new div. From 2565e627a31dccd562133d2e0f91413f09f302a5 Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Wed, 17 Dec 2014 14:47:14 -0600 Subject: [PATCH 09/10] Refactors draw method in PDFPageView; makes optional some PDFPageViewOptions options --- web/annotations_layer_builder.js | 19 +++++++++++++++++ web/interfaces.js | 4 ++-- web/pdf_page_view.js | 36 ++++++++++++++++++++++++-------- web/pdf_rendering_queue.js | 5 ++++- web/text_layer_builder.js | 21 +++++++++++++++++++ web/thumbnail_view.js | 16 +++++++++----- 6 files changed, 84 insertions(+), 17 deletions(-) diff --git a/web/annotations_layer_builder.js b/web/annotations_layer_builder.js index 309f7c033..494ceffb0 100644 --- a/web/annotations_layer_builder.js +++ b/web/annotations_layer_builder.js @@ -154,3 +154,22 @@ var AnnotationsLayerBuilder = (function AnnotationsLayerBuilderClosure() { }; return AnnotationsLayerBuilder; })(); + +/** + * @constructor + * @implements IPDFAnnotationsLayerFactory + */ +function DefaultAnnotationsLayerFactory() {} +DefaultAnnotationsLayerFactory.prototype = { + /** + * @param {HTMLDivElement} pageDiv + * @param {PDFPage} pdfPage + * @returns {AnnotationsLayerBuilder} + */ + createAnnotationsLayerBuilder: function (pageDiv, pdfPage) { + return new AnnotationsLayerBuilder({ + pageDiv: pageDiv, + pdfPage: pdfPage + }); + } +}; diff --git a/web/interfaces.js b/web/interfaces.js index b99338c49..37a7b19d0 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -67,9 +67,9 @@ IRenderableView.prototype = { */ get renderingState() {}, /** - * @param {function} callback - The draw completion callback. + * @returns {Promise} Resolved on draw completion. */ - draw: function (callback) {}, + draw: function () {}, resume: function () {}, }; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 50d3c2b9b..cb0725a3c 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -14,7 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals RenderingStates, PDFJS, CustomStyle, CSS_UNITS, getOutputScale */ +/* globals RenderingStates, PDFJS, CustomStyle, CSS_UNITS, getOutputScale, + TextLayerBuilder, AnnotationsLayerBuilder, Promise */ 'use strict'; @@ -286,7 +287,7 @@ var PDFPageView = (function PDFPageViewClosure() { return this.viewport.convertToPdfPoint(x, y); }, - draw: function PDFPageView_draw(callback) { + draw: function PDFPageView_draw() { if (this.renderingState !== RenderingStates.INITIAL) { console.error('Must be in new state before drawing'); } @@ -373,6 +374,12 @@ var PDFPageView = (function PDFPageViewClosure() { ctx.scale(outputScale.sx, outputScale.sy); } + var resolveRenderPromise, rejectRenderPromise; + var promise = new Promise(function (resolve, reject) { + resolveRenderPromise = resolve; + rejectRenderPromise = reject; + }); + // Rendering area var self = this; @@ -385,6 +392,7 @@ var PDFPageView = (function PDFPageViewClosure() { } if (error === 'cancelled') { + rejectRenderPromise(error); return; } @@ -412,14 +420,16 @@ var PDFPageView = (function PDFPageViewClosure() { }); div.dispatchEvent(event); - callback(); + if (!error) { + resolveRenderPromise(undefined); + } else { + rejectRenderPromise(error); + } } - var renderContext = { - canvasContext: ctx, - viewport: this.viewport, - // intent: 'default', // === 'display' - continueCallback: function pdfViewcContinueCallback(cont) { + var renderContinueCallback = null; + if (this.renderingQueue) { + renderContinueCallback = function renderContinueCallback(cont) { if (!self.renderingQueue.isHighestPriority(self)) { self.renderingState = RenderingStates.PAUSED; self.resume = function resumeCallback() { @@ -429,7 +439,14 @@ var PDFPageView = (function PDFPageViewClosure() { return; } cont(); - } + }; + } + + var renderContext = { + canvasContext: ctx, + viewport: this.viewport, + // intent: 'default', // === 'display' + continueCallback: renderContinueCallback }; var renderTask = this.renderTask = this.pdfPage.render(renderContext); @@ -462,6 +479,7 @@ var PDFPageView = (function PDFPageViewClosure() { if (self.onBeforeDraw) { self.onBeforeDraw(); } + return promise; }, beforePrint: function PDFPageView_beforePrint() { diff --git a/web/pdf_rendering_queue.js b/web/pdf_rendering_queue.js index c7b1b3aba..b17a1ee58 100644 --- a/web/pdf_rendering_queue.js +++ b/web/pdf_rendering_queue.js @@ -165,7 +165,10 @@ var PDFRenderingQueue = (function PDFRenderingQueueClosure() { break; case RenderingStates.INITIAL: this.highestPriorityPage = view.renderingId; - view.draw(this.renderHighestPriority.bind(this)); + var continueRendering = function () { + this.renderHighestPriority(); + }.bind(this); + view.draw().then(continueRendering, continueRendering); break; } return true; diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index d300f6354..020dfdf1c 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -385,3 +385,24 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { }; 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 + }); + } +}; diff --git a/web/thumbnail_view.js b/web/thumbnail_view.js index f8c207ee9..e07ab3ab9 100644 --- a/web/thumbnail_view.js +++ b/web/thumbnail_view.js @@ -139,17 +139,22 @@ var ThumbnailView = function thumbnailView(container, id, defaultViewport, return !this.hasImage; }; - this.draw = function thumbnailViewDraw(callback) { + this.draw = function thumbnailViewDraw() { if (this.renderingState !== RenderingStates.INITIAL) { console.error('Must be in new state before drawing'); } this.renderingState = RenderingStates.RUNNING; if (this.hasImage) { - callback(); - return; + return Promise.resolve(undefined); } + var resolveRenderPromise, rejectRenderPromise; + var promise = new Promise(function (resolve, reject) { + resolveRenderPromise = resolve; + rejectRenderPromise = reject; + }); + var self = this; var ctx = this.getPageDrawContext(); var drawViewport = this.viewport.clone({ scale: this.scale }); @@ -171,14 +176,15 @@ var ThumbnailView = function thumbnailView(container, id, defaultViewport, this.pdfPage.render(renderContext).promise.then( function pdfPageRenderCallback() { self.renderingState = RenderingStates.FINISHED; - callback(); + resolveRenderPromise(undefined); }, function pdfPageRenderError(error) { self.renderingState = RenderingStates.FINISHED; - callback(); + rejectRenderPromise(error); } ); this.hasImage = true; + return promise; }; function getTempCanvas(width, height) { From 513a3d8c91c2948fcc95d2f1f297d787f45fa416 Mon Sep 17 00:00:00 2001 From: Yury Delendik <ydelendik@mozilla.com> Date: Fri, 19 Dec 2014 18:10:57 -0600 Subject: [PATCH 10/10] Replaces text selection example --- examples/components/pageviewer.html | 46 +++++++++ examples/components/pageviewer.js | 58 +++++++++++ examples/text-selection/css/minimal.css | 43 -------- examples/text-selection/index.html | 26 ----- examples/text-selection/js/minimal.js | 97 ------------------- examples/text-selection/pdf/TestDocument.pdf | Bin 35153 -> 0 bytes web/annotations_layer_builder.js | 4 +- web/pdf_viewer.component.js | 9 +- web/text_layer_builder.css | 4 + web/viewer.css | 7 -- 10 files changed, 119 insertions(+), 175 deletions(-) create mode 100644 examples/components/pageviewer.html create mode 100644 examples/components/pageviewer.js delete mode 100644 examples/text-selection/css/minimal.css delete mode 100644 examples/text-selection/index.html delete mode 100644 examples/text-selection/js/minimal.js delete mode 100644 examples/text-selection/pdf/TestDocument.pdf diff --git a/examples/components/pageviewer.html b/examples/components/pageviewer.html new file mode 100644 index 000000000..ac49d273c --- /dev/null +++ b/examples/components/pageviewer.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<!-- +Copyright 2014 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. +--> +<html dir="ltr" mozdisallowselectionprint moznomarginboxes> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> + <meta name="google" content="notranslate"> + <title>PDF.js page viewer using built components</title> + + <style> + body { + background-color: #808080; + margin: 0; + padding: 0; + } + </style> + + <link rel="stylesheet" href="../../build/components/pdf_viewer.css"> + + <!-- for legacy browsers --> + <script src="../../build/components/compatibility.js"></script> + <script src="../../build/pdf.js"></script> + <script src="../../build/components/pdf_viewer.js"></script> +</head> + +<body tabindex="1"> + <div id="pageContainer" class="pdfPage"></div> + + <script src="pageviewer.js"></script> +</body> +</html> + diff --git a/examples/components/pageviewer.js b/examples/components/pageviewer.js new file mode 100644 index 000000000..41c1b35da --- /dev/null +++ b/examples/components/pageviewer.js @@ -0,0 +1,58 @@ +/* Copyright 2014 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'; + +if (!PDFJS.PDFViewer || !PDFJS.getDocument) { + alert('Please build the library and components using\n' + + ' `node make generic components`'); +} + +// In cases when the pdf.worker.js is located at the different folder than the +// pdf.js's one, or the pdf.js is executed via eval(), the workerSrc property +// shall be specified. +// +// PDFJS.workerSrc = '../../build/pdf.worker.js'; + +// Some PDFs need external cmaps. +// +// PDFJS.cMapUrl = '../../external/bcmaps/'; +// PDFJS.cMapPacked = true; + +var DEFAULT_URL = '../../web/compressed.tracemonkey-pldi-09.pdf'; +var PAGE_TO_VIEW = 1; +var SCALE = 1.0; + +var container = document.getElementById('pageContainer'); + +// Loading document. +PDFJS.getDocument(DEFAULT_URL).then(function (pdfDocument) { + // Document loaded, retrieving the page. + return pdfDocument.getPage(PAGE_TO_VIEW).then(function (pdfPage) { + // Creating the page view with default parameters. + var pdfPageView = new PDFJS.PDFPageView({ + container: container, + id: PAGE_TO_VIEW, + scale: SCALE, + defaultViewport: pdfPage.getViewport(SCALE), + // We can enable text/annotations layers, if needed + textLayerFactory: new PDFJS.DefaultTextLayerFactory(), + annotationsLayerFactory: new PDFJS.DefaultAnnotationsLayerFactory() + }); + // Associates the actual page with the view, and drawing it + pdfPageView.setPdfPage(pdfPage); + return pdfPageView.draw(); + }); +}); diff --git a/examples/text-selection/css/minimal.css b/examples/text-selection/css/minimal.css deleted file mode 100644 index 6a1124484..000000000 --- a/examples/text-selection/css/minimal.css +++ /dev/null @@ -1,43 +0,0 @@ -body { - font-family: arial, verdana, sans-serif; -} - -/* Allow absolute positioning of the canvas and textLayer in the page. They - will be the same size and will be placed on top of each other. */ -.pdfPage { - position: relative; - overflow: visible; - border: 1px solid #000000; -} - -.pdfPage > canvas { - position: absolute; - top: 0; - left: 0; -} - -/* CSS classes used by TextLayerBuilder to style the text layer divs */ - -/* This stuff is important! Otherwise when you select the text, - the text in the divs will show up! */ -::selection { background:rgba(0,0,255,0.3); } -::-moz-selection { background:rgba(0,0,255,0.3); } - -.textLayer { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - color: #000; - font-family: sans-serif; - overflow: hidden; -} - -.textLayer > div { - color: transparent; - position: absolute; - line-height: 1; - white-space: pre; - cursor: text; -} diff --git a/examples/text-selection/index.html b/examples/text-selection/index.html deleted file mode 100644 index fc1e43ba1..000000000 --- a/examples/text-selection/index.html +++ /dev/null @@ -1,26 +0,0 @@ -<html> - <head> - <title>Minimal pdf.js text-selection demo</title> - <link href="css/minimal.css" rel="stylesheet" media="screen" /> - - <!-- you will need to run "node make generic" first before you can use this --> - <script src="../../build/generic/build/pdf.js"></script> - - <!-- These files are viewer components that you will need to get text-selection to work --> - <script src="../../web/ui_utils.js"></script> - <script src="../../web/text_layer_builder.js"></script> - - <script> - // Specify the main script used to create a new PDF.JS web worker. - // In production, change this to point to the combined `pdf.js` file. - </script> - - <script src="js/minimal.js"></script> - </head> - <body> - This is a minimal pdf.js text-selection demo. The existing minimal-example shows you how to render a PDF, but not - how to enable text-selection. This example shows you how to do both. <br /><br /> - <div id="pdfContainer"> - </div> - </body> -</html> diff --git a/examples/text-selection/js/minimal.js b/examples/text-selection/js/minimal.js deleted file mode 100644 index e87bb837a..000000000 --- a/examples/text-selection/js/minimal.js +++ /dev/null @@ -1,97 +0,0 @@ -// Minimal PDF rendering and text-selection example using PDF.js by Vivin Suresh Paliath (http://vivin.net) -// This example uses a built version of PDF.js that contains all modules that it requires. -// -// The problem with understanding text selection was that the text selection code has heavily intertwined -// with viewer.html and viewer.js. I have extracted the parts I need out of viewer.js into a separate file -// which contains the bare minimum required to implement text selection. The key component is TextLayerBuilder, -// which is the object that handles the creation of text-selection divs. I have added this code as an external -// resource. -// -// This demo uses a PDF that only has one page. You can render other pages if you wish, but the focus here is -// just to show you how you can render a PDF with text selection. Hence the code only loads up one page. -// -// The CSS used here is also very important since it sets up the CSS for the text layer divs overlays that -// you actually end up selecting. -// -// NOTE: The original example was changed to remove jQuery usage, re-structure and add more comments. - -window.onload = function () { - if (typeof PDFJS === 'undefined') { - alert('Built version of pdf.js is not found\nPlease run `node make generic`'); - return; - } - - var scale = 1.5; //Set this to whatever you want. This is basically the "zoom" factor for the PDF. - PDFJS.workerSrc = '../../build/generic/build/pdf.worker.js'; - - function loadPdf(pdfPath) { - var pdf = PDFJS.getDocument(pdfPath); - return pdf.then(renderPdf); - } - - function renderPdf(pdf) { - return pdf.getPage(1).then(renderPage); - } - - function renderPage(page) { - var viewport = page.getViewport(scale); - - // Create and append the 'pdf-page' div to the pdf container. - var pdfPage = document.createElement('div'); - pdfPage.className = 'pdfPage'; - var pdfContainer = document.getElementById('pdfContainer'); - pdfContainer.appendChild(pdfPage); - - // Set the canvas height and width to the height and width of the viewport. - var canvas = document.createElement('canvas'); - var context = canvas.getContext('2d'); - - // The following few lines of code set up scaling on the context, if we are - // on a HiDPI display. - var outputScale = getOutputScale(context); - canvas.width = (Math.floor(viewport.width) * outputScale.sx) | 0; - canvas.height = (Math.floor(viewport.height) * outputScale.sy) | 0; - context._scaleX = outputScale.sx; - context._scaleY = outputScale.sy; - if (outputScale.scaled) { - context.scale(outputScale.sx, outputScale.sy); - } - - // The page, canvas and text layer elements will have the same size. - canvas.style.width = Math.floor(viewport.width) + 'px'; - canvas.style.height = Math.floor(viewport.height) + 'px'; - - pdfPage.style.width = canvas.style.width; - pdfPage.style.height = canvas.style.height; - pdfPage.appendChild(canvas); - - var textLayerDiv = document.createElement('div'); - textLayerDiv.className = 'textLayer'; - textLayerDiv.style.width = canvas.style.width; - textLayerDiv.style.height = canvas.style.height; - pdfPage.appendChild(textLayerDiv); - - // Painting the canvas... - var renderContext = { - canvasContext: context, - viewport: viewport - }; - var renderTask = page.render(renderContext); - - // ... and at the same time, getting the text and creating the text layer. - var textLayerPromise = page.getTextContent().then(function (textContent) { - var textLayerBuilder = new TextLayerBuilder({ - textLayerDiv: textLayerDiv, - viewport: viewport, - pageIndex: 0 - }); - textLayerBuilder.setTextContent(textContent); - }); - - // We might be interested when rendering complete and text layer is built. - return Promise.all([renderTask.promise, textLayerPromise]); - } - - loadPdf('pdf/TestDocument.pdf'); -}; - diff --git a/examples/text-selection/pdf/TestDocument.pdf b/examples/text-selection/pdf/TestDocument.pdf deleted file mode 100644 index 843fd9d2bda87f634a58449d017d7624fe17c02b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35153 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	I@akv)b#CuGw#f-4^-;NmzM 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 diff --git a/web/annotations_layer_builder.js b/web/annotations_layer_builder.js index 494ceffb0..b5ef814ec 100644 --- a/web/annotations_layer_builder.js +++ b/web/annotations_layer_builder.js @@ -103,7 +103,9 @@ var AnnotationsLayerBuilder = (function AnnotationsLayerBuilderClosure() { element = PDFJS.AnnotationUtils.getHtmlElement(data, pdfPage.commonObjs); element.setAttribute('data-annotation-id', data.id); - mozL10n.translate(element); + if (typeof mozL10n !== 'undefined') { + mozL10n.translate(element); + } var rect = data.rect; var view = pdfPage.view; diff --git a/web/pdf_viewer.component.js b/web/pdf_viewer.component.js index 22c483854..8c1223ab7 100644 --- a/web/pdf_viewer.component.js +++ b/web/pdf_viewer.component.js @@ -15,7 +15,9 @@ * limitations under the License. */ /*jshint globalstrict: false */ -/* globals PDFJS, PDFViewer */ +/* globals PDFJS, PDFViewer, PDFPageView, TextLayerBuilder, + DefaultTextLayerFactory, AnnotationsLayerBuilder, + DefaultAnnotationsLayerFactory */ // Initializing PDFJS global object (if still undefined) if (typeof PDFJS === 'undefined') { @@ -29,4 +31,9 @@ if (typeof PDFJS === 'undefined') { //#include pdf_viewer.js PDFJS.PDFViewer = PDFViewer; + PDFJS.PDFPageView = PDFPageView; + PDFJS.TextLayerBuilder = TextLayerBuilder; + PDFJS.DefaultTextLayerFactory = DefaultTextLayerFactory; + PDFJS.AnnotationsLayerBuilder = AnnotationsLayerBuilder; + PDFJS.DefaultAnnotationsLayerFactory = DefaultAnnotationsLayerFactory; }).call((typeof window === 'undefined') ? this : window); diff --git a/web/text_layer_builder.css b/web/text_layer_builder.css index 32f7c497b..f5a55df10 100644 --- a/web/text_layer_builder.css +++ b/web/text_layer_builder.css @@ -20,6 +20,7 @@ right: 0; bottom: 0; overflow: hidden; + opacity: 0.2; } .textLayer > div { @@ -57,3 +58,6 @@ .textLayer .highlight.selected { background-color: rgb(0, 100, 0); } + +.textLayer ::selection { background: rgb(0,0,255); } +.textLayer ::-moz-selection { background: rgb(0,0,255); } diff --git a/web/viewer.css b/web/viewer.css index 066b12aeb..5fcb3f0fd 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -1258,19 +1258,12 @@ html[dir='rtl'] .attachmentsItem > button { cursor: default; } - /* TODO: file FF bug to support ::-moz-selection:window-inactive so we can override the opaque grey background when the window is inactive; see https://bugzilla.mozilla.org/show_bug.cgi?id=706209 */ ::selection { background: rgba(0,0,255,0.3); } ::-moz-selection { background: rgba(0,0,255,0.3); } -.textLayer ::selection { background: rgb(0,0,255); } -.textLayer ::-moz-selection { background: rgb(0,0,255); } -.textLayer { - opacity: 0.2; -} - #errorWrapper { background: none repeat scroll 0 0 #FF5555; color: white;