Native macOS PDF reader. Skim alternative built on PDFKit with smart dark mode (text inverted, images preserved). Pure native, no embedded browser, no PDF.js — uses Apple's PDFKit so zoom/scroll/selection/search are GPU-composited and butter-smooth.
Website · Latest release · Source
- ✅ PDFKit-based viewer: native zoom, continuous scroll, link history, text selection, find-in-document (⌘F), bookmarks/outline (via PDFKit)
- ✅ Smart dark mode — inverts each visible page in place via a
GPU
differenceBlendModelayer, with image regions cut out by an even-odd mask path so figures keep their original colors (⌘⇧D to toggle) - ✅ Image regions extracted directly from each PDF's content stream
(
CGPDFScanner), cached per-page on a background queue - ✅ Skim-style nav history: ⌘[ Back / ⌘] Forward (uses PDFView's own destination stack)
- ✅ Native ⌘O open; double-click in Finder works (
CFBundleDocumentTypes) - ✅ Highlight annotations (⌘H) via real
PDFAnnotationHighlight; persisted to~/.xeil/annotations/<sha256>.json(keyed by PDF content hash, not path — survives renames/moves) and re-applied on next open - ✅ Export annotated PDF (⌘E) — Preview/Skim/Acrobat compatible
- ⏳ Outline + thumbnail sidebar
- ⏳ App icon, code signing, notarization
./build.sh
open -a build/Xeil.app some.pdf
Requires macOS 13+ and the Swift CLI toolchain (xcode-select --install
if swift --version fails). No Xcode needed.
The default build contains only the core PDF reader. AI Guided Reading
depends on the copilot CLI and is opt-in at compile time:
XEIL_ENABLE_AI=1 ./build.sh
The default build performs no telemetry or document uploads. In an AI-enabled build, Xeil asks for per-document confirmation before sending extracted PDF text through the external Copilot CLI.
Xeil releases are currently ad-hoc signed and not notarized. Verify the
published SHA-256 checksum, move Xeil.app into Applications, then
right-click it and choose Open. On macOS 15+, approval may appear under
System Settings → Privacy & Security → Open Anyway after the first
launch attempt.
If you build from source with ./build.sh instead, neither step is
needed — the script ad-hoc signs the bundle and the local copy in
/Applications never gets a quarantine xattr.
xeil/
├── XeilApp/
│ ├── Package.swift
│ ├── Sources/XeilApp/
│ │ ├── main.swift # NSApplication boot
│ │ ├── AppDelegate.swift # menu wiring, file open, doc types
│ │ ├── PDFViewController.swift # PDFView host + dark toggle wiring
│ │ ├── XeilPDFPage.swift # custom draw(): invert + image preserve
│ │ ├── XeilPDFDocumentDelegate.swift # tells PDFKit to use XeilPDFPage
│ │ ├── PDFImageRegionScanner.swift # content-stream → image rects
│ │ └── AnnotationStore.swift # ~/.xeil/annotations/<sha256>.json store
├── build.sh # → build/Xeil.app
└── README.md
XeilPDFPage subclasses PDFPage and overrides draw(with:to:). When
dark mode is on, instead of letting PDFKit rasterize the page normally,
we:
- Render the page to an offscreen 8-bit sRGB CGContext sized to the device-pixel resolution PDFView is asking for (gathered from the destination CTM scale).
- Wrap the bitmap in a
CIImageand apply aCIColorMatrixinversion with α=0.86 — soft inversion (text → off-white ~218, background → dark grey ~36), avoiding the harsh pure-black/pure- white you'd get fromCIColorInvert. Same constant veil arrived at empirically. - Draw the inverted bitmap back into the destination CGContext at page-space coordinates.
- For each image rect from
PDFImageRegionScanner, clip the context anddrawPDFPage(cgPage)again — the original image pixels paint over the inverted version, untouched.
PDFKit caches per-page rasterizations internally so step 1-3 only run
when a page is first scrolled into view or the user zooms. The doc
delegate (XeilPDFDocumentDelegate.classForPage) tells PDFKit to use
our subclass for every page.
AnnotationStore snapshots all PDFAnnotationHighlight from the live
document into ~/.xeil/annotations/<sha256-of-pdf>.json (page, x/y/w/h in PDF points, text,
color). Reload re-creates the annotations on the document so the user
sees them and can edit them with PDFKit's native handlers.
| Action | Shortcut |
|---|---|
| Open… | ⌘O |
| Export Annotated PDF… | ⌘E |
| Save Annotations | ⌘S |
| Find | ⌘F |
| Highlight Selection | ⌘H |
| Remove Highlights On Page | ⌘⇧H |
| Toggle Dark Mode | ⌘⇧D |
| Back | ⌘[ |
| Forward | ⌘] |
| Zoom In | ⌘+ |
| Zoom Out | ⌘- |
| Actual Size | ⌘0 |
Bare keys work whenever the PDF has focus. Press ? in Xeil for the
in-app reference. Counts work with movement commands (5j, 3]]).
| Action | Key |
|---|---|
| Scroll left / down / up / right | h / j / k / l |
| Scroll half-page down / up | d / u |
| Scroll full-page down / up | Space / Shift-Space |
| Top / bottom of document | gg / G |
| Far left / far right | zH / zL |
| Previous / next PDF page | [[ / ]] |
| Hint and open a visible PDF link | f |
| Find; next / previous match | /; n / N |
| Back / forward in link history | H / L |
| Previous / next document window | J / K |
| Refresh rendering / open a PDF | r / o |
| Copy current file URL | yy |
| Cancel hints, queued keys, or find | Esc or Ctrl-[ |