From d7198d3e178214e91e9c19ab8f8f095d1d4ffcbc Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 1 Aug 2017 14:02:16 +0200 Subject: [PATCH 1/3] Rename `web/pdf_viewer.js` to `web/base_viewer.js` Please note that the only reason for this change is to try and improve reviewability of later patches, by keeping the diffs more manageable. --- web/app.js | 2 +- web/{pdf_viewer.js => base_viewer.js} | 0 web/pdf_viewer.component.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename web/{pdf_viewer.js => base_viewer.js} (100%) diff --git a/web/app.js b/web/app.js index dc3c6044b..2918764df 100644 --- a/web/app.js +++ b/web/app.js @@ -27,7 +27,7 @@ import { import { CursorTool, PDFCursorTools } from './pdf_cursor_tools'; import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue'; import { PDFSidebar, SidebarView } from './pdf_sidebar'; -import { PDFViewer, PresentationModeState } from './pdf_viewer'; +import { PDFViewer, PresentationModeState } from './base_viewer'; import { getGlobalEventBus } from './dom_events'; import { OverlayManager } from './overlay_manager'; import { PasswordPrompt } from './password_prompt'; diff --git a/web/pdf_viewer.js b/web/base_viewer.js similarity index 100% rename from web/pdf_viewer.js rename to web/base_viewer.js diff --git a/web/pdf_viewer.component.js b/web/pdf_viewer.component.js index 584aa9ed6..42b7cba2a 100644 --- a/web/pdf_viewer.component.js +++ b/web/pdf_viewer.component.js @@ -16,7 +16,7 @@ 'use strict'; var pdfjsLib = require('./pdfjs.js'); -var pdfjsWebPDFViewer = require('./pdf_viewer.js'); +var pdfjsWebPDFViewer = require('./base_viewer.js'); var pdfjsWebPDFPageView = require('./pdf_page_view.js'); var pdfjsWebPDFLinkService = require('./pdf_link_service.js'); var pdfjsWebTextLayerBuilder = require('./text_layer_builder.js'); From 5fa9cca8dd1d213b6dd0be9ee6ea911deb80b98a Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 1 Aug 2017 14:11:28 +0200 Subject: [PATCH 2/3] Refactor `PDFViewer` to extend an abstract `BaseViewer` class This patch introduces an abstract `BaseViewer` class, that the existing `PDFViewer` then extends. *Please note:* This lays the necessary foundation for the next patch. --- web/app.js | 4 +- web/base_viewer.js | 119 +++++++++++++----------------------- web/pdf_viewer.component.js | 2 +- web/pdf_viewer.js | 83 +++++++++++++++++++++++++ web/ui_utils.js | 8 +++ 5 files changed, 138 insertions(+), 78 deletions(-) create mode 100644 web/pdf_viewer.js diff --git a/web/app.js b/web/app.js index 2918764df..ed5e6fbe2 100644 --- a/web/app.js +++ b/web/app.js @@ -17,7 +17,7 @@ import { animationStarted, DEFAULT_SCALE_VALUE, getPDFFileNameFromURL, isValidRotation, MAX_SCALE, MIN_SCALE, noContextMenuHandler, normalizeWheelEventDelta, - parseQueryString, ProgressBar, RendererType + parseQueryString, PresentationModeState, ProgressBar, RendererType } from './ui_utils'; import { build, createBlob, getDocument, getFilenameFromUrl, InvalidPDFException, @@ -27,7 +27,6 @@ import { import { CursorTool, PDFCursorTools } from './pdf_cursor_tools'; import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue'; import { PDFSidebar, SidebarView } from './pdf_sidebar'; -import { PDFViewer, PresentationModeState } from './base_viewer'; import { getGlobalEventBus } from './dom_events'; import { OverlayManager } from './overlay_manager'; import { PasswordPrompt } from './password_prompt'; @@ -40,6 +39,7 @@ import { PDFLinkService } from './pdf_link_service'; import { PDFOutlineViewer } from './pdf_outline_viewer'; import { PDFPresentationMode } from './pdf_presentation_mode'; import { PDFThumbnailViewer } from './pdf_thumbnail_viewer'; +import { PDFViewer } from './pdf_viewer'; import { SecondaryToolbar } from './secondary_toolbar'; import { Toolbar } from './toolbar'; import { ViewHistory } from './view_history'; diff --git a/web/base_viewer.js b/web/base_viewer.js index b4f9e3b38..dcb7125de 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -15,9 +15,9 @@ import { createPromiseCapability, PDFJS } from 'pdfjs-lib'; import { - CSS_UNITS, DEFAULT_SCALE, DEFAULT_SCALE_VALUE, getVisibleElements, - isValidRotation, MAX_AUTO_SCALE, NullL10n, RendererType, SCROLLBAR_PADDING, - scrollIntoView, UNKNOWN_SCALE, VERTICAL_PADDING, watchScroll + CSS_UNITS, DEFAULT_SCALE, DEFAULT_SCALE_VALUE, isValidRotation, + MAX_AUTO_SCALE, NullL10n, PresentationModeState, RendererType, + SCROLLBAR_PADDING, UNKNOWN_SCALE, VERTICAL_PADDING, watchScroll } from './ui_utils'; import { PDFRenderingQueue, RenderingStates } from './pdf_rendering_queue'; import { AnnotationLayerBuilder } from './annotation_layer_builder'; @@ -26,13 +26,6 @@ import { PDFPageView } from './pdf_page_view'; import { SimpleLinkService } from './pdf_link_service'; import { TextLayerBuilder } from './text_layer_builder'; -const PresentationModeState = { - UNKNOWN: 0, - NORMAL: 1, - CHANGING: 2, - FULLSCREEN: 3, -}; - const DEFAULT_CACHE_SIZE = 10; /** @@ -60,7 +53,7 @@ const DEFAULT_CACHE_SIZE = 10; function PDFPageViewBuffer(size) { let data = []; - this.push = function cachePush(view) { + this.push = function(view) { let i = data.indexOf(view); if (i >= 0) { data.splice(i, 1); @@ -70,7 +63,7 @@ function PDFPageViewBuffer(size) { data.shift().destroy(); } }; - this.resize = function (newSize) { + this.resize = function(newSize) { size = newSize; while (data.length > size) { data.shift().destroy(); @@ -98,11 +91,16 @@ function isPortraitOrientation(size) { * Simple viewer control to display PDF content/pages. * @implements {IRenderableView} */ -class PDFViewer { +class BaseViewer { /** * @param {PDFViewerOptions} options */ constructor(options) { + if (this.constructor === BaseViewer) { + throw new Error('Cannot initialize BaseViewer.'); + } + this._name = this.constructor.name; + this.container = options.container; this.viewer = options.viewer || options.container.firstElementChild; this.eventBus = options.eventBus || getGlobalEventBus(); @@ -182,7 +180,7 @@ class PDFViewer { if (!(0 < val && val <= this.pagesCount)) { console.error( - `PDFViewer._setCurrentPageNumber: "${val}" is out of bounds.`); + `${this._name}._setCurrentPageNumber: "${val}" is out of bounds.`); return; } @@ -305,6 +303,10 @@ class PDFViewer { } } + get _setDocumentViewerElement() { + throw new Error('Not implemented: _setDocumentViewerElement'); + } + /** * @param pdfDocument {PDFDocument} */ @@ -364,7 +366,7 @@ class PDFViewer { textLayerFactory = this; } let pageView = new PDFPageView({ - container: this.viewer, + container: this._setDocumentViewerElement, eventBus: this.eventBus, id: pageNum, scale, @@ -437,7 +439,7 @@ class PDFViewer { } else if (!(labels instanceof Array && this.pdfDocument.numPages === labels.length)) { this._pageLabels = null; - console.error('PDFViewer.setPageLabels: Invalid page labels.'); + console.error(`${this._name}.setPageLabels: Invalid page labels.`); } else { this._pageLabels = labels; } @@ -472,6 +474,10 @@ class PDFViewer { this.update(); } + _scrollIntoView({ pageDiv, pageSpot = null, pageNumber = null, }) { + throw new Error('Not implemented: _scrollIntoView'); + } + _setScaleDispatchEvent(newScale, newValue, preset = false) { let arg = { source: this, @@ -560,7 +566,7 @@ class PDFViewer { break; default: console.error( - `PDFViewer._setScale: "${value}" is an unknown zoom value.`); + `${this._name}._setScale: "${value}" is an unknown zoom value.`); return; } this._setScaleUpdatePages(scale, value, noScroll, /* preset = */ true); @@ -578,7 +584,7 @@ class PDFViewer { } let pageView = this._pages[this._currentPageNumber - 1]; - scrollIntoView(pageView.div); + this._scrollIntoView({ pageDiv: pageView.div, }); } /** @@ -615,7 +621,7 @@ class PDFViewer { let pageView = this._pages[pageNumber - 1]; if (!pageView) { console.error( - 'PDFViewer.scrollPageIntoView: Invalid "pageNumber" parameter.'); + `${this._name}.scrollPageIntoView: Invalid "pageNumber" parameter.`); return; } let x = 0, y = 0; @@ -675,7 +681,7 @@ class PDFViewer { scale = Math.min(Math.abs(widthScale), Math.abs(heightScale)); break; default: - console.error(`PDFViewer.scrollPageIntoView: "${dest[1].name}" ` + + console.error(`${this._name}.scrollPageIntoView: "${dest[1].name}" ` + 'is not a valid destination type.'); return; } @@ -687,7 +693,10 @@ class PDFViewer { } if (scale === 'page-fit' && !dest[4]) { - scrollIntoView(pageView.div); + this._scrollIntoView({ + pageDiv: pageView.div, + pageNumber, + }); return; } @@ -705,7 +714,17 @@ class PDFViewer { left = Math.max(left, 0); top = Math.max(top, 0); } - scrollIntoView(pageView.div, { left, top, }); + this._scrollIntoView({ + pageDiv: pageView.div, + pageSpot: { left, top, }, + pageNumber, + }); + } + + _resizeBuffer(numVisiblePages) { + let suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE, + 2 * numVisiblePages + 1); + this._buffer.resize(suggestedCacheSize); } _updateLocation(firstPage) { @@ -738,48 +757,7 @@ class PDFViewer { } update() { - let visible = this._getVisiblePages(); - let visiblePages = visible.views; - if (visiblePages.length === 0) { - return; - } - - let suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE, - 2 * visiblePages.length + 1); - this._buffer.resize(suggestedCacheSize); - - this.renderingQueue.renderHighestPriority(visible); - - let currentId = this._currentPageNumber; - let firstPage = visible.first; - let stillFullyVisible = false; - - for (let i = 0, ii = visiblePages.length; i < ii; ++i) { - let page = visiblePages[i]; - - if (page.percent < 100) { - break; - } - if (page.id === currentId) { - stillFullyVisible = true; - break; - } - } - - if (!stillFullyVisible) { - currentId = visiblePages[0].id; - } - - if (!this.isInPresentationMode) { - this._setCurrentPageNumber(currentId); - } - - this._updateLocation(firstPage); - - this.eventBus.dispatch('updateviewarea', { - source: this, - location: this._location, - }); + throw new Error('Not implemented: update'); } containsElement(element) { @@ -804,15 +782,7 @@ class PDFViewer { } _getVisiblePages() { - if (!this.isInPresentationMode) { - return getVisibleElements(this.container, this._pages, true); - } - // The algorithm in getVisibleElements doesn't work in all browsers and - // configurations when presentation mode is active. - let visible = []; - let currentPage = this._pages[this._currentPageNumber - 1]; - visible.push({ id: currentPage.id, view: currentPage, }); - return { first: currentPage, last: currentPage, views: visible, }; + throw new Error('Not implemented: _getVisiblePages'); } cleanup() { @@ -974,6 +944,5 @@ class PDFViewer { } export { - PresentationModeState, - PDFViewer, + BaseViewer, }; diff --git a/web/pdf_viewer.component.js b/web/pdf_viewer.component.js index 42b7cba2a..584aa9ed6 100644 --- a/web/pdf_viewer.component.js +++ b/web/pdf_viewer.component.js @@ -16,7 +16,7 @@ 'use strict'; var pdfjsLib = require('./pdfjs.js'); -var pdfjsWebPDFViewer = require('./base_viewer.js'); +var pdfjsWebPDFViewer = require('./pdf_viewer.js'); var pdfjsWebPDFPageView = require('./pdf_page_view.js'); var pdfjsWebPDFLinkService = require('./pdf_link_service.js'); var pdfjsWebTextLayerBuilder = require('./text_layer_builder.js'); diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js new file mode 100644 index 000000000..c32065ff6 --- /dev/null +++ b/web/pdf_viewer.js @@ -0,0 +1,83 @@ +/* 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. + */ + +import { getVisibleElements, scrollIntoView } from './ui_utils'; +import { BaseViewer } from './base_viewer'; +import { shadow } from 'pdfjs-lib'; + +class PDFViewer extends BaseViewer { + get _setDocumentViewerElement() { + return shadow(this, '_setDocumentViewerElement', this.viewer); + } + + _scrollIntoView({ pageDiv, pageSpot = null, }) { + scrollIntoView(pageDiv, pageSpot); + } + + _getVisiblePages() { + if (!this.isInPresentationMode) { + return getVisibleElements(this.container, this._pages, true); + } + // The algorithm in getVisibleElements doesn't work in all browsers and + // configurations when presentation mode is active. + let currentPage = this._pages[this._currentPageNumber - 1]; + let visible = [{ id: currentPage.id, view: currentPage, }]; + return { first: currentPage, last: currentPage, views: visible, }; + } + + update() { + let visible = this._getVisiblePages(); + let visiblePages = visible.views, numVisiblePages = visiblePages.length; + + if (numVisiblePages === 0) { + return; + } + this._resizeBuffer(numVisiblePages); + + this.renderingQueue.renderHighestPriority(visible); + + let currentId = this._currentPageNumber; + let stillFullyVisible = false; + + for (let i = 0; i < numVisiblePages; ++i) { + let page = visiblePages[i]; + + if (page.percent < 100) { + break; + } + if (page.id === currentId) { + stillFullyVisible = true; + break; + } + } + + if (!stillFullyVisible) { + currentId = visiblePages[0].id; + } + if (!this.isInPresentationMode) { + this._setCurrentPageNumber(currentId); + } + + this._updateLocation(visible.first); + this.eventBus.dispatch('updateviewarea', { + source: this, + location: this._location, + }); + } +} + +export { + PDFViewer, +}; diff --git a/web/ui_utils.js b/web/ui_utils.js index aec8a5793..394e1e625 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -25,6 +25,13 @@ const MAX_AUTO_SCALE = 1.25; const SCROLLBAR_PADDING = 40; const VERTICAL_PADDING = 5; +const PresentationModeState = { + UNKNOWN: 0, + NORMAL: 1, + CHANGING: 2, + FULLSCREEN: 3, +}; + const RendererType = { CANVAS: 'canvas', SVG: 'svg', @@ -661,6 +668,7 @@ export { VERTICAL_PADDING, isValidRotation, cloneObj, + PresentationModeState, RendererType, mozL10n, NullL10n, From 23daafd7289a779d2e8ffc25315f769d285dfb12 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 1 Aug 2017 14:13:49 +0200 Subject: [PATCH 3/3] Implement a `PDFSinglePageViewer` class (issue 8188) The new `PDFSinglePageViewer` class extends the previously created abstract `BaseViewer` class. There's *a lot* of existing functionality in `PDFViewer` that depends on all the pages being loaded and synchronously available, once the `setDocument` method has been called. Given that initializing `PDFPageView` instances requires passing a DOM element to which the page is attached, the simplest solution I could come up with is to append all pages to a (hidden) document fragment and just swap them (one at a time) into the viewer when page switching occurs. --- web/pdf_single_page_viewer.js | 149 ++++++++++++++++++++++++++++++++++ web/pdf_viewer.component.js | 2 + 2 files changed, 151 insertions(+) create mode 100644 web/pdf_single_page_viewer.js diff --git a/web/pdf_single_page_viewer.js b/web/pdf_single_page_viewer.js new file mode 100644 index 000000000..94fb7823e --- /dev/null +++ b/web/pdf_single_page_viewer.js @@ -0,0 +1,149 @@ +/* Copyright 2017 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. + */ + +import { BaseViewer } from './base_viewer'; +import { scrollIntoView } from './ui_utils'; +import { shadow } from 'pdfjs-lib'; + +class PDFSinglePageViewer extends BaseViewer { + constructor(options) { + super(options); + + this.eventBus.on('pagesinit', (evt) => { + // Since the pages are placed in a `DocumentFragment`, make sure that + // the current page becomes visible upon loading of the document. + this._ensurePageViewVisible(); + }); + } + + get _setDocumentViewerElement() { + // Since we only want to display *one* page at a time when using the + // `PDFSinglePageViewer`, we cannot append them to the `viewer` DOM element. + // Instead, they are placed in a `DocumentFragment`, and only the current + // page is displayed in the viewer (refer to `this._ensurePageViewVisible`). + return shadow(this, '_setDocumentViewerElement', this._shadowViewer); + } + + _resetView() { + super._resetView(); + this._previousPageNumber = 1; + this._shadowViewer = document.createDocumentFragment(); + } + + _ensurePageViewVisible() { + let pageView = this._pages[this._currentPageNumber - 1]; + let previousPageView = this._pages[this._previousPageNumber - 1]; + + let viewerNodes = this.viewer.childNodes; + switch (viewerNodes.length) { + case 0: // Should *only* occur on initial loading. + this.viewer.appendChild(pageView.div); + break; + case 1: // The normal page-switching case. + if (viewerNodes[0] !== previousPageView.div) { + throw new Error( + '_ensurePageViewVisible: Unexpected previously visible page.'); + } + if (pageView === previousPageView) { + break; // The correct page is already visible. + } + // Switch visible pages, and reset the viewerContainer scroll position. + this._shadowViewer.appendChild(previousPageView.div); + this.viewer.appendChild(pageView.div); + + this.container.scrollTop = 0; + break; + default: + throw new Error( + '_ensurePageViewVisible: Only one page should be visible at a time.'); + } + this._previousPageNumber = this._currentPageNumber; + } + + _scrollUpdate() { + if (this._updateScrollDown) { + this._updateScrollDown(); + } + super._scrollUpdate(); + } + + _scrollIntoView({ pageDiv, pageSpot = null, pageNumber = null, }) { + if (pageNumber) { // Ensure that `this._currentPageNumber` is correct. + this._setCurrentPageNumber(pageNumber); + } + let scrolledDown = this._currentPageNumber >= this._previousPageNumber; + let previousLocation = this._location; + this._ensurePageViewVisible(); + + scrollIntoView(pageDiv, pageSpot); + + // Since scrolling is tracked using `requestAnimationFrame`, update the + // scroll direction during the next `this._scrollUpdate` invocation. + this._updateScrollDown = () => { + this.scroll.down = scrolledDown; + delete this._updateScrollDown; + }; + // If the scroll position doesn't change as a result of the `scrollIntoView` + // call, ensure that rendering always occurs to avoid showing a blank page. + setTimeout(() => { + if (this._location === previousLocation) { + if (this._updateScrollDown) { + this._updateScrollDown(); + } + this.update(); + } + }, 0); + } + + _getVisiblePages() { + if (!this.pagesCount) { + return { views: [], }; + } + let pageView = this._pages[this._currentPageNumber - 1]; + // NOTE: Compute the `x` and `y` properties of the current view, + // since `this._updateLocation` depends of them being available. + let element = pageView.div; + + let view = { + id: pageView.id, + x: element.offsetLeft + element.clientLeft, + y: element.offsetTop + element.clientTop, + view: pageView, + }; + return { first: view, last: view, views: [view], }; + } + + update() { + let visible = this._getVisiblePages(); + let visiblePages = visible.views, numVisiblePages = visiblePages.length; + + if (numVisiblePages === 0) { + return; + } + this._resizeBuffer(numVisiblePages); + + this.renderingQueue.renderHighestPriority(visible); + + this._updateLocation(visible.first); + this.eventBus.dispatch('updateviewarea', { + source: this, + location: this._location, + }); + } +} + +export { + PDFSinglePageViewer, +}; diff --git a/web/pdf_viewer.component.js b/web/pdf_viewer.component.js index 584aa9ed6..8c9f50ef4 100644 --- a/web/pdf_viewer.component.js +++ b/web/pdf_viewer.component.js @@ -17,6 +17,7 @@ var pdfjsLib = require('./pdfjs.js'); var pdfjsWebPDFViewer = require('./pdf_viewer.js'); +var pdfjsWebPDFSinglePageViewer = require('./pdf_single_page_viewer'); var pdfjsWebPDFPageView = require('./pdf_page_view.js'); var pdfjsWebPDFLinkService = require('./pdf_link_service.js'); var pdfjsWebTextLayerBuilder = require('./text_layer_builder.js'); @@ -30,6 +31,7 @@ var pdfjsWebGenericL10n = require('./genericl10n.js'); var PDFJS = pdfjsLib.PDFJS; PDFJS.PDFViewer = pdfjsWebPDFViewer.PDFViewer; +PDFJS.PDFSinglePageViewer = pdfjsWebPDFSinglePageViewer.PDFSinglePageViewer; PDFJS.PDFPageView = pdfjsWebPDFPageView.PDFPageView; PDFJS.PDFLinkService = pdfjsWebPDFLinkService.PDFLinkService; PDFJS.SimpleLinkService = pdfjsWebPDFLinkService.SimpleLinkService;