Repository navigation
Releases: sjvrensburg/railmark
Release list
v0.16.0
Replies are real threads now
Built on RailReader.Core 0.71.0, which can write /IRT. Replies added with --apply-markup carry a genuine /IRT link to their parent, so they nest in Acrobat and other readers that support threads, not only in RailReader2 and RailMark. The /NM-based link is kept as a fallback for files Core cannot link (signed or read-only files, RC4/AES-128 encryption) and for PDFs written by earlier RailMark versions.
Running --apply-markup on a PDF with older RailMark replies now writes the missing /IRT for them as well.
Removing a comment removes its replies
--remove-ids, --replace-author and --replace-unauthored now take a removed annotation's whole reply thread with it: replies to replies, replies on other pages, and the page-break tail of a reply. Previously the replies were left behind as orphan notes. The removed count includes them.
Behaviour change: the cascade is by thread, not by author, so --replace-author "AI Reviewer" also removes another author's reply to one of the AI's annotations.
Other changes
- Markdown output takes its reply threading from Core's
AnnotationThreads. A malformed loop of replies is now cut at its earliest member (shown as a plain note, with the rest nested beneath it) instead of every member being shown as a plain note. - RailReader.Export, RailReader.Renderer.Skia and RailReader.Core.Analysis updated from 0.67.0 to 0.71.0.
v0.15.0
New
- Near-duplicate warning. When an entry is
addedand a markup of the same type already on the page covers much the same text (any author, however the lines are split), the entry now carries aNear-duplicate of <id>warning. It suggests anupdateentry; a short markup nested inside a longer one is not flagged. It is a warning only, so exit codes are unchanged. This catches duplicates from re-applying--format jsonrecords, which the exact-geometry dedupe missed.
Notes
- Only the first part of a page-break markup is checked for near-duplicates.
- The build is now free of compiler warnings.
v0.14.0
0.14.0
Behaviour change: highlights are now written at 35% opacity, matching RailReader2's own highlight tool, so saturated colours no longer hide the text under them. Highlights already in a PDF are not touched. To get the old look, pass "opacity": 1 on an entry or --opacity 1 for a whole run.
New
- Update an annotation in place:
{"page": 17, "update": "<id>", "comment": …, "color": …, "author": …, "opacity": …}changes an existing annotation of any type, notes and ink included, keeping its id, creation date and position (#35). - Opacity: a per-entry
opacity(0–1) and a--opacityflag.--format jsonnow reportsopacity(#36). - Remove annotations without an author:
--replace-unauthored(or--replace-author "") (#34). - Remove annotations by id:
--remove-ids a,b,c(#34).
Notes
typeis now optional in a plan entry, but only for anupdateentry.- Changing an annotation's author with
updatechanges how re-applying a plan finds it. Removing a parent annotation leaves its replies as top-level notes. - Requested in #33.
v0.13.0
What's new
This release is about where sticky notes land. Everything below is covered by the integration smoke against real pdfium geometry, which now reads the written /Rect values back from the PDF.
Sticky-note placement (--apply-markup)
- Notes sit beside their text. A
noteicon used to be pinned a fixed 24pt from the page's right edge, so in a wide-margin or two-column document it sat far from the passage it was about. It now goes just outside the text column its quoted line belongs to, found from the page's character boxes, on whichever margin has room nearer: the right margin on a single-column page, the gutter or left margin for the left column of a two-column page. It is kept clear of any glyphs it would cover and inside the page. - Notes on one line stack instead of piling up. A note that would overlap an icon already there moves down (then up, at the foot of a page) to the nearest free spot, so several notes on one passage are all visible and read top to bottom.
- Reply threads stay together. A reply takes the spot directly beneath its parent note and the replies already there, pushing any other note in the way down the column, so a thread reads parent, reply, reply without unrelated notes in between.
- Re-applying a plan still finds its notes. A note is identified by its text and where it would ideally sit, with an allowance for how far stacking or a thread may have moved it, so re-running a plan reports
unchangedrather than adding duplicates. Notes written by earlier releases at the page edge are recognised too. The allowance is six icons: a note with more than six replies inserted above it on the same line is re-added rather than matched.
Skill
The railmark-markup-plan skill now picks the annotation by the scope of the feedback. A remark about specific words or a sentence goes on the text as a highlight, underline, squiggly, strikeout or replace with the remark in its comment; a note is for a paragraph or section as a whole, placed beside the passage rather than on it, quoting the paragraph's opening words so the icon lands beside its first line. The schema reference and example plan are updated to match.
Upgrading
railmark --version # should report 0.13.0
See install.sh in this repo, or download the platform artifact below directly.
Full Changelog: v0.12.0...v0.13.0
v0.12.0
What's new
This release closes the loop between an AI agent's markup plan and the PDF: a plan can be drafted, applied, re-applied and read back as a plan again. Everything below is covered by the integration smoke against real pdfium geometry.
Applying markup (--apply-markup)
- Pick which occurrence a quote marks. New
occurrence(1-based),contextBeforeandcontextAfterfields on a plan entry narrow a quote that repeats on its page. An unqualified quote that matches more than once still resolves to the first match, but the result now carriesmatchCountand awarningso the agent can retry with context.--dry-runadds acontextfield per entry showing what surrounds each match. replacemarkup type. A suggested replacement is one strikeout carryingReplace with: <text>(plus the comment), since PDFium cannot write Acrobat's caret + strikeout pair. Annotation mode reads it back as a single suggested replacement entry.- Re-applying a plan is safe. Resolved markups are merged into what the page already holds: the same markup (type, author, geometry) is updated in place when its comment or colour changed, and left alone otherwise. Two entries of one run that would write different things to the same spot produce a conflict on the later entry rather than a silent overwrite.
--replace-author <name>removes that author's earlier annotations first, for a fresh pass. - Quotes across a page break. A quote that starts at the bottom of one page and ends at the top of the next resolves as a head annotation plus a continuation on the following page, reported as one markup with
endPage. The split must fall at a real line break, so a quote on the wrong page fails naming the page it was found on instead of half-matching. - Replies. A
notewithinReplyToset to an existing annotation's id threads beneath it. Successive replies to the same parent stack below it. - Plans from stdin.
--apply-markup -reads the plan from standard input.
Extracting annotations
--format json. One record per annotation in reading order, filed under the same headings as the Markdown. Field names match the plan schema, so extracted records can be fed straight back in as a plan;occurrenceis filled only where the quote repeats on its page, and continuations fold into their head withendPage.- Author and date filters.
--authorand--exclude-author(case-insensitive, repeatable) and--since <date>filter both output formats.--sinceuses the modified date, falling back to created, and reports how many undated annotations it dropped. - Reply notes are threaded under their parent as nested blockquotes in Markdown. Heading assignment now uses each heading's position on the page, so two headings on one page split their annotations correctly.
Dependencies
- RailReader.Core 0.62.2 → 0.67.0. All upstream changes in that range are additive for RailMark. The useful one is 0.64.2: the signed-PDF probe behind
Savereads a few KB instead of the whole file, so applying markup to a large scanned PDF no longer pays a full read up front.RailReader.Core.Analysisnow pulls in NetTopologySuite (pure managed); it is bundled in all three artifacts.
The railmark-markup-plan skill's schema reference and example plan are updated for the new fields.
Upgrading
railmark --version # should report 0.12.0
See install.sh in this repo, or download the platform artifact below directly.
Full Changelog: v0.11.0...v0.12.0
v0.11.0
What's new
-
--apply-markupdefault colours now match RailReader2's annotation palette. The old defaults were chosen before RailReader2 had a fixed palette, and three of them were colours it can't reproduce. Now, when a plan entry has nocolor, the entry gets RailReader2's default colour for that tool:Type 0.10.x 0.11.0 highlight #FFFF00#FFFF00(unchanged)underline #00AAFF#00A000greenstrikeout #FF0000#FF0000(unchanged)squiggly #FF8800#00A000greennote #FFCC00#FFFF00yellowAI-authored markup now looks the same as markup a reader adds in RailReader2, and one
--colorfilter selects both. Explicitcolorvalues in a plan are unaffected. Therailmark-markup-planskill's schema reference lists the new defaults and the full five-colour palette (yellow, green, red, blue#0066FF, black#000000).Watch out: if you filter annotation-mode output with
--color 00AAFF,FF8800orFFCC00to find RailMark-written markup, PDFs marked up by 0.11.0 need00A000orFFFF00instead. -
RailReader.Core bumped 0.62.1 → 0.62.2. Upstream this release only fixes auto-scroll in the GUI, so nothing changes in RailMark.
Upgrading
railmark --version # should report 0.11.0
See install.sh in this repo, or download the platform artifact below directly.
Full Changelog: v0.10.1...v0.11.0
v0.10.1
Dependency bump
Bumps the RailReader.Core packages (RailReader.Export, RailReader.Renderer.Skia,
RailReader.Core.Analysis) from 0.60.2 to 0.62.1.
No annotation, export, or markup-plan behavior changed upstream in that range — this
release exists to pick up two transparent perf wins with no code changes required:
SkiaPdfServicenow caches page sizes instead of re-opening the document per lookup- Rendered-page bitmaps are marked immutable, avoiding a pixel copy on every
SKImage.FromBitmapwrap (used by--imagescrop rendering)
All 88 unit tests and the full integration smoke suite (export, apply-markup,
annotation extraction, image crops) pass unchanged.
Full Changelog: v0.10.0...v0.10.1
v0.10.0
What's new
- RailReader.Core bumped 0.56.1 → 0.60.2 — picks up several upstream bug-fix releases (OCR request cancellation and threading fixes, scanned-page deskew, and the 0.60.2 GPU layout-detection correctness fix), none of which are breaking for RailMark.
- New
--accelerator cpu|gpuflag for--export— opportunistically runs layout detection on a native WebGPU execution provider, falling back to CPU automatically if no GPU device is available. Always uses the plain FP32 PP-DocLayoutV3 model on GPU (never Heron, never an FP16 export), verified to produce byte-identical Markdown to the CPU path.
Upgrading
railmark --version # should report 0.10.0
See install.sh in this repo, or download the platform artifact below directly.
v0.9.1 — mirrored screenshot crops, split hyphenated words, Windows image links
Bug-fix release. Two defects in annotation mode, a Core bump, and the release pipeline that now builds and tests these artifacts automatically.
Fixed
--imagescropped every screenshot from the wrong half of the page. Rectangle and freehand crops were mirrored vertically: an annotation near the top of a page produced an image of the bottom. Only annotations straddling the page's vertical centre came out looking right, which is how it went unnoticed. (#22)- Wrap-hyphenated words split apart in annotation output. A highlight spanning a line-break hyphen rendered as
inter pretableinstead ofinterpretable. The markup geometry was always correct — this was the extracted text. (#21) - Invisible control characters in exported Markdown.
--exportemitted U+0002 where it rejoined a hyphenated word, so the output read correctly but did not contain the word as a searchable string. Fixed upstream in RailReader.Core 0.56.1 (RailReaderCore#101).
Changed
- RailReader.Core packages 0.56.0 → 0.56.1.
- The
railmark-markup-planskill is now self-installing: point Claude Code at its SKILL.md and ask it to install, and it sets up both the CLI and the skill, on Linux or Windows. (#20)
Infrastructure
- CI builds and tests every push and pull request, and asserts the native libraries land in both the
linux-x64andwin-x64publish output. - Tagged releases now build, smoke-test and package all three artifacts automatically, with
SHA256SUMS. - A synthetic test fixture plus 23 integration checks that run against each release artifact, not just against a dev build. Both fixed defects above were caught by that fixture. (#19)
Acknowledgements
Several text-handling behaviours in annotation mode were adapted from pdfannots by Andrew Baumann — de-hyphenation and its real-hyphen guard, ligature and smart-quote normalisation, Skim duplicate-comment suppression, the caret/strikeout editorial vocabulary, and within-page annotation ordering. See the README's Acknowledgements section.
Artifacts
All self-contained — the .NET runtime, native libraries and the ONNX layout model are bundled, so there is nothing else to install. Verify against SHA256SUMS.
| File | Platform |
|---|---|
railmark-0.9.1-linux-x86_64.AppImage |
Linux x86-64 — recommended |
railmark-0.9.1-linux-x86_64.tar.gz |
Linux x86-64 — for hosts without FUSE |
railmark-0.9.1-win-x64.zip |
Windows x64 |
Install
bash <(curl -fsSL https://raw.githubusercontent.com/sjvrensburg/railmark/main/install.sh)Or download an artifact below and put it on your $PATH.
v0.9.0 — better quote matching, full text-markup rendering
Feature release. Bumps the RailReader.Core packages and adapts several text-handling techniques from 0xabu/pdfannots.
Quote matching is much more forgiving
--apply-markup resolves a quote against the PDF's extracted text. Two forms you'd naturally write previously failed outright:
- Ligatures — quoting
the first difficultynow matches a PDF that encodesfi/ffias single glyphs. - Smart punctuation — straight
"and'match curly“ ” ‘ ’, and...matches…. - Wrap hyphens — quoting
interpretable in practicematches a PDF that breaks it asinter-\npretable in practice, and the markup correctly spans both lines. Real hyphens survive (COVID-19,H-1).
Annotation output
- Fixed: underline, strikeout and squiggly annotations were silently dropped from annotation-mode Markdown. All four text-markup types are now rendered.
- Fixed: underline and squiggly annotations produced no text at all, because their stored geometry is a thin baseline band rather than the glyph box.
- Strikeouts and carets now read as editorial intent — a strikeout is a suggested deletion, a caret a suggested insertion, and a caret paired with a strikeout collapses into a single suggested replacement (
~~old~~ → **new**). - A comment that merely repeats the text it covers — as Skim writes it — is dropped instead of printed twice.
- Headings are matched by position within the page, so two headings on one page no longer file every annotation under the first.
- The summary table's
Highlightscolumn is nowText markupand counts all four subtypes.
Under the hood
RailReader.Export / Renderer.Skia / Core.Analysis 0.52.1 → 0.56.0. Purely additive — the notable addition is OCR deskew, which corrects sub-degree scanner rotation on scanned pages going through --export, with nothing to configure.
Install
bash <(curl -fsSL https://raw.githubusercontent.com/sjvrensburg/railmark/main/install.sh)Or download the AppImage below and put it on your $PATH.