| name | pdf-to-markdown |
| description | Split a PDF into chunks and convert it to readable markdown text. Use when the user wants to read, extract, or convert a PDF document. |
| user-invocable | true |
| argument-hint | ["path/to/file.pdf"] |
| allowed-tools | Read, Bash(mdls *), Bash(pdftotext *), Bash(which *), Write |
PDF Split & Convert
Convert a PDF file to readable markdown text. Handles large PDFs efficiently.
Input
$ARGUMENTS[0] — Path to the PDF file (required)
Choose the method first
Before reading anything, check whether pdftotext (part of poppler) is available:
which pdftotext
- If available → use the pdftotext path below. It is 10–100× faster than the Read tool, uses no model context for the text content, and doesn't suffer from stream idle timeouts. Always prefer this for PDFs longer than ~30 pages.
- If not available → fall back to the Read-tool path. Warn the user that large PDFs (>40 pages) may hit stream idle timeouts when run in subagents (~12 min cap). Prefer running in the main conversation for large files.
Resolve the path and get page count
- If the path is relative, resolve it relative to the current working directory.
- Run
mdls -name kMDItemNumberOfPages "<pdf_path>" to get the total page count. If mdls is unavailable or returns (null), use pdftotext or Read to probe.
Method A — pdftotext (preferred)
Extract the full PDF to text in one shot, then trim at references/appendix and add page markers. pdftotext emits a form-feed character (\f) at every page break — use that for pagination.
Reference implementation (bash + awk):
PDF="$1"
OUT="${PDF%.pdf}.md"
TITLE=$(basename "${PDF%.pdf}")
TMPTXT=$(mktemp)
pdftotext -layout "$PDF" "$TMPTXT"
awk -v title="$TITLE" '
BEGIN {
print "# " title
print ""
page = 1
printf "---\n## Pages %d-%d\n---\n", page, page+19
next_marker = page + 20
}
{
# Convert form-feed page breaks to newlines and count pages
n = gsub(/\f/, "\n")
if (n > 0) {
page += n
if (page >= next_marker) {
printf "\n---\n## Pages %d-%d\n---\n", next_marker, next_marker+19
next_marker += 20
}
}
# Build a stripped copy for heading detection.
# CRITICAL: strip both form feeds AND embedded newlines — gsub above inserts
# newlines into $0, which will defeat regex anchors like ^ and $ if you skip this.
stripped = $0
gsub(/[\f\n]/, "", stripped)
sub(/^[ \t]+/, "", stripped)
sub(/[ \t]+$/, "", stripped)
if (length(stripped) > 0) {
# References / Bibliography — standalone word, optionally numbered, short, no prose punctuation
if (length(stripped) < 50 && stripped !~ /[(),;]/) {
if (stripped ~ /^([0-9]+\.?[ \t]+)?(References|REFERENCES|Bibliography|BIBLIOGRAPHY)$/) exit
if (stripped == "Works Cited") exit
}
# Appendix — length up to ~120 chars (some titles are long), no prose punctuation
if (length(stripped) < 120 && stripped !~ /[(),;]/) {
# "Appendix A" alone (bare letter, no title)
if (stripped ~ /^Appendix[ \t]+[A-Z][0-9]*$/) exit
# "Appendix A. Title" or "Appendix A: Title" — punctuation REQUIRED to avoid
# matching body-text references like "Appendix H examines the effect..."
if (stripped ~ /^Appendix[ \t]+[A-Z][0-9]*[.:][ \t]+[A-Z].*$/) exit
# "APPENDIX A" variants
if (stripped ~ /^APPENDIX[ \t]+[A-Z][0-9]*([ \t]+.*)?$/) exit
# "Online Appendix [A]"
if (stripped ~ /^Online[ \t]+Appendix([ \t]+[A-Z].*)?$/) exit
# "Supplemental/Supplementary/Internet Appendix"
if (stripped ~ /^(Supplement(al|ary)|Internet)[ \t]+Appendix([ \t]+.*)?$/) exit
}
}
print
}
' "$TMPTXT" > "$OUT"
rm -f
After running, sanity-check the output:
grep -cE "^[[:space:]]*(References|Bibliography|REFERENCES|BIBLIOGRAPHY)[[:space:]]*$" "$OUT"
tail -5 "$OUT"
If the tail looks truncated mid-paragraph, the heading detection likely fired on a false positive. If the tail shows references or appendix content, the detection missed the heading — inspect the PDF text around that area and extend the regex.
Method B — Read tool (fallback when pdftotext unavailable)
- Read the PDF in chunks of up to 20 pages at a time using the Read tool's
pages parameter: 1-20, 21-40, 41-60, etc.
- Focus on the MAIN TEXT ONLY. Stop including content once you hit "References", "Bibliography", "Works Cited", or an appendix section. If references appear mid-chunk, keep everything before them and drop the rest.
- Compile output:
- Save alongside the PDF with a
.md extension.
- Header:
# [Original Filename]
- Page markers between chunks:
---\n## Pages X-Y\n---
- Preserve extracted text as-is.
Warning: The Read tool is slow for large PDFs (roughly 30–60 seconds per 20-page chunk). A 60-page paper can take 3–4 minutes, and sub-agents have a ~13-minute stream idle timeout that this can hit. When running a batch of conversions, run them sequentially in the main conversation or use Method A.
Heading-detection pitfalls (hard-won lessons)
These false positives broke earlier attempts — keep them in mind whether you use Method A or B:
- Parenthetical references in body text like
(see Appendix B.5) — exclude lines containing (, ), ,, or ;.
- Body text starting with "Appendix X ..." like
Appendix H examines the differential effect... — require punctuation (. or :) immediately after the appendix letter when a title follows. Bare "Appendix A" alone on a line is still valid.
- Line-wrapped headings —
pdftotext can wrap Online Appendix across two lines if the PDF's layout is unusual. You'll see Online on one line and Appendix on the next. The regex above matches the joined form; if you see false trims at a lone Appendix line, inspect and tighten.
- Form-feed at start of page —
pdftotext emits \f as the first character on every new page. After gsub(/\f/, "\n") on $0, the line has an embedded \n that defeats ^ / $ anchors unless you also gsub(/[\f\n]/, "", stripped) on your detection copy.
- Length thresholds — simple "< 50 chars" is too tight for appendix titles. "Appendix A. Merging Mortgages with the Real Estate Database" is 59 chars. Use ~120 for appendix patterns, ~50 for bare References.
- Numbered section headings — some papers format as
7 References or 7. References. Allow an optional leading number.
Report results
Tell the user:
- Method used (pdftotext vs Read tool)
- Total pages processed
- Output file path and final line count
- Where trimming occurred (last section / page number included)
- Any pages that were unreadable or empty
For batch conversions, print a summary table and note any files whose trim point looks suspicious (very short output, or output ending mid-sentence).