From 3d83c646c6b9ff8046a8a1485e170e088ed6fbaf Mon Sep 17 00:00:00 2001 From: Ryan Hendrickson Date: Mon, 14 May 2018 23:10:32 -0400 Subject: [PATCH] Add spread modes to web viewer This builds on the scrolling mode work to add three buttons for joining page spreads together: one for the default view, with no page spreads, and two for spreads starting on odd-numbered or even-numbered pages. --- l10n/en-US/viewer.properties | 7 +++ web/app.js | 5 +++ web/base_viewer.js | 25 +++++++++++ .../secondaryToolbarButton-spreadEven.png | Bin 0 -> 347 bytes .../secondaryToolbarButton-spreadEven@2x.png | Bin 0 -> 694 bytes .../secondaryToolbarButton-spreadNone.png | Bin 0 -> 179 bytes .../secondaryToolbarButton-spreadNone@2x.png | Bin 0 -> 261 bytes .../secondaryToolbarButton-spreadOdd.png | Bin 0 -> 344 bytes .../secondaryToolbarButton-spreadOdd@2x.png | Bin 0 -> 621 bytes web/pdf_viewer.css | 26 +++++++++--- web/pdf_viewer.js | 40 ++++++++++++++++-- web/secondary_toolbar.js | 23 +++++++++- web/ui_utils.js | 26 ++++++------ web/viewer.css | 24 +++++++++++ web/viewer.html | 14 +++++- web/viewer.js | 3 ++ 16 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 web/images/secondaryToolbarButton-spreadEven.png create mode 100644 web/images/secondaryToolbarButton-spreadEven@2x.png create mode 100644 web/images/secondaryToolbarButton-spreadNone.png create mode 100644 web/images/secondaryToolbarButton-spreadNone@2x.png create mode 100644 web/images/secondaryToolbarButton-spreadOdd.png create mode 100644 web/images/secondaryToolbarButton-spreadOdd@2x.png 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 0000000000000000000000000000000000000000..3fa07e703eaa56fa201db3e4ed6cde8bf849d608 GIT binary patch literal 347 zcmV-h0i^zkP)f*c{dlCL1q@VmAOItx{sSlR;WCL2patwCy#$P9 z!#;o~U>_)eWneV7(o#p%hB~JXrl}QwPUoUJqYhQPa_|N$0IR@xL>#N#%t23mO7{06 z;#BSB4j$ACwVCvW`uL4ssS`)8q~5z$bWfY($TBzu~7bp@DAwE>`)HK?lxz&J3i9#$X#I-SmCQ52p0lXSb?vMkG~ te!K)|Hk zJ!n)x6otRLCTJlEs31{61T7RHpkj!XC@2;diikhp52Uab1RFciK#J5BS_mo#f}oLf zMnokcVh}W95>yBX62x7QBud_~$lYZ!dGj`4Vc}jFc4y|C;m(;eb00LK5smoY!>WHI zJpy*+U=(A4VKY0t05s3v>9{;jbuA0|VFKd;K#4@F^O)J@D1osAJ{+s{&1?%mQvk?v zNL6B%qex1rdYA=?G$lv_zztyB`x}81z)_%7CxL0;Ht-mj1e%?H5;&4`EHw!~cI3$f z*@!DJRg^#md;+@6Y)aA&ckcFRaTO?#1piWktnD>>_BPdk-{4Q3*W423|m~tyV d$^SQ3`dEaa-jzTV1)wV!JYD@<);T3K0RT5nKwSU; literal 0 HcmV?d00001 diff --git a/web/images/secondaryToolbarButton-spreadNone@2x.png b/web/images/secondaryToolbarButton-spreadNone@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8e51cf3b7d6ebed644db207d4e99391898ff3952 GIT binary patch literal 261 zcmV+g0s8)lP)PM>6+3`IT=LQA_{vFT; zlDs+RCMl%Ic?WRmE#NVeKp_9*1#s5nJ+5#oSIC?~P!|Cd{-d=AS{%RvDO+ye>HrsR z-|WKH60jo7L;!p2fwet=%G{Tw*wq0ldjpnZXubz1j3TVY@SE&?&K6Brt9iEQ0j?;| z?hZ(6nN|*P>ry&s15--RLWKCuv3(r~AzWjOCrzyPK8?rbz|Y_X&cCXl5!ijiZAtWjM3s91u3Q6yMb5v`(3@v(c=@2 z@EUiUVsjkhV@>c1Yps>TB#)DeewI?Ulf1(jbk_un>O&pm0+;wwN_mrH4^OT;7~wO% z;V<@a&?J~u@iTnId;Dn~qzcxtf!|F#aF%4Ni61RyRG{zs@8j{fZyO$lq3gQt(JH=! q>2&&HHk*yxfXQUC{DoEgzr__9eVLMZ=Mj zyK59t6vlritY{&tt&m2HSSg}GQLwVM5G}OO%0?6mL5Zl9O+>`Mz*?*WwyAOs;RFrC0C#sW)b zcBTeemp|&bluV_|Lix~v_W?i|M=L&NHWfMWE`ty6M*q!hKR_n{lnJ3quuMcG7pXQ( z&5?1!2t)(0NXPmPB~Bl zpMVKTJAli;ZeS;H(hF}0tTm-Tz}-wCsima_OnUx9eZPPg9+kcBP}x)i%JgrUxK9;f z*gXT_wwb+=WWfEXqmpeHJCiezN*)%iNL&>ee{<50-W9uM(GJ~swoAZ&FpJ!8JG=#Rafw{VGs>A zv4Lw547cH8V$52!Kt_BsJ088+tIX1g*R$5e?O0P0vc)!RYULV@ilHcq)o9i$G5-a~ zu{8?hTTf9GD;sK{*Xzyp`~6*;`HbpzyG4ArRrxWE>7UUb)UPDB$k`IH00000NkvXX Hu0mjf-&h!* literal 0 HcmV?d00001 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: {