Files
ngo-toolkit/lib/exports/pdf.ts
T
2026-03-18 18:26:24 +08:00

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, '\\)');
}