KillerPDF Download
The Tech of

A plain-English look at how KillerPDF works under the hood, with the deeper detail kept for the nerds who want it.

Tech stack

KillerPDF is a native Windows app. There is no Electron, no browser engine, and no runtime to install. It relies on a small set of focused libraries, each handling 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.67, vendored and patched - save, page ops, annotation / stamp flattening
SigningPDFsharp 6.2.4 (separate namespace) - CMS / SHA-256 signatures
OCRTesseract 5.2 - native libs embedded, language packs on demand
PackagingCostura.Fody - one self-contained .exe

Install, folders & data

KillerPDF is portable first - the single exe runs from anywhere with nothing to install. Installing is optional. A per-user install needs no administrator rights, while the all-users option requests elevation and installs under Program Files.

What the installer does

A per-user install copies the running exe to %LOCALAPPDATA%\Programs\KillerPDF\. An all-users install copies it to %ProgramFiles%\KillerPDF\ after a Windows elevation prompt. Both modes add Start Menu and optional desktop shortcuts, register the PDF handler and killerpdf: protocol at the matching user or machine scope, and create an Add/Remove Programs entry. Uninstalling removes the installed files, shortcuts, and registry entries.

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.

Architecture

MainWindow is the application shell. It owns the shared toolbar, sidebar, dialogs and application-level workflows. Each document pane is a reusable PdfViewer control with its own view state, tab strip, render surfaces and document interaction code. Two viewer instances create split pane without opening a second window. Rendering, annotations, forms, links, crop, selection, text editing, tabs and zoom are divided across 16 Controls/Viewer/PdfViewer.* partials. Bridge files keep the remaining shell features connected while the refactor continues, and shared work lives under Services/ and feature controllers. In total the app is about 120 C# source files and 42,000 lines, not counting tests or the vendored PdfSharpCore fork under third_party/.

Two render pipelines

KillerPDF draws pages in two different ways depending on the view you are in. Single, Two-Page and Grid views draw into one main tile (RenderPage). Continuous scrolling uses a separate path (RenderContinuousPages) that streams page images in the background as you scroll. RenderPage is deliberately switched off during Continuous so the two systems never fight over the same tile.

Per-tab sessions & an LRU cache

Each open document is a DocumentSession that remembers its own zoom, fit and view mode, grid columns, current tool, page, and scroll position, along with its per-document data (annotations, page sizes, rotations, form values, and undo history). Switching tabs simply points the app back at that session. Each session also keeps a cache of already-drawn page images, so flipping back to a recent tab is instant and skips the renderer entirely. To keep memory in check, only the three most recently used tabs hold on to that cache.

Canvas & overlay maps

When KillerPDF displays a page, it stacks two things on top of each other: the page itself, drawn as a flat image, and a clear sheet sitting directly over it. That clear sheet is called an overlay, and it is where everything you add to a page (highlights, text boxes, drawn ink, signatures) is painted. The image underneath is never altered; your marks live on the overlay above it. A page's image-and-overlay pair is referred to as a tile.

The wrinkle is that KillerPDF has four ways of showing pages: one at a time, a continuous scroll through the whole document, a grid of pages, and two pages side by side. Each of those lays pages out differently, so each page you can see needs its own overlay in its own place. There is no single fixed sheet to draw on. Instead KillerPDF keeps a lookup table that takes a page number and hands back that page's current overlay. In the code this table is named _pages, and it is the one place the app trusts: every time it repaints a page's annotations, it asks this table which overlay to use and draws there.

A second table, _continuousCanvases, tracks the overlays for whichever multi-page layout happens to be on screen (the continuous scroll, the grid, or the two-page view). When KillerPDF builds a new overlay it registers it in both tables at once, through a helper named WirePageOverlay, so the two can never disagree about where a page lives.

One rule keeps the whole thing from breaking. The single-page style of layout is rebuilt by a routine that begins by throwing away the multi-page overlays. If that routine were allowed to run while the continuous scroll was showing, it would empty the lookup table, and every page would fall back to pointing at the hidden single-page tile. Your annotations would then be painted off-screen, out of sight, until you switched view modes. To prevent that, the routine is blocked from doing anything at all while the continuous scroll is active.

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.
The single-page repaint routine (RenderPage) begins by clearing the multi-page overlays, which empties the page-to-overlay table. It is therefore switched off while the continuous scroll is on screen. If it ran there, every overlay would point back at the hidden single-page tile and your annotations would paint off-screen until you changed view modes.

The coordinate space

KillerPDF keeps where a mark sits separate from how large it is drawn. The position is stored in a fixed internal measurement that never changes as you zoom, while the number of pixels actually drawn grows as you zoom in. Holding those two ideas apart is what lets an annotation stay locked to the same spot on the page while the page is always drawn sharply at whatever magnification you are using.

1. A fixed internal size ("render-dim")

Every page is first scaled to a standard internal size: the longer of its two sides is set to exactly 2048 units, and the shorter side is scaled to keep the page's proportions. These units are counted on their own and have nothing to do with screen pixels, so they do not change when you zoom or switch view modes. Annotation positions are recorded in this fixed measurement, which is why a mark stays in exactly the same place on the page no matter how far you zoom or which layout you view. The code calls this render-dim space, and the two resulting numbers, the page's width and height in these units, are written rdW and rdH below. One point, the unit the PDF itself uses, is 1/72 of an inch.

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

2. Drawing sharpness follows zoom, not position

The page image (the bitmap, meaning the grid of pixels the page is rendered into) is drawn at a resolution that follows two things: your display's pixel density and your current zoom. Magnify a page three times and it is redrawn from three times as many pixels rather than stretched, so text and lines stay sharp instead of turning blocky. A ceiling of 6144 pixels on the longest side limits how much memory a single page can take. This affects only sharpness; the stored positions from step 1 are untouched, which is the entire reason the two are kept apart.

scaledMax = min( 6144, int( 2048 × max(dpiScaleX, dpiScaleY) × max(1.0, zoom) ) )
// dpiScale = display pixel density (1.0 at 96 DPI, 1.5 at 150%); zoom = current magnification

3. The coordinate Y-flip

The final step is that the PDF format and the screen disagree about where a page begins. A PDF measures positions from the bottom-left corner and counts upward, while the screen, and KillerPDF's drawing surface, measures from the top-left corner and counts downward. So every mark has to be turned top-to-bottom as it is drawn, and scaled from the PDF's own points into render-dim units at the same time. In the formula below, pdfW and pdfH are the page's width and height in PDF points; renderW and renderH are the same page in render-dim units (the rdW and rdH from step 1); sx and sy are the scale factors between the two; left and top are the mark's position read from the PDF; and the result, canvasX and canvasY, is where the mark is placed on the drawing surface.

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 units (measured from the bottom-left) and flipped onto the drawing surface (measured from the top-left) every time it is painted, using canvasY = renderH - top × sy. The drawing surface is measured in device-independent pixels (DIP), a unit that stays constant no matter the screen's density. Because the same stored point feeds all four view modes, it lands on the same spot in each.

Because rdW and rdH are worked out once and then reused everywhere, the same measurements feed all four view modes, so a mark placed in one mode lands on the exact same spot 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 color
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 color 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 changes the document you are looking at. KillerPDF writes a clean copy with no annotations, then permanently bakes in stamps first, then annotations, and reopens that fresh copy. Starting from a clean base every time means it can never accidentally bake the same thing in twice.

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.

Standards conformance

A PDF editor has one obligation above all others: saving your file must not damage it. Starting with 1.6.4, that claim is tested rather than assumed. Every release is validated against veraPDF, the industry reference validator for PDF/A and PDF/UA, across a 2,907-file public conformance corpus (the veraPDF test corpus, the Isartor PDF/A-1b suite, and the TWG test files). These are deliberately hostile files, most built to violate exactly one clause of a standard, so any structural damage a save introduces shows up immediately as a newly failed rule.

How the test works

Every corpus file is validated pristine, resaved through KillerPDF's normal open/save pipeline (headlessly, via --batch-resave), and validated again. A comparison script then flags any file that fails a rule after the resave that it did not fail before. The bar for release is zero regressions. A second pass runs qpdf --check on every original/resave pair and flags any file whose structural health worsened.

The numbers (v1.7.0, veraPDF 1.30.2)

OutcomeFiles
Corpus total2,907
Resaved and revalidated2,236
Refused (encrypted or unparseable; source untouched)671
Conformance regressions0 (one documented PDF 2.0 header limitation, below)
Files that came out more conformant63
qpdf structural check worsened0 (195 improved)

Patching the write engine at the source

Getting to zero meant fixing the write engine itself. PdfSharpCore is vendored into the repo and patched rather than worked around; each patch is marked in the source and documented with the ISO clause it satisfies:

  • No Producer/Creator stamped into the Info dictionary of an imported document (PDF/A requires Info to stay equivalent to the XMP metadata).
  • No /ModDate rewritten the moment a file is opened for modification (same rule).
  • No transparency /Group injected onto every page (forbidden outright in PDF/A-1).
  • Stream /Length always matches the spec's exact byte count, empty streams included.
  • Booleans serialized as the PDF keywords true / false; the library wrote True, .NET's capitalization, which is not a valid PDF token at all.
  • The debug-only "verbose" file layout removed, so no build can pad object tokens in ways the object-syntax rules forbid.

On top of that, every save scrubs the damage older libraries left behind: dangling outline references, degenerate crop boxes, and dead digital-signature values (a signature's digest must cover the whole file, so any edit invalidates it; leaving the stale value in place fails strict validation).

The one known limitation: PDF/A-4 is built on PDF 2.0, and the write engine serializes PDF 1.7, so the single PDF/A-4 corpus file picks up the version-marker rules on resave. That is a header limitation, not structural damage, and KillerPDF does not claim PDF/A-4 output.

The full report, the comparison script, and instructions for reproducing the run ship in the repo under validation/RESULTS.md.

Localization

Every visible string lives in a per-locale ResourceDictionary under Strings/ - ten 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 / themes10 locales / 6 themes + accent variants

A full 47-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.

Glossary

Plain-language definitions of the terms and libraries used on this page, in alphabetical order.

TermWhat it means
AnnotationAnything you add on top of a PDF: a highlight, text box, drawn line, signature, or image. It sits on the overlay, separate from the original page, and only becomes part of the file when you save (see Flatten).
BitmapA grid of colored pixels. To display a page, KillerPDF draws (rasterizes) it into a bitmap at a resolution chosen from your zoom and screen density.
CMS signatureCryptographic Message Syntax, the standard format for a digital signature embedded in a PDF. KillerPDF signs using SHA-256 over the whole file.
Costura (Costura.Fody)A build-time tool that packs every library the app needs inside the single .exe, so there is nothing extra to install.
Cover / cover-pairA Cover is an opaque box that hides the original words on a page. To edit existing PDF text, KillerPDF lays a Cover over the old text and a Text box on top holding your replacement; the two are locked together as a cover-pair.
Cross-reference table (xref)The index near the end of a PDF that records where every internal object is stored. If it is damaged, many readers fail to open the file, so KillerPDF has recovery paths that rebuild it.
Device-independent pixel (DIP)The unit KillerPDF's drawing surface uses. One DIP is a fixed physical size regardless of how dense the screen is, so layouts look the same across monitors.
Docnet (Docnet.Core)The library that lets KillerPDF call PDFium from .NET to turn pages into bitmaps.
DPI (dots per inch)How many pixels a screen packs into an inch, i.e. its density. Windows reports it as a scale: 1.0 at 96 DPI (100%), 1.5 at 150%. Higher DPI means more pixels are drawn for the same physical size.
Flatten (flattening)Burning annotations permanently into the page when you save, so they become part of the PDF's own content instead of separate, editable marks.
Form fieldAn interactive field built into a PDF, such as a text box, checkbox, or dropdown, that a person fills in.
InkA freehand drawn stroke. The Line tool is stored as ink as well.
Linearized PDFA PDF arranged so a viewer can start showing the first page before the whole file has loaded (sometimes called "fast web view"). These files often keep their structure in object streams.
LRU cache (least recently used)A memory-saving rule that keeps the most recently used items and drops the oldest. KillerPDF keeps the drawn-page cache for only the three most recent tabs.
.NET Framework 4.8 (net48)The version of Microsoft's .NET platform KillerPDF is built for. It ships with every supported version of Windows, so users install no separate runtime.
Object streamA compressed container inside modern PDFs (version 1.5 and later) that bundles many of the file's internal objects together. Some libraries cannot read them, which is why KillerPDF falls back to PDFium for certain jobs.
OCR (optical character recognition)Reading the actual text out of a scanned image so it can be searched, selected, or copied. KillerPDF uses Tesseract.
OverlayThe transparent sheet drawn directly on top of a page image, where your annotations are painted. The page image underneath is never changed.
PDFiumGoogle's PDF engine (the same one inside Chrome). KillerPDF uses it to render pages to images and to read structures other libraries cannot.
PdfPigA .NET library KillerPDF uses to read text from a PDF: search, selection, and detecting each letter's font and size.
PdfSharpCore / PDFsharpThe .NET libraries KillerPDF uses to write changes back into a PDF: saving, page operations, and flattening (PdfSharpCore), and digital signatures (PDFsharp 6.2, in a separate namespace).
PointThe PDF's own unit of length. One point is 1/72 of an inch.
RasterizeTo convert a page's text and shapes into a grid of pixels (a bitmap) for display.
Render-dim spaceKillerPDF's fixed internal coordinate system for a page, sized so the longer side is 2048 units and unrelated to zoom or screen pixels. Annotation positions are stored here so they never drift as you zoom or change view mode.
Temp-reloadSaving the current state to a temporary copy and reopening it, used for operations like rotation and repair, so your original file is never edited in place.
TesseractThe open-source OCR engine embedded in KillerPDF.
TileA single page's image-and-overlay pair as it is laid out on screen.
WPF (Windows Presentation Foundation)The native Windows framework KillerPDF's interface is built with. No browser engine is involved.
Y-flipTurning a mark upside-down when drawing it, because a PDF measures upward from the bottom-left corner while the screen measures downward from the top-left.