From c9a0955c9c7ed857c98696732c6edeb6f4901f43 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Wed, 19 Oct 2016 09:16:57 -0500 Subject: [PATCH 1/2] Refactors PDFPageView_draw. --- web/pdf_page_view.js | 328 ++++++++++++++++++++++++------------------- 1 file changed, 181 insertions(+), 147 deletions(-) diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 14e9fd0b6..3e3bd2fff 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -93,9 +93,11 @@ var PDFPageView = (function PDFPageViewClosure() { this.textLayerFactory = textLayerFactory; this.annotationLayerFactory = annotationLayerFactory; - this.renderTask = null; + this.paintTask = null; + this.paintedViewport = null; this.renderingState = RenderingStates.INITIAL; this.resume = null; + this.error = null; this.onBeforeDraw = null; this.onAfterDraw = null; @@ -171,6 +173,9 @@ var PDFPageView = (function PDFPageViewClosure() { this.canvas.height = 0; delete this.canvas; } + if (!currentZoomLayerNode) { + this.paintedViewport = null; + } this.loadingIconDiv = document.createElement('div'); this.loadingIconDiv.className = 'loadingIcon'; @@ -224,9 +229,9 @@ var PDFPageView = (function PDFPageViewClosure() { }, cancelRendering: function PDFPageView_cancelRendering() { - if (this.renderTask) { - this.renderTask.cancel(); - this.renderTask = null; + if (this.paintTask) { + this.paintTask.cancel(); + this.paintTask = null; } this.renderingState = RenderingStates.INITIAL; this.resume = null; @@ -258,7 +263,8 @@ var PDFPageView = (function PDFPageViewClosure() { canvas.style.height = canvas.parentNode.style.height = div.style.height = Math.floor(height) + 'px'; // The canvas may have been originally rotated, rotate relative to that. - var relativeRotation = this.viewport.rotation - canvas._viewport.rotation; + var relativeRotation = this.viewport.rotation - + this.paintedViewport.rotation; var absRotation = Math.abs(relativeRotation); var scaleX = 1, scaleY = 1; if (absRotation === 90 || absRotation === 270) { @@ -337,6 +343,7 @@ var PDFPageView = (function PDFPageViewClosure() { this.renderingState = RenderingStates.RUNNING; + var self = this; var pdfPage = this.pdfPage; var viewport = this.viewport; var div = this.div; @@ -347,20 +354,165 @@ var PDFPageView = (function PDFPageViewClosure() { canvasWrapper.style.height = div.style.height; canvasWrapper.classList.add('canvasWrapper'); - var canvas = document.createElement('canvas'); - canvas.id = 'page' + this.id; - // Keep the canvas hidden until the first draw callback, or until drawing - // is complete when `!this.renderingQueue`, to prevent black flickering. - canvas.setAttribute('hidden', 'hidden'); - var isCanvasHidden = true; - - canvasWrapper.appendChild(canvas); if (this.annotationLayer && this.annotationLayer.div) { // annotationLayer needs to stay on top div.insertBefore(canvasWrapper, this.annotationLayer.div); } else { div.appendChild(canvasWrapper); } + + var textLayerDiv = null; + var textLayer = null; + if (this.textLayerFactory) { + textLayerDiv = document.createElement('div'); + textLayerDiv.className = 'textLayer'; + textLayerDiv.style.width = canvasWrapper.style.width; + textLayerDiv.style.height = canvasWrapper.style.height; + if (this.annotationLayer && this.annotationLayer.div) { + // annotationLayer needs to stay on top + div.insertBefore(textLayerDiv, this.annotationLayer.div); + } else { + div.appendChild(textLayerDiv); + } + + textLayer = this.textLayerFactory. + createTextLayerBuilder(textLayerDiv, this.id - 1, this.viewport, + this.enhanceTextSelection); + } + this.textLayer = textLayer; + + var renderContinueCallback = null; + if (this.renderingQueue) { + renderContinueCallback = function renderContinueCallback(cont) { + if (!self.renderingQueue.isHighestPriority(self)) { + self.renderingState = RenderingStates.PAUSED; + self.resume = function resumeCallback() { + self.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + }; + } + + var finishPaintTask = function finishPaintTask(error) { + // The paintTask may have been replaced by a new one, so only remove + // the reference to the paintTask if it matches the one that is + // triggering this callback. + if (paintTask === self.paintTask) { + self.paintTask = null; + } + + if (error === 'cancelled') { + self.error = null; + return; + } + + self.renderingState = RenderingStates.FINISHED; + + if (self.loadingIconDiv) { + div.removeChild(self.loadingIconDiv); + delete self.loadingIconDiv; + } + + if (self.zoomLayer) { + // Zeroing the width and height causes Firefox to release graphics + // resources immediately, which can greatly reduce memory consumption. + var zoomLayerCanvas = self.zoomLayer.firstChild; + zoomLayerCanvas.width = 0; + zoomLayerCanvas.height = 0; + + if (div.contains(self.zoomLayer)) { + // Prevent "Node was not found" errors if the `zoomLayer` was + // already removed. This may occur intermittently if the scale + // changes many times in very quick succession. + div.removeChild(self.zoomLayer); + } + self.zoomLayer = null; + } + + self.error = error; + self.stats = pdfPage.stats; + if (self.onAfterDraw) { + self.onAfterDraw(); + } + self.eventBus.dispatch('pagerendered', { + source: self, + pageNumber: self.id, + cssTransform: false, + }); + }; + + var paintTask = this.paintOnCanvas(canvasWrapper); + paintTask.onRenderContinue = renderContinueCallback; + this.paintTask = paintTask; + + var resultPromise = paintTask.promise.then(function () { + finishPaintTask(null); + if (textLayer) { + pdfPage.getTextContent({ + normalizeWhitespace: true, + }).then(function textContentResolved(textContent) { + textLayer.setTextContent(textContent); + textLayer.render(TEXT_LAYER_RENDER_DELAY); + }); + } + }, function (reason) { + finishPaintTask(reason); + throw reason; + }); + + if (this.annotationLayerFactory) { + if (!this.annotationLayer) { + this.annotationLayer = this.annotationLayerFactory. + createAnnotationLayerBuilder(div, pdfPage, + this.renderInteractiveForms); + } + this.annotationLayer.render(this.viewport, 'display'); + } + div.setAttribute('data-loaded', true); + + if (this.onBeforeDraw) { + this.onBeforeDraw(); + } + return resultPromise; + }, + + paintOnCanvas: function (canvasWrapper) { + var resolveRenderPromise, rejectRenderPromise; + var promise = new Promise(function (resolve, reject) { + resolveRenderPromise = resolve; + rejectRenderPromise = reject; + }); + + var result = { + promise: promise, + onRenderContinue: function (cont) { + cont(); + }, + cancel: function () { + renderTask.cancel(); + } + }; + + var self = this; + var pdfPage = this.pdfPage; + var viewport = this.viewport; + var canvas = document.createElement('canvas'); + canvas.id = 'page' + this.id; + // Keep the canvas hidden until the first draw callback, or until drawing + // is complete when `!this.renderingQueue`, to prevent black flickering. + canvas.setAttribute('hidden', 'hidden'); + var isCanvasHidden = true; + var showCanvas = function () { + if (isCanvasHidden) { + canvas.removeAttribute('hidden'); + isCanvasHidden = false; + } + }; + + canvasWrapper.appendChild(canvas); this.canvas = canvas; if (typeof PDFJSDev === 'undefined' || @@ -402,115 +554,9 @@ var PDFPageView = (function PDFPageViewClosure() { canvas.style.width = roundToDivide(viewport.width, sfx[1]) + 'px'; canvas.style.height = roundToDivide(viewport.height, sfy[1]) + 'px'; // Add the viewport so it's known what it was originally drawn with. - canvas._viewport = viewport; - - var textLayerDiv = null; - var textLayer = null; - if (this.textLayerFactory) { - textLayerDiv = document.createElement('div'); - textLayerDiv.className = 'textLayer'; - textLayerDiv.style.width = canvasWrapper.style.width; - textLayerDiv.style.height = canvasWrapper.style.height; - if (this.annotationLayer && this.annotationLayer.div) { - // annotationLayer needs to stay on top - div.insertBefore(textLayerDiv, this.annotationLayer.div); - } else { - div.appendChild(textLayerDiv); - } - - textLayer = this.textLayerFactory. - createTextLayerBuilder(textLayerDiv, this.id - 1, this.viewport, - this.enhanceTextSelection); - } - this.textLayer = textLayer; - - var resolveRenderPromise, rejectRenderPromise; - var promise = new Promise(function (resolve, reject) { - resolveRenderPromise = resolve; - rejectRenderPromise = reject; - }); + this.paintedViewport = viewport; // 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; - } - - if (error === 'cancelled') { - rejectRenderPromise(error); - return; - } - - self.renderingState = RenderingStates.FINISHED; - - if (isCanvasHidden) { - self.canvas.removeAttribute('hidden'); - isCanvasHidden = false; - } - - if (self.loadingIconDiv) { - div.removeChild(self.loadingIconDiv); - delete self.loadingIconDiv; - } - - if (self.zoomLayer) { - // Zeroing the width and height causes Firefox to release graphics - // resources immediately, which can greatly reduce memory consumption. - var zoomLayerCanvas = self.zoomLayer.firstChild; - zoomLayerCanvas.width = 0; - zoomLayerCanvas.height = 0; - - if (div.contains(self.zoomLayer)) { - // Prevent "Node was not found" errors if the `zoomLayer` was - // already removed. This may occur intermittently if the scale - // changes many times in very quick succession. - div.removeChild(self.zoomLayer); - } - self.zoomLayer = null; - } - - self.error = error; - self.stats = pdfPage.stats; - if (self.onAfterDraw) { - self.onAfterDraw(); - } - self.eventBus.dispatch('pagerendered', { - source: self, - pageNumber: self.id, - cssTransform: false, - }); - - if (!error) { - resolveRenderPromise(undefined); - } else { - rejectRenderPromise(error); - } - } - - var renderContinueCallback = null; - if (this.renderingQueue) { - renderContinueCallback = function renderContinueCallback(cont) { - if (!self.renderingQueue.isHighestPriority(self)) { - self.renderingState = RenderingStates.PAUSED; - self.resume = function resumeCallback() { - self.renderingState = RenderingStates.RUNNING; - cont(); - }; - return; - } - if (isCanvasHidden) { - self.canvas.removeAttribute('hidden'); - isCanvasHidden = false; - } - cont(); - }; - } - var transform = !outputScale.scaled ? null : [outputScale.sx, 0, 0, outputScale.sy, 0, 0]; var renderContext = { @@ -520,40 +566,28 @@ var PDFPageView = (function PDFPageViewClosure() { renderInteractiveForms: this.renderInteractiveForms, // intent: 'default', // === 'display' }; - var renderTask = this.renderTask = this.pdfPage.render(renderContext); - renderTask.onContinue = renderContinueCallback; + var renderTask = this.pdfPage.render(renderContext); + renderTask.onContinue = function (cont) { + showCanvas(); + if (result.onRenderContinue) { + result.onRenderContinue(cont); + } else { + cont(); + } + }; - this.renderTask.promise.then( + renderTask.promise.then( function pdfPageRenderCallback() { - pageViewDrawCallback(null); - if (textLayer) { - self.pdfPage.getTextContent({ - normalizeWhitespace: true, - }).then(function textContentResolved(textContent) { - textLayer.setTextContent(textContent); - textLayer.render(TEXT_LAYER_RENDER_DELAY); - }); - } + showCanvas(); + resolveRenderPromise(undefined); }, function pdfPageRenderError(error) { - pageViewDrawCallback(error); + showCanvas(); + rejectRenderPromise(error); } ); - if (this.annotationLayerFactory) { - if (!this.annotationLayer) { - this.annotationLayer = this.annotationLayerFactory. - createAnnotationLayerBuilder(div, this.pdfPage, - this.renderInteractiveForms); - } - this.annotationLayer.render(this.viewport, 'display'); - } - div.setAttribute('data-loaded', true); - - if (self.onBeforeDraw) { - self.onBeforeDraw(); - } - return promise; + return result; }, /** From f7d6f3a7393fcefecc9aa66b7dd9581400e559a7 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Fri, 18 Nov 2016 13:03:49 -0600 Subject: [PATCH 2/2] Adds SVG rendering capabilities to the PDFViewer. --- extensions/chromium/preferences_schema.json | 8 +++ src/display/svg.js | 1 + web/app.js | 13 +++- web/default_preferences.json | 1 + web/pdf_page_view.js | 74 +++++++++++++++++++-- web/pdf_viewer.js | 4 ++ web/ui_utils.js | 6 ++ 7 files changed, 100 insertions(+), 7 deletions(-) diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index 9bff7d805..43bd863d6 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -103,6 +103,14 @@ "type": "boolean", "default": false }, + "renderer": { + "type": "string", + "enum": [ + "canvas", + "svg" + ], + "default": "canvas" + }, "renderInteractiveForms": { "type": "boolean", "default": false diff --git a/src/display/svg.js b/src/display/svg.js index eb7ca4805..92be5e788 100644 --- a/src/display/svg.js +++ b/src/display/svg.js @@ -1147,6 +1147,7 @@ var SVGGraphics = (function SVGGraphicsClosure() { svg.setAttributeNS(null, 'version', '1.1'); svg.setAttributeNS(null, 'width', viewport.width + 'px'); svg.setAttributeNS(null, 'height', viewport.height + 'px'); + svg.setAttributeNS(null, 'preserveAspectRatio', 'none'); svg.setAttributeNS(null, 'viewBox', '0 0 ' + viewport.width + ' ' + viewport.height); diff --git a/web/app.js b/web/app.js index b53d779c5..7a192bbe3 100644 --- a/web/app.js +++ b/web/app.js @@ -102,6 +102,7 @@ var getGlobalEventBus = domEventsLib.getGlobalEventBus; var normalizeWheelEventDelta = uiUtilsLib.normalizeWheelEventDelta; var animationStarted = uiUtilsLib.animationStarted; var localized = uiUtilsLib.localized; +var RendererType = uiUtilsLib.RendererType; var DEFAULT_SCALE_DELTA = 1.1; var DISABLE_AUTO_FETCH_LOADING_BAR_TIMEOUT = 5000; @@ -383,6 +384,12 @@ var PDFViewerApplication = { } PDFJS.externalLinkTarget = value; }), + Preferences.get('renderer').then(function resolved(value) { + // TODO: Like the `enhanceTextSelection` preference, move the + // initialization and fetching of `Preferences` to occur + // before the various viewer components are initialized. + self.pdfViewer.renderer = value; + }), Preferences.get('renderInteractiveForms').then(function resolved(value) { // TODO: Like the `enhanceTextSelection` preference, move the // initialization and fetching of `Preferences` to occur @@ -1140,7 +1147,11 @@ var PDFViewerApplication = { } this.pdfViewer.cleanup(); this.pdfThumbnailViewer.cleanup(); - this.pdfDocument.cleanup(); + + // We don't want to remove fonts used by active page SVGs. + if (this.pdfViewer.renderer !== RendererType.SVG) { + this.pdfDocument.cleanup(); + } }, forceRendering: function pdfViewForceRendering() { diff --git a/web/default_preferences.json b/web/default_preferences.json index 15371f6b3..7e8114f31 100644 --- a/web/default_preferences.json +++ b/web/default_preferences.json @@ -13,6 +13,7 @@ "useOnlyCssZoom": false, "externalLinkTarget": 0, "enhanceTextSelection": false, + "renderer": "canvas", "renderInteractiveForms": false, "disablePageLabels": false } diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 3e3bd2fff..d26712d41 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -36,6 +36,7 @@ var DEFAULT_SCALE = uiUtils.DEFAULT_SCALE; var getOutputScale = uiUtils.getOutputScale; var approximateFraction = uiUtils.approximateFraction; var roundToDivide = uiUtils.roundToDivide; +var RendererType = uiUtils.RendererType; var RenderingStates = pdfRenderingQueue.RenderingStates; var TEXT_LAYER_RENDER_DELAY = 200; // ms @@ -54,6 +55,7 @@ var TEXT_LAYER_RENDER_DELAY = 200; // ms * enhancement. The default is `false`. * @property {boolean} renderInteractiveForms - Turns on rendering of * interactive form elements. The default is `false`. + * @property {string} renderer - 'canvas' or 'svg'. The default is 'canvas'. */ /** @@ -92,6 +94,7 @@ var PDFPageView = (function PDFPageViewClosure() { this.renderingQueue = renderingQueue; this.textLayerFactory = textLayerFactory; this.annotationLayerFactory = annotationLayerFactory; + this.renderer = options.renderer || RendererType.CANVAS; this.paintTask = null; this.paintedViewport = null; @@ -173,6 +176,9 @@ var PDFPageView = (function PDFPageViewClosure() { this.canvas.height = 0; delete this.canvas; } + if (this.svg) { + delete this.svg; + } if (!currentZoomLayerNode) { this.paintedViewport = null; } @@ -195,6 +201,17 @@ var PDFPageView = (function PDFPageViewClosure() { rotation: totalRotation }); + if (this.svg) { + this.cssTransform(this.svg, true); + + this.eventBus.dispatch('pagerendered', { + source: this, + pageNumber: this.id, + cssTransform: true, + }); + return; + } + var isScalingRestricted = false; if (this.canvas && pdfjsLib.PDFJS.maxCanvasPixels > 0) { var outputScale = this.outputScale; @@ -251,16 +268,16 @@ var PDFPageView = (function PDFPageViewClosure() { } }, - cssTransform: function PDFPageView_transform(canvas, redrawAnnotations) { + cssTransform: function PDFPageView_transform(target, redrawAnnotations) { var CustomStyle = pdfjsLib.CustomStyle; - // Scale canvas, canvas wrapper, and page container. + // Scale target (canvas or svg), its 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 = + target.style.width = target.parentNode.style.width = div.style.width = Math.floor(width) + 'px'; - canvas.style.height = canvas.parentNode.style.height = div.style.height = + target.style.height = target.parentNode.style.height = div.style.height = Math.floor(height) + 'px'; // The canvas may have been originally rotated, rotate relative to that. var relativeRotation = this.viewport.rotation - @@ -274,7 +291,7 @@ var PDFPageView = (function PDFPageViewClosure() { } var cssTransform = 'rotate(' + relativeRotation + 'deg) ' + 'scale(' + scaleX + ',' + scaleY + ')'; - CustomStyle.setProp('transform', canvas, cssTransform); + CustomStyle.setProp('transform', target, cssTransform); if (this.textLayer) { // Rotating the text layer is more complicated since the divs inside the @@ -444,7 +461,9 @@ var PDFPageView = (function PDFPageViewClosure() { }); }; - var paintTask = this.paintOnCanvas(canvasWrapper); + var paintTask = this.renderer === RendererType.SVG ? + this.paintOnSvg(canvasWrapper) : + this.paintOnCanvas(canvasWrapper); paintTask.onRenderContinue = renderContinueCallback; this.paintTask = paintTask; @@ -590,6 +609,49 @@ var PDFPageView = (function PDFPageViewClosure() { return result; }, + paintOnSvg: function PDFPageView_paintOnSvg(wrapper) { + if (typeof PDFJSDev !== 'undefined' && + PDFJSDev.test('FIREFOX || MOZCENTRAL || CHROME')) { + return Promise.resolve('SVG rendering is not supported.'); + } + + var cancelled = false; + var ensureNotCancelled = function () { + if (cancelled) { + throw 'cancelled'; + } + }; + + var self = this; + var pdfPage = this.pdfPage; + var SVGGraphics = pdfjsLib.SVGGraphics; + var actualSizeViewport = this.viewport.clone({scale: CSS_UNITS}); + var promise = pdfPage.getOperatorList().then(function (opList) { + ensureNotCancelled(); + var svgGfx = new SVGGraphics(pdfPage.commonObjs, pdfPage.objs); + return svgGfx.getSVG(opList, actualSizeViewport).then(function (svg) { + ensureNotCancelled(); + self.svg = svg; + self.paintedViewport = actualSizeViewport; + + svg.style.width = wrapper.style.width; + svg.style.height = wrapper.style.height; + self.renderingState = RenderingStates.FINISHED; + wrapper.appendChild(svg); + }); + }); + + return { + promise: promise, + onRenderContinue: function (cont) { + cont(); + }, + cancel: function () { + cancelled = true; + } + }; + }, + /** * @param {string|null} label */ diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index ede997ef2..459d3d95a 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -45,6 +45,7 @@ var MAX_AUTO_SCALE = uiUtils.MAX_AUTO_SCALE; var CSS_UNITS = uiUtils.CSS_UNITS; var DEFAULT_SCALE = uiUtils.DEFAULT_SCALE; var DEFAULT_SCALE_VALUE = uiUtils.DEFAULT_SCALE_VALUE; +var RendererType = uiUtils.RendererType; var scrollIntoView = uiUtils.scrollIntoView; var watchScroll = uiUtils.watchScroll; var getVisibleElements = uiUtils.getVisibleElements; @@ -80,6 +81,7 @@ var DEFAULT_CACHE_SIZE = 10; * text selection behaviour. The default is `false`. * @property {boolean} renderInteractiveForms - (optional) Enables rendering of * interactive form elements. The default is `false`. + * @property {string} renderer - 'canvas' or 'svg'. The default is 'canvas'. */ /** @@ -133,6 +135,7 @@ var PDFViewer = (function pdfViewer() { this.removePageBorders = options.removePageBorders || false; this.enhanceTextSelection = options.enhanceTextSelection || false; this.renderInteractiveForms = options.renderInteractiveForms || false; + this.renderer = options.renderer || RendererType.CANVAS; this.defaultRenderingQueue = !options.renderingQueue; if (this.defaultRenderingQueue) { @@ -393,6 +396,7 @@ var PDFViewer = (function pdfViewer() { annotationLayerFactory: this, enhanceTextSelection: this.enhanceTextSelection, renderInteractiveForms: this.renderInteractiveForms, + renderer: this.renderer, }); bindOnAfterAndBeforeDraw(pageView); this._pages.push(pageView); diff --git a/web/ui_utils.js b/web/ui_utils.js index 9679c4235..2e6726eb6 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -35,6 +35,11 @@ var MAX_AUTO_SCALE = 1.25; var SCROLLBAR_PADDING = 40; var VERTICAL_PADDING = 5; +var RendererType = { + CANVAS: 'canvas', + SVG: 'svg', +}; + var mozL10n = document.mozL10n || document.webL10n; var PDFJS = pdfjsLib.PDFJS; @@ -568,6 +573,7 @@ exports.UNKNOWN_SCALE = UNKNOWN_SCALE; exports.MAX_AUTO_SCALE = MAX_AUTO_SCALE; exports.SCROLLBAR_PADDING = SCROLLBAR_PADDING; exports.VERTICAL_PADDING = VERTICAL_PADDING; +exports.RendererType = RendererType; exports.mozL10n = mozL10n; exports.EventBus = EventBus; exports.ProgressBar = ProgressBar;