[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 });
    });
  }

...

 

去抖动滚动事件触发的函数。

Ref: requestAnimationFrame用法

在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",
  };
}
View Code

在这里填充 html 元素,完成显示。

        <div id="viewerContainer" tabindex="0">
          <div id="viewer" class="pdfViewer"></div>
        </div>

 

End.

posted @ 2020-07-02 09:11  郝壹贰叁  阅读(671)  评论(0)    收藏  举报