import jsPDF from 'jspdf'; const W = 792; const H = 612; const MARGIN = 36; const ACCENT = [245, 165, 35]; const DARK = [18, 18, 18]; const HEADER_H = 64; function formatDate(dateStr) { if (!dateStr) return '-'; const d = new Date(`${dateStr}T12:00:00`); return d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); } async function blobToDataUrl(blob) { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (event) => resolve(event.target?.result || null); reader.onerror = () => resolve(null); reader.readAsDataURL(blob); }); } async function resolvePhoto(source) { if (!source) return null; if (source instanceof File || source instanceof Blob) { return blobToDataUrl(source); } if (typeof source === 'string') return source.startsWith('data:') ? source : null; return null; } function getFormat(dataUrl) { return dataUrl?.startsWith('data:image/png') ? 'PNG' : 'JPEG'; } function loadImage(src) { return new Promise((resolve) => { if (!src) return resolve(null); const img = new Image(); img.onload = () => resolve(img); img.onerror = () => resolve(null); img.src = src; }); } const PX_PER_PT = 300 / 72; function toPrintJpeg(dataUrl, img, displayPtW, displayPtH, quality = 0.82) { if (!dataUrl || dataUrl.startsWith('data:image/png')) return dataUrl; const tW = Math.round(displayPtW * PX_PER_PT); const tH = Math.round(displayPtH * PX_PER_PT); if (img.naturalWidth <= tW && img.naturalHeight <= tH) return dataUrl; const canvas = document.createElement('canvas'); canvas.width = tW; canvas.height = tH; canvas.getContext('2d').drawImage(img, 0, 0, tW, tH); return canvas.toDataURL('image/jpeg', quality); } function addHeader(doc, title, pageNum, totalPages) { doc.setFillColor(255, 255, 255); doc.rect(0, 0, W, HEADER_H, 'F'); doc.setFillColor(...ACCENT); doc.rect(0, 0, 4, HEADER_H, 'F'); doc.setDrawColor(220, 220, 220); doc.setLineWidth(0.3); doc.line(0, HEADER_H, W, HEADER_H); doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.setTextColor(...DARK); doc.text(title.toUpperCase(), MARGIN, HEADER_H / 2 + 4); doc.setFontSize(8); doc.setFont('helvetica', 'normal'); doc.setTextColor(140, 140, 140); doc.text(`${pageNum} / ${totalPages}`, W - MARGIN, HEADER_H / 2 + 4, { align: 'right' }); } function drawCoverImage(doc, dataUrl, img, x, y, w, h) { if (!dataUrl || !img) return; const srcAspect = img.naturalWidth / img.naturalHeight; const boxAspect = w / h; let drawW; let drawH; let drawX; let drawY; if (srcAspect > boxAspect) { drawW = w; drawH = w / srcAspect; drawX = x; drawY = y + (h - drawH) / 2; } else { drawH = h; drawW = h * srcAspect; drawX = x + (w - drawW) / 2; drawY = y; } const printableDataUrl = toPrintJpeg(dataUrl, img, drawW, drawH); doc.addImage(printableDataUrl, getFormat(printableDataUrl), drawX, drawY, drawW, drawH); } export async function generateSurveyMakerPdf(data) { const { clientName, siteAddress, surveyDate, preparedBy, projectName, signs } = data; const doc = new jsPDF({ orientation: 'landscape', format: 'letter', unit: 'pt', compress: true }); const signPhotos = await Promise.all((signs || []).map(async (sign) => { const mainDataUrl = await resolvePhoto(sign.mainPhoto); const contextDataUrls = await Promise.all([ resolvePhoto(sign.contextPhoto1), resolvePhoto(sign.contextPhoto2), resolvePhoto(sign.contextPhoto3), ]); const mainImage = mainDataUrl ? await loadImage(mainDataUrl) : null; const contextImages = await Promise.all(contextDataUrls.map(async (dataUrl) => (dataUrl ? loadImage(dataUrl) : null))); return { mainDataUrl, mainImage, contextDataUrls, contextImages }; })); const totalPages = Math.max(1, 1 + signs.length); doc.setFillColor(255, 255, 255); doc.rect(0, 0, W, H, 'F'); doc.setFillColor(...ACCENT); doc.rect(0, 0, W, 5, 'F'); doc.rect(0, H - 5, W, 5, 'F'); doc.setFontSize(30); doc.setFont('helvetica', 'bold'); doc.setTextColor(...DARK); doc.text(projectName || 'Sign Survey', MARGIN, 88); const details = [ ['Client', clientName || '-'], ['Site Address', siteAddress || '-'], ['Survey Date', formatDate(surveyDate)], ['Prepared By', preparedBy || '-'], ['Sign Count', String(signs.length)], ]; let y = 148; details.forEach(([label, value]) => { doc.setFontSize(9); doc.setFont('helvetica', 'bold'); doc.setTextColor(150, 150, 150); doc.text(label.toUpperCase(), MARGIN, y); y += 16; doc.setFontSize(14); doc.setFont('helvetica', 'normal'); doc.setTextColor(40, 40, 40); const lines = doc.splitTextToSize(value, 320); doc.text(lines, MARGIN, y); y += lines.length * 16 + 18; }); if (signPhotos[0]?.mainDataUrl && signPhotos[0]?.mainImage) { const photoX = 410; const photoY = 110; const photoW = W - photoX - MARGIN; const photoH = 360; drawCoverImage(doc, signPhotos[0].mainDataUrl, signPhotos[0].mainImage, photoX, photoY, photoW, photoH); } doc.setFontSize(8); doc.setTextColor(140, 140, 140); doc.text('1 / ' + totalPages, W - MARGIN, H - 18, { align: 'right' }); for (let index = 0; index < signs.length; index += 1) { const sign = signs[index]; const photo = signPhotos[index]; doc.addPage(); addHeader(doc, sign.signName || `Sign ${index + 1}`, index + 2, totalPages); const top = HEADER_H + 20; const leftW = 270; const rightX = MARGIN + leftW + 20; const rightW = W - rightX - MARGIN; const mainPhotoH = 280; const contextGap = 10; const contextPhotoY = top + mainPhotoH + 12; const contextPhotoW = (rightW - contextGap * 2) / 3; const contextPhotoH = H - contextPhotoY - 36; doc.setFontSize(18); doc.setFont('helvetica', 'bold'); doc.setTextColor(...DARK); doc.text(sign.signName || `Sign ${index + 1}`, MARGIN, top + 10); let textY = top + 42; const blocks = [ ['Measurements', sign.measurements || '-'], ['Notes', sign.notes || '-'], ]; blocks.forEach(([label, value]) => { doc.setFontSize(9); doc.setFont('helvetica', 'bold'); doc.setTextColor(150, 150, 150); doc.text(label.toUpperCase(), MARGIN, textY); textY += 16; doc.setFontSize(11); doc.setFont('helvetica', 'normal'); doc.setTextColor(50, 50, 50); const lines = doc.splitTextToSize(value, leftW); doc.text(lines, MARGIN, textY); textY += lines.length * 14 + 24; }); if (photo?.mainDataUrl && photo?.mainImage) { drawCoverImage(doc, photo.mainDataUrl, photo.mainImage, rightX, top, rightW, mainPhotoH); } else { doc.setFontSize(10); doc.setTextColor(170, 170, 170); doc.text('No Main Photo', rightX + rightW / 2, top + mainPhotoH / 2, { align: 'center' }); } for (let contextIndex = 0; contextIndex < 3; contextIndex += 1) { const x = rightX + contextIndex * (contextPhotoW + contextGap); const contextDataUrl = photo?.contextDataUrls?.[contextIndex]; const contextImage = photo?.contextImages?.[contextIndex]; if (contextDataUrl && contextImage) { drawCoverImage(doc, contextDataUrl, contextImage, x, contextPhotoY, contextPhotoW, contextPhotoH); } else { doc.setFontSize(9); doc.setTextColor(180, 180, 180); doc.text(`Context ${contextIndex + 1}`, x + contextPhotoW / 2, contextPhotoY + contextPhotoH / 2, { align: 'center' }); } } } const safe = (value) => (value || '').replace(/[^a-zA-Z0-9 ]/g, '').trim().replace(/\s+/g, ' '); const filename = [safe(projectName || 'Survey'), safe(clientName), formatDate(surveyDate).replace(/[^a-zA-Z0-9]/g, '')].filter(Boolean).join('.'); doc.save(`${filename || 'SurveyMaker'}.pdf`); }