Skip to content

Instantly share code, notes, and snippets.

@danielrose7
Last active April 24, 2026 00:36
Show Gist options
  • Select an option

  • Save danielrose7/adb604b9334118d98de6e856efd043df to your computer and use it in GitHub Desktop.

Select an option

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
/**
* 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
}
}
/**
* 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