0

I'm trying to generate a PDF with html2pdf.js that includes a table of contents with accurate page numbers for each section. My current implementation isn't working correctly - the page numbers in the TOC don't match the actual section positions in the final PDF.

Current Approach

I've tried a two-pass solution:

  1. First pass to generate the PDF and determine section positions
  2. Update the TOC with page numbers
  3. Second pass to generate the final PDF

Expected PDF Structure

  1. Cover page (no page number)
  2. Table of Contents (page ii)
  3. Section 1 (page 1)
  4. Section 2 (page 3) etc.

Problem

The page numbers in the TOC don't accurately reflect where sections actually appear in the final PDF.

Code

const convertToPdf = async () => { const { default: html2pdf } = await import('html2pdf.js'); if (typeof window === 'undefined' || typeof document === 'undefined') return; const content = document.getElementById('pdf-content'); const opt = { filename: 'Incident Report Analysis.pdf', pagebreak: { mode: ['css', 'legacy'], before: '.avoid-page-break', avoid: ['div', '#breakdata', 'tr', 'table'] }, image: { type: 'jpeg', quality: 1 }, html2canvas: { scale: 1.4, logging: true }, jsPDF: { unit: 'in', format: 'A3', orientation: 'portrait' }, margin: [0.1, 0, 0.30, 0], }; // First pass to get section positions const firstPassPdf = await html2pdf().set(opt).from(content).toPdf().get('pdf'); await new Promise(resolve => setTimeout(resolve, 500)); const extractedSections = extractSectionPositions(firstPassPdf, content, opt.html2canvas.scale); // Adjust page numbers (+1 for TOC page) const updatedSections = extractedSections.map(section => ({ ...section, page: section.page + 1 })); // Update the TOC in the DOM with real page numbers updatedSections.forEach(section => { const span = document.querySelector(`.toc-page[data-target="${section.title}"]`); if (span) { span.textContent = section.page; } }); // Force a re-render of the component with updated sections setSections(updatedSections); // Wait for React to update the DOM await new Promise(resolve => setTimeout(resolve, 500)); // Now generate the final PDF with correct TOC const finalContent = document.getElementById('pdf-content'); const finalPdf = await html2pdf().set(opt).from(finalContent).toPdf().get('pdf'); // Add footer and other elements const totalPages = finalPdf.internal.getNumberOfPages(); const pageWidth = finalPdf.internal.pageSize.width; const pageHeight = finalPdf.internal.pageSize.height; const footerHeight = 0.4; const footerYPosition = pageHeight - footerHeight; const centerYPosition = footerYPosition + footerHeight / 2; for (let i = 1; i <= totalPages; i++) { finalPdf.setPage(i); finalPdf.setFontSize(12); finalPdf.setFont('helvetica'); if (i === 2) { // TOC page finalPdf.setFontSize(14); finalPdf.setTextColor(0, 0, 0); finalPdf.text("Table of Contents", 0.3, 0.5); } if (i > 1) { finalPdf.setFillColor(51, 51, 51); finalPdf.rect(0, footerYPosition, pageWidth, footerHeight, 'F'); finalPdf.setTextColor(255, 255, 255); finalPdf.text('WWW.ICORPSECURITY.COM.AU', 0.3, centerYPosition, { align: 'left', baseline: 'middle' }); finalPdf.text(`Page ${i} of ${totalPages}`, pageWidth - 0.3, centerYPosition, { align: 'right', baseline: 'middle' }); } else { finalPdf.setFillColor(245, 245, 246); finalPdf.rect(0, footerYPosition, pageWidth, footerHeight, 'F'); } } finalPdf.save(`Incident Analysis (${dayjs(formattedStartDateRange).format('DD/MM/YYYY')} - ${dayjs(formattedEndDateRange || formattedStartDateRange).format('DD/MM/YYYY')}).pdf`); if (onDownloadComplete) { onDownloadComplete(); } }; const extractSectionPositions = (pdf, content, scale) => { const sectionElements = content.querySelectorAll('[data-section]'); const pixelsPerInch = 96; const pixelsPerPage = (pdf.internal.pageSize.height * pixelsPerInch) / scale; const sections = Array.from(sectionElements).map(el => { const top = el.offsetTop; const pageNum = Math.floor(top / pixelsPerPage) + 1; console.log(pageNum, "pageNum"); return { title: el.getAttribute('data-section'), page: pageNum }; }).sort((a, b) => a.page - b.page); return sections; }; 

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.