File size: 3,907 Bytes
4cc00df | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | /**
* ReportDownloader — exports an inspection result as PNG or PDF.
* Uses html2canvas to capture the DOM node passed via `targetRef`,
* then jsPDF to wrap it into a letter-sized PDF if needed.
*/
import { useRef, useState } from "react";
import html2canvas from "html2canvas";
import { jsPDF } from "jspdf";
import { Download, Image as ImageIcon, FileText, Loader2 } from "lucide-react";
async function captureNode(node, scale = 2) {
return html2canvas(node, {
backgroundColor: "#0A0A0A",
scale,
useCORS: true,
logging: false,
});
}
export default function ReportDownloader({ targetRef, inspectionId, disabled }) {
const [busy, setBusy] = useState(null); // "png" | "pdf" | null
const filename = `forgesight-report-${(inspectionId || "inspection").slice(0, 8)}`;
const downloadPNG = async () => {
if (!targetRef?.current || busy) return;
setBusy("png");
try {
const canvas = await captureNode(targetRef.current, 2);
const link = document.createElement("a");
link.download = `${filename}.png`;
link.href = canvas.toDataURL("image/png");
link.click();
} finally {
setBusy(null);
}
};
const downloadPDF = async () => {
if (!targetRef?.current || busy) return;
setBusy("pdf");
try {
const canvas = await captureNode(targetRef.current, 2);
const imgData = canvas.toDataURL("image/png");
// A4 portrait in mm
const pdf = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
const pageW = pdf.internal.pageSize.getWidth();
const pageH = pdf.internal.pageSize.getHeight();
// Scale image to fit full page width, paginate if tall
const imgW = canvas.width;
const imgH = canvas.height;
const ratio = pageW / (imgW / 2); // /2 because scale=2
const scaledH = (imgH / 2) * ratio;
let yOffset = 0;
let remaining = scaledH;
let page = 0;
while (remaining > 0) {
if (page > 0) pdf.addPage();
const sliceH = Math.min(remaining, pageH);
// sourceY in original canvas pixels
const srcY = page * pageH * (imgW / 2) / pageW;
pdf.addImage(
imgData,
"PNG",
0, 0,
pageW, sliceH,
undefined, "FAST",
0,
);
remaining -= pageH;
page++;
}
// Metadata
pdf.setProperties({
title: `ForgeSight Inspection Report`,
subject: "Automated QC Report — AMD MI300X × Qwen2-VL",
author: "ForgeSight",
creator: "ForgeSight · AMD Developer Hackathon",
});
pdf.save(`${filename}.pdf`);
} finally {
setBusy(null);
}
};
if (disabled) return null;
return (
<div className="flex items-center gap-2">
<span className="fs-label hidden sm:inline">Export</span>
{/* PNG */}
<button
onClick={downloadPNG}
disabled={!!busy}
title="Download as PNG"
className="fs-chip inline-flex items-center gap-1.5 hover:border-white/40 hover:text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
data-testid="download-png-btn"
>
{busy === "png" ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<ImageIcon className="w-3 h-3" />
)}
PNG
</button>
{/* PDF */}
<button
onClick={downloadPDF}
disabled={!!busy}
title="Download as PDF"
className="fs-chip inline-flex items-center gap-1.5 hover:border-white/40 hover:text-white disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
data-testid="download-pdf-btn"
>
{busy === "pdf" ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<FileText className="w-3 h-3" />
)}
PDF
</button>
</div>
);
}
|