Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a56ec43
Wire the CSS Fragmentation vocabulary CssEngine already parses
jhaygood86 Aug 21, 2026
7833caa
Add the immutable fragment record types (Fragments/)
jhaygood86 Aug 21, 2026
5e9321d
Add the fragmentation classification and break-token types
jhaygood86 Aug 21, 2026
daab5d2
Produce a trivial single-fragmentainer FragmentTree after layout
jhaygood86 Aug 21, 2026
27663ae
Add a minimal FragmentPainter that paints from the fragment tree
jhaygood86 Aug 21, 2026
10f792d
Add block-level page-break corrections (D2)
jhaygood86 Aug 21, 2026
2e2e968
Bucket the fragment tree into one fragmentainer per page
jhaygood86 Aug 21, 2026
90729d4
Add inline-flow page-break corrections: real widows/orphans (D3)
jhaygood86 Aug 21, 2026
7f30f40
Add table fragmentation: row-level avoidance and repeated <thead> (D4)
jhaygood86 Aug 21, 2026
3b840f0
Cut PdfGenerator over to page-per-fragmentainer rendering (F1)
jhaygood86 Aug 21, 2026
dc2099e
Make FragmentPainter the default paint path for WinForms/WPF (F2)
jhaygood86 Aug 21, 2026
b056b6c
Remove the crude per-word/per-box BreakPage nudge (F3, partial)
jhaygood86 Aug 21, 2026
b7fc8c9
Add real per-type fragment content painters (image/hr/frame/marker)
jhaygood86 Aug 21, 2026
3914ffc
Fix multi-page PDF text/image invisibility, delete CssBox.Paint/PaintImp
jhaygood86 Aug 21, 2026
fda44e3
R0: paint text from the fragment tree, not live CssRect geometry
jhaygood86 Aug 21, 2026
b5b1408
R1: real resumable pass loop for forced page breaks
jhaygood86 Aug 21, 2026
8903205
R3: break-inside:avoid/monolithic relocation via real relayout (R2 fo…
jhaygood86 Aug 21, 2026
d8c4c00
R4: real keep-with-next, fixing a genuine pre-existing gap
jhaygood86 Aug 21, 2026
380a207
R5/R6: neither needed new resumption machinery - fixed real bugs instead
jhaygood86 Aug 21, 2026
03aa597
Fix R1 regression: forced breaks nested inside table cells corrupted …
jhaygood86 Aug 21, 2026
4360e0c
R7: table content already benefits from R1-R6 fixes; one known limita…
jhaygood86 Aug 21, 2026
23cd304
R9: keep-with-next across a real pass boundary already works; no rewi…
jhaygood86 Aug 21, 2026
cb1bdf9
Fix catastrophic layout corruption when a keep-with-next run exceeds …
jhaygood86 Aug 21, 2026
0496e67
R10: delete dead break-token/relaxation scaffolding never wired up
jhaygood86 Aug 21, 2026
f41c56d
Fix orphans never enforced on a paragraph's first page-fragment
jhaygood86 Aug 21, 2026
07f4a09
Fix rowspan cells silently skipped by the table's break-inside:avoid …
jhaygood86 Aug 21, 2026
45ede10
Fix container-left-behind: a moved box's ancestor now follows it up
jhaygood86 Aug 21, 2026
5f56ad2
Document that list markers survive container relocation without help
jhaygood86 Aug 21, 2026
68fbb66
Preserve table rows unfragmented by default, per css-tables-3 6.1
jhaygood86 Aug 22, 2026
736529b
Repeat position:fixed content on every page, per css-position-3
jhaygood86 Aug 22, 2026
b9cf2ec
Fix CI page-count tests fragile to font substitution on non-Windows
jhaygood86 Aug 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Source/HtmlRenderer.PdfSharp/HtmlContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,31 @@ public void PerformPaint(XGraphics g)
}
}

/// <summary>
/// The immutable fragment tree layout produced from the box tree on the last <see cref="PerformLayout"/>
/// call - one fragmentainer per real page the document spans. Null before the first layout.
/// </summary>
internal Core.Fragments.FragmentTree FragmentTree
{
get { return _htmlContainerInt.FragmentTree; }
}

/// <summary>
/// Render one fragmentainer using the given device, reading from the immutable fragment tree
/// rather than walking the mutable box tree directly.
/// </summary>
/// <param name="g">the device to use to render</param>
/// <param name="fragmentainer">the fragmentainer to paint</param>
internal void PerformPaint(XGraphics g, Core.Fragments.FragmentainerFragment fragmentainer)
{
ArgChecker.AssertArgNotNull(g, "g");

using (var ig = new GraphicsAdapter(g))
{
_htmlContainerInt.PerformPaint(ig, fragmentainer);
}
}

public void Dispose()
{
_htmlContainerInt.Dispose();
Expand Down
78 changes: 56 additions & 22 deletions Source/HtmlRenderer.PdfSharp/PdfGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
using PdfSharp.Drawing;
using PdfSharp.Pdf;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using TheArtOfDev.HtmlRenderer.Adapters;
using TheArtOfDev.HtmlRenderer.Core;
using TheArtOfDev.HtmlRenderer.Core.Entities;
using TheArtOfDev.HtmlRenderer.Core.Fragments;
using TheArtOfDev.HtmlRenderer.Core.Utils;
using TheArtOfDev.HtmlRenderer.PdfSharp.Adapters;

Expand Down Expand Up @@ -195,9 +197,14 @@ public static async Task AddPdfPages(PdfDocument document, string html, PdfGener
container.PerformLayout(measure);
}

// while there is un-rendered HTML, create another PDF page and render with proper offset for the next page
double scrollOffset = 0;
while (scrollOffset > -container.ActualSize.Height)
// One PDF page per fragmentainer the fragment tree actually materialized - a
// content-empty page slot (CSS Paged Media 3 3.2, e.g. a huge margin that would
// otherwise paginate through blank vertical space - see the margin-truncation
// correction in BlockFragmentation) is simply never in this list, which is what
// gives blank-page skipping for free here instead of the old ceil(height/pageHeight)
// loop's naive page count.
var tree = container.FragmentTree;
foreach (var fragmentainer in tree?.Fragmentainers ?? (IReadOnlyList<FragmentainerFragment>)Array.Empty<FragmentainerFragment>())
{
var page = document.AddPage();
page.Height = XUnit.FromPoint(orgPageSize.Height);
Expand All @@ -206,17 +213,14 @@ public static async Task AddPdfPages(PdfDocument document, string html, PdfGener

using (var g = XGraphics.FromPdfPage(page))
{
//g.IntersectClip(new XRect(config.MarginLeft, config.MarginTop, pageSize.Width, pageSize.Height));
g.IntersectClip(new XRect(0, 0, page.Width.Point, page.Height.Point));

container.ScrollOffset = new XPoint(0, scrollOffset);
container.PerformPaint(g);
container.PerformPaint(g, fragmentainer);
}
scrollOffset -= pageSize.Height;
}

// add web links and anchors
HandleLinks(document, container, orgPageSize, pageSize);
HandleLinks(document, container, orgPageSize, tree);
}
}
}
Expand All @@ -228,44 +232,74 @@ public static async Task AddPdfPages(PdfDocument document, string html, PdfGener
/// <summary>
/// Handle HTML links by create PDF Documents link either to external URL or to another page in the document.
/// </summary>
private static void HandleLinks(PdfDocument document, HtmlContainer container, XSize orgPageSize, XSize pageSize)
private static void HandleLinks(PdfDocument document, HtmlContainer container, XSize orgPageSize, FragmentTree tree)
{
if (tree == null || tree.Fragmentainers.Count == 0)
return;

// Pagination slot -> PDF page index. Not a bare multiply/divide by page height any more:
// a content-empty slot is never materialized as a fragmentainer at all (blank-page
// skipping), so slot indices are not contiguous across tree.Fragmentainers the way a
// fixed-size page grid's would be.
var slotToPage = new Dictionary<int, int>();
for (var pageIndex = 0; pageIndex < tree.Fragmentainers.Count; pageIndex++)
{
slotToPage[tree.Fragmentainers[pageIndex].SlotIndex] = pageIndex;
}

foreach (var link in container.GetLinks())
{
int i = (int)(link.Rectangle.Top / pageSize.Height);
for (; i < document.Pages.Count && pageSize.Height * i < link.Rectangle.Bottom; i++)
foreach (var fragmentainer in tree.Fragmentainers)
{
var offset = pageSize.Height * i;
var bandTop = fragmentainer.Geometry.Top;
var bandBottom = bandTop + fragmentainer.Geometry.Height;
if (link.Rectangle.Top >= bandBottom || link.Rectangle.Bottom <= bandTop)
continue; // this link has no part on this fragmentainer's page

var pageIndex = slotToPage[fragmentainer.SlotIndex];

// fucking position is from the bottom of the page
var xRect = new XRect(link.Rectangle.Left, orgPageSize.Height - (link.Rectangle.Height + link.Rectangle.Top - offset), link.Rectangle.Width, link.Rectangle.Height);
var xRect = new XRect(link.Rectangle.Left, orgPageSize.Height - (link.Rectangle.Height + link.Rectangle.Top - bandTop), link.Rectangle.Width, link.Rectangle.Height);

if (link.IsAnchor)
{
// create link to another page in the document
var anchorRect = container.GetElementRectangle(link.AnchorId);
if (anchorRect.HasValue)
{
var anchorSlot = SlotContaining(tree, anchorRect.Value.Top);
// document links to the same page as the link is not allowed
int anchorPageIdx = (int)(anchorRect.Value.Top / pageSize.Height);

// in case that not find the page index, set to the first page.
if (anchorPageIdx == 0)
anchorPageIdx = 1;

if (i != anchorPageIdx)
document.Pages[i].AddDocumentLink(new PdfRectangle(xRect), anchorPageIdx);
if (anchorSlot.HasValue && slotToPage.TryGetValue(anchorSlot.Value, out var anchorPageIdx) && pageIndex != anchorPageIdx)
{
document.Pages[pageIndex].AddDocumentLink(new PdfRectangle(xRect), anchorPageIdx);
}
}
}
else
{
// create link to URL
document.Pages[i].AddWebLink(new PdfRectangle(xRect), link.Href);
document.Pages[pageIndex].AddWebLink(new PdfRectangle(xRect), link.Href);
}
}
}
}

/// <summary>
/// The pagination slot whose content band contains document-space Y coordinate <paramref name="y"/>,
/// or null if it falls in no materialized fragmentainer's band (e.g. an anchor inside a
/// content-empty page slot that was skipped, or past the end of the document).
/// </summary>
private static int? SlotContaining(FragmentTree tree, double y)
{
foreach (var fragmentainer in tree.Fragmentainers)
{
var bandTop = fragmentainer.Geometry.Top;
if (y >= bandTop && y < bandTop + fragmentainer.Geometry.Height)
return fragmentainer.SlotIndex;
}
return null;
}

#endregion
}
}
24 changes: 21 additions & 3 deletions Source/HtmlRenderer/Core/CssDefaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,20 @@ internal static class CssDefaults
*[DIR=""ltr""] { direction: ltr; unicode-bidi: embed }
*[DIR=""rtl""] { direction: rtl; unicode-bidi: embed }

/* Ported from PeachPDF's CssDefaults (spelt with css-break-3's break-* properties rather
than the legacy page-break-* aliases - the two share their storage and initial value,
see InitialValues below, so this is the same cascade either way). Replaces this engine's
own older `h1 { page-break-before: always }` default, which forced a leading blank page
before any document that opened with a heading now that break-before is actually
consumed by layout - break-after: avoid (keep-with-next) is the behavior real print
engines give headings by default. */
@media print {
h1 { page-break-before: always }
h1, h2, h3,
h4, h5, h6 { page-break-after: avoid }
ul, ol, dl { page-break-before: avoid }
h4, h5, h6 { break-after: avoid }

/* css-tables-3 6.2 repeats a header or footer group across the pages a table spans only
where the group carries an avoid break-inside. */
thead, tfoot { break-inside: avoid }
}

/* Not in the specification but necessary */
Expand Down Expand Up @@ -191,6 +200,14 @@ @media print {
{ "padding-right", "0" },
{ "padding-top", "0" },
{ "page-break-inside", "auto" },
{ "break-inside", "auto" },
{ "break-before", "auto" },
{ "break-after", "auto" },
{ "page-break-before", "auto" },
{ "page-break-after", "auto" },
{ "widows", "2" },
{ "orphans", "2" },
{ "page", "auto" },
{ "text-align", "" },
{ "text-decoration-line", "" },
{ "text-indent", "0" },
Expand Down Expand Up @@ -225,6 +242,7 @@ @media print {
"line-height",
"word-break",
"direction",
"widows", "orphans",
};

/// <summary>
Expand Down
Loading
Loading