PDF OCR Bookmarks
Overview
Add navigation bookmarks (PDF outline/TOC) to scanned PDF documents by using OCR to read the table of contents page and locate section headers across all pages. Ensures bookmark jump targets match the actual content pages, not the logical page numbers printed in the TOC.
When to Use
- User asks to add bookmarks / navigation / TOC / outline to a PDF
- User asks to add 目录 / 导航栏 / 书签 to a PDF
- The PDF is a scanned image document (no extractable text layer)
- The PDF has a visible table of contents page but no embedded bookmarks
Workflow
Step 1: Detect PDF Type
Check whether the PDF has a text layer or is a scanned image:
import fitz
doc = fitz.open(input_pdf)
page = doc[0]
text = page.get_text().strip()
if len(text) < 10:
# Scanned PDF - no text layer, needs OCR
else:
# Text-layer PDF - extract text directly
- If text-layer PDF: extract TOC entries from text, find matching pages, write bookmarks. Skip to Step 5.
- If scanned PDF: proceed to Step 2.
Step 2: Set Up OCR Environment
Refer to references/environment_setup.md for detailed setup instructions. Key requirements:
- Tesseract OCR installed at
C:\Program Files\Tesseract-OCR\(Windows) - Chinese language pack (
chi_sim.traineddata) downloaded to a custom tessdata directory - PyMuPDF installed (use pdfkit-py skill's venv if direct pip install fails)
- pytesseract + Pillow installed in the same venv
Set environment variables before importing pytesseract:
os.environ['TESSDATA_PREFIX'] = 'C:/Users/USERNAME/.workbuddy/tessdata' # dir containing .traineddata files
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
Step 3: OCR the Table of Contents Page
- Ask the user which page contains the table of contents (commonly page 2-4)
- Render the TOC page at 300 DPI and OCR with
chi_sim+eng - Parse OCR text to extract section titles and their printed page numbers
- Build a preliminary bookmark list:
[[level, title, printed_page], ...]
OCR tips for TOC pages:
- Use --psm 4 (single column, variable sizes) or --psm 6 (uniform block)
- 300 DPI gives good accuracy for most scans
- Chinese-numeral section headers (一、二、三...) are reliable anchors
Step 4: OCR All Pages to Find Actual Section Positions
The printed page numbers in the TOC often do NOT match PDF page indices (cover pages, unnumbered pages cause offset). To ensure correct jump targets:
- Run
scripts/add_pdf_bookmarks.py --mode find_sectionsto OCR all pages (top 50% at 200 DPI) - Search each page's OCR text for section header keywords from the TOC
- Match each TOC entry to the actual PDF page where its header first appears
- For unmatched entries, try alternative keywords or visual inspection
Key matching strategy:
- Extract 1-3 distinctive keywords per TOC entry (e.g., "投标函", "开标一览表")
- Search page OCR text for these keywords in the first 5-8 lines (header region)
- If a keyword appears on multiple pages, prefer the earliest page where it appears as a header (short line, near top)
Step 5: Write Bookmarks to PDF
Use PyMuPDF set_toc() to write bookmarks:
import fitz
doc = fitz.open(input_pdf)
# toc = [[level, title, page_number_1based], ...]
doc.set_toc(toc)
doc.save(output_pdf, garbage=4, deflate=True)
doc.close()
Notes:
- Page numbers in TOC are 1-based (page 1 = first page)
- For parent-level bookmarks (level 1) without their own page, set page to the first child's page
- garbage=4 removes unused objects; deflate=True compresses streams
- File content is never modified - only bookmark metadata is added
Step 6: Verify and Deliver
- Reopen the output PDF and call
doc.get_toc()to verify bookmark count and page numbers - Copy to the user's desired output location
- Present the file to the user
Scripts
scripts/add_pdf_bookmarks.py
General-purpose script supporting three modes:
--mode ocr_titles: OCR each page's top region and generate one bookmark per page (fallback when no TOC page exists).
--mode find_sections: OCR all pages, then search for user-specified section keywords to find actual page positions. Outputs a JSON file mapping section names to found pages.
--mode write_bookmarks: Write a predefined bookmark list (from JSON) to a PDF.
Usage:
python add_pdf_bookmarks.py --input input.pdf --output output.pdf --mode find_sections --keywords keywords.json
python add_pdf_bookmarks.py --input input.pdf --output output.pdf --mode write_bookmarks --bookmarks bookmarks.json
Common Pitfalls
- Printed page numbers != PDF page indices: Always verify by OCR-searching for actual headers. A 3-page offset (cover + TOC) is common.
- Tesseract language pack missing: Default Windows install only includes
eng. Downloadchi_sim.traineddataseparately. - TESSDATA_PREFIX path: Must point to the directory containing the
.traineddatafiles, not a parent directory. - PyMuPDF on Python 3.13: Direct
pip install pymupdfmay fail. Use the pdfkit-py skill's venv which has PyMuPDF pre-installed. - Console encoding on Windows: Set
sys.stdout.reconfigure(encoding='utf-8')or usePYTHONUNBUFFERED=1to avoid GBK encoding errors with Chinese text. - Sandbox file access: Writing to Desktop may fail if the file is open. Save to a temp directory first, then copy.
- OCR speed: ~1 page/second at 200 DPI with
chi_sim+eng. A 100-page PDF takes ~2 minutes.