Skip to content

Commit 39358d0

Browse files
committed
Fix page origin, stale fragment tree and anchor page index
- Keep the top margin on every PDF page - Paint the whole document on the continuous surface when the container has a Y offset - Reset the fragment tree in Clear() - Pass one-based page numbers to AddDocumentLink - Don't paginate descendants of fixed boxes
1 parent e5a3704 commit 39358d0

7 files changed

Lines changed: 97 additions & 24 deletions

File tree

‎Source/HtmlRenderer.PdfSharp/PdfGenerator.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ private static void HandleLinks(PdfDocument document, HtmlContainer container, X
259259
var pageIndex = slotToPage[fragmentainer.SlotIndex];
260260

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

264264
if (link.IsAnchor)
265265
{
@@ -271,7 +271,7 @@ private static void HandleLinks(PdfDocument document, HtmlContainer container, X
271271
// document links to the same page as the link is not allowed
272272
if (anchorSlot.HasValue && slotToPage.TryGetValue(anchorSlot.Value, out var anchorPageIdx) && pageIndex != anchorPageIdx)
273273
{
274-
document.Pages[pageIndex].AddDocumentLink(new PdfRectangle(xRect), anchorPageIdx);
274+
document.Pages[pageIndex].AddDocumentLink(new PdfRectangle(xRect), anchorPageIdx + 1); // PdfSharp page numbers are one-based
275275
}
276276
}
277277
}

‎Source/HtmlRenderer/Core/Fragmentation/BlockFragmentation.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ internal static bool TryGetForcedBreakTarget(CssBox box, CssBox prevSibling, dou
120120
internal static void RelocateIfNeeded(RGraphics g, CssBox child)
121121
{
122122
var container = child.HtmlContainer;
123-
if (container == null || !container.HasRealPageGrid || child.IsOutOfFlow)
123+
if (container == null || !container.HasRealPageGrid || child.IsOutOfFlow || child.IsFixed)
124124
return;
125125

126126
var top = child.EffectiveTop;
@@ -188,7 +188,7 @@ internal static void RelocateIfNeeded(RGraphics g, CssBox child)
188188
internal static void EnforceKeepWithNext(RGraphics g, CssBox child)
189189
{
190190
var container = child.HtmlContainer;
191-
if (container == null || !container.HasRealPageGrid || child.IsOutOfFlow)
191+
if (container == null || !container.HasRealPageGrid || child.IsOutOfFlow || child.IsFixed)
192192
return;
193193

194194
var prevSibling = DomUtils.GetPreviousSibling(child);

‎Source/HtmlRenderer/Core/Fragmentation/FragmentEmitter.cs‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ internal FragmentTree Finish()
3939
if (!_container.HasRealPageGrid)
4040
{
4141
// No bounded page grid (WinForms/WPF's continuous-scroll convention, or any container
42-
// that never set a real PageSize) - the whole document is one fragmentainer.
42+
// that never set a real PageSize) - the whole document is one fragmentainer. The band is
43+
// unbounded since content starts at the root's Location.Y, not at 0.
4344
var rect = new RRect(RPoint.Empty, _container.ActualSize);
44-
var band = new PageBand(0, rect.Height);
45+
var band = new PageBand(0, double.MaxValue);
4546
var geometry = new PageBandGeometry(0, rect.Height, _container.MarginTop, _container.MarginRight, _container.MarginBottom, _container.MarginLeft);
4647
var rootFragment = BuildBoxFragment(root, 0, band);
4748
var fragmentainer = new FragmentainerFragment(rect, 0, geometry, 0, rootFragment);
@@ -71,7 +72,7 @@ internal FragmentTree Finish()
7172
{
7273
var bandTop = _container.PageTopOf(slot);
7374
var bandBottom = _container.PageBottomOf(slot);
74-
var band = new PageBand(bandTop, bandBottom);
75+
var band = new PageBand(bandTop, bandBottom, bandTop - _container.MarginTop);
7576

7677
// CSS Paged Media 3 3.2: a page-slot no box has any content in is never materialized -
7778
// this falls out of the walk rather than being special-cased, since a box only gets
@@ -94,7 +95,7 @@ internal FragmentTree Finish()
9495

9596
var rect = new RRect(0, 0, _container.PageSize.Width, band.Height);
9697
var geometry = new PageBandGeometry(bandTop, band.Height, _container.MarginTop, _container.MarginRight, _container.MarginBottom, _container.MarginLeft);
97-
fragmentainers.Add(new FragmentainerFragment(rect, slot, geometry, bandTop, rootFragment));
98+
fragmentainers.Add(new FragmentainerFragment(rect, slot, geometry, band.Origin, rootFragment));
9899
}
99100

100101
return new FragmentTree(fragmentainers);
@@ -259,7 +260,7 @@ private static RRect Clip(RRect rect, PageBand band)
259260
return new RRect(rect.X, top, rect.Width, Math.Max(0, bottom - top));
260261
}
261262

262-
private static RRect ToLocal(RRect rect, PageBand band) => new RRect(rect.X, rect.Y - band.Top, rect.Width, rect.Height);
263+
private static RRect ToLocal(RRect rect, PageBand band) => new RRect(rect.X, rect.Y - band.Origin, rect.Width, rect.Height);
263264

264265
/// <summary>
265266
/// A no-op <see cref="SliceGeometry"/> - every edge is treated as a real box edge, since real

‎Source/HtmlRenderer/Core/Fragmentation/InlineFragmentation.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ internal static void ApplyLineBreaking(CssBox blockBox)
4747
// CollectFixedRoots) - unlike a float or an absolutely-positioned box, which stay in normal
4848
// document flow and must still paginate like anything else, the UA "must not paginate the
4949
// content of fixed-positioned boxes" (css-position-3), so this correction does not apply.
50-
if (container == null || !container.HasRealPageGrid || blockBox.Position == CssConstants.Fixed)
50+
if (container == null || !container.HasRealPageGrid || blockBox.IsFixed)
5151
return;
5252

5353
var lines = blockBox.LineBoxes;

‎Source/HtmlRenderer/Core/Fragmentation/PageBand.cs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,25 @@ namespace TheArtOfDev.HtmlRenderer.Core.Fragmentation
1010
internal readonly struct PageBand
1111
{
1212
public PageBand(double top, double bottom)
13+
: this(top, bottom, top)
14+
{
15+
}
16+
17+
public PageBand(double top, double bottom, double origin)
1318
{
1419
Top = top;
1520
Bottom = bottom;
21+
Origin = origin;
1622
}
1723

1824
public double Top { get; }
1925
public double Bottom { get; }
26+
27+
/// <summary>
28+
/// The document Y that maps to Y=0 on the fragmentainer's own surface. On a page this is the band's
29+
/// top minus the page's top margin, so content keeps the margin above it.
30+
/// </summary>
31+
public double Origin { get; }
2032
public double Height => Bottom - Top;
2133

2234
public bool Contains(double y) => y >= Top && y < Bottom;

‎Source/HtmlRenderer/Core/HtmlContainerInt.cs‎

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,8 @@ public void Clear()
654654

655655
_hoverBoxes = null;
656656
}
657+
658+
FragmentTree = null;
657659
}
658660

659661
/// <summary>
@@ -875,18 +877,9 @@ public void PerformPaint(RGraphics g)
875877
/// <param name="g">the device to use to render</param>
876878
/// <param name="fragmentainer">the fragmentainer to paint</param>
877879
/// <remarks>
878-
/// The pushed clip's Y origin is always 0, never <see cref="MarginTop"/>/<see cref="_location"/>'s
879-
/// Y - unlike <see cref="PerformPaint(RGraphics)"/>'s multi-fragmentainer loop (which paints every
880-
/// band back onto one continuous, absolute-Y surface via a per-band page-origin translate),
881-
/// <paramref name="fragmentainer"/> here is painted alone onto its own fresh surface (a real PDF
882-
/// page, one per <see cref="PdfGenerator"/> loop iteration) with no such translate - so its content
883-
/// paints at exactly the fragment-local coordinates <see cref="Fragmentation.FragmentEmitter"/>
884-
/// already produced (band-local Y = document Y - band top, per that type's own doc comment). A real
885-
/// bug found while confirming this: the clip previously started at Y=<see cref="MarginTop"/>
886-
/// (mirroring the single-surface overload's own absolute-Y convention), silently clipping away the
887-
/// first <see cref="MarginTop"/>-tall strip of every single page's own content - confirmed by a
888-
/// list item landing entirely within that clipped strip and never appearing in the paint log at all,
889-
/// with no exception raised (the visibility cull is a quiet no-op, not a thrown error).
880+
/// The fragmentainer is painted alone on its own surface (one PDF page). Its local coordinates already
881+
/// keep the page's top margin (see <see cref="Fragmentation.PageBand.Origin"/>), so the clip starts at
882+
/// <see cref="MarginTop"/> like the continuous overload's does.
890883
/// </remarks>
891884
internal void PerformPaint(RGraphics g, Fragments.FragmentainerFragment fragmentainer)
892885
{
@@ -895,11 +888,11 @@ internal void PerformPaint(RGraphics g, Fragments.FragmentainerFragment fragment
895888

896889
if (MaxSize.Height > 0)
897890
{
898-
g.PushClip(new RRect(_location.X, 0, Math.Min(_maxSize.Width, PageSize.Width), Math.Min(_maxSize.Height, PageSize.Height)));
891+
g.PushClip(new RRect(_location.X, _location.Y, Math.Min(_maxSize.Width, PageSize.Width), Math.Min(_maxSize.Height, PageSize.Height)));
899892
}
900893
else
901894
{
902-
g.PushClip(new RRect(MarginLeft, 0, PageSize.Width, PageSize.Height));
895+
g.PushClip(new RRect(MarginLeft, MarginTop, PageSize.Width, PageSize.Height));
903896
}
904897

905898
new Paint.FragmentPainter(this).Paint(g, fragmentainer);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using System.Linq;
3+
using HtmlRenderer.IntegrationTest.TestSupport;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
using TheArtOfDev.HtmlRenderer.Adapters.Entities;
6+
using TheArtOfDev.HtmlRenderer.Core;
7+
8+
namespace HtmlRenderer.IntegrationTest.Painting;
9+
10+
/// <summary>
11+
/// Where fragment content lands on the painted surface: a PDF page keeps its top margin, and the continuous
12+
/// (non-paged) surface paints everything even when the container doesn't start at Y=0.
13+
/// </summary>
14+
[TestClass]
15+
public sealed class PageOriginPaintIntegrationTests
16+
{
17+
[TestMethod]
18+
public void EveryPage_KeepsTheTopMargin()
19+
{
20+
const double margin = 20;
21+
var (_, container) = PaintHarness.LayoutPaginated(PaintHarness.Wrap(
22+
"<p style='margin:0;height:150px'>PageOneMarker</p>"
23+
+ "<p style='margin:0;height:150px;page-break-before:always'>PageTwoMarker</p>"),
24+
pageHeight: 200, margin: margin);
25+
26+
Assert.AreEqual(2, container.FragmentTree!.Fragmentainers.Count);
27+
28+
var first = PaintHarness.PaintPage(container, 0).DrawStringCalls.Single(c => c.Text.Contains("PageOneMarker"));
29+
var second = PaintHarness.PaintPage(container, 1).DrawStringCalls.Single(c => c.Text.Contains("PageTwoMarker"));
30+
31+
Assert.IsGreaterThanOrEqualTo(margin - 0.001, first.Point.Y, "first page content starts above the top margin");
32+
Assert.AreEqual(first.Point.Y, second.Point.Y, 0.001, "both pages start their content at the same Y");
33+
}
34+
35+
[TestMethod]
36+
public void ContinuousSurface_WithAnOffset_StillPaintsTheBottomOfTheDocument()
37+
{
38+
var container = new HtmlContainerInt(new MockAdapter())
39+
{
40+
MaxSize = new RSize(400, 0),
41+
Location = new RPoint(0, 300),
42+
PageSize = new RSize(99999, 99999)
43+
};
44+
container.SetHtml(PaintHarness.Wrap(
45+
"<div style='margin:0;height:400px'>top</div><p style='margin:0'>BottomMarker</p>"));
46+
47+
using var layout = new RecordingGraphics();
48+
container.PerformLayout(layout);
49+
Assert.IsFalse(container.HasRealPageGrid);
50+
51+
var g = new RecordingGraphics();
52+
container.PerformPaint(g);
53+
54+
Assert.IsTrue(g.DrawStringCalls.Any(c => c.Text.Contains("BottomMarker")));
55+
}
56+
57+
[TestMethod]
58+
public void Clear_DropsTheFragmentTree()
59+
{
60+
var (_, container) = PaintHarness.Layout(PaintHarness.Wrap("<p>x</p>"));
61+
Assert.IsNotNull(container.FragmentTree);
62+
63+
container.Clear();
64+
65+
Assert.IsNull(container.FragmentTree);
66+
}
67+
}

0 commit comments

Comments
 (0)