export pdf nativo

This commit is contained in:
LORENZO\pacio 2025-11-10 18:13:36 +01:00
parent 3beefba72e
commit e574410982
13 changed files with 599 additions and 107 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
<title>Unilab</title>
</head>
<body>
<div id="app"></div>

75
package-lock.json generated
View File

@ -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",

View File

@ -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"
},

View File

@ -1,7 +1,12 @@
<template>
<div>
<button class="pdf-button" @click="exportPdf(intestazione.nomefile ? `${intestazione.nomefile}.pdf` : 'certificato-prova.pdf')">Esporta PDF</button>
<div class="report" ref="printable" contenteditable="true">
<div class="no-print pull-right" style="text-align: right; margin: 10px;">
<button class="pdf-button no-print" @click="exportPdfNative()">Esporta PDF</button>
<!--<button class="pdf-button no-print" @click="exportPdf(intestazione.nomefile ? `${intestazione.nomefile}.pdf` : 'certificato-prova.pdf')">Esporta PDF</button>-->
<!--<button class="pdf-button no-print" @click="exportVectorPdf">Esporta PDF v2</button>
<button class="pdf-button no-print" @click="exportPdfNative">Esporta PDF v3</button>-->
</div>
<div id=report class="report" ref="printable" contenteditable="true">
<header>
<img src="/report_header.png" alt="Intestazione Unilab" class="report-header-image" />
</header>
@ -87,11 +92,9 @@
</template>
<script setup>
import { nextTick } from 'vue';
import { computed } from 'vue';
import { printPdfNative } from './utils/printPdfNative.js';
import { ref, onMounted, watch } from 'vue'
import html2pdf from 'html2pdf.js'
import { exportPdfV1 } from './utils/printpdfV1.js'
import { exportPdfV2 } from './utils/printpdfV2.js';
import { ref, onMounted, watch, nextTick } from 'vue'
import A_CUB from './components/risultati/A_CUB.vue'
import A_CIL from './components/risultati/A_CIL.vue'
import B_BAR from './components/risultati/B_BAR.vue'
@ -184,6 +187,21 @@ async function fetchReportDataWithToken(token, pSERCER) {
return await response.json();
}
async function exportPdf(filename = 'certificato-prova.pdf') {
await exportPdfV1({
root: printable.value, // your ref="printable"
filename
});
}
async function exportPdfNative() {
const name = intestazione?.value?.nomefile
? `${intestazione.value.nomefile}.pdf`
: 'certificato-prova.pdf';
await exportPdfV2(name, { rootSelector: '#report' });
}
function capitalizeAllFields(obj) {
const result = {}
for (const key in obj) {
@ -207,47 +225,6 @@ function capitalizeEachWord(str) {
}
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 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' });
}
function formatDate(dateStr) {
const date = new Date(dateStr)
return date.toLocaleDateString('it-IT')
@ -290,20 +267,20 @@ function calcolaMediaRcPerGruppo(data) {
<style>
* {
/** {
box-sizing: border-box;
}
}*/
body {
font-family: Arial, sans-serif;
padding: 2px;
/*padding: 2px;*/
}
.header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px; /* opzionale, spazio sotto la riga */
margin-bottom: 4px; /* opzionale, spazio sotto la riga */
}
.header-row .section-title {
@ -321,7 +298,7 @@ body {
.report-header-image {
width: 100%;
height: auto;
margin-bottom: 8px;
margin-bottom: 4px;
display: block;
}
@ -385,8 +362,7 @@ td {
background-image: url("/watermark.png");
background-repeat: no-repeat;
background-position: right;
background-clip: content-box;
background-origin: content-box;
background-size: auto 100%;
border: 1px solid black;
margin: 0 auto;
border-collapse: collapse;
@ -423,16 +399,12 @@ td {
}
.pdf-button {
position: fixed;
top: 20px;
right: 20px;
background-color: #007BFF;
color: white;
padding: 10px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
z-index: 999;
}
.pdf-button:hover {
@ -461,11 +433,11 @@ footer .signatures {
}
footer .sign1{
font-size: 1em;
font-size: 0.9em;
}
footer .sign2{
font-size: 0.9em;
font-size: 0.8em;
}
footer .signblock{

View File

@ -1,7 +1,10 @@
/* Page setup */
@page {
size: A4;
margin: 10mm 10mm 15mm 10mm;
@page { size: A4; margin: 10mm 10mm 15mm 10mm; }
@media print {
.no-print { display: none !important; }
thead { display: table-header-group; }
tfoot { display: table-footer-group; }
tr, th, td, img { break-inside: avoid; }
#report * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
/* Optional: hide controls while printing */
body.printing .no-print { display: none !important; }

28
src/assets/snapshot.css Normal file
View File

@ -0,0 +1,28 @@
/* Applied only during the export pass */
.pdf-pass #report .grid-crisp {
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed; /* avoids sub-pixel columns */
width: 794px; /* A4 @ 96dpi → integer px */
}
/* Single-border scheme: only draw right+bottom, then close top/left on edges */
.pdf-pass #report .grid-crisp th,
.pdf-pass #report .grid-crisp td {
border: 0;
border-right: 1px solid #222;
border-bottom: 1px solid #222;
padding: 1px;
box-sizing: border-box;
}
.pdf-pass #report .grid-crisp tr th:first-child,
.pdf-pass #report .grid-crisp tr td:first-child { border-left: 1px solid #222; }
.pdf-pass #report .grid-crisp thead tr:first-child > * { border-top: 1px solid #222; }
/* Optional: tighten note tables without borders */
.pdf-pass #report .note-table td { border: 0 !important; }
/* Remove any table-level border during snapshot to avoid outer double line */
.pdf-pass #report .grid-crisp { border: none !important; }

View File

@ -1,6 +1,7 @@
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
//import './style.css'
import './assets/print.css';
import './assets/snapshot.css';
createApp(App).mount('#app')

View File

@ -1,8 +0,0 @@
body {
font-family: sans-serif;
background: #f3f4f6;
}
h1, h2 {
color: #1f2937;
}

156
src/utils/exportJsPdf.js Normal file
View File

@ -0,0 +1,156 @@
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
/** Fetch an image and return a dataURL */
async function fetchAsDataURL(url) {
const res = await fetch(url, { cache: 'force-cache' });
const blob = await res.blob();
return await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
/** Extract text from a cell (handles nested tags) */
function cellText(td) {
return (td?.textContent || '').trim();
}
/** Build body rows for a 2-col key/value table from DOM rows */
function buildKeyValueBody(tableEl) {
const rows = [];
tableEl.querySelectorAll('tr').forEach(tr => {
const tds = tr.querySelectorAll('td,th');
if (tds.length >= 2) rows.push([cellText(tds[0]), cellText(tds[1])]);
});
return rows;
}
/**
* Export the current report (#report) to a vector PDF.
* @param {Object} opts
* @param {string} opts.filename
* @param {string} opts.rootSelector
* @param {Object} opts.intestazione - optional metadata (intest1, intest2)
* @param {string} opts.headerImageSelector - selector for the header img
*/
export async function exportReportToPdf({
filename = 'report.pdf',
rootSelector = '#report',
intestazione = {},
headerImageSelector = '.report-header-image',
} = {}) {
const doc = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' });
const pageW = doc.internal.pageSize.getWidth();
const pageH = doc.internal.pageSize.getHeight();
const margin = { top: 10, right: 10, bottom: 15, left: 10 };
let cursorY = margin.top;
const root = document.querySelector(rootSelector);
if (!root) throw new Error(`Root not found: ${rootSelector}`);
// 1) Header image (if present)
const headerImg = root.querySelector(headerImageSelector);
if (headerImg && headerImg.getAttribute('src')) {
try {
const dataUrl = await fetchAsDataURL(headerImg.getAttribute('src'));
const targetW = pageW - margin.left - margin.right;
// scale height by aspect ratio (assume original dimensions unknown)
// jsPDF needs explicit width/height; try a 20mm height as default
const targetH = 20;
doc.addImage(dataUrl, 'PNG', margin.left, cursorY, targetW, targetH, undefined, 'FAST');
cursorY += targetH + 4;
} catch (_) {
// ignore header image errors; continue
}
}
// 2) Titles (intestazione)
if (intestazione?.intest1) {
doc.setFont('helvetica', 'bold');
doc.setFontSize(14);
doc.text(String(intestazione.intest1), pageW / 2, cursorY, { align: 'center' });
cursorY += 8;
}
if (intestazione?.modreport) {
doc.setFont('helvetica', 'normal');
doc.setFontSize(10);
doc.text(String(intestazione.modreport), pageW - margin.right, cursorY, { align: 'right' });
}
if (intestazione?.intest2) {
cursorY += 4;
doc.setFont('helvetica', 'bold');
doc.setFontSize(12);
doc.text(String(intestazione.intest2), pageW / 2, cursorY, { align: 'center' });
cursorY += 4;
}
// Helper to ensure we have room or add a page
const ensureSpace = (needed) => {
if (cursorY + needed > pageH - margin.bottom) {
doc.addPage();
cursorY = margin.top;
}
};
// 3) Info table (2-column key/value) as vector, no header row
const infoTableEl = root.querySelector('.info-table');
if (infoTableEl) {
const body = buildKeyValueBody(infoTableEl);
ensureSpace(10);
autoTable(doc, {
startY: cursorY,
head: [], // no header
body,
theme: 'grid',
styles: { font: 'helvetica', fontSize: 9, lineWidth: 0.2, cellPadding: 1 },
headStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0] },
columnStyles: {
0: { cellWidth: 60, halign: 'left', fontStyle: 'bold' },
1: { cellWidth: pageW - margin.left - margin.right - 60, halign: 'left' },
},
margin: { left: margin.left, right: margin.right },
});
cursorY = doc.lastAutoTable.finalY + 4;
}
// 4) Other tables in the report → vector via AutoTable
// - Skips .info-table (already handled)
// - Skips .no-export
// - .note-table exported with theme 'plain'
const tables = Array.from(root.querySelectorAll('table'))
.filter(t => !t.classList.contains('info-table') && !t.classList.contains('no-export'));
for (const t of tables) {
ensureSpace(10);
const isNote = t.classList.contains('note-table');
const theme = isNote ? 'plain' : 'grid';
autoTable(doc, {
html: t, // parse the existing table head/body
startY: cursorY,
theme,
styles: { font: 'helvetica', fontSize: 9, lineWidth: isNote ? 0 : 0.2, cellPadding: 1 },
headStyles: { fillColor: [245, 245, 245], textColor: [0, 0, 0], fontStyle: 'bold' },
bodyStyles: { textColor: [0, 0, 0] },
margin: { left: margin.left, right: margin.right },
});
cursorY = doc.lastAutoTable.finalY + 4;
}
// 5) Footer (page numbers) on every page
const total = doc.getNumberOfPages();
for (let i = 1; i <= total; i++) {
doc.setPage(i);
doc.setFont('helvetica', 'normal');
doc.setFontSize(9);
doc.text(`Pagina ${i} di ${total}`, pageW / 2, pageH - 7, { align: 'center' });
}
// 6) Download with your custom filename
doc.save(filename);
}

113
src/utils/printIframe.js Normal file
View File

@ -0,0 +1,113 @@
// Print the given element using a hidden iframe, setting a custom default
// filename by changing ONLY the iframe's document.title (not the host).
export async function printViaIframe({
root, // HTMLElement to print (required)
filename = 'report.pdf', // default download name
extraCss = '', // optional extra CSS to inject
} = {}) {
if (!(root instanceof Element)) {
throw new Error('printViaIframe: "root" must be an HTMLElement');
}
// Clone the root so we can normalize asset URLs safely
const clone = root.cloneNode(true);
// Normalize <img src> to absolute URLs so they load inside the blob iframe
clone.querySelectorAll('img').forEach(img => {
const src = img.getAttribute('src');
if (!src) return;
try {
// Handle "/", "./", "../", and absolute http(s)
const abs = new URL(src, window.location.origin).href;
img.setAttribute('src', abs);
} catch { /* ignore */ }
});
// Gather existing <link rel="stylesheet"> so the iframe uses the same CSS bundle(s)
const linksHtml = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
.map(l => `<link rel="stylesheet" href="${l.href}">`)
.join('\n');
// Gather inline <style> tags content
const stylesHtml = Array.from(document.querySelectorAll('style'))
.map(s => s.textContent)
.join('\n');
// Minimal print CSS + your extras
const printCss = `
@page { size: A4; margin: 10mm 10mm 15mm 10mm; }
@media print {
.no-print { display: none !important; }
thead { display: table-header-group; }
tfoot { display: table-footer-group; }
tr, th, td, img { break-inside: avoid; }
}
/* Keep colors/borders as on screen */
#report, #report * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
/* Optional crisp table defaults */
#report table { border-collapse: collapse; width: 100%; table-layout: fixed; }
#report th, #report td { border: 1px solid #000; padding: 1px; box-sizing: border-box; }
${extraCss || ''}`;
// Build a standalone HTML for the iframe
const safeTitle = String(filename).replace(/\.pdf$/i, '');
const html = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>${safeTitle}</title>
${linksHtml}
<style>${stylesHtml}\n${printCss}</style>
</head>
<body>
<!-- Wrap in #report so your existing CSS targets still apply -->
<div id="report">${clone.outerHTML}</div>
<script>
(function () {
function readyToPrint() {
var imgs = Array.from(document.images).filter(i => !i.complete);
if (imgs.length === 0) return Promise.resolve();
return Promise.all(imgs.map(i => new Promise(r => { i.onload = r; i.onerror = r; })));
}
readyToPrint().then(function () {
setTimeout(function () {
window.focus();
window.print();
// Tell parent to cleanup after print opens
setTimeout(function () { parent.postMessage({ __closePrintIframe: true }, '*'); }, 100);
}, 0);
});
})();
</script>
</body>
</html>`;
// 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 {}
}
}

65
src/utils/printpdfV0.js Normal file
View File

@ -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' });
}

90
src/utils/printpdfV1.js Normal file
View File

@ -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);
}
}

55
src/utils/printpdfV2.js Normal file
View File

@ -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';
}