r/csharp • u/smilaise • 2h ago
Showcase I built a PDF engine in pure C#. Version 1.9.3 reads EPUB and DjVu too.
I've been building KillerPdf.Engine, a PDF 2.0 engine written entirely in C# on .NET 10, as the core of my free PDF editor. It has no native PDF library underneath, so everything from the lexer to the rasterizer is managed code.
Creating a document looks like this:
using KillerPdf.Engine.Authoring;
byte[] pdf = new PdfDocumentBuilder()
.SetMetadata(new PdfDocumentMetadata
{
Title = "Hello from The KillerPDF.Engine",
Author = "Example application",
Language = "en-US"
})
.AddBlankPage(612, 792)
.Build();And this pulls words with their positions off a page:
using KillerPdf.Engine.Documents;
PdfDocument document = PdfDocument.Open(File.ReadAllBytes("input.pdf"), string.Empty);
var reader = new PdfPageContentReader(document);
PdfPageContent page = reader.Read(0);
foreach (PdfExtractedWord word in page.Words)
Console.WriteLine($"{word.Text}: {word.BoundingBox}");Beyond that, it renders, edits, redacts, fills and flattens forms, signs, encrypts, runs OCR, exports to Office, HTML, Markdown and JSON, and does ICC color management with printer proofing. As of 1.9.3, it also reads EPUB and DjVu natively and converts them to PDF.
A few things that surprised me building it
The spec is the easy part. Real PDFs break it constantly: cross-reference tables that point at the wrong byte, trailers with the keyword mangled, numbers like
1-.5inside content streams. Most of the engine's code is recovery, not parsing. A strict reader that rejects all of it would open far fewer of the files people actually have. The 47,024-file test corpus exists because no amount of reading the spec tells you what's actually out there.Lazy beats fast. The big performance wins came from doing less work, not from optimizing what was already there. That meant reading the file fewer times on open, building each page only when it's drawn, and rendering thumbnails only for the pages on screen. In the desktop app, a 20,000-page PDF went from about 20 seconds to its first page down to about 9, and from 1.9 GB peak memory to about 700 MB.
Writing a PDF is harder than reading one. Reading only has to survive a file once. Writing has to leave signatures, encryption, form values and everyone else's content intact, often as an incremental update appended to the original. Size matters too. Early on, every added text note embedded its own copy of the font. Sharing one subset per font across the document took a 3,000-page test from a 103 MB save in 32 seconds to an 11 MB save in about 7.5.
It's 514 files, about 167,000 lines and 13,052 tests, and it's licensed GPL-3.0.
dotnet add package KillerPdf.Enginehttps://github.com/SteveTheKiller/KillerPDF/tree/main/engine
