Skip to content

PDF Annotation

Annotate PDFs in Potato with the pdf display type, covering span highlighting, bounding boxes on the page, cross-page linking, and OCR for scanned files.

The pdf display renders a PDF in the browser with PDF.js and puts the annotation surface on the rendered page rather than on text extracted beforehand. An annotator sees the real document, with its columns, tables, figures, and page breaks intact, and labels it in place. The display takes one required key, the field holding the PDF path or URL, and everything else is a display option.

yaml
instance_display:
  fields:
    - key: pdf
      type: pdf
      label: "Document"
      display_options:
        view_mode: scroll
        max_height: 760
        zoom: page-width

view_mode takes scroll, paginated, or side-by-side. Continuous scroll stacks every page in one container, which matters when an annotation has to reach across a page break. Paginated view shows one page at a time with navigation controls and suits long documents where the annotator works page by page.

Three annotation modes

annotation_mode decides what the annotator can draw, and it is the option that changes the task rather than its appearance. The default is span.

ModeWhat the annotator doesAnchored to
spanSelects text and applies a labelThe PDF.js text layer
bounding_boxDraws a box anywhere on the pagePage coordinates
linkMarks anchors, then connects themText spans and page regions

Span mode depends on the PDF carrying a text layer, which is true of any PDF generated from a word processor or a typesetting system and false of a scan. Bounding-box mode does not, so a scanned page can still be labeled by region without any text extraction at all.

Because the pdf display anchors spans through the PDF.js text layer rather than the .text-content wrapper that other displays use, it does not accept the span_target flag. Span annotation still works, through the PDF's own text layer.

Cross-page linking

Link mode is for relations that span pages, such as a claim on page 2 that rests on a figure on page 9. The annotator marks anchors first, then draws typed links between them, and Potato records the anchors and the links under separate schema names.

yaml
instance_display:
  fields:
    - key: pdf
      type: pdf
      label: "Document"
      display_options:
        annotation_mode: link
        view_mode: scroll
        enable_text_anchors: true
        enable_region_anchors: true
        anchor_schema: pdf_anchors
        link_schema: pdf_links
        anchor_labels:
          - name: claim
            color: "#dc2626"
          - name: figure
            color: "#2563eb"
        link_types:
          - name: refers_to
            directed: true
            color: "#dc2626"
            allowed_source_labels: [claim]
            allowed_target_labels: [figure]

allowed_source_labels and allowed_target_labels restrict which anchors a link type can join, which is how a guideline becomes something the interface enforces instead of something the annotator has to remember. A refers_to link configured as above can start only on a claim and end only on a figure, so a link drawn from a figure back to a claim is refused. Set directed: false for a symmetric relation such as same_as.

Anchors come in two kinds and both can be turned off independently. enable_text_anchors allows highlighting a text span, and enable_region_anchors allows drawing a region box, which is what a figure or a table needs. A complete worked example ships upstream in examples/advanced/pdf-link-scroll/.

Scanned documents and OCR

ocr is off by default, and Potato reads it only in link mode. When it is set, words are extracted server-side and used to build a client text layer, so a scan with no embedded text can still carry text anchors. The option takes false, true, or auto, and Potato rejects any other value.

yaml
instance_display:
  fields:
    - key: pdf
      type: pdf
      display_options:
        annotation_mode: link
        ocr: auto
        ocr_dpi: 200
        ocr_lang: eng

The three settings differ in when the pass runs. false uses the embedded text layer alone, true always runs OCR, and auto falls back to OCR only when the embedded text layer comes back empty. Prefer auto on a mixed corpus, because OCR is slow and needs Tesseract installed. ocr_dpi controls the resolution each page is rasterized at before OCR reads it, and raising it costs time on every page. ocr_lang takes a Tesseract language code and defaults to eng.

Display options

OptionDefaultEffect
view_modescrollscroll, paginated, or side-by-side
max_height700Container height in pixels
max_widthnoneContainer width
text_layertrueEnables text selection
show_page_controlstruePage navigation controls
initial_page1Page shown first
zoomautoauto, page-fit, page-width, or a percentage
annotation_modespanspan, bounding_box, or link
bbox_min_size10Smallest box accepted, in pixels
bbox_colorsnonePer-label box colors
show_bbox_labelstrueDraws labels on boxes
thumbnail_sidebartruePage thumbnails in paginated view
enable_text_anchorstrueText spans usable as link anchors
enable_region_anchorstrueRegion boxes usable as link anchors
anchor_schemapdf_anchorsSchema recorded on anchors
link_schemapdf_linksSchema recorded on links
ocrfalsefalse, true, or auto
ocr_dpi200OCR rendering resolution
ocr_langengTesseract language code

Word and Markdown files

A DOCX or Markdown file uses the document display instead, which keeps the document's heading and paragraph structure and does accept span_target. It reads annotation_mode of span or bounding_box, and show_outline builds a table of contents from the headings in the body. Potato passes the rendered markup through an allowlist before it reaches the annotator, so a corpus field carrying an executable tag is stripped rather than run.

yaml
instance_display:
  fields:
    - key: report
      type: document
      label: "Report"
      display_options:
        show_outline: true
        max_height: 600

Further reading