ScreenshotNeo

BlogHTML to image & PDF

How to Fix “No Appropriate Font Found” in PdfSharpCore HtmlRendererCore

Fix PdfSharpCore’s “No appropriate font found” error with a cross-platform font resolver, deployment checklist, troubleshooting guide, and runnable code.

By the ScreenshotNeo team30 September 20268 min read

How to Fix “No Appropriate Font Found” in PdfSharpCore HtmlRendererCore

Direct answer: No appropriate font found. means PDFsharp could not resolve a usable font face for the requested family and style. In PdfSharpCore or the PDFsharp Core build, the reliable fix is to ship the required TTF or OTF files, implement IFontResolver for every family and style you use, and assign it to GlobalFontSettings.FontResolver before creating fonts or calling PdfGenerator.GeneratePdf. Installing a font on your development machine is not enough for Linux, Docker, Kubernetes, or other reproducible deployments.

What the exception actually means

PDFsharp throws InvalidOperationException("No appropriate font found.") when its font factory cannot resolve the requested typeface. The failure is in font resolution, not proof that the family name is universally invalid. A resolver can fail because the family is absent, the requested bold or italic face is not mapped, the font bytes were not deployed, or a renderer path requested a fallback family you did not expect.

The official PDFsharp font-management documentation describes font resolving as obtaining a font face from a specified typeface. In the Core build, that responsibility belongs to your application. See the PDFsharp font-management documentation.

First identify the PDFsharp build and runtime

Package flavor determines how fonts are discovered:

Build Supported environment Font behavior Typical choice
Core Cross-platform .NET, including Windows, Linux, and macOS Supply fonts through IFontResolver or an application resolver Web apps, services, containers, and portable applications
GDI+ Windows only Can use Windows font discovery Windows-specific applications that intentionally depend on GDI+
WPF Windows only Uses WPF font facilities Windows desktop applications

PDFsharp’s package guide describes the Core build as pure .NET and cross-platform, while GDI+ and WPF are Windows-only variants. For a console or web application that must run on more than Windows, use the unsuffixed PDFsharp package and treat fonts as application assets. Do not assume a host-installed font exists in a container.

Implement a resolver with real font files

The resolver needs two pieces:

A resolver maps each requested style to a font file before PDF rendering.
A resolver maps each requested style to a font file before PDF rendering.
  1. ResolveTypeface maps a requested family and style to a stable face name.
  2. GetFont returns the bytes for that face name.

The following example supports normal, bold, italic, and bold-italic Tinos faces. Add the font files to your project and mark them as embedded resources, or copy them to the output directory with a deterministic path.

using PdfSharp.Fonts;
using System.Reflection;

public sealed class AppFontResolver : IFontResolver
{
    private static readonly Assembly Assembly = typeof(AppFontResolver).Assembly;

    public string DefaultFontName => "Tinos#regular";

    public FontResolverInfo? ResolveTypeface(
        string familyName,
        bool isBold,
        bool isItalic)
    {
        var family = familyName.Trim().ToLowerInvariant();

        // Replace aliases and CSS fallbacks with a family you actually ship.
        if (family is "times new roman" or "serif" or "tinos")
        {
            if (isBold && isItalic) return new FontResolverInfo("Tinos#bolditalic");
            if (isBold) return new FontResolverInfo("Tinos#bold");
            if (isItalic) return new FontResolverInfo("Tinos#italic");
            return new FontResolverInfo("Tinos#regular");
        }

        // Returning null is appropriate only for families you deliberately reject.
        return null;
    }

    public byte[] GetFont(string faceName)
    {
        var resourceName = faceName switch
        {
            "Tinos#regular" => "MyApp.Fonts.Tinos-Regular.ttf",
            "Tinos#bold" => "MyApp.Fonts.Tinos-Bold.ttf",
            "Tinos#italic" => "MyApp.Fonts.Tinos-Italic.ttf",
            "Tinos#bolditalic" => "MyApp.Fonts.Tinos-BoldItalic.ttf",
            _ => throw new InvalidOperationException($"Unknown font face: {faceName}")
        };

        using var stream = Assembly.GetManifestResourceStream(resourceName)
            ?? throw new FileNotFoundException($"Embedded font not found: {resourceName}");
        using var buffer = new MemoryStream();
        stream.CopyTo(buffer);
        return buffer.ToArray();
    }
}

Register the resolver once, before any font object, document, or HTML renderer is created:

using PdfSharp.Fonts;

if (GlobalFontSettings.FontResolver is null)
{
    GlobalFontSettings.FontResolver = new AppFontResolver();
}

var pdf = PdfGenerator.GeneratePdf(html, PageSize.A4);
pdf.Save("output.pdf");

In an ASP.NET Core application, perform registration during startup, before requests can render PDFs. Avoid replacing a resolver installed by another library; configure one shared resolver that covers every family used by your application.

Make the font files available at runtime

Embedded resources

Embedding keeps deployment self-contained. Set each file’s build action to EmbeddedResource. The manifest name is normally based on the default namespace and folder path; inspect the compiled assembly or use a known naming convention if your project changes its root namespace.

Copied files

You can store fonts under a known application directory and read them with AppContext.BaseDirectory. This is simpler to inspect in a container, but your Dockerfile and publish profile must copy the files:

public byte[] GetFont(string faceName)
{
    var file = faceName switch
    {
        "Tinos#regular" => "Tinos-Regular.ttf",
        "Tinos#bold" => "Tinos-Bold.ttf",
        "Tinos#italic" => "Tinos-Italic.ttf",
        "Tinos#bolditalic" => "Tinos-BoldItalic.ttf",
        _ => throw new InvalidOperationException(faceName)
    };

    return File.ReadAllBytes(Path.Combine(
        AppContext.BaseDirectory, "Fonts", file));
}

For containers, verify the published image contains the files and that the process can read them. Keep font licensing and redistribution rights with your project records; a font that is technically usable may not be licensed for redistribution.

Cover CSS, fallback, and hidden renderer requests

Inspect every font-family declaration, including stylesheets and templates. Map the four common combinations explicitly: normal, bold, italic, and bold-italic. A family that works for body text can still fail when a heading uses font-weight: 700 or an em element uses italic styling.

Container deployments need explicit font assets instead of relying on host-installed fonts.
Container deployments need explicit font assets instead of relying on host-installed fonts.

Do not inspect only your HTML. Error and diagnostic paths can create fonts internally. A documented Linux/Docker report found that ImageRenderer.RenderFailureImage requested hardcoded Courier New. If your resolver handles only the family in your CSS, a missing-image path can still throw the same exception. Map such families to a supplied equivalent or include the required font bytes.

if (family is "courier new" or "courier" or "monospace")
{
    if (isBold && isItalic) return new FontResolverInfo("Tinos#bolditalic");
    if (isBold) return new FontResolverInfo("Tinos#bold");
    if (isItalic) return new FontResolverInfo("Tinos#italic");
    return new FontResolverInfo("Tinos#regular");
}

Mapping a generic CSS family to a metrically different font can alter line wrapping. If exact pagination matters, use a deliberately selected family and validate the resulting layout.

Diagnostic workflow

  1. Capture the exact request. Record the family, weight, italic flag, stack trace, and the HTML or CSS being rendered.
  2. Confirm the package. Check whether the application references PDFsharp Core, GDI+, WPF, PdfSharpCore, and which target framework it runs.
  3. Check the deployment OS. Reproduce in the same Linux distribution, Windows image, macOS host, or container used in production.
  4. Inventory font assets. Confirm every TTF or OTF file is embedded or present in the published output.
  5. Trace resolver coverage. Log requested family and style during development. Ensure all four style combinations return a face name.
  6. Exercise fallback paths. Render normal text, bold, italic, bold-italic, CSS fallback families, missing images, and error pages.
  7. Register early. Set GlobalFontSettings.FontResolver before XFont, PdfGenerator.GeneratePdf, or any document rendering call.

Common errors and fixes

Symptom Likely cause Fix
Works locally, fails in Docker Development machine has the font; image does not Embed or copy the font and register a resolver
Regular text works, bold fails Only the regular face is mapped Map bold and return its actual bytes
Failure occurs only for missing images Renderer fallback requests Courier New or another hidden family Map the fallback family or ship its font
Resolver returns a face but rendering still fails GetFont returns null, empty bytes, or a wrong resource Throw a clear file/resource error and verify the bytes
Font name appears correct but is rejected Name differs by alias, whitespace, or case Normalize family names and support aliases intentionally
Global settings behave inconsistently Resolver is assigned after a font was already created Register once during startup before any rendering
Layout changes after adding a resolver Replacement font has different metrics Choose a metrically suitable font and review pagination

Package and version considerations

A report involving HtmlRendererCore.PdfSharpCore 1.0.1 shows the exception at PdfGenerator.GeneratePdf(HTML, PageSize.A4, 0). One posted response suggested changing page mode, but it provided no diagnosis or evidence that the workaround generalizes. Treat that as anecdotal. The source-backed remedy is to make every requested family and style resolvable for the selected PDFsharp build.

Upgrading a package can be sensible for security, compatibility, or bug fixes, but an upgrade alone does not guarantee that a missing runtime font becomes available. Check the package flavor and resolver configuration first.

Performance, reliability, and cost

  • Cache font bytes. Reading embedded resources or files for every glyph request adds avoidable work. Load each face once and reuse the byte array for the process lifetime.
  • Keep the resolver deterministic. Avoid searching arbitrary host directories. Deterministic assets make local, CI, and container output reproducible.
  • Limit supported families deliberately. A small, documented set is easier to license, deploy, and troubleshoot than an implicit dependency on system fonts.
  • Watch memory in high concurrency. Large font files retained for reuse consume process memory, so measure the selected families and deployment limits.
  • Validate output, not just exceptions. A resolver can prevent a crash while still producing incorrect glyphs, changed line breaks, or missing characters.
  • Account for licensing. The engineering cost includes obtaining fonts with redistribution rights and maintaining them when images are rebuilt.

Or skip the browser setup

If the task around your HTML is producing a web screenshot or PDF preview rather than generating a PDF inside .NET, ScreenshotNeo returns a capture from one GET request. The API can produce PNG, JPEG, WebP, or PDF and accepts options for full-page capture, CSS selectors, dark mode, device presets, custom CSS and JavaScript, waits, blocked resources, headers, cookies, authentication, timezone, geolocation, resizing, caching, signed links, asynchronous jobs, bulk capture, and PDF page settings.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo API documentation for request options and response details. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. An MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Can I fix this by installing Microsoft fonts on Linux?

Installing host packages can work in a controlled image, but it creates an environment dependency. A resolver with shipped font files is more reproducible for the PDFsharp Core build.

Should ResolveTypeface ever return null?

Return null only for families you intentionally do not support. Returning null for a family used by HTML, fallback CSS, or diagnostics causes the exception.

Do I need separate files for bold and italic?

For predictable output, map normal, bold, italic, and bold-italic separately. Do not assume synthetic styles match the metrics or glyph coverage of real faces.

Is PdfPageMode.UseOutlines a general fix?

No. A report mentions it as an anecdotal workaround. It does not replace supplying and registering fonts.

Which package should a Linux web service use?

Use the cross-platform Core package and provide the fonts through an application resolver. GDI+ and WPF are Windows-only choices.