ultisuite-client/lib/drive/docs-print.ts
R3D347HR4Y bd9605c853
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(drive): update document printing and PDF export functionality
- Replaced `html2canvas` with `html2canvas-pro` for improved rendering in document capture.
- Enhanced error handling in print functions to provide user feedback on print failures.
- Introduced new `docs-page-capture` module to streamline page capture logic for PDF exports.
- Refactored PDF export process to utilize captured canvases, improving performance and reliability.
- Updated print styles for better document layout during printing and PDF generation.
2026-06-15 17:53:27 +02:00

203 lines
5.2 KiB
TypeScript

import type { DocPageLayout } from "@/lib/drive/doc-page-setup"
import type { DocsExportSnapshot } from "@/lib/drive/docs-export-snapshot"
import { captureDocsPagesFromCanvas } from "@/lib/drive/docs-page-capture"
const PRINT_IFRAME_ID = "docs-print-iframe"
function buildPageRule(pageLayout: DocPageLayout): string {
const wMm = pageLayout.format.widthMm
const hMm = pageLayout.format.heightMm
const landscape = pageLayout.format.widthMm > pageLayout.format.heightMm
const size = landscape ? `${hMm}mm ${wMm}mm` : `${wMm}mm ${hMm}mm`
return `@page { size: ${size}; margin: 0; }`
}
function buildPrintStylesheet(pageLayout: DocPageLayout): string {
const pageWidth = pageLayout.widthPx
const pageHeight = pageLayout.heightPx
return `
${buildPageRule(pageLayout)}
html, body {
margin: 0;
padding: 0;
background: white;
}
.docs-print-page {
width: ${pageWidth}px;
height: ${pageHeight}px;
overflow: hidden;
break-after: page;
page-break-after: always;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
.docs-print-page:last-child {
break-after: auto;
page-break-after: auto;
}
.docs-print-page img {
display: block;
width: 100%;
height: 100%;
}
`
}
function canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {
return new Promise((resolve, reject) => {
canvas.toBlob(
(blob) => {
if (blob) resolve(blob)
else reject(new Error("Impossible de convertir la page en image"))
},
"image/jpeg",
0.92
)
})
}
function removePrintIframe(): void {
document.getElementById(PRINT_IFRAME_ID)?.remove()
}
async function mountPrintDocument(
doc: Document,
canvases: HTMLCanvasElement[],
pageLayout: DocPageLayout
): Promise<() => void> {
const pageWidth = pageLayout.widthPx
const pageHeight = pageLayout.heightPx
const objectUrls: string[] = []
doc.documentElement.lang = "fr"
doc.head.replaceChildren()
doc.body.replaceChildren()
const style = doc.createElement("style")
style.textContent = buildPrintStylesheet(pageLayout)
doc.head.appendChild(style)
for (const canvas of canvases) {
const blob = await canvasToBlob(canvas)
const url = URL.createObjectURL(blob)
objectUrls.push(url)
const page = doc.createElement("div")
page.className = "docs-print-page"
page.style.width = `${pageWidth}px`
page.style.height = `${pageHeight}px`
const img = doc.createElement("img")
img.alt = ""
img.width = pageWidth
img.height = pageHeight
img.src = url
page.appendChild(img)
doc.body.appendChild(page)
await img.decode().catch(() => undefined)
}
return () => {
for (const url of objectUrls) {
URL.revokeObjectURL(url)
}
}
}
async function printPageCanvasesInIframe(
canvases: HTMLCanvasElement[],
pageLayout: DocPageLayout
): Promise<void> {
if (canvases.length === 0) {
throw new Error("Aucune page à imprimer")
}
removePrintIframe()
const iframe = document.createElement("iframe")
iframe.id = PRINT_IFRAME_ID
iframe.setAttribute("aria-hidden", "true")
iframe.style.cssText =
"position:fixed;left:-100000px;top:0;width:1px;height:1px;border:0;opacity:0;pointer-events:none"
const docReady = new Promise<Document>((resolve, reject) => {
iframe.onload = () => {
const doc = iframe.contentDocument
if (doc) resolve(doc)
else reject(new Error("Impossible de préparer la fenêtre d'impression"))
}
iframe.onerror = () => reject(new Error("Impossible de préparer la fenêtre d'impression"))
})
iframe.src = "about:blank"
document.body.appendChild(iframe)
const doc = await docReady
const win = iframe.contentWindow
if (!win) {
removePrintIframe()
throw new Error("Impossible de préparer la fenêtre d'impression")
}
let revokeObjectUrls = () => {}
try {
revokeObjectUrls = await mountPrintDocument(doc, canvases, pageLayout)
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
if (doc.fonts?.ready) {
await doc.fonts.ready
}
await new Promise<void>((resolve, reject) => {
const cleanup = () => {
win.removeEventListener("afterprint", onAfterPrint)
revokeObjectUrls()
removePrintIframe()
}
const onAfterPrint = () => {
cleanup()
resolve()
}
win.addEventListener("afterprint", onAfterPrint)
try {
win.focus()
win.print()
} catch (error) {
cleanup()
reject(error)
return
}
window.setTimeout(() => {
if (document.getElementById(PRINT_IFRAME_ID)) {
cleanup()
resolve()
}
}, 2000)
})
} catch (error) {
revokeObjectUrls()
removePrintIframe()
throw error
}
}
export type DocsPrintMode = "print" | "pdf-capture"
/** @deprecated Live DOM is no longer mutated; kept for PDF export compatibility. */
export async function prepareDocsPrintEnvironment(
_snapshot: DocsExportSnapshot,
_mode: DocsPrintMode = "print"
): Promise<() => void> {
return () => {}
}
export async function printDocsDocument(snapshot: DocsExportSnapshot): Promise<void> {
const canvases = await captureDocsPagesFromCanvas(snapshot, { scale: 2 })
await printPageCanvasesInIframe(canvases, snapshot.pageLayout)
}