Multi-Page PDF Anchors & Cross-Page Linking¶
Annotate multi-page PDFs by creating anchors on any page and linking them together — including across pages. Two kinds of anchors are supported and are freely linkable in any combination:
- Text spans — highlight text on a page.
- Region (bbox) anchors — draw a box around a non-text region such as a figure, table, or image.
Links (text↔text, text↔region, region↔region) are drawn as SVG arcs over a single overlay that spans the whole page stack, so a link between page 1 and page 3 is drawn as one continuous arc.
This is enabled with the PDF display's annotation_mode: link.
For plain single-field PDF text/region annotation without linking, see Format Support → PDF. This page covers the linking mode.
Quick start¶
Two runnable examples ship with Potato — launch either from the repo root and compare:
# Continuous scroll: all pages stacked, arcs drawn across the scroll
python potato/flask_server.py start examples/advanced/pdf-link-scroll/config.yaml -p 8000
# Paginated: one page at a time, page-thumbnail browser + pin-navigate
python potato/flask_server.py start examples/advanced/pdf-link-paginated/config.yaml -p 8000
Configuration¶
instance_display:
fields:
- key: pdf
type: pdf
label: "Document"
display_options:
annotation_mode: link # enable anchors + cross-page linking
view_mode: scroll # "scroll" (all pages) or "paginated" (page browser)
max_height: 720
zoom: page-width # auto | page-fit | page-width | <number>
enable_text_anchors: true # allow text-span anchors
enable_region_anchors: true # allow region (bbox) anchors
thumbnail_sidebar: true # page-thumbnail browser (paginated view)
anchor_schema: pdf_anchors # schema recorded on saved anchors
link_schema: pdf_links # schema recorded on saved links
# Labels applied to text spans and region boxes
anchor_labels:
- name: claim
color: "#dc2626"
- name: figure
color: "#2563eb"
- name: citation
color: "#059669"
# Relationship types between anchors
link_types:
- name: refers_to
directed: true # draws an arrowhead
color: "#dc2626"
allowed_source_labels: [claim] # optional constraints
allowed_target_labels: [figure]
- name: same_as
directed: false
color: "#7c3aed"
The pdf field value is a URL or a local path served via the /media/ route
(set media_directory: in the config and reference /media/<file>.pdf).
Display options¶
| Option | Default | Description |
|---|---|---|
annotation_mode |
span |
Set to link for this mode |
view_mode |
scroll |
scroll stacks all pages; paginated shows one page + a thumbnail browser |
zoom |
auto |
auto, page-fit, page-width, or a numeric scale |
enable_text_anchors |
true |
Allow highlighting text as anchors |
enable_region_anchors |
true |
Allow drawing region boxes as anchors |
thumbnail_sidebar |
true |
Show the page-thumbnail browser (paginated view) |
anchor_labels |
— | List of labels (name or {name, color}) for anchors |
link_types |
[{name: related_to}] |
Relationship types; each supports directed, color, max_spans, allowed_source_labels, allowed_target_labels |
anchor_schema / link_schema |
pdf_anchors / pdf_links |
Schema names recorded on saved annotations |
Using the interface¶
- Pick a label in the toolbar.
- Text anchor: select text on a page. Region anchor: click Draw region, then drag a box on a page.
- Link: click Link mode, choose a link type, then click two anchors
(on the same or different pages). For a directed link the first click is the
source. Directed links with
allowed_source_labels/allowed_target_labelsenforce those constraints. - In scroll view all pages are visible, so cross-page arcs draw directly.
In paginated view, use the thumbnail browser to navigate; if a link's
other endpoint is on a page that isn't currently rendered, an off-page stub
(
→ p.N) is shown until you navigate to make both endpoints visible. - Hover an anchor and click × to delete it (its links are removed too); click an arc to delete a link.
How it persists¶
Anchors and links use Potato's existing storage — no new data model:
- Each anchor is a
SpanAnnotationwhose geometry rides informat_coords:{"format": "pdf", "anchor_kind": "text"|"region", "page": N, "bbox": [x, y, w, h] (normalized 0–1), "start": s, "end": e}. - Each link is a
SpanLinkreferencing anchor ids, withanchor_pagesandanchor_kindsstored inproperties.
Everything round-trips through /updateinstance, /api/spans/<id> and
/api/links/<id> and restores on reload.
Scanned / image-only PDFs (OCR)¶
Region anchors work on any PDF. Text anchors need a text layer; scanned PDFs
have none. Enable opt-in OCR directly on the PDF field: words are extracted
server-side (Tesseract) and handed to the client to build a selectable text
layer over the page image, so text-span anchors work on scanned pages too. It is
off by default because it is slow to initialize and requires pytesseract
plus the tesseract binary.
- key: pdf
type: pdf
display_options:
annotation_mode: link
ocr: auto # false (default) | true (always OCR) | auto (only pages with no embedded text)
ocr_dpi: 200 # rasterization DPI (higher = slower/sharper)
ocr_lang: eng # Tesseract language
With auto, pages that already have an embedded text layer use it (fast) and
only image-only pages are OCR'd. OCR runs when the instance is rendered, so
initial load is slower for scanned documents.
Offline / air-gapped deployments¶
PDF.js is vendored at potato/static/vendor/pdfjs/ and loaded locally, so this
mode works without internet access (it falls back to a CDN only if the local
copy is missing).
Related¶
- Format Support — single-field PDF/document display
- Span Linking — arc-based linking for plain text spans