Dont revoke blob URLs while printing but do it after

It fixes #19988.

In Firefox, when printing, the document is cloned and use an image cache
which isn't available when there's a service worker.
This commit is contained in:
Calixte Denizet 2026-04-08 14:04:03 +02:00
parent a67b952118
commit ea15ac31ef
2 changed files with 125 additions and 8 deletions

View File

@ -1784,4 +1784,113 @@ describe("PDF viewer", () => {
);
});
});
describe("PDFPrintService", () => {
describe("blob URL revocation (issue #19988)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"basicapi.pdf",
".textLayer .endOfContent",
null,
{
earlySetup: () => {
// Track blob URLs created during the print phase (between
// beforeprint and afterprint).
let trackPrintURLs = false;
window._printBlobURLs = [];
const origCreate = URL.createObjectURL.bind(URL);
URL.createObjectURL = blob => {
const url = origCreate(blob);
if (trackPrintURLs) {
window._printBlobURLs.push(url);
}
return url;
};
// beforeprint fires before renderPages(); start tracking here.
window.addEventListener("beforeprint", () => {
trackPrintURLs = true;
});
// window.print() is called by performPrint() after renderPages()
// completes and all images are loaded into #printContainer.
window.print = () => {
const isFirefox = navigator.userAgent.includes("Firefox");
if (isFirefox) {
// Firefox re-fetches blob URLs when rendering the print
// preview (especially when a service worker is registered).
// Verify the URLs are still accessible at this point.
window._printImagesAccessible = Promise.all(
window._printBlobURLs.map(url =>
fetch(url).then(
() => true,
() => false
)
)
);
} else {
// Chrome uses the cached decoded data already in the <img>
// elements and does not re-fetch blob URLs for printing.
// Just verify the images rendered correctly.
const imgs = document.querySelectorAll("#printContainer img");
window._printImagesAccessible = Promise.resolve(
Array.from(imgs).map(
img => img.complete && img.naturalWidth > 0
)
);
}
};
},
appSetup: app => {
app._testPrintResolver = Promise.withResolvers();
},
eventBusSetup: eventBus => {
eventBus.on(
"afterprint",
() => {
// Wait for the checks initiated in window.print() before
// resolving, so the test can assert on them.
(window._printImagesAccessible ?? Promise.resolve([])).then(
window.PDFViewerApplication._testPrintResolver.resolve
);
},
{ once: true }
);
},
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must keep print image blob URLs accessible until destroy() is called", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitAndClick(page, "#printButton");
// Resolves with an array of booleans, one per print page image.
const accessible = await awaitPromise(
await page.evaluateHandle(() => [
window.PDFViewerApplication._testPrintResolver.promise,
])
);
expect(accessible.length)
.withContext(`In ${browserName}: print pages were rendered`)
.toBeGreaterThan(0);
expect(accessible.every(v => v))
.withContext(
`In ${browserName}: all print images accessible at print time`
)
.toBeTrue();
})
);
});
});
});
});

View File

@ -144,6 +144,12 @@ class PDFPrintService {
this.pageStyleSheet.remove();
this.pageStyleSheet = null;
}
if (this._blobURLs) {
for (const url of this._blobURLs) {
URL.revokeObjectURL(url);
}
this._blobURLs = null;
}
this.scratchCanvas.width = this.scratchCanvas.height = 0;
this.scratchCanvas = null;
activeService = null;
@ -189,7 +195,13 @@ class PDFPrintService {
this.throwIfInactive();
const img = document.createElement("img");
this.scratchCanvas.toBlob(blob => {
img.src = URL.createObjectURL(blob);
const blobURL = URL.createObjectURL(blob);
img.src = blobURL;
// Defer revocation until after printing completes (in destroy()) to avoid
// broken print images in Firefox when a service worker is registered,
// since Firefox re-fetches blob URLs when rendering the print dialog.
// See https://github.com/mozilla/pdf.js/issues/19988
(this._blobURLs ??= []).push(blobURL);
});
const wrapper = document.createElement("div");
@ -201,13 +213,9 @@ class PDFPrintService {
img.onload = resolve;
img.onerror = reject;
promise
.catch(() => {
// Avoid "Uncaught promise" messages in the console.
})
.then(() => {
URL.revokeObjectURL(img.src);
});
promise.catch(() => {
// Avoid "Uncaught promise" messages in the console.
});
return promise;
}