Claude Skill

pdf

Extract text from PDF documents

LLM Mart · 0 points · 18 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download axoviq-ai-synthadoc-synthadoc_skills_pdf-8dee0ee.zip · 3 KB
Part of axoviq-ai/synthadoc — 10 skills

Install

skills CLI npx skills add https://github.com/axoviq-ai/synthadoc/tree/main/synthadoc/skills/pdf
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install axoviq-ai-synthadoc@llmmart
Git git clone https://github.com/axoviq-ai/synthadoc.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole axoviq-ai/synthadoc collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

PDF Skill

Extracts text from PDF files using pypdf as the primary parser, with pdfminer.six as a fallback for CJK fonts that pypdf cannot decode (detected when pypdf yields fewer than 50 characters per page on average).

Setup

pip install pypdf pdfminer.six

Standalone usage

import asyncio
from synthadoc.skills.pdf.scripts.main import PdfSkill

skill = PdfSkill()

async def main():
    result = await skill.extract("/path/to/paper.pdf")
    print(result.text)          # extracted text from all pages
    print(result.metadata)      # {"pages": N, "cjk_fallback": bool, ...}

asyncio.run(main())

When this skill is used

  • Source path ends with .pdf
  • User intent contains: pdf, research paper

Scripts

  • scripts/main.py — PdfSkill class

References

  • references/cjk-notes.md — notes on CJK font handling
Files (synthadoc)
  • references
    • cjk-notes.md 399 B
      # CJK Font Handling Notes
      
      pypdf cannot decode PDF files that use fonts without embedded ToUnicode CMaps —
      common in documents typeset with Chinese, Japanese, or Korean character sets.
      In this case pypdf returns near-empty text (< 50 chars/page on average).
      
      pdfminer.six uses its own CMap tables and handles these fonts correctly.
      The threshold `_MIN_CHARS_PER_PAGE = 50` was chosen empirically.
      
  • scripts
    • main.py 3.6 KB
      # SPDX-License-Identifier: AGPL-3.0-or-later
      # Copyright (C) 2026 Paul Chen / axoviq.com
      import asyncio
      import logging
      
      import pypdf
      
      from synthadoc.skills.base import BaseSkill, ExtractedContent, SkillMeta
      
      logger = logging.getLogger(__name__)
      
      # pypdf logs benign structural warnings (e.g. incorrect startxref pointer) at WARNING
      # level for many real-world PDFs. Suppress them so they don't pollute the console.
      logging.getLogger("pypdf").setLevel(logging.ERROR)
      
      # If pypdf extracts fewer than this many characters per page on average,
      # the PDF likely uses CJK fonts whose ToUnicode CMaps pypdf cannot decode.
      # In that case we fall back to pdfminer.six which has better CJK support.
      _MIN_CHARS_PER_PAGE = 50
      
      
      def _build_pagemap(page_texts: list[str]) -> dict[int, int]:
          """Return {first_line_of_page: pdf_page_number} for each non-empty page.
      
          Line numbers are 1-based and reference the concatenated extracted text.
          Empty pages are skipped and do not advance the line counter.
          """
          pagemap: dict[int, int] = {}
          current_line = 1
          for page_num, text in enumerate(page_texts, start=1):
              if text:
                  pagemap[current_line] = page_num
                  current_line += text.count("\n") + 1
          return pagemap
      
      
      class PdfSkill(BaseSkill):
          meta = SkillMeta(name="pdf", description="Extract text from PDF files", extensions=[".pdf"])
      
          async def extract(self, source: str) -> ExtractedContent:
              # pypdf and pdfminer are synchronous CPU-bound libraries; run them in a
              # thread pool so they do not block the asyncio event loop and starve
              # other coroutines (e.g. HTTP handlers, jobs list) while processing
              # large PDFs.
              text, num_pages, pagemap = await asyncio.to_thread(self._extract_pypdf, source)
      
              # Low yield → likely CJK fonts that pypdf cannot decode; try pdfminer fallback
              if num_pages > 0 and len(text.strip()) < num_pages * _MIN_CHARS_PER_PAGE:
                  logger.debug(
                      "pypdf yielded %d chars for %d page(s) in %s — trying pdfminer fallback",
                      len(text.strip()), num_pages, source,
                  )
                  fallback = await asyncio.to_thread(self._extract_pdfminer, source)
                  if len(fallback.strip()) > len(text.strip()):
                      text = fallback
                      pagemap = {1: 1}  # pdfminer has no page-level info
      
              return ExtractedContent(text=text, source_path=source,
                                      metadata={"pages": num_pages, "page_boundaries": pagemap})
      
          def _extract_pypdf(self, source: str) -> tuple[str, int, dict[int, int]]:
              try:
                  parts = []
                  page_texts: list[str] = []
                  with open(source, "rb") as f:
                      reader = pypdf.PdfReader(f)
                      num_pages = len(reader.pages)
                      for page in reader.pages:
                          t = page.extract_text()
                          page_texts.append(t or "")
                          if t:
                              parts.append(t)
                  pagemap = _build_pagemap(page_texts)
                  return "\n".join(parts), num_pages, pagemap
              except (FileNotFoundError, IsADirectoryError):
                  raise
              except Exception as exc:
                  raise ValueError(
                      f"Cannot read '{source}' as a PDF file: {exc}. "
                      "Ensure the file is a valid PDF document."
                  ) from exc
      
          def _extract_pdfminer(self, source: str) -> str:
              try:
                  from pdfminer.high_level import extract_text
                  return extract_text(source) or ""
              except Exception as exc:
                  logger.debug("pdfminer fallback failed for %s: %s", source, exc)
                  return ""
      
    • __init__.py 0 B
  • requirements.txt 19 B
    pypdf
    pdfminer.six
    
  • SKILL.md 1.1 KB
    ---
    name: pdf
    version: "1.0"
    description: Extract text from PDF documents
    entry:
      script: scripts/main.py
      class: PdfSkill
    triggers:
      extensions:
        - ".pdf"
      intents:
        - "pdf"
        - "research paper"
    requires:
      - pypdf
      - pdfminer.six
    author: axoviq.com
    license: AGPL-3.0-or-later
    ---
    
    # PDF Skill
    
    Extracts text from PDF files using `pypdf` as the primary parser, with
    `pdfminer.six` as a fallback for CJK fonts that pypdf cannot decode
    (detected when pypdf yields fewer than 50 characters per page on average).
    
    ## Setup
    
    ```bash
    pip install pypdf pdfminer.six
    ```
    
    ## Standalone usage
    
    ```python
    import asyncio
    from synthadoc.skills.pdf.scripts.main import PdfSkill
    
    skill = PdfSkill()
    
    async def main():
        result = await skill.extract("/path/to/paper.pdf")
        print(result.text)          # extracted text from all pages
        print(result.metadata)      # {"pages": N, "cjk_fallback": bool, ...}
    
    asyncio.run(main())
    ```
    
    ## When this skill is used
    
    - Source path ends with `.pdf`
    - User intent contains: `pdf`, `research paper`
    
    ## Scripts
    
    - `scripts/main.py` — `PdfSkill` class
    
    ## References
    
    - `references/cjk-notes.md` — notes on CJK font handling
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related