/* * Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl * Source-Available Code / Dual-Licensed. * * Free for non-commercial and evaluation use under terms of BSL/GPLv3. * Commercial or production use requires a valid paid license. * See LICENSE file in repository root for details. */ import { t } from "@rustpad/i18n"; import { toast } from "@rustpad/toast"; import { loadHighlight, loadMermaid } from "@rustpad/vendor-libs"; const STORAGE_PREFIX = "rustpad:pdf-export:"; const PAGE_FORMATS = Object.freeze({ "a4-portrait": { cssSize: "A4 portrait", widthMm: 210, heightMm: 297 }, "a4-landscape": { cssSize: "A4 landscape", widthMm: 297, heightMm: 210 }, "a3-portrait": { cssSize: "A3 portrait", widthMm: 297, heightMm: 420 }, "a3-landscape": { cssSize: "A3 landscape", widthMm: 420, heightMm: 297 }, "a5-portrait": { cssSize: "A5 portrait", widthMm: 148, heightMm: 210 }, "letter-portrait": { cssSize: "letter portrait", widthMm: 215.9, heightMm: 279.4 }, "letter-landscape": { cssSize: "letter landscape", widthMm: 279.4, heightMm: 215.9 }, "screen-16-9": { cssSize: "338.667mm 190.5mm", widthMm: 338.667, heightMm: 190.5 }, "screen-4-3": { cssSize: "254mm 190.5mm", widthMm: 254, heightMm: 190.5 }, }); function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function saved(key, fallback) { try { return localStorage.getItem(`${STORAGE_PREFIX}${key}`) || fallback; } catch { return fallback; } } function save(key, value) { try { localStorage.setItem(`${STORAGE_PREFIX}${key}`, String(value)); } catch { /* optional preference */ } } function stylesheetLinks() { return [...document.querySelectorAll('link[rel="stylesheet"][href]')] .map(link => ``) .join(""); } async function ensureSyntaxHighlight(root) { const nodes = [...root.querySelectorAll('pre code[class*="language-"]:not(.language-mermaid)')]; if (!nodes.length) return; let hljs; try { hljs = await loadHighlight(); } catch { return; } const highlightText = (text, language) => { try { return language ? hljs.highlight(text, { language, ignoreIllegals: true }).value : hljs.highlightAuto(text).value; } catch { return hljs.highlightAuto(text).value; } }; nodes.forEach(node => { const language = [...node.classList].find(name => name.startsWith("language-"))?.slice(9); const lines = [...node.querySelectorAll(".code-line")]; if (lines.length) { lines.forEach(line => { if (line.querySelector('[class*="hljs-"]')) return; line.innerHTML = highlightText(line.textContent || "", language); }); node.classList.add("hljs"); return; } if (!node.querySelector('[class*="hljs-"]')) { node.innerHTML = highlightText(node.textContent || "", language); } node.classList.add("hljs"); }); } function mmToPx(doc) { const probe = doc.createElement("div"); probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;width:100mm;height:100mm;"; doc.body.appendChild(probe); const pxPerMm = probe.getBoundingClientRect().height / 100; probe.remove(); return pxPerMm; } function nextFrame(view) { return new Promise(resolve => view.requestAnimationFrame(() => resolve())); } async function waitForPrintableLayout(doc) { try { await doc.fonts?.ready; } catch { /* print with available fonts */ } const images = [...doc.images]; if (images.length) { const imageReady = Promise.all(images.map(image => { if (image.complete) { try { return image.decode?.() || Promise.resolve(); } catch { return Promise.resolve(); } } return new Promise(resolve => { image.addEventListener("load", resolve, { once: true }); image.addEventListener("error", resolve, { once: true }); }); })); await Promise.race([ imageReady, new Promise(resolve => setTimeout(resolve, 1500)), ]); } await nextFrame(doc.defaultView); await nextFrame(doc.defaultView); } function isBlankPrintLine(node) { if (!(node instanceof Element) || !node.classList.contains("preview-source-line")) return false; if (node.textContent.trim()) return false; return node.children.length === 1 && node.firstElementChild?.tagName === "BR"; } function collapseHeadingGaps(root) { root.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach(heading => { let next = heading.nextElementSibling; while (isBlankPrintLine(next)) { const remove = next; next = next.nextElementSibling; remove.remove(); } }); } function directPrintBlock(root, node) { let block = node; while (block?.parentElement && block.parentElement !== root) block = block.parentElement; return block?.parentElement === root ? block : null; } function shortLeadBlock(node) { if (!node) return false; if (node.matches("h1, h2, h3, h4, h5, h6")) return true; if (!node.matches("p")) return false; const text = node.textContent.trim(); return text.length > 0 && text.length <= 320; } function previousNonBlank(node) { let current = node?.previousElementSibling || null; while (isBlankPrintLine(current)) current = current.previousElementSibling; return current; } function groupAtomicVisuals(root) { const blocks = new Set(); root.querySelectorAll(".mermaid, .rustpad-media").forEach(node => { const block = directPrintBlock(root, node); if (block) blocks.add(block); }); root.querySelectorAll("img").forEach(node => { const block = directPrintBlock(root, node); if (block && !block.textContent.trim()) blocks.add(block); }); for (const block of blocks) { if (block.parentElement !== root) continue; block.classList.add("pdf-atomic-visual"); let first = block; const lead = previousNonBlank(block); if (shortLeadBlock(lead)) { first = lead; if (lead.matches("p")) { const heading = previousNonBlank(lead); if (heading?.matches("h1, h2, h3, h4, h5, h6")) first = heading; } } if (first === block) continue; const wrapper = document.createElement("div"); wrapper.className = "pdf-keep-unit"; root.insertBefore(wrapper, first); let current = first; while (current) { const next = current.nextElementSibling; if (isBlankPrintLine(current)) current.remove(); else wrapper.appendChild(current); if (current === block) break; current = next; } } } function preparePrintPagination(root) { collapseHeadingGaps(root); groupAtomicVisuals(root); } async function ensurePrintableMermaid(root) { const nodes = [...root.querySelectorAll(".mermaid[data-mermaid-source]")]; if (!nodes.length) return; const originals = nodes.map(node => ({ node, html: node.innerHTML, processed: node.getAttribute("data-processed"), })); const staging = document.createElement("div"); staging.setAttribute("aria-hidden", "true"); staging.style.cssText = "position:fixed;left:-200vw;top:0;width:1200px;visibility:hidden;pointer-events:none;z-index:-1"; try { const mermaid = await loadMermaid(); nodes.forEach(node => { node.removeAttribute("data-processed"); node.textContent = node.dataset.mermaidSource || ""; }); staging.appendChild(root); document.body.appendChild(staging); mermaid.initialize({ startOnLoad: false, theme: "default", securityLevel: "strict" }); await mermaid.run({ nodes }); } catch { originals.forEach(({ node, html, processed }) => { node.innerHTML = html; if (processed === null) node.removeAttribute("data-processed"); else node.setAttribute("data-processed", processed); }); } finally { if (root.parentElement === staging) staging.removeChild(root); staging.remove(); } } function measureLastPageFill(doc, documentRoot, pageContentHeight) { const content = documentRoot.querySelector(":scope > .pdf-content"); if (!content) return null; const width = documentRoot.getBoundingClientRect().width; if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(pageContentHeight) || pageContentHeight <= 0) return null; const probe = doc.createElement("div"); probe.setAttribute("aria-hidden", "true"); probe.style.cssText = [ "position:absolute", "visibility:hidden", "pointer-events:none", "top:0", `left:${-(Math.ceil(width) + 200)}px`, `width:${width}px`, `height:${pageContentHeight}px`, `column-width:${width}px`, "column-gap:0", "column-fill:auto", "overflow:visible", "box-sizing:border-box", "z-index:-1", ].join(";"); const title = documentRoot.querySelector(":scope > .pdf-document-title"); if (title) probe.appendChild(title.cloneNode(true)); const flow = content.cloneNode(true); flow.querySelectorAll(".pdf-page-break").forEach(pageBreak => { pageBreak.style.breakAfter = "column"; pageBreak.style.pageBreakAfter = "auto"; }); probe.appendChild(flow); const sentinel = doc.createElement("span"); sentinel.style.cssText = "display:inline-block;width:1px;height:1px;margin:0;padding:0;border:0;vertical-align:top;"; probe.appendChild(sentinel); doc.body.appendChild(probe); const probeRect = probe.getBoundingClientRect(); const sentinelRect = sentinel.getBoundingClientRect(); const fill = sentinelRect.bottom - probeRect.top; probe.remove(); if (!Number.isFinite(fill) || fill < 0 || fill > pageContentHeight + 4) return null; return Math.max(0, Math.min(pageContentHeight, fill)); } function fallbackLastPageFill(documentRoot, spacer, pageContentHeight) { const rootTop = documentRoot.getBoundingClientRect().top; const contentEnd = Math.max(0, spacer.getBoundingClientRect().top - rootTop); return ((contentEnd % pageContentHeight) + pageContentHeight) % pageContentHeight; } function alignFooterToLastPage(doc, pageHeightMm, marginMm) { const documentRoot = doc.querySelector(".pdf-document"); const spacer = doc.querySelector(".pdf-footer-spacer"); const footer = doc.querySelector(".pdf-generated-footer"); if (!documentRoot || !spacer || !footer) return; spacer.style.height = "0px"; footer.style.removeProperty("margin-top"); const pxPerMm = mmToPx(doc); const pageContentHeight = (Number(pageHeightMm) - (2 * Number(marginMm))) * pxPerMm; const footerHeight = footer.getBoundingClientRect().height; const footerMarginTop = Number.parseFloat(doc.defaultView?.getComputedStyle(footer).marginTop || "0") || 0; const bottomGuard = 0.75 * pxPerMm; if (!Number.isFinite(pageContentHeight) || pageContentHeight <= footerHeight || footerHeight <= 0) return; const measuredFill = measureLastPageFill(doc, documentRoot, pageContentHeight); const positionOnPage = measuredFill ?? fallbackLastPageFill(documentRoot, spacer, pageContentHeight); const available = Math.max(0, pageContentHeight - positionOnPage); let actualMarginTop = footerMarginTop; let spacerHeight; if (available >= footerHeight + bottomGuard) { actualMarginTop = Math.min( footerMarginTop, Math.max(0, available - footerHeight - bottomGuard), ); spacerHeight = available - footerHeight - actualMarginTop - bottomGuard; } else { spacerHeight = available + pageContentHeight - footerHeight - actualMarginTop - bottomGuard; } footer.style.marginTop = `${Math.max(0, actualMarginTop)}px`; spacer.style.height = `${Math.max(0, spacerHeight)}px`; } function preparePreview(preview) { const clone = preview.cloneNode(true); clone.removeAttribute("id"); clone.classList.remove("preview"); clone.classList.add("pdf-content"); const computed = getComputedStyle(preview); clone.style.fontFamily = computed.fontFamily; clone.style.fontSize = computed.fontSize; clone.style.lineHeight = computed.lineHeight; clone.querySelectorAll('[contenteditable="true"]').forEach(node => node.removeAttribute("contenteditable")); clone.querySelectorAll(".image-alias-tools, .image-alias-resize-handle").forEach(node => node.remove()); clone.querySelectorAll("details").forEach(node => { node.open = true; const replacement = document.createElement("section"); [...node.attributes].forEach(attribute => replacement.setAttribute(attribute.name, attribute.value)); replacement.setAttribute("open", ""); while (node.firstChild) replacement.appendChild(node.firstChild); node.replaceWith(replacement); }); clone.querySelectorAll("input, button, select, textarea").forEach(node => { node.tabIndex = -1; }); if (clone.classList.contains("preview--raw")) { clone.querySelectorAll(".preview-source-line").forEach(line => { if (!/^\[PAGEBREAK\]$/i.test(line.textContent.trim())) return; const pageBreak = document.createElement("div"); pageBreak.className = "pdf-page-break"; pageBreak.setAttribute("aria-hidden", "true"); line.replaceWith(pageBreak); }); } clone.querySelectorAll("hr.page-break-marker, hr[data-page-break='true']").forEach(marker => { const pageBreak = document.createElement("div"); pageBreak.className = "pdf-page-break"; pageBreak.setAttribute("aria-hidden", "true"); marker.replaceWith(pageBreak); }); preparePrintPagination(clone); return clone; } function printStyles(cssSize, pageWidthMm, pageHeightMm, marginMm, preserveColors) { const pageContentWidthMm = Math.max(20, Number(pageWidthMm) - (2 * Number(marginMm))); const pageContentHeightMm = Math.max(20, Number(pageHeightMm) - (2 * Number(marginMm))); const atomicMaxHeightMm = Math.max(20, pageContentHeightMm - 20); const mediaMaxWidthMm = atomicMaxHeightMm * (16 / 9); return ` @page { size: ${cssSize}; margin: ${marginMm}mm; } html { color-scheme: light; background: #fff !important; } body { width: ${pageContentWidthMm}mm; max-width: ${pageContentWidthMm}mm; margin: 0 !important; background: #fff !important; color: #16181d !important; } .pdf-document { width: ${pageContentWidthMm}mm; box-sizing: border-box; } .pdf-document-title { margin: 0 0 1.2em; padding: 0 0 .45em; border-bottom: 1px solid #d7dce2; } .pdf-document-title h1 { margin: 0; font: 700 1.7rem/1.18 Inter, ui-sans-serif, system-ui, sans-serif; color: #111318; } .pdf-footer-spacer { display: block; width: 100%; height: 0; margin: 0; padding: 0; border: 0; } .pdf-generated-footer { box-sizing: border-box; margin-top: 8mm; padding-top: 1.5mm; border-top: .2mm solid #e1e4e8; color: #9299a3; font: 500 7.5pt/1.2 Inter, ui-sans-serif, system-ui, sans-serif; letter-spacing: .01em; text-align: center; break-inside: avoid-page; page-break-inside: avoid; } .pdf-content { width: 100%; min-height: 0; margin: 0; padding: 0 !important; overflow: visible !important; color: #16181d !important; box-sizing: border-box; } .pdf-content .preview-source-line::before { display: none !important; content: none !important; } .pdf-content .preview-source-line { min-height: 0; position: static !important; } .pdf-content .preview-editable:hover, .pdf-content .preview-editable:focus { background: transparent !important; box-shadow: none !important; } .pdf-content a { color: inherit !important; text-decoration: underline; } /* Book-like pagination: keep small semantic units together, not whole sections. */ /* * Do not rely on break-after: avoid for the main Markdown headings. * Chromium can first paint the heading at the end of the old page and * then move it to the next page, leaving a clipped strip of glyphs behind. * A small invisible reserve inside the heading keeps one following line * with it without triggering that fragmentation bug. */ .pdf-content h1, .pdf-content h2, .pdf-content h3 { break-after: auto; page-break-after: auto; padding-bottom: 1.15em; margin-bottom: calc(.18em - 1.15em); } .pdf-content h4, .pdf-content h5, .pdf-content h6, .pdf-content .markdown-details > summary, .pdf-content dt { break-after: avoid-page; page-break-after: avoid; } .pdf-content h1, .pdf-content h2, .pdf-content h3, .pdf-content h4, .pdf-content h5, .pdf-content h6, .pdf-content blockquote, .pdf-content dd, .pdf-content .footnotes li { break-inside: avoid-page; page-break-inside: avoid; } .pdf-content p, .pdf-content li, .pdf-content dd { orphans: 3; widows: 3; } .pdf-content ul, .pdf-content ol, .pdf-content dl, .pdf-content .table-wrap, .pdf-content table, .pdf-content .markdown-alert, .pdf-content .markdown-details, .pdf-content .markdown-details__content, .pdf-content .footnotes, .pdf-content pre { break-inside: auto; page-break-inside: auto; } .pdf-content li { break-inside: avoid-page; page-break-inside: avoid; } .pdf-content li:has(> ul), .pdf-content li:has(> ol) { break-inside: auto; page-break-inside: auto; } .pdf-content .table-wrap { overflow: visible !important; } .pdf-content table { width: 100% !important; } .pdf-content thead { display: table-header-group; } .pdf-content tfoot { display: table-footer-group; } .pdf-content tr { break-inside: avoid-page; page-break-inside: avoid; } .pdf-content pre { overflow: visible !important; white-space: pre-wrap !important; orphans: 3; widows: 3; box-decoration-break: clone; -webkit-box-decoration-break: clone; } .pdf-content pre code, .pdf-content pre.code-with-lines .code-line { white-space: pre-wrap !important; overflow-wrap: anywhere; word-break: break-word; } .pdf-content pre.code-with-lines .code-line { break-inside: avoid-page; page-break-inside: avoid; } .pdf-content .markdown-alert, .pdf-content .markdown-details { box-decoration-break: clone; -webkit-box-decoration-break: clone; } .pdf-content .pdf-keep-unit { /* * Chromium can leave a 1-2 px fragment of an avoided block on the * previous page before moving the real block to the next page. * Making the keep unit an atomic inline-block prevents that ghost * fragment while preserving book-like pagination. */ display: inline-block; width: 100%; vertical-align: top; box-sizing: border-box; break-inside: avoid-page; page-break-inside: avoid; } .pdf-content .pdf-keep-unit > :first-child { margin-top: 0 !important; } .pdf-content .pdf-keep-unit > :last-child { margin-bottom: 0 !important; } .pdf-content .pdf-atomic-visual { display: inline-block; width: 100%; vertical-align: top; box-sizing: border-box; break-before: avoid-page; page-break-before: avoid; } /* Atomic visual items are scaled to the printable page instead of being fragmented. */ .pdf-content img { max-width: 100% !important; max-height: ${atomicMaxHeightMm}mm !important; width: auto; height: auto !important; object-fit: contain; break-inside: avoid-page; page-break-inside: avoid; } .pdf-content .markdown-alias-image--sized { height: auto !important; aspect-ratio: auto !important; } .pdf-content .markdown-alias-image--sized > img { width: auto !important; max-width: 100% !important; height: auto !important; } .pdf-content .mermaid { overflow: hidden !important; break-inside: avoid-page; page-break-inside: avoid; } .pdf-content .mermaid svg { display: block; width: auto !important; max-width: 100% !important; max-height: ${atomicMaxHeightMm}mm !important; height: auto !important; margin-inline: auto; } .pdf-content .rustpad-media { width: min(100%, ${mediaMaxWidthMm}mm) !important; max-width: 100% !important; margin-inline: auto !important; box-shadow: none !important; break-inside: avoid-page; page-break-inside: avoid; } .pdf-content .rustpad-media__player, .pdf-content .rustpad-media__player iframe, .pdf-content .rustpad-media__player video, .pdf-content video.rustpad-media__player { max-height: ${atomicMaxHeightMm}mm !important; } ${preserveColors ? ` .pdf-document, .pdf-document * { print-color-adjust: exact !important; -webkit-print-color-adjust: exact !important; } .pdf-content a { color: #5d50a5 !important; } .pdf-content code:not(pre code) { background: #eeeaf7 !important; border-color: #bcb2a6 !important; color: #403a33 !important; } .pdf-content pre { background: #efeae2 !important; border-color: #e2dcd3 !important; color: #413b34 !important; } .pdf-content pre code, .pdf-content .hljs { background: transparent !important; color: #403a33 !important; } .pdf-content .hljs-keyword, .pdf-content .hljs-selector-tag, .pdf-content .hljs-literal, .pdf-content .hljs-section, .pdf-content .hljs-link { color: #7046a5 !important; } .pdf-content .hljs-string, .pdf-content .hljs-attr, .pdf-content .hljs-regexp, .pdf-content .hljs-template-tag, .pdf-content .hljs-template-variable { color: #36704a !important; } .pdf-content .hljs-number, .pdf-content .hljs-symbol, .pdf-content .hljs-bullet { color: #a54c25 !important; } .pdf-content .hljs-title, .pdf-content .hljs-title.class_, .pdf-content .hljs-title.function_, .pdf-content .hljs-built_in, .pdf-content .hljs-type { color: #3568b8 !important; } .pdf-content .hljs-variable, .pdf-content .hljs-selector-id, .pdf-content .hljs-selector-class, .pdf-content .hljs-property, .pdf-content .hljs-attribute { color: #9b3443 !important; } .pdf-content .hljs-comment, .pdf-content .hljs-quote { color: #756c61 !important; font-style: italic; } .pdf-content .hljs-meta { color: #7e5d1d !important; } .pdf-content blockquote { border-left-color: #6859b5 !important; color: #625a50 !important; } .pdf-content mark { background: #f4e7be !important; color: #624700 !important; } .pdf-content th { background: #f5f1ea !important; color: #29251f !important; } .pdf-content .markdown-alert--success { border-color: #4c8064 !important; background: rgba(61, 121, 88, .10) !important; } .pdf-content .markdown-alert--info { border-color: #56759c !important; background: rgba(68, 103, 148, .09) !important; } .pdf-content .markdown-alert--warning { border-color: #a27831 !important; background: rgba(155, 112, 35, .11) !important; } .pdf-content .markdown-alert--danger { border-color: #a54b55 !important; background: rgba(155, 52, 67, .09) !important; } .pdf-content .task-checkbox { accent-color: #6859b5 !important; } ` : ""} .pdf-page-break { display: block; width: 100%; height: 0; margin: 0; padding: 0; border: 0; break-after: page; page-break-after: always; } `; } function openPrintWindow({ preview, title, formatKey, marginMm, includeTitle, preserveColors }) { const format = PAGE_FORMATS[formatKey] || PAGE_FORMATS["a4-portrait"]; const safeMargin = [8, 12, 20].includes(Number(marginMm)) ? Number(marginMm) : 12; const printWindow = window.open("", "_blank"); if (!printWindow) throw new Error(t("editor.pdfPopupBlocked", {}, "The PDF window was blocked by the browser.")); try { printWindow.opener = null; } catch { /* same-origin hardening is best effort */ } const content = preparePreview(preview); const titleBlock = includeTitle && title ? `

${escapeHtml(title)}

` : ""; const lang = escapeHtml(document.documentElement.lang || "en"); const base = escapeHtml(`${location.origin}/`); const css = printStyles(format.cssSize, format.widthMm, format.heightMm, safeMargin, preserveColors); const footerText = escapeHtml(t("editor.pdfGeneratedFooter", {}, "Generated by RustPad")); return (async () => { await ensurePrintableMermaid(content); if (preserveColors) await ensureSyntaxHighlight(content); printWindow.document.open(); printWindow.document.write(`${escapeHtml(title || "RustPad")}${stylesheetLinks()}
${titleBlock}${content.outerHTML}
`); printWindow.document.close(); const runPrint = async () => { await waitForPrintableLayout(printWindow.document); alignFooterToLastPage(printWindow.document, format.heightMm, safeMargin); await nextFrame(printWindow); alignFooterToLastPage(printWindow.document, format.heightMm, safeMargin); await nextFrame(printWindow); printWindow.focus(); printWindow.print(); }; printWindow.addEventListener("afterprint", () => printWindow.close(), { once: true }); if (printWindow.document.readyState === "complete") await runPrint(); else await new Promise(resolve => { printWindow.addEventListener("load", async () => { await runPrint(); resolve(); }, { once: true }); }); })().catch(error => { try { printWindow.close(); } catch { /* best effort */ } throw error; }); } export function bindPdfExport({ button, dialog, form, preview, title }) { if (!button || !dialog || !form || !preview) return; const formatSelect = form.querySelector("#pdf-page-format"); const marginSelect = form.querySelector("#pdf-page-margin"); const includeTitle = form.querySelector("#pdf-include-title"); const preserveColors = form.querySelector("#pdf-preserve-colors"); const cancelButtons = form.querySelectorAll("[data-pdf-cancel]"); const savedFormat = saved("format", "a4-portrait"); formatSelect.value = Object.hasOwn(PAGE_FORMATS, savedFormat) ? savedFormat : "a4-portrait"; marginSelect.value = ["8", "12", "20"].includes(saved("margin", "12")) ? saved("margin", "12") : "12"; includeTitle.checked = saved("include-title", "off") === "on"; if (preserveColors) preserveColors.checked = saved("preserve-colors", "on") !== "off"; button.addEventListener("click", () => dialog.showModal()); cancelButtons.forEach(cancel => cancel.addEventListener("click", () => dialog.close("cancel"))); dialog.addEventListener("click", event => { if (event.target === dialog) dialog.close("cancel"); }); form.addEventListener("submit", event => { event.preventDefault(); const formatKey = formatSelect.value; const marginMm = Number(marginSelect.value); save("format", formatKey); save("margin", marginMm); save("include-title", includeTitle.checked ? "on" : "off"); save("preserve-colors", preserveColors?.checked === false ? "off" : "on"); try { const printJob = openPrintWindow({ preview, title: typeof title === "function" ? title() : String(title || ""), formatKey, marginMm, includeTitle: includeTitle.checked, preserveColors: preserveColors?.checked !== false, }); dialog.close("confirm"); void printJob.catch(error => { toast.danger(error?.message || t("editor.pdfExportFailed", {}, "Could not prepare the PDF."), { title: t("editor.exportPdf", {}, "Export PDF"), }); }); } catch (error) { toast.danger(error?.message || t("editor.pdfExportFailed", {}, "Could not prepare the PDF."), { title: t("editor.exportPdf", {}, "Export PDF"), }); } }); }