Repository navigation
Expand file tree
/
Copy pathPdfGenerator.cs
More file actions
305 lines (271 loc) · 16.1 KB
/
Copy pathPdfGenerator.cs
File metadata and controls
305 lines (271 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"
using PdfSharp;
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;
namespace TheArtOfDev.HtmlRenderer.PdfSharp
{
/// <summary>
/// TODO:a add doc
/// </summary>
public static class PdfGenerator
{
/// <summary>
/// Adds a font mapping from <paramref name="fromFamily"/> to <paramref name="toFamily"/> iff the <paramref name="fromFamily"/> is not found.<br/>
/// When the <paramref name="fromFamily"/> font is used in rendered HTML and is not found in existing
/// fonts (installed or added) it will be replaced by <paramref name="toFamily"/>.<br/>
/// </summary>
/// <remarks>
/// This font mapping can be used as a fallback in case the requested font is not installed in the client system.
/// </remarks>
/// <param name="fromFamily">the font family to replace</param>
/// <param name="toFamily">the font family to replace with</param>
public static void AddFontFamilyMapping(string fromFamily, string toFamily)
{
ArgChecker.AssertArgNotNullOrEmpty(fromFamily, "fromFamily");
ArgChecker.AssertArgNotNullOrEmpty(toFamily, "toFamily");
PdfSharpAdapter.Instance.AddFontFamilyMapping(fromFamily, toFamily);
}
/// <summary>
/// Registers a custom directory to search for font files (TTF/OTF).<br/>
/// Fonts in this directory will be discovered and made available for rendering.
/// </summary>
/// <remarks>
/// This will automatically trigger a rediscovery of all fonts.
/// </remarks>
/// <param name="fontDirectory">the directory path containing font files</param>
public static void RegisterCustomFontDirectory(string fontDirectory)
{
ArgChecker.AssertArgNotNullOrEmpty(fontDirectory, "fontDirectory");
PdfSharpAdapter.Instance.FontResolver.RegisterCustomFontDirectory(fontDirectory);
// Trigger a rediscovery
var fontFamilies = PdfSharpAdapter.Instance.FontResolver.DiscoverFontFamilies();
foreach (var fontFamily in fontFamilies)
{
PdfSharpAdapter.Instance.AddFontFamily(new FontFamilyAdapter(new XFontFamily(fontFamily)));
}
}
/// <summary>
/// Parse the given stylesheet to <see cref="CssData"/> object.<br/>
/// If <paramref name="combineWithDefault"/> is true the parsed CSS blocks are added to the
/// default CSS data (as defined by W3), merged if class name already exists. If false only the data in the given stylesheet is returned.
/// </summary>
/// <seealso cref="http://www.w3.org/TR/CSS21/sample.html"/>
/// <param name="stylesheet">the stylesheet source to parse</param>
/// <param name="combineWithDefault">true - combine the parsed CSS data with default CSS data, false - return only the parsed CSS data</param>
/// <returns>the parsed CSS data</returns>
public static Task<CssData> ParseStyleSheet(string stylesheet, bool combineWithDefault = true)
{
// CssData.Parse -> CssParser.ParseStyleSheet is not itself async yet (its @import resolution
// bridges into the async resource-loading pipeline synchronously for now - it's also called
// directly from several UI controls' public API, out of scope for this conversion) - wrapped
// in Task.FromResult here so this method's own shape matches the rest of this async-only API.
return Task.FromResult(CssData.Parse(PdfSharpAdapter.Instance, stylesheet, combineWithDefault));
}
/// <summary>
/// Create PDF document from given HTML.<br/>
/// </summary>
/// <param name="html">HTML source to create PDF from</param>
/// <param name="pageSize">the page size to use for each page in the generated pdf </param>
/// <param name="margin">the margin to use between the HTML and the edges of each page</param>
/// <param name="cssData">optional: the style to use for HTML rendering (default - use W3 default style)</param>
/// <param name="stylesheetLoad">optional: can be used to overwrite stylesheet resolution logic</param>
/// <param name="imageLoad">optional: can be used to overwrite image resolution logic</param>
/// <returns>the generated image of the HTML</returns>
public static async Task<PdfDocument> GeneratePdf(string html, PageSize pageSize, int margin = 20, CssData cssData = null, EventHandler<HtmlStylesheetLoadEventArgs> stylesheetLoad = null, EventHandler<HtmlImageLoadEventArgs> imageLoad = null)
{
var config = new PdfGenerateConfig();
config.PageSize = pageSize;
config.SetMargins(margin);
return await GeneratePdf(html, config, cssData, stylesheetLoad, imageLoad).ConfigureAwait(false);
}
/// <summary>
/// Create PDF document from given HTML.<br/>
/// </summary>
/// <param name="html">HTML source to create PDF from</param>
/// <param name="config">the configuration to use for the PDF generation (page size/page orientation/margins/etc.)</param>
/// <param name="cssData">optional: the style to use for HTML rendering (default - use W3 default style)</param>
/// <param name="stylesheetLoad">optional: can be used to overwrite stylesheet resolution logic</param>
/// <param name="imageLoad">optional: can be used to overwrite image resolution logic</param>
/// <returns>the generated image of the HTML</returns>
public static async Task<PdfDocument> GeneratePdf(string html, PdfGenerateConfig config, CssData cssData = null, EventHandler<HtmlStylesheetLoadEventArgs> stylesheetLoad = null, EventHandler<HtmlImageLoadEventArgs> imageLoad = null)
{
// create PDF document to render the HTML into
var document = new PdfDocument();
// add rendered PDF pages to document
await AddPdfPages(document, html, config, cssData, stylesheetLoad, imageLoad).ConfigureAwait(false);
return document;
}
/// <summary>
/// Create PDF pages from given HTML and appends them to the provided PDF document.<br/>
/// </summary>
/// <param name="document">PDF document to append pages to</param>
/// <param name="html">HTML source to create PDF from</param>
/// <param name="pageSize">the page size to use for each page in the generated pdf </param>
/// <param name="margin">the margin to use between the HTML and the edges of each page</param>
/// <param name="cssData">optional: the style to use for HTML rendering (default - use W3 default style)</param>
/// <param name="stylesheetLoad">optional: can be used to overwrite stylesheet resolution logic</param>
/// <param name="imageLoad">optional: can be used to overwrite image resolution logic</param>
/// <returns>the generated image of the HTML</returns>
public static Task AddPdfPages(PdfDocument document, string html, PageSize pageSize, int margin = 20, CssData cssData = null, EventHandler<HtmlStylesheetLoadEventArgs> stylesheetLoad = null, EventHandler<HtmlImageLoadEventArgs> imageLoad = null)
{
var config = new PdfGenerateConfig();
config.PageSize = pageSize;
config.SetMargins(margin);
return AddPdfPages(document, html, config, cssData, stylesheetLoad, imageLoad);
}
/// <summary>
/// Create PDF pages from given HTML and appends them to the provided PDF document.<br/>
/// </summary>
/// <param name="document">PDF document to append pages to</param>
/// <param name="html">HTML source to create PDF from</param>
/// <param name="config">the configuration to use for the PDF generation (page size/page orientation/margins/etc.)</param>
/// <param name="cssData">optional: the style to use for HTML rendering (default - use W3 default style)</param>
/// <param name="stylesheetLoad">optional: can be used to overwrite stylesheet resolution logic</param>
/// <param name="imageLoad">optional: can be used to overwrite image resolution logic</param>
/// <returns>the generated image of the HTML</returns>
public static async Task AddPdfPages(PdfDocument document, string html, PdfGenerateConfig config, CssData cssData = null, EventHandler<HtmlStylesheetLoadEventArgs> stylesheetLoad = null, EventHandler<HtmlImageLoadEventArgs> imageLoad = null)
{
XSize orgPageSize;
// get the size of each page to layout the HTML in
if (config.PageSize != PageSize.Undefined)
orgPageSize = PageSizeConverter.ToSize(config.PageSize);
else
orgPageSize = config.ManualPageSize;
if (config.PageOrientation == PageOrientation.Landscape)
{
// invert pagesize for landscape
orgPageSize = new XSize(orgPageSize.Height, orgPageSize.Width);
}
var pageSize = new XSize(orgPageSize.Width - config.MarginLeft - config.MarginRight, orgPageSize.Height - config.MarginTop - config.MarginBottom);
if (!string.IsNullOrEmpty(html))
{
using (var container = new HtmlContainer())
{
if (stylesheetLoad != null)
container.StylesheetLoad += stylesheetLoad;
if (imageLoad != null)
container.ImageLoad += imageLoad;
container.Location = new XPoint(config.MarginLeft, config.MarginTop);
container.MaxSize = new XSize(pageSize.Width, 0);
await container.SetHtml(html, cssData).ConfigureAwait(false);
container.PageSize = pageSize;
container.MarginBottom = config.MarginBottom;
container.MarginLeft = config.MarginLeft;
container.MarginRight = config.MarginRight;
container.MarginTop = config.MarginTop;
// layout the HTML with the page width restriction to know how many pages are required
using (var measure = XGraphics.CreateMeasureContext(pageSize, XGraphicsUnit.Point, XPageDirection.Downwards))
{
container.PerformLayout(measure);
}
// 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);
page.Width = XUnit.FromPoint(orgPageSize.Width);
page.Orientation = config.PageOrientation;
using (var g = XGraphics.FromPdfPage(page))
{
g.IntersectClip(new XRect(0, 0, page.Width.Point, page.Height.Point));
container.PerformPaint(g, fragmentainer);
}
}
// add web links and anchors
HandleLinks(document, container, orgPageSize, tree);
}
}
}
#region Private/Protected methods
/// <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, 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())
{
foreach (var fragmentainer in tree.Fragmentainers)
{
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 - 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
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[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
}
}