Table of Contents
- Introduction
- Understanding the Rowspan Issue
- Visual Comparison
- The Solution
- Implementation Guide
- Custom Font Embedding
- Results
- Summary
- 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>
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:
- PDF rendering engines don't dynamically recalculate layouts like browsers do.
- Rowspan cells need explicit height calculations.
- The renderer doesn't know the total height needed for spanned rows until it processes all rows.
- 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:
- Detects rowspan usage - Scans all table cells for rowspan attributes
- Expands table structure - Replaces rowspan with explicit rows
- Inserts layout-safe filler cells - Adds empty cells to maintain column alignment
- Preserves visual alignment - Applies CSS styling to hide borders on filler cells
- 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
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>
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]);
}
}
}
}
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;
}}
";
}
}
}
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);
Step 2: Encode to Base64
var fontBase64 = Convert.ToBase64String(fontBytes);
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; }}
";
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;
}}
";
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>
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)