RailMark

PDF review markup, in both directions.

Reads the highlights and comments in a PDF out as Markdown or JSON. Exports any PDF to layout-aware Markdown. Writes an AI reviewer's markup back in as native PDF annotations. One self-contained binary.

p. 1

Introduction

This document exists so the RailMark test suite has a PDF whose exact contents are known. The model described here is interpretable in practice, which is the point of the exercise.

AI Reviewer Key claim. Interpretable to whom? Cite the evaluation.

The reviewer called it “the best available option” at the time.


Results

No attempt was made to preserve the original ordering of the samples. Samples were processed in random order. The effect was small but consistent across every run of the experiment.

AI Reviewer Say what was done, not what was not.
AI Reviewer Give the effect size and the number of runs.
p. 2
{
  "entries": [
    { "page": 1,
      "type": "highlight",
      "quote": "interpretable in practice",
      "comment": "Key claim. Interpretable to whom?
                  Cite the evaluation." },
    { "page": 2,
      "type": "replace",
      "quote": "No attempt was made to preserve the
                original ordering of the samples.",
      "replacement": "Samples were processed in
                      random order.",
      "comment": "Say what was done, not what was not." },
    { "page": 2,
      "type": "underline",
      "quote": "small but consistent",
      "comment": "Give the effect size and the
                  number of runs." }
  ]
}

$ railmark sample.pdf --apply-markup plan.json
Applied 3/3 markup entries (3 added).

The repository's test fixture with the plan applied. The other tabs are what RailMark reads back out of the marked-up file, abridged.

Three modes, one binary

Annotation extraction is the default. The other two are a flag away.

railmark document.pdf -o notes.md

Read the markup out of a PDF

Every annotation the file carries, filed under the document's headings in reading order, with a summary table. Whoever wrote it: Acrobat, Preview, Okular, Zotero, Skim, RailReader2 or RailMark.

Highlights appear in bold inside their sentence, on the occurrence that was marked. A strikeout reads as a suggested deletion, a caret as an insertion, the pair as a replacement. Replies thread under the comment they answer. Ligatures, soft hyphens and words broken by a wrap hyphen are cleaned up.

Filter by page, colour, author or date. Add screenshots of drawings with --images, or get one JSON record per annotation with --format json.

Filters

railmark paper.pdf --pages "1,3,5-10"
railmark paper.pdf --color "ff0000,ffcc00"
railmark paper.pdf --exclude-author "AI Reviewer"
railmark paper.pdf --since 2026-09-01
railmark paper.pdf --images
railmark paper.pdf --format json -o -
railmark document.pdf --export

Export any PDF to Markdown

Layout-aware Markdown for the whole document, with headings, paragraphs and reading order recovered by the bundled PP-DocLayoutV3 model. Figures, tables and equations can be transcribed by a vision model through any OpenAI-compatible endpoint, or left as placeholders with --no-vlm.

Existing annotations are folded in. This is how an agent reads a document before reviewing it, so the quotes it sees are the quotes RailMark can later find.

Options

--no-vlm
--vlm-endpoint <url>
--vlm-model <name>
--vlm-api-key <key>
--accelerator cpu|gpu
railmark document.pdf --apply-markup plan.json

Write a reviewer's markup back in

A JSON plan lists entries: page, verbatim quote, markup type, optional comment. Each becomes a real PDF annotation: highlight, underline, strikeout, squiggly, suggested replacement, margin note, or a reply to an existing comment. Matching tolerates whitespace, ligatures, smart quotes and wrap hyphens, then resolves the quote to exact page geometry.

A repeated quote is flagged, and the entry can say which occurrence it means. A quote may run over a page break. Re-applying a plan never duplicates markup, and --replace-author clears an earlier round. Every entry reports back as JSON, so a script can retry what did not resolve.

The input PDF is copied, not modified, unless you ask for --in-place.

Report on stdout

[
  { "page": 1, "quote": "interpretable in practice",
    "type": "Highlight", "success": true,
    "matchCount": 1, "action": "added" },
  { "page": 4, "quote": "the null hypothesis",
    "type": "Underline", "success": true,
    "matchCount": 3, "action": "added",
    "warning": "Ambiguous: the quote occurs 3 times
      on page 4; marked the first. Set occurrence,
      add contextBefore/contextAfter, or lengthen
      the quote." }
]

For AI agents

RailMark exists so an agent can review a document and leave its work where a human reviewer would: in the PDF. A Claude Code skill in the repository teaches the loop. Ask Claude Code to install it; the file carries its own setup instructions for Linux and Windows.

Install this skill: https://github.com/sjvrensburg/railmark/blob/main/.claude/skills/railmark-markup-plan/SKILL.md

The review loop

  1. Export the document with --export and read it. Quotes come from this text.
  2. Draft a plan. Each entry quotes a passage verbatim, picks a markup type and says why. The skill carries conventions for which type to use.
  3. Apply it, or check it first with --dry-run, which also shows the text around each match.
  4. Retry what failed. The report names each failure; fix the quote or add context and apply again. Markup already in place is left alone.

Records come back out with --format json, shaped like plan entries and carrying an id, so a later round can reply to a human's comment, and a human can answer the agent's in any PDF reader.

Install

Linux x86-64. Downloads the AppImage to ~/.local/bin/railmark.

bash <(curl -fsSL https://raw.githubusercontent.com/sjvrensburg/railmark/main/install.sh)

Every release ships three artefacts. Each bundles the .NET runtime, the native libraries and the layout model, and can be checked against the release's SHA256SUMS.

ArtefactPlatformNotes
railmark-<version>-linux-x86_64.AppImageLinux x86-64Recommended. Needs FUSE, or run with --appimage-extract-and-run.
railmark-<version>-linux-x86_64.tar.gzLinux x86-64For containers without FUSE. Extract and run the railmark launcher at the root.
railmark-<version>-win-x64.zipWindows x64Extract and run railmark.exe.
Linux tarball install
mkdir -p ~/.local/opt ~/.local/bin
tar -xzf railmark-<version>-linux-x86_64.tar.gz -C ~/.local/opt/
ln -sf ~/.local/opt/railmark-<version>-linux-x86_64/railmark ~/.local/bin/railmark
Build from source

Requires .NET 10 or later.

git clone https://github.com/sjvrensburg/railmark
cd railmark
dotnet build
dotnet run --project RailMark/ -- document.pdf -o notes.md