of
";
429 |
430 | public bool preferCSSPageSize { get; set; } = false;
431 | public bool generateDocumentOutline { get; set; } = true;
432 |
433 | public string ToJson()
434 | {
435 | // avoid using a serializer
436 | return
437 | $$"""
438 | {
439 | "landscape": {{landscape.ToJson()}},
440 | "printBackground": {{printBackground.ToJson()}},
441 | "scale": {{scale.ToJson()}},
442 | "paperWidth": {{paperWidth.ToJson()}},
443 | "paperHeight": {{paperHeight.ToJson()}},
444 | "marginTop": {{marginTop.ToJson()}},
445 | "marginBottom": {{marginBottom.ToJson()}},
446 | "marginLeft": {{marginLeft.ToJson()}},
447 | "marginRight": {{marginRight.ToJson()}},
448 | "pageRanges": {{(pageRanges ?? string.Empty).ToJson()}},
449 | "headerTemplate": {{headerTemplate.ToJson()}},
450 | "footerTemplate": {{footerTemplate.ToJson()}},
451 | "displayHeaderFooter": {{displayHeaderFooter.ToJson()}},
452 | "preferCSSPageSize": {{preferCSSPageSize.ToJson()}},
453 | "generateDocumentOutline": {{generateDocumentOutline.ToJson()}}
454 | }
455 | """
456 | .Trim();
457 |
458 |
459 | }
460 | }
461 |
--------------------------------------------------------------------------------
/Westwind.WebView.HtmlToPdf/HtmlToPdfHost.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Web.WebView2.Core;
2 | using System;
3 | using System.Drawing;
4 | using System.IO;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using System.Windows.Forms;
9 |
10 | namespace Westwind.WebView.HtmlToPdf
11 | {
12 |
13 |
14 | ///
15 | /// Converts an HTML document to PDF using the Windows WebView control.
16 | ///
17 | ///
18 | /// * Recommend you use a new instance for each PDF generation
19 | /// * Works only on Windows
20 | /// * Requires net8.0-windows target to work
21 | ///
22 | public class HtmlToPdfHost
23 | {
24 | internal WebViewPrintSettings WebViewPrintSettings = new WebViewPrintSettings();
25 | internal TaskCompletionSource
IsCompleteTaskCompletionSource { get; set; } = new TaskCompletionSource();
26 |
27 | ///
28 | /// A flag you can check to see if the conversion process has completed.
29 | ///
30 | public bool IsComplete { get; set; }
31 |
32 | ///
33 | /// The location of the WebView environment folder that is required
34 | /// for WebView operation. Uses a default in the temp folder but you
35 | /// can customize to use an application specific folder.
36 | ///
37 | /// (If you already use a WebView keep all WebViews pointing at the same environment:
38 | /// https://weblog.west-wind.com/posts/2023/Oct/31/Caching-your-WebView-Environment-to-manage-multiple-WebView2-Controls
39 | ///
40 | public string WebViewEnvironmentPath { get; set; } = Path.Combine(Path.GetTempPath(), "WebView2_Environment");
41 |
42 |
43 | ///
44 | /// Options to inject and optimize CSS for print operations in PDF generation.
45 | ///
46 | public PdfCssAndScriptOptions CssAndScriptOptions { get; set; } = new PdfCssAndScriptOptions();
47 |
48 |
49 | ///
50 | /// Specify the background color of the PDF frame which contains
51 | /// the margins of the document.
52 | ///
53 | /// Defaults to white, but if you use a non-white background for your
54 | /// document you'll likely want to match it to your document background.
55 | ///
56 | /// Also note that non-white colors may have to use custom HeaderTemplate and
57 | /// FooterTemplate to set the foregraound color of the text to match the background.
58 | ///
59 | public string BackgroundHtmlColor { get; set; } = "#ffffff";
60 |
61 |
62 | ///
63 | /// If set delays PDF generation to allow the document to complete loading if
64 | /// content is dynamically loaded. By default PDF generation fires off
65 | /// DomContentLoaded which fires when all embedded resources have loaded,
66 | /// but in some cases when resources load very slow, or when resources are dynamically
67 | /// loaded you might need to delay the PDF generation to allow the document to
68 | /// completely load.
69 | ///
70 | /// Specify in milliseconds, default is no delay.
71 | ///
72 | public int DelayPdfGenerationMs { get; set; }
73 |
74 |
75 | ///
76 | /// This method prints a PDF from an HTML URl or File to PDF and awaits
77 | /// the result to be returned. Result is returned as a Memory Stream in
78 | /// result.ResultStream on success.
79 | ///
80 | /// Check result.IsSuccess to check for successful completion.
81 | ///
82 | /// File or URL to print to PDF
83 | /// WebView PDF generation settings
84 | public virtual Task PrintToPdfStreamAsync(string url,
85 | WebViewPrintSettings webViewPrintSettings = null)
86 | {
87 | IsComplete = false;
88 | WebViewPrintSettings = webViewPrintSettings ?? WebViewPrintSettings;
89 |
90 | PdfPrintResult result = new PdfPrintResult()
91 | {
92 | IsSuccess = false,
93 | Message = "PDF generation didn't complete.",
94 | };
95 |
96 | var tcs = new TaskCompletionSource();
97 |
98 | Thread thread = new Thread( () =>
99 | {
100 | // Create a Windows Forms Synchronization Context we can execute
101 | // which works without a desktop!
102 | SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
103 | if (SynchronizationContext.Current == null)
104 | {
105 | tcs.SetResult(new PdfPrintResult { IsSuccess = false, Message = "Couldn't create STA Synchronization Context." });
106 | return;
107 | }
108 | SynchronizationContext.Current.Post( async (state)=>
109 | {
110 | try
111 | {
112 | IsComplete = false;
113 | IsCompleteTaskCompletionSource = new TaskCompletionSource();
114 |
115 | var host = new CoreWebViewHeadlessHost(this);
116 | await host.PrintFromUrlStream(url);
117 |
118 | await IsCompleteTaskCompletionSource.Task;
119 |
120 | if (!host.IsComplete)
121 | {
122 | result = new PdfPrintResult()
123 | {
124 | IsSuccess = false,
125 | Message = "Pdf generation timed out or failed to render inside of a non-Desktop context."
126 | };
127 | }
128 | else
129 | {
130 | result = new PdfPrintResult()
131 | {
132 | IsSuccess = host.IsSuccess,
133 | Message = host.IsSuccess ? "PDF was generated." : "PDF generation failed: " + host.LastException?.Message,
134 | ResultStream = host.ResultStream,
135 | LastException = host.LastException
136 | };
137 | }
138 | tcs.SetResult(result);
139 | }
140 | catch (Exception ex)
141 | {
142 | result.IsSuccess = false;
143 | result.Message = ex.ToString();
144 | result.LastException = ex;
145 | tcs.SetResult(result);
146 | }
147 | finally
148 | {
149 | IsComplete = true;
150 | Application.ExitThread(); // now kill the event loop and thread
151 | }
152 | }, null);
153 | Application.Run(); // Windows Event loop needed for WebView in system context!
154 | });
155 |
156 | thread.SetApartmentState(ApartmentState.STA); // MUST BE STA!
157 | thread.Start();
158 |
159 | return tcs.Task;
160 | }
161 |
162 | ///
163 | /// This method prints a PDF from an HTML URl or File to PDF and awaits
164 | /// the result to be returned. Result is returned as a Memory Stream in
165 | /// result.ResultStream on success.
166 | ///
167 | /// Check result.IsSuccess to check for successful completion.
168 | ///
169 | /// Stream of an HTML document to print to PDF
170 | /// WebView PDF generation settings
171 | /// Encoding of the HTML stream. Defaults to UTF-8
172 | public virtual Task PrintToPdfStreamAsync(Stream htmlStream,
173 | WebViewPrintSettings webViewPrintSettings = null,
174 | Encoding encoding = null)
175 | {
176 | if (encoding == null)
177 | encoding = Encoding.UTF8;
178 |
179 | IsComplete = false;
180 | WebViewPrintSettings = webViewPrintSettings ?? WebViewPrintSettings;
181 |
182 | PdfPrintResult result = new PdfPrintResult()
183 | {
184 | IsSuccess = false,
185 | Message = "PDF generation didn't complete.",
186 | };
187 |
188 | var tcs = new TaskCompletionSource();
189 |
190 | Thread thread = new Thread(() =>
191 | {
192 | // Create a Windows Forms Synchronization Context we can execute
193 | // which works without a desktop!
194 | SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
195 | if (SynchronizationContext.Current == null)
196 | {
197 | tcs.SetResult(new PdfPrintResult { IsSuccess = false, Message = "Couldn't create STA Synchronization Context." });
198 | return;
199 | }
200 | SynchronizationContext.Current.Post(async (state) =>
201 | {
202 | try
203 | {
204 | IsComplete = false;
205 | IsCompleteTaskCompletionSource = new TaskCompletionSource();
206 |
207 | var host = new CoreWebViewHeadlessHost(this);
208 | await host.PrintFromHtmlStreamToStream(htmlStream, encoding);
209 |
210 | await IsCompleteTaskCompletionSource.Task;
211 |
212 | if (!host.IsComplete)
213 | {
214 | result = new PdfPrintResult()
215 | {
216 | IsSuccess = false,
217 | Message = "Pdf generation timed out or failed to render inside of a non-Desktop context."
218 | };
219 | }
220 | else
221 | {
222 | result = new PdfPrintResult()
223 | {
224 | IsSuccess = host.IsSuccess,
225 | Message = host.IsSuccess ? "PDF was generated." : "PDF generation failed: " + host.LastException?.Message,
226 | ResultStream = host.ResultStream,
227 | LastException = host.LastException
228 | };
229 | }
230 | tcs.SetResult(result);
231 | }
232 | catch (Exception ex)
233 | {
234 | result.IsSuccess = false;
235 | result.Message = ex.ToString();
236 | result.LastException = ex;
237 | tcs.SetResult(result);
238 | }
239 | finally
240 | {
241 | IsComplete = true;
242 | Application.ExitThread(); // now kill the event loop and thread
243 | }
244 | }, null);
245 | Application.Run(); // Windows Event loop needed for WebView in system context!
246 | });
247 |
248 | thread.SetApartmentState(ApartmentState.STA); // MUST BE STA!
249 | thread.Start();
250 |
251 | return tcs.Task;
252 | }
253 |
254 |
255 |
256 | // await WebBrowser.CoreWebView2.CallDevToolsProtocolMethodAsync("Page.printToPdf", "{}");
257 |
258 |
259 |
260 |
261 | ///
262 | /// This method prints a PDF from an HTML URl or File to PDF and awaits
263 | /// the result to be returned. Check result.IsSuccess to check for
264 | /// successful completion of the file output generation or use File.Exists()
265 | ///
266 | /// File or URL to print to PDF
267 | /// output file for generated PDF
268 | /// WebView PDF generation settings
269 | public virtual Task PrintToPdfAsync(string url,
270 | string outputFile,
271 | WebViewPrintSettings webViewPrintSettings = null)
272 | {
273 | IsComplete = false;
274 | WebViewPrintSettings = webViewPrintSettings ?? WebViewPrintSettings;
275 |
276 | PdfPrintResult result = new PdfPrintResult {
277 | IsSuccess = false,
278 | Message = "PDF generation didn't complete.",
279 | };
280 |
281 | var tcs = new TaskCompletionSource();
282 | Thread thread = new Thread(() =>
283 | {
284 | // Create a Windows Forms Synchronization Context we can execute
285 | // which works without a desktop!
286 | SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
287 | if (SynchronizationContext.Current == null)
288 | {
289 | tcs.SetResult(new PdfPrintResult { IsSuccess = false, Message = "Couldn't create STA Synchronization Context." });
290 | return;
291 | }
292 | SynchronizationContext.Current.Post(async (state) =>
293 | {
294 | try
295 | {
296 | IsComplete = false;
297 | IsCompleteTaskCompletionSource = new TaskCompletionSource();
298 |
299 | var host = new CoreWebViewHeadlessHost(this);
300 | await host.PrintFromUrl(url, outputFile);
301 |
302 | await IsCompleteTaskCompletionSource.Task;
303 |
304 | if (!host.IsComplete)
305 | {
306 | result = new PdfPrintResult()
307 | {
308 | IsSuccess = false,
309 | Message = "Pdf generation timed out or failed to render inside of a non-Desktop context."
310 | };
311 | }
312 | else
313 | {
314 | result = new PdfPrintResult()
315 | {
316 | IsSuccess = host.IsSuccess,
317 | Message = host.IsSuccess ? "PDF was generated." : "PDF generation failed: " + host.LastException?.Message,
318 | LastException = host.LastException
319 | };
320 | }
321 | tcs.SetResult(result);
322 | }
323 | catch (Exception ex)
324 | {
325 | result.IsSuccess = false;
326 | result.Message = ex.ToString();
327 | result.LastException = ex;
328 | tcs.SetResult(result);
329 | }
330 | finally
331 | {
332 | IsComplete = true;
333 | Application.ExitThread(); // now kill the event loop and thread
334 | }
335 | }, null);
336 | Application.Run(); // Windows Event loop needed for WebView in system context!
337 | });
338 |
339 | thread.SetApartmentState(ApartmentState.STA); // MUST BE STA!
340 | thread.Start();
341 |
342 | return tcs.Task;
343 | }
344 |
345 |
346 | ///
347 | /// This method prints a PDF from an HTML URl or File to PDF
348 | /// using a new thread and a hosted form returning the result
349 | /// as an in-memory stream in result.ResultStream.
350 | ///
351 | /// You get notified via OnPrintCompleteAction 'event' (Action) when the
352 | /// output operation is complete.
353 | ///
354 | /// The filename or URL to print to PDF
355 | /// Optional action to fire when printing (or failure) is complete
356 | /// PDF output options
357 | public virtual void PrintToPdfStream(string url, Action onPrintComplete = null, WebViewPrintSettings webViewPrintSettings = null)
358 | {
359 | WebViewPrintSettings = webViewPrintSettings ?? WebViewPrintSettings;
360 |
361 | PdfPrintResult result = new PdfPrintResult
362 | {
363 | IsSuccess = false,
364 | Message = "PDF generation didn't complete.",
365 | };
366 |
367 | Thread thread = new Thread(() =>
368 | {
369 | // Create a Windows Forms Synchronization Context we can execute
370 | // which works without a desktop!
371 | SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
372 | if (SynchronizationContext.Current == null)
373 | {
374 | IsComplete = true;
375 | onPrintComplete?.Invoke(new PdfPrintResult { IsSuccess = false, Message = "Couldn't create STA Synchronization Context." });
376 | return;
377 | }
378 | SynchronizationContext.Current.Post(async (state) =>
379 | {
380 | try
381 | {
382 | IsComplete = false;
383 | IsCompleteTaskCompletionSource = new TaskCompletionSource();
384 |
385 | var host = new CoreWebViewHeadlessHost(this);
386 | await host.PrintFromUrlStream(url);
387 |
388 | await IsCompleteTaskCompletionSource.Task;
389 |
390 | if (!host.IsComplete)
391 | {
392 | result = new PdfPrintResult()
393 | {
394 | IsSuccess = false,
395 | Message = "Pdf generation timed out or failed to render inside of a non-Desktop context."
396 | };
397 | }
398 | else
399 | {
400 | result = new PdfPrintResult()
401 | {
402 | IsSuccess = host.IsSuccess,
403 | Message = host.IsSuccess ? "PDF was generated." : "PDF generation failed: " + host.LastException?.Message,
404 | ResultStream = host.ResultStream,
405 | LastException = host.LastException
406 | };
407 | }
408 | onPrintComplete?.Invoke(result);
409 | }
410 | catch (Exception ex)
411 | {
412 | result.IsSuccess = false;
413 | result.Message = ex.ToString();
414 | result.LastException = ex;
415 | }
416 | finally
417 | {
418 | IsComplete = true;
419 | Application.ExitThread(); // now kill the event loop and thread
420 | }
421 | }, null);
422 | Application.Run(); // Windows Event loop needed for WebView in system context!
423 | });
424 |
425 | thread.SetApartmentState(ApartmentState.STA);
426 | thread.Start();
427 | }
428 |
429 | ///
430 | /// This method prints a PDF from an HTML Url or File to PDF
431 | /// using a new thread and a hosted form. The method **returns immediately**
432 | /// and returns completion via the `onPrintComplete` Action parameter.
433 | ///
434 | /// This method works in non-UI scenarios as it creates its own STA thread
435 | ///
436 | /// The filename or URL to print to PDF
437 | /// File to generate the output to
438 | /// Action to fire when printing is complete
439 | /// PDF output options
440 | public virtual void PrintToPdf(string url, string outputFile,
441 | Action onPrintComplete = null,
442 | WebViewPrintSettings webViewPrintSettings = null)
443 | {
444 | WebViewPrintSettings = webViewPrintSettings ?? WebViewPrintSettings;
445 |
446 | PdfPrintResult result = new PdfPrintResult
447 | {
448 | IsSuccess = false,
449 | Message = "PDF generation didn't complete.",
450 | };
451 |
452 | Thread thread = new Thread(() =>
453 | {
454 | // Create a Windows Forms Synchronization Context we can execute
455 | // which works without a desktop!
456 | SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
457 | if (SynchronizationContext.Current == null)
458 | {
459 | IsComplete = true;
460 | onPrintComplete?.Invoke(new PdfPrintResult { IsSuccess = false, Message = "Couldn't create STA Synchronization Context." });
461 | return;
462 | }
463 | SynchronizationContext.Current.Post(async (state) =>
464 | {
465 | try
466 | {
467 | IsComplete = false;
468 | IsCompleteTaskCompletionSource = new TaskCompletionSource();
469 |
470 | var host = new CoreWebViewHeadlessHost(this);
471 | await host.PrintFromUrl(url, outputFile);
472 |
473 | await IsCompleteTaskCompletionSource.Task;
474 |
475 | if (!host.IsComplete)
476 | {
477 | result = new PdfPrintResult()
478 | {
479 | IsSuccess = false,
480 | Message = "Pdf generation timed out or failed to render inside of a non-Desktop context."
481 | };
482 | }
483 | else
484 | {
485 | result = new PdfPrintResult()
486 | {
487 | IsSuccess = host.IsSuccess,
488 | Message = host.IsSuccess ? "PDF was generated." : "PDF generation failed: " + host.LastException?.Message,
489 | LastException = host.LastException
490 | };
491 | }
492 | onPrintComplete?.Invoke(result);
493 | }
494 | catch (Exception ex)
495 | {
496 | result.IsSuccess = false;
497 | result.Message = ex.ToString();
498 | result.LastException = ex;
499 | }
500 | finally
501 | {
502 | IsComplete = true;
503 | Application.ExitThread(); // now kill the event loop and thread
504 | }
505 | }, null);
506 | Application.Run(); // Windows Event loop needed for WebView in system context!
507 | });
508 |
509 | thread.SetApartmentState(ApartmentState.STA);
510 | thread.Start();
511 | }
512 | }
513 |
514 | public class PdfCssAndScriptOptions
515 | {
516 | ///
517 | /// Injects @media print CSS that attempts to keep text from breaking across pages by:
518 | ///
519 | /// * Minimizing paragraph breaks
520 | /// * List breaks
521 | /// * Keeping headers and following text together
522 | /// * Keeping code blocks from breaking
523 | ///
524 | /// Uses page-break and break CSS styles to control page breaks. If you already have
525 | /// @media print style in your HTML source you probably don't need this.
526 | ///
527 | public bool KeepTextTogether { get; set; } = false;
528 |
529 | ///
530 | /// Optionally inject custom CSS into the Html document header before printing.
531 | ///
532 | public string CssToInject { get; set; }
533 |
534 |
535 | ///
536 | /// If set to true adds fonts for Windows and Apple native fonts that work best
537 | /// for PDF generation using built-in fonts. This can help reduce the size of the
538 | /// PDF and also improve rendering for extended characters like emojis.
539 | ///
540 | /// Use this if you see invalid characters in your PDF output
541 | ///
542 | public bool OptimizePdfFonts { get; set; }
543 |
544 | ///
545 | /// Not implemented yet.
546 | ///
547 | /// Optionally inject custom JavaScript that can execute before the page is printed.
548 | /// Allows you to potentially modify the page before printing.
549 | ///
550 | public string ScriptToInject { get; set; }
551 | }
552 |
553 | }
554 |
--------------------------------------------------------------------------------