Skip to content

Commit

Permalink
feat: support PDF header footer template (dotnet#9543)
Browse files Browse the repository at this point in the history
  • Loading branch information
yufeih authored and p-kostov committed Jun 28, 2024
1 parent 2474608 commit 44eb5b5
Show file tree
Hide file tree
Showing 16 changed files with 940 additions and 871 deletions.
22 changes: 22 additions & 0 deletions docs/docs/pdf.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ Indicates whether to include a "Table of Contents" pages at the beginning.

A path to an HTML page relative to the root of the output directory. The HTML page will be inserted at the beginning of the PDF file as cover page.

### `pdfHeaderTemplate`

HTML template for the print header. Should be valid HTML markup with following HTML elements used to inject printing values into them:

- `<span class='pageNumber'></span>`: current page number.
- `<span class='totalPages'></span>`: total pages in the document.

> [!NOTE]
> For text to appear in the header and footer HTML template, you need to explicitly set the `font-size` CSS style.

### `pdfFooterTemplate`

HTML template for the print footer. Should use the same format as the [header template](#pdfheadertemplate). Uses the following default footer template if unspecified:

```html
<div style="width: 100%; font-size: 12px;">
<div style="float: right; padding: 0 2em">
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>
</div>
```

> [!NOTE]
> For the cover page to appear in PDF, it needs to be included in build.
> For instance, if `cover.md` is outputted to `_site/cover.html`, you should set `pdfCoverPage` to `cover.html`.
Expand Down
85 changes: 66 additions & 19 deletions src/Docfx.App/PdfBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

using UglyToad.PdfPig;
using UglyToad.PdfPig.Actions;
using UglyToad.PdfPig.Content;
using UglyToad.PdfPig.Graphics.Operations.SpecialGraphicsState;
using UglyToad.PdfPig.Outline;
using UglyToad.PdfPig.Outline.Destinations;
using UglyToad.PdfPig.Writer;
Expand All @@ -40,6 +40,9 @@ class Outline
public string? pdfFileName { get; init; }
public bool pdfTocPage { get; init; }
public string? pdfCoverPage { get; init; }

public string? pdfHeaderTemplate { get; init; }
public string? pdfFooterTemplate { get; init; }
}

public static Task Run(BuildJsonConfig config, string configDirectory, string? outputDirectory = null)
Expand Down Expand Up @@ -88,6 +91,7 @@ public static async Task CreatePdf(string outputFolder)

using var pageLimiter = new SemaphoreSlim(Environment.ProcessorCount, Environment.ProcessorCount);
var pagePool = new ConcurrentBag<IPage>();
var headerFooterCache = new ConcurrentDictionary<(string, string), Task<byte[]>>();

await AnsiConsole.Progress().StartAsync(async progress =>
{
Expand All @@ -99,7 +103,7 @@ await Parallel.ForEachAsync(pdfTocs, async (item, _) =>
var outputPath = Path.Combine(outputFolder, outputName);

await CreatePdf(
PrintPdf, task, new(baseUrl, url), toc, outputPath,
PrintPdf, PrintHeaderFooter, task, new(baseUrl, url), toc, outputPath,
pageNumbers => pdfPageNumbers[url] = pageNumbers);

task.Value = task.MaxValue;
Expand Down Expand Up @@ -163,10 +167,50 @@ IResult TocPage(string url)
pageLimiter.Release();
}
}

Task<byte[]> PrintHeaderFooter(Outline toc, int pageNumber, int totalPages)
{
var headerTemplate = ExpandTemplate(toc.pdfHeaderTemplate, pageNumber, totalPages);
var footerTemplate = ExpandTemplate(toc.pdfFooterTemplate ?? DefaultFooterTemplate, pageNumber, totalPages);

return headerFooterCache.GetOrAdd((headerTemplate, footerTemplate), _ => PrintHeaderFooterCore());

async Task<byte[]> PrintHeaderFooterCore()
{
await pageLimiter.WaitAsync();
var page = pagePool.TryTake(out var pooled) ? pooled : await context.NewPageAsync();

try
{
await page.GotoAsync("about:blank");

return await page.PdfAsync(new()
{
DisplayHeaderFooter = true,
HeaderTemplate = headerTemplate,
FooterTemplate = footerTemplate,
});
}
finally
{
pagePool.Add(page);
pageLimiter.Release();
}
}

static string ExpandTemplate(string? pdfTemplate, int pageNumber, int totalPages)
{
return (pdfTemplate ?? "")
.Replace("<span class='pageNumber'></span>", $"<span>{pageNumber}</span>")
.Replace("<span class=\"pageNumber\"></span>", $"<span>{pageNumber}</span>")
.Replace("<span class='totalPages'></span>", $"<span>{totalPages}</span>")
.Replace("<span class=\"totalPages\"></span>", $"<span>{totalPages}</span>");
}
}
}

static async Task CreatePdf(
Func<Uri, Task<byte[]?>> printPdf, ProgressTask task,
Func<Uri, Task<byte[]?>> printPdf, Func<Outline, int, int, Task<byte[]>> printHeaderFooter, ProgressTask task,
Uri outlineUrl, Outline outline, string outputPath, Action<Dictionary<Outline, int>> updatePageNumbers)
{
var tempDirectory = Path.Combine(Path.GetTempPath(), ".docfx", "pdf", "pages");
Expand Down Expand Up @@ -280,30 +324,24 @@ async Task MergePdf()
for (var i = 1; i <= document.NumberOfPages; i++)
{
pageNumber++;

var pageBuilder = builder.AddPage(document, i, x => CopyLink(node, x));

if (isTocPage)
continue;

// Draw page number before PDF content to
// 1. Allow backgrounds in PDF content to cover page numbers.
// 2. Use the default PDF rendering transformation matrix because chromium resets the matrix.
pageBuilder.SelectContentStream(0);
pageBuilder.NewContentStreamBefore();
var headerFooter = await printHeaderFooter(outline, pageNumber, numberOfPages);
using var headerFooterDocument = PdfDocument.Open(headerFooter);

DrawPageNumber(pageBuilder, document.GetPage(i), pageNumber);
}
}
pageBuilder.NewContentStreamBefore();
pageBuilder.CurrentStream.Operations.Add(Push.Value);

void DrawPageNumber(PdfPageBuilder pageBuilder, Page page, int pageNumber)
{
const int FontSize = 10;
const int Margin = 10;
// PDF produced by chromimum modifies global transformation matrix.
// Push and pop graphics state to fix graphics state
pageBuilder.CopyFrom(headerFooterDocument.GetPage(1));

var text = $"{pageNumber}";
var letters = pageBuilder.MeasureText(text, FontSize, new(0, 0), font);
var width = letters[^1].GlyphRectangle.Right;
pageBuilder.AddText(text, FontSize, new(page.Width - width - Margin, Margin), font);
pageBuilder.CurrentStream.Operations.Add(Pop.Value);
}
}
}

Expand Down Expand Up @@ -462,4 +500,13 @@ static HtmlTemplate TocHtmlTemplate(Uri baseUrl, Outline node, Dictionary<Outlin
}
})
""";

static string DefaultFooterTemplate =>
"""
<div style="width: 100%; font-size: 12px;">
<div style="float: right; padding: 0 2em">
<span class="pageNumber"></span> / <span class="totalPages"></span>
</div>
</div>
""";
}
Loading

0 comments on commit 44eb5b5

Please sign in to comment.