UNPKG

faj-cli

Version:

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

1,166 lines 49.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ElegantPDFGenerator = void 0; const pdf_lib_1 = require("pdf-lib"); const fontkit = __importStar(require("@pdf-lib/fontkit")); const Logger_1 = require("../../utils/Logger"); const SystemFontLoader_1 = require("./SystemFontLoader"); const CDNFontLoader_1 = require("./CDNFontLoader"); class ElegantPDFGenerator { logger; themes = { modern: { primary: [0.4, 0.49, 0.92], // #667eea - Purple blue secondary: [0.46, 0.29, 0.64], // #764ba2 - Purple accent: [0.20, 0.60, 0.86], // #3498db - Sky blue text: [0.13, 0.13, 0.13], // #212121 - Almost black light: [0.96, 0.96, 0.98], // #f5f5fa - Light gray background: [0.98, 0.98, 1.0] // #fafaff - Very light blue }, professional: { primary: [0.17, 0.24, 0.31], // #2c3e50 - Dark blue gray secondary: [0.52, 0.58, 0.64], // #85929e - Medium gray accent: [0.15, 0.68, 0.38], // #27ae60 - Green text: [0.17, 0.17, 0.17], // #2b2b2b - Dark gray light: [0.95, 0.96, 0.97], // #f3f4f6 - Light gray background: [1.0, 1.0, 1.0] // #ffffff - White }, minimalist: { primary: [0, 0, 0], // #000000 - Black secondary: [0.4, 0.4, 0.4], // #666666 - Gray accent: [0.2, 0.2, 0.2], // #333333 - Dark gray text: [0.1, 0.1, 0.1], // #1a1a1a - Near black light: [0.97, 0.97, 0.97], // #f7f7f7 - Light gray background: [1.0, 1.0, 1.0] // #ffffff - White } }; constructor() { this.logger = new Logger_1.Logger('ElegantPDFGenerator'); } async generatePDF(resume, themeName = 'modern') { const theme = this.themes[themeName] || this.themes.modern; // Create PDF document with metadata const pdfDoc = await pdf_lib_1.PDFDocument.create(); pdfDoc.setTitle(`${resume.basicInfo?.name || 'Resume'} - Resume`); pdfDoc.setAuthor(resume.basicInfo?.name || 'Unknown'); pdfDoc.setCreator('FAJ Resume Builder'); pdfDoc.setProducer('FAJ'); pdfDoc.setCreationDate(new Date()); pdfDoc.setModificationDate(new Date()); // Register fontkit for custom fonts pdfDoc.registerFontkit(fontkit); // Load fonts with CJK support const { font, boldFont } = await this.loadFonts(pdfDoc); // Create page with A4 size const page = pdfDoc.addPage(pdf_lib_1.PageSizes.A4); const { width, height } = page.getSize(); // Draw based on theme switch (themeName) { case 'minimalist': await this.drawMinimalistTheme(page, resume, theme, font, boldFont, width, height); break; case 'professional': await this.drawProfessionalTheme(page, resume, theme, font, boldFont, width, height); break; default: // modern await this.drawModernTheme(page, resume, theme, font, boldFont, width, height); break; } // Save PDF const pdfBytes = await pdfDoc.save(); return Buffer.from(pdfBytes); } async loadFonts(pdfDoc) { let font; let boldFont; try { // Try system fonts first const systemFontLoader = new SystemFontLoader_1.SystemFontLoader(); const systemFonts = await systemFontLoader.findSystemFonts(); if (systemFonts.regular && systemFonts.bold) { font = await pdfDoc.embedFont(systemFonts.regular); boldFont = await pdfDoc.embedFont(systemFonts.bold); this.logger.info('Using system CJK fonts'); } else { // Load from CDN const cdnLoader = new CDNFontLoader_1.CDNFontLoader(); const cdnFonts = await cdnLoader.loadFonts(); font = await pdfDoc.embedFont(cdnFonts.regular); boldFont = await pdfDoc.embedFont(cdnFonts.bold); this.logger.info('Using CDN fonts'); } } catch (error) { // Fallback this.logger.warn('Using standard fonts as fallback'); font = await pdfDoc.embedFont(pdf_lib_1.StandardFonts.Helvetica); boldFont = await pdfDoc.embedFont(pdf_lib_1.StandardFonts.HelveticaBold); } return { font, boldFont }; } async drawModernTheme(page, resume, theme, font, boldFont, width, height) { let y = height - 40; const margin = 50; const contentWidth = width - (margin * 2); // Modern gradient header background page.drawRectangle({ x: 0, y: height - 120, width: width, height: 120, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]), opacity: 0.05 }); // Draw decorative accent line at top page.drawRectangle({ x: 0, y: height - 3, width: width, height: 3, color: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]) }); // Name with modern styling if (resume.basicInfo?.name) { const nameSize = 32; page.drawText(resume.basicInfo.name, { x: margin, y: y, size: nameSize, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); y -= 45; } // Contact info with icons simulation const contactY = y; if (resume.basicInfo) { const contactSize = 10; let contactX = margin; const contacts = [ resume.basicInfo.email, resume.basicInfo.phone, resume.basicInfo.location, resume.basicInfo.githubUrl ].filter(Boolean); contacts.forEach((contact) => { if (contact) { // Draw small circle as icon placeholder page.drawCircle({ x: contactX + 4, y: contactY + 4, size: 3, color: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]), opacity: 0.7 }); page.drawText(contact, { x: contactX + 12, y: contactY, size: contactSize, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); contactX += this.getTextWidth(contact, font, contactSize) + 30; } }); y -= 35; } // Modern section divider this.drawGradientLine(page, margin, y, contentWidth, theme); y -= 25; // Professional Summary with card-like background if (resume.content.summary) { this.drawSectionHeader(page, 'PROFESSIONAL SUMMARY', margin, y, theme, boldFont, 'modern', width, margin); y -= 25; // Light background for summary const summaryLines = this.wrapText(resume.content.summary, contentWidth - 20, 11, font); const summaryHeight = summaryLines.length * 16 + 10; page.drawRectangle({ x: margin - 5, y: y - summaryHeight + 10, width: contentWidth + 10, height: summaryHeight, color: (0, pdf_lib_1.rgb)(theme.light[0], theme.light[1], theme.light[2]), borderColor: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]), borderWidth: 0.5, borderOpacity: 0.3 }); summaryLines.forEach(line => { page.drawText(line, { x: margin + 5, y: y, size: 11, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); y -= 16; }); y -= 20; } // Experience Section with timeline if (resume.content.experience?.length > 0) { this.drawSectionHeader(page, 'PROFESSIONAL EXPERIENCE', margin, y, theme, boldFont, 'modern', width, margin); y -= 25; resume.content.experience.forEach((exp, index) => { // Timeline dot page.drawCircle({ x: margin - 10, y: y + 6, size: 4, color: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]) }); // Timeline line (if not last item) if (index < resume.content.experience.length - 1) { page.drawLine({ start: { x: margin - 10, y: y + 2 }, end: { x: margin - 10, y: y - 80 }, thickness: 1, color: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]), opacity: 0.3 }); } // Job title and company page.drawText(exp.title, { x: margin + 5, y: y, size: 13, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); page.drawText(` @ ${exp.company}`, { x: margin + 5 + this.getTextWidth(exp.title, boldFont, 13), y: y, size: 12, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); // Date on the right const dateText = `${exp.startDate} - ${exp.current ? 'Present' : exp.endDate}`; this.drawCompactText(page, dateText, width - margin - this.getTextWidth(dateText, font, 10), y, 10, font, [theme.secondary[0], theme.secondary[1], theme.secondary[2]]); y -= 20; // Description if (exp.description) { const descLines = this.wrapText(exp.description, contentWidth - 10, 10, font); descLines.forEach(line => { page.drawText(line, { x: margin + 5, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); y -= 14; }); } // Highlights with modern bullets if (exp.highlights?.length > 0) { y -= 5; exp.highlights.slice(0, 3).forEach(highlight => { // Modern bullet point page.drawRectangle({ x: margin + 15, y: y + 3, width: 3, height: 3, color: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]) }); const highlightLines = this.wrapText(highlight, contentWidth - 30, 10, font); highlightLines.forEach(line => { page.drawText(line, { x: margin + 25, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); y -= 14; }); }); } y -= 20; }); } // Projects Section with modern styling if (resume.content.projects?.length > 0) { this.drawSectionHeader(page, 'KEY PROJECTS', margin, y, theme, boldFont, 'modern', width, margin); y -= 25; resume.content.projects.slice(0, 3).forEach(project => { // Project icon placeholder page.drawRectangle({ x: margin, y: y + 4, width: 3, height: 10, color: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]) }); // Project name page.drawText(project.name, { x: margin + 8, y: y, size: 12, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); if (project.role) { page.drawText(` - ${project.role}`, { x: margin + 8 + this.getTextWidth(project.name, boldFont, 12), y: y, size: 11, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); } y -= 18; // Project description if (project.description) { const descLines = this.wrapText(project.description, contentWidth - 10, 10, font); descLines.forEach(line => { page.drawText(line, { x: margin + 8, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); y -= 14; }); } // Project highlights if (project.highlights?.length > 0) { project.highlights.slice(0, 2).forEach(highlight => { page.drawCircle({ x: margin + 18, y: y + 3, size: 2, color: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]), opacity: 0.7 }); const highlightLines = this.wrapText(highlight, contentWidth - 30, 9, font); highlightLines.forEach(line => { page.drawText(line, { x: margin + 25, y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); y -= 13; }); }); } // Technologies used if (project.technologies?.length > 0) { y -= 5; const techText = `Tech: ${project.technologies.slice(0, 5).join(', ')}`; page.drawText(techText, { x: margin + 8, y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]), opacity: 0.8 }); y -= 15; } y -= 10; }); } // Skills Section with tags if (resume.content.skills?.length > 0) { this.drawSectionHeader(page, 'TECHNICAL SKILLS', margin, y, theme, boldFont, 'modern', width, margin); y -= 25; const skillsByCategory = this.groupSkills(resume.content.skills); Object.entries(skillsByCategory).forEach(([category, skills]) => { // Category label page.drawText(this.getCategoryLabel(category), { x: margin, y: y, size: 10, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); // Skills as tags let tagX = margin + 100; skills.slice(0, 10).forEach((skill, idx) => { const skillText = skill.name; const skillWidth = this.getTextWidth(skillText, font, 9); // Tag background page.drawRectangle({ x: tagX - 3, y: y - 3, width: skillWidth + 6, height: 14, color: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]), opacity: 0.1, borderColor: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]), borderWidth: 0.5, borderOpacity: 0.3 }); page.drawText(skillText, { x: tagX, y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); tagX += skillWidth + 12; // Wrap to next line if needed if (tagX > width - margin - 50 && idx < skills.length - 1) { tagX = margin + 100; y -= 20; } }); y -= 25; }); } // Education Section if (resume.content.education?.length > 0) { this.drawSectionHeader(page, 'EDUCATION', margin, y, theme, boldFont, 'modern', width, margin); y -= 25; resume.content.education.forEach(edu => { // Degree icon placeholder page.drawRectangle({ x: margin, y: y, width: 4, height: 14, color: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]) }); page.drawText(`${edu.degree} in ${edu.field}`, { x: margin + 10, y: y, size: 12, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); const eduDate = `${edu.startDate} - ${edu.endDate}`; this.drawCompactText(page, eduDate, width - margin - this.getTextWidth(eduDate, font, 10), y, 10, font, [theme.secondary[0], theme.secondary[1], theme.secondary[2]]); y -= 18; page.drawText(edu.institution, { x: margin + 10, y: y, size: 11, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); if (edu.gpa) { page.drawText(` • GPA: ${edu.gpa}`, { x: margin + 10 + this.getTextWidth(edu.institution, font, 11), y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); } y -= 25; }); } } async drawProfessionalTheme(page, resume, theme, font, boldFont, width, height) { let y = height - 50; const margin = 60; const contentWidth = width - (margin * 2); // Professional header with clean lines if (resume.basicInfo?.name) { // Name in classic style const nameSize = 28; const nameWidth = this.getTextWidth(resume.basicInfo.name || '', boldFont, nameSize); page.drawText(resume.basicInfo.name, { x: (width - nameWidth) / 2, y: y, size: nameSize, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); y -= 35; // Professional title line page.drawLine({ start: { x: margin, y: y + 10 }, end: { x: width - margin, y: y + 10 }, thickness: 2, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); // Contact information centered if (resume.basicInfo) { const contactItems = [ resume.basicInfo.email, resume.basicInfo.phone, resume.basicInfo.location ].filter(Boolean); const contactText = contactItems.join(' • '); const contactWidth = this.getTextWidth(contactText, font, 10); page.drawText(contactText, { x: (width - contactWidth) / 2, y: y - 8, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); y -= 35; } } // Professional Summary if (resume.content.summary) { this.drawSectionHeader(page, 'PROFESSIONAL SUMMARY', margin, y, theme, boldFont, 'professional', width, margin); y -= 20; const summaryLines = this.wrapText(resume.content.summary, 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 Section if (resume.content.experience?.length > 0) { this.drawSectionHeader(page, 'PROFESSIONAL EXPERIENCE', margin, y, theme, boldFont, 'professional', width, margin); y -= 20; resume.content.experience.forEach(exp => { // Traditional format page.drawText(exp.title, { x: margin, y: y, size: 12, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); const dateText = `${exp.startDate} - ${exp.current ? 'Present' : exp.endDate}`; this.drawCompactText(page, dateText, width - margin - this.getTextWidth(dateText, font, 10), y, 10, font, [theme.secondary[0], theme.secondary[1], theme.secondary[2]]); y -= 16; page.drawText(exp.company || '', { x: margin, y: y, size: 11, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]), opacity: 0.9 }); y -= 18; if (exp.description) { const descLines = this.wrapText(exp.description, 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 (exp.highlights?.length > 0) { y -= 3; exp.highlights.slice(0, 4).forEach(highlight => { page.drawText('•', { x: margin + 10, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); const highlightLines = this.wrapText(highlight, contentWidth - 20, 10, font); highlightLines.forEach((line, idx) => { page.drawText(line, { x: margin + 20, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); if (idx < highlightLines.length - 1) y -= 14; }); y -= 14; }); } y -= 18; }); } // Projects Section - Professional format if (resume.content.projects?.length > 0) { this.drawSectionHeader(page, 'PROJECTS', margin, y, theme, boldFont, 'professional', width, margin); y -= 20; resume.content.projects.slice(0, 3).forEach(project => { // Project name and role page.drawText(project.name, { 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) { page.drawText(` | ${project.role}`, { x: margin + this.getTextWidth(project.name, boldFont, 11), y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); } y -= 16; // Project description if (project.description) { const descLines = this.wrapText(project.description, 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; }); } // Project achievements if (project.highlights?.length > 0) { project.highlights.slice(0, 3).forEach(highlight => { page.drawText('•', { x: margin + 10, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); const highlightLines = this.wrapText(highlight, contentWidth - 20, 10, font); highlightLines.forEach((line, idx) => { page.drawText(line, { x: margin + 20, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); if (idx < highlightLines.length - 1) y -= 14; }); y -= 14; }); } // Technologies if (project.technologies?.length > 0) { const techText = `Technologies: ${project.technologies.join(', ')}`; const techLines = this.wrapText(techText, contentWidth, 9, font); techLines.forEach(line => { page.drawText(line, { x: margin, y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); y -= 13; }); } y -= 15; }); } // Skills Section - Traditional format if (resume.content.skills?.length > 0) { this.drawSectionHeader(page, 'TECHNICAL SKILLS', margin, y, theme, boldFont, 'professional', width, margin); y -= 20; const skillsByCategory = this.groupSkills(resume.content.skills); Object.entries(skillsByCategory).forEach(([category, skills]) => { const categoryLabel = this.getCategoryLabel(category); 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 skillNames = skills.map(s => s.name).join(', '); const skillLines = this.wrapText(skillNames, contentWidth - 120, 10, font); skillLines.forEach((line, idx) => { page.drawText(line, { x: margin + 120, 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) + 8; }); y -= 10; } // Education Section if (resume.content.education?.length > 0) { this.drawSectionHeader(page, 'EDUCATION', margin, y, theme, boldFont, 'professional', width, margin); y -= 20; resume.content.education.forEach(edu => { page.drawText(`${edu.degree} in ${edu.field}`, { x: margin, y: y, size: 11, font: boldFont, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); const eduDate = `${edu.startDate} - ${edu.endDate}`; this.drawCompactText(page, eduDate, width - margin - this.getTextWidth(eduDate, font, 10), y, 10, font, [theme.secondary[0], theme.secondary[1], theme.secondary[2]]); y -= 16; page.drawText(edu.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) { y -= 14; page.drawText(`GPA: ${edu.gpa}`, { x: margin, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); } y -= 20; }); } } async drawMinimalistTheme(page, resume, theme, font, _boldFont, width, height) { let y = height - 60; const margin = 70; const contentWidth = width - (margin * 2); // Minimalist header - just the name if (resume.basicInfo?.name) { const nameSize = 24; page.drawText(resume.basicInfo.name, { x: margin, y: y, size: nameSize, font: font, // Using regular font for minimalist color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); y -= 30; // Simple contact line const contacts = [ resume.basicInfo.email, resume.basicInfo.phone, resume.basicInfo.location ].filter(Boolean).join(' '); if (contacts) { page.drawText(contacts, { x: margin, y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); y -= 30; } // Thin separator line page.drawLine({ start: { x: margin, y: y }, end: { x: width - margin, y: y }, thickness: 0.5, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]), opacity: 0.3 }); y -= 25; } // Summary - no header, just text if (resume.content.summary) { const summaryLines = this.wrapText(resume.content.summary, contentWidth, 10, font); summaryLines.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]), opacity: 0.9 }); y -= 15; }); y -= 20; } // Experience - minimal styling if (resume.content.experience?.length > 0) { page.drawText('Experience', { x: margin, y: y, size: 11, font: font, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); y -= 20; resume.content.experience.forEach(exp => { // Simple layout page.drawText(`${exp.title}, ${exp.company}`, { x: margin, y: y, size: 10, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); const dateText = `${exp.startDate} - ${exp.current ? 'Now' : exp.endDate}`; page.drawText(dateText, { x: width - margin - this.getTextWidth(dateText, font, 9), y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); y -= 16; if (exp.description) { const descLines = this.wrapText(exp.description, contentWidth, 9, font); descLines.forEach(line => { page.drawText(line, { x: margin, y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]), opacity: 0.8 }); y -= 13; }); } if (exp.highlights?.length > 0) { exp.highlights.slice(0, 2).forEach(highlight => { const highlightLines = this.wrapText(`– ${highlight}`, contentWidth, 9, font); highlightLines.forEach(line => { page.drawText(line, { x: margin + 10, y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]), opacity: 0.7 }); y -= 13; }); }); } y -= 15; }); } // Projects - minimalist style if (resume.content.projects?.length > 0) { page.drawText('Projects', { x: margin, y: y, size: 11, font: font, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); y -= 20; resume.content.projects.slice(0, 2).forEach(project => { // Simple project listing page.drawText(project.name, { 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.description) { const descLines = this.wrapText(project.description, contentWidth, 9, font); descLines.forEach(line => { page.drawText(line, { x: margin, y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]), opacity: 0.8 }); y -= 13; }); } if (project.technologies?.length > 0) { const techText = project.technologies.slice(0, 5).join(' · '); page.drawText(techText, { x: margin, y: y, size: 8, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]), opacity: 0.7 }); y -= 13; } y -= 10; }); } // Skills - inline simple format if (resume.content.skills?.length > 0) { page.drawText('Skills', { x: margin, y: y, size: 11, font: font, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); y -= 18; const allSkills = resume.content.skills.map(s => s.name).join(' · '); const skillLines = this.wrapText(allSkills, contentWidth, 9, font); skillLines.forEach(line => { page.drawText(line, { x: margin, y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]), opacity: 0.8 }); y -= 13; }); y -= 15; } // Education - minimal if (resume.content.education?.length > 0) { page.drawText('Education', { x: margin, y: y, size: 11, font: font, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); y -= 18; resume.content.education.forEach(edu => { page.drawText(`${edu.degree}, ${edu.institution}`, { x: margin, y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.text[0], theme.text[1], theme.text[2]) }); const eduDate = `${edu.endDate}`; page.drawText(eduDate, { x: width - margin - this.getTextWidth(eduDate, font, 9), y: y, size: 9, font: font, color: (0, pdf_lib_1.rgb)(theme.secondary[0], theme.secondary[1], theme.secondary[2]) }); y -= 20; }); } } drawSectionHeader(page, text, x, y, theme, font, style, width = 595, margin = 60) { if (style === 'modern') { // Modern style with accent color and underline page.drawText(text, { x: x, y: y, size: 13, font: font, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); // Gradient underline const textWidth = this.getTextWidth(text, font, 13); page.drawRectangle({ x: x, y: y - 4, width: textWidth, height: 2, color: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]), opacity: 0.6 }); } else if (style === 'professional') { // Professional style with full underline page.drawText(text, { x: x, y: y, size: 12, font: font, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); page.drawLine({ start: { x: x, y: y - 4 }, end: { x: width - margin, y: y - 4 }, thickness: 1, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]), opacity: 0.3 }); } else { // Minimalist - just text page.drawText(text.charAt(0) + text.slice(1).toLowerCase(), { x: x, y: y, size: 11, font: font, color: (0, pdf_lib_1.rgb)(theme.primary[0], theme.primary[1], theme.primary[2]) }); } } drawGradientLine(page, x, y, width, theme) { // Simulate gradient with multiple rectangles const segments = 20; const segmentWidth = width / segments; for (let i = 0; i < segments; i++) { const opacity = 0.1 + (0.2 * (i / segments)); page.drawRectangle({ x: x + (i * segmentWidth), y: y, width: segmentWidth + 1, height: 1, color: (0, pdf_lib_1.rgb)(theme.accent[0], theme.accent[1], theme.accent[2]), opacity: opacity }); } } wrapText(text, maxWidth, fontSize, font) { const lines = []; let currentLine = ''; // Check if text contains CJK characters const hasCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(text); if (hasCJK) { // For CJK text, wrap character by character for (const char of text) { const testLine = currentLine + char; try { const textWidth = this.getTextWidth(testLine, font, fontSize); if (textWidth > maxWidth && currentLine) { lines.push(currentLine); currentLine = char; } else { currentLine = testLine; } } catch { currentLine = testLine; } } } else { // For non-CJK text, wrap by words const words = text.split(' '); for (const word of words) { const testLine = currentLine ? `${currentLine} ${word}` : word; try { const textWidth = this.getTextWidth(testLine, font, fontSize); if (textWidth > maxWidth && currentLine) { lines.push(currentLine); currentLine = word; } else { currentLine = testLine; } } catch { currentLine = testLine; } } } if (currentLine) { lines.push(currentLine); } return lines; } getTextWidth(text, font, size) { try { const width = font.widthOfTextAtSize(text, size); // Adjust for CJK fonts that may have wider number spacing if (/\d/.test(text) && /[\u4e00-\u9fff]/.test(text)) { // If text contains both numbers and CJK, apply adjustment return width * 0.95; } return width; } catch { // Fallback calculation return text.length * size * 0.5; } } drawCompactText(page, text, x, y, size, font, color) { // For date strings with numbers, draw each character individually with tighter spacing if (/^\d{4}-\d{2}/.test(text) || /^\d{4}\/\d{2}/.test(text)) { let currentX = x; for (const char of text) { page.drawText(char, { x: currentX, y: y, size: size, font: font, color: (0, pdf_lib_1.rgb)(color[0], color[1], color[2]) }); // Use tighter spacing for numbers and punctuation const charWidth = this.getTextWidth(char, font, size); currentX += charWidth * (char === '-' || char === '/' ? 0.8 : 0.85); } } else { // Normal text rendering page.drawText(text, { x: x, y: y, size: size, font: font, color: (0, pdf_lib_1.rgb)(color[0], color[1], color[2]) }); } } 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.ElegantPDFGenerator = ElegantPDFGenerator; //# sourceMappingURL=ElegantPDFGenerator.js.map