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:
- First pass to generate the PDF and determine section positions
- Update the TOC with page numbers
- Second pass to generate the final PDF
Expected PDF Structure
- Cover page (no page number)
- Table of Contents (page ii)
- Section 1 (page 1)
- 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; };