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.
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:
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));
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.
WidthChangesandMissingCodeson each entry say where the substitute differs. - Where the faces come from. Set
doc.FontProvider, callFontsPackage.Register()once at startup, or pass aProviderinPdfSubstituteEmbedOptions. Without one, nothing is embedded and every entry saysNoFontProvider. - During a save.
EmbedSubstituteFonts = trueinPdfAOptionsorPdfUAOptionsdoes 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 throughFaceForFontinPdfSubstituteEmbedOptions. - 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.