faj-cli
Version:
FAJ - A powerful CLI resume builder with AI enhancement and multi-format export
390 lines (382 loc) • 14.2 kB
JavaScript
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.SimplePDFGenerator = void 0;
const ResumeTemplates_1 = require("../../templates/ResumeTemplates");
const Logger_1 = require("../../utils/Logger");
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
class SimplePDFGenerator {
logger;
constructor() {
this.logger = new Logger_1.Logger('SimplePDFGenerator');
}
/**
* Generate a temporary HTML file that can be opened and printed to PDF
* This ensures perfect rendering with all fonts and styles
*/
async generateHTMLForPDF(resume, themeName = 'modern') {
try {
// Generate HTML content with the selected theme
let htmlContent = (0, ResumeTemplates_1.generateHTMLResume)(resume, themeName);
// Add print-specific CSS to ensure proper A4 formatting
const printCSS = `
<style>
@media print {
@page {
size: A4;
margin: 10mm;
}
body {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
color-adjust: exact !important;
}
.resume-container {
width: 100% !important;
max-width: none !important;
margin: 0 !important;
box-shadow: none !important;
border-radius: 0 !important;
}
.header {
break-inside: avoid;
page-break-inside: avoid;
}
.section {
break-inside: avoid;
page-break-inside: avoid;
}
.experience-item, .project-item, .education-item {
break-inside: avoid;
page-break-inside: avoid;
}
/* Ensure background colors print */
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
}
/* Additional screen styles for better preview */
@media screen {
body {
background: #f0f0f0;
padding: 20px;
}
.resume-container {
max-width: 210mm;
min-height: 297mm;
margin: 0 auto;
background: white;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
}
</style>
`;
// Insert print CSS before closing head tag
htmlContent = htmlContent.replace('</head>', printCSS + '</head>');
// Add instructions for printing
const instructions = `
<div id="print-instructions" style="
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 15px;
border-radius: 5px;
z-index: 10000;
font-family: Arial, sans-serif;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
">
<h3 style="margin: 0 0 10px 0;">📄 生成PDF说明 / Export to PDF</h3>
<ol style="margin: 5px 0; padding-left: 20px;">
<li>按 Ctrl+P (Windows/Linux) 或 Cmd+P (Mac)</li>
<li>选择"另存为PDF" / Select "Save as PDF"</li>
<li>确保选择A4纸张 / Ensure A4 paper size</li>
<li>勾选"背景图形" / Check "Background graphics"</li>
</ol>
<button onclick="window.print(); return false;" style="
background: white;
color: #4CAF50;
border: none;
padding: 8px 16px;
border-radius: 3px;
cursor: pointer;
font-weight: bold;
margin-top: 10px;
">立即打印 / Print Now</button>
<button onclick="this.parentElement.style.display='none'" style="
background: transparent;
color: white;
border: 1px solid white;
padding: 8px 16px;
border-radius: 3px;
cursor: pointer;
margin-left: 10px;
">关闭 / Close</button>
</div>
<style>
@media print {
#print-instructions {
display: none !important;
}
}
</style>
`;
// Add instructions after body tag
htmlContent = htmlContent.replace('<body>', '<body>' + instructions);
// Save to temporary file
const tmpDir = os.tmpdir();
const fileName = `resume_${Date.now()}.html`;
const filePath = path.join(tmpDir, fileName);
await fs.writeFile(filePath, htmlContent, 'utf-8');
this.logger.success(`HTML file created: ${filePath}`);
return filePath;
}
catch (error) {
this.logger.error('Failed to generate HTML for PDF', error);
throw new Error('HTML generation failed: ' + error.message);
}
}
/**
* Generate PDF using PDFKit with better Unicode support
* Note: This method has limitations with complex layouts
*/
async generateWithPDFKit(resume, themeName = 'modern') {
const PDFDocument = require('pdfkit');
// Create a new PDF document with Unicode font support
const doc = new PDFDocument({
size: 'A4',
margins: {
top: 50,
bottom: 50,
left: 50,
right: 50
},
autoFirstPage: true,
bufferPages: true
});
const chunks = [];
doc.on('data', (chunk) => chunks.push(chunk));
// Try to register fonts that support Unicode
try {
// For development, we'll use the default fonts
// In production, you should bundle proper Unicode fonts
if (process.platform === 'darwin') {
// macOS - use system fonts
doc.registerFont('ChineseFont', '/System/Library/Fonts/PingFang.ttc');
doc.font('ChineseFont');
}
else {
// Use default Helvetica which doesn't support Chinese
// Add a warning for Chinese content
doc.font('Helvetica');
}
}
catch (err) {
this.logger.warn('Could not load Unicode fonts, using fallback');
doc.font('Helvetica');
}
// Get theme colors
const themes = {
modern: {
primary: '#667eea',
secondary: '#764ba2',
text: '#2c3e50'
},
professional: {
primary: '#2c3e50',
secondary: '#34495e',
text: '#1a1a1a'
},
minimalist: {
primary: '#000000',
secondary: '#666666',
text: '#333333'
}
};
const colors = themes[themeName] || themes.modern;
// Header
if (resume.basicInfo) {
// Name
doc.fontSize(28)
.fillColor(colors.primary)
.text(resume.basicInfo.name || 'Name', {
align: 'center'
});
doc.moveDown(0.5);
// Contact info
const contactInfo = [];
if (resume.basicInfo.email)
contactInfo.push(resume.basicInfo.email);
if (resume.basicInfo.phone)
contactInfo.push(resume.basicInfo.phone);
if (resume.basicInfo.location)
contactInfo.push(resume.basicInfo.location);
if (contactInfo.length > 0) {
doc.fontSize(11)
.fillColor(colors.secondary)
.text(contactInfo.join(' | '), {
align: 'center'
});
}
// Add line separator
doc.moveDown();
doc.moveTo(50, doc.y)
.lineTo(545, doc.y)
.strokeColor(colors.primary)
.lineWidth(2)
.stroke();
doc.moveDown();
}
// Professional Summary
if (resume.content?.summary) {
doc.fontSize(14)
.fillColor(colors.primary)
.text('PROFESSIONAL SUMMARY');
doc.fontSize(10)
.fillColor(colors.text)
.text(resume.content.summary, {
align: 'justify'
});
doc.moveDown(1.5);
}
// Work Experience
if (resume.content?.experience && resume.content.experience.length > 0) {
doc.fontSize(14)
.fillColor(colors.primary)
.text('WORK EXPERIENCE');
doc.moveDown(0.5);
for (const exp of resume.content.experience) {
doc.fontSize(12)
.fillColor(colors.text)
.text(exp.title || 'Position', {
continued: true
})
.fillColor(colors.secondary)
.text(` at ${exp.company || 'Company'}`);
doc.fontSize(9)
.fillColor('#666666')
.text(`${exp.startDate || ''} - ${exp.current ? 'Present' : exp.endDate || ''}`);
if (exp.description) {
doc.fontSize(10)
.fillColor(colors.text)
.text(exp.description);
}
if (exp.highlights && exp.highlights.length > 0) {
doc.moveDown(0.3);
for (const highlight of exp.highlights) {
doc.fontSize(9)
.fillColor(colors.text)
.text(`• ${highlight}`, {
indent: 20
});
}
}
doc.moveDown();
}
}
// Projects
if (resume.content?.projects && resume.content.projects.length > 0) {
doc.fontSize(14)
.fillColor(colors.primary)
.text('PROJECTS');
doc.moveDown(0.5);
for (const proj of resume.content.projects) {
doc.fontSize(12)
.fillColor(colors.text)
.text(proj.name || 'Project');
if (proj.role) {
doc.fontSize(9)
.fillColor(colors.secondary)
.text(proj.role);
}
if (proj.description) {
doc.fontSize(10)
.fillColor(colors.text)
.text(proj.description);
}
doc.moveDown();
}
}
// Skills
if (resume.content?.skills && resume.content.skills.length > 0) {
doc.fontSize(14)
.fillColor(colors.primary)
.text('TECHNICAL SKILLS');
doc.moveDown(0.5);
const skillText = resume.content.skills.map((s) => s.name).join(', ');
doc.fontSize(10)
.fillColor(colors.text)
.text(skillText);
doc.moveDown();
}
// Education
if (resume.content?.education && resume.content.education.length > 0) {
doc.fontSize(14)
.fillColor(colors.primary)
.text('EDUCATION');
doc.moveDown(0.5);
for (const edu of resume.content.education) {
doc.fontSize(12)
.fillColor(colors.text)
.text(`${edu.degree || 'Degree'} in ${edu.field || 'Field'}`);
doc.fontSize(10)
.fillColor(colors.secondary)
.text(edu.institution || 'Institution');
doc.fontSize(9)
.fillColor('#666666')
.text(`${edu.startDate || ''} - ${edu.endDate || ''}`);
if (edu.gpa) {
doc.text(`GPA: ${edu.gpa}`);
}
doc.moveDown();
}
}
// End the document
doc.end();
return new Promise((resolve, reject) => {
doc.on('end', () => {
const pdfBuffer = Buffer.concat(chunks);
resolve(pdfBuffer);
});
doc.on('error', reject);
});
}
}
exports.SimplePDFGenerator = SimplePDFGenerator;
//# sourceMappingURL=SimplePDFGenerator.js.map
;