Recursive AI

Migrating from PdfPig to OnePDF

Move C# code from PdfPig 0.1.16 to OnePDF: what changes, samples run on both libraries, and the difference for text order, images, merging and editing.

Most PdfPig code moves to OnePDF with renames: PdfDocument.Open keeps its name, pages are numbered from 0 instead of 1, page.ExtractText() gives the words and lines, and Bounds replaces BoundingBox. License isn’t the usual reason to switch, since PdfPig is Apache-2.0 and OnePDF is MIT OR Apache-2.0. People switch when they need more than reading: text in reading order without assembling a layout pipeline, images from files that use JPEG 2000, and files you can change, merge, fill, sign and save. This guide runs three jobs on both libraries side by side, quotes the published extraction benchmarks, and links to the full API map in the docs. Every sample was compiled and run with PdfPig 0.1.16 (the current release on NuGet, 22 August 2026) and OnePDF 1.11.0 on .NET 10.

Check these first

  • .NET version. OnePDF targets .NET 10 only. PdfPig 0.1.16 also targets .NET Framework 4.6.2 and 4.7.1, .NET Standard 2.0 and 2.1, and .NET 6, 8 and 9 (NuGet). Code that must stay on an older runtime can’t move.
  • Page numbers start at 0. document.GetPage(1) becomes doc.Pages[0], and page.Number becomes page.Index. This is the most common bug in a port.
  • Text comes in reading order by default. PdfPig’s page.Text is the content-stream order, and its README recommends ContentOrderTextExtractor or its layout analysis tools instead. OnePDF returns reading order (the structure tree of a tagged file, geometry otherwise). For content order, ask for PdfTextMode.ContentOrder.
  • Rendering to images. OnePDF doesn’t rasterize pages. For page images in .NET, see PDF to image in C# on oneimage.dev.
TerminalNuGet
dotnet remove package PdfPig
dotnet add package RecursiveAI.OnePdf --version 1.11.0

Words with their positions

PdfPig gives a page’s words with GetWords():

Program.csPdfPig 0.1.16
using UglyToad.PdfPig;
using UglyToad.PdfPig.Content;
 
using PdfDocument document = PdfDocument.Open("report.pdf");
Page page = document.GetPage(1);                          // the first page is 1
foreach (Word word in page.GetWords().Take(4))
{
    var box = word.BoundingBox;
    Console.WriteLine($"{word.Text,-8} left {box.Left:0.0} bottom {box.Bottom:0.0} right {box.Right:0.0} top {box.Top:0.0}");
}
OutputConsole
Harbor   left 54.0 bottom 729.5 right 81.5 top 736.0
trial    left 84.0 bottom 729.5 right 98.5 top 736.0
report   left 101.0 bottom 729.5 right 124.5 top 735.3
Week     left 54.0 bottom 708.3 right 105.9 top 722.8

In OnePDF, ExtractText() returns the page’s text with its words, lines and blocks:

Program.csOnePDF 1.11.0
using OnePdf;
using OnePdf.Text;
 
using PdfDocument doc = PdfDocument.Open("report.pdf");
PdfPageText text = doc.Pages[0].ExtractText();             // the first page is 0
foreach (PdfTextWord word in text.Words.Take(4))
{
    PdfRectangle box = word.Bounds;
    Console.WriteLine($"{word.Text,-8} left {box.Left:0.0} bottom {box.Bottom:0.0} right {box.Right:0.0} top {box.Top:0.0}");
}
OutputConsole
Harbor   left 54.0 bottom 727.7 right 81.5 top 737.6
trial    left 84.0 bottom 727.7 right 98.5 top 737.6
report   left 101.0 bottom 727.7 right 124.5 top 737.6
Week     left 54.0 bottom 704.3 right 105.9 top 726.3

Both use PDF points with the origin at the bottom left, and the left and right edges agree. The heights differ by design. PdfPig’s box follows the visible glyphs, so “report”, which has no ascender as tall as the H of “Harbor”, has a lower top. OnePDF’s box runs from a fixed ascent to a fixed descent of the font size, so words of one size on a line share a top and a bottom, which makes grouping words into lines and table rows simpler. Each word also has IsVisible, so text drawn invisibly, such as the OCR layer of a scan, can be told apart.

Extraction quality and speed, as published

The OnePDF repository publishes a comparison with PdfPig 0.1.16. On its generated set of 62 files with exact reference text, the character error rate is 0.3% for OnePDF, against 21.9% for PdfPig’s default Page.Text and 5.6% for its documented layout pipeline (Docstrum blocks plus unsupervised reading order). On a real-world set of 92 files in 11 categories, it is 5.6% against 13.5%, the better of PdfPig’s two methods for each category. Reading-order error on the real-world set is 15.9% against 36.5%.

Some speed rows from the same benchmark, single-threaded unless noted:

TaskOnePDF 1.11.0PdfPig 0.1.16
Full text, born-digital, 300 pages211 ms238 ms
Full text, 300 pages, 4 workers68.9 ms123 ms
Full text, CJK, 120 pages57.5 ms229 ms
Words with positions, one dense page4.64 ms13.8 ms
Open and page count, broken file that needs repair4.41 ms17.5 ms

These are BenchmarkDotNet means on an AMD Ryzen 9 7950X3D under Linux with .NET 10.0.13. PdfPig has no parallel mode, so in the four-worker row each worker opens its own copy of the file. Results are for those files on that machine. The README has every row, the memory figures and the method. Over three public corpora (GovDocs1, a SafeDocs sample and the veraPDF corpus), PdfPig opened no file that OnePDF failed to open, and OnePDF opened 20 files that PdfPig didn’t.

Extract images

PdfPig’s wiki describes page.GetImages(), TryGetPng for common image types, and RawBytes as a valid JPEG file for a JPEG image. This sample follows it, run on USGS Fact Sheet 2016–3020:

Program.csPdfPig 0.1.16
using UglyToad.PdfPig;
using UglyToad.PdfPig.Content;
 
using PdfDocument document = PdfDocument.Open("fs20163020.pdf");
Directory.CreateDirectory("images");
int png = 0, jpeg = 0, notSaved = 0;
foreach (Page page in document.GetPages())                 // pages are numbered from 1
{
    int i = 0;
    foreach (IPdfImage image in page.GetImages())
    {
        string path = Path.Combine("images", $"page{page.Number}-{++i}");
        if (image.TryGetPng(out byte[]? bytes))
        {
            File.WriteAllBytes(path + ".png", bytes);
            png++;
        }
        else if (image.RawBytes is [0xFF, 0xD8, ..])      // the wiki: for a JPEG, RawBytes is a valid JPEG file
        {
            File.WriteAllBytes(path + ".jpg", image.RawBytes.ToArray());
            jpeg++;
        }
        else notSaved++;
    }
}
Console.WriteLine($"{png} saved as .png, {jpeg} as .jpg, {notSaved} not saved");
OutputConsole
91 saved as .png, 0 as .jpg, 12 not saved

The 91 PNGs are 10 distinct indexed-color icons, repeated, all RGB without their soft masks. The 12 entries not saved are the JPEG 2000 photos and maps. The wiki says JPEG 2000, JBIG2 and JPEG decoding need separate filter packages, and we didn’t test those. None of the file’s JPEG, grayscale or spot-color images were among the images GetImages() returned. The OnePDF program from extract images from a PDF in C#, on the same file:

OutputConsole
fs20163020.pdf: 5 saved as .jpg (original bytes), 38 as .png, 0 skipped

That is all 43 distinct images that poppler’s pdfimages lists, each saved once, with transparency as an alpha channel and CMYK converted to sRGB.

Merge files

PdfPig merges with PdfMerger, as its README shows. Here a two-page chapter and the IRS Form W-9 (Rev. March 2024), which has 23 form fields:

Program.csPdfPig 0.1.16
using UglyToad.PdfPig.Writer;
 
byte[] merged = PdfMerger.Merge("chapter-1.pdf", "fw9.pdf");
File.WriteAllBytes("merged-pdfpig.pdf", merged);
Console.WriteLine($"{merged.Length:N0} bytes");
OutputConsole
95,490 bytes
Program.csOnePDF 1.11.0
using OnePdf;
 
using PdfDocument chapter = PdfDocument.Open("chapter-1.pdf");
using PdfDocument w9 = PdfDocument.Open("fw9.pdf");
using PdfDocument merged = PdfDocument.Merge([chapter, w9]);
merged.Save("merged-onepdf.pdf");
Console.WriteLine($"{merged.PageCount} pages, {merged.Form?.TerminalFields.Count() ?? 0} form fields, {merged.Outline.Count} top-level bookmarks");
OutputConsole
8 pages, 23 form fields, 2 top-level bookmarks

Checked with qpdf and pdfinfo, both files have the 8 pages. PdfPig’s file has no form fields, no bookmarks and no tags. OnePDF’s keeps the 23 fields, the chapter’s 2 bookmarks and the structure tree of both tagged inputs. This matches what PdfPig’s README says of its document creation: it doesn’t support “copying or changing annotations, metadata or document structure data”. More on merging, including byte arrays and clashing field names, is in merge PDF files in C#.

Change existing files

PdfPig’s README says its forms are read-only, annotations can’t be edited and hyperlinks can’t be added. OnePDF opens a file for editing with new PdfOpenOptions { AllowEditing = true } and saves it whole or as an incremental update. What that covers, with a guide for each:

Creating documents

PdfPig’s PdfDocumentBuilder places text, lines and rectangles at coordinates. OnePDF has the same low level, PdfDocument.CreateEmpty() and page.BeginContent(), with embedded fonts, images, paths, colors and transparency. For documents that flow across pages, with tables, headers, footers and shaped Arabic or Indic text, use RecursiveAI.OnePdf.Layout: see create a PDF in C#.

What OnePDF doesn’t do

  • OCR. Scanned pages without a text layer give no text.
  • Table extraction. You get words and lines with positions to build tables from, not a table API.
  • Older runtimes. .NET 10 only, as above.

Where next