Extracting Hyperlinks from PDFs as Markdown Links Using pdf-inspector

Use pdf-inspector to parse PDF link annotations and render them as [text](url) Markdown syntax through a three-stage pipeline: annotation detection, TextItem creation with ItemType::Link(url), and final Markdown conversion.

The pdf-inspector library by Firecrawl provides a robust Rust-based solution for extracting hyperlinks from PDF documents and converting them to standard Markdown link format. Unlike simple text extraction tools, it preserves the semantic structure of links by parsing PDF annotation dictionaries directly.

The extraction pipeline operates in three distinct stages, each isolated to handle any document type—text-based, scanned, or mixed.

The extractor walks each page's /Annots array in src/extractor/links.rs, filtering for annotations where Subtype equals Link. The extract_page_links function reads the rectangle (Rect) for positioning and calls extract_link_uri to retrieve the destination URI from the annotation's A dictionary.

let annots = page_dict.get(b"Annots") …;   // line 56-64

Only annotations matching the Link subtype are processed:

if let Ok(subtype) = annot_dict.get(b"Subtype") {
    if let Ok(name) = subtype.as_name() {
        if name != b"Link" { continue; }
    }
}

For every valid link, pdf-inspector builds a TextItem with position fields (x, y, width, height) and item_type set to ItemType::Link(url). This allows the rest of the pipeline to treat links like normal text while preserving the URL.

links.push(TextItem {
    text: url.clone(),
    // geometry fields...
    item_type: ItemType::Link(url),
    // ...
});

Stage 3: Markdown Conversion

During final rendering in src/markdown/convert.rs, the renderer inspects each TextItem. When it encounters ItemType::Link(_), it emits standard Markdown link syntax:

ItemType::Link(url) => format!("[{}]({})", escaped_text, url);

The pdf2md CLI tool provides the fastest way to extract PDF hyperlinks as Markdown:

pdf2md --json my-document.pdf > output.md

The --json flag returns structured output where each link appears as:

{
  "text": "https://example.com",
  "page": 2,
  "item_type": { "Link": "https://example.com" },
  ...
}

Use extract_text_with_positions to pull links and print them as Markdown:

use pdf_inspector::extractor::extract_text_with_positions;
use pdf_inspector::types::ItemType;

fn main() -> Result<(), pdf_inspector::PdfError> {
    let items = extract_text_with_positions("my-document.pdf")?;
    for item in items {
        if let ItemType::Link(url) = &item.item_type {
            // Use surrounding text as label; here we reuse the URL
            println!("[{}]({})", url, url);
        }
    }
    Ok(())
}

Full PDF-to-Markdown Conversion

Let the library handle all link formatting automatically:

use pdf_inspector::markdown::convert::to_markdown;
use pdf_inspector::extractor::extract_text_with_positions;

let items = extract_text_with_positions("my-document.pdf")?;
let markdown = to_markdown(items, pdf_inspector::markdown::MarkdownOptions::default());
// `markdown` now contains all text with `[label](url)` links

Key Source Files and Their Roles

File Purpose
src/extractor/links.rs Detects link annotations, extracts URIs via extract_link_uri, creates TextItem with ItemType::Link
src/markdown/convert.rs Renders ItemType::Link as [text](url) Markdown syntax
src/bin/pdf2md.rs CLI front-end calling the extractor and outputting Markdown/JSON
src/types.rs Defines TextItem struct and ItemType enum including the Link variant
src/extractor/mod.rs Public API wiring link extraction into the overall text-extraction flow

Safety and Performance Considerations

The link extraction is deliberately isolated from text-layout logic, ensuring compatibility across document types. Budget constants MAX_FORM_FIELD_NODES and MAX_FORM_FIELD_DEPTH protect against pathological PDFs that could cause stack overflows during AcroForm field traversal.

Summary

  • Link detection happens in src/extractor/links.rs via extract_page_links and extract_link_uri
  • URI extraction reads the A dictionary's URI entry from PDF annotation objects
  • Markdown output uses ItemType::Link(url) pattern matching in src/markdown/convert.rs
  • CLI usage: pdf2md --json document.pdf outputs structured links
  • Library usage: extract_text_with_positions followed by to_markdown for full conversion

Frequently Asked Questions

Does pdf-inspector handle both text-based and scanned PDFs?

Yes. The link extraction operates on PDF annotation dictionaries, which exist independently of whether the page content is text or images. The position-based TextItem system treats links uniformly regardless of underlying content type.

What URL formats does pdf-inspector support?

The library extracts any URI string present in the annotation's A dictionary URI entry. This includes http://, https://, mailto:, file://, and custom scheme URLs as encoded in the PDF.

The renderer uses the surrounding or associated text from the TextItem as the link label. If the link annotation has no explicit text content, the raw URL becomes the label. You can customize this by manipulating TextItem fields before calling to_markdown.

Yes. Call extract_text_with_positions directly and filter for ItemType::Link(url) variants. This bypasses the Markdown renderer entirely while still giving you access to all hyperlink data including positions and page numbers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →