Merge pull request #19128 from nicolo-ribaudo/draw-page-portion
[api-minor] Render high-res partial page views when falling back to CSS zoom (bug 1492303)
This commit is contained in:
commit
553ec7babc
@ -3232,16 +3232,26 @@ class PDFObjects {
|
|||||||
class RenderTask {
|
class RenderTask {
|
||||||
#internalRenderTask = null;
|
#internalRenderTask = null;
|
||||||
|
|
||||||
constructor(internalRenderTask) {
|
|
||||||
this.#internalRenderTask = internalRenderTask;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback for incremental rendering -- a function that will be called
|
* Callback for incremental rendering -- a function that will be called
|
||||||
* each time the rendering is paused. To continue rendering call the
|
* each time the rendering is paused. To continue rendering call the
|
||||||
* function that is the first argument to the callback.
|
* function that is the first argument to the callback.
|
||||||
* @type {function}
|
* @type {function}
|
||||||
*/
|
*/
|
||||||
this.onContinue = null;
|
onContinue = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that will be synchronously called when the rendering tasks
|
||||||
|
* finishes with an error (either because of an actual error, or because the
|
||||||
|
* rendering is cancelled).
|
||||||
|
*
|
||||||
|
* @type {function}
|
||||||
|
* @param {Error} error
|
||||||
|
*/
|
||||||
|
onError = null;
|
||||||
|
|
||||||
|
constructor(internalRenderTask) {
|
||||||
|
this.#internalRenderTask = internalRenderTask;
|
||||||
|
|
||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||||
// For testing purposes.
|
// For testing purposes.
|
||||||
@ -3399,13 +3409,13 @@ class InternalRenderTask {
|
|||||||
}
|
}
|
||||||
InternalRenderTask.#canvasInUse.delete(this._canvas);
|
InternalRenderTask.#canvasInUse.delete(this._canvas);
|
||||||
|
|
||||||
this.callback(
|
error ||= new RenderingCancelledException(
|
||||||
error ||
|
|
||||||
new RenderingCancelledException(
|
|
||||||
`Rendering cancelled, page ${this._pageIndex + 1}`,
|
`Rendering cancelled, page ${this._pageIndex + 1}`,
|
||||||
extraDelay
|
extraDelay
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
this.callback(error);
|
||||||
|
|
||||||
|
this.task.onError?.(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
operatorListChanged() {
|
operatorListChanged() {
|
||||||
|
|||||||
@ -17,11 +17,15 @@ import os from "os";
|
|||||||
|
|
||||||
const isMac = os.platform() === "darwin";
|
const isMac = os.platform() === "darwin";
|
||||||
|
|
||||||
function loadAndWait(filename, selector, zoom, setups, options) {
|
function loadAndWait(filename, selector, zoom, setups, options, viewport) {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
global.integrationSessions.map(async session => {
|
global.integrationSessions.map(async session => {
|
||||||
const page = await session.browser.newPage();
|
const page = await session.browser.newPage();
|
||||||
|
|
||||||
|
if (viewport) {
|
||||||
|
await page.setViewport(viewport);
|
||||||
|
}
|
||||||
|
|
||||||
// In order to avoid errors because of checks which depend on
|
// In order to avoid errors because of checks which depend on
|
||||||
// a locale.
|
// a locale.
|
||||||
await page.evaluateOnNewDocument(() => {
|
await page.evaluateOnNewDocument(() => {
|
||||||
@ -566,8 +570,12 @@ function waitForAnnotationModeChanged(page) {
|
|||||||
|
|
||||||
function waitForPageRendered(page) {
|
function waitForPageRendered(page) {
|
||||||
return createPromise(page, resolve => {
|
return createPromise(page, resolve => {
|
||||||
window.PDFViewerApplication.eventBus.on("pagerendered", resolve, {
|
const { eventBus } = window.PDFViewerApplication;
|
||||||
once: true,
|
eventBus.on("pagerendered", function handler(e) {
|
||||||
|
if (!e.isDetailView) {
|
||||||
|
resolve();
|
||||||
|
eventBus.off("pagerendered", handler);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -454,4 +454,646 @@ describe("PDF viewer", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Detail view on zoom", () => {
|
||||||
|
const BASE_MAX_CANVAS_PIXELS = 1e6;
|
||||||
|
|
||||||
|
function setupPages(zoom, devicePixelRatio, setups = {}) {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"colors.pdf",
|
||||||
|
null,
|
||||||
|
zoom,
|
||||||
|
{
|
||||||
|
// When running Firefox with Puppeteer, setting the
|
||||||
|
// devicePixelRatio Puppeteer option does not properly set
|
||||||
|
// the `window.devicePixelRatio` value. Set it manually.
|
||||||
|
earlySetup: `() => {
|
||||||
|
window.devicePixelRatio = ${devicePixelRatio};
|
||||||
|
}`,
|
||||||
|
...setups,
|
||||||
|
},
|
||||||
|
{ maxCanvasPixels: BASE_MAX_CANVAS_PIXELS * devicePixelRatio ** 2 },
|
||||||
|
{ height: 600, width: 800, devicePixelRatio }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
return function forEachPage(fn) {
|
||||||
|
return Promise.all(
|
||||||
|
pages.map(([browserName, page]) => fn(browserName, page))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractCanvases(pageNumber) {
|
||||||
|
const pageOne = document.querySelector(
|
||||||
|
`.page[data-page-number='${pageNumber}']`
|
||||||
|
);
|
||||||
|
return Array.from(pageOne.querySelectorAll("canvas"), canvas => {
|
||||||
|
const { width, height } = canvas;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
const topLeft = ctx.getImageData(2, 2, 1, 1).data;
|
||||||
|
const bottomRight = ctx.getImageData(width - 3, height - 3, 1, 1).data;
|
||||||
|
return {
|
||||||
|
size: width * height,
|
||||||
|
topLeft: globalThis.pdfjsLib.Util.makeHexColor(...topLeft),
|
||||||
|
bottomRight: globalThis.pdfjsLib.Util.makeHexColor(...bottomRight),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForDetailRendered(page) {
|
||||||
|
return createPromise(page, resolve => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
window.PDFViewerApplication.eventBus.on(
|
||||||
|
"pagerendered",
|
||||||
|
({ isDetailView }) => {
|
||||||
|
if (isDetailView) {
|
||||||
|
resolve();
|
||||||
|
controller.abort();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal: controller.signal }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pixelRatio of [1, 2]) {
|
||||||
|
describe(`with pixel ratio ${pixelRatio}`, () => {
|
||||||
|
describe("setupPages()", () => {
|
||||||
|
const forEachPage = setupPages("100%", pixelRatio);
|
||||||
|
|
||||||
|
it("sets the proper devicePixelRatio", async () => {
|
||||||
|
await forEachPage(async (browserName, page) => {
|
||||||
|
const devicePixelRatio = await page.evaluate(
|
||||||
|
() => window.devicePixelRatio
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(devicePixelRatio)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toBe(pixelRatio);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when zooming in past max canvas size", () => {
|
||||||
|
const forEachPage = setupPages("100%", pixelRatio);
|
||||||
|
|
||||||
|
it("must render the detail view", async () => {
|
||||||
|
await forEachPage(async (browserName, page) => {
|
||||||
|
await page.waitForSelector(
|
||||||
|
".page[data-page-number='1'] .textLayer"
|
||||||
|
);
|
||||||
|
|
||||||
|
const before = await page.evaluate(extractCanvases, 1);
|
||||||
|
|
||||||
|
expect(before.length)
|
||||||
|
.withContext(`In ${browserName}, before`)
|
||||||
|
.toBe(1);
|
||||||
|
expect(before[0].size)
|
||||||
|
.withContext(`In ${browserName}, before`)
|
||||||
|
.toBeLessThan(BASE_MAX_CANVAS_PIXELS * pixelRatio ** 2);
|
||||||
|
expect(before[0])
|
||||||
|
.withContext(`In ${browserName}, before`)
|
||||||
|
.toEqual(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
topLeft: "#85200c", // dark berry
|
||||||
|
bottomRight: "#b6d7a8", // light green
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const factor = 3;
|
||||||
|
// Check that we are going to trigger CSS zoom.
|
||||||
|
expect(before[0].size * factor ** 2)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toBeGreaterThan(BASE_MAX_CANVAS_PIXELS * pixelRatio ** 2);
|
||||||
|
|
||||||
|
const handle = await waitForDetailRendered(page);
|
||||||
|
await page.evaluate(scaleFactor => {
|
||||||
|
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||||
|
drawingDelay: 0,
|
||||||
|
scaleFactor,
|
||||||
|
});
|
||||||
|
}, factor);
|
||||||
|
await awaitPromise(handle);
|
||||||
|
|
||||||
|
const after = await page.evaluate(extractCanvases, 1);
|
||||||
|
|
||||||
|
expect(after.length)
|
||||||
|
.withContext(`In ${browserName}, after`)
|
||||||
|
.toBe(2);
|
||||||
|
expect(after[0].size)
|
||||||
|
.withContext(`In ${browserName}, after (first)`)
|
||||||
|
.toBeLessThan(4e6);
|
||||||
|
expect(after[0])
|
||||||
|
.withContext(`In ${browserName}, after (first)`)
|
||||||
|
.toEqual(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
topLeft: "#85200c", // dark berry
|
||||||
|
bottomRight: "#b6d7a8", // light green
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(after[1].size)
|
||||||
|
.withContext(`In ${browserName}, after (second)`)
|
||||||
|
.toBeLessThan(4e6);
|
||||||
|
expect(after[1])
|
||||||
|
.withContext(`In ${browserName}, after (second)`)
|
||||||
|
.toEqual(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
topLeft: "#85200c", // dark berry
|
||||||
|
bottomRight: "#ff0000", // bright red
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when starting already zoomed in past max canvas size", () => {
|
||||||
|
const forEachPage = setupPages("300%", pixelRatio);
|
||||||
|
|
||||||
|
it("must render the detail view", async () => {
|
||||||
|
await forEachPage(async (browserName, page) => {
|
||||||
|
await page.waitForSelector(
|
||||||
|
".page[data-page-number='1'] canvas:nth-child(2)"
|
||||||
|
);
|
||||||
|
|
||||||
|
const canvases = await page.evaluate(extractCanvases, 1);
|
||||||
|
|
||||||
|
expect(canvases.length).withContext(`In ${browserName}`).toBe(2);
|
||||||
|
expect(canvases[0].size)
|
||||||
|
.withContext(`In ${browserName} (first)`)
|
||||||
|
.toBeLessThan(4e6);
|
||||||
|
expect(canvases[0])
|
||||||
|
.withContext(`In ${browserName} (first)`)
|
||||||
|
.toEqual(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
topLeft: "#85200c", // dark berry
|
||||||
|
bottomRight: "#b6d7a8", // light green
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(canvases[1].size)
|
||||||
|
.withContext(`In ${browserName} (second)`)
|
||||||
|
.toBeLessThan(4e6);
|
||||||
|
expect(canvases[1])
|
||||||
|
.withContext(`In ${browserName} (second)`)
|
||||||
|
.toEqual(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
topLeft: "#85200c", // dark berry
|
||||||
|
bottomRight: "#ff0000", // bright red
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when scrolling", () => {
|
||||||
|
const forEachPage = setupPages("300%", pixelRatio);
|
||||||
|
|
||||||
|
it("must update the detail view", async () => {
|
||||||
|
await forEachPage(async (browserName, page) => {
|
||||||
|
await page.waitForSelector(
|
||||||
|
".page[data-page-number='1'] canvas:nth-child(2)"
|
||||||
|
);
|
||||||
|
|
||||||
|
const handle = await waitForDetailRendered(page);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const container = document.getElementById("viewerContainer");
|
||||||
|
container.scrollTop += 1600;
|
||||||
|
container.scrollLeft += 1100;
|
||||||
|
});
|
||||||
|
await awaitPromise(handle);
|
||||||
|
|
||||||
|
const canvases = await page.evaluate(extractCanvases, 1);
|
||||||
|
|
||||||
|
expect(canvases.length).withContext(`In ${browserName}`).toBe(2);
|
||||||
|
expect(canvases[1].size)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toBeLessThan(4e6);
|
||||||
|
expect(canvases[1])
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
topLeft: "#ff9900", // bright orange
|
||||||
|
bottomRight: "#ffe599", // light yellow
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when scrolling little enough that the existing detail covers the new viewport", () => {
|
||||||
|
const forEachPage = setupPages("300%", pixelRatio);
|
||||||
|
|
||||||
|
it("must not re-create the detail canvas", async () => {
|
||||||
|
await forEachPage(async (browserName, page) => {
|
||||||
|
const detailCanvasSelector =
|
||||||
|
".page[data-page-number='1'] canvas:nth-child(2)";
|
||||||
|
|
||||||
|
await page.waitForSelector(detailCanvasSelector);
|
||||||
|
|
||||||
|
const detailCanvasHandle = await page.$(detailCanvasSelector);
|
||||||
|
|
||||||
|
let rendered = false;
|
||||||
|
const handle = await waitForDetailRendered(page);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const container = document.getElementById("viewerContainer");
|
||||||
|
container.scrollTop += 10;
|
||||||
|
container.scrollLeft += 10;
|
||||||
|
});
|
||||||
|
awaitPromise(handle)
|
||||||
|
.then(() => {
|
||||||
|
rendered = true;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
// Give some time to the page to re-render. If it re-renders it's
|
||||||
|
// a bug, but without waiting we would never catch it.
|
||||||
|
await new Promise(resolve => {
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSame = await page.evaluate(
|
||||||
|
(prev, selector) => prev === document.querySelector(selector),
|
||||||
|
detailCanvasHandle,
|
||||||
|
detailCanvasSelector
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(isSame).withContext(`In ${browserName}`).toBe(true);
|
||||||
|
expect(rendered).withContext(`In ${browserName}`).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when scrolling to have two visible pages", () => {
|
||||||
|
const forEachPage = setupPages("300%", pixelRatio);
|
||||||
|
|
||||||
|
it("must update the detail view", async () => {
|
||||||
|
await forEachPage(async (browserName, page) => {
|
||||||
|
await page.waitForSelector(
|
||||||
|
".page[data-page-number='1'] canvas:nth-child(2)"
|
||||||
|
);
|
||||||
|
|
||||||
|
const handle = await createPromise(page, resolve => {
|
||||||
|
// wait for two 'pagerendered' events for detail views
|
||||||
|
let second = false;
|
||||||
|
const { eventBus } = window.PDFViewerApplication;
|
||||||
|
eventBus.on(
|
||||||
|
"pagerendered",
|
||||||
|
function onPageRendered({ isDetailView }) {
|
||||||
|
if (!isDetailView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!second) {
|
||||||
|
second = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
eventBus.off("pagerendered", onPageRendered);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const container = document.getElementById("viewerContainer");
|
||||||
|
container.scrollLeft += 600;
|
||||||
|
container.scrollTop += 3000;
|
||||||
|
});
|
||||||
|
await awaitPromise(handle);
|
||||||
|
|
||||||
|
const [canvases1, canvases2] = await Promise.all([
|
||||||
|
page.evaluate(extractCanvases, 1),
|
||||||
|
page.evaluate(extractCanvases, 2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(canvases1.length)
|
||||||
|
.withContext(`In ${browserName}, first page`)
|
||||||
|
.toBe(2);
|
||||||
|
expect(canvases1[1].size)
|
||||||
|
.withContext(`In ${browserName}, first page`)
|
||||||
|
.toBeLessThan(4e6);
|
||||||
|
expect(canvases1[1])
|
||||||
|
.withContext(`In ${browserName}, first page`)
|
||||||
|
.toEqual(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
topLeft: "#38761d", // dark green
|
||||||
|
bottomRight: "#b6d7a8", // light green
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(canvases2.length)
|
||||||
|
.withContext(`In ${browserName}, second page`)
|
||||||
|
.toBe(2);
|
||||||
|
expect(canvases2[1].size)
|
||||||
|
.withContext(`In ${browserName}, second page`)
|
||||||
|
.toBeLessThan(4e6);
|
||||||
|
expect(canvases2[1])
|
||||||
|
.withContext(`In ${browserName}, second page`)
|
||||||
|
.toEqual(
|
||||||
|
jasmine.objectContaining({
|
||||||
|
topLeft: "#134f5c", // dark cyan
|
||||||
|
bottomRight: "#a2c4c9", // light cyan
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pagerendered event", () => {
|
||||||
|
const forEachPage = setupPages("100%", pixelRatio, {
|
||||||
|
eventBusSetup: eventBus => {
|
||||||
|
globalThis.__pageRenderedEvents = [];
|
||||||
|
|
||||||
|
eventBus.on(
|
||||||
|
"pagerendered",
|
||||||
|
({ pageNumber, isDetailView, cssTransform }) => {
|
||||||
|
globalThis.__pageRenderedEvents.push({
|
||||||
|
pageNumber,
|
||||||
|
isDetailView,
|
||||||
|
cssTransform,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is dispatched properly", async () => {
|
||||||
|
await forEachPage(async (browserName, page) => {
|
||||||
|
const getPageRenderedEvents = () =>
|
||||||
|
page.evaluate(() => {
|
||||||
|
const events = globalThis.__pageRenderedEvents;
|
||||||
|
globalThis.__pageRenderedEvents = [];
|
||||||
|
return events;
|
||||||
|
});
|
||||||
|
const waitForPageRenderedEvent = filter =>
|
||||||
|
page.waitForFunction(
|
||||||
|
filterStr =>
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
globalThis.__pageRenderedEvents.some(eval(filterStr)),
|
||||||
|
{ polling: 50 },
|
||||||
|
String(filter)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
await waitForPageRenderedEvent(e => e.pageNumber === 2);
|
||||||
|
expect(await getPageRenderedEvents())
|
||||||
|
.withContext(`In ${browserName}, initial render`)
|
||||||
|
.toEqual([
|
||||||
|
{ pageNumber: 1, isDetailView: false, cssTransform: false },
|
||||||
|
{ pageNumber: 2, isDetailView: false, cssTransform: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Zoom-in without triggering the detail view
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||||
|
drawingDelay: -1,
|
||||||
|
scaleFactor: 1.05,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForPageRenderedEvent(e => e.pageNumber === 2);
|
||||||
|
expect(await getPageRenderedEvents())
|
||||||
|
.withContext(`In ${browserName}, first zoom`)
|
||||||
|
.toEqual([
|
||||||
|
{ pageNumber: 1, isDetailView: false, cssTransform: false },
|
||||||
|
{ pageNumber: 2, isDetailView: false, cssTransform: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Zoom-in on the first page, triggering the detail view
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||||
|
drawingDelay: -1,
|
||||||
|
scaleFactor: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await Promise.all([
|
||||||
|
waitForPageRenderedEvent(
|
||||||
|
e => e.isDetailView && e.pageNumber === 1
|
||||||
|
),
|
||||||
|
waitForPageRenderedEvent(e => e.pageNumber === 2),
|
||||||
|
]);
|
||||||
|
expect(await getPageRenderedEvents())
|
||||||
|
.withContext(`In ${browserName}, second zoom`)
|
||||||
|
.toEqual([
|
||||||
|
{ pageNumber: 1, isDetailView: false, cssTransform: false },
|
||||||
|
{ pageNumber: 1, isDetailView: true, cssTransform: false },
|
||||||
|
{ pageNumber: 2, isDetailView: false, cssTransform: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Zoom-in more
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||||
|
drawingDelay: -1,
|
||||||
|
scaleFactor: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await Promise.all([
|
||||||
|
waitForPageRenderedEvent(
|
||||||
|
e => e.isDetailView && e.pageNumber === 1
|
||||||
|
),
|
||||||
|
waitForPageRenderedEvent(e => e.pageNumber === 2),
|
||||||
|
]);
|
||||||
|
expect(await getPageRenderedEvents())
|
||||||
|
.withContext(`In ${browserName}, third zoom`)
|
||||||
|
.toEqual([
|
||||||
|
{ pageNumber: 1, isDetailView: false, cssTransform: true },
|
||||||
|
{ pageNumber: 2, isDetailView: false, cssTransform: true },
|
||||||
|
{ pageNumber: 1, isDetailView: true, cssTransform: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Scroll to another area of the first page
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const container = document.getElementById("viewerContainer");
|
||||||
|
container.scrollTop += 1600;
|
||||||
|
container.scrollLeft += 1100;
|
||||||
|
});
|
||||||
|
await waitForPageRenderedEvent(e => e.isDetailView);
|
||||||
|
expect(await getPageRenderedEvents())
|
||||||
|
.withContext(`In ${browserName}, first scroll`)
|
||||||
|
.toEqual([
|
||||||
|
{ pageNumber: 1, isDetailView: true, cssTransform: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Scroll to the second page
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const container = document.getElementById("viewerContainer");
|
||||||
|
const pageElement = document.querySelector(".page");
|
||||||
|
container.scrollTop +=
|
||||||
|
pageElement.getBoundingClientRect().height;
|
||||||
|
});
|
||||||
|
await waitForPageRenderedEvent(e => e.isDetailView);
|
||||||
|
expect(await getPageRenderedEvents())
|
||||||
|
.withContext(`In ${browserName}, second scroll`)
|
||||||
|
.toEqual([
|
||||||
|
{ pageNumber: 2, isDetailView: true, cssTransform: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Zoom-out, to not have the detail view anymore
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||||
|
drawingDelay: -1,
|
||||||
|
scaleFactor: 0.25,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await Promise.all([
|
||||||
|
waitForPageRenderedEvent(e => e.pageNumber === 1),
|
||||||
|
waitForPageRenderedEvent(e => e.pageNumber === 2),
|
||||||
|
]);
|
||||||
|
expect(await getPageRenderedEvents())
|
||||||
|
.withContext(`In ${browserName}, second zoom`)
|
||||||
|
.toEqual([
|
||||||
|
{ pageNumber: 2, isDetailView: false, cssTransform: false },
|
||||||
|
{ pageNumber: 1, isDetailView: false, cssTransform: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const canvasesPerPage = await page.evaluate(() =>
|
||||||
|
Array.from(
|
||||||
|
document.querySelectorAll(".canvasWrapper"),
|
||||||
|
wrapper => wrapper.childElementCount
|
||||||
|
)
|
||||||
|
);
|
||||||
|
expect(canvasesPerPage)
|
||||||
|
.withContext(`In ${browserName}, number of canvases`)
|
||||||
|
.toEqual([1, 1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("when immediately cancelled and re-rendered", () => {
|
||||||
|
const forEachPage = setupPages("100%", 1, {
|
||||||
|
eventBusSetup: eventBus => {
|
||||||
|
globalThis.__pageRenderedEvents = [];
|
||||||
|
eventBus.on("pagerendered", ({ pageNumber, isDetailView }) => {
|
||||||
|
globalThis.__pageRenderedEvents.push({ pageNumber, isDetailView });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propely cleans up old canvases from the dom", async () => {
|
||||||
|
await forEachPage(async (browserName, page) => {
|
||||||
|
const waitForPageRenderedEvent = filter =>
|
||||||
|
page.waitForFunction(
|
||||||
|
filterStr => {
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
if (globalThis.__pageRenderedEvents.some(eval(filterStr))) {
|
||||||
|
globalThis.__pageRenderedEvents = [];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
{ polling: 50 },
|
||||||
|
String(filter)
|
||||||
|
);
|
||||||
|
const getCanvasCount = () =>
|
||||||
|
page.evaluate(
|
||||||
|
() =>
|
||||||
|
document.querySelector("[data-page-number='1'] .canvasWrapper")
|
||||||
|
.childElementCount
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitForPageRenderedEvent(e => e.pageNumber === 1);
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||||
|
drawingDelay: -1,
|
||||||
|
scaleFactor: 4,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForPageRenderedEvent(
|
||||||
|
e => e.pageNumber === 1 && e.isDetailView
|
||||||
|
);
|
||||||
|
expect(await getCanvasCount())
|
||||||
|
.withContext(`In ${browserName}, after the first zoom-in`)
|
||||||
|
.toBe(2);
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||||
|
drawingDelay: -1,
|
||||||
|
scaleFactor: 0.75,
|
||||||
|
});
|
||||||
|
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||||
|
drawingDelay: -1,
|
||||||
|
scaleFactor: 0.25,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForPageRenderedEvent(e => e.pageNumber === 1);
|
||||||
|
expect(await getCanvasCount())
|
||||||
|
.withContext(`In ${browserName}, after the two zoom-out`)
|
||||||
|
.toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when cancelled and re-rendered after 1 microtick", () => {
|
||||||
|
const forEachPage = setupPages("100%", 1, {
|
||||||
|
eventBusSetup: eventBus => {
|
||||||
|
globalThis.__pageRenderedEvents = [];
|
||||||
|
eventBus.on("pagerendered", ({ pageNumber, isDetailView }) => {
|
||||||
|
globalThis.__pageRenderedEvents.push({ pageNumber, isDetailView });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propely cleans up old canvases from the dom", async () => {
|
||||||
|
await forEachPage(async (browserName, page) => {
|
||||||
|
const waitForPageRenderedEvent = filter =>
|
||||||
|
page.waitForFunction(
|
||||||
|
filterStr => {
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
if (globalThis.__pageRenderedEvents.some(eval(filterStr))) {
|
||||||
|
globalThis.__pageRenderedEvents = [];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
{ polling: 50 },
|
||||||
|
String(filter)
|
||||||
|
);
|
||||||
|
const getCanvasCount = () =>
|
||||||
|
page.evaluate(
|
||||||
|
() =>
|
||||||
|
document.querySelector("[data-page-number='1'] .canvasWrapper")
|
||||||
|
.childElementCount
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitForPageRenderedEvent(e => e.pageNumber === 1);
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||||
|
drawingDelay: -1,
|
||||||
|
scaleFactor: 4,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForPageRenderedEvent(
|
||||||
|
e => e.pageNumber === 1 && e.isDetailView
|
||||||
|
);
|
||||||
|
expect(await getCanvasCount())
|
||||||
|
.withContext(`In ${browserName}, after the first zoom-in`)
|
||||||
|
.toBe(2);
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||||
|
drawingDelay: -1,
|
||||||
|
scaleFactor: 0.75,
|
||||||
|
});
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||||
|
drawingDelay: -1,
|
||||||
|
scaleFactor: 0.25,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForPageRenderedEvent(e => e.pageNumber === 1);
|
||||||
|
expect(await getCanvasCount())
|
||||||
|
.withContext(`In ${browserName}, after the two zoom-out`)
|
||||||
|
.toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -706,3 +706,4 @@
|
|||||||
!bug1019475_1.pdf
|
!bug1019475_1.pdf
|
||||||
!bug1019475_2.pdf
|
!bug1019475_2.pdf
|
||||||
!issue19505.pdf
|
!issue19505.pdf
|
||||||
|
!colors.pdf
|
||||||
|
|||||||
BIN
test/pdfs/colors.pdf
Normal file
BIN
test/pdfs/colors.pdf
Normal file
Binary file not shown.
@ -279,12 +279,11 @@ describe("ui_utils", function () {
|
|||||||
viewTop < scrollBottom &&
|
viewTop < scrollBottom &&
|
||||||
viewBottom > scrollTop
|
viewBottom > scrollTop
|
||||||
) {
|
) {
|
||||||
const hiddenHeight =
|
const minY = Math.max(0, scrollTop - viewTop);
|
||||||
Math.max(0, scrollTop - viewTop) +
|
const minX = Math.max(0, scrollLeft - viewLeft);
|
||||||
Math.max(0, viewBottom - scrollBottom);
|
|
||||||
const hiddenWidth =
|
const hiddenHeight = minY + Math.max(0, viewBottom - scrollBottom);
|
||||||
Math.max(0, scrollLeft - viewLeft) +
|
const hiddenWidth = minX + Math.max(0, viewRight - scrollRight);
|
||||||
Math.max(0, viewRight - scrollRight);
|
|
||||||
|
|
||||||
const fractionHeight =
|
const fractionHeight =
|
||||||
(div.clientHeight - hiddenHeight) / div.clientHeight;
|
(div.clientHeight - hiddenHeight) / div.clientHeight;
|
||||||
@ -292,12 +291,23 @@ describe("ui_utils", function () {
|
|||||||
(div.clientWidth - hiddenWidth) / div.clientWidth;
|
(div.clientWidth - hiddenWidth) / div.clientWidth;
|
||||||
const percent = (fractionHeight * fractionWidth * 100) | 0;
|
const percent = (fractionHeight * fractionWidth * 100) | 0;
|
||||||
|
|
||||||
|
let visibleArea = null;
|
||||||
|
if (percent < 100) {
|
||||||
|
visibleArea = {
|
||||||
|
minX,
|
||||||
|
minY,
|
||||||
|
maxX: Math.min(viewRight, scrollRight) - viewLeft,
|
||||||
|
maxY: Math.min(viewBottom, scrollBottom) - viewTop,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
views.push({
|
views.push({
|
||||||
id: view.id,
|
id: view.id,
|
||||||
x: viewLeft,
|
x: viewLeft,
|
||||||
y: viewTop,
|
y: viewTop,
|
||||||
view,
|
view,
|
||||||
percent,
|
percent,
|
||||||
|
visibleArea,
|
||||||
widthPercent: (fractionWidth * 100) | 0,
|
widthPercent: (fractionWidth * 100) | 0,
|
||||||
});
|
});
|
||||||
ids.add(view.id);
|
ids.add(view.id);
|
||||||
|
|||||||
@ -507,6 +507,7 @@ const PDFViewerApplication = {
|
|||||||
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
||||||
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
||||||
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
||||||
|
enableDetailCanvas: AppOptions.get("enableDetailCanvas"),
|
||||||
enablePermissions: AppOptions.get("enablePermissions"),
|
enablePermissions: AppOptions.get("enablePermissions"),
|
||||||
pageColors,
|
pageColors,
|
||||||
mlManager: this.mlManager,
|
mlManager: this.mlManager,
|
||||||
@ -2331,7 +2332,7 @@ function onPageRender({ pageNumber }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPageRendered({ pageNumber, error }) {
|
function onPageRendered({ pageNumber, isDetailView, error }) {
|
||||||
// If the page is still visible when it has finished rendering,
|
// If the page is still visible when it has finished rendering,
|
||||||
// ensure that the page number input loading indicator is hidden.
|
// ensure that the page number input loading indicator is hidden.
|
||||||
if (pageNumber === this.page) {
|
if (pageNumber === this.page) {
|
||||||
@ -2339,7 +2340,7 @@ function onPageRendered({ pageNumber, error }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the rendered page to set the corresponding thumbnail image.
|
// Use the rendered page to set the corresponding thumbnail image.
|
||||||
if (this.pdfSidebar?.visibleView === SidebarView.THUMBS) {
|
if (!isDetailView && this.pdfSidebar?.visibleView === SidebarView.THUMBS) {
|
||||||
const pageView = this.pdfViewer.getPageView(/* index = */ pageNumber - 1);
|
const pageView = this.pdfViewer.getPageView(/* index = */ pageNumber - 1);
|
||||||
const thumbnailView = this.pdfThumbnailViewer?.getThumbnail(
|
const thumbnailView = this.pdfThumbnailViewer?.getThumbnail(
|
||||||
/* index = */ pageNumber - 1
|
/* index = */ pageNumber - 1
|
||||||
|
|||||||
@ -200,6 +200,11 @@ const defaultOptions = {
|
|||||||
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("MOZCENTRAL"),
|
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("MOZCENTRAL"),
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||||
},
|
},
|
||||||
|
enableDetailCanvas: {
|
||||||
|
/** @type {boolean} */
|
||||||
|
value: true,
|
||||||
|
kind: OptionKind.VIEWER,
|
||||||
|
},
|
||||||
enableGuessAltText: {
|
enableGuessAltText: {
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
value: true,
|
value: true,
|
||||||
|
|||||||
227
web/base_pdf_page_view.js
Normal file
227
web/base_pdf_page_view.js
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
/* 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 { RenderingCancelledException } from "pdfjs-lib";
|
||||||
|
import { RenderingStates } from "./ui_utils.js";
|
||||||
|
|
||||||
|
class BasePDFPageView {
|
||||||
|
#enableHWA = false;
|
||||||
|
|
||||||
|
#loadingId = null;
|
||||||
|
|
||||||
|
#renderError = null;
|
||||||
|
|
||||||
|
#renderingState = RenderingStates.INITIAL;
|
||||||
|
|
||||||
|
#showCanvas = null;
|
||||||
|
|
||||||
|
canvas = null;
|
||||||
|
|
||||||
|
/** @type {null | HTMLDivElement} */
|
||||||
|
div = null;
|
||||||
|
|
||||||
|
eventBus = null;
|
||||||
|
|
||||||
|
id = null;
|
||||||
|
|
||||||
|
pageColors = null;
|
||||||
|
|
||||||
|
renderingQueue = null;
|
||||||
|
|
||||||
|
renderTask = null;
|
||||||
|
|
||||||
|
resume = null;
|
||||||
|
|
||||||
|
constructor(options) {
|
||||||
|
this.#enableHWA =
|
||||||
|
#enableHWA in options ? options.#enableHWA : options.enableHWA || false;
|
||||||
|
this.eventBus = options.eventBus;
|
||||||
|
this.id = options.id;
|
||||||
|
this.pageColors = options.pageColors || null;
|
||||||
|
this.renderingQueue = options.renderingQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get renderingState() {
|
||||||
|
return this.#renderingState;
|
||||||
|
}
|
||||||
|
|
||||||
|
set renderingState(state) {
|
||||||
|
if (state === this.#renderingState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#renderingState = state;
|
||||||
|
|
||||||
|
if (this.#loadingId) {
|
||||||
|
clearTimeout(this.#loadingId);
|
||||||
|
this.#loadingId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case RenderingStates.PAUSED:
|
||||||
|
this.div.classList.remove("loading");
|
||||||
|
break;
|
||||||
|
case RenderingStates.RUNNING:
|
||||||
|
this.div.classList.add("loadingIcon");
|
||||||
|
this.#loadingId = setTimeout(() => {
|
||||||
|
// Adding the loading class is slightly postponed in order to not have
|
||||||
|
// it with loadingIcon.
|
||||||
|
// If we don't do that the visibility of the background is changed but
|
||||||
|
// the transition isn't triggered.
|
||||||
|
this.div.classList.add("loading");
|
||||||
|
this.#loadingId = null;
|
||||||
|
}, 0);
|
||||||
|
break;
|
||||||
|
case RenderingStates.INITIAL:
|
||||||
|
case RenderingStates.FINISHED:
|
||||||
|
this.div.classList.remove("loadingIcon", "loading");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_createCanvas(onShow, hideUntilComplete = false) {
|
||||||
|
const { pageColors } = this;
|
||||||
|
const hasHCM = !!(pageColors?.background && pageColors?.foreground);
|
||||||
|
const prevCanvas = this.canvas;
|
||||||
|
|
||||||
|
// In HCM, a final filter is applied on the canvas which means that
|
||||||
|
// before it's applied we've normal colors. Consequently, to avoid to
|
||||||
|
// have a final flash we just display it once all the drawing is done.
|
||||||
|
const updateOnFirstShow = !prevCanvas && !hasHCM && !hideUntilComplete;
|
||||||
|
|
||||||
|
const canvas = (this.canvas = document.createElement("canvas"));
|
||||||
|
|
||||||
|
this.#showCanvas = isLastShow => {
|
||||||
|
if (updateOnFirstShow) {
|
||||||
|
// Don't add the canvas until the first draw callback, or until
|
||||||
|
// drawing is complete when `!this.renderingQueue`, to prevent black
|
||||||
|
// flickering.
|
||||||
|
onShow(canvas);
|
||||||
|
this.#showCanvas = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isLastShow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevCanvas) {
|
||||||
|
prevCanvas.replaceWith(canvas);
|
||||||
|
prevCanvas.width = prevCanvas.height = 0;
|
||||||
|
} else {
|
||||||
|
onShow(canvas);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d", {
|
||||||
|
alpha: false,
|
||||||
|
willReadFrequently: !this.#enableHWA,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { canvas, prevCanvas, ctx };
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderContinueCallback = cont => {
|
||||||
|
this.#showCanvas?.(false);
|
||||||
|
if (this.renderingQueue && !this.renderingQueue.isHighestPriority(this)) {
|
||||||
|
this.renderingState = RenderingStates.PAUSED;
|
||||||
|
this.resume = () => {
|
||||||
|
this.renderingState = RenderingStates.RUNNING;
|
||||||
|
cont();
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cont();
|
||||||
|
};
|
||||||
|
|
||||||
|
_resetCanvas() {
|
||||||
|
const { canvas } = this;
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas.remove();
|
||||||
|
canvas.width = canvas.height = 0;
|
||||||
|
this.canvas = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _drawCanvas(options, onCancel, onFinish) {
|
||||||
|
const renderTask = (this.renderTask = this.pdfPage.render(options));
|
||||||
|
renderTask.onContinue = this.#renderContinueCallback;
|
||||||
|
renderTask.onError = error => {
|
||||||
|
if (error instanceof RenderingCancelledException) {
|
||||||
|
onCancel();
|
||||||
|
this.#renderError = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let error = null;
|
||||||
|
try {
|
||||||
|
await renderTask.promise;
|
||||||
|
this.#showCanvas?.(true);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
// When zooming with a `drawingDelay` set, avoid temporarily showing
|
||||||
|
// a black canvas if rendering was cancelled before the `onContinue`-
|
||||||
|
// callback had been invoked at least once.
|
||||||
|
if (error instanceof RenderingCancelledException) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#showCanvas?.(true);
|
||||||
|
} finally {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#renderError = error;
|
||||||
|
|
||||||
|
this.renderingState = RenderingStates.FINISHED;
|
||||||
|
|
||||||
|
onFinish(renderTask);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelRendering({ cancelExtraDelay = 0 } = {}) {
|
||||||
|
if (this.renderTask) {
|
||||||
|
this.renderTask.cancel(cancelExtraDelay);
|
||||||
|
this.renderTask = null;
|
||||||
|
}
|
||||||
|
this.resume = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchPageRender() {
|
||||||
|
this.eventBus.dispatch("pagerender", {
|
||||||
|
source: this,
|
||||||
|
pageNumber: this.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchPageRendered(cssTransform, isDetailView) {
|
||||||
|
this.eventBus.dispatch("pagerendered", {
|
||||||
|
source: this,
|
||||||
|
pageNumber: this.id,
|
||||||
|
cssTransform,
|
||||||
|
isDetailView,
|
||||||
|
timestamp: performance.now(),
|
||||||
|
error: this.#renderError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { BasePDFPageView };
|
||||||
272
web/pdf_page_detail_view.js
Normal file
272
web/pdf_page_detail_view.js
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
/* 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 { 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 } = 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 * (window.devicePixelRatio || 1) ** 2;
|
||||||
|
const maxDetailToVisibleLinearRatio = Math.sqrt(
|
||||||
|
maxCanvasPixels / 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
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, ctx } = 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.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
|
const { width, height } = viewport;
|
||||||
|
|
||||||
|
const area = this.#detailArea;
|
||||||
|
|
||||||
|
const { devicePixelRatio = 1 } = window;
|
||||||
|
const transform = [
|
||||||
|
devicePixelRatio,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
devicePixelRatio,
|
||||||
|
-area.minX * devicePixelRatio,
|
||||||
|
-area.minY * devicePixelRatio,
|
||||||
|
];
|
||||||
|
|
||||||
|
canvas.width = area.width * devicePixelRatio;
|
||||||
|
canvas.height = area.height * devicePixelRatio;
|
||||||
|
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.pageView._getRenderingContext(ctx, 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 };
|
||||||
@ -28,7 +28,6 @@ import {
|
|||||||
AnnotationMode,
|
AnnotationMode,
|
||||||
OutputScale,
|
OutputScale,
|
||||||
PixelsPerInch,
|
PixelsPerInch,
|
||||||
RenderingCancelledException,
|
|
||||||
setLayerDimensions,
|
setLayerDimensions,
|
||||||
shadow,
|
shadow,
|
||||||
} from "pdfjs-lib";
|
} from "pdfjs-lib";
|
||||||
@ -44,8 +43,10 @@ import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.
|
|||||||
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
||||||
import { AppOptions } from "./app_options.js";
|
import { AppOptions } from "./app_options.js";
|
||||||
import { Autolinker } from "./autolinker.js";
|
import { Autolinker } from "./autolinker.js";
|
||||||
|
import { BasePDFPageView } from "./base_pdf_page_view.js";
|
||||||
import { DrawLayerBuilder } from "./draw_layer_builder.js";
|
import { DrawLayerBuilder } from "./draw_layer_builder.js";
|
||||||
import { GenericL10n } from "web-null_l10n";
|
import { GenericL10n } from "web-null_l10n";
|
||||||
|
import { PDFPageDetailView } from "./pdf_page_detail_view.js";
|
||||||
import { SimpleLinkService } from "./pdf_link_service.js";
|
import { SimpleLinkService } from "./pdf_link_service.js";
|
||||||
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
|
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
|
||||||
import { TextAccessibilityManager } from "./text_accessibility.js";
|
import { TextAccessibilityManager } from "./text_accessibility.js";
|
||||||
@ -77,6 +78,12 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
|||||||
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
||||||
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
|
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
|
||||||
* CSS-only zooming. The default value is 4096 * 8192 (32 mega-pixels).
|
* CSS-only zooming. The default value is 4096 * 8192 (32 mega-pixels).
|
||||||
|
* @property {boolean} [enableDetailCanvas] - When enabled, if the rendered
|
||||||
|
* pages would need a canvas that is larger than `maxCanvasPixels`, it will
|
||||||
|
* draw a second canvas on top of the CSS-zoomed one, that only renders the
|
||||||
|
* part of the page that is close to the viewport. The default value is
|
||||||
|
* `true`.
|
||||||
|
|
||||||
* @property {Object} [pageColors] - Overwrites background and foreground colors
|
* @property {Object} [pageColors] - Overwrites background and foreground colors
|
||||||
* with user defined ones in order to improve readability in high contrast
|
* with user defined ones in order to improve readability in high contrast
|
||||||
* mode.
|
* mode.
|
||||||
@ -116,13 +123,11 @@ const LAYERS_ORDER = new Map([
|
|||||||
/**
|
/**
|
||||||
* @implements {IRenderableView}
|
* @implements {IRenderableView}
|
||||||
*/
|
*/
|
||||||
class PDFPageView {
|
class PDFPageView extends BasePDFPageView {
|
||||||
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
||||||
|
|
||||||
#canvasWrapper = null;
|
#canvasWrapper = null;
|
||||||
|
|
||||||
#enableHWA = false;
|
|
||||||
|
|
||||||
#enableAutoLinking = false;
|
#enableAutoLinking = false;
|
||||||
|
|
||||||
#hasRestrictedScaling = false;
|
#hasRestrictedScaling = false;
|
||||||
@ -131,7 +136,7 @@ class PDFPageView {
|
|||||||
|
|
||||||
#layerProperties = null;
|
#layerProperties = null;
|
||||||
|
|
||||||
#loadingId = null;
|
#needsRestrictedScaling = false;
|
||||||
|
|
||||||
#originalViewport = null;
|
#originalViewport = null;
|
||||||
|
|
||||||
@ -141,10 +146,6 @@ class PDFPageView {
|
|||||||
|
|
||||||
#scaleRoundY = 1;
|
#scaleRoundY = 1;
|
||||||
|
|
||||||
#renderError = null;
|
|
||||||
|
|
||||||
#renderingState = RenderingStates.INITIAL;
|
|
||||||
|
|
||||||
#textLayerMode = TextLayerMode.ENABLE;
|
#textLayerMode = TextLayerMode.ENABLE;
|
||||||
|
|
||||||
#userUnit = 1;
|
#userUnit = 1;
|
||||||
@ -161,10 +162,11 @@ class PDFPageView {
|
|||||||
* @param {PDFPageViewOptions} options
|
* @param {PDFPageViewOptions} options
|
||||||
*/
|
*/
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
const container = options.container;
|
const container = options.container;
|
||||||
const defaultViewport = options.defaultViewport;
|
const defaultViewport = options.defaultViewport;
|
||||||
|
|
||||||
this.id = options.id;
|
|
||||||
this.renderingId = "page" + this.id;
|
this.renderingId = "page" + this.id;
|
||||||
this.#layerProperties = options.layerProperties || DEFAULT_LAYER_PROPERTIES;
|
this.#layerProperties = options.layerProperties || DEFAULT_LAYER_PROPERTIES;
|
||||||
|
|
||||||
@ -180,21 +182,16 @@ class PDFPageView {
|
|||||||
this.#annotationMode =
|
this.#annotationMode =
|
||||||
options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
|
options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
|
||||||
this.imageResourcesPath = options.imageResourcesPath || "";
|
this.imageResourcesPath = options.imageResourcesPath || "";
|
||||||
|
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
||||||
this.maxCanvasPixels =
|
this.maxCanvasPixels =
|
||||||
options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
|
options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
|
||||||
this.pageColors = options.pageColors || null;
|
|
||||||
this.#enableHWA = options.enableHWA || false;
|
|
||||||
this.#enableAutoLinking = options.enableAutoLinking || false;
|
this.#enableAutoLinking = options.enableAutoLinking || false;
|
||||||
|
|
||||||
this.eventBus = options.eventBus;
|
|
||||||
this.renderingQueue = options.renderingQueue;
|
|
||||||
this.l10n = options.l10n;
|
this.l10n = options.l10n;
|
||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||||
this.l10n ||= new GenericL10n();
|
this.l10n ||= new GenericL10n();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderTask = null;
|
|
||||||
this.resume = null;
|
|
||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||||
this._isStandalone = !this.renderingQueue?.hasViewer();
|
this._isStandalone = !this.renderingQueue?.hasViewer();
|
||||||
this._container = container;
|
this._container = container;
|
||||||
@ -209,6 +206,8 @@ class PDFPageView {
|
|||||||
this.structTreeLayer = null;
|
this.structTreeLayer = null;
|
||||||
this.drawLayer = null;
|
this.drawLayer = null;
|
||||||
|
|
||||||
|
this.detailView = null;
|
||||||
|
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "page";
|
div.className = "page";
|
||||||
div.setAttribute("data-page-number", this.id);
|
div.setAttribute("data-page-number", this.id);
|
||||||
@ -278,43 +277,6 @@ class PDFPageView {
|
|||||||
this.div.prepend(div);
|
this.div.prepend(div);
|
||||||
}
|
}
|
||||||
|
|
||||||
get renderingState() {
|
|
||||||
return this.#renderingState;
|
|
||||||
}
|
|
||||||
|
|
||||||
set renderingState(state) {
|
|
||||||
if (state === this.#renderingState) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.#renderingState = state;
|
|
||||||
|
|
||||||
if (this.#loadingId) {
|
|
||||||
clearTimeout(this.#loadingId);
|
|
||||||
this.#loadingId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case RenderingStates.PAUSED:
|
|
||||||
this.div.classList.remove("loading");
|
|
||||||
break;
|
|
||||||
case RenderingStates.RUNNING:
|
|
||||||
this.div.classList.add("loadingIcon");
|
|
||||||
this.#loadingId = setTimeout(() => {
|
|
||||||
// Adding the loading class is slightly postponed in order to not have
|
|
||||||
// it with loadingIcon.
|
|
||||||
// If we don't do that the visibility of the background is changed but
|
|
||||||
// the transition isn't triggered.
|
|
||||||
this.div.classList.add("loading");
|
|
||||||
this.#loadingId = null;
|
|
||||||
}, 0);
|
|
||||||
break;
|
|
||||||
case RenderingStates.INITIAL:
|
|
||||||
case RenderingStates.FINISHED:
|
|
||||||
this.div.classList.remove("loadingIcon", "loading");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#setDimensions() {
|
#setDimensions() {
|
||||||
const { div, viewport } = this;
|
const { div, viewport } = this;
|
||||||
|
|
||||||
@ -558,14 +520,8 @@ class PDFPageView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#resetCanvas() {
|
_resetCanvas() {
|
||||||
const { canvas } = this;
|
super._resetCanvas();
|
||||||
if (!canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
canvas.remove();
|
|
||||||
canvas.width = canvas.height = 0;
|
|
||||||
this.canvas = null;
|
|
||||||
this.#originalViewport = null;
|
this.#originalViewport = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -575,6 +531,7 @@ class PDFPageView {
|
|||||||
keepXfaLayer = false,
|
keepXfaLayer = false,
|
||||||
keepTextLayer = false,
|
keepTextLayer = false,
|
||||||
keepCanvasWrapper = false,
|
keepCanvasWrapper = false,
|
||||||
|
preserveDetailViewState = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
this.cancelRendering({
|
this.cancelRendering({
|
||||||
keepAnnotationLayer,
|
keepAnnotationLayer,
|
||||||
@ -632,7 +589,18 @@ class PDFPageView {
|
|||||||
|
|
||||||
if (!keepCanvasWrapper && this.#canvasWrapper) {
|
if (!keepCanvasWrapper && this.#canvasWrapper) {
|
||||||
this.#canvasWrapper = null;
|
this.#canvasWrapper = null;
|
||||||
this.#resetCanvas();
|
this._resetCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preserveDetailViewState) {
|
||||||
|
this.detailView?.reset({ keepCanvas: keepCanvasWrapper });
|
||||||
|
|
||||||
|
// If we are keeping the canvas around we must also keep the `detailView`
|
||||||
|
// object, so that next time we need a detail view we'll update the
|
||||||
|
// existing canvas rather than creating a new one.
|
||||||
|
if (!keepCanvasWrapper) {
|
||||||
|
this.detailView = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -654,6 +622,22 @@ class PDFPageView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateVisibleArea(visibleArea) {
|
||||||
|
if (this.enableDetailCanvas) {
|
||||||
|
if (
|
||||||
|
this.#needsRestrictedScaling &&
|
||||||
|
this.maxCanvasPixels > 0 &&
|
||||||
|
visibleArea
|
||||||
|
) {
|
||||||
|
this.detailView ??= new PDFPageDetailView({ pageView: this });
|
||||||
|
this.detailView.update({ visibleArea });
|
||||||
|
} else if (this.detailView) {
|
||||||
|
this.detailView.reset();
|
||||||
|
this.detailView = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} PDFPageViewUpdateParameters
|
* @typedef {Object} PDFPageViewUpdateParameters
|
||||||
* @property {number} [scale] The new scale, if specified.
|
* @property {number} [scale] The new scale, if specified.
|
||||||
@ -709,22 +693,11 @@ class PDFPageView {
|
|||||||
this._container?.style.setProperty("--scale-factor", this.viewport.scale);
|
this._container?.style.setProperty("--scale-factor", this.viewport.scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#computeScale();
|
||||||
|
|
||||||
if (this.canvas) {
|
if (this.canvas) {
|
||||||
let onlyCssZoom = false;
|
const onlyCssZoom =
|
||||||
if (this.#hasRestrictedScaling) {
|
this.#hasRestrictedScaling && this.#needsRestrictedScaling;
|
||||||
if (
|
|
||||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
|
|
||||||
this.maxCanvasPixels === 0
|
|
||||||
) {
|
|
||||||
onlyCssZoom = true;
|
|
||||||
} else if (this.maxCanvasPixels > 0) {
|
|
||||||
const { width, height } = this.viewport;
|
|
||||||
const { sx, sy } = this.outputScale;
|
|
||||||
onlyCssZoom =
|
|
||||||
((Math.floor(width) * sx) | 0) * ((Math.floor(height) * sy) | 0) >
|
|
||||||
this.maxCanvasPixels;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000;
|
const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000;
|
||||||
|
|
||||||
if (postponeDrawing || onlyCssZoom) {
|
if (postponeDrawing || onlyCssZoom) {
|
||||||
@ -758,18 +731,16 @@ class PDFPageView {
|
|||||||
hideTextLayer: postponeDrawing,
|
hideTextLayer: postponeDrawing,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (postponeDrawing) {
|
|
||||||
// The "pagerendered"-event will be dispatched once the actual
|
// The "pagerendered"-event will be dispatched once the actual
|
||||||
// rendering is done, hence don't dispatch it here as well.
|
// rendering is done, hence don't dispatch it here as well.
|
||||||
return;
|
if (!postponeDrawing) {
|
||||||
|
this.detailView?.update({ underlyingViewUpdated: true });
|
||||||
|
|
||||||
|
this.dispatchPageRendered(
|
||||||
|
/* cssTransform */ true,
|
||||||
|
/* isDetailView */ false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.eventBus.dispatch("pagerendered", {
|
|
||||||
source: this,
|
|
||||||
pageNumber: this.id,
|
|
||||||
cssTransform: true,
|
|
||||||
timestamp: performance.now(),
|
|
||||||
error: this.#renderError,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -780,7 +751,38 @@ class PDFPageView {
|
|||||||
keepXfaLayer: true,
|
keepXfaLayer: true,
|
||||||
keepTextLayer: true,
|
keepTextLayer: true,
|
||||||
keepCanvasWrapper: true,
|
keepCanvasWrapper: true,
|
||||||
|
// It will be reset by the .update call below
|
||||||
|
preserveDetailViewState: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.detailView?.update({ underlyingViewUpdated: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
#computeScale() {
|
||||||
|
const { width, height } = this.viewport;
|
||||||
|
const outputScale = (this.outputScale = new OutputScale());
|
||||||
|
|
||||||
|
if (
|
||||||
|
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
|
||||||
|
this.maxCanvasPixels === 0
|
||||||
|
) {
|
||||||
|
const invScale = 1 / this.scale;
|
||||||
|
// Use a scale that makes the canvas have the originally intended size
|
||||||
|
// of the page.
|
||||||
|
outputScale.sx *= invScale;
|
||||||
|
outputScale.sy *= invScale;
|
||||||
|
this.#needsRestrictedScaling = true;
|
||||||
|
} else if (this.maxCanvasPixels > 0) {
|
||||||
|
const pixelsInViewport = width * height;
|
||||||
|
const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport);
|
||||||
|
if (outputScale.sx > maxScale || outputScale.sy > maxScale) {
|
||||||
|
outputScale.sx = maxScale;
|
||||||
|
outputScale.sy = maxScale;
|
||||||
|
this.#needsRestrictedScaling = true;
|
||||||
|
} else {
|
||||||
|
this.#needsRestrictedScaling = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -794,11 +796,7 @@ class PDFPageView {
|
|||||||
keepTextLayer = false,
|
keepTextLayer = false,
|
||||||
cancelExtraDelay = 0,
|
cancelExtraDelay = 0,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
if (this.renderTask) {
|
super.cancelRendering({ cancelExtraDelay });
|
||||||
this.renderTask.cancel(cancelExtraDelay);
|
|
||||||
this.renderTask = null;
|
|
||||||
}
|
|
||||||
this.resume = null;
|
|
||||||
|
|
||||||
if (this.textLayer && (!keepTextLayer || !this.textLayer.div)) {
|
if (this.textLayer && (!keepTextLayer || !this.textLayer.div)) {
|
||||||
this.textLayer.cancel();
|
this.textLayer.cancel();
|
||||||
@ -897,37 +895,29 @@ class PDFPageView {
|
|||||||
return this.viewport.convertToPdfPoint(x, y);
|
return this.viewport.convertToPdfPoint(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #finishRenderTask(renderTask, error = null) {
|
// Wrap the canvas so that if it has a CSS transform for high DPI the
|
||||||
// The renderTask may have been replaced by a new one, so only remove
|
// overflow will be hidden in Firefox.
|
||||||
// the reference to the renderTask if it matches the one that is
|
_ensureCanvasWrapper() {
|
||||||
// triggering this callback.
|
let canvasWrapper = this.#canvasWrapper;
|
||||||
if (renderTask === this.renderTask) {
|
if (!canvasWrapper) {
|
||||||
this.renderTask = null;
|
canvasWrapper = this.#canvasWrapper = document.createElement("div");
|
||||||
|
canvasWrapper.classList.add("canvasWrapper");
|
||||||
|
this.#addLayer(canvasWrapper, "canvasWrapper");
|
||||||
|
}
|
||||||
|
return canvasWrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof RenderingCancelledException) {
|
_getRenderingContext(canvasContext, transform) {
|
||||||
this.#renderError = null;
|
return {
|
||||||
return;
|
canvasContext,
|
||||||
}
|
transform,
|
||||||
this.#renderError = error;
|
viewport: this.viewport,
|
||||||
|
annotationMode: this.#annotationMode,
|
||||||
this.renderingState = RenderingStates.FINISHED;
|
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
||||||
|
annotationCanvasMap: this._annotationCanvasMap,
|
||||||
// Ensure that the thumbnails won't become partially (or fully) blank,
|
pageColors: this.pageColors,
|
||||||
// for documents that contain interactive form elements.
|
isEditing: this.#isEditing,
|
||||||
this.#useThumbnailCanvas.regularAnnotations = !renderTask.separateAnnots;
|
};
|
||||||
|
|
||||||
this.eventBus.dispatch("pagerendered", {
|
|
||||||
source: this,
|
|
||||||
pageNumber: this.id,
|
|
||||||
cssTransform: false,
|
|
||||||
timestamp: performance.now(),
|
|
||||||
error: this.#renderError,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async draw() {
|
async draw() {
|
||||||
@ -935,7 +925,7 @@ class PDFPageView {
|
|||||||
console.error("Must be in new state before drawing");
|
console.error("Must be in new state before drawing");
|
||||||
this.reset(); // Ensure that we reset all state to prevent issues.
|
this.reset(); // Ensure that we reset all state to prevent issues.
|
||||||
}
|
}
|
||||||
const { div, l10n, pageColors, pdfPage, viewport } = this;
|
const { div, l10n, pdfPage, viewport } = this;
|
||||||
|
|
||||||
if (!pdfPage) {
|
if (!pdfPage) {
|
||||||
this.renderingState = RenderingStates.FINISHED;
|
this.renderingState = RenderingStates.FINISHED;
|
||||||
@ -944,14 +934,7 @@ class PDFPageView {
|
|||||||
|
|
||||||
this.renderingState = RenderingStates.RUNNING;
|
this.renderingState = RenderingStates.RUNNING;
|
||||||
|
|
||||||
// Wrap the canvas so that if it has a CSS transform for high DPI the
|
const canvasWrapper = this._ensureCanvasWrapper();
|
||||||
// overflow will be hidden in Firefox.
|
|
||||||
let canvasWrapper = this.#canvasWrapper;
|
|
||||||
if (!canvasWrapper) {
|
|
||||||
canvasWrapper = this.#canvasWrapper = document.createElement("div");
|
|
||||||
canvasWrapper.classList.add("canvasWrapper");
|
|
||||||
this.#addLayer(canvasWrapper, "canvasWrapper");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.textLayer &&
|
!this.textLayer &&
|
||||||
@ -1009,84 +992,21 @@ class PDFPageView {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderContinueCallback = cont => {
|
|
||||||
showCanvas?.(false);
|
|
||||||
if (this.renderingQueue && !this.renderingQueue.isHighestPriority(this)) {
|
|
||||||
this.renderingState = RenderingStates.PAUSED;
|
|
||||||
this.resume = () => {
|
|
||||||
this.renderingState = RenderingStates.RUNNING;
|
|
||||||
cont();
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cont();
|
|
||||||
};
|
|
||||||
|
|
||||||
const { width, height } = viewport;
|
const { width, height } = viewport;
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.setAttribute("role", "presentation");
|
|
||||||
|
|
||||||
const hasHCM = !!(pageColors?.background && pageColors?.foreground);
|
|
||||||
const prevCanvas = this.canvas;
|
|
||||||
|
|
||||||
// In HCM, a final filter is applied on the canvas which means that
|
|
||||||
// before it's applied we've normal colors. Consequently, to avoid to
|
|
||||||
// have a final flash we just display it once all the drawing is done.
|
|
||||||
const updateOnFirstShow = !prevCanvas && !hasHCM;
|
|
||||||
this.canvas = canvas;
|
|
||||||
this.#originalViewport = viewport;
|
this.#originalViewport = viewport;
|
||||||
|
|
||||||
let showCanvas = isLastShow => {
|
const { canvas, prevCanvas, ctx } = this._createCanvas(newCanvas => {
|
||||||
if (updateOnFirstShow) {
|
// Always inject the canvas as the first element in the wrapper.
|
||||||
// Don't add the canvas until the first draw callback, or until
|
canvasWrapper.prepend(newCanvas);
|
||||||
// drawing is complete when `!this.renderingQueue`, to prevent black
|
|
||||||
// flickering.
|
|
||||||
// In whatever case, the canvas must be the first child.
|
|
||||||
canvasWrapper.prepend(canvas);
|
|
||||||
showCanvas = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isLastShow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevCanvas) {
|
|
||||||
prevCanvas.replaceWith(canvas);
|
|
||||||
prevCanvas.width = prevCanvas.height = 0;
|
|
||||||
} else {
|
|
||||||
canvasWrapper.prepend(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
showCanvas = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d", {
|
|
||||||
alpha: false,
|
|
||||||
willReadFrequently: !this.#enableHWA,
|
|
||||||
});
|
});
|
||||||
const outputScale = (this.outputScale = new OutputScale());
|
canvas.setAttribute("role", "presentation");
|
||||||
|
|
||||||
if (
|
if (!this.outputScale) {
|
||||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
|
this.#computeScale();
|
||||||
this.maxCanvasPixels === 0
|
|
||||||
) {
|
|
||||||
const invScale = 1 / this.scale;
|
|
||||||
// Use a scale that makes the canvas have the originally intended size
|
|
||||||
// of the page.
|
|
||||||
outputScale.sx *= invScale;
|
|
||||||
outputScale.sy *= invScale;
|
|
||||||
this.#hasRestrictedScaling = true;
|
|
||||||
} else if (this.maxCanvasPixels > 0) {
|
|
||||||
const pixelsInViewport = width * height;
|
|
||||||
const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport);
|
|
||||||
if (outputScale.sx > maxScale || outputScale.sy > maxScale) {
|
|
||||||
outputScale.sx = maxScale;
|
|
||||||
outputScale.sy = maxScale;
|
|
||||||
this.#hasRestrictedScaling = true;
|
|
||||||
} else {
|
|
||||||
this.#hasRestrictedScaling = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const { outputScale } = this;
|
||||||
|
this.#hasRestrictedScaling = this.#needsRestrictedScaling;
|
||||||
|
|
||||||
const sfx = approximateFraction(outputScale.sx);
|
const sfx = approximateFraction(outputScale.sx);
|
||||||
const sfy = approximateFraction(outputScale.sy);
|
const sfy = approximateFraction(outputScale.sy);
|
||||||
|
|
||||||
@ -1116,24 +1036,24 @@ class PDFPageView {
|
|||||||
const transform = outputScale.scaled
|
const transform = outputScale.scaled
|
||||||
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
|
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
|
||||||
: null;
|
: null;
|
||||||
const renderContext = {
|
const resultPromise = this._drawCanvas(
|
||||||
canvasContext: ctx,
|
this._getRenderingContext(ctx, transform),
|
||||||
transform,
|
() => {
|
||||||
viewport,
|
prevCanvas?.remove();
|
||||||
annotationMode: this.#annotationMode,
|
this._resetCanvas();
|
||||||
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
},
|
||||||
annotationCanvasMap: this._annotationCanvasMap,
|
renderTask => {
|
||||||
pageColors,
|
// Ensure that the thumbnails won't become partially (or fully) blank,
|
||||||
isEditing: this.#isEditing,
|
// for documents that contain interactive form elements.
|
||||||
};
|
this.#useThumbnailCanvas.regularAnnotations =
|
||||||
const renderTask = (this.renderTask = pdfPage.render(renderContext));
|
!renderTask.separateAnnots;
|
||||||
renderTask.onContinue = renderContinueCallback;
|
|
||||||
|
|
||||||
const resultPromise = renderTask.promise.then(
|
|
||||||
async () => {
|
|
||||||
showCanvas?.(true);
|
|
||||||
await this.#finishRenderTask(renderTask);
|
|
||||||
|
|
||||||
|
this.dispatchPageRendered(
|
||||||
|
/* cssTransform */ false,
|
||||||
|
/* isDetailView */ false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
).then(async () => {
|
||||||
this.structTreeLayer ||= new StructTreeLayerBuilder(
|
this.structTreeLayer ||= new StructTreeLayerBuilder(
|
||||||
pdfPage,
|
pdfPage,
|
||||||
viewport.rawDims
|
viewport.rawDims
|
||||||
@ -1174,20 +1094,7 @@ class PDFPageView {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.#renderAnnotationEditorLayer();
|
this.#renderAnnotationEditorLayer();
|
||||||
},
|
});
|
||||||
error => {
|
|
||||||
// When zooming with a `drawingDelay` set, avoid temporarily showing
|
|
||||||
// a black canvas if rendering was cancelled before the `onContinue`-
|
|
||||||
// callback had been invoked at least once.
|
|
||||||
if (!(error instanceof RenderingCancelledException)) {
|
|
||||||
showCanvas?.(true);
|
|
||||||
} else {
|
|
||||||
prevCanvas?.remove();
|
|
||||||
this.#resetCanvas();
|
|
||||||
}
|
|
||||||
return this.#finishRenderTask(renderTask, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pdfPage.isPureXfa) {
|
if (pdfPage.isPureXfa) {
|
||||||
if (!this.xfaLayer) {
|
if (!this.xfaLayer) {
|
||||||
@ -1204,10 +1111,8 @@ class PDFPageView {
|
|||||||
|
|
||||||
div.setAttribute("data-loaded", true);
|
div.setAttribute("data-loaded", true);
|
||||||
|
|
||||||
this.eventBus.dispatch("pagerender", {
|
this.dispatchPageRender();
|
||||||
source: this,
|
|
||||||
pageNumber: this.id,
|
|
||||||
});
|
|
||||||
return resultPromise;
|
return resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -102,15 +102,23 @@ class PDFRenderingQueue {
|
|||||||
* @param {Array} views
|
* @param {Array} views
|
||||||
* @param {boolean} scrolledDown
|
* @param {boolean} scrolledDown
|
||||||
* @param {boolean} [preRenderExtra]
|
* @param {boolean} [preRenderExtra]
|
||||||
|
* @param {boolean} [ignoreDetailViews]
|
||||||
*/
|
*/
|
||||||
getHighestPriority(visible, views, scrolledDown, preRenderExtra = false) {
|
getHighestPriority(
|
||||||
|
visible,
|
||||||
|
views,
|
||||||
|
scrolledDown,
|
||||||
|
preRenderExtra = false,
|
||||||
|
ignoreDetailViews = false
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* The state has changed. Figure out which page has the highest priority to
|
* The state has changed. Figure out which page has the highest priority to
|
||||||
* render next (if any).
|
* render next (if any).
|
||||||
*
|
*
|
||||||
* Priority:
|
* Priority:
|
||||||
* 1. visible pages
|
* 1. visible pages
|
||||||
* 2. if last scrolled down, the page after the visible pages, or
|
* 2. zoomed-in partial views of visible pages, unless `ignoreDetailViews`
|
||||||
|
* 3. if last scrolled down, the page after the visible pages, or
|
||||||
* if last scrolled up, the page before the visible pages
|
* if last scrolled up, the page before the visible pages
|
||||||
*/
|
*/
|
||||||
const visibleViews = visible.views,
|
const visibleViews = visible.views,
|
||||||
@ -125,6 +133,16 @@ class PDFRenderingQueue {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ignoreDetailViews) {
|
||||||
|
for (let i = 0; i < numVisible; i++) {
|
||||||
|
const { detailView } = visibleViews[i].view;
|
||||||
|
if (detailView && !this.isViewFinished(detailView)) {
|
||||||
|
return detailView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const firstId = visible.first.id,
|
const firstId = visible.first.id,
|
||||||
lastId = visible.last.id;
|
lastId = visible.last.id;
|
||||||
|
|
||||||
|
|||||||
@ -292,7 +292,9 @@ class PDFThumbnailViewer {
|
|||||||
const thumbView = this.renderingQueue.getHighestPriority(
|
const thumbView = this.renderingQueue.getHighestPriority(
|
||||||
visibleThumbs,
|
visibleThumbs,
|
||||||
this._thumbnails,
|
this._thumbnails,
|
||||||
scrollAhead
|
scrollAhead,
|
||||||
|
/* preRenderExtra */ false,
|
||||||
|
/* ignoreDetailViews */ true
|
||||||
);
|
);
|
||||||
if (thumbView) {
|
if (thumbView) {
|
||||||
this.#ensurePdfPageLoaded(thumbView).then(() => {
|
this.#ensurePdfPageLoaded(thumbView).then(() => {
|
||||||
|
|||||||
@ -118,6 +118,11 @@ function isValidAnnotationEditorMode(mode) {
|
|||||||
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
||||||
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
|
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
|
||||||
* CSS-only zooming. The default value is 4096 * 8192 (32 mega-pixels).
|
* CSS-only zooming. The default value is 4096 * 8192 (32 mega-pixels).
|
||||||
|
* @property {boolean} [enableDetailCanvas] - When enabled, if the rendered
|
||||||
|
* pages would need a canvas that is larger than `maxCanvasPixels`, it will
|
||||||
|
* draw a second canvas on top of the CSS-zoomed one, that only renders the
|
||||||
|
* part of the page that is close to the viewport. The default value is
|
||||||
|
* `true`.
|
||||||
* @property {IL10n} [l10n] - Localization service.
|
* @property {IL10n} [l10n] - Localization service.
|
||||||
* @property {boolean} [enablePermissions] - Enables PDF document permissions,
|
* @property {boolean} [enablePermissions] - Enables PDF document permissions,
|
||||||
* when they exist. The default value is `false`.
|
* when they exist. The default value is `false`.
|
||||||
@ -236,6 +241,8 @@ class PDFViewer {
|
|||||||
|
|
||||||
#mlManager = null;
|
#mlManager = null;
|
||||||
|
|
||||||
|
#scrollTimeoutId = null;
|
||||||
|
|
||||||
#switchAnnotationEditorModeAC = null;
|
#switchAnnotationEditorModeAC = null;
|
||||||
|
|
||||||
#switchAnnotationEditorModeTimeoutId = null;
|
#switchAnnotationEditorModeTimeoutId = null;
|
||||||
@ -319,6 +326,7 @@ class PDFViewer {
|
|||||||
this.removePageBorders = options.removePageBorders || false;
|
this.removePageBorders = options.removePageBorders || false;
|
||||||
}
|
}
|
||||||
this.maxCanvasPixels = options.maxCanvasPixels;
|
this.maxCanvasPixels = options.maxCanvasPixels;
|
||||||
|
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
||||||
this.l10n = options.l10n;
|
this.l10n = options.l10n;
|
||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||||
this.l10n ||= new GenericL10n();
|
this.l10n ||= new GenericL10n();
|
||||||
@ -873,7 +881,7 @@ class PDFViewer {
|
|||||||
eventBus._on("pagerender", onBeforeDraw, { signal });
|
eventBus._on("pagerender", onBeforeDraw, { signal });
|
||||||
|
|
||||||
const onAfterDraw = evt => {
|
const onAfterDraw = evt => {
|
||||||
if (evt.cssTransform) {
|
if (evt.cssTransform || evt.isDetailView) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._onePageRenderedCapability.resolve({ timestamp: evt.timestamp });
|
this._onePageRenderedCapability.resolve({ timestamp: evt.timestamp });
|
||||||
@ -993,6 +1001,7 @@ class PDFViewer {
|
|||||||
annotationMode,
|
annotationMode,
|
||||||
imageResourcesPath: this.imageResourcesPath,
|
imageResourcesPath: this.imageResourcesPath,
|
||||||
maxCanvasPixels: this.maxCanvasPixels,
|
maxCanvasPixels: this.maxCanvasPixels,
|
||||||
|
enableDetailCanvas: this.enableDetailCanvas,
|
||||||
pageColors,
|
pageColors,
|
||||||
l10n: this.l10n,
|
l10n: this.l10n,
|
||||||
layerProperties: this._layerProperties,
|
layerProperties: this._layerProperties,
|
||||||
@ -1234,6 +1243,15 @@ class PDFViewer {
|
|||||||
if (this.pagesCount === 0) {
|
if (this.pagesCount === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.#scrollTimeoutId) {
|
||||||
|
clearTimeout(this.#scrollTimeoutId);
|
||||||
|
}
|
||||||
|
this.#scrollTimeoutId = setTimeout(() => {
|
||||||
|
this.#scrollTimeoutId = null;
|
||||||
|
this.update();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1672,6 +1690,15 @@ class PDFViewer {
|
|||||||
const newCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * numVisiblePages + 1);
|
const newCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * numVisiblePages + 1);
|
||||||
this.#buffer.resize(newCacheSize, visible.ids);
|
this.#buffer.resize(newCacheSize, visible.ids);
|
||||||
|
|
||||||
|
for (const { view, visibleArea } of visiblePages) {
|
||||||
|
view.updateVisibleArea(visibleArea);
|
||||||
|
}
|
||||||
|
for (const view of this.#buffer) {
|
||||||
|
if (!visible.ids.has(view.id)) {
|
||||||
|
view.updateVisibleArea(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.renderingQueue.renderHighestPriority(visible);
|
this.renderingQueue.renderHighestPriority(visible);
|
||||||
|
|
||||||
const isSimpleLayout =
|
const isSimpleLayout =
|
||||||
@ -1835,11 +1862,21 @@ class PDFViewer {
|
|||||||
this._spreadMode !== SpreadMode.NONE &&
|
this._spreadMode !== SpreadMode.NONE &&
|
||||||
this._scrollMode !== ScrollMode.HORIZONTAL;
|
this._scrollMode !== ScrollMode.HORIZONTAL;
|
||||||
|
|
||||||
|
// If we are scrolling and the rendering of a detail view was just
|
||||||
|
// cancelled, it's because the user is scrolling too quickly and so
|
||||||
|
// we constantly need to re-render a different area.
|
||||||
|
// Don't attempt to re-render it: this will be done once the user
|
||||||
|
// stops scrolling.
|
||||||
|
const ignoreDetailViews =
|
||||||
|
this.#scrollTimeoutId !== null &&
|
||||||
|
visiblePages.views.some(page => page.detailView?.renderingCancelled);
|
||||||
|
|
||||||
const pageView = this.renderingQueue.getHighestPriority(
|
const pageView = this.renderingQueue.getHighestPriority(
|
||||||
visiblePages,
|
visiblePages,
|
||||||
this._pages,
|
this._pages,
|
||||||
scrollAhead,
|
scrollAhead,
|
||||||
preRenderExtra
|
preRenderExtra,
|
||||||
|
ignoreDetailViews
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pageView) {
|
if (pageView) {
|
||||||
@ -2433,6 +2470,10 @@ class PDFViewer {
|
|||||||
clearTimeout(this.#scaleTimeoutId);
|
clearTimeout(this.#scaleTimeoutId);
|
||||||
this.#scaleTimeoutId = null;
|
this.#scaleTimeoutId = null;
|
||||||
}
|
}
|
||||||
|
if (this.#scrollTimeoutId !== null) {
|
||||||
|
clearTimeout(this.#scrollTimeoutId);
|
||||||
|
this.#scrollTimeoutId = null;
|
||||||
|
}
|
||||||
if (!noUpdate) {
|
if (!noUpdate) {
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -554,10 +554,11 @@ function getVisibleElements({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hiddenHeight =
|
const minY = Math.max(0, top - currentHeight);
|
||||||
Math.max(0, top - currentHeight) + Math.max(0, viewBottom - bottom);
|
const minX = Math.max(0, left - currentWidth);
|
||||||
const hiddenWidth =
|
|
||||||
Math.max(0, left - currentWidth) + Math.max(0, viewRight - right);
|
const hiddenHeight = minY + Math.max(0, viewBottom - bottom);
|
||||||
|
const hiddenWidth = minX + Math.max(0, viewRight - right);
|
||||||
|
|
||||||
const fractionHeight = (viewHeight - hiddenHeight) / viewHeight,
|
const fractionHeight = (viewHeight - hiddenHeight) / viewHeight,
|
||||||
fractionWidth = (viewWidth - hiddenWidth) / viewWidth;
|
fractionWidth = (viewWidth - hiddenWidth) / viewWidth;
|
||||||
@ -567,6 +568,18 @@ function getVisibleElements({
|
|||||||
id: view.id,
|
id: view.id,
|
||||||
x: currentWidth,
|
x: currentWidth,
|
||||||
y: currentHeight,
|
y: currentHeight,
|
||||||
|
visibleArea:
|
||||||
|
// We only specify which part of the page is visible when it's not
|
||||||
|
// the full page, as there is no point in handling a partial page
|
||||||
|
// rendering otherwise.
|
||||||
|
percent === 100
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
minX,
|
||||||
|
minY,
|
||||||
|
maxX: Math.min(viewRight, right) - currentWidth,
|
||||||
|
maxY: Math.min(viewBottom, bottom) - currentHeight,
|
||||||
|
},
|
||||||
view,
|
view,
|
||||||
percent,
|
percent,
|
||||||
widthPercent: (fractionWidth * 100) | 0,
|
widthPercent: (fractionWidth * 100) | 0,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user