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<string, any>;
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<string, DocumentTemplate>;
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<Buffer> {
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<string[]> {
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<string>();
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<void> {
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();