Last active
April 24, 2026 00:36
-
-
Save danielrose7/adb604b9334118d98de6e856efd043df to your computer and use it in GitHub Desktop.
Print PDF from blob URL with cross-browser iframe handling + document.title guard for Chrome Save as PDF filename
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * guardPrintTitle — fix Chrome PDF print filename via document.title guard | |
| * | |
| * When printing a PDF inside an iframe, Chrome uses `document.title` — not the | |
| * iframe's own `<title>` — to pre-fill the "Save as PDF" filename. Frameworks | |
| * like React (<Head>), Next.js, or Vue router can reset the title between our | |
| * set and Chrome's read via their own lifecycle/rendering. | |
| * | |
| * This function temporarily sets `document.title` to the desired filename and | |
| * uses a MutationObserver to enforce it against framework resets. Returns a | |
| * cleanup function — call it after the print dialog is dismissed to restore the | |
| * original title and disconnect the observer. | |
| * | |
| * Pipe characters are replaced with dashes since they cause issues in filenames. | |
| * | |
| * @param {string} title - The document title to hold during printing | |
| * @returns {function} cleanup - Call to restore the original title | |
| * | |
| * @example | |
| * const restoreTitle = guardPrintTitle('My Report') | |
| * iframeEl.contentWindow.focus() | |
| * iframeEl.contentWindow.print() | |
| * // Hold title long enough for Chrome to read it (3s is reliable) | |
| * setTimeout(() => restoreTitle(), 3000) | |
| * | |
| * @see https://github.com/crabbly/Print.js/pull/724 | |
| */ | |
| function guardPrintTitle (title) { | |
| var safeTitle = title ? title.replace(/\|/g, '-') : null | |
| if (!safeTitle) return function () {} | |
| var originalTitle = document.title | |
| document.title = safeTitle | |
| var titleEl = document.querySelector('title') | |
| var observer = null | |
| if (titleEl && typeof MutationObserver !== 'undefined') { | |
| observer = new MutationObserver(function () { | |
| if (document.title !== safeTitle) document.title = safeTitle | |
| }) | |
| observer.observe(titleEl, { characterData: true, childList: true, subtree: true }) | |
| } | |
| return function () { | |
| if (observer) observer.disconnect() | |
| document.title = originalTitle | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * printBlobUrl — print a PDF from a blob URL using a hidden iframe | |
| * | |
| * Creates a hidden iframe, loads the PDF blob URL, waits for it to load, | |
| * then triggers the browser print dialog. Handles browser-specific quirks: | |
| * | |
| * - Firefox/Safari: need a visible (but transparent) iframe and extra delay | |
| * for the PDF viewer to initialize before printing | |
| * - Chrome: reads document.title asynchronously after print(), so we hold the | |
| * title override for 3s via guardPrintTitle (see companion gist file) | |
| * - Timeout: if the iframe doesn't load within 10s, rejects with an error | |
| * | |
| * @param {string} blobUrl - A blob: URL pointing to a PDF | |
| * @param {string} [fileName] - Optional filename for "Save as PDF" dialog | |
| * @returns {Promise<void>} | |
| * | |
| * @example | |
| * // From a base64 PDF: | |
| * const base64Pdf = await generatePdfBase64(pdfDoc) | |
| * const blobUrl = URL.createObjectURL( | |
| * new Blob([Uint8Array.from(atob(base64Pdf), c => c.charCodeAt(0))], { type: 'application/pdf' }) | |
| * ) | |
| * try { | |
| * await printBlobUrl(blobUrl, 'Invoice 1234') | |
| * } finally { | |
| * URL.revokeObjectURL(blobUrl) | |
| * } | |
| */ | |
| var IFRAME_LOAD_TIMEOUT_MS = 10000 | |
| var PDF_PRINT_TITLE_HOLD_MS = 3000 | |
| function isFirefox () { | |
| return /firefox/i.test(navigator.userAgent) | |
| } | |
| function isSafari () { | |
| return /^((?!chrome|android).)*safari/i.test(navigator.userAgent) | |
| } | |
| function waitForTimeout (ms) { | |
| return new Promise(function (resolve) { setTimeout(resolve, ms) }) | |
| } | |
| /** Wait for an iframe to fire its load event, with a timeout and error handler. */ | |
| function onceLoaded (iframe) { | |
| return new Promise(function (resolve, reject) { | |
| var timer = setTimeout(function () { | |
| reject(new Error('PDF iframe load timed out')) | |
| }, IFRAME_LOAD_TIMEOUT_MS) | |
| iframe.onload = function () { | |
| clearTimeout(timer) | |
| resolve() | |
| } | |
| iframe.onerror = function () { | |
| clearTimeout(timer) | |
| reject(new Error('PDF iframe failed to load')) | |
| } | |
| }) | |
| } | |
| async function printBlobUrl (blobUrl, fileName) { | |
| var needsVisibleIframe = isFirefox() || isSafari() | |
| var iframe = document.createElement('iframe') | |
| // Firefox/Safari won't render a 0×0 PDF — use a visible-but-transparent iframe instead | |
| iframe.style.cssText = needsVisibleIframe | |
| ? 'width:1px;height:100px;position:fixed;left:0;top:0;opacity:0;border:none;' | |
| : 'position:fixed;right:0;bottom:0;width:0;height:0;border:none;' | |
| document.body.appendChild(iframe) | |
| iframe.src = blobUrl | |
| try { | |
| await onceLoaded(iframe) | |
| // Firefox/Safari need extra time for the PDF viewer to initialize | |
| if (needsVisibleIframe) await waitForTimeout(1000) | |
| var restoreTitle = guardPrintTitle(fileName) | |
| iframe.contentWindow.focus() | |
| iframe.contentWindow.print() | |
| // Firefox/Safari: hide instead of removing immediately | |
| if (needsVisibleIframe) { | |
| iframe.style.visibility = 'hidden' | |
| iframe.style.left = '-1px' | |
| } | |
| // Hold title long enough for Chrome to read it for "Save as PDF" filename | |
| await waitForTimeout(PDF_PRINT_TITLE_HOLD_MS) | |
| restoreTitle() | |
| // Brief delay before cleanup | |
| await waitForTimeout(1000) | |
| } finally { | |
| document.body.removeChild(iframe) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment