Skip to content

About

Renders PDF pages to PNG images via PDFium

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

171 Commits

Folders and files

Repository files navigation

Morph.PDFium

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 or HTML 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.

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

Convert every page in parallel

ProcessPages loads each page, hands it to a callback and collects the results in page order, with the pages spread across cores. PDFium works on one page at a time, but the PNG encoding and whatever else the callback does for a page overlap the native work on the others, which makes converting a whole document this way 1.7–2.3× faster than a page-by-page loop. The callback runs on several threads at once and must not keep the page, which is disposed when it returns. RenderPages is built on it.

using var document = PdfiumDocument.Load("multi-page.pdf");
// Pages are spread across cores: PDFium works on one at a time, the PNG encoding overlaps
var pages = document.ProcessPages(_ => (Text: _.GetText(), Png: _.Render(dpi: 96)));

snippet source | anchor

Document metadata

using var document = PdfiumDocument.Load("multi-page.pdf");
Console.WriteLine(document.PageCount);
foreach (var size in document.GetPageSizes())
{
    Console.WriteLine($"{size.Width} x {size.Height}");
}

foreach (var (name, value) in document.GetProperties() ?? [])
{
    Console.WriteLine($"{name}: {value}");
}

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

PNG compression level

Pages are deflated at PngCompressionLevel.Optimal (zlib level 6) by default. Lower levels encode faster for a somewhat larger file: on text pages Level4 is about 20% faster for files about 3% larger. Set it per render, or globally for the renders that take no options.

using var document = PdfiumDocument.Load("sample.pdf");
// For one render; Optimal (zlib level 6) is the default
var png = document.RenderPage(
    0,
    new RenderOptions
    {
        CompressionLevel = PngCompressionLevel.Level4
    });

// For every render not given a level of its own
RenderOptions.DefaultCompressionLevel = PngCompressionLevel.Level4;

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

Render an open page

A page already loaded for its text or annotations can be rendered directly, which skips parsing it a second time.

using var document = PdfiumDocument.Load("sample.pdf");
using var page = document.LoadPage(0);
var text = page.GetText();
// Renders the page that is already loaded, rather than parsing it again
var png = page.Render(dpi: 96);

snippet source | anchor

Render to HTML

RenderHtml converts a document to a single HTML page. Each page is its rendered image with the page's text laid exactly over it, transparent, in an inline SVG, so the HTML looks like the PDF while its text can be selected, copied and found with the browser's search. Links to other pages, URLs and neighbouring files stay clickable, and the outline becomes a list of links at the top. The pages are rendered in parallel, as ProcessPages does. Each page prints on a sheet of its own size, with no margins, so a document mixing portrait, landscape and odd sizes prints one sheet per page.

using var document = PdfiumDocument.Load("multi-page.pdf");
// Each page's image with its text selectable over it; the images are embedded
var html = document.RenderHtml();

snippet source | anchor

Images are embedded as data URIs by default, which keeps the HTML self-contained. HtmlOptions.ImageUrl lets them be written out as files instead, and HtmlOptions.Render sets how they are rendered. For another image format, such as WebP or JPEG, set HtmlOptions.ImageUrlFromPixels instead: it is handed each page's RGBA pixels before any PNG is encoded, to encode with an image library and return the image's URL (a file beside the HTML, or a data URI). Images after the first page load lazily as they are scrolled near; set LazyLoadImages = false for HTML that is printed or captured without scrolling.

For a long document, pass a TextWriter (a StreamWriter over a file, say) to RenderHtml. Each page is written as soon as it and the pages before it are done, so only a few pages are in memory at once. Converting 1,000 pages at 150 DPI this way peaks at about half the memory of building the whole string.

HtmlOptions.MaxPages renders only a document's first pages, without changing the document. A bookmark to a page left out stays in the outline as plain text, links to those pages are dropped, and only the pages rendered get a printed page size.

IncludeHeadersAndFooters = false leaves out the running headers and footers that a tagged PDF marks as pagination artifacts (/Artifact marked content with /Type /Pagination, /Subtype /Header or /Footer, as Word writes them), from each page's image, text and links. Untagged content stays, whatever it looks like. PDFium cannot read the subtype when it is a name, as it usually is, so a pagination artifact counts only when it lies wholly within the top or bottom 15% of the displayed page: a watermark across the body stays, but a page or Bates number in those margins goes with the footer, as would a watermark drawn wholly within them.

using var document = PdfiumDocument.Load("multi-page.pdf");
var html = document.RenderHtml(
    new()
    {
        Render = new()
        {
            Dpi = 150
        },
        // Write each page's image beside the HTML rather than embedding it
        ImageUrl = (index, png) =>
        {
            var name = $"page{index + 1}.png";
            File.WriteAllBytes(Path.Combine(directory, name), png);
            return name;
        }
    });
File.WriteAllText(Path.Combine(directory, "document.html"), html);

snippet source | anchor

With HtmlTextMode.Visible, the HTML draws the text itself. It keeps its color, weight and style, filled or outlined as the PDF draws it, stretched to the width it has on the page. It uses the PDF font's family when the browser has that font, otherwise the nearest generic family; the PDF's embedded fonts are not carried over. The page image is then rendered without its text, so images and drawings stay while the text is sharp at any zoom. The text-free images are also much smaller: at 150 DPI they were 3% of the full render's size for text pages, about 10% for forms and tables, and around half for photo-heavy pages. The trade-off is that the text is in the browser's fonts, so it looks less like the PDF than the default overlay.

using var document = PdfiumDocument.Load("multi-page.pdf");
// Visible text over the page image rendered without its text: images and drawings
// stay, and the text is sharp at any zoom
var options = new HtmlOptions
{
    Text = HtmlTextMode.Visible
};
var html = document.RenderHtml(options);

snippet source | anchor

RenderPageHtml (or PdfPage.RenderHtml on an open page) renders one page as a fragment: a div carrying the styles it needs inline, to place in any page. For text-only HTML, turn off the image as well.

using var document = PdfiumDocument.Load("sample.pdf");
// Text-only HTML: the text in its own color and style, with no page image
var options = new HtmlOptions
{
    IncludeImage = false,
    Text = HtmlTextMode.Visible
};
var fragment = document.RenderPageHtml(0, options);

snippet source | anchor

Only http, https, mailto, ftp and tel links and relative references are kept: javascript: links, launch actions and links into embedded files are dropped. For PDFs from an untrusted source, narrow that with HtmlOptions.AllowedLinkSchemes and AllowRelativeLinks. Links to the document's own pages are kept either way.

using var document = PdfiumDocument.Load("multi-page.pdf");
// Only web and mail links, and links between the document's pages
var options = new HtmlOptions
{
    AllowedLinkSchemes = ["http", "https", "mailto"],
    AllowRelativeLinks = false
};
var html = document.RenderHtml(options);

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. PDFium itself works on one page at a time, but the PNG encoding and other managed work for a page overlap it, which is what ProcessPages, RenderPages and RenderHtml make use of.
  • Rendering is deterministic for a given Morph.PDFium version: for documents whose fonts are embedded, the same input produces the same pixels on every machine and OS, which makes the output suitable for snapshot testing. A font the document does not embed, the standard 14 (Helvetica, Times, Courier and so on) included, is drawn with a matching font from the machine where PDFium finds one, and with its own built-in substitutes otherwise, so text in it can render differently between machines: Helvetica and Times, for instance, come out differently on a Linux machine with the fonts Playwright installs for Chromium than on Windows and macOS. The same goes for the HTML's text widths and link boxes, which follow the glyphs drawn. The PNG bytes also depend on the .NET runtime's deflate, which may change between runtime versions even when the pixels do not. Verify.PDFium builds on this.

Icon

PDF designed by Meilia from The Noun Project.

About

Renders PDF pages to PNG images via PDFium

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages