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,17 +3232,27 @@ class PDFObjects {
|
||||
class RenderTask {
|
||||
#internalRenderTask = null;
|
||||
|
||||
/**
|
||||
* Callback for incremental rendering -- a function that will be called
|
||||
* each time the rendering is paused. To continue rendering call the
|
||||
* function that is the first argument to the callback.
|
||||
* @type {function}
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Callback for incremental rendering -- a function that will be called
|
||||
* each time the rendering is paused. To continue rendering call the
|
||||
* function that is the first argument to the callback.
|
||||
* @type {function}
|
||||
*/
|
||||
this.onContinue = null;
|
||||
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
// For testing purposes.
|
||||
Object.defineProperty(this, "getOperatorList", {
|
||||
@ -3399,13 +3409,13 @@ class InternalRenderTask {
|
||||
}
|
||||
InternalRenderTask.#canvasInUse.delete(this._canvas);
|
||||
|
||||
this.callback(
|
||||
error ||
|
||||
new RenderingCancelledException(
|
||||
`Rendering cancelled, page ${this._pageIndex + 1}`,
|
||||
extraDelay
|
||||
)
|
||||
error ||= new RenderingCancelledException(
|
||||
`Rendering cancelled, page ${this._pageIndex + 1}`,
|
||||
extraDelay
|
||||
);
|
||||
this.callback(error);
|
||||
|
||||
this.task.onError?.(error);
|
||||
}
|
||||
|
||||
operatorListChanged() {
|
||||
|
||||
@ -17,11 +17,15 @@ import os from "os";
|
||||
|
||||
const isMac = os.platform() === "darwin";
|
||||
|
||||
function loadAndWait(filename, selector, zoom, setups, options) {
|
||||
function loadAndWait(filename, selector, zoom, setups, options, viewport) {
|
||||
return Promise.all(
|
||||
global.integrationSessions.map(async session => {
|
||||
const page = await session.browser.newPage();
|
||||
|
||||
if (viewport) {
|
||||
await page.setViewport(viewport);
|
||||
}
|
||||
|
||||
// In order to avoid errors because of checks which depend on
|
||||
// a locale.
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
@ -566,8 +570,12 @@ function waitForAnnotationModeChanged(page) {
|
||||
|
||||
function waitForPageRendered(page) {
|
||||
return createPromise(page, resolve => {
|
||||
window.PDFViewerApplication.eventBus.on("pagerendered", resolve, {
|
||||
once: true,
|
||||
const { eventBus } = window.PDFViewerApplication;
|
||||
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_2.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 &&
|
||||
viewBottom > scrollTop
|
||||
) {
|
||||
const hiddenHeight =
|
||||
Math.max(0, scrollTop - viewTop) +
|
||||
Math.max(0, viewBottom - scrollBottom);
|
||||
const hiddenWidth =
|
||||
Math.max(0, scrollLeft - viewLeft) +
|
||||
Math.max(0, viewRight - scrollRight);
|
||||
const minY = Math.max(0, scrollTop - viewTop);
|
||||
const minX = Math.max(0, scrollLeft - viewLeft);
|
||||
|
||||
const hiddenHeight = minY + Math.max(0, viewBottom - scrollBottom);
|
||||
const hiddenWidth = minX + Math.max(0, viewRight - scrollRight);
|
||||
|
||||
const fractionHeight =
|
||||
(div.clientHeight - hiddenHeight) / div.clientHeight;
|
||||
@ -292,12 +291,23 @@ describe("ui_utils", function () {
|
||||
(div.clientWidth - hiddenWidth) / div.clientWidth;
|
||||
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({
|
||||
id: view.id,
|
||||
x: viewLeft,
|
||||
y: viewTop,
|
||||
view,
|
||||
percent,
|
||||
visibleArea,
|
||||
widthPercent: (fractionWidth * 100) | 0,
|
||||
});
|
||||
ids.add(view.id);
|
||||
|
||||
@ -507,6 +507,7 @@ const PDFViewerApplication = {
|
||||
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
||||
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
||||
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
||||
enableDetailCanvas: AppOptions.get("enableDetailCanvas"),
|
||||
enablePermissions: AppOptions.get("enablePermissions"),
|
||||
pageColors,
|
||||
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,
|
||||
// ensure that the page number input loading indicator is hidden.
|
||||
if (pageNumber === this.page) {
|
||||
@ -2339,7 +2340,7 @@ function onPageRendered({ pageNumber, error }) {
|
||||
}
|
||||
|
||||
// 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 thumbnailView = this.pdfThumbnailViewer?.getThumbnail(
|
||||
/* index = */ pageNumber - 1
|
||||
|
||||
@ -200,6 +200,11 @@ const defaultOptions = {
|
||||
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("MOZCENTRAL"),
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableDetailCanvas: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
enableGuessAltText: {
|
||||
/** @type {boolean} */
|
||||
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,
|
||||
OutputScale,
|
||||
PixelsPerInch,
|
||||
RenderingCancelledException,
|
||||
setLayerDimensions,
|
||||
shadow,
|
||||
} from "pdfjs-lib";
|
||||
@ -44,8 +43,10 @@ import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.
|
||||
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
||||
import { AppOptions } from "./app_options.js";
|
||||
import { Autolinker } from "./autolinker.js";
|
||||
import { BasePDFPageView } from "./base_pdf_page_view.js";
|
||||
import { DrawLayerBuilder } from "./draw_layer_builder.js";
|
||||
import { GenericL10n } from "web-null_l10n";
|
||||
import { PDFPageDetailView } from "./pdf_page_detail_view.js";
|
||||
import { SimpleLinkService } from "./pdf_link_service.js";
|
||||
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.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
|
||||
* 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).
|
||||
* @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
|
||||
* with user defined ones in order to improve readability in high contrast
|
||||
* mode.
|
||||
@ -116,13 +123,11 @@ const LAYERS_ORDER = new Map([
|
||||
/**
|
||||
* @implements {IRenderableView}
|
||||
*/
|
||||
class PDFPageView {
|
||||
class PDFPageView extends BasePDFPageView {
|
||||
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
||||
|
||||
#canvasWrapper = null;
|
||||
|
||||
#enableHWA = false;
|
||||
|
||||
#enableAutoLinking = false;
|
||||
|
||||
#hasRestrictedScaling = false;
|
||||
@ -131,7 +136,7 @@ class PDFPageView {
|
||||
|
||||
#layerProperties = null;
|
||||
|
||||
#loadingId = null;
|
||||
#needsRestrictedScaling = false;
|
||||
|
||||
#originalViewport = null;
|
||||
|
||||
@ -141,10 +146,6 @@ class PDFPageView {
|
||||
|
||||
#scaleRoundY = 1;
|
||||
|
||||
#renderError = null;
|
||||
|
||||
#renderingState = RenderingStates.INITIAL;
|
||||
|
||||
#textLayerMode = TextLayerMode.ENABLE;
|
||||
|
||||
#userUnit = 1;
|
||||
@ -161,10 +162,11 @@ class PDFPageView {
|
||||
* @param {PDFPageViewOptions} options
|
||||
*/
|
||||
constructor(options) {
|
||||
super(options);
|
||||
|
||||
const container = options.container;
|
||||
const defaultViewport = options.defaultViewport;
|
||||
|
||||
this.id = options.id;
|
||||
this.renderingId = "page" + this.id;
|
||||
this.#layerProperties = options.layerProperties || DEFAULT_LAYER_PROPERTIES;
|
||||
|
||||
@ -180,21 +182,16 @@ class PDFPageView {
|
||||
this.#annotationMode =
|
||||
options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
|
||||
this.imageResourcesPath = options.imageResourcesPath || "";
|
||||
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
||||
this.maxCanvasPixels =
|
||||
options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
|
||||
this.pageColors = options.pageColors || null;
|
||||
this.#enableHWA = options.enableHWA || false;
|
||||
this.#enableAutoLinking = options.enableAutoLinking || false;
|
||||
|
||||
this.eventBus = options.eventBus;
|
||||
this.renderingQueue = options.renderingQueue;
|
||||
this.l10n = options.l10n;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
this.l10n ||= new GenericL10n();
|
||||
}
|
||||
|
||||
this.renderTask = null;
|
||||
this.resume = null;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
this._isStandalone = !this.renderingQueue?.hasViewer();
|
||||
this._container = container;
|
||||
@ -209,6 +206,8 @@ class PDFPageView {
|
||||
this.structTreeLayer = null;
|
||||
this.drawLayer = null;
|
||||
|
||||
this.detailView = null;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "page";
|
||||
div.setAttribute("data-page-number", this.id);
|
||||
@ -278,43 +277,6 @@ class PDFPageView {
|
||||
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() {
|
||||
const { div, viewport } = this;
|
||||
|
||||
@ -558,14 +520,8 @@ class PDFPageView {
|
||||
}
|
||||
}
|
||||
|
||||
#resetCanvas() {
|
||||
const { canvas } = this;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
canvas.remove();
|
||||
canvas.width = canvas.height = 0;
|
||||
this.canvas = null;
|
||||
_resetCanvas() {
|
||||
super._resetCanvas();
|
||||
this.#originalViewport = null;
|
||||
}
|
||||
|
||||
@ -575,6 +531,7 @@ class PDFPageView {
|
||||
keepXfaLayer = false,
|
||||
keepTextLayer = false,
|
||||
keepCanvasWrapper = false,
|
||||
preserveDetailViewState = false,
|
||||
} = {}) {
|
||||
this.cancelRendering({
|
||||
keepAnnotationLayer,
|
||||
@ -632,7 +589,18 @@ class PDFPageView {
|
||||
|
||||
if (!keepCanvasWrapper && this.#canvasWrapper) {
|
||||
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
|
||||
* @property {number} [scale] The new scale, if specified.
|
||||
@ -709,22 +693,11 @@ class PDFPageView {
|
||||
this._container?.style.setProperty("--scale-factor", this.viewport.scale);
|
||||
}
|
||||
|
||||
this.#computeScale();
|
||||
|
||||
if (this.canvas) {
|
||||
let onlyCssZoom = false;
|
||||
if (this.#hasRestrictedScaling) {
|
||||
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 onlyCssZoom =
|
||||
this.#hasRestrictedScaling && this.#needsRestrictedScaling;
|
||||
const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000;
|
||||
|
||||
if (postponeDrawing || onlyCssZoom) {
|
||||
@ -758,18 +731,16 @@ class PDFPageView {
|
||||
hideTextLayer: postponeDrawing,
|
||||
});
|
||||
|
||||
if (postponeDrawing) {
|
||||
// The "pagerendered"-event will be dispatched once the actual
|
||||
// rendering is done, hence don't dispatch it here as well.
|
||||
return;
|
||||
// The "pagerendered"-event will be dispatched once the actual
|
||||
// rendering is done, hence don't dispatch it here as well.
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -780,7 +751,38 @@ class PDFPageView {
|
||||
keepXfaLayer: true,
|
||||
keepTextLayer: 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,
|
||||
cancelExtraDelay = 0,
|
||||
} = {}) {
|
||||
if (this.renderTask) {
|
||||
this.renderTask.cancel(cancelExtraDelay);
|
||||
this.renderTask = null;
|
||||
}
|
||||
this.resume = null;
|
||||
super.cancelRendering({ cancelExtraDelay });
|
||||
|
||||
if (this.textLayer && (!keepTextLayer || !this.textLayer.div)) {
|
||||
this.textLayer.cancel();
|
||||
@ -897,37 +895,29 @@ class PDFPageView {
|
||||
return this.viewport.convertToPdfPoint(x, y);
|
||||
}
|
||||
|
||||
async #finishRenderTask(renderTask, 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;
|
||||
// Wrap the canvas so that if it has a CSS transform for high DPI the
|
||||
// overflow will be hidden in Firefox.
|
||||
_ensureCanvasWrapper() {
|
||||
let canvasWrapper = this.#canvasWrapper;
|
||||
if (!canvasWrapper) {
|
||||
canvasWrapper = this.#canvasWrapper = document.createElement("div");
|
||||
canvasWrapper.classList.add("canvasWrapper");
|
||||
this.#addLayer(canvasWrapper, "canvasWrapper");
|
||||
}
|
||||
return canvasWrapper;
|
||||
}
|
||||
|
||||
if (error instanceof RenderingCancelledException) {
|
||||
this.#renderError = null;
|
||||
return;
|
||||
}
|
||||
this.#renderError = error;
|
||||
|
||||
this.renderingState = RenderingStates.FINISHED;
|
||||
|
||||
// Ensure that the thumbnails won't become partially (or fully) blank,
|
||||
// for documents that contain interactive form elements.
|
||||
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;
|
||||
}
|
||||
_getRenderingContext(canvasContext, transform) {
|
||||
return {
|
||||
canvasContext,
|
||||
transform,
|
||||
viewport: this.viewport,
|
||||
annotationMode: this.#annotationMode,
|
||||
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
||||
annotationCanvasMap: this._annotationCanvasMap,
|
||||
pageColors: this.pageColors,
|
||||
isEditing: this.#isEditing,
|
||||
};
|
||||
}
|
||||
|
||||
async draw() {
|
||||
@ -935,7 +925,7 @@ class PDFPageView {
|
||||
console.error("Must be in new state before drawing");
|
||||
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) {
|
||||
this.renderingState = RenderingStates.FINISHED;
|
||||
@ -944,14 +934,7 @@ class PDFPageView {
|
||||
|
||||
this.renderingState = RenderingStates.RUNNING;
|
||||
|
||||
// Wrap the canvas so that if it has a CSS transform for high DPI the
|
||||
// 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");
|
||||
}
|
||||
const canvasWrapper = this._ensureCanvasWrapper();
|
||||
|
||||
if (
|
||||
!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 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;
|
||||
|
||||
let 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.
|
||||
// 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 { canvas, prevCanvas, ctx } = this._createCanvas(newCanvas => {
|
||||
// Always inject the canvas as the first element in the wrapper.
|
||||
canvasWrapper.prepend(newCanvas);
|
||||
});
|
||||
const outputScale = (this.outputScale = new OutputScale());
|
||||
canvas.setAttribute("role", "presentation");
|
||||
|
||||
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.#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;
|
||||
}
|
||||
if (!this.outputScale) {
|
||||
this.#computeScale();
|
||||
}
|
||||
const { outputScale } = this;
|
||||
this.#hasRestrictedScaling = this.#needsRestrictedScaling;
|
||||
|
||||
const sfx = approximateFraction(outputScale.sx);
|
||||
const sfy = approximateFraction(outputScale.sy);
|
||||
|
||||
@ -1116,78 +1036,65 @@ class PDFPageView {
|
||||
const transform = outputScale.scaled
|
||||
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
|
||||
: null;
|
||||
const renderContext = {
|
||||
canvasContext: ctx,
|
||||
transform,
|
||||
viewport,
|
||||
annotationMode: this.#annotationMode,
|
||||
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
||||
annotationCanvasMap: this._annotationCanvasMap,
|
||||
pageColors,
|
||||
isEditing: this.#isEditing,
|
||||
};
|
||||
const renderTask = (this.renderTask = pdfPage.render(renderContext));
|
||||
renderTask.onContinue = renderContinueCallback;
|
||||
|
||||
const resultPromise = renderTask.promise.then(
|
||||
async () => {
|
||||
showCanvas?.(true);
|
||||
await this.#finishRenderTask(renderTask);
|
||||
|
||||
this.structTreeLayer ||= new StructTreeLayerBuilder(
|
||||
pdfPage,
|
||||
viewport.rawDims
|
||||
);
|
||||
|
||||
const textLayerPromise = this.#renderTextLayer();
|
||||
|
||||
if (this.annotationLayer) {
|
||||
await this.#renderAnnotationLayer();
|
||||
|
||||
if (this.#enableAutoLinking && this.annotationLayer) {
|
||||
await this.#injectLinkAnnotations(textLayerPromise);
|
||||
}
|
||||
}
|
||||
|
||||
const { annotationEditorUIManager } = this.#layerProperties;
|
||||
|
||||
if (!annotationEditorUIManager) {
|
||||
return;
|
||||
}
|
||||
this.drawLayer ||= new DrawLayerBuilder({
|
||||
pageIndex: this.id,
|
||||
});
|
||||
await this.#renderDrawLayer();
|
||||
this.drawLayer.setParent(canvasWrapper);
|
||||
|
||||
this.annotationEditorLayer ||= new AnnotationEditorLayerBuilder({
|
||||
uiManager: annotationEditorUIManager,
|
||||
pdfPage,
|
||||
l10n,
|
||||
structTreeLayer: this.structTreeLayer,
|
||||
accessibilityManager: this._accessibilityManager,
|
||||
annotationLayer: this.annotationLayer?.annotationLayer,
|
||||
textLayer: this.textLayer,
|
||||
drawLayer: this.drawLayer.getDrawLayer(),
|
||||
onAppend: annotationEditorLayerDiv => {
|
||||
this.#addLayer(annotationEditorLayerDiv, "annotationEditorLayer");
|
||||
},
|
||||
});
|
||||
this.#renderAnnotationEditorLayer();
|
||||
const resultPromise = this._drawCanvas(
|
||||
this._getRenderingContext(ctx, transform),
|
||||
() => {
|
||||
prevCanvas?.remove();
|
||||
this._resetCanvas();
|
||||
},
|
||||
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);
|
||||
renderTask => {
|
||||
// Ensure that the thumbnails won't become partially (or fully) blank,
|
||||
// for documents that contain interactive form elements.
|
||||
this.#useThumbnailCanvas.regularAnnotations =
|
||||
!renderTask.separateAnnots;
|
||||
|
||||
this.dispatchPageRendered(
|
||||
/* cssTransform */ false,
|
||||
/* isDetailView */ false
|
||||
);
|
||||
}
|
||||
);
|
||||
).then(async () => {
|
||||
this.structTreeLayer ||= new StructTreeLayerBuilder(
|
||||
pdfPage,
|
||||
viewport.rawDims
|
||||
);
|
||||
|
||||
const textLayerPromise = this.#renderTextLayer();
|
||||
|
||||
if (this.annotationLayer) {
|
||||
await this.#renderAnnotationLayer();
|
||||
|
||||
if (this.#enableAutoLinking && this.annotationLayer) {
|
||||
await this.#injectLinkAnnotations(textLayerPromise);
|
||||
}
|
||||
}
|
||||
|
||||
const { annotationEditorUIManager } = this.#layerProperties;
|
||||
|
||||
if (!annotationEditorUIManager) {
|
||||
return;
|
||||
}
|
||||
this.drawLayer ||= new DrawLayerBuilder({
|
||||
pageIndex: this.id,
|
||||
});
|
||||
await this.#renderDrawLayer();
|
||||
this.drawLayer.setParent(canvasWrapper);
|
||||
|
||||
this.annotationEditorLayer ||= new AnnotationEditorLayerBuilder({
|
||||
uiManager: annotationEditorUIManager,
|
||||
pdfPage,
|
||||
l10n,
|
||||
structTreeLayer: this.structTreeLayer,
|
||||
accessibilityManager: this._accessibilityManager,
|
||||
annotationLayer: this.annotationLayer?.annotationLayer,
|
||||
textLayer: this.textLayer,
|
||||
drawLayer: this.drawLayer.getDrawLayer(),
|
||||
onAppend: annotationEditorLayerDiv => {
|
||||
this.#addLayer(annotationEditorLayerDiv, "annotationEditorLayer");
|
||||
},
|
||||
});
|
||||
this.#renderAnnotationEditorLayer();
|
||||
});
|
||||
|
||||
if (pdfPage.isPureXfa) {
|
||||
if (!this.xfaLayer) {
|
||||
@ -1204,10 +1111,8 @@ class PDFPageView {
|
||||
|
||||
div.setAttribute("data-loaded", true);
|
||||
|
||||
this.eventBus.dispatch("pagerender", {
|
||||
source: this,
|
||||
pageNumber: this.id,
|
||||
});
|
||||
this.dispatchPageRender();
|
||||
|
||||
return resultPromise;
|
||||
}
|
||||
|
||||
|
||||
@ -102,15 +102,23 @@ class PDFRenderingQueue {
|
||||
* @param {Array} views
|
||||
* @param {boolean} scrolledDown
|
||||
* @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
|
||||
* render next (if any).
|
||||
*
|
||||
* Priority:
|
||||
* 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
|
||||
*/
|
||||
const visibleViews = visible.views,
|
||||
@ -125,6 +133,16 @@ class PDFRenderingQueue {
|
||||
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,
|
||||
lastId = visible.last.id;
|
||||
|
||||
|
||||
@ -292,7 +292,9 @@ class PDFThumbnailViewer {
|
||||
const thumbView = this.renderingQueue.getHighestPriority(
|
||||
visibleThumbs,
|
||||
this._thumbnails,
|
||||
scrollAhead
|
||||
scrollAhead,
|
||||
/* preRenderExtra */ false,
|
||||
/* ignoreDetailViews */ true
|
||||
);
|
||||
if (thumbView) {
|
||||
this.#ensurePdfPageLoaded(thumbView).then(() => {
|
||||
|
||||
@ -118,6 +118,11 @@ function isValidAnnotationEditorMode(mode) {
|
||||
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
||||
* 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).
|
||||
* @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 {boolean} [enablePermissions] - Enables PDF document permissions,
|
||||
* when they exist. The default value is `false`.
|
||||
@ -236,6 +241,8 @@ class PDFViewer {
|
||||
|
||||
#mlManager = null;
|
||||
|
||||
#scrollTimeoutId = null;
|
||||
|
||||
#switchAnnotationEditorModeAC = null;
|
||||
|
||||
#switchAnnotationEditorModeTimeoutId = null;
|
||||
@ -319,6 +326,7 @@ class PDFViewer {
|
||||
this.removePageBorders = options.removePageBorders || false;
|
||||
}
|
||||
this.maxCanvasPixels = options.maxCanvasPixels;
|
||||
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
||||
this.l10n = options.l10n;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
this.l10n ||= new GenericL10n();
|
||||
@ -873,7 +881,7 @@ class PDFViewer {
|
||||
eventBus._on("pagerender", onBeforeDraw, { signal });
|
||||
|
||||
const onAfterDraw = evt => {
|
||||
if (evt.cssTransform) {
|
||||
if (evt.cssTransform || evt.isDetailView) {
|
||||
return;
|
||||
}
|
||||
this._onePageRenderedCapability.resolve({ timestamp: evt.timestamp });
|
||||
@ -993,6 +1001,7 @@ class PDFViewer {
|
||||
annotationMode,
|
||||
imageResourcesPath: this.imageResourcesPath,
|
||||
maxCanvasPixels: this.maxCanvasPixels,
|
||||
enableDetailCanvas: this.enableDetailCanvas,
|
||||
pageColors,
|
||||
l10n: this.l10n,
|
||||
layerProperties: this._layerProperties,
|
||||
@ -1234,6 +1243,15 @@ class PDFViewer {
|
||||
if (this.pagesCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#scrollTimeoutId) {
|
||||
clearTimeout(this.#scrollTimeoutId);
|
||||
}
|
||||
this.#scrollTimeoutId = setTimeout(() => {
|
||||
this.#scrollTimeoutId = null;
|
||||
this.update();
|
||||
}, 100);
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -1672,6 +1690,15 @@ class PDFViewer {
|
||||
const newCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * numVisiblePages + 1);
|
||||
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);
|
||||
|
||||
const isSimpleLayout =
|
||||
@ -1835,11 +1862,21 @@ class PDFViewer {
|
||||
this._spreadMode !== SpreadMode.NONE &&
|
||||
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(
|
||||
visiblePages,
|
||||
this._pages,
|
||||
scrollAhead,
|
||||
preRenderExtra
|
||||
preRenderExtra,
|
||||
ignoreDetailViews
|
||||
);
|
||||
|
||||
if (pageView) {
|
||||
@ -2433,6 +2470,10 @@ class PDFViewer {
|
||||
clearTimeout(this.#scaleTimeoutId);
|
||||
this.#scaleTimeoutId = null;
|
||||
}
|
||||
if (this.#scrollTimeoutId !== null) {
|
||||
clearTimeout(this.#scrollTimeoutId);
|
||||
this.#scrollTimeoutId = null;
|
||||
}
|
||||
if (!noUpdate) {
|
||||
this.update();
|
||||
}
|
||||
|
||||
@ -554,10 +554,11 @@ function getVisibleElements({
|
||||
continue;
|
||||
}
|
||||
|
||||
const hiddenHeight =
|
||||
Math.max(0, top - currentHeight) + Math.max(0, viewBottom - bottom);
|
||||
const hiddenWidth =
|
||||
Math.max(0, left - currentWidth) + Math.max(0, viewRight - right);
|
||||
const minY = Math.max(0, top - currentHeight);
|
||||
const minX = Math.max(0, left - currentWidth);
|
||||
|
||||
const hiddenHeight = minY + Math.max(0, viewBottom - bottom);
|
||||
const hiddenWidth = minX + Math.max(0, viewRight - right);
|
||||
|
||||
const fractionHeight = (viewHeight - hiddenHeight) / viewHeight,
|
||||
fractionWidth = (viewWidth - hiddenWidth) / viewWidth;
|
||||
@ -567,6 +568,18 @@ function getVisibleElements({
|
||||
id: view.id,
|
||||
x: currentWidth,
|
||||
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,
|
||||
percent,
|
||||
widthPercent: (fractionWidth * 100) | 0,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user