DEV Community

Cover image for Fixing HTML Table Rowspan Issues in PDFs with IronPDF and C#
Nizamuddien T I
Nizamuddien T I

Posted on

Fixing HTML Table Rowspan Issues in PDFs with IronPDF and C#

Table of Contents

  1. Introduction
  2. Understanding the Rowspan Issue
  3. Visual Comparison
  4. The Solution
  5. Implementation Guide
  6. Custom Font Embedding
  7. Results
  8. Summary
  9. References

Introduction

Generating PDFs from HTML is straightforwardβ€”until you encounter complex table layouts. One of the most common real-world issues developers face when converting HTML tables to PDFs using IronPDF is rowspan misalignment.

While your tables may appear perfect in a browser, they often render incorrectly in PDF output due to the fundamental differences between how browsers and PDF renderers handle dynamic layouts.

This tutorial shows how to preprocess tables with rowspan using HtmlAgilityPack and generate clean PDFs with IronPDF in C#. Step-by-step guidance, sample code, and a GitHub repository make it easy to implement this fix in your projects. Perfect for .NET developers working with complex HTML-to-PDF conversions.


Understanding the Rowspan Issue

What is Rowspan?

The rowspan attribute in HTML tables allows a single cell to span multiple rows:

<table>
        <tr>
            <th></th>
            <th>Rows</th>
            <th>Description</th>
        </tr>
        <tr>
            <td rowspan="100">Row span 100</td>
            <td>1</td>
            <td>Row 1</td>
        </tr>
        <tr>
            <td>2</td>
            <td>Row 2</td>
        </tr>
        <tr>
            <td>3</td>
            <td>Row 3</td>
        </tr>
        ........    // other rows
        <tr>
            <td>100</td>
            <td>Row 100</td>
        </tr>
    </table>
Enter fullscreen mode Exit fullscreen mode

The Problem:

When browsers render this HTML, they dynamically calculate cell positions and maintain layout consistency. However, PDF rendering requires fixed layouts with predefined coordinates. This fundamental difference causes several issues:

Common Problems:

πŸ”΄ Misaligned columns - Columns shift position incorrectly
πŸ”΄ Broken borders - Table borders appear disconnected
πŸ”΄ Inconsistent row heights - Rows have unpredictable heights
πŸ”΄ Overlapping content - Text or cells may overlap

Why This Happens:
  1. PDF rendering engines don't dynamically recalculate layouts like browsers do.
  2. Rowspan cells need explicit height calculations.
  3. The renderer doesn't know the total height needed for spanned rows until it processes all rows.
  4. This creates ambiguity in cell positioning and sizing.

Visual Comparison

Browser Rendering (Works Fine):

PDF Without Fix (Broken):

Page - 1:

Page - 2:

Page - 5 (Page - 3 and 4 are blank):


the rows are continued till 100

Page - 8 (Final page):


The Solution:

Pre-Render Normalization

How It Works

Instead of trying to fix the PDF rendering directly, we normalize the HTML before rendering it to PDF. This approach:

  1. Detects rowspan usage - Scans all table cells for rowspan attributes
  2. Expands table structure - Replaces rowspan with explicit rows
  3. Inserts layout-safe filler cells - Adds empty cells to maintain column alignment
  4. Preserves visual alignment - Applies CSS styling to hide borders on filler cells
  5. Generates clean PDFs - IronPDF renders the normalized structure perfectly
Algorithm Overview
Input: HTML with rowspan="100"
  β”‚
  β”œβ”€ Parse HTML using HtmlAgilityPack
  β”‚
  β”œβ”€ For each table:
  β”‚   β”œβ”€ Iterate through all rows
  β”‚   β”œβ”€ Track active rowspans per column index
  β”‚   β”œβ”€ For cells with rowspan:
  β”‚   β”‚  β”œβ”€ Remove bottom border from original cell
  β”‚   β”‚  β”œβ”€ Create (rowspan-1) cloned cells
  β”‚   β”‚  β”œβ”€ Remove borders from clones
  β”‚   β”‚  └─ Queue them for insertion in subsequent rows
  β”‚   └─ Insert queued cells at appropriate column positions
  β”‚
  └─ Output: HTML with normalized table structure

Result: PDF with perfect table alignment
Enter fullscreen mode Exit fullscreen mode
Key Benefits
Benefit Description
Simple Implementation Works with standard HTML/CSS
No Library Modifications Uses IronPDF as-is
Compatible Works with any table structure
Predictable Results Normalizes HTML before rendering
No Performance Impact Runs in milliseconds

Implementation Guide

Step 1: Install Required Dependencies
<!-- Add to .csproj -->
<ItemGroup>
  <PackageReference Include="IronPdf" Version="2024.12.3" />
  <PackageReference Include="HtmlAgilityPack" Version="1.11.61" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode
Step 2: Create the RowSpan Expander Helper

The core logic that fixes rowspan issues:

using HtmlAgilityPack;

namespace IronPdfMVC.Helpers
{
    public static class RowspanExpander
    {
        private const string STYLE_ATTRIBUTE = "style";

        /// <summary>
        /// Expands all rowspans in HTML tables to create a normalized structure
        /// </summary>
        public static string ExpandRowspans(string html)
        {
            var doc = new HtmlDocument();
            doc.LoadHtml(html);

            var tables = doc.DocumentNode.SelectNodes("//table");
            if (tables == null)
                return html;

            foreach (var table in tables)
            {
                ExpandTable(table);
            }

            return doc.DocumentNode.OuterHtml;
        }

        private static void ExpandTable(HtmlNode table)
        {
            var rows = table.SelectNodes(".//tr");
            if (rows == null)
                return;

            // Tracks active rowspans per column index
            // Key: column index, Value: queue of cloned cells to insert
            var activeRowspans = new Dictionary<int, Queue<HtmlNode>>();

            foreach (var row in rows)
            {
                var cells = row.Elements("td")
                               .Concat(row.Elements("th"))
                               .ToList();

                int colIndex = 0;

                // Step 1: Inject pending rowspan cells from previous rows
                while (activeRowspans.ContainsKey(colIndex))
                {
                    var queue = activeRowspans[colIndex];
                    if (queue.Count == 0)
                    {
                        activeRowspans.Remove(colIndex);
                        break;
                    }

                    var clonedCell = queue.Dequeue();
                    InsertCellAtColumn(row, clonedCell.Clone(), colIndex);
                    colIndex++;
                }

                // Step 2: Process current row's cells
                foreach (var cell in cells)
                {
                    // Skip columns with active rowspans from previous rows
                    while (activeRowspans.ContainsKey(colIndex))
                    {
                        colIndex++;
                    }

                    // Check if this cell has a rowspan > 1
                    if (cell.Attributes["rowspan"] != null &&
                        int.TryParse(cell.Attributes["rowspan"].Value, out int span) &&
                        span > 1)
                    {
                        // Remove bottom border to create seamless appearance
                        AppendInlineStyle(cell, "border-bottom: none;");

                        var clones = new Queue<HtmlNode>();

                        // Create (span - 1) filler cells
                        for (int i = 1; i < span; i++)
                        {
                            var clone = cell.Clone();
                            clone.Attributes.Remove("rowspan");
                            clone.InnerHtml = string.Empty; // Empty the cell content

                            // Style filler cells to be invisible
                            AppendInlineStyle(clone, 
                                "border-top: none;border-bottom: none;padding-top: 0;padding-bottom: 0;");

                            clones.Enqueue(clone);
                        }

                        // Queue these cells for insertion in subsequent rows
                        activeRowspans[colIndex] = clones;

                        // Remove rowspan attribute (now handled by explicit cells)
                        cell.Attributes.Remove("rowspan");
                    }

                    colIndex++;
                }
            }
        }

        /// <summary>
        /// Appends CSS styles to a cell's inline style attribute
        /// </summary>
        private static void AppendInlineStyle(HtmlNode node, string style)
        {
            var existing = node.GetAttributeValue(STYLE_ATTRIBUTE, string.Empty);

            if (!string.IsNullOrWhiteSpace(existing) && !existing.Trim().EndsWith(";"))
            {
                existing += ";";
            }

            node.SetAttributeValue(STYLE_ATTRIBUTE, existing + style);
        }

        /// <summary>
        /// Inserts a cell at the specified column position
        /// </summary>
        private static void InsertCellAtColumn(HtmlNode row, HtmlNode cell, int colIndex)
        {
            var existingCells = row.Elements("td")
                                   .Concat(row.Elements("th"))
                                   .ToList();

            if (colIndex >= existingCells.Count)
            {
                row.AppendChild(cell);
            }
            else
            {
                row.InsertBefore(cell, existingCells[colIndex]);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
Step 3: Create the PDF Generation Controller
using IronPdf;
using IronPdfMVC.Enums;
using IronPdfMVC.Factories;
using IronPdfMVC.Helpers;
using IronPdfMVC.Models;
using Microsoft.AspNetCore.Mvc;

namespace IronPdfMVC.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            var model = new PrintModel
            {
                RowspanOutput = RowspanOutput.Raw,
                RowspanOutputOptions = typeof(RowspanOutput).ToSelectList(RowspanOutput.Raw),
                SelectedFont = "ToThePointRegular",
            };
            return View(model);
        }

        [HttpPost]
        public IActionResult PrintPdf(PrintModel model)
        {
            // Build CSS for table styling
            var normalCss = @"
                table, th, td {
                    border: 1px solid black;
                    border-collapse: collapse;
                    padding: 8px;
                }
            ";

            // Build HTML content with table containing rowspan
            string htmlContent = $@"
                <!DOCTYPE html>
                <html lang=""en"">
                <head>
                    <meta charset=""UTF-8"">
                    <meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
                    <title>Document</title>
                    <style>{normalCss}";

            // Add custom font CSS if selected
            if (model.UseCustomFont)
            {
                htmlContent += GetFontEmbeddingCss(model.SelectedFont);
            }

            htmlContent += @"
                    </style>
                </head>
                <body>
                    <h2>Table with Rowspan</h2>
                    <table>
                        <tr>
                            <th></th>
                            <th>Rows</th>
                            <th>Description</th>
                        </tr>
                        <tr>
                            <td rowspan=""100"">Row span 100</td>";

            // Add table rows (1-100)
            htmlContent = InvoicePdfFactory.PrepareRowspanTableContent(htmlContent);

            htmlContent += @"
                        </table>
                    </body>
                </html>";

            // Create PDF renderer
            var renderer = new ChromePdfRenderer();
            PdfDocument pdf;

            // Apply rowspan fix if selected
            if (model.RowspanOutput == RowspanOutput.Fixed)
            {
                // FIX: Normalize HTML before rendering
                var expandedHtml = RowspanExpander.ExpandRowspans(htmlContent);
                pdf = renderer.RenderHtmlAsPdf(expandedHtml);
                pdf.SaveAs("Fixed_Rowspan.pdf");
            }
            else
            {
                // Raw: Render without fix
                pdf = renderer.RenderHtmlAsPdf(htmlContent);
                pdf.SaveAs("Raw_Rowspan.pdf");
            }

            // Return PDF to user
            return File(pdf.BinaryData, "application/pdf");
        }

        private string GetFontEmbeddingCss(string fontName)
        {
            // Load font file from wwwroot/fonts/
            var fontPath = Path.Combine(
                Directory.GetCurrentDirectory(), 
                "wwwroot", 
                "fonts", 
                $"{fontName}.ttf"
            );

            // Read and encode to Base64
            var fontBytes = System.IO.File.ReadAllBytes(fontPath);
            var fontBase64 = Convert.ToBase64String(fontBytes);

            // Return @font-face CSS with embedded font
            return $@"
                @font-face {{
                    font-family: 'CustomFont';
                    src: url('data:font/ttf;base64,{fontBase64}') format('truetype');
                    font-weight: normal;
                    font-style: normal;
                    font-display: swap;
                }}
                body, table, td, th {{ 
                    font-family: 'CustomFont', sans-serif; 
                }}
            ";
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Custom Font Embedding

Why Embed Fonts?

When you generate PDFs, you need to ensure the fonts are available on the server and properly embedded in the PDF. Without embedding:

  • ❌ Fonts may not render on all systems
  • ❌ PDF viewers might substitute fonts
  • ❌ Brand typography becomes inconsistent
How It Works

The process involves three steps:
Step 1: Load Font File

var fontPath = Path.Combine(
    Directory.GetCurrentDirectory(), 
    "wwwroot", 
    "fonts", 
    "ToThePointRegular.ttf"
);
var fontBytes = System.IO.File.ReadAllBytes(fontPath);
Enter fullscreen mode Exit fullscreen mode

Step 2: Encode to Base64

var fontBase64 = Convert.ToBase64String(fontBytes);
Enter fullscreen mode Exit fullscreen mode

Step 3: Embed in CSS

var fontCss = $@"
    @font-face {{
        font-family: 'ToThePoint';
        src: url('data:font/ttf;base64,{fontBase64}') format('truetype');
        font-weight: normal;
        font-style: normal;
        font-display: swap;
    }}
    body, table, td, th {{ font-family: 'ToThePoint', sans-serif; }}
";
Enter fullscreen mode Exit fullscreen mode
Font Format Support

IronPDF supports multiple font formats:

Format Extension CSS Format Support
TrueType .ttf format('truetype') βœ… Excellent
OpenType .otf format('opentype') βœ… Excellent
WOFF .woff format('woff') βœ… Good
WOFF2 .woff2 format('woff2') βœ… Good
Complete Font Embedding Example
// Embed multiple font weights
var fontCss = $@"
    @font-face {{
        font-family: 'CustomFont';
        src: url('data:font/ttf;base64,{regular}') format('truetype');
        font-weight: normal;
    }}

    @font-face {{
        font-family: 'CustomFont';
        src: url('data:font/ttf;base64,{bold}') format('truetype');
        font-weight: bold;
    }}

    @font-face {{
        font-family: 'CustomFont';
        src: url('data:font/ttf;base64,{italic}') format('truetype');
        font-style: italic;
    }}

    body, table, td, th {{ 
        font-family: 'CustomFont', Arial, sans-serif; 
    }}
";
Enter fullscreen mode Exit fullscreen mode

Results

Transformed HTML:
<table>
  <tr>
    <td style="border-bottom: none;">Row span 100</td>
    <td>Row 1</td>
  </tr>
  <tr>
    <td style="border-top: none; border-bottom: none; padding-top: 0; padding-bottom: 0;"></td>
    <!-- ↑ Filler cell created by RowspanExpander -->
    <td>Row 2</td>
  </tr>
  <tr>
    <td style="border-top: none; border-bottom: none; padding-top: 0; padding-bottom: 0;"></td>
    <!-- ↑ Filler cell created by RowspanExpander -->
    <td>Row 3</td>
  </tr>
  ...
</table>
Enter fullscreen mode Exit fullscreen mode
PDF Rendering Result:
  • βœ… All columns properly aligned
  • βœ… Seamless borders maintained
  • βœ… Consistent row heights
  • βœ… Professional appearance

Summary

By implementing the rowspan expansion approach, you can:

  • βœ… Reliably generate PDFs with complex table layouts
  • βœ… Maintain visual consistency across all devices and viewers
  • βœ… Embed custom fonts for branded documents
  • βœ… Scale to production with minimal performance impact
  • βœ… Handle edge cases with proper error handling

References

Repository: https://github.com/nizambajal/ironpdf-rowspan-fix-dotnet

IronPDF Docs: https://ironsoftware.com/csharp/pdf/

HtmlAgilityPack: https://html-agility-pack.net/

HTML Tables MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table

Top comments (0)