123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- /* 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.
- */
- /** @typedef {import("./interfaces").IL10n} IL10n */
- /** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
- /** @typedef {import("./interfaces").IRenderableView} IRenderableView */
- // eslint-disable-next-line max-len
- /** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
- import { OutputScale, RenderingStates } from "./ui_utils.js";
- import { RenderingCancelledException } from "pdfjs-lib";
- const DRAW_UPSCALE_FACTOR = 2; // See comment in `PDFThumbnailView.draw` below.
- const MAX_NUM_SCALING_STEPS = 3;
- const THUMBNAIL_CANVAS_BORDER_WIDTH = 1; // px
- const THUMBNAIL_WIDTH = 98; // px
- /**
- * @typedef {Object} PDFThumbnailViewOptions
- * @property {HTMLDivElement} container - The viewer element.
- * @property {number} id - The thumbnail's unique ID (normally its number).
- * @property {PageViewport} defaultViewport - The page viewport.
- * @property {Promise<OptionalContentConfig>} [optionalContentConfigPromise] -
- * A promise that is resolved with an {@link OptionalContentConfig} instance.
- * The default value is `null`.
- * @property {IPDFLinkService} linkService - The navigation/linking service.
- * @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
- * @property {IL10n} l10n - Localization service.
- * @property {Object} [pageColors] - Overwrites background and foreground colors
- * with user defined ones in order to improve readability in high contrast
- * mode.
- */
- class TempImageFactory {
- static #tempCanvas = null;
- static getCanvas(width, height) {
- const tempCanvas = (this.#tempCanvas ||= document.createElement("canvas"));
- tempCanvas.width = width;
- tempCanvas.height = height;
- // Since this is a temporary canvas, we need to fill it with a white
- // background ourselves. `_getPageDrawContext` uses CSS rules for this.
- const ctx = tempCanvas.getContext("2d", { alpha: false });
- ctx.save();
- ctx.fillStyle = "rgb(255, 255, 255)";
- ctx.fillRect(0, 0, width, height);
- ctx.restore();
- return [tempCanvas, tempCanvas.getContext("2d")];
- }
- static destroyCanvas() {
- const tempCanvas = this.#tempCanvas;
- if (tempCanvas) {
- // Zeroing the width and height causes Firefox to release graphics
- // resources immediately, which can greatly reduce memory consumption.
- tempCanvas.width = 0;
- tempCanvas.height = 0;
- }
- this.#tempCanvas = null;
- }
- }
- /**
- * @implements {IRenderableView}
- */
- class PDFThumbnailView {
- /**
- * @param {PDFThumbnailViewOptions} options
- */
- constructor({
- container,
- id,
- defaultViewport,
- optionalContentConfigPromise,
- linkService,
- renderingQueue,
- l10n,
- pageColors,
- }) {
- this.id = id;
- this.renderingId = "thumbnail" + id;
- this.pageLabel = null;
- this.pdfPage = null;
- this.rotation = 0;
- this.viewport = defaultViewport;
- this.pdfPageRotate = defaultViewport.rotation;
- this._optionalContentConfigPromise = optionalContentConfigPromise || null;
- this.pageColors = pageColors || null;
- this.linkService = linkService;
- this.renderingQueue = renderingQueue;
- this.renderTask = null;
- this.renderingState = RenderingStates.INITIAL;
- this.resume = null;
- const pageWidth = this.viewport.width,
- pageHeight = this.viewport.height,
- pageRatio = pageWidth / pageHeight;
- this.canvasWidth = THUMBNAIL_WIDTH;
- this.canvasHeight = (this.canvasWidth / pageRatio) | 0;
- this.scale = this.canvasWidth / pageWidth;
- this.l10n = l10n;
- const anchor = document.createElement("a");
- anchor.href = linkService.getAnchorUrl("#page=" + id);
- this._thumbPageTitle.then(msg => {
- anchor.title = msg;
- });
- anchor.onclick = function () {
- linkService.goToPage(id);
- return false;
- };
- this.anchor = anchor;
- const div = document.createElement("div");
- div.className = "thumbnail";
- div.setAttribute("data-page-number", this.id);
- this.div = div;
- const ring = document.createElement("div");
- ring.className = "thumbnailSelectionRing";
- const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH;
- ring.style.width = this.canvasWidth + borderAdjustment + "px";
- ring.style.height = this.canvasHeight + borderAdjustment + "px";
- this.ring = ring;
- div.append(ring);
- anchor.append(div);
- container.append(anchor);
- }
- setPdfPage(pdfPage) {
- this.pdfPage = pdfPage;
- this.pdfPageRotate = pdfPage.rotate;
- const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
- this.viewport = pdfPage.getViewport({ scale: 1, rotation: totalRotation });
- this.reset();
- }
- reset() {
- this.cancelRendering();
- this.renderingState = RenderingStates.INITIAL;
- const pageWidth = this.viewport.width,
- pageHeight = this.viewport.height,
- pageRatio = pageWidth / pageHeight;
- this.canvasHeight = (this.canvasWidth / pageRatio) | 0;
- this.scale = this.canvasWidth / pageWidth;
- this.div.removeAttribute("data-loaded");
- const ring = this.ring;
- ring.textContent = ""; // Remove the thumbnail from the DOM.
- const borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH;
- ring.style.width = this.canvasWidth + borderAdjustment + "px";
- ring.style.height = this.canvasHeight + borderAdjustment + "px";
- if (this.canvas) {
- // Zeroing the width and height causes Firefox to release graphics
- // resources immediately, which can greatly reduce memory consumption.
- this.canvas.width = 0;
- this.canvas.height = 0;
- delete this.canvas;
- }
- if (this.image) {
- this.image.removeAttribute("src");
- delete this.image;
- }
- }
- update({ rotation = null }) {
- if (typeof rotation === "number") {
- this.rotation = rotation; // The rotation may be zero.
- }
- const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
- this.viewport = this.viewport.clone({
- scale: 1,
- rotation: totalRotation,
- });
- this.reset();
- }
- /**
- * PLEASE NOTE: Most likely you want to use the `this.reset()` method,
- * rather than calling this one directly.
- */
- cancelRendering() {
- if (this.renderTask) {
- this.renderTask.cancel();
- this.renderTask = null;
- }
- this.resume = null;
- }
- /**
- * @private
- */
- _getPageDrawContext(upscaleFactor = 1) {
- // Keep the no-thumbnail outline visible, i.e. `data-loaded === false`,
- // until rendering/image conversion is complete, to avoid display issues.
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d", { alpha: false });
- const outputScale = new OutputScale();
- canvas.width = (upscaleFactor * this.canvasWidth * outputScale.sx) | 0;
- canvas.height = (upscaleFactor * this.canvasHeight * outputScale.sy) | 0;
- const transform = outputScale.scaled
- ? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
- : null;
- return { ctx, canvas, transform };
- }
- /**
- * @private
- */
- _convertCanvasToImage(canvas) {
- if (this.renderingState !== RenderingStates.FINISHED) {
- throw new Error("_convertCanvasToImage: Rendering has not finished.");
- }
- const reducedCanvas = this._reduceImage(canvas);
- const image = document.createElement("img");
- image.className = "thumbnailImage";
- this._thumbPageCanvas.then(msg => {
- image.setAttribute("aria-label", msg);
- });
- image.style.width = this.canvasWidth + "px";
- image.style.height = this.canvasHeight + "px";
- image.src = reducedCanvas.toDataURL();
- this.image = image;
- this.div.setAttribute("data-loaded", true);
- this.ring.append(image);
- // Zeroing the width and height causes Firefox to release graphics
- // resources immediately, which can greatly reduce memory consumption.
- reducedCanvas.width = 0;
- reducedCanvas.height = 0;
- }
- draw() {
- if (this.renderingState !== RenderingStates.INITIAL) {
- console.error("Must be in new state before drawing");
- return Promise.resolve();
- }
- const { pdfPage } = this;
- if (!pdfPage) {
- this.renderingState = RenderingStates.FINISHED;
- return Promise.reject(new Error("pdfPage is not loaded"));
- }
- this.renderingState = RenderingStates.RUNNING;
- const finishRenderTask = async (error = null) => {
- // The renderTask may have been replaced by a new one, so only remove
- // the reference to the renderTask if it matches the one that is
- // triggering this callback.
- if (renderTask === this.renderTask) {
- this.renderTask = null;
- }
- if (error instanceof RenderingCancelledException) {
- return;
- }
- this.renderingState = RenderingStates.FINISHED;
- this._convertCanvasToImage(canvas);
- if (error) {
- throw error;
- }
- };
- // Render the thumbnail at a larger size and downsize the canvas (similar
- // to `setImage`), to improve consistency between thumbnails created by
- // the `draw` and `setImage` methods (fixes issue 8233).
- // NOTE: To primarily avoid increasing memory usage too much, but also to
- // reduce downsizing overhead, we purposely limit the up-scaling factor.
- const { ctx, canvas, transform } =
- this._getPageDrawContext(DRAW_UPSCALE_FACTOR);
- const drawViewport = this.viewport.clone({
- scale: DRAW_UPSCALE_FACTOR * this.scale,
- });
- const renderContinueCallback = cont => {
- if (!this.renderingQueue.isHighestPriority(this)) {
- this.renderingState = RenderingStates.PAUSED;
- this.resume = () => {
- this.renderingState = RenderingStates.RUNNING;
- cont();
- };
- return;
- }
- cont();
- };
- const renderContext = {
- canvasContext: ctx,
- transform,
- viewport: drawViewport,
- optionalContentConfigPromise: this._optionalContentConfigPromise,
- pageColors: this.pageColors,
- };
- const renderTask = (this.renderTask = pdfPage.render(renderContext));
- renderTask.onContinue = renderContinueCallback;
- const resultPromise = renderTask.promise.then(
- function () {
- return finishRenderTask(null);
- },
- function (error) {
- return finishRenderTask(error);
- }
- );
- resultPromise.finally(() => {
- // Zeroing the width and height causes Firefox to release graphics
- // resources immediately, which can greatly reduce memory consumption.
- canvas.width = 0;
- canvas.height = 0;
- // Only trigger cleanup, once rendering has finished, when the current
- // pageView is *not* cached on the `BaseViewer`-instance.
- const pageCached = this.linkService.isPageCached(this.id);
- if (!pageCached) {
- this.pdfPage?.cleanup();
- }
- });
- return resultPromise;
- }
- setImage(pageView) {
- if (this.renderingState !== RenderingStates.INITIAL) {
- return;
- }
- const { thumbnailCanvas: canvas, pdfPage, scale } = pageView;
- if (!canvas) {
- return;
- }
- if (!this.pdfPage) {
- this.setPdfPage(pdfPage);
- }
- if (scale < this.scale) {
- // Avoid upscaling the image, since that makes the thumbnail look blurry.
- return;
- }
- this.renderingState = RenderingStates.FINISHED;
- this._convertCanvasToImage(canvas);
- }
- /**
- * @private
- */
- _reduceImage(img) {
- const { ctx, canvas } = this._getPageDrawContext();
- if (img.width <= 2 * canvas.width) {
- ctx.drawImage(
- img,
- 0,
- 0,
- img.width,
- img.height,
- 0,
- 0,
- canvas.width,
- canvas.height
- );
- return canvas;
- }
- // drawImage does an awful job of rescaling the image, doing it gradually.
- let reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS;
- let reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS;
- const [reducedImage, reducedImageCtx] = TempImageFactory.getCanvas(
- reducedWidth,
- reducedHeight
- );
- while (reducedWidth > img.width || reducedHeight > img.height) {
- reducedWidth >>= 1;
- reducedHeight >>= 1;
- }
- reducedImageCtx.drawImage(
- img,
- 0,
- 0,
- img.width,
- img.height,
- 0,
- 0,
- reducedWidth,
- reducedHeight
- );
- while (reducedWidth > 2 * canvas.width) {
- reducedImageCtx.drawImage(
- reducedImage,
- 0,
- 0,
- reducedWidth,
- reducedHeight,
- 0,
- 0,
- reducedWidth >> 1,
- reducedHeight >> 1
- );
- reducedWidth >>= 1;
- reducedHeight >>= 1;
- }
- ctx.drawImage(
- reducedImage,
- 0,
- 0,
- reducedWidth,
- reducedHeight,
- 0,
- 0,
- canvas.width,
- canvas.height
- );
- return canvas;
- }
- get _thumbPageTitle() {
- return this.l10n.get("thumb_page_title", {
- page: this.pageLabel ?? this.id,
- });
- }
- get _thumbPageCanvas() {
- return this.l10n.get("thumb_page_canvas", {
- page: this.pageLabel ?? this.id,
- });
- }
- /**
- * @param {string|null} label
- */
- setPageLabel(label) {
- this.pageLabel = typeof label === "string" ? label : null;
- this._thumbPageTitle.then(msg => {
- this.anchor.title = msg;
- });
- if (this.renderingState !== RenderingStates.FINISHED) {
- return;
- }
- this._thumbPageCanvas.then(msg => {
- this.image?.setAttribute("aria-label", msg);
- });
- }
- }
- export { PDFThumbnailView, TempImageFactory };
|