UNPKG

faj-cli

Version:

FAJ - A powerful CLI resume builder with AI enhancement and multi-format export

397 lines 16.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.LightweightPDFGenerator = void 0; const pdf_lib_1 = require("pdf-lib"); const Logger_1 = require("../../utils/Logger"); /** * Lightweight PDF Generator that uses system fonts or standard fonts * Produces much smaller PDF files (typically < 100KB) * Trade-off: CJK characters will be replaced with romanized versions or placeholders */ class LightweightPDFGenerator { logger; themes = { modern: { primary: [0.4, 0.49, 0.92], secondary: [0.46, 0.29, 0.64], accent: [0.20, 0.60, 0.86], text: [0.13, 0.13, 0.13], light: [0.96, 0.96, 0.98] }, professional: { primary: [0.17, 0.24, 0.31], secondary: [0.52, 0.58, 0.64], accent: [0.15, 0.68, 0.38], text: [0.17, 0.17, 0.17], light: [0.95, 0.96, 0.97] }, minimalist: { primary: [0, 0, 0], secondary: [0.4, 0.4, 0.4], accent: [0.2, 0.2, 0.2], text: [0.1, 0.1, 0.1], light: [0.97, 0.97, 0.97] } }; constructor() { this.logger = new Logger_1.Logger('LightweightPDFGenerator'); } async generatePDF(resume, themeName = 'modern') { const theme = this.themes[themeName] || this.themes.modern; // Check if content has CJK characters const resumeText = JSON.stringify(resume); const hasCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(resumeText); if (hasCJK) { this.logger.warn('CJK characters detected. They will be transliterated or shown as [CJK] in the PDF.'); this.logger.info('For full CJK support, use the "html-print" export option or enable font embedding.'); } // Create PDF with metadata const pdfDoc = await pdf_lib_1.PDFDocument.create(); pdfDoc.setTitle(`${this.sanitizeText(resume.basicInfo?.name || 'Resume')} - Resume`); pdfDoc.setAuthor(this.sanitizeText(resume.basicInfo?.name || 'Unknown')); pdfDoc.setCreator('FAJ Resume Builder'); pdfDoc.setProducer('FAJ Lightweight'); // Use standard fonts only (no embedding needed) const font = await pdfDoc.embedFont(pdf_lib_1.StandardFonts.Helvetica); const boldFont = await pdfDoc.embedFont(pdf_lib_1.StandardFonts.HelveticaBold); // Create A4 page const page = pdfDoc.addPage(pdf_lib_1.PageSizes.A4); const { width, height } = page.getSize(); let y = height - 50; const margin = 60; const contentWidth = width - (margin * 2); // Header if (resume.basicInfo?.name) { const name = this.sanitizeText(resume.basicInfo.name); page.drawText(name, { x: margin, y: y, size: 28, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); y -= 35; // Contact info const contacts = [ this.sanitizeText(resume.basicInfo.email), this.sanitizeText(resume.basicInfo.phone), this.sanitizeText(resume.basicInfo.location) ].filter(Boolean); if (contacts.length > 0) { const contactText = contacts.join(' • '); page.drawText(contactText, { x: margin, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); y -= 25; } // Separator line page.drawLine({ start: { x: margin, y: y }, end: { x: width - margin, y: y }, thickness: 1, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]), opacity: 0.3 }); y -= 25; } // Professional Summary if (resume.content.summary) { this.drawSection(page, 'PROFESSIONAL SUMMARY', margin, y, theme, boldFont); y -= 20; const summaryText = this.sanitizeText(resume.content.summary); const summaryLines = this.wrapText(summaryText, contentWidth, 11, font); summaryLines.forEach(line => { page.drawText(line, { x: margin, y: y, size: 11, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); y -= 16; }); y -= 15; } // Experience if (resume.content.experience?.length > 0) { this.drawSection(page, 'PROFESSIONAL EXPERIENCE', margin, y, theme, boldFont); y -= 20; resume.content.experience.forEach(exp => { // Title and company const title = this.sanitizeText(exp.title); const company = this.sanitizeText(exp.company || ''); page.drawText(title, { x: margin, y: y, size: 12, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); if (company) { page.drawText(` at ${company}`, { x: margin + font.widthOfTextAtSize(title, 12) + 5, y: y, size: 11, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); } // Date const dateText = `${exp.startDate} - ${exp.current ? 'Present' : exp.endDate}`; const dateWidth = font.widthOfTextAtSize(dateText, 10); page.drawText(dateText, { x: width - margin - dateWidth, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); y -= 18; // Description if (exp.description) { const descText = this.sanitizeText(exp.description); const descLines = this.wrapText(descText, contentWidth, 10, font); descLines.forEach(line => { page.drawText(line, { x: margin, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); y -= 14; }); } // Highlights if (exp.highlights?.length > 0) { exp.highlights.slice(0, 3).forEach(highlight => { const highlightText = this.sanitizeText(highlight); const lines = this.wrapText(`• ${highlightText}`, contentWidth - 20, 10, font); lines.forEach(line => { page.drawText(line, { x: margin + 10, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); y -= 14; }); }); } y -= 15; }); } // Projects if (resume.content.projects?.length > 0) { this.drawSection(page, 'PROJECTS', margin, y, theme, boldFont); y -= 20; resume.content.projects.slice(0, 2).forEach(project => { const projectName = this.sanitizeText(project.name); page.drawText(projectName, { x: margin, y: y, size: 11, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); if (project.role) { const role = this.sanitizeText(project.role); page.drawText(` - ${role}`, { x: margin + font.widthOfTextAtSize(projectName, 11) + 3, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); } y -= 16; if (project.description) { const descText = this.sanitizeText(project.description); const descLines = this.wrapText(descText, contentWidth, 10, font); descLines.forEach(line => { page.drawText(line, { x: margin, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); y -= 14; }); } if (project.technologies?.length > 0) { const techText = `Tech: ${project.technologies.join(', ')}`; page.drawText(techText, { x: margin, y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); y -= 14; } y -= 10; }); } // Skills if (resume.content.skills?.length > 0) { this.drawSection(page, 'TECHNICAL SKILLS', margin, y, theme, boldFont); y -= 20; const skillsByCategory = this.groupSkills(resume.content.skills); Object.entries(skillsByCategory).forEach(([category, skills]) => { const categoryLabel = this.getCategoryLabel(category); const skillNames = skills.map(s => s.name).join(', '); page.drawText(`${categoryLabel}: `, { x: margin, y: y, size: 10, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); const labelWidth = boldFont.widthOfTextAtSize(`${categoryLabel}: `, 10); const skillLines = this.wrapText(skillNames, contentWidth - labelWidth, 10, font); skillLines.forEach((line, idx) => { page.drawText(line, { x: margin + (idx === 0 ? labelWidth : 0), y: y - (idx * 14), size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); }); y -= skillLines.length * 14 + 5; }); y -= 10; } // Education if (resume.content.education?.length > 0) { this.drawSection(page, 'EDUCATION', margin, y, theme, boldFont); y -= 20; resume.content.education.forEach(edu => { const degree = this.sanitizeText(`${edu.degree} in ${edu.field}`); const institution = this.sanitizeText(edu.institution); page.drawText(degree, { x: margin, y: y, size: 11, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); const dateText = `${edu.startDate} - ${edu.endDate}`; const dateWidth = font.widthOfTextAtSize(dateText, 10); page.drawText(dateText, { x: width - margin - dateWidth, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); y -= 16; page.drawText(institution, { x: margin, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); if (edu.gpa) { const gpa = ` • GPA: ${edu.gpa}`; page.drawText(gpa, { x: margin + font.widthOfTextAtSize(institution, 10), y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); } y -= 20; }); } // Save PDF const pdfBytes = await pdfDoc.save(); this.logger.success(`Generated lightweight PDF: ${pdfBytes.length / 1024}KB`); return Buffer.from(pdfBytes); } drawSection(page, title, x, y, theme, font) { page.drawText(title, { x: x, y: y, size: 12, font: font, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); // Underline const textWidth = font.widthOfTextAtSize(title, 12); page.drawLine({ start: { x: x, y: y - 3 }, end: { x: x + textWidth, y: y - 3 }, thickness: 0.5, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]), opacity: 0.5 }); } /** * Sanitize text to remove or transliterate CJK characters * This ensures the PDF can be rendered with standard fonts */ sanitizeText(text) { if (!text) return ''; // Replace CJK characters with romanized versions or placeholders return text .replace(/[\u4e00-\u9fff]+/g, '[Chinese]') // Chinese characters .replace(/[\u3040-\u309f]+/g, '[Hiragana]') // Japanese Hiragana .replace(/[\u30a0-\u30ff]+/g, '[Katakana]') // Japanese Katakana .replace(/[\uac00-\ud7af]+/g, '[Korean]') // Korean characters .replace(/[^\x00-\x7F]/g, ''); // Remove other non-ASCII } wrapText(text, maxWidth, fontSize, font) { const words = text.split(' '); const lines = []; let currentLine = ''; for (const word of words) { const testLine = currentLine ? `${currentLine} ${word}` : word; const textWidth = font.widthOfTextAtSize(testLine, fontSize); if (textWidth > maxWidth && currentLine) { lines.push(currentLine); currentLine = word; } else { currentLine = testLine; } } if (currentLine) { lines.push(currentLine); } return lines; } groupSkills(skills) { const grouped = {}; skills.forEach(skill => { const category = skill.category || 'other'; if (!grouped[category]) { grouped[category] = []; } grouped[category].push(skill); }); return grouped; } getCategoryLabel(category) { const labels = { 'programming_languages': 'Languages', 'frameworks': 'Frameworks', 'databases': 'Databases', 'tools': 'Tools', 'cloud': 'Cloud', 'other': 'Other' }; return labels[category.toLowerCase()] || category; } } exports.LightweightPDFGenerator = LightweightPDFGenerator; //# sourceMappingURL=LightweightPDFGenerator.js.map