export pdf nativo
This commit is contained in:
parent
3beefba72e
commit
e574410982
@ -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
75
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
94
src/App.vue
94
src/App.vue
@ -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{
|
||||
|
||||
@ -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
28
src/assets/snapshot.css
Normal 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; }
|
||||
@ -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')
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
color: #1f2937;
|
||||
}
|
||||
156
src/utils/exportJsPdf.js
Normal file
156
src/utils/exportJsPdf.js
Normal 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
113
src/utils/printIframe.js
Normal 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
65
src/utils/printpdfV0.js
Normal 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
90
src/utils/printpdfV1.js
Normal 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
55
src/utils/printpdfV2.js
Normal 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';
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user