Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
be7559f
Wire the CSS Fragmentation vocabulary CssEngine already parses
jhaygood86 Aug 21, 2026
1fb4cd1
Add the immutable fragment record types (Fragments/)
jhaygood86 Aug 21, 2026
925e0ae
Add the fragmentation classification and break-token types
jhaygood86 Aug 21, 2026
3eb352b
Produce a trivial single-fragmentainer FragmentTree after layout
jhaygood86 Aug 21, 2026
fe70643
Add a minimal FragmentPainter that paints from the fragment tree
jhaygood86 Aug 21, 2026
13092c7
Add block-level page-break corrections (D2)
jhaygood86 Aug 21, 2026
7a9499e
Bucket the fragment tree into one fragmentainer per page
jhaygood86 Aug 21, 2026
1ba0f33
Add inline-flow page-break corrections: real widows/orphans (D3)
jhaygood86 Aug 21, 2026
9869c0e
Add table fragmentation: row-level avoidance and repeated <thead> (D4)
jhaygood86 Aug 21, 2026
83e568d
Cut PdfGenerator over to page-per-fragmentainer rendering (F1)
jhaygood86 Aug 21, 2026
51a3b62
Make FragmentPainter the default paint path for WinForms/WPF (F2)
jhaygood86 Aug 21, 2026
46b1b6f
Remove the crude per-word/per-box BreakPage nudge (F3, partial)
jhaygood86 Aug 21, 2026
b3b4507
Add real per-type fragment content painters (image/hr/frame/marker)
jhaygood86 Aug 21, 2026
e7da595
Fix multi-page PDF text/image invisibility, delete CssBox.Paint/PaintImp
jhaygood86 Aug 21, 2026
3ac00ec
R0: paint text from the fragment tree, not live CssRect geometry
jhaygood86 Aug 21, 2026
068b57c
R1: real resumable pass loop for forced page breaks
jhaygood86 Aug 21, 2026
4d5ae58
R3: break-inside:avoid/monolithic relocation via real relayout (R2 fo…
jhaygood86 Aug 21, 2026
9f086b8
R4: real keep-with-next, fixing a genuine pre-existing gap
jhaygood86 Aug 21, 2026
86d624c
R5/R6: neither needed new resumption machinery - fixed real bugs instead
jhaygood86 Aug 21, 2026
8bfdd9a
Fix R1 regression: forced breaks nested inside table cells corrupted …
jhaygood86 Aug 21, 2026
991af5b
R7: table content already benefits from R1-R6 fixes; one known limita…
jhaygood86 Aug 21, 2026
dc57a7c
R9: keep-with-next across a real pass boundary already works; no rewi…
jhaygood86 Aug 21, 2026
17766fb
Fix catastrophic layout corruption when a keep-with-next run exceeds …
jhaygood86 Aug 21, 2026
9bd7663
R10: delete dead break-token/relaxation scaffolding never wired up
jhaygood86 Aug 21, 2026
7b6f874
Fix orphans never enforced on a paragraph's first page-fragment
jhaygood86 Aug 21, 2026
4f7ffcd
Fix rowspan cells silently skipped by the table's break-inside:avoid …
jhaygood86 Aug 21, 2026
2015821
Fix container-left-behind: a moved box's ancestor now follows it up
jhaygood86 Aug 21, 2026
3dff788
Document that list markers survive container relocation without help
jhaygood86 Aug 21, 2026
362dee9
Preserve table rows unfragmented by default, per css-tables-3 6.1
jhaygood86 Aug 22, 2026
d5826e4
Repeat position:fixed content on every page, per css-position-3
jhaygood86 Aug 22, 2026
1be6c4f
Fix CI page-count tests fragile to font substitution on non-Windows
jhaygood86 Aug 22, 2026
059afcb
Fix PaintHarness after rebase: paint from the fragment tree, not dele…
jhaygood86 Aug 29, 2026
5fbc4e0
Port PeachPDF's pure-logic fragmentation unit tests (Batch 1)
jhaygood86 Aug 29, 2026
d8b2587
Port PeachPDF's core forced-break/keep-with-next/orphans-widows tests…
jhaygood86 Aug 29, 2026
11c35b3
Port PeachPDF's fragment-claiming, marker and paint tests (Batch 3)
jhaygood86 Aug 29, 2026
ea99b55
Port PeachPDF's table fragmentation tests (Batch 4)
jhaygood86 Aug 29, 2026
42fff5b
Port PeachPDF's PDF-pipeline end-to-end tests (Batch 5, final)
jhaygood86 Aug 29, 2026
6c67f40
Fix InlineFragmentation's widows/orphans merge-back to shift, not jus…
jhaygood86 Aug 29, 2026
267c3e0
Retire Orphans_RejectsZero: not a real gap, fix the test's own premis…
jhaygood86 Aug 29, 2026
dbe33fe
Fix RelocateIfNeeded off-by-one: a box exactly one page tall can be m…
jhaygood86 Aug 29, 2026
416ea3d
Preserve a box's own top margin at a forced page break
jhaygood86 Aug 29, 2026
d710fff
Update a table's own Location when row-atomicity shifts its first row
jhaygood86 Aug 29, 2026
b97ee61
Port PeachPDF's float/clear layout tests, restore dropped clear cases
jhaygood86 Aug 29, 2026
0b33325
Implement position:relative/absolute/fixed layout, per CSS 2.1 §9.4.3…
jhaygood86 Aug 29, 2026
543e402
Document the z-index/stacking-context paint-order gap with a failing-…
jhaygood86 Aug 29, 2026
9911bd2
Implement CSS 2.1 §12.4 counter-reset/counter-increment for content: …
jhaygood86 Aug 29, 2026
0f793a5
Add <li value> support, fix list-style-type:square's wrong glyph
jhaygood86 Aug 29, 2026
e5ec0fa
Fix a stale Ignore and its own test bug on an already-working ::after…
jhaygood86 Aug 29, 2026
33b4be6
Implement outline painting (CSS 2.1 §18.1) - was parsed but never pai…
jhaygood86 Aug 29, 2026
9c19db3
Add background-color paint-call test coverage
jhaygood86 Aug 29, 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