Recursive AI

Embed missing fonts in a PDF in C#

All onepdf recipes

Open the PDF with editing allowed, give it the font faces from the RecursiveAI.OnePdf.Fonts package, call EmbedSubstituteFonts and save. Each font the file names but doesn’t carry gets a metric-compatible face embedded, so no text moves. The program below does this for the IRS Form W-9, a public PDF you can download to try it.

Terminal
dotnet new console -o EmbedFonts && cd EmbedFonts
dotnet add package RecursiveAI.OnePdf --version 1.14.0
dotnet add package RecursiveAI.OnePdf.Fonts --version 1.14.0
curl -LO https://www.irs.gov/pub/irs-pdf/fw9.pdf

Put this in Program.cs and run dotnet run:

C#
using OnePdf;
 
using PdfDocument doc = PdfDocument.Open("fw9.pdf",
    new PdfOpenOptions { AllowEditing = true });
// the Liberation and Noto faces
doc.FontProvider = OnePdf.Fonts.FontsPackage.Provider;
string before = AllText(doc);
 
// Embed a metric-compatible face for each font the file names but doesn't embed.
PdfSubstituteEmbedReport report = doc.EmbedSubstituteFonts();
foreach (PdfSubstituteEntry e in report.Entries)
    if (e.Outcome != PdfSubstituteOutcome.AlreadyEmbedded)
        Console.WriteLine($"{e.BaseFont}: {e.Message}");
 
doc.Save("fw9-fonts.pdf");
Console.WriteLine($"Text unchanged: {AllText(doc) == before}");
 
static string AllText(PdfDocument doc) =>
    string.Concat(doc.ExtractText().Select(p => p.Text));
Text
ZapfDingbats: Embedded NotoSansSymbols2-Regular as FPQCCR+ZapfDingbats.
Helvetica: Embedded LiberationSans-Regular as BVXYHA+Helvetica.
HelveticaLTStd-Bold: The name is not a standard face or a common alias of one.
Text unchanged: True

The W-9 names three fonts without embedding them, all used by its form fields. ZapfDingbats and Helvetica now have embedded subsets of Noto Sans Symbols 2 and Liberation Sans. The third, HelveticaLTStd-Bold, has no metric-compatible face in the package, so it is reported and left as it was.

How it works

  • Which face. Arial and Helvetica get Liberation Sans, Times and Times New Roman Liberation Serif, Courier Liberation Mono, with bold and italic taken from the name. Symbol gets Noto Sans Math and ZapfDingbats Noto Sans Symbols 2.
  • Nothing moves. Only the glyphs the pages use are embedded, with their widths set to the PDF’s own, so every character keeps its position. WidthChanges and MissingCodes on each entry say where the substitute differs.
  • Where the faces come from. Set doc.FontProvider, call FontsPackage.Register() once at startup, or pass a Provider in PdfSubstituteEmbedOptions. Without one, nothing is embedded and every entry says NoFontProvider.
  • During a save. EmbedSubstituteFonts = true in PdfAOptions or PdfUAOptions does the same as part of a PDF/A or PDF/UA save.
  • Unicode maps. doc.AddMissingToUnicode() adds text mappings that extraction can already derive, so the text can be searched and read aloud. Nothing is guessed.

Limits

  • Letter shapes change. A substitute matches widths, not looks. Liberation Sans is close to Arial; compare the rendered pages where it matters.
  • No substitute for most fonts. Verdana, Calibri, HelveticaLTStd-Bold and other faces without a metric-compatible match report NoSubstitute. If you have the font and the right to embed it, supply it through FaceForFont in PdfSubstituteEmbedOptions.
  • Signatures are invalidated by the full save.

This is the font step of Convert a PDF to PDF/A. Every option and outcome is in Embedding fonts into existing PDFs.