KillerPDF Download
The Tech of

How KillerPDF works under the hood - for the nerds.

Tech stack

KillerPDF is a native Windows app - no Electron, no browser engine, no runtime to install. It leans on a small set of focused libraries, each doing one job:

ComponentLibrary
UIWPF on .NET Framework 4.8 (net48), x64, custom window chrome
RenderingPDFium via Docnet.Core 2.6 - every page bitmap and thumbnail
TextPdfPig 0.1.14 - search, selection, font sniffing
Write enginePdfSharpCore 1.3 - save, page ops, annotation / stamp flattening
SigningPDFsharp 6.2 (separate namespace) - CMS / SHA-256 signatures
OCRTesseract 5.2 - native libs embedded, language packs on demand
PackagingCostura.Fody - one self-contained .exe

Architecture

The entire UI is one partial class MainWindow decomposed across roughly forty source files - about 27,000 lines of C# on WPF / .NET Framework 4.8. Dialogs are standalone classes; cross-cutting work lives under Services/.

Two render pipelines

Single, Two-Page and Grid render into a primary tile via RenderPage(idx); Continuous owns a separate path where RenderContinuousPages streams page bitmaps on a background thread. RenderPage is guarded to no-op in Continuous, so it can't repoint overlays at the hidden primary tile.

Per-tab sessions & an LRU cache

Each open document is a DocumentSession holding its zoom, fit and view mode, grid columns, tool, page, scroll offsets, and the per-document dictionaries (annotations, render dims, rotations, form values, undo stack). Switching tabs re-points the live fields at the session by reference. Each session also carries a frozen-bitmap RenderCache keyed by (page, size-bucket, rotation), so returning to a recent tab skips PDFium entirely; whole-tab caches evict beyond the three most recent.

Canvas & overlay maps

Annotations never paint onto a fixed surface - they paint onto whichever overlay _pages currently points at. Two maps plus one hard invariant keep that straight.

RenderAllAnnotations(page) paints onto CanvasForPage(page) [page] _pages <int, Canvas> authoritative page → overlay map (the primary tile is one of its entries) Primary XAML tile PageImage + _annotationCanvas Single · Grid · Two-Page Secondary overlays code-built per-page Canvas tiles continuous, or grid / two-page primary entry + secondaries _continuousCanvases the live multi-page tile system same overlays WirePageOverlay() registers an overlay in BOTH maps ClearSecondaryPages() clears _continuousCanvases and prunes those pages from _pages.
RenderPage calls ClearSecondaryPages() - which wipes _pages - so it is guarded to no-op in Continuous. Were it to run there, every overlay would repoint at the hidden primary tile and annotations would paint off-screen until a mode switch.

The coordinate space

Two numbers describe every page: where things are (zoom-independent) and how many pixels we draw (zoom-dependent). Keeping them separate is what makes annotations stay put while text stays crisp.

1. Render-dim space

Every page is normalised so its longest side is exactly 2048 device-independent units. Annotation coordinates are stored here, identical at every zoom and across all four pipelines.

maxDim = max(pageWidthPt, pageHeightPt)
rdW = round(2048 × pageWidthPt / maxDim)
rdH = round(2048 × pageHeightPt / maxDim) // longest side -> 2048

2. Decoupled bitmap resolution

The bitmap rasterises at a resolution that follows display DPI and zoom, so a page magnified 3× is drawn from 3× the pixels rather than upscaled. The 6144 px ceiling bounds memory.

scaledMax = min( 6144, int( 2048 × max(dpiScaleX, dpiScaleY) × max(1.0, zoom) ) )

3. The coordinate Y-flip

PDF uses a bottom-left origin in points; WPF canvases a top-left origin in DIP. Every mark crosses that boundary:

sx = renderW / pdfW sy = renderH / pdfH
canvasX = left × sx
canvasY = renderH - top × sy // flip the vertical axis
PDF point space origin bottom-left · y up y x (0,0) mark left top scale & flip Y sx, sy WPF canvas (DIP) origin top-left · y down y x (0,0) mark left·sx renderH - top·sy
A mark is stored once in render-dim points (bottom-left origin) and flipped into canvas DIP (top-left) on every paint, via canvasY = renderH - top × sy. Because the same stored point feeds all four pipelines, it lands on the same pixel in each.

Because rdW/rdH are rounded once and reused, the same integers drive the primary, continuous and grid tiles - a mark in one mode lands on the same pixel in every other.

The annotation model

Every annotation lives in _annotations, a Dictionary<int, List<PageAnnotation>> keyed by page. Six types cover everything the toolbar can draw:

TypeWhat it is
TextEditable text box with its own font, size and colour
CoverOpaque box, always paired with a replacement Text (see below)
HighlightOne annotation, three modes - Fill, Strikethrough, Underline
InkFreehand strokes; also backs the straight Line tool
SignatureDrawn or imported vector signature
ImagePasted or imported raster, freely resized

One funnel, every repaint

No commit path paints annotations directly - they all call RenderAllAnnotations(page), which clears that page's overlay, repaints every annotation in _annotations[page], re-adds the live form fields, then re-applies the search highlights last. That tail ordering is why a highlight survives every re-render, scroll and zoom instead of being painted over.

Undo

Edits push onto a single _undoStack. A linked pair (Cover + Text) goes on as one entry, so a single Ctrl+Z removes both halves together rather than leaving an orphaned cover behind.

Editing existing text

Double-click a line of real PDF text and KillerPDF reads the words underneath, then drops two linked annotations as a single undo: an opaque Cover that blocks the original, and an editable Text box on top. A shared PairId locks them together.

z-order: bottom → top Original words Page text — untouched underneath cover (opaque) Cover — blocks original, 100% opaque New words Text — editable, resizable PairId shared GUID
The cover samples the page colour so it blends in, and can never be made translucent. Right-click either half to Select Cover / Select Text, or to Unpair them into two independent annotations. On save the pair flattens to a clean filled box with the new text on top.

Zoom & interaction

Cursor-anchored zoom

Ctrl+wheel zooms around the cursor. The cursor position and scroll offsets are captured before the zoom changes, then the offsets that keep that point fixed are applied once layout settles:

ratio = newZoom / oldZoom
newHOff = (oldHOff + cursorX) × ratio - cursorX
newVOff = (oldVOff + cursorY) × ratio - cursorY // clamped at >= 0

Grid zoom by column count

Grid snaps to a whole number of columns: the count is authoritative and the zoom is derived from it, so the grid lays out exactly N pages with no leftover gap.

GridZoomForN(n) = (viewportW - 24) / ( n × (rdW + 12) )

Saving & the temp-reload dance

Saving never mutates the document on screen. KillerPDF writes a clean, annotation-free snapshot, burns stamps first, then annotations into a copy of it, then reopens that clean copy - so the next save starts from a pristine base and can never double-burn what it already flattened.

Why structural edits reload

Rotation, page operations and decryption route through SaveTempAndReload. It zeroes every page's /Rotate entry before writing, because Docnet (PDFium) sizes the page bitmap from the unrotated MediaBox - leave the rotation in and the rendered content clips. The file is written flat, reopened in Modify mode, and the rotations are re-applied in memory so the page still reads right.

Robustness

KillerPDF is built to open the PDFs other viewers choke on. It tries a normal open first, then catches each kind of failure and routes it to a specific recovery. Two rules hold throughout: heavy recovery work runs off the UI thread behind the busy overlay, and a repair never edits your file - it always produces a copy.

The open fallback ladder

The open path is a chain of typed exception handlers, each rung catching one class of failure:

FailureRecovery
Owner / permission lock, no open passwordReopen read-only so it can still be viewed and printed
Open passwordPrompt for it, then save a decrypted temp copy so PDFium can render
Malformed xrefDrop to read-only with a warning; if that also fails, offer a repair
"Unexpected EOF" on a valid fileRe-save losslessly through PDFium; opens clean, no save nag
Anything unclassifiedOffer a PDFium repair, which recovers most damaged files

Encryption is stripped at open

PdfSharpCore can read an encrypted PDF but cannot re-serialize a modified one - it would write back stale encrypted bytes and fail. So when a file is encrypted, the encryption is removed at open time (PDFium, lossless, with a PdfSharpCore Import fallback). That pass is CPU-heavy, so it runs on a background thread behind the busy overlay; every later edit and save then behaves like a normal document.

Network and partial reads

UNC shares and the WSL \\wsl$ 9P filesystem sometimes hand back partial reads, which the parser sees as a truncated file. Before opening anything on a network path, KillerPDF copies it to a local temp with a single read-to-EOF, then opens the complete copy - while keeping your original path for display and Save.

Repair works on a copy

When recovery falls through to a repair, the file is piped through PDFium, which has aggressive error recovery and rewrites a correct cross-reference table into a brand-new file. The original on disk is never touched. Repaired copies can lose bookmarks, forms and other interactive features, and the dialog says so before proceeding.

Install, folders & data

KillerPDF is portable first - the single exe runs from anywhere with nothing to install. Installing is opt-in, per-user, and needs no administrator rights: it never writes to Program Files or HKLM.

What the installer does

It copies the running exe to %LOCALAPPDATA%\Programs\KillerPDF\, drops a Start Menu shortcut (and an optional desktop one), registers itself as a per-user PDF handler, and writes an Add/Remove Programs entry so Windows can uninstall it cleanly. Uninstalling (KillerPDF.exe /uninstall) removes the folder, shortcuts, and registry keys.

Where things live

LocationHolds
%LOCALAPPDATA%\Programs\KillerPDF\the installed exe and its PDF-file icon
%LOCALAPPDATA%\KillerPDF\Temp\session working files - decrypted copies, repaired files, rotation snapshots
%LOCALAPPDATA%\KillerPDF\tessdata\OCR language data (see below)
%LOCALAPPDATA%\KillerPDF\ocr\<version>\x64\OCR native engine libraries
%LOCALAPPDATA%\KillerPDF\Logs\crash logs
HKCU\Software\KillerPDF\Settingsyour settings - in the registry, not a file

Everything sits under %LOCALAPPDATA% on purpose: it is per-user (no admin), user-private, and not indexed by Windows Search - so temporary copies of your documents never surface in search or another account.

Where OCR files go

OCR is bundled in the exe but unpacks on first use. The Tesseract native libraries extract to a per-version cache at %LOCALAPPDATA%\KillerPDF\ocr\<version>\x64\ (version-stamped, so an update gets fresh binaries). The language data lives in %LOCALAPPDATA%\KillerPDF\tessdata\: English ships inside the exe and is written there the first time you OCR, and any extra languages you pick are downloaded into that same folder. It is version-independent, so downloaded languages survive app updates. Both are read from these locations on every OCR run.

Temp files

Working files are written as killerpdf_<tag>_<guid>.pdf and tracked for the session. They are deleted when you close the app; anything a crash leaves behind is swept on the next launch (both the current Temp folder and the legacy %TEMP% location). Files still open in another instance are skipped and cleared later.

Clear all data

The Clear all Data link in the About window wipes everything KillerPDF has stored: the registry settings, the downloaded OCR languages and native cache, and the temp folder. It is best-effort - anything locked by the running session (a loaded native DLL, say) is skipped and clears on the next restart. Your actual PDFs are never touched.

Localization

Every visible string lives in a per-locale ResourceDictionary under Strings/ - eight locales, one XAML file each. Nothing hard-codes text; code and XAML resolve a key through Loc("Str_...") or a DynamicResource, so switching language reflows the whole UI live, no restart.

Captions and tooltips are separate strings

A toolbar button carries two independent strings: the hover tooltip (Str_TT_*) and the text caption under or beside the icon (Str_Lbl_*, mapped per glyph). Because they are localized separately, the toolbar can shed captions to save width while every tooltip stays intact.

Constants & limits

SpecificationValue
Render-dim longest side2048 DIP (zoom-stable)
Bitmap resolution cap6144 px
Print / OCR render300 DPI / 2600 px (~300 DPI on Letter)
Zoom range / step5% to 500%, 15% steps
Render-cache tabs (LRU)3 most-recently-used
Folder / zip import cap50 files
Signature reservation16,384 bytes, SHA-256, whole chain
Languages / themes8 locales / 6 themes + accent variants

A full 41-page technical brochure (the same facts, with the interactive forms and theme gallery) ships in the repo as KillerPDF.pdf - open it in KillerPDF itself.