/// <summary> /// Builds the outline asynchronous. /// </summary> /// <param name="coverIncluded">if set to <c>true</c> indicates a cover is included in the PDF.</param> /// <param name="tableOfContentsIncluded">if set to <c>true</c> indicates a table of contents is included in the PDF.</param> /// <param name="outputDottedLinesInTableOfContents">if set to <c>true</c> outputs dotted lines in the table of contents.</param> /// <param name="htmlToPdfFiles">The HTML to PDF files.</param> /// <param name="outlineBuilder">The outline builder.</param> /// <param name="defaultTableOfContentsStyleSheetBuilder">The default table of contents style sheet builder.</param> /// <param name="pdfPrinter">The PDF printer.</param> /// <param name="htmlToPdfOptions">The HTML to PDF options.</param> /// <param name="variables">The variables.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> internal static async Task BuildOutlineAsync( bool coverIncluded, bool tableOfContentsIncluded, bool outputDottedLinesInTableOfContents, ConcurrentBag <HtmlToPdfFile> htmlToPdfFiles, Action <XmlWriter, IReadOnlyCollection <HtmlToPdfFile>, bool> outlineBuilder, Func <bool, string> defaultTableOfContentsStyleSheetBuilder, PdfPrinter pdfPrinter, HtmlToPdfOptions htmlToPdfOptions, Dictionary <string, string> variables) { int tocIndex = coverIncluded ? 1 : 0; int tocPageNumber = htmlToPdfFiles.Where(x => x.Index < tocIndex).Sum(x => x.NumberOfPages) + 1; foreach (HtmlToPdfFile htmlToPdfFile in htmlToPdfFiles.Where(x => x.Index >= tocIndex)) { htmlToPdfFile.Index += 1; } HtmlToPdfFile tocHtmlToPdfFile = new HtmlToPdfFile { Index = tocIndex, // TODO: extract wkhtmltopdf specific details Input = Path.Combine(Path.GetTempPath(), "__WKANCHOR_2").ToLower(), // TODO: localization Title = "Table of Contents", TitleAndHeadings = new List <HtmlHeading> { new HtmlHeading { Level = 0, Page = 0, Text = "Table of Contents", }, new HtmlHeading { Level = 1, Page = tocPageNumber, Text = "Table of Contents", }, }, }; htmlToPdfFiles.Add(tocHtmlToPdfFile); using (TempHtmlFile tempHtmlFile = new TempHtmlFile()) { string defaultTocXsl = defaultTableOfContentsStyleSheetBuilder(outputDottedLinesInTableOfContents); using (StringReader stringReader = new StringReader(defaultTocXsl)) { using (XmlReader tocXslXmlReader = XmlReader.Create(stringReader)) { XslCompiledTransform xslCompiledTransform = new XslCompiledTransform(); xslCompiledTransform.Load(tocXslXmlReader); using (MemoryStream memoryStream = new MemoryStream()) { using (XmlWriter xmlWriter = XmlWriter.Create(memoryStream)) { outlineBuilder(xmlWriter, htmlToPdfFiles, tableOfContentsIncluded); } // Reset stream position to read from the beginning memoryStream.Seek(0, SeekOrigin.Begin); using (XmlReader xmlReader = XmlReader.Create(memoryStream)) { using (XmlWriter xmlWriter = XmlWriter.Create(tempHtmlFile.FilePath)) { xslCompiledTransform.Transform(xmlReader, xmlWriter); } } } } } // print as pdf string pdfFile = await pdfPrinter.PrintAsPdfAsync( tempHtmlFile.FilePath, htmlToPdfOptions, variables, false); int numberOfPages = PdfDocument.CountNumberOfPages(pdfFile); tocHtmlToPdfFile.PdfFilePath = pdfFile; tocHtmlToPdfFile.NumberOfPages = numberOfPages; } }
/// <summary> /// Prints as PDF asynchronously. /// </summary> /// <param name="input">The input.</param> /// <param name="options">The options.</param> /// <param name="variables">The variables.</param> /// <param name="createLinksForHeadings">if set to <c>true</c> creates links for headings.</param> /// <returns> /// A task with the PDF file path as a result. /// </returns> /// <exception cref="System.IO.FileNotFoundException">File not found: {fullPath}.</exception> internal async Task <string> PrintAsPdfAsync( string input, HtmlToPdfOptions options, Dictionary <string, string> variables, bool createLinksForHeadings = true) { string fullPath = Path.GetFullPath(input); if (!File.Exists(fullPath)) { throw new FileNotFoundException($"File not found: {fullPath}", fullPath); } string tempPdfFilePath = TempPdfFile.Create(); NavigationOptions navigationOptions = new NavigationOptions { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }, Timeout = 0, }; AddTagOptions addTagOptions = null; if (!string.IsNullOrEmpty(options.StyleSheet)) { if (File.Exists(options.StyleSheet)) { addTagOptions = new AddTagOptions { Type = "text/css", Path = options.StyleSheet, }; } else { this.logger.LogWarning($"File not found: {options.StyleSheet}"); } } // the paper format takes priority over width and height // if a page width or height is set, unset the paper format PaperFormat paperFormat = options.PaperFormat; string width = options.Width; string height = options.Height; if ((!string.IsNullOrEmpty(width)) || (!string.IsNullOrEmpty(height))) { paperFormat = null; } using (TempCopyHtmlFile tempHtmlFile = new TempCopyHtmlFile(fullPath)) { this.PrependEmptyPages(tempHtmlFile.FilePath, options.PageOffset + options.PageNumberOffset); // TODO: do not create links for headings if not generating an outline if (createLinksForHeadings) { this.CreateLinksForHeadings(tempHtmlFile.FilePath); } string pageRanges = string.Empty; if ((options.PageOffset + options.PageNumberOffset) > 0) { int fromPage = options.PageOffset + options.PageNumberOffset + 1; int toPage = options.PageOffset + options.PageNumberOffset + options.NumberOfPages; pageRanges = $"{fromPage}-"; if (options.NumberOfPages != 0) { pageRanges += $"{toPage}"; } this.logger.LogDebug($"Printing pages {pageRanges} of page '{fullPath}'."); } // page variables Dictionary <string, string> pageVariables = new Dictionary <string, string>(); if (variables != null) { foreach (KeyValuePair <string, string> keyValuePair in variables) { pageVariables.Add(keyValuePair.Key, keyValuePair.Value); } } pageVariables.Add("webpage", fullPath); string footerTemplate = string.Empty; bool displayFooter = options.FooterTemplateBuilder.DisplayTemplate; if (displayFooter) { footerTemplate = options.FooterTemplateBuilder.Build(pageVariables); } string headerTemplate = string.Empty; bool displayHeader = options.HeaderTemplateBuilder.DisplayTemplate; if (displayHeader) { headerTemplate = options.HeaderTemplateBuilder.Build(pageVariables); } PdfOptions pdfOptions = new PdfOptions { DisplayHeaderFooter = displayHeader || displayFooter, FooterTemplate = footerTemplate, Format = paperFormat, HeaderTemplate = headerTemplate, Height = height, Landscape = options.Landscape, MarginOptions = options.MarginOptions, PreferCSSPageSize = false, PageRanges = pageRanges, PrintBackground = options.PrintBackground, Scale = 1, Width = width, }; PolicyResult policyResult = await Policy .Handle <TargetClosedException>() .Or <Exception>() .RetryAsync(2, onRetry: (ex, retryCount, context) => { // executed before each retry // ex. PuppeteerSharp.TargetClosedException: Protocol error(IO.read): Target closed. (Page failed to process Inspector.targetCrashed. Exception of type 'PuppeteerSharp.TargetCrashedException' was thrown.. at PuppeteerSharp.Page.OnTargetCrashed() // at PuppeteerSharp.Page.<Client_MessageReceived>d__230.MoveNext()) // at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) // at PuppeteerSharp.CDPSession.<SendAsync>d__30.MoveNext() // --- End of stack trace from previous location where exception was thrown --- // at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) // at PuppeteerSharp.CDPSession.<SendAsync>d__29`1.MoveNext() // --- End of stack trace from previous location where exception was thrown --- // at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // at PuppeteerSharp.Helpers.ProtocolStreamReader.<ReadProtocolStreamByteAsync>d__1.MoveNext() // --- End of stack trace from previous location where exception was thrown --- // at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) // at PuppeteerSharp.Page.<PdfInternalAsync>d__171.MoveNext() // --- End of stack trace from previous location where exception was thrown --- // at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) // at PuppeteerSharp.Page.<PdfAsync>d__166.MoveNext() // --- End of stack trace from previous location where exception was thrown --- // at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) // at HtmlToPdf.PdfPrinter.<PrintAsPdfAsync>d__3.MoveNext() in D:\a\HtmlToPdf\HtmlToPdf\src\HtmlToPdf\PdfPrinter.cs:line 167 // --- End of inner exception stack trace --- // at HtmlToPdf.PdfPrinter.<PrintAsPdfAsync>d__3.MoveNext() in D:\a\HtmlToPdf\HtmlToPdf\src\HtmlToPdf\PdfPrinter.cs:line 171 // --- End of stack trace from previous location where exception was thrown --- // at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) // at HtmlToPdf.HtmlToPdfProcessor.<>c__DisplayClass0_1.<<ProcessAsync>b__6>d.MoveNext() in D:\a\HtmlToPdf\HtmlToPdf\src\HtmlToPdf\HtmlToPdfProcessor.cs:line 183 // --- End of stack trace from previous location where exception was thrown --- // at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // at HtmlToPdf.HtmlToPdfProcessor.<ProcessAsync>d__0.MoveNext() in D:\a\HtmlToPdf\HtmlToPdf\src\HtmlToPdf\HtmlToPdfProcessor.cs:line 206 // --- End of stack trace from previous location where exception was thrown --- // Verbose:[PdfCommand.PDF.wkhtmltopdf]got ... HtmlToPdf.Console.exe output 0Bytes // at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // at HtmlToPdf.HtmlToPdfProcessor.<ProcessAsync>d__0.MoveNext() in D:\a\HtmlToPdf\HtmlToPdf\src\HtmlToPdf\HtmlToPdfProcessor.cs:line 294 // --- End of stack trace from previous location where exception was thrown --- // at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() // at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) // at HtmlToPdf.Console.Program.<RunAsync>d__7.MoveNext() in D:\a\HtmlToPdf\HtmlToPdf\src\HtmlToPdf.Console\Program.cs:line 216 // Error:[PdfCommand.PDF]Error happen when converting articles/toc.json to Pdf. Details: iTextSharp.text.exceptions.InvalidPdfException: PDF header signature not found. // at iTextSharp.text.pdf.PdfReader..ctor(ReaderProperties properties, IRandomAccessSource byteSource) // at Microsoft.DocAsCode.HtmlToPdf.HtmlToPdfConverter.SaveCore(Stream stream) // at Microsoft.DocAsCode.HtmlToPdf.HtmlToPdfConverter.Save(String outputFileName) // at Microsoft.DocAsCode.HtmlToPdf.ConvertWrapper.<>c__DisplayClass7_0.<ConvertCore>b__1(ManifestItem tocFile) this.logger.LogWarning(ex.ToString()); Thread.Sleep(1000); }) .ExecuteAndCaptureAsync(async() => { using (Page page = await this.browser.NewPageAsync()) { // disable navigation timeout // otherwise, the following exception occurs: // PuppeteerSharp.NavigationException: Timeout of 30000 ms exceeded ---> System.TimeoutException: Timeout of 30000 ms exceeded page.DefaultNavigationTimeout = 0; page.DefaultTimeout = 0; await page.GoToAsync(tempHtmlFile.FilePath, navigationOptions); if (addTagOptions != null) { await page.AddStyleTagAsync(addTagOptions); } await page.WaitForTimeoutAsync(options.JavascriptDelayInMilliseconds); try { await page.PdfAsync(tempPdfFilePath, pdfOptions); } finally { await page.CloseAsync(); } } }); if (policyResult.Outcome == OutcomeType.Failure) { if (policyResult.FinalException is TargetClosedException) { throw policyResult.FinalException; } else { throw new PrintToPdfException(fullPath, policyResult.FinalException); } } } return(tempPdfFilePath); }
/// <summary> /// Converts the HTML files to a PDF. /// </summary> /// <param name="options">The options.</param> /// <param name="logger">The logger.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> public static async Task <ConcurrentBag <HtmlToPdfFile> > ProcessAsync(Options options, ILogger logger) { DateTime dtNow = DateTime.Now; // local time Encoding encoding = Encoding.Default; if (!string.IsNullOrEmpty(options.Encoding)) { encoding = Encoding.GetEncoding(options.Encoding); } if (!options.Inputs.Any() && !options.DumpDefaultTocXsl) { throw new ApplicationException($"At least one input must be specified."); } if (string.IsNullOrEmpty(options.OutputFilePath) && !options.DumpDefaultTocXsl) { throw new ApplicationException($"An output must be specified."); } if (options.DumpDefaultTocXsl) { string defaultTocXsl = options.DefaultTableOfContentsStyleSheetBuilder(options.OutputDottedLinesInTableOfContents); logger.LogOutput(defaultTocXsl); } ConcurrentBag <HtmlToPdfFile> htmlToPdfFiles = new ConcurrentBag <HtmlToPdfFile>(); foreach (string input in options.Inputs) { logger.LogDebug(input); } string outputFilePath = options.OutputFilePath; options.UserStyleSheet = options.UserStyleSheet?.Trim('"'); string title = options.Title; BrowserDownloader.DownloadBrowser(logger); using (TempDirectory tempDirectory = new TempDirectory()) { var launchOptions = new LaunchOptions { SlowMo = 0, Headless = true, Timeout = 0, LogProcess = false, EnqueueTransportMessages = true, Devtools = false, WebSocketFactory = WebSocketFactory, UserDataDir = tempDirectory.DirectoryPath, Args = options.AdditionalArguments?.ToArray() ?? new string[] { }, }; MarginOptions marginOptions = new MarginOptions { Bottom = options.BottomMargin, Left = options.LeftMargin, Right = options.RightMargin, Top = options.TopMargin, }; await Policy .Handle <ProcessException>() .RetryForeverAsync(onRetry: ex => { // executed before each retry // https://github.com/hardkoded/puppeteer-sharp/issues/1509 // ex. PuppeteerSharp.ProcessException: Failed to launch Chromium! [0909/142354.872:FATAL:feature_list.cc(282)] Check failed: !g_initialized_from_accessor. // Error: Backtrace: // Error: ovly_debug_event [0x00007FFE262A1252+16183762] // Error: ovly_debug_event [0x00007FFE262A0832+16181170] // Error: ovly_debug_event [0x00007FFE262B3383+16257795] // Error: ovly_debug_event [0x00007FFE262A3386+16192262] // Error: ovly_debug_event [0x00007FFE25DF4B2E+11283118] // Error: ovly_debug_event [0x00007FFE2621DB58+15645400] // Error: ovly_debug_event [0x00007FFE2621DACD+15645261] // Error: ovly_debug_event [0x00007FFE26248F28+15822504] // Error: ovly_debug_event [0x00007FFE2621D35E+15643358] // Error: ovly_debug_event [0x00007FFE262483E3+15819619] // Error: ovly_debug_event [0x00007FFE262482BB+15819323] // Error: ovly_debug_event [0x00007FFE262480F2+15818866] // Error: ChromeMain [0x00007FFE253311B6+286] // Error: Ordinal0 [0x00007FF65A33275F+10079] // Error: Ordinal0 [0x00007FF65A33182D+6189] // Error: GetHandleVerifier [0x00007FF65A43B7C2+697538] // Error: BaseThreadInitThunk [0x00007FFE5B2D84D4+20] // Error: RtlUserThreadStart [0x00007FFE5B95E871+33] // ex. PuppeteerSharp.ProcessException: Failed to create connection ---> System.TimeoutException: Timeout of 30000 ms exceeded logger.LogWarning(ex.ToString()); Thread.Sleep(1000); }) .ExecuteAsync(async() => { bool coverAdded = false; using (Browser browser = await Puppeteer.LaunchAsync(launchOptions)) { try { PdfPrinter pdfPrinter = new PdfPrinter(browser, logger); // cover options HtmlToPdfOptions htmlToPdfOptions = new HtmlToPdfOptions { StyleSheet = options.UserStyleSheet, JavascriptDelayInMilliseconds = options.JavascriptDelayInMilliseconds, Landscape = options.Landscape, PaperFormat = options.PaperFormat, Height = options.PageHeight, Width = options.PageWidth, PrintBackground = options.PrintBackground, }; if (!string.IsNullOrEmpty(options.Cover) && (!coverAdded)) { // print cover string pdfFile = await pdfPrinter.PrintAsPdfAsync( options.Cover, htmlToPdfOptions, null); int numberOfPages = PdfDocument.CountNumberOfPages(pdfFile); logger.LogDebug($"Cover file \"{options.Cover}\" contains number of PDF pages: {numberOfPages}."); HtmlToPdfFile htmlToPdfFile = new HtmlToPdfFile { Input = options.Cover, Index = 0, PdfFilePath = pdfFile, PrintHeaderAndFooter = false, NumberOfPages = numberOfPages, }; htmlToPdfFiles.Add(htmlToPdfFile); coverAdded = true; } // page options htmlToPdfOptions.MarginOptions = marginOptions; // header htmlToPdfOptions.HeaderTemplateBuilder.Left = options.HeaderLeft; htmlToPdfOptions.HeaderTemplateBuilder.Center = options.HeaderCenter; htmlToPdfOptions.HeaderTemplateBuilder.Right = options.HeaderRight; string headerFontSize = options.HeaderFontSize.AppendUnits("px"); htmlToPdfOptions.HeaderTemplateBuilder.FontSize = headerFontSize; htmlToPdfOptions.HeaderTemplateBuilder.FontName = options.HeaderFontName; htmlToPdfOptions.HeaderTemplateBuilder.Html = options.HeaderHtml; // footer htmlToPdfOptions.FooterTemplateBuilder.Left = options.FooterLeft; htmlToPdfOptions.FooterTemplateBuilder.Center = options.FooterCenter; htmlToPdfOptions.FooterTemplateBuilder.Right = options.FooterRight; string footerFontSize = options.FooterFontSize.AppendUnits("px"); htmlToPdfOptions.FooterTemplateBuilder.FontSize = footerFontSize; htmlToPdfOptions.FooterTemplateBuilder.FontName = options.FooterFontName; htmlToPdfOptions.FooterTemplateBuilder.Html = options.FooterHtml; // global header/footer variables // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF Dictionary <string, string> variables = new Dictionary <string, string> { { "page", "<span class=\"pageNumber\"></span>" }, { "date", dtNow.ToString("d") }, // M/dd/yyyy { "title", "<span class=\"title\"></span>" }, { "frompage", (options.PageOffset + 1).ToString() }, { "isodate", dtNow.ToString("yyyy-MM-dd") }, // ISO 8601 extended format { "time", dtNow.ToString("h:mm:ss tt") }, // ex. 3:58:45 PM { "doctitle", title }, }; // count the number of PDF pages each HTML file will be printed as var tasks = options.Inputs .Where(x => htmlToPdfFiles.All(y => y.Input != x)) .Select(async input => { // print as pdf // insert an empty page to avoid unexpected margins on the first page, which would affect the page count // https://stackoverflow.com/a/55480268/90287 // https://github.com/puppeteer/puppeteer/issues/2592 HtmlToPdfOptions tempHtmlToPdfOptions = htmlToPdfOptions.Copy(); tempHtmlToPdfOptions.PageOffset = 1; string pdfFile = await pdfPrinter.PrintAsPdfAsync( input, tempHtmlToPdfOptions, variables); // count the number of pages int numberOfPages = PdfDocument.CountNumberOfPages(pdfFile); logger.LogDebug($"\"{input}\" contains number of PDF pages: {numberOfPages}."); HtmlToPdfFile htmlToPdfFile = new HtmlToPdfFile { Input = input, Index = options.Inputs.IndexOf(input), PdfFilePath = pdfFile, PrintHeaderAndFooter = true, NumberOfPages = numberOfPages, }; htmlToPdfFiles.Add(htmlToPdfFile); }); await Task.WhenAll(tasks); variables.Add("topage", htmlToPdfFiles.Sum(x => x.NumberOfPages).ToString()); // update models with title and headings List <Task> updateTitleAndHeadingsTasks = new List <Task>(); foreach (HtmlToPdfFile htmlToPdfFile in htmlToPdfFiles) { updateTitleAndHeadingsTasks.Add(Task.Run(() => { // set the title and headings HtmlFileParser htmlFileParser = new HtmlFileParser(htmlToPdfFile.Input); htmlToPdfFile.TitleAndHeadings = htmlFileParser.GetTitleAndHeadings(options.AddTableOfContents); htmlToPdfFile.Title = htmlToPdfFile.TitleAndHeadings.First().Text; })); } await Task.WhenAll(updateTitleAndHeadingsTasks); // create table of contents if (options.AddTableOfContents) { await PdfOutlineBuilder.BuildOutlineAsync( coverAdded, options.AddTableOfContents, options.OutputDottedLinesInTableOfContents, htmlToPdfFiles, options.OutlineBuilder, options.DefaultTableOfContentsStyleSheetBuilder, pdfPrinter, htmlToPdfOptions, variables); } // update models and re-print HTML files to include headers/footers with page numbers tasks = htmlToPdfFiles.Select(async htmlToPdfFile => { if (string.IsNullOrEmpty(title) && (htmlToPdfFile.Index == 0)) { // set the PDF title title = htmlToPdfFile.Title; variables["doctitle"] = title; } // sum the number of pages in previous documents to get the current page number offset int currentPageNumber = htmlToPdfFiles .Where(x => x.Index < htmlToPdfFile.Index) .Sum(x => x.NumberOfPages) + 1; if ((currentPageNumber + htmlToPdfFile.NumberOfPages) <= (options.PageOffset + 1)) { logger.LogDebug($"Skipping printing {htmlToPdfFile.Input}"); htmlToPdfFile.Skip = true; return; } // print as pdf with page number offset htmlToPdfFile.OutputPdfFilePageNumber = currentPageNumber; logger.LogDebug($"'{htmlToPdfFile.Input}' mapped to output PDF file page number {currentPageNumber}."); htmlToPdfOptions.PageOffset = currentPageNumber - 1; htmlToPdfOptions.PageNumberOffset = options.PageOffset; htmlToPdfOptions.NumberOfPages = htmlToPdfFile.NumberOfPages; // TODO: only print as PDF again if topage variable is actually used in the header/footer if (htmlToPdfFile.PrintHeaderAndFooter) { // delete previously created PDF file File.Delete(htmlToPdfFile.PdfFilePath); // print as pdf string pdfFile = await pdfPrinter.PrintAsPdfAsync( htmlToPdfFile.Input, htmlToPdfOptions, variables); htmlToPdfFile.PdfFilePath = pdfFile; } // parse PDF to get heading page numbers PdfDocument.SetHeadingPageNumbers(htmlToPdfFile); }); await Task.WhenAll(tasks); } finally { await browser.CloseAsync(); } } }); } // merge pdf files List <string> pdfFilesToMerge = htmlToPdfFiles .Where(x => !x.Skip) .OrderBy(x => x.OutputPdfFilePageNumber) .Select(x => x.PdfFilePath) .ToList(); if (!string.IsNullOrEmpty(outputFilePath)) { byte[] mergedBytes = PdfMerger.Merge(pdfFilesToMerge); File.WriteAllBytes(outputFilePath, mergedBytes); try { // update external file links to internal document links PdfDocument.UpdateLinks(outputFilePath, htmlToPdfFiles, logger); } catch (Exception ex) { throw new UpdatePdfLinksException(outputFilePath, htmlToPdfFiles, ex); } PdfDocument.SetTitle(outputFilePath, title); } // delete temporary PDF files var deleteTempFileTasks = htmlToPdfFiles .Where(x => !string.IsNullOrEmpty(x.PdfFilePath)) .Select(async input => { await Task.Factory.StartNew(() => { if (File.Exists(input.PdfFilePath)) { File.Delete(input.PdfFilePath); } }); }); await Task.WhenAll(deleteTempFileTasks); return(htmlToPdfFiles); }