Description
I was trying to create a resume pdf from a json data structure but the issue is pdf is creation in single page but all content is overlapping with each other and unable to read the resume properly . I have added script and generated document as well . Could you pls suggest me what wrong I am doing. It should be multi page pdf and end of it one QR code will be there for public profile .
Script :
`// wwwroot/js/resumeGenerator.js
window.ResumePdfGenerator = {
generateResume: function (structuredResumeDataJson, publicLinkQrDataUrl, templateName = 'modernCompact', fileName = 'AI-Enhanced-Resume.pdf') {
console.log("generateResume called. Template:", templateName);
try {
const resumeData = JSON.parse(structuredResumeDataJson);
const doc = new jspdf.jsPDF({ orientation: 'p', unit: 'mm', format: 'a4', putOnlyUsedFonts: true });
const page = {
width: doc.internal.pageSize.getWidth(),
height: doc.internal.pageSize.getHeight(),
margin: 15,
contentWidth: doc.internal.pageSize.getWidth() - (2 * 15),
maxYBeforeBreak: doc.internal.pageSize.getHeight() - 15, // Y limit before needing new page
};
const fonts = { heading: 'Helvetica', body: 'Helvetica', bold: 'Helvetica-Bold', italic: 'Helvetica-Oblique' };
const fontSizes = { name: 18, contact: 9, sectionTitle: 12, entryTitle: 10, body: 9, small: 8 };
const LINE_HEIGHT_FACTOR = 1.4; // EXPERIMENT WITH THIS
doc.setLineHeightFactor(LINE_HEIGHT_FACTOR);
let currentY = page.margin;
const getLineHeight = (fontSize) => (fontSize / doc.internal.scaleFactor) * LINE_HEIGHT_FACTOR;
// --- Core Text Drawing Function with Page Break Check for EACH LINE ---
// This function now directly modifies the global 'currentY' from the 'generateResume' scope.
const drawTextLines = (text, x, options = {}) => {
if (!text || String(text).trim() === "") return;
const maxWidth = options.maxWidth || (page.contentWidth - (x - page.margin));
const fontSize = options.fontSize || fontSizes.body;
const font = options.font || fonts.body;
const fontStyle = options.fontStyle || 'normal';
const color = options.color || '#333333';
const paddingBottom = options.paddingBottom || 0; // Space AFTER this entire text block
doc.setFont(font, fontStyle);
doc.setFontSize(fontSize);
doc.setTextColor(color);
const lines = doc.splitTextToSize(String(text), maxWidth);
const singleLineHeight = getLineHeight(fontSize);
lines.forEach(line => {
// Check if THIS line fits
if (currentY + singleLineHeight > page.maxYBeforeBreak) {
doc.addPage();
currentY = page.margin;
}
// Baseline adjustment for visual top alignment
doc.text(line, x, currentY + (fontSize / doc.internal.scaleFactor * 0.8));
currentY += singleLineHeight; // Advance Y for each line drawn
});
currentY += paddingBottom; // Add padding after the whole block
};
const drawSectionTitle = (title, options = {}) => {
const fontSize = fontSizes.sectionTitle;
const paddingBottom = options.paddingBottom || 2;
const titleHeight = getLineHeight(fontSize); // Estimate for one line
if (currentY + titleHeight + paddingBottom > page.maxYBeforeBreak) {
doc.addPage();
currentY = page.margin;
}
const textYBaseline = currentY + (fontSize / doc.internal.scaleFactor * 0.8);
doc.setFont(fonts.bold, 'normal');
doc.setFontSize(fontSize);
doc.setTextColor('#2c3e50'); // Darker blue/grey
doc.text(title.toUpperCase(), page.margin, textYBaseline);
currentY += titleHeight; // Advance by title height
// Underline
const titleWidth = doc.getStringUnitWidth(title.toUpperCase()) * fontSize / doc.internal.scaleFactor;
const lineY = currentY + 1; // Underline slightly below baseline of title
if (lineY + 0.5 < page.maxYBeforeBreak) {
doc.setDrawColor('#AEB6BF'); // Lighter grey for underline
doc.setLineWidth(0.4);
doc.line(page.margin, lineY, page.margin + Math.min(titleWidth, page.contentWidth), lineY);
currentY += 1.5; // Space for underline and a bit more
}
currentY += paddingBottom; // Space after the title block
};
// --- Get CSS Variables (Fallback if needed) ---
const rootStyles = getComputedStyle(document.documentElement);
const vars = {
primaryColor: rootStyles.getPropertyValue('--primary-color').trim() || '#007bff',
};
doc.setDrawColor(vars.primaryColor); // Set primary color for potential lines
// --- Render Template ---
if (templateName === 'modernCompact') {
// The template function now uses 'drawTextLines' and 'drawSectionTitle',
// which directly modify the 'currentY' from this 'generateResume' scope.
this.renderModernCompactTemplate(doc, resumeData, page, fonts, fontSizes, getLineHeight, drawTextLines, drawSectionTitle, publicLinkQrDataUrl, vars);
} else { /* ... */ }
doc.save(fileName);
return true;
} catch (error) { /* ... */ return false; }
},
// ===================================================================================
// TEMPLATE: Modern Compact (Single Column) - Relies on global currentY
// The 'currentY' is NOT passed or returned by this function. It uses the one from generateResume scope.
// ===================================================================================
renderModernCompactTemplate: function (doc, data, page, fonts, fontSizes, getLineHeight, _addText, _addSectionTitle, publicLinkQrDataUrl, cssVars) {
// Note: _addText and _addSectionTitle are the helpers from generateResume scope
// that internally use drawElement and update the global currentY.
const sectionTitlePadding = 2;
const entryHeaderPadding = 0.5;
const entryMetaPadding = 1.5;
const listItemPadding = 1.5; // Space after each bullet item
const entryEndPadding = 3; // Space after a full entry (job, project)
const sectionEndPadding = 6; // Space after a whole section
const bulletX = page.margin + 2;
const textXAfterBullet = bulletX + 4; // Keep some space for the bullet
const listItemMaxWidth = page.contentWidth - (textXAfterBullet - page.margin);
// --- 1. PERSONAL DETAILS ---
if (data.personal_details) {
const pd = data.personal_details;
if (pd.full_name) {
const nameWidth = doc.getStringUnitWidth(pd.full_name) * fontSizes.name / doc.internal.scaleFactor;
const nameX = page.margin + Math.max(0, (page.contentWidth - nameWidth) / 2);
_addText(pd.full_name, nameX, { fontSize: fontSizes.name, font: fonts.bold, color: '#1a237e', paddingBottom: 1 });
}
let contactLine = [pd.email, pd.phone, pd.location].filter(Boolean).join(' • ');
if (contactLine) {
const contactWidth = doc.getStringUnitWidth(contactLine) * fontSizes.contact / doc.internal.scaleFactor;
const contactX = page.margin + Math.max(0, (page.contentWidth - contactWidth) / 2);
_addText(contactLine, contactX, { fontSize: fontSizes.contact, color: '#34495e', paddingBottom: 1 });
}
let linkLineArray = [];
if (pd.linkedin_url) linkLineArray.push(pd.linkedin_url.replace(/^(https?:\/\/)?(www\.)?/, ''));
if (pd.portfolio_url) linkLineArray.push(pd.portfolio_url.replace(/^(https?:\/\/)?(www\.)?/, ''));
if (linkLineArray.length > 0) {
const linksText = linkLineArray.join(' | ');
const linksWidth = doc.getStringUnitWidth(linksText) * fontSizes.small / doc.internal.scaleFactor;
const linksX = page.margin + Math.max(0, (page.contentWidth - linksWidth) / 2);
_addText(linksText, linksX, { fontSize: fontSizes.small, color: '#0066cc', paddingBottom: 2 });
}
// Divider Line - This needs careful handling with drawElement
const lineElementHeight = 0.5 + 2; // line thickness + padding after
drawElement(lineElementHeight, (yDrawPos) => { // drawElement is from generateResume scope
doc.setDrawColor(200); doc.setLineWidth(0.3);
doc.line(page.margin, yDrawPos + 0.25, page.width - page.margin, yDrawPos + 0.25);
});
}
// --- 2. PROFESSIONAL SUMMARY ---
if (data.professional_summary) {
_addSectionTitle("SUMMARY", { paddingBottom: sectionTitlePadding });
_addText(data.professional_summary, page.margin, { fontSize: fontSizes.body, paddingBottom: sectionEndPadding, lineHeightFactor: 1.5 });
}
// --- 3. SKILLS ---
if (data.skills && (data.skills.categorized?.length || data.skills.general?.length)) {
_addSectionTitle("SKILLS", { paddingBottom: sectionTitlePadding + 1 }); // Extra padding after title
const skillIndent = page.margin + 2;
if (data.skills.categorized?.length) {
data.skills.categorized.forEach(cat => {
_addText(cat.category_name.toUpperCase(), page.margin, { fontSize: fontSizes.entryTitle, font: fonts.bold, color: '#333', paddingBottom: 1 });
_addText((cat.skill_list || []).join(' • '), skillIndent, { fontSize: fontSizes.body, maxWidth: page.contentWidth - (skillIndent - page.margin), paddingBottom: entryItemSpacing });
});
}
if (data.skills.general?.length) {
_addText("GENERAL SKILLS", page.margin, { fontSize: fontSizes.entryTitle, font: fonts.bold, color: '#333', paddingBottom: 1 });
_addText(data.skills.general.join(' • '), skillIndent, { fontSize: fontSizes.body, maxWidth: page.contentWidth - (skillIndent - page.margin), paddingBottom: entryItemSpacing });
}
// The last addText call in skills will have entryItemSpacing. We want sectionEndPadding after the whole block.
// So, if currentY was updated, add the difference.
// This kind of manual adjustment is tricky. It's better if addText's paddingBottom is the final word.
// Let's ensure the last call to addText in skills has sectionEndPadding
// For simplicity, if a section has multiple parts, the LAST addText call for that section should use sectionEndPadding
// OR we add a manual space using an empty addText call if needed.
// The current `addText` includes padding in its height for `drawElement`.
// So the last call's paddingBottom becomes the section's bottom margin.
// If Skills was the last section before Experience:
// The last addText in Skills should have { paddingBottom: sectionEndPadding }
}
// --- 4. EXPERIENCE ---
if (data.experience?.length) {
_addSectionTitle("PROFESSIONAL EXPERIENCE", { paddingBottom: sectionTitlePadding });
data.experience.forEach((exp, expIdx) => {
_addText(exp.job_title, page.margin, { fontSize: fontSizes.entryTitle, font: fonts.bold, paddingBottom: entryHeaderPadding });
_addText(`${exp.company_name} | ${exp.dates} ${exp.location ? ' | ' + exp.location : ''}`, page.margin, { fontSize: fontSizes.small, color: '#555555', paddingBottom: entryMetaPadding });
if (exp.responsibilities_achievements?.length) {
exp.responsibilities_achievements.forEach(ach => {
const lines = doc.splitTextToSize(ach, listItemMaxWidth);
const itemBlockHeight = lines.length * getLineHeight(fontSizes.body);
// Add padding specific to this bullet item
const totalItemHeightWithPadding = itemBlockHeight + listItemPadding;
drawElement(totalItemHeightWithPadding, (yPos) => {
const textBaselineY = yPos + (fontSizes.body / doc.internal.scaleFactor * 0.8);
doc.setFont(fonts.body, 'normal'); doc.setFontSize(fontSizes.body); doc.setTextColor('#333333');
doc.text("•", bulletX, textBaselineY);
doc.text(lines, textXAfterBullet, textBaselineY);
});
});
}
// Add entryEndPadding only if it's not the last experience entry OR if it's the last one and Projects section follows
if (expIdx < data.experience.length - 1 || (expIdx === data.experience.length - 1 && (data.education?.length || data.projects?.length))) {
drawElement(entryEndPadding, (yPos) => { }); // Just advance Y
}
});
drawElement(sectionEndPadding - entryEndPadding, (yPos) => { }); // Remaining section padding
}
// --- 5. EDUCATION --- (Apply similar structure, using entryEndPadding for each edu entry, and sectionEndPadding after all)
if (data.education?.length) {
_addSectionTitle("EDUCATION", { paddingBottom: sectionTitlePadding });
data.education.forEach((edu, eduIdx) => {
_addText(edu.degree_name, page.margin, { fontSize: fontSizes.entryTitle, font: fonts.bold, paddingBottom: entryHeaderPadding });
_addText(edu.institution_name, page.margin, { fontSize: fontSizes.body, color: '#444444', paddingBottom: entryHeaderPadding });
_addText(`${edu.graduation_date} ${edu.location ? ' | ' + edu.location : ''}`, page.margin, { fontSize: fontSizes.small, color: '#555555', paddingBottom: entryMetaPadding });
if (edu.details?.length) {
edu.details.forEach(detail => {
const lines = doc.splitTextToSize(detail, listItemMaxWidth);
const itemHeight = lines.length * getLineHeight(fontSizes.body) + listItemPadding;
drawElement(itemHeight, (yPos) => { /* ... draw bullet and text ... */
const textBaselineY = yPos + (fontSizes.body / doc.internal.scaleFactor * 0.8);
doc.setFont(fonts.body, 'normal'); doc.setFontSize(fontSizes.body); doc.setTextColor('#333333');
doc.text("•", bulletX, textBaselineY);
doc.text(lines, textXAfterBullet, textBaselineY);
});
});
}
if (eduIdx < data.education.length - 1) drawElement(entryEndPadding, (yPos) => { });
});
drawElement(sectionEndPadding - (data.education.length > 0 ? entryEndPadding : 0) + (data.education.length > 0 && data.education[data.education.length - 1].details?.length ? 0 : listItemPadding), (yPos) => { });
}
// --- 6. PROJECTS --- (Similar logic)
if (data.projects?.length) {
_addSectionTitle("PROJECTS", { paddingBottom: sectionTitlePadding });
data.projects.forEach((proj, projIdx) => {
_addText(proj.project_name, page.margin, { fontSize: fontSizes.entryTitle, font: fonts.bold, paddingBottom: entryHeaderPadding });
if (proj.technologies_used?.length) {
_addText(`Technologies: ${proj.technologies_used.join(', ')}`, page.margin, { fontSize: fontSizes.small, color: '#555555', paddingBottom: entryMetaPadding });
}
if (proj.description_contributions?.length) {
proj.description_contributions.forEach(desc => {
const lines = doc.splitTextToSize(desc, listItemMaxWidth);
const itemHeight = lines.length * getLineHeight(fontSizes.body) + listItemPadding;
drawElement(itemHeight, (yPos) => { /* ... draw bullet and text ... */
const textBaselineY = yPos + (fontSizes.body / doc.internal.scaleFactor * 0.8);
doc.setFont(fonts.body, 'normal'); doc.setFontSize(fontSizes.body); doc.setTextColor('#333333');
doc.text("•", bulletX, textBaselineY);
doc.text(lines, textXAfterBullet, textBaselineY);
});
});
}
if (proj.project_url) {
_addText(`Link: ${proj.project_url}`, page.margin, { fontSize: fontSizes.small, color: '#007bff', paddingBottom: entryHeaderPadding });
}
if (projIdx < data.projects.length - 1) drawElement(entryEndPadding, (yPos) => { });
});
drawElement(sectionEndPadding - entryEndPadding + (data.projects.length > 0 && (data.projects[data.projects.length - 1].project_url || data.projects[data.projects.length - 1].description_contributions?.length) ? 0 : entryHeaderPadding), (yPos) => { });
}
// --- 7. CERTIFICATIONS ---
if (data.certifications?.length) {
_addSectionTitle("CERTIFICATIONS", { paddingBottom: sectionTitlePadding });
data.certifications.forEach((cert, certIdx) => {
_addText(cert.certification_name, page.margin, { fontSize: fontSizes.body, font: fonts.bold, color: '#444444', paddingBottom: 0 });
let certDetails = [cert.issuing_organization, cert.date_obtained].filter(Boolean).join(' - ');
if (certDetails) {
_addText(certDetails, page.margin, { fontSize: fontSizes.small, color: '#555555', paddingBottom: (certIdx === data.certifications.length - 1 ? sectionEndPadding : entryItemSpacing) });
} else {
// If no details, add appropriate spacing
drawElement((certIdx === data.certifications.length - 1 ? sectionEndPadding : entryItemSpacing), (yPos) => { });
}
});
}
// --- 8. ADDITIONAL SECTIONS ---
if (data.additional_sections?.length) {
data.additional_sections.forEach((addSection, addIdx) => {
_addSectionTitle((addSection.section_title || "Additional Information").toUpperCase(), { paddingBottom: 1 });
const contentLines = String(addSection.section_content || "").split('\n');
contentLines.forEach(line => {
// Simplified: treat each line as a separate text block for now
_addText(line, page.margin, { fontSize: fontSizes.body, paddingBottom: lineSpacing / 2 });
});
if (addIdx < data.additional_sections.length - 1) drawElement(entryEndPadding, (yPos) => { });
});
drawElement(sectionEndPadding - entryEndPadding + (data.additional_sections.length > 0 ? lineSpacing / 2 : 0), (yPos) => { });
}
// --- QR Code (at the very end) ---
if (publicLinkQrDataUrl) {
const qrSize = 25;
const qrText = "View Online Profile";
const qrTextLines = doc.splitTextToSize(qrText, qrSize);
const qrTextHeight = qrTextLines.length * getLineHeight(fontSizes.xsmall);
const totalQrBlockHeight = qrSize + qrTextHeight + 2; // +2 for text padding
// Get currentY from the generateResume scope BEFORE drawing QR
let yForQr = currentY; // This `currentY` is the one from the outer `generateResume` function
if (yForQr + totalQrBlockHeight + 5 > page.height - page.margin) {
doc.addPage();
yForQr = page.margin;
}
let qrX = page.width - page.margin - qrSize;
let finalQrY = yForQr; // Place after current content by default
// If there's enough space to put it at bottom of current page, try that
if (page.height - page.margin - yForQr >= totalQrBlockHeight + 5) {
finalQrY = page.height - page.margin - totalQrBlockHeight;
}
// If after adding the QR block, it still overflows *this attempt* (rare if check above is good)
// it means it should have gone to a new page.
if (finalQrY < page.margin) finalQrY = page.margin; // Safety if new page was added
try {
doc.addImage(publicLinkQrDataUrl, 'PNG', qrX, finalQrY, qrSize, qrSize);
// We need to use the _addText that updates the global currentY if we want it to flow
// But for QR text, it's usually fixed relative to QR. So direct draw is fine here.
const qrTextY = finalQrY + qrSize + 1;
doc.setFont(fonts.body, 'normal');
doc.setFontSize(fontSizes.xsmall);
doc.setTextColor('#777777');
doc.text(qrTextLines, qrX + (qrSize - doc.getStringUnitWidth(qrTextLines[0]) * fontSizes.xsmall / doc.internal.scaleFactor) / 2, qrTextY + (fontSizes.xsmall / doc.internal.scaleFactor * 0.8));
} catch (qrError) { console.warn("Could not add QR code to PDF:", qrError); }
}
},
};
};`
Calling function :
bool success = await JSRuntime.InvokeAsync<bool>( "ResumePdfGenerator.generateResume", resumeJson, currentQrDataUrl, // Pass QR data URL or null "modernCompact", // Choose a template name defined in resumeGenerator.js fileName );