diff --git a/index.html b/index.html index 8388c4b..f658118 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Vite + Vue + Unilab
diff --git a/package-lock.json b/package-lock.json index 40f80ae..4590f3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "dependencies": { "html2pdf.js": "^0.10.3", + "jspdf": "^3.0.3", + "jspdf-autotable": "^5.0.2", "vue": "^3.5.13", "xlsx": "^0.18.5" }, @@ -790,6 +792,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", @@ -927,18 +935,6 @@ "node": ">=0.8" } }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -948,18 +944,6 @@ "node": ">= 0.6.0" } }, - "node_modules/btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", - "license": "(MIT OR Apache-2.0)", - "bin": { - "btoa": "bin/btoa.js" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/canvg": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", @@ -1116,6 +1100,17 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fdir": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", @@ -1185,15 +1180,20 @@ "jspdf": "^3.0.0" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/jspdf": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz", - "integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz", + "integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.26.7", - "atob": "^2.1.2", - "btoa": "^1.2.1", + "@babel/runtime": "^7.26.9", + "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { @@ -1203,6 +1203,15 @@ "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/jspdf-autotable": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz", + "integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==", + "license": "MIT", + "peerDependencies": { + "jspdf": "^2 || ^3" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -1230,6 +1239,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", diff --git a/package.json b/package.json index da5fa3f..c0bf6b2 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ }, "dependencies": { "html2pdf.js": "^0.10.3", + "jspdf": "^3.0.3", + "jspdf-autotable": "^5.0.2", "vue": "^3.5.13", "xlsx": "^0.18.5" }, diff --git a/src/App.vue b/src/App.vue index dbce8cc..626a5ac 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,7 +1,12 @@ + +`; + + // Create the iframe pointed to a blob URL + const blob = new Blob([html], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const iframe = document.createElement('iframe'); + iframe.style.position = 'fixed'; + iframe.style.right = '0'; + iframe.style.bottom = '0'; + iframe.style.width = '0'; + iframe.style.height = '0'; + iframe.style.border = '0'; + iframe.src = url; + document.body.appendChild(iframe); + + // Cleanup when the iframe signals it printed (or after a timeout) + const onMsg = (ev) => { + if (ev?.data?.__closePrintIframe) finish(); + }; + const timer = setTimeout(finish, 10000); + window.addEventListener('message', onMsg); + + function finish() { + try { + window.removeEventListener('message', onMsg); + clearTimeout(timer); + URL.revokeObjectURL(url); + iframe.remove(); + } catch {} + } +} diff --git a/src/utils/printpdfV0.js b/src/utils/printpdfV0.js new file mode 100644 index 0000000..4c354de --- /dev/null +++ b/src/utils/printpdfV0.js @@ -0,0 +1,65 @@ +async function exportVectorPdf() { + const filename = intestazione.value?.nomefile + ? `${intestazione.value.nomefile}.pdf` + : 'certificato-prova.pdf'; + + await exportReportToPdf({ + filename, + rootSelector: '#report', + intestazione: intestazione.value + }); +} + + +function exportPdf(filename = 'certificato-prova.pdf') { + const opt = { + margin: 0, + filename, + html2canvas: { scale: 4 }, + jsPDF: { unit: 'mm', format: [210, 297], orientation: 'portrait' }, + pagebreak: { mode: ['css','legacy'] } + }; + + html2pdf() + .set(opt) + .from(printable.value) + .toPdf() + .get('pdf') + .then(pdf => { + const totalPages = pdf.internal.getNumberOfPages(); + const pageWidth = pdf.internal.pageSize.getWidth(); + const pageHeight = pdf.internal.pageSize.getHeight(); + + for (let i = 1; i <= totalPages; i++) { + pdf.setPage(i); + pdf.setFontSize(9); + pdf.text( + `Pagina ${i} di ${totalPages}`, + pageWidth / 2, + pageHeight - 8, // 8mm up from bottom + { align: 'center' } + ); + } + }) + .save(); +} + +async function exportPdfNative() { + await nextTick(); // make sure DOM is ready + const root = printable.value; // HTMLElement + if (!root) { console.error('printable is null'); return; } + + const filename = intestazione.value?.nomefile + ? `${intestazione.value.nomefile}.pdf` + : 'certificato-prova.pdf'; + + await printViaIframe({ root, filename }); +} + +async function exportNative(filename = 'certificato-prova.pdf') { + + console.log ('Exporting PDF via native method...'); + // '#report' is the root of what you want to print + printPdfNative(filename.valueOf, { rootSelector: '#report' }); + +} \ No newline at end of file diff --git a/src/utils/printpdfV1.js b/src/utils/printpdfV1.js new file mode 100644 index 0000000..e64e67a --- /dev/null +++ b/src/utils/printpdfV1.js @@ -0,0 +1,90 @@ +import html2pdf from 'html2pdf.js'; + +/** + * Export from the LIVE DOM (no clones) and temporarily enforce + * single-border grid + integer widths to avoid doubled lines. + */ +export async function exportPdfV1({ + root, // HTMLElement (preferred) + rootSelector = '#report', // fallback + filename = 'report.pdf', + a4px = 794, // ~210mm @ 96dpi (integer) + scale = 2, // crisp 1px -> 2 device px + addFooter = true, + lineColor = '#222', +} = {}) { + const el = root instanceof Element ? root : document.querySelector(rootSelector); + if (!el) throw new Error(`exportPdfV1: root not found (${rootSelector})`); + + // Mark the element so our snapshot CSS can target it specifically + el.setAttribute('data-pdf-pass', '1'); + + // Inject temporary CSS that only applies during export + const styleEl = document.createElement('style'); + styleEl.textContent = ` + /* Target ONLY the exporting subtree */ + [data-pdf-pass="1"] { width: 210mm !important; padding-bottom: 16mm !important; } + [data-pdf-pass="1"] * { box-sizing: border-box !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; } + /* Guarantee integer table math & single lines */ + [data-pdf-pass="1"] table { border-collapse: collapse !important; table-layout: fixed !important; width: ${a4px}px !important; } + [data-pdf-pass="1"] th, [data-pdf-pass="1"] td { + border: 0 !important; + border-right: 1px solid ${lineColor} !important; + border-bottom: 1px solid ${lineColor} !important; + padding: 4px !important; + font-size: 12px; color: #000; background: #fff; + } + [data-pdf-pass="1"] tr > *:first-child { border-left: 1px solid ${lineColor} !important; } + [data-pdf-pass="1"] thead tr:first-child > * { border-top: 1px solid ${lineColor} !important; } + /* Reduce row-split ugliness */ + [data-pdf-pass="1"] tr, [data-pdf-pass="1"] th, [data-pdf-pass="1"] td, [data-pdf-pass="1"] img { break-inside: avoid; } + `; + document.head.appendChild(styleEl); + + // Force integer layout width on the container while capturing + const prevWidth = el.style.width; + const prevPadB = el.style.paddingBottom; + el.style.width = `${a4px}px`; + el.style.paddingBottom = '16mm'; + + try { + await html2pdf() + .set({ + margin: 0, + filename, + image: { type: 'png', quality: 1 }, // PNG avoids JPEG halos on lines + html2canvas: { + scale, + useCORS: true, + backgroundColor: '#ffffff', + letterRendering: true, + windowWidth: a4px, // match the integer layout width + scrollX: 0, + scrollY: -window.scrollY, + }, + jsPDF: { unit: 'mm', format: [210, 297], orientation: 'portrait' }, + pagebreak: { mode: ['css'] }, + }) + .from(el) + .toPdf() + .get('pdf') + .then(pdf => { + if (!addFooter) return; + const total = pdf.internal.getNumberOfPages(); + const w = pdf.internal.pageSize.getWidth(); + const h = pdf.internal.pageSize.getHeight(); + pdf.setFontSize(9); + for (let i = 1; i <= total; i++) { + pdf.setPage(i); + pdf.text(`Pagina ${i} di ${total}`, w / 2, h - 8, { align: 'center' }); + } + }) + .save(); + } finally { + // Restore everything + el.style.width = prevWidth; + el.style.paddingBottom = prevPadB; + el.removeAttribute('data-pdf-pass'); + if (styleEl.parentNode) styleEl.parentNode.removeChild(styleEl); + } +} diff --git a/src/utils/printpdfV2.js b/src/utils/printpdfV2.js new file mode 100644 index 0000000..1630e9c --- /dev/null +++ b/src/utils/printpdfV2.js @@ -0,0 +1,55 @@ +// Native print with a custom default filename (no html2pdf). +// Temporarily sets document.title, opens the print dialog, then restores it. +export async function exportPdfV2( + filename = 'report.pdf', + { rootSelector = '#report', printClass = 'printing', preloadImages = true } = {} +) { + const root = document.querySelector(rootSelector) || document.body; + + // 1) (Optional) ensure images & fonts are loaded in the print area + if (preloadImages) { + const imgs = Array.from(root.querySelectorAll('img')).filter(i => !i.complete); + if (imgs.length) { + await Promise.all(imgs.map(i => new Promise(res => { i.onload = res; i.onerror = res; }))); + } + } + try { + if (document.fonts && typeof document.fonts.ready?.then === 'function') { + await document.fonts.ready; + } + } catch { /* ignore */ } + + // 2) Prepare temporary title and print-only class + const originalTitle = document.title; + const titleNoExt = sanitizeForTitle(filename.replace(/\.pdf$/i, '')); + + const cleanup = () => { + document.title = originalTitle; + document.body.classList.remove(printClass); + window.removeEventListener('afterprint', cleanup); + document.removeEventListener('visibilitychange', onVisibilityBack); + }; + + const onVisibilityBack = () => { + if (document.visibilityState === 'visible') setTimeout(cleanup, 0); + }; + + document.body.classList.add(printClass); + document.title = titleNoExt; + window.addEventListener('afterprint', cleanup); + document.addEventListener('visibilitychange', onVisibilityBack); + + // 3) Give the browser a tick to apply the new title, then print + requestAnimationFrame(() => { + setTimeout(() => { + window.print(); + // final safety net in case neither event fires + setTimeout(cleanup, 8000); + }, 0); + }); +} + +function sanitizeForTitle(s) { + // keep it safe for titles / default filenames + return String(s).replace(/[\\/:*?"<>|]+/g, ' ').trim() || 'document'; +}