Morph.PDFium

July 17, 2026 ยท View on GitHub

Build status NuGet Status

A .NET wrapper over PDFium, using the prebuilt native binaries from pdfium-binaries (Windows, Linux, and macOS). No image library dependency: PNG encoding is built in.

It renders pages to PNG and also covers text extraction and search, navigation (bookmarks, destinations, links), annotations, AcroForm fields, page manipulation (import/merge, rotate, insert, delete, flatten), content editing, save, attachments and signatures. See native API coverage for the full wrapped surface.

See Milestones for release notes.

Open Source Maintenance Fee

This project participates in the Open Source Maintenance Fee. The source code is freely available under the terms of the license. To support sustainable maintenance, use of the project's official binary releases in revenue-generating activities and all government agencies requires adherence to the Open Source Maintenance Fee EULA. The fee is paid by sponsoring Papyrine.

This project uses SponsorCheck to surface a build-time reminder in consuming projects that are not yet sponsoring.

NuGet package

Morph.PDFium

Native binaries

Morph.PDFium references all three pdfium-binaries packages (bblanchon.PDFium.Win32, bblanchon.PDFium.Linux and bblanchon.PDFium.macOS), so it runs on Windows, Linux and macOS with no extra setup. The trade-off is that a restore pulls all three (~50 MB), and a runtime-agnostic build copies every runtime into bin (~110 MB across 12 RIDs).

None of that reaches a published app. NuGet copies only the binary matching the target runtime, so dotnet publish -r win-x64 emits a single pdfium.dll.

To trim the build output as well, set UseCurrentRuntimeIdentifier in an app or test project:

<PropertyGroup>
  <UseCurrentRuntimeIdentifier>true</UseCurrentRuntimeIdentifier>
  <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>

UseCurrentRuntimeIdentifier resolves to the runtime of the machine doing the build (win-x64 locally, linux-x64 on CI, and so on), so only the matching pdfium.dll is copied. AppendRuntimeIdentifierToOutputPath keeps output at bin/<configuration>/<tfm>/, which the runtime identifier would otherwise push down to bin/<configuration>/<tfm>/<rid>/.

Both affect build output only: a packed library is keyed by target framework rather than runtime, so setting them in one changes nothing about the resulting package.

Usage

Render a page

using var document = PdfiumDocument.Load("sample.pdf");
var png = document.RenderPage(0, dpi: 96);

snippet source | anchor

Render all pages

using var document = PdfiumDocument.Load("multi-page.pdf");
List<byte[]> pages = document.RenderPages();

snippet source | anchor

Document metadata

using var document = PdfiumDocument.Load("multi-page.pdf");
Console.WriteLine(document.PageCount);
Console.WriteLine(document.GetPageSizes());
Console.WriteLine(document.GetProperties());

snippet source | anchor

Grayscale and other render options

RenderOptions controls dpi, grayscale, anti-aliasing, print optimization and background.

using var document = PdfiumDocument.Load("sample.pdf");
var png = document.RenderPage(0, new RenderOptions
{
    Grayscale = true,
    Dpi = 150
});

snippet source | anchor

Render a region (clip / tile)

Render a single rectangle of a page (in page points, origin bottom-left), scaled to the dpi.

using var document = PdfiumDocument.Load("sample.pdf");
// A clip rectangle in page points (origin bottom-left): the top-left quadrant.
var clip = new PdfRectangle(0, 396, 306, 792);
var png = document.RenderRegion(
    0,
    clip,
    new()
    {
        Dpi = 96
    });

snippet source | anchor

Extract text

using var document = PdfiumDocument.Load("sample.pdf");
using var page = document.LoadPage(0);
var text = page.GetText();

snippet source | anchor

Search within a page

using var document = PdfiumDocument.Load("sample.pdf");
using var page = document.LoadPage(0);
var matches = page.Search("paragraph");
// Map a match back to rectangles on the page (e.g. for highlighting).
var first = matches[0];
var rectangles = page.GetTextRectangles(first.CharIndex, first.CharCount);

snippet source | anchor

Read bookmarks (outline)

using var document = PdfiumDocument.Load("multi-page.pdf");
foreach (var bookmark in document.GetBookmarks())
{
    Console.WriteLine($"{bookmark.Title} -> page {bookmark.Destination?.PageIndex}");
}

snippet source | anchor

Read annotations

using var document = PdfiumDocument.Load("sample.pdf");
using var page = document.LoadPage(0);
foreach (var annotation in page.GetAnnotations())
{
    Console.WriteLine($"{annotation.Type}: {annotation.Contents}");
}

snippet source | anchor

Read AcroForm fields

using var document = PdfiumDocument.Load("sample.pdf");
using var form = document.LoadForm();
using var page = document.LoadPage(0);
foreach (var field in form.GetFields(page))
{
    Console.WriteLine($"{field.Name} ({field.Type}) = {field.Value}");
}

snippet source | anchor

Merge / import pages

using var merged = PdfiumDocument.CreateNew();
using (var first = PdfiumDocument.Load("sample.pdf"))
using (var second = PdfiumDocument.Load("multi-page.pdf"))
{
    merged.ImportPages(first);
    merged.ImportPages(second, "1-2");
}

var bytes = merged.Save();

snippet source | anchor

Edit and save

Add content (text, rectangles), then serialize with Save.

using var document = PdfiumDocument.Load("sample.pdf");
using (var page = document.LoadPage(0))
{
    page.AddRectangle(new(40, 700, 240, 760), fill: new(220, 230, 250, 255));
    page.AddText("Stamped", 50, 720, fontSize: 24);
}

var stamped = document.Save();

snippet source | anchor

Embedded file attachments

using var document = PdfiumDocument.Load("sample.pdf");
document.AddAttachment("notes.txt", [.. "embedded data"u8]);
var withAttachment = document.Save();

snippet source | anchor

For the full list of native PDFium entry points that are wrapped (and those intentionally left out), see native API coverage.

Behavior notes

  • Pages render with a white background and annotations included.
  • Output dimensions are pageSizeInPoints / 72 * dpi, rounded to the nearest pixel. The default 96 dpi renders an A4 page at 794 x 1123 and a Letter page at 816 x 1056.
  • The PNG includes a pHYs chunk recording the render dpi.
  • PDFium is not thread safe, so all native calls are serialized on a process-wide lock. PdfiumDocument instances can safely be used from multiple threads, but rendering does not parallelize.
  • Rendering is deterministic for a given Morph.PDFium version: the same input produces byte-identical PNGs on every machine and OS, which makes the output suitable for snapshot testing. Verify.PDFium builds on this.

Icon

PDF designed by Meilia from The Noun Project.