diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index dc3bcf06e..af1765fc1 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -72,6 +72,13 @@ scroll_horizontal_label=Horizontal Scrolling scroll_wrapped.title=Use Wrapped Scrolling scroll_wrapped_label=Wrapped Scrolling +spread_none.title=Do not join page spreads +spread_none_label=No Spreads +spread_odd.title=Join page spreads starting with odd-numbered pages +spread_odd_label=Odd Spreads +spread_even.title=Join page spreads starting with even-numbered pages +spread_even_label=Even Spreads + # Document properties dialog box document_properties.title=Document Properties… document_properties_label=Document Properties… diff --git a/web/app.js b/web/app.js index 705c1a523..5891d1de7 100644 --- a/web/app.js +++ b/web/app.js @@ -1386,6 +1386,7 @@ let PDFViewerApplication = { eventBus.on('rotatecw', webViewerRotateCw); eventBus.on('rotateccw', webViewerRotateCcw); eventBus.on('switchscrollmode', webViewerSwitchScrollMode); + eventBus.on('switchspreadmode', webViewerSwitchSpreadMode); eventBus.on('documentproperties', webViewerDocumentProperties); eventBus.on('find', webViewerFind); eventBus.on('findfromurlhash', webViewerFindFromUrlHash); @@ -1453,6 +1454,7 @@ let PDFViewerApplication = { eventBus.off('rotatecw', webViewerRotateCw); eventBus.off('rotateccw', webViewerRotateCcw); eventBus.off('switchscrollmode', webViewerSwitchScrollMode); + eventBus.off('switchspreadmode', webViewerSwitchSpreadMode); eventBus.off('documentproperties', webViewerDocumentProperties); eventBus.off('find', webViewerFind); eventBus.off('findfromurlhash', webViewerFindFromUrlHash); @@ -1965,6 +1967,9 @@ function webViewerRotateCcw() { function webViewerSwitchScrollMode(evt) { PDFViewerApplication.pdfViewer.setScrollMode(evt.mode); } +function webViewerSwitchSpreadMode(evt) { + PDFViewerApplication.pdfViewer.setSpreadMode(evt.mode); +} function webViewerDocumentProperties() { PDFViewerApplication.pdfDocumentProperties.open(); } diff --git a/web/base_viewer.js b/web/base_viewer.js index 94de1779c..1c8d5bce4 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -35,6 +35,12 @@ const ScrollMode = { WRAPPED: 2, }; +const SpreadMode = { + NONE: 0, // The default value. + ODD: 1, + EVEN: 2, +}; + /** * @typedef {Object} PDFViewerOptions * @property {HTMLDivElement} container - The container for the viewer element. @@ -71,6 +77,10 @@ const ScrollMode = { * document pages should be laid out within the scrolling container. The * constants from {ScrollMode} should be used. The default value is * `ScrollMode.VERTICAL`. + * @property {number} spreadMode - (optional) If not `SpreadMode.NONE`, groups + * pages into spreads, starting with odd- or even-numbered pages. The + * constants from {SpreadMode} should be used. The default value is + * `SpreadMode.NONE`. */ function PDFPageViewBuffer(size) { @@ -153,6 +163,7 @@ class BaseViewer { this.maxCanvasPixels = options.maxCanvasPixels; this.l10n = options.l10n || NullL10n; this.scrollMode = options.scrollMode || ScrollMode.VERTICAL; + this.spreadMode = options.spreadMode || SpreadMode.NONE; this.defaultRenderingQueue = !options.renderingQueue; if (this.defaultRenderingQueue) { @@ -428,6 +439,9 @@ class BaseViewer { bindOnAfterAndBeforeDraw(pageView); this._pages.push(pageView); } + if (this.spreadMode !== SpreadMode.NONE) { + this._regroupSpreads(); + } // Fetch all the pages since the viewport is needed before printing // starts to create the correct size canvas. Wait until one page is @@ -1020,9 +1034,20 @@ class BaseViewer { classList.toggle('scrollHorizontal', mode === ScrollMode.HORIZONTAL); classList.toggle('scrollWrapped', mode === ScrollMode.WRAPPED); } + + setSpreadMode(mode) { + if (mode !== this.spreadMode) { + this.spreadMode = mode; + this.eventBus.dispatch('spreadmodechanged', { mode, }); + this._regroupSpreads(); + } + } + + _regroupSpreads() {} } export { BaseViewer, ScrollMode, + SpreadMode, }; diff --git a/web/images/secondaryToolbarButton-spreadEven.png b/web/images/secondaryToolbarButton-spreadEven.png new file mode 100644 index 000000000..3fa07e703 Binary files /dev/null and b/web/images/secondaryToolbarButton-spreadEven.png differ diff --git a/web/images/secondaryToolbarButton-spreadEven@2x.png b/web/images/secondaryToolbarButton-spreadEven@2x.png new file mode 100644 index 000000000..32e5033d7 Binary files /dev/null and b/web/images/secondaryToolbarButton-spreadEven@2x.png differ diff --git a/web/images/secondaryToolbarButton-spreadNone.png b/web/images/secondaryToolbarButton-spreadNone.png new file mode 100644 index 000000000..161147354 Binary files /dev/null and b/web/images/secondaryToolbarButton-spreadNone.png differ diff --git a/web/images/secondaryToolbarButton-spreadNone@2x.png b/web/images/secondaryToolbarButton-spreadNone@2x.png new file mode 100644 index 000000000..8e51cf3b7 Binary files /dev/null and b/web/images/secondaryToolbarButton-spreadNone@2x.png differ diff --git a/web/images/secondaryToolbarButton-spreadOdd.png b/web/images/secondaryToolbarButton-spreadOdd.png new file mode 100644 index 000000000..5126313a1 Binary files /dev/null and b/web/images/secondaryToolbarButton-spreadOdd.png differ diff --git a/web/images/secondaryToolbarButton-spreadOdd@2x.png b/web/images/secondaryToolbarButton-spreadOdd@2x.png new file mode 100644 index 000000000..5996b74db Binary files /dev/null and b/web/images/secondaryToolbarButton-spreadOdd@2x.png differ diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index a0adbde3f..28f9f79a7 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -46,29 +46,40 @@ border: none; } -.pdfViewer.scrollHorizontal, .pdfViewer.scrollWrapped { +.pdfViewer.scrollHorizontal, .pdfViewer.scrollWrapped, .spread { margin-left: 3.5px; margin-right: 3.5px; text-align: center; } -.pdfViewer.scrollHorizontal { +.pdfViewer.scrollHorizontal, .spread { white-space: nowrap; } -.pdfViewer.removePageBorders { +.pdfViewer.removePageBorders, +.pdfViewer.scrollHorizontal .spread, +.pdfViewer.scrollWrapped .spread { margin-left: 0; margin-right: 0; } +.spread .page, .pdfViewer.scrollHorizontal .page, -.pdfViewer.scrollWrapped .page { +.pdfViewer.scrollWrapped .page, +.pdfViewer.scrollHorizontal .spread, +.pdfViewer.scrollWrapped .spread { display: inline-block; - margin-left: -3.5px; - margin-right: -3.5px; vertical-align: middle; } +.spread .page, +.pdfViewer.scrollHorizontal .page, +.pdfViewer.scrollWrapped .page { + margin-left: -3.5px; + margin-right: -3.5px; +} + +.pdfViewer.removePageBorders .spread .page, .pdfViewer.removePageBorders.scrollHorizontal .page, .pdfViewer.removePageBorders.scrollWrapped .page { margin-left: 5px; @@ -99,7 +110,8 @@ margin-right: 0; } -.pdfPresentationMode .pdfViewer .page { +.pdfPresentationMode .pdfViewer .page, +.pdfPresentationMode .pdfViewer .spread { display: block; } diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index e6e520f5f..2a6c46ca8 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { BaseViewer, ScrollMode } from './base_viewer'; +import { BaseViewer, ScrollMode, SpreadMode } from './base_viewer'; import { getVisibleElements, scrollIntoView } from './ui_utils'; import { shadow } from 'pdfjs-lib'; @@ -23,8 +23,14 @@ class PDFViewer extends BaseViewer { } _scrollIntoView({ pageDiv, pageSpot = null, }) { - if (!pageSpot && this.scrollMode === ScrollMode.HORIZONTAL) { - pageSpot = { left: 0, top: 0, }; + if (!pageSpot) { + const left = pageDiv.offsetLeft + pageDiv.clientLeft; + const right = left + pageDiv.clientWidth; + const { scrollLeft, clientWidth, } = this.container; + if (this.scrollMode === ScrollMode.HORIZONTAL || + left < scrollLeft || right > scrollLeft + clientWidth) { + pageSpot = { left: 0, top: 0, }; + } } scrollIntoView(pageDiv, pageSpot); } @@ -80,6 +86,34 @@ class PDFViewer extends BaseViewer { location: this._location, }); } + + _regroupSpreads() { + const container = this._setDocumentViewerElement, pages = this._pages; + while (container.firstChild) { + container.firstChild.remove(); + } + if (this.spreadMode === SpreadMode.NONE) { + for (let i = 0, iMax = pages.length; i < iMax; ++i) { + container.appendChild(pages[i].div); + } + } else { + const parity = this.spreadMode - 1; + let spread = null; + for (let i = 0, iMax = pages.length; i < iMax; ++i) { + if (spread === null) { + spread = document.createElement('div'); + spread.className = 'spread'; + container.appendChild(spread); + } else if (i % 2 === parity) { + spread = spread.cloneNode(false); + container.appendChild(spread); + } + spread.appendChild(pages[i].div); + } + } + this.scrollPageIntoView({ pageNumber: this._currentPageNumber, }); + this.update(); + } } export { diff --git a/web/secondary_toolbar.js b/web/secondary_toolbar.js index a332f668b..a44be0642 100644 --- a/web/secondary_toolbar.js +++ b/web/secondary_toolbar.js @@ -13,9 +13,9 @@ * limitations under the License. */ +import { ScrollMode, SpreadMode } from './base_viewer'; import { CursorTool } from './pdf_cursor_tools'; import { SCROLLBAR_PADDING } from './ui_utils'; -import { ScrollMode } from './base_viewer'; /** * @typedef {Object} SecondaryToolbarOptions @@ -83,6 +83,12 @@ class SecondaryToolbar { eventDetails: { mode: ScrollMode.HORIZONTAL, }, close: true, }, { element: options.scrollWrappedButton, eventName: 'switchscrollmode', eventDetails: { mode: ScrollMode.WRAPPED, }, close: true, }, + { element: options.spreadNoneButton, eventName: 'switchspreadmode', + eventDetails: { mode: SpreadMode.NONE, }, close: true, }, + { element: options.spreadOddButton, eventName: 'switchspreadmode', + eventDetails: { mode: SpreadMode.ODD, }, close: true, }, + { element: options.spreadEvenButton, eventName: 'switchspreadmode', + eventDetails: { mode: SpreadMode.EVEN, }, close: true, }, { element: options.documentPropertiesButton, eventName: 'documentproperties', close: true, }, ]; @@ -102,10 +108,12 @@ class SecondaryToolbar { this.reset(); - // Bind the event listeners for click, cursor tool, and scroll mode actions. + // Bind the event listeners for click, cursor tool, and scroll/spread mode + // actions. this._bindClickListeners(); this._bindCursorToolsListener(options); this._bindScrollModeListener(options); + this._bindSpreadModeListener(options); // Bind the event listener for adjusting the 'max-height' of the toolbar. this.eventBus.on('resize', this._setMaxHeight.bind(this)); @@ -191,6 +199,17 @@ class SecondaryToolbar { }); } + _bindSpreadModeListener(buttons) { + this.eventBus.on('spreadmodechanged', function(evt) { + buttons.spreadNoneButton.classList.toggle('toggled', + evt.mode === SpreadMode.NONE); + buttons.spreadOddButton.classList.toggle('toggled', + evt.mode === SpreadMode.ODD); + buttons.spreadEvenButton.classList.toggle('toggled', + evt.mode === SpreadMode.EVEN); + }); + } + open() { if (this.opened) { return; diff --git a/web/ui_utils.js b/web/ui_utils.js index b38489341..455bec351 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -316,9 +316,10 @@ function getPageSizeInches({ view, userUnit, rotate, }) { */ function backtrackBeforeAllVisibleElements(index, views, top) { // binarySearchFirstItem's assumption is that the input is ordered, with only - // one index where the conditions flips from false to true: - // [false ..., true...]. With wrapped scrolling, it is possible to have - // [false ..., true, false, true ...]. + // one index where the conditions flips from false to true: [false ..., + // true...]. With vertical scrolling and spreads, it is possible to have + // [false ..., true, false, true ...]. With wrapped scrolling we can have a + // similar sequence, with many more mixed true and false in the middle. // // So there is no guarantee that the binary search yields the index of the // first visible element. It could have been any of the other visible elements @@ -451,10 +452,11 @@ function getVisibleElements(scrollEl, views, sortByVisibility = false, isElementBottomAfterViewTop); if (views.length > 0 && !horizontal) { - // In wrapped scrolling, with some page sizes, isElementBottomAfterViewTop - // doesn't satisfy the binary search condition: there can be pages with - // bottoms above the view top between pages with bottoms below. This - // function detects and corrects that error; see it for more comments. + // In wrapped scrolling (or vertical scrolling with spreads), with some page + // sizes, isElementBottomAfterViewTop doesn't satisfy the binary search + // condition: there can be pages with bottoms above the view top between + // pages with bottoms below. This function detects and corrects that error; + // see it for more comments. firstVisibleElementInd = backtrackBeforeAllVisibleElements(firstVisibleElementInd, views, top); } @@ -462,11 +464,11 @@ function getVisibleElements(scrollEl, views, sortByVisibility = false, // lastEdge acts as a cutoff for us to stop looping, because we know all // subsequent pages will be hidden. // - // When using wrapped scrolling, we can't simply stop the first time we reach - // a page below the bottom of the view; the tops of subsequent pages on the - // same row could still be visible. In horizontal scrolling, we don't have - // that issue, so we can stop as soon as we pass `right`, without needing the - // code below that handles the -1 case. + // When using wrapped scrolling or vertical scrolling with spreads, we can't + // simply stop the first time we reach a page below the bottom of the view; + // the tops of subsequent pages on the same row could still be visible. In + // horizontal scrolling, we don't have that issue, so we can stop as soon as + // we pass `right`, without needing the code below that handles the -1 case. let lastEdge = horizontal ? right : -1; for (let i = firstVisibleElementInd, ii = views.length; i < ii; i++) { diff --git a/web/viewer.css b/web/viewer.css index 838ba81a6..9c3a3cada 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -978,6 +978,18 @@ html[dir="rtl"] .secondaryToolbarButton > span { content: url(images/secondaryToolbarButton-scrollWrapped.png); } +.secondaryToolbarButton.spreadNone::before { + content: url(images/secondaryToolbarButton-spreadNone.png); +} + +.secondaryToolbarButton.spreadOdd::before { + content: url(images/secondaryToolbarButton-spreadOdd.png); +} + +.secondaryToolbarButton.spreadEven::before { + content: url(images/secondaryToolbarButton-spreadEven.png); +} + .secondaryToolbarButton.documentProperties::before { content: url(images/secondaryToolbarButton-documentProperties.png); } @@ -1713,6 +1725,18 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { content: url(images/secondaryToolbarButton-scrollWrapped@2x.png); } + .secondaryToolbarButton.spreadNone::before { + content: url(images/secondaryToolbarButton-spreadNone@2x.png); + } + + .secondaryToolbarButton.spreadOdd::before { + content: url(images/secondaryToolbarButton-spreadOdd@2x.png); + } + + .secondaryToolbarButton.spreadEven::before { + content: url(images/secondaryToolbarButton-spreadEven@2x.png); + } + .secondaryToolbarButton.documentProperties::before { content: url(images/secondaryToolbarButton-documentProperties@2x.png); } diff --git a/web/viewer.html b/web/viewer.html index e92592e1a..ba0ac8e49 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -180,7 +180,19 @@ See https://github.com/adobe-type-tools/cmap-resources
- + + + +
+ + diff --git a/web/viewer.js b/web/viewer.js index 8206b9008..42088f314 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -99,6 +99,9 @@ function getViewerConfiguration() { scrollVerticalButton: document.getElementById('scrollVertical'), scrollHorizontalButton: document.getElementById('scrollHorizontal'), scrollWrappedButton: document.getElementById('scrollWrapped'), + spreadNoneButton: document.getElementById('spreadNone'), + spreadOddButton: document.getElementById('spreadOdd'), + spreadEvenButton: document.getElementById('spreadEven'), documentPropertiesButton: document.getElementById('documentProperties'), }, fullscreen: {