diff --git a/test/integration/viewer_spec.mjs b/test/integration/viewer_spec.mjs index 8125a6128..deb059ef1 100644 --- a/test/integration/viewer_spec.mjs +++ b/test/integration/viewer_spec.mjs @@ -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 + // 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(); + }) + ); + }); + }); + }); }); diff --git a/web/pdf_print_service.js b/web/pdf_print_service.js index e6ecbd363..8d50f0f1f 100644 --- a/web/pdf_print_service.js +++ b/web/pdf_print_service.js @@ -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; }