feat(pdf): add pdf extract command with pdftotext/pypdf fallback

Add `mytoolkit pdf extract` to extract text from PDFs. Tries pdftotext
(poppler) first for layout quality, falls back to pypdf.
This commit is contained in:
Zhengshou Lai
2026-06-09 14:38:24 +08:00
parent 3f7c625781
commit 5d0ed21e07
+72
View File
@@ -1,6 +1,7 @@
"""PDF utilities."""
import shutil
import subprocess
from pathlib import Path
import click
@@ -165,6 +166,77 @@ def merge_pdfs(files, output, bookmark_depth, dry_run):
click.echo(f"Merged {len(processed)} files into {output}")
@pdf.command("extract")
@click.argument("input_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option(
"-o",
"--output",
type=click.Path(dir_okay=False, path_type=Path),
help="Output text file. Defaults to stdout.",
)
@click.option(
"-p",
"--pages",
help="Page range to extract (e.g. 1-5, 1,3,5). Defaults to all pages.",
)
@click.option(
"--layout/--no-layout",
default=False,
help="Preserve original layout using pdftotext -layout. Only applies when pdftotext is available.",
)
@handle_errors
def extract_text(input_file, output, pages, layout):
"""Extract text from a PDF file.
Tries pdftotext (poppler) first for best quality, falls back to pypdf.
"""
text = ""
use_pdftotext = shutil.which("pdftotext") is not None
if use_pdftotext:
cmd = ["pdftotext"]
if layout:
cmd.append("-layout")
if pages:
cmd.extend(["-f", str(pages.split("-")[0]), "-l", str(pages.split("-")[-1])])
cmd.extend([str(input_file), "-"])
result = subprocess.run(cmd, capture_output=True, text=True)
text = result.stdout
else:
reader = PdfReader(str(input_file))
page_indices = _parse_page_range(pages, len(reader.pages))
text = "\n\n".join(
reader.pages[i].extract_text() or ""
for i in page_indices
)
if output:
output.write_text(text, encoding="utf-8")
click.echo(f"Extracted text to {output}")
else:
click.echo(text)
def _parse_page_range(pages: str | None, total: int) -> list[int]:
"""Parse a page range string into 0-based page indices."""
if not pages:
return list(range(total))
indices = set()
for part in pages.split(","):
part = part.strip()
if "-" in part:
start, end = part.split("-", 1)
start = int(start.strip()) - 1 if start.strip() else 0
end = int(end.strip()) if end.strip() else total
indices.update(range(max(0, start), min(total, end)))
else:
idx = int(part) - 1
if 0 <= idx < total:
indices.add(idx)
return sorted(indices)
@pdf.command("bookmark")
@click.argument("input_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option(