[PDF] Display PDF by PDFViewer
其实,判断页码的过程,蕴含了Canva positioning的线索。
Let's 探寻一下。
一、确定Canvas的定位
Ref: https://github.com/mozilla/pdf.js/blob/master/web/ui_utils.js

[Screen Size: 411 x 731]
<top, left>
canvas的左上角显示的位置。
<viewRight, right>
pdf view的右边在canvas上的相对位置;canvas当前显示的右边界的位置。
<clientHeight, clientWidth>
显示区域的大小。
<element.Height, element.Width>
一页 pdf view的宽高。
ui_utils.js:452 Debug: [ui_utils.js] *** top = 0, scrollEl.clientHeight = 699 ui_utils.js:453 Debug: [ui_utils.js] *** left = 0, scrollEl.clientWidth = 411 ui_utils.js:523 Debug: [ui_utils.js] *** [ 0 ] ui_utils.js:524 Debug: [ui_utils.js] *** element.offsetLeft = 38, element.clientLeft = 9 ui_utils.js:525 Debug: [ui_utils.js] *** element.offsetTop = 1, element.clientTop = 9 ui_utils.js:566 Debug: [ui_utils.js] top = 0, currentHeight = 10, viewBottom = 458, bottom = 699 ui_utils.js:567 Debug: [ui_utils.js] left = 0, currentWidth = 47, viewRight = 364, right = 411 ui_utils.js:575 debug: [ui_utils.js] 100 = (((448 - 0) * (317 - 0) * 100 /448 / 317) ui_utils.js:576 debug: [ui_utils.js] hiddenHeight = 0, hiddenWidth = 0 ui_utils.js:523 Debug: [ui_utils.js] *** [ 1 ] ui_utils.js:524 Debug: [ui_utils.js] *** element.offsetLeft = 38, element.clientLeft = 9 ui_utils.js:525 Debug: [ui_utils.js] *** element.offsetTop = 460, element.clientTop = 9 ui_utils.js:566 Debug: [ui_utils.js] top = 0, currentHeight = 469, viewBottom = 917, bottom = 699 ui_utils.js:567 Debug: [ui_utils.js] left = 0, currentWidth = 47, viewRight = 364, right = 411 ui_utils.js:575 debug: [ui_utils.js] 51 = (((448 - 218) * (317 - 0) * 100 /448 / 317) ui_utils.js:576 debug: [ui_utils.js] hiddenHeight = 218, hiddenWidth = 0
核心代码:
/** * Generic helper to find out what elements are visible within a scroll pane. * * Well, pretty generic. There are some assumptions placed on the elements * referenced by `views`: * - If `horizontal`, no left of any earlier element is to the right of the * left of any later element. * - Otherwise, `views` can be split into contiguous rows where, within a row, * no top of any element is below the bottom of any other element, and * between rows, no bottom of any element in an earlier row is below the * top of any element in a later row. * * (Here, top, left, etc. all refer to the padding edge of the element in * question. For pages, that ends up being equivalent to the bounding box of the * rendering canvas. Earlier and later refer to index in `views`, not page * layout.) * * @param scrollEl {HTMLElement} - a container that can possibly scroll * @param views {Array} - objects with a `div` property that contains an * HTMLElement, which should all be descendents of `scrollEl` satisfying the * above layout assumptions * @param sortByVisibility {boolean} - if true, the returned elements are sorted * in descending order of the percent of their padding box that is visible * @param horizontal {boolean} - if true, the elements are assumed to be laid * out horizontally instead of vertically * @returns {Object} `{ first, last, views: [{ id, x, y, view, percent }] }` */ function getVisibleElements( scrollEl, views, sortByVisibility = false, horizontal = false ) {
const top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight; const left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth; // Throughout this "generic" function, comments will assume we're working with // PDF document pages, which is the most important and complex case. In this // case, the visible elements we're actually interested is the page canvas, // which is contained in a wrapper which adds no padding/border/margin, which // is itself contained in `view.div` which adds no padding (but does add a // border). So, as specified in this function's doc comment, this function // does all of its work on the padding edge of the provided views, starting at // offsetLeft/Top (which includes margin) and adding clientLeft/Top (which is // the border). Adding clientWidth/Height gets us the bottom-right corner of // the padding edge. function isElementBottomAfterViewTop(view) { const element = view.div; const elementBottom = element.offsetTop + element.clientTop + element.clientHeight; return elementBottom > top; }
function isElementRightAfterViewLeft(view) { const element = view.div; const elementRight = element.offsetLeft + element.clientLeft + element.clientWidth; return elementRight > left; } const visible = [], numViews = views.length;
let firstVisibleElementInd = numViews === 0 ? 0 : binarySearchFirstItem( views, horizontal ? isElementRightAfterViewLeft : isElementBottomAfterViewTop ); // Please note the return value of the `binarySearchFirstItem` function when // no valid element is found (hence the `firstVisibleElementInd` check below). if ( firstVisibleElementInd > 0 && firstVisibleElementInd < numViews && !horizontal ) { // 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 ); } // lastEdge acts as a cutoff for us to stop looping, because we know all // subsequent pages will be hidden. // // 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; i < numViews; i++) { const view = views[i], element = view.div;
const currentWidth = element.offsetLeft + element.clientLeft; const currentHeight = element.offsetTop + element.clientTop;
const viewWidth = element.clientWidth, viewHeight = element.clientHeight;
const viewRight = currentWidth + viewWidth; const viewBottom = currentHeight + viewHeight; if (lastEdge === -1) { // As commented above, this is only needed in non-horizontal cases. // Setting lastEdge to the bottom of the first page that is partially // visible ensures that the next page fully below lastEdge is on the // next row, which has to be fully hidden along with all subsequent rows. if (viewBottom >= bottom) { lastEdge = viewBottom; } } else if ((horizontal ? currentWidth : currentHeight) > lastEdge) { break; } if ( viewBottom <= top || currentHeight >= bottom || viewRight <= left || currentWidth >= right ) { continue; } const hiddenHeight = Math.max(0, top - currentHeight) + Math.max(0, viewBottom - bottom); const hiddenWidth = Math.max(0, left - currentWidth) + Math.max(0, viewRight - right);
const percent = (((viewHeight - hiddenHeight) * (viewWidth - hiddenWidth) * 100) / viewHeight / viewWidth) | 0;
visible.push({ id: view.id, x: currentWidth, y: currentHeight, view, percent, }); }
二、Canvas 更新
谁触发了update()?
update() {
const visible = this._getVisiblePages(); // 这里获得"该如何显示" const visiblePages = visible.views, numVisiblePages = visiblePages.length; if (numVisiblePages === 0) { return; } const newCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * numVisiblePages + 1); this._buffer.resize(newCacheSize, visiblePages); this.renderingQueue.renderHighestPriority(visible); this._updateHelper(visiblePages); // Run any class-specific update code. this._updateLocation(visible.first); this.eventBus.dispatch("updateviewarea", { // 这里具体执行新的显示结果 source: this, location: this._location, }); }
进一步地,套在这里。
_scrollUpdate() { if (this.pagesCount === 0) { return; } this.update(); }
看一下 基础类 BaseViewer 的 构建函数。
/** * Simple viewer control to display PDF content/pages. * @implements {IRenderableView} */ 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; this.linkService = options.linkService || new SimpleLinkService(); this.downloadManager = options.downloadManager || null; this.findController = options.findController || null; this.removePageBorders = options.removePageBorders || false; this.textLayerMode = Number.isInteger(options.textLayerMode) ? options.textLayerMode : TextLayerMode.ENABLE; this.imageResourcesPath = options.imageResourcesPath || ""; this.renderInteractiveForms = options.renderInteractiveForms || false; this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; this.renderer = options.renderer || RendererType.CANVAS; this.enableWebGL = options.enableWebGL || false; this.useOnlyCssZoom = options.useOnlyCssZoom || false; this.maxCanvasPixels = options.maxCanvasPixels; this.l10n = options.l10n || NullL10n; this.defaultRenderingQueue = !options.renderingQueue; if (this.defaultRenderingQueue) { // Custom rendering queue is not specified, using default one this.renderingQueue = new PDFRenderingQueue(); this.renderingQueue.setViewer(this); } else { this.renderingQueue = options.renderingQueue; } this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this)); this.presentationModeState = PresentationModeState.UNKNOWN; this._onBeforeDraw = this._onAfterDraw = null; this._resetView(); if (this.removePageBorders) { this.viewer.classList.add("removePageBorders"); } // Defer the dispatching of this event, to give other viewer components // time to initialize *and* register 'baseviewerinit' event listeners. Promise.resolve().then(() => { this.eventBus.dispatch("baseviewerinit", { source: this }); }); }
...
去抖动滚动事件触发的函数。
在Web应用中,实现动画效果的方法比较多,Javascript 中可以通过
-
- 定时器
setTimeout来实现, - css3 可以使用
transition和animation来实现, - html5 中的 canvas 也可以实现。
- 除此之外,html5 还提供一个专门用于请求动画的API,那就是
requestAnimationFrame,顾名思义就是请求动画帧。
- 定时器
/** * Helper function to start monitoring the scroll event and converting them into * PDF.js friendly one: with scroll debounce and scroll direction. */ function watchScroll(viewAreaElement, callback) {
// 去抖动滚动 const debounceScroll = function (evt) { if (rAF) { return; }
// schedule an invocation of scroll for next animation frame. rAF = window.requestAnimationFrame(function viewAreaElementScrolled() { rAF = null; const currentX =viewAreaElement.scrollLeft; const lastX = state.lastX; if (currentX !== lastX) { state.right = currentX > lastX; } state.lastX = currentX; const currentY =viewAreaElement.scrollTop; const lastY = state.lastY; if (currentY !== lastY) { state.down = currentY > lastY; } state.lastY = currentY; callback(state); // <-- update ui }); }; const state = { right: true, down: true, lastX: viewAreaElement.scrollLeft, lastY: viewAreaElement.scrollTop, _eventHandler: debounceScroll, }; let rAF = null; viewAreaElement.addEventListener("scroll", debounceScroll, true); return state; }
去抖动滚动事件从哪里来。可能是ts里的一个标准库里。
PDFViewer 继承 BaseViewer。其中构造函数container变化时,会自动更新界面。
Ref: https://github.com/mozilla/pdf.js/blob/master/web/app.js
const container = appConfig.mainContainer; const viewer = appConfig.viewerContainer; this.pdfViewer = new PDFViewer({ container, viewer, eventBus, renderingQueue: pdfRenderingQueue, linkService: pdfLinkService, downloadManager, findController, renderer: AppOptions.get("renderer"), enableWebGL: AppOptions.get("enableWebGL"), l10n: this.l10n, textLayerMode: AppOptions.get("textLayerMode"), imageResourcesPath: AppOptions.get("imageResourcesPath"), renderInteractiveForms: AppOptions.get("renderInteractiveForms"), enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"), });
三、HTML element 构建
其实终归来自于:https://github.com/mozilla/pdf.js/blob/master/web/viewer.js
function getViewerConfiguration() { return { appContainer: document.body, mainContainer: document.getElementById("viewerContainer"), viewerContainer: document.getElementById("viewer"), eventBus: null, toolbar: { container: document.getElementById("toolbarViewer"), numPages: document.getElementById("numPages"), pageNumber: document.getElementById("pageNumber"), scaleSelectContainer: document.getElementById("scaleSelectContainer"), scaleSelect: document.getElementById("scaleSelect"), customScaleOption: document.getElementById("customScaleOption"), previous: document.getElementById("previous"), next: document.getElementById("next"), zoomIn: document.getElementById("zoomIn"), zoomOut: document.getElementById("zoomOut"), viewFind: document.getElementById("viewFind"), openFile: document.getElementById("openFile"), print: document.getElementById("print"), presentationModeButton: document.getElementById("presentationMode"), download: document.getElementById("download"), viewBookmark: document.getElementById("viewBookmark"), }, secondaryToolbar: { toolbar: document.getElementById("secondaryToolbar"), toggleButton: document.getElementById("secondaryToolbarToggle"), toolbarButtonContainer: document.getElementById( "secondaryToolbarButtonContainer" ), presentationModeButton: document.getElementById( "secondaryPresentationMode" ), openFileButton: document.getElementById("secondaryOpenFile"), printButton: document.getElementById("secondaryPrint"), downloadButton: document.getElementById("secondaryDownload"), viewBookmarkButton: document.getElementById("secondaryViewBookmark"), firstPageButton: document.getElementById("firstPage"), lastPageButton: document.getElementById("lastPage"), pageRotateCwButton: document.getElementById("pageRotateCw"), pageRotateCcwButton: document.getElementById("pageRotateCcw"), cursorSelectToolButton: document.getElementById("cursorSelectTool"), cursorHandToolButton: document.getElementById("cursorHandTool"), 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: { contextFirstPage: document.getElementById("contextFirstPage"), contextLastPage: document.getElementById("contextLastPage"), contextPageRotateCw: document.getElementById("contextPageRotateCw"), contextPageRotateCcw: document.getElementById("contextPageRotateCcw"), }, sidebar: { // Divs (and sidebar button) outerContainer: document.getElementById("outerContainer"), viewerContainer: document.getElementById("viewerContainer"), toggleButton: document.getElementById("sidebarToggle"), // Buttons thumbnailButton: document.getElementById("viewThumbnail"), outlineButton: document.getElementById("viewOutline"), attachmentsButton: document.getElementById("viewAttachments"), // Views thumbnailView: document.getElementById("thumbnailView"), outlineView: document.getElementById("outlineView"), attachmentsView: document.getElementById("attachmentsView"), }, sidebarResizer: { outerContainer: document.getElementById("outerContainer"), resizer: document.getElementById("sidebarResizer"), }, findBar: { bar: document.getElementById("findbar"), toggleButton: document.getElementById("viewFind"), findField: document.getElementById("findInput"), highlightAllCheckbox: document.getElementById("findHighlightAll"), caseSensitiveCheckbox: document.getElementById("findMatchCase"), entireWordCheckbox: document.getElementById("findEntireWord"), findMsg: document.getElementById("findMsg"), findResultsCount: document.getElementById("findResultsCount"), findPreviousButton: document.getElementById("findPrevious"), findNextButton: document.getElementById("findNext"), }, passwordOverlay: { overlayName: "passwordOverlay", container: document.getElementById("passwordOverlay"), label: document.getElementById("passwordText"), input: document.getElementById("password"), submitButton: document.getElementById("passwordSubmit"), cancelButton: document.getElementById("passwordCancel"), }, documentProperties: { overlayName: "documentPropertiesOverlay", container: document.getElementById("documentPropertiesOverlay"), closeButton: document.getElementById("documentPropertiesClose"), fields: { fileName: document.getElementById("fileNameField"), fileSize: document.getElementById("fileSizeField"), title: document.getElementById("titleField"), author: document.getElementById("authorField"), subject: document.getElementById("subjectField"), keywords: document.getElementById("keywordsField"), creationDate: document.getElementById("creationDateField"), modificationDate: document.getElementById("modificationDateField"), creator: document.getElementById("creatorField"), producer: document.getElementById("producerField"), version: document.getElementById("versionField"), pageCount: document.getElementById("pageCountField"), pageSize: document.getElementById("pageSizeField"), linearized: document.getElementById("linearizedField"), }, }, errorWrapper: { container: document.getElementById("errorWrapper"), errorMessage: document.getElementById("errorMessage"), closeButton: document.getElementById("errorClose"), errorMoreInfo: document.getElementById("errorMoreInfo"), moreInfoButton: document.getElementById("errorShowMore"), lessInfoButton: document.getElementById("errorShowLess"), }, printContainer: document.getElementById("printContainer"), openFileInputName: "fileInput", debuggerScriptPath: "./debugger.js", }; }
在这里填充 html 元素,完成显示。
<div id="viewerContainer" tabindex="0"> <div id="viewer" class="pdfViewer"></div> </div>
End.

浙公网安备 33010602011771号