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.
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.
using var document = PdfiumDocument.Load("sample.pdf");
var png = document.RenderPage(0, dpi: 96);using var document = PdfiumDocument.Load("multi-page.pdf");
List<byte[]> pages = document.RenderPages();using var document = PdfiumDocument.Load("multi-page.pdf");
Console.WriteLine(document.PageCount);
Console.WriteLine(document.GetPageSizes());
Console.WriteLine(document.GetProperties());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
});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
});using var document = PdfiumDocument.Load("sample.pdf");
using var page = document.LoadPage(0);
var text = page.GetText();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);using var document = PdfiumDocument.Load("multi-page.pdf");
foreach (var bookmark in document.GetBookmarks())
{
Console.WriteLine($"{bookmark.Title} -> page {bookmark.Destination?.PageIndex}");
}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}");
}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}");
}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();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();using var document = PdfiumDocument.Load("sample.pdf");
document.AddAttachment("notes.txt", [.. "embedded data"u8]);
var withAttachment = document.Save();For the full list of native PDFium entry points that are wrapped (and those intentionally left out), see native API coverage.
- 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
pHYschunk recording the render dpi. - PDFium is not thread safe, so all native calls are serialized on a process-wide lock.
PdfiumDocumentinstances 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.
PDF designed by Meilia from The Noun Project.