Skip to content

Content is overlapping with each other when trying to create multipage pdf from json data set #3861

Open
@rajibmahata

Description

@rajibmahata

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 );

Rajib_Mahata_Modern124.pdf

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions