import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; import fontkit from '@pdf-lib/fontkit'; import fs from 'fs-extra'; import path from 'path'; import Handlebars from 'handlebars'; import { Language } from '../types'; interface DocumentRequest { type: 'fir' | 'rent_agreement' | 'affidavit'; details: Record; language: string; } interface DocumentTemplate { name: string; content: string; fonts?: { [key: string]: string; }; } export class DocumentService { private readonly templatesDir: string; private readonly fontsDir: string; private readonly outputDir: string; private templates: Map; constructor() { this.templatesDir = path.join(__dirname, '../../templates'); this.fontsDir = path.join(__dirname, '../../fonts'); this.outputDir = path.join(__dirname, '../../tmp/documents'); this.templates = new Map(); // Ensure directories exist fs.ensureDirSync(this.templatesDir); fs.ensureDirSync(this.fontsDir); fs.ensureDirSync(this.outputDir); // Load templates this.loadTemplates(); } private async loadTemplates() { const templateFiles = await fs.readdir(this.templatesDir); for (const file of templateFiles) { if (file.endsWith('.json')) { const templatePath = path.join(this.templatesDir, file); const template = await fs.readJSON(templatePath); this.templates.set(template.name, template); } } } private async loadFont(fontPath: string) { const fontBytes = await fs.readFile(fontPath); return fontBytes; } async generateDocument({ type, details, language }: DocumentRequest): Promise { const template = this.templates.get(type); if (!template) { throw new Error(`Template not found for document type: ${type}`); } // Create a new PDF document const pdfDoc = await PDFDocument.create(); pdfDoc.registerFontkit(fontkit); // Load fonts based on language const fontPath = path.join(this.fontsDir, `${language}.ttf`); let font; try { const fontBytes = await this.loadFont(fontPath); font = await pdfDoc.embedFont(fontBytes); } catch (error) { // Fallback to standard font if language-specific font not found font = await pdfDoc.embedFont(StandardFonts.TimesRoman); } // Create template with Handlebars const compiledTemplate = Handlebars.compile(template.content); const content = compiledTemplate(details); // Add a page to the document const page = pdfDoc.addPage(); const { width, height } = page.getSize(); // Add content to the page page.drawText(content, { x: 50, y: height - 50, size: 12, font, color: rgb(0, 0, 0), }); // Save the PDF const pdfBytes = await pdfDoc.save(); // Generate unique filename const filename = `${type}_${Date.now()}.pdf`; const outputPath = path.join(this.outputDir, filename); // Write to file await fs.writeFile(outputPath, pdfBytes); return pdfBytes; } async getTemplateFields(type: string): Promise { const template = this.templates.get(type); if (!template) { throw new Error(`Template not found for document type: ${type}`); } // Extract field names from template using Handlebars AST const ast = Handlebars.parse(template.content); const fields = new Set(); function extractFields(node: any) { if (node.type === 'MustacheStatement') { fields.add(node.path.original); } if (node.program) { node.program.body.forEach(extractFields); } } ast.body.forEach(extractFields); return Array.from(fields); } async cleanup(olderThan: number = 24 * 60 * 60 * 1000): Promise { const files = await fs.readdir(this.outputDir); const now = Date.now(); for (const file of files) { const filePath = path.join(this.outputDir, file); const stats = await fs.stat(filePath); if (now - stats.mtimeMs > olderThan) { await fs.unlink(filePath); } } } } export const documentService = new DocumentService();