Enhance AI Insight with new state management and PDF export functionality. Added selectedArea state to App for better context in reports. Introduced new markdown files for documentation on AI Insight improvements and setup instructions. Updated package dependencies and fixed TypeScript errors for improved stability.
CI / lint-and-test (push) Canceled after 0s
CI / lint-and-test (push) Canceled after 0s
This commit is contained in:
1 parent
a8b49374f1
commit
2a627a2c3f
84 files changed
+8415
-2692
No files matched your search
@@ -0,0 +1,111 @@
|
||||
import { jsPDF } from 'jspdf';
|
||||
import html2canvas from 'html2canvas';
|
||||
import { revealPdfHeaders } from '../components/shared/PdfReportHeader.tsx';
|
||||
|
||||
/**
|
||||
* Render a card to a multi-page A4 PDF without slicing through its contents.
|
||||
*
|
||||
* The previous approach placed ONE tall image on every page at a shifting
|
||||
* negative offset, so a page break landed wherever 277mm happened to fall —
|
||||
* straight through the middle of a chart. This version cuts the canvas at
|
||||
* declared block boundaries instead: mark the parts that must stay whole with
|
||||
* `data-pdf-block`, and a break is pushed up to the nearest boundary that fits.
|
||||
*
|
||||
* Boundaries are measured inside `onclone`, because html2canvas lays the clone
|
||||
* out in its own iframe at `windowWidth` — offsets taken from the live DOM would
|
||||
* describe a different layout.
|
||||
*/
|
||||
export const exportCardToPdf = async (
|
||||
element: HTMLElement,
|
||||
filename: string,
|
||||
options?: { widthPx?: number }
|
||||
): Promise<void> => {
|
||||
const widthPx = options?.widthPx ?? 800;
|
||||
const scale = 2;
|
||||
/** Offsets, in cloned-CSS pixels, where a page may safely break. */
|
||||
const safeBreaks: number[] = [];
|
||||
|
||||
const canvas = await html2canvas(element, {
|
||||
scale,
|
||||
useCORS: true,
|
||||
backgroundColor: '#ffffff',
|
||||
windowWidth: widthPx,
|
||||
onclone: (_clonedDoc, clonedElement) => {
|
||||
clonedElement.style.width = `${widthPx}px`;
|
||||
clonedElement.style.maxWidth = `${widthPx}px`;
|
||||
clonedElement.style.margin = '0';
|
||||
revealPdfHeaders(clonedElement);
|
||||
|
||||
// Side-by-side small multiples do not fit an A4 column; stack them so
|
||||
// each chart keeps a readable width in print.
|
||||
clonedElement.querySelectorAll<HTMLElement>('[data-pdf-stack]').forEach((node) => {
|
||||
node.style.display = 'block';
|
||||
});
|
||||
|
||||
const top = clonedElement.getBoundingClientRect().top;
|
||||
clonedElement.querySelectorAll<HTMLElement>('[data-pdf-block]').forEach((node) => {
|
||||
const rect = node.getBoundingClientRect();
|
||||
safeBreaks.push(rect.top - top, rect.bottom - top);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
|
||||
const pageWidthMm = doc.internal.pageSize.getWidth();
|
||||
const pageHeightMm = doc.internal.pageSize.getHeight();
|
||||
const marginMm = 10;
|
||||
const contentWidthMm = pageWidthMm - marginMm * 2;
|
||||
const usableHeightMm = pageHeightMm - marginMm * 2;
|
||||
|
||||
const mmPerPx = contentWidthMm / canvas.width;
|
||||
const usableHeightPx = usableHeightMm / mmPerPx;
|
||||
|
||||
// Clone-CSS px -> canvas px, de-duplicated and ordered.
|
||||
const breakPoints = [...new Set(safeBreaks.map((value) => Math.round(value * scale)))]
|
||||
.filter((value) => value > 0 && value < canvas.height)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
let cursor = 0;
|
||||
let firstPage = true;
|
||||
|
||||
while (cursor < canvas.height) {
|
||||
const remaining = canvas.height - cursor;
|
||||
let sliceHeight: number;
|
||||
|
||||
if (remaining <= usableHeightPx) {
|
||||
sliceHeight = remaining;
|
||||
} else {
|
||||
const limit = cursor + usableHeightPx;
|
||||
const candidate = [...breakPoints]
|
||||
.reverse()
|
||||
.find((point) => point > cursor && point <= limit);
|
||||
// No declared boundary fits — the block is taller than a page, so fall
|
||||
// back to a hard cut rather than looping forever.
|
||||
sliceHeight = candidate ? candidate - cursor : usableHeightPx;
|
||||
}
|
||||
|
||||
const slice = document.createElement('canvas');
|
||||
slice.width = canvas.width;
|
||||
slice.height = Math.max(1, Math.round(sliceHeight));
|
||||
const ctx = slice.getContext('2d');
|
||||
if (!ctx) break;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, slice.width, slice.height);
|
||||
ctx.drawImage(canvas, 0, cursor, canvas.width, slice.height, 0, 0, canvas.width, slice.height);
|
||||
|
||||
if (!firstPage) doc.addPage();
|
||||
doc.addImage(
|
||||
slice.toDataURL('image/png'),
|
||||
'PNG',
|
||||
marginMm,
|
||||
marginMm,
|
||||
contentWidthMm,
|
||||
slice.height * mmPerPx
|
||||
);
|
||||
|
||||
firstPage = false;
|
||||
cursor += slice.height;
|
||||
}
|
||||
|
||||
doc.save(filename);
|
||||
};
|
||||
Reference in new issue
Block a user