104 lines
2.8 KiB
TypeScript
104 lines
2.8 KiB
TypeScript
// Simple PDF generation using a text-to-PDF approach
|
|
// @react-pdf/renderer requires React component rendering which is complex in API routes
|
|
// Using a simpler approach with manual PDF construction
|
|
|
|
export async function generatePdf(
|
|
content: string,
|
|
toolName: string
|
|
): Promise<Buffer> {
|
|
// Simple PDF using minimal PDF spec
|
|
const title = toolName.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
const header = `NGO Toolkit Lab — ${title}`;
|
|
|
|
// Clean content for PDF
|
|
const cleanContent = content
|
|
.replace(/\*\*/g, '')
|
|
.replace(/#{1,3}\s/g, '')
|
|
.replace(/\|/g, ' | ');
|
|
|
|
const lines = cleanContent.split('\n').filter((l) => l.trim());
|
|
|
|
// Build minimal PDF
|
|
const objects: string[] = [];
|
|
let objectCount = 0;
|
|
const offsets: number[] = [];
|
|
|
|
const addObject = (content: string): number => {
|
|
objectCount++;
|
|
offsets.push(0); // placeholder
|
|
objects.push(content);
|
|
return objectCount;
|
|
};
|
|
|
|
// Catalog
|
|
addObject('1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj');
|
|
|
|
// Pages
|
|
addObject('2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj');
|
|
|
|
// Build page content
|
|
let textContent = `BT\n/F1 16 Tf\n50 800 Td\n(${escPdf(header)}) Tj\n`;
|
|
textContent += `/F1 10 Tf\n0 -30 Td\n`;
|
|
|
|
let y = 770;
|
|
for (const line of lines) {
|
|
if (y < 50) break; // Simple single-page for now
|
|
const safeLine = escPdf(line.substring(0, 100));
|
|
textContent += `0 -14 Td\n(${safeLine}) Tj\n`;
|
|
y -= 14;
|
|
}
|
|
|
|
// Footer
|
|
textContent += `0 -30 Td\n/F1 8 Tf\n(Generated by NGO Toolkit Lab) Tj\n`;
|
|
textContent += 'ET';
|
|
|
|
const streamBytes = Buffer.from(textContent, 'utf-8');
|
|
|
|
// Page content stream
|
|
addObject(
|
|
`4 0 obj\n<< /Length ${streamBytes.length} >>\nstream\n${textContent}\nendstream\nendobj`
|
|
);
|
|
|
|
// Font
|
|
addObject('5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj');
|
|
|
|
// Page
|
|
// Replace object 3 — insert at position 2
|
|
objects.splice(
|
|
2,
|
|
0,
|
|
`3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\nendobj`
|
|
);
|
|
offsets.splice(2, 0, 0);
|
|
objectCount = objects.length;
|
|
|
|
// Build PDF
|
|
let pdf = '%PDF-1.4\n';
|
|
for (let i = 0; i < objects.length; i++) {
|
|
offsets[i] = pdf.length;
|
|
pdf += objects[i] + '\n';
|
|
}
|
|
|
|
const xrefOffset = pdf.length;
|
|
pdf += 'xref\n';
|
|
pdf += `0 ${objectCount + 1}\n`;
|
|
pdf += '0000000000 65535 f \n';
|
|
for (let i = 0; i < objectCount; i++) {
|
|
pdf += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`;
|
|
}
|
|
pdf += 'trailer\n';
|
|
pdf += `<< /Size ${objectCount + 1} /Root 1 0 R >>\n`;
|
|
pdf += 'startxref\n';
|
|
pdf += `${xrefOffset}\n`;
|
|
pdf += '%%EOF';
|
|
|
|
return Buffer.from(pdf, 'utf-8');
|
|
}
|
|
|
|
function escPdf(text: string): string {
|
|
return text
|
|
.replace(/\\/g, '\\\\')
|
|
.replace(/\(/g, '\\(')
|
|
.replace(/\)/g, '\\)');
|
|
}
|