Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Xeil

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

Status: v0.2

  • ✅ 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 differenceBlendMode layer, 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

Run

./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.

Install (from a release download)

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.

Architecture

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

How "image-preserving dark mode" works

XeilPDFPage subclasses PDFPage and overrides draw(with:to:). When dark mode is on, instead of letting PDFKit rasterize the page normally, we:

  1. 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).
  2. Wrap the bitmap in a CIImage and apply a CIColorMatrix inversion with α=0.86 — soft inversion (text → off-white ~218, background → dark grey ~36), avoiding the harsh pure-black/pure- white you'd get from CIColorInvert. Same constant veil arrived at empirically.
  3. Draw the inverted bitmap back into the destination CGContext at page-space coordinates.
  4. For each image rect from PDFImageRegionScanner, clip the context and drawPDFPage(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.

Highlights persistence

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.

Shortcuts

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

Vimium-style navigation

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-[

About

A fast, local-first PDF reader for macOS.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages