Some checks failed
		
		
	
	Types tests / Test (lts/*) (push) Has been cancelled
				
			Lint / Lint (lts/*) (push) Has been cancelled
				
			CodeQL / Analyze (javascript) (push) Has been cancelled
				
			CI / Test (20) (push) Has been cancelled
				
			CI / Test (22) (push) Has been cancelled
				
			CI / Test (24) (push) Has been cancelled
				
			
		
			
				
	
	
		
			321 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			321 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* Copyright 2012 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 { BasePDFPageView } from "./base_pdf_page_view.js";
 | 
						|
import { OutputScale } from "pdfjs-lib";
 | 
						|
import { RenderingStates } from "./ui_utils.js";
 | 
						|
 | 
						|
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
 | 
						|
 | 
						|
/**
 | 
						|
 * @implements {IRenderableView}
 | 
						|
 */
 | 
						|
class PDFPageDetailView extends BasePDFPageView {
 | 
						|
  #detailArea = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * @type {boolean} True when the last rendering attempt of the view was
 | 
						|
   *                 cancelled due to a `.reset()` call. This will happen when
 | 
						|
   *                 the visible area changes so much during the rendering that
 | 
						|
   *                 we need to cancel the rendering and start over.
 | 
						|
   */
 | 
						|
  renderingCancelled = false;
 | 
						|
 | 
						|
  constructor({ pageView }) {
 | 
						|
    super(pageView);
 | 
						|
 | 
						|
    this.pageView = pageView;
 | 
						|
    this.renderingId = "detail" + this.id;
 | 
						|
 | 
						|
    this.div = pageView.div;
 | 
						|
  }
 | 
						|
 | 
						|
  setPdfPage(pdfPage) {
 | 
						|
    this.pageView.setPdfPage(pdfPage);
 | 
						|
  }
 | 
						|
 | 
						|
  get pdfPage() {
 | 
						|
    return this.pageView.pdfPage;
 | 
						|
  }
 | 
						|
 | 
						|
  get renderingState() {
 | 
						|
    return super.renderingState;
 | 
						|
  }
 | 
						|
 | 
						|
  set renderingState(value) {
 | 
						|
    this.renderingCancelled = false;
 | 
						|
    super.renderingState = value;
 | 
						|
  }
 | 
						|
 | 
						|
  reset({ keepCanvas = false } = {}) {
 | 
						|
    const renderingCancelled =
 | 
						|
      this.renderingCancelled ||
 | 
						|
      this.renderingState === RenderingStates.RUNNING ||
 | 
						|
      this.renderingState === RenderingStates.PAUSED;
 | 
						|
    this.cancelRendering();
 | 
						|
    this.renderingState = RenderingStates.INITIAL;
 | 
						|
    this.renderingCancelled = renderingCancelled;
 | 
						|
 | 
						|
    if (!keepCanvas) {
 | 
						|
      this._resetCanvas();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #shouldRenderDifferentArea(visibleArea) {
 | 
						|
    if (!this.#detailArea) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    const minDetailX = this.#detailArea.minX;
 | 
						|
    const minDetailY = this.#detailArea.minY;
 | 
						|
    const maxDetailX = this.#detailArea.width + minDetailX;
 | 
						|
    const maxDetailY = this.#detailArea.height + minDetailY;
 | 
						|
 | 
						|
    if (
 | 
						|
      visibleArea.minX < minDetailX ||
 | 
						|
      visibleArea.minY < minDetailY ||
 | 
						|
      visibleArea.maxX > maxDetailX ||
 | 
						|
      visibleArea.maxY > maxDetailY
 | 
						|
    ) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    const {
 | 
						|
      width: maxWidth,
 | 
						|
      height: maxHeight,
 | 
						|
      scale,
 | 
						|
    } = this.pageView.viewport;
 | 
						|
 | 
						|
    if (this.#detailArea.scale !== scale) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    const paddingLeftSize = visibleArea.minX - minDetailX;
 | 
						|
    const paddingRightSize = maxDetailX - visibleArea.maxX;
 | 
						|
    const paddingTopSize = visibleArea.minY - minDetailY;
 | 
						|
    const paddingBottomSize = maxDetailY - visibleArea.maxY;
 | 
						|
 | 
						|
    // If the user is moving in any direction such that the remaining area
 | 
						|
    // rendered outside of the screen is less than MOVEMENT_THRESHOLD of the
 | 
						|
    // padding we render on each side, trigger a re-render. This is so that if
 | 
						|
    // the user then keeps scrolling in that direction, we have a chance of
 | 
						|
    // finishing rendering the new detail before they get past the rendered
 | 
						|
    // area.
 | 
						|
 | 
						|
    const MOVEMENT_THRESHOLD = 0.5;
 | 
						|
    const ratio = (1 + MOVEMENT_THRESHOLD) / MOVEMENT_THRESHOLD;
 | 
						|
 | 
						|
    if (
 | 
						|
      (minDetailX > 0 && paddingRightSize / paddingLeftSize > ratio) ||
 | 
						|
      (maxDetailX < maxWidth && paddingLeftSize / paddingRightSize > ratio) ||
 | 
						|
      (minDetailY > 0 && paddingBottomSize / paddingTopSize > ratio) ||
 | 
						|
      (maxDetailY < maxHeight && paddingTopSize / paddingBottomSize > ratio)
 | 
						|
    ) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  update({ visibleArea = null, underlyingViewUpdated = false } = {}) {
 | 
						|
    if (underlyingViewUpdated) {
 | 
						|
      this.cancelRendering();
 | 
						|
      this.renderingState = RenderingStates.INITIAL;
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!this.#shouldRenderDifferentArea(visibleArea)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    const { viewport, maxCanvasPixels, capCanvasAreaFactor } = this.pageView;
 | 
						|
 | 
						|
    const visibleWidth = visibleArea.maxX - visibleArea.minX;
 | 
						|
    const visibleHeight = visibleArea.maxY - visibleArea.minY;
 | 
						|
 | 
						|
    // "overflowScale" represents which percentage of the width and of the
 | 
						|
    // height the detail area extends outside of the visible area. We want to
 | 
						|
    // draw a larger area so that we don't have to constantly re-draw while
 | 
						|
    // scrolling. The detail area's dimensions thus become
 | 
						|
    // visibleLength * (2 * overflowScale + 1).
 | 
						|
    // We default to adding a whole height/length of detail area on each side,
 | 
						|
    // but we can reduce it to make sure that we stay within the maxCanvasPixels
 | 
						|
    // limit.
 | 
						|
    const visiblePixels =
 | 
						|
      visibleWidth * visibleHeight * OutputScale.pixelRatio ** 2;
 | 
						|
    const maxDetailToVisibleLinearRatio = Math.sqrt(
 | 
						|
      OutputScale.capPixels(maxCanvasPixels, capCanvasAreaFactor) /
 | 
						|
        visiblePixels
 | 
						|
    );
 | 
						|
    const maxOverflowScale = (maxDetailToVisibleLinearRatio - 1) / 2;
 | 
						|
    let overflowScale = Math.min(1, maxOverflowScale);
 | 
						|
    if (overflowScale < 0) {
 | 
						|
      overflowScale = 0;
 | 
						|
      // In this case, we render a detail view that is exactly as big as the
 | 
						|
      // visible area, but we ignore the .maxCanvasPixels limit.
 | 
						|
      // TODO: We should probably instead give up and not render the detail view
 | 
						|
      // in this case. It's quite rare to hit it though, because usually
 | 
						|
      // .maxCanvasPixels will at least have enough pixels to cover the visible
 | 
						|
      // screen.
 | 
						|
    }
 | 
						|
 | 
						|
    const overflowWidth = visibleWidth * overflowScale;
 | 
						|
    const overflowHeight = visibleHeight * overflowScale;
 | 
						|
 | 
						|
    const minX = Math.max(0, visibleArea.minX - overflowWidth);
 | 
						|
    const maxX = Math.min(viewport.width, visibleArea.maxX + overflowWidth);
 | 
						|
    const minY = Math.max(0, visibleArea.minY - overflowHeight);
 | 
						|
    const maxY = Math.min(viewport.height, visibleArea.maxY + overflowHeight);
 | 
						|
    const width = maxX - minX;
 | 
						|
    const height = maxY - minY;
 | 
						|
 | 
						|
    this.#detailArea = { minX, minY, width, height, scale: viewport.scale };
 | 
						|
 | 
						|
    this.reset({ keepCanvas: true });
 | 
						|
  }
 | 
						|
 | 
						|
  _getRenderingContext(canvas, transform) {
 | 
						|
    const baseContext = this.pageView._getRenderingContext(
 | 
						|
      canvas,
 | 
						|
      transform,
 | 
						|
      false
 | 
						|
    );
 | 
						|
    const recordedBBoxes = this.pdfPage.recordedBBoxes;
 | 
						|
 | 
						|
    if (!recordedBBoxes || !this.enableOptimizedPartialRendering) {
 | 
						|
      return baseContext;
 | 
						|
    }
 | 
						|
 | 
						|
    const {
 | 
						|
      viewport: { width: vWidth, height: vHeight },
 | 
						|
    } = this.pageView;
 | 
						|
    const {
 | 
						|
      width: aWidth,
 | 
						|
      height: aHeight,
 | 
						|
      minX: aMinX,
 | 
						|
      minY: aMinY,
 | 
						|
    } = this.#detailArea;
 | 
						|
 | 
						|
    const detailMinX = aMinX / vWidth;
 | 
						|
    const detailMinY = aMinY / vHeight;
 | 
						|
    const detailMaxX = (aMinX + aWidth) / vWidth;
 | 
						|
    const detailMaxY = (aMinY + aHeight) / vHeight;
 | 
						|
 | 
						|
    return {
 | 
						|
      ...baseContext,
 | 
						|
      operationsFilter(index) {
 | 
						|
        if (recordedBBoxes.isEmpty(index)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        return (
 | 
						|
          recordedBBoxes.minX(index) <= detailMaxX &&
 | 
						|
          recordedBBoxes.maxX(index) >= detailMinX &&
 | 
						|
          recordedBBoxes.minY(index) <= detailMaxY &&
 | 
						|
          recordedBBoxes.maxY(index) >= detailMinY
 | 
						|
        );
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  async draw() {
 | 
						|
    // The PDFPageView might have already dropped this PDFPageDetailView. In
 | 
						|
    // that case, simply do nothing.
 | 
						|
    if (this.pageView.detailView !== this) {
 | 
						|
      return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    // If there is already the lower resolution canvas behind,
 | 
						|
    // we don't show the new one until when it's fully ready.
 | 
						|
    const hideUntilComplete =
 | 
						|
      this.pageView.renderingState === RenderingStates.FINISHED ||
 | 
						|
      this.renderingState === RenderingStates.FINISHED;
 | 
						|
 | 
						|
    if (this.renderingState !== RenderingStates.INITIAL) {
 | 
						|
      console.error("Must be in new state before drawing");
 | 
						|
      this.reset(); // Ensure that we reset all state to prevent issues.
 | 
						|
    }
 | 
						|
    const { div, pdfPage, viewport } = this.pageView;
 | 
						|
 | 
						|
    if (!pdfPage) {
 | 
						|
      this.renderingState = RenderingStates.FINISHED;
 | 
						|
      throw new Error("pdfPage is not loaded");
 | 
						|
    }
 | 
						|
 | 
						|
    this.renderingState = RenderingStates.RUNNING;
 | 
						|
 | 
						|
    const canvasWrapper = this.pageView._ensureCanvasWrapper();
 | 
						|
 | 
						|
    const { canvas, prevCanvas } = this._createCanvas(newCanvas => {
 | 
						|
      // If there is already the background canvas, inject this new canvas
 | 
						|
      // after it. We cannot simply use .append because all canvases must
 | 
						|
      // be before the SVG elements used for drawings.
 | 
						|
      if (canvasWrapper.firstElementChild?.tagName === "CANVAS") {
 | 
						|
        canvasWrapper.firstElementChild.after(newCanvas);
 | 
						|
      } else {
 | 
						|
        canvasWrapper.prepend(newCanvas);
 | 
						|
      }
 | 
						|
    }, hideUntilComplete);
 | 
						|
    canvas.ariaHidden = true;
 | 
						|
    if (this.enableOptimizedPartialRendering) {
 | 
						|
      canvas.className = "detailView";
 | 
						|
    }
 | 
						|
 | 
						|
    const { width, height } = viewport;
 | 
						|
 | 
						|
    const area = this.#detailArea;
 | 
						|
 | 
						|
    const { pixelRatio } = OutputScale;
 | 
						|
    const transform = [
 | 
						|
      pixelRatio,
 | 
						|
      0,
 | 
						|
      0,
 | 
						|
      pixelRatio,
 | 
						|
      -area.minX * pixelRatio,
 | 
						|
      -area.minY * pixelRatio,
 | 
						|
    ];
 | 
						|
 | 
						|
    canvas.width = area.width * pixelRatio;
 | 
						|
    canvas.height = area.height * pixelRatio;
 | 
						|
    const { style } = canvas;
 | 
						|
    style.width = `${(area.width * 100) / width}%`;
 | 
						|
    style.height = `${(area.height * 100) / height}%`;
 | 
						|
    style.top = `${(area.minY * 100) / height}%`;
 | 
						|
    style.left = `${(area.minX * 100) / width}%`;
 | 
						|
 | 
						|
    const renderingPromise = this._drawCanvas(
 | 
						|
      this._getRenderingContext(canvas, transform),
 | 
						|
      () => {
 | 
						|
        // If the rendering is cancelled, keep the old canvas visible.
 | 
						|
        this.canvas?.remove();
 | 
						|
        this.canvas = prevCanvas;
 | 
						|
      },
 | 
						|
      () => {
 | 
						|
        this.dispatchPageRendered(
 | 
						|
          /* cssTransform */ false,
 | 
						|
          /* isDetailView */ true
 | 
						|
        );
 | 
						|
      }
 | 
						|
    );
 | 
						|
 | 
						|
    div.setAttribute("data-loaded", true);
 | 
						|
 | 
						|
    this.dispatchPageRender();
 | 
						|
 | 
						|
    return renderingPromise;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export { PDFPageDetailView };
 |