f84f2646f4
This fixes invalid type references (either due to invalid paths for the import or missing imports) in the JS doc, as well as some missing or invalid parameter names for @param annotations.
459 lines
13 KiB
JavaScript
459 lines
13 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.
|
|
*/
|
|
|
|
// eslint-disable-next-line max-len
|
|
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
|
|
// eslint-disable-next-line max-len
|
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
|
/** @typedef {import("./event_utils").EventBus} EventBus */
|
|
/** @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_WIDTH = 98; // px
|
|
|
|
/**
|
|
* @typedef {Object} PDFThumbnailViewOptions
|
|
* @property {HTMLDivElement} container - The viewer element.
|
|
* @property {EventBus} eventBus - The application event bus.
|
|
* @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,
|
|
eventBus,
|
|
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.eventBus = eventBus;
|
|
this.linkService = linkService;
|
|
this.renderingQueue = renderingQueue;
|
|
|
|
this.renderTask = null;
|
|
this.renderingState = RenderingStates.INITIAL;
|
|
this.resume = null;
|
|
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;
|
|
this.#updateDims();
|
|
|
|
const img = document.createElement("div");
|
|
img.className = "thumbnailImage";
|
|
this._placeholderImg = img;
|
|
|
|
div.append(img);
|
|
anchor.append(div);
|
|
container.append(anchor);
|
|
}
|
|
|
|
#updateDims() {
|
|
const { width, height } = this.viewport;
|
|
const ratio = width / height;
|
|
|
|
this.canvasWidth = THUMBNAIL_WIDTH;
|
|
this.canvasHeight = (this.canvasWidth / ratio) | 0;
|
|
this.scale = this.canvasWidth / width;
|
|
|
|
const { style } = this.div;
|
|
style.setProperty("--thumbnail-width", `${this.canvasWidth}px`);
|
|
style.setProperty("--thumbnail-height", `${this.canvasHeight}px`);
|
|
}
|
|
|
|
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;
|
|
|
|
this.div.removeAttribute("data-loaded");
|
|
this.image?.replaceWith(this._placeholderImg);
|
|
this.#updateDims();
|
|
|
|
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.src = reducedCanvas.toDataURL();
|
|
this.image = image;
|
|
|
|
this.div.setAttribute("data-loaded", true);
|
|
this._placeholderImg.replaceWith(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;
|
|
}
|
|
|
|
async #finishRenderTask(renderTask, canvas, 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;
|
|
}
|
|
}
|
|
|
|
async draw() {
|
|
if (this.renderingState !== RenderingStates.INITIAL) {
|
|
console.error("Must be in new state before drawing");
|
|
return undefined;
|
|
}
|
|
const { pdfPage } = this;
|
|
|
|
if (!pdfPage) {
|
|
this.renderingState = RenderingStates.FINISHED;
|
|
throw new Error("pdfPage is not loaded");
|
|
}
|
|
|
|
this.renderingState = RenderingStates.RUNNING;
|
|
|
|
// 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(
|
|
() => this.#finishRenderTask(renderTask, canvas),
|
|
error => this.#finishRenderTask(renderTask, canvas, 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;
|
|
|
|
this.eventBus.dispatch("thumbnailrendered", {
|
|
source: this,
|
|
pageNumber: this.id,
|
|
pdfPage: this.pdfPage,
|
|
});
|
|
});
|
|
|
|
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 };
|