mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
* fix(verify-layout): walk PATH candidates for pdftotext supporting -bbox (#655) Git for Windows prepends C:\Program Files\Git\mingw64\bin to PATH under Git Bash (which Claude Code's Bash tool uses on Windows). That directory ships an xpdf pdftotext 4.00 without -bbox support. Previously, verify_layout.py resolved whichever pdftotext shutil.which found first, causing the layout check to exit 2 with skipped: on Windows even when a working Poppler build (e.g. from MiKTeX, Chocolatey, or Scoop) was installed further down PATH. Walk all candidates returned by shutil.which("pdftotext", path=entry) for each entry in os.get_exec_path(), de-duplicated. Probe each candidate's -h output for -bbox support, and call subprocess.run with the full path of the first working binary instead of the bare command name. If no candidate supports -bbox, keep today's skip message on failure. Tests added to TestExtractorFailure in tests/test_verify_layout.py: - First candidate lacks -bbox and second has it (second candidate's full path is run) - No candidate has -bbox (skip path is kept) - PATH candidate de-duplication (avoids redundant probes) Fixes #655 * perf(verify-layout): cache resolved pdftotext binary at module level * changelog: place the #668 entry under [Unreleased] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Mads Lorentzen <madslorentzen_17@hotmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4 files changed
+187
-8
No files matched your search
+50
-7
@@ -27,9 +27,9 @@ Geometry comes from Poppler word bounding boxes (`pdftotext -bbox`). Poppler is
|
||||
repo-wide - since #369 `verify_pdf.py` prefers pypdf and falls back to Poppler - but word
|
||||
bounding boxes have no pypdf equivalent, so this is the one step that still wants it.
|
||||
Without it, or with a broken extractor, the check reports `skipped:` and exits 2 rather
|
||||
than inventing a layout failure. A broken extractor has two distinct causes: an xpdf-based
|
||||
`pdftotext` (Git for Windows ships one ahead of Poppler in PATH) has no `-bbox` flag at
|
||||
all, and Poppler 26.0x before 26.05 aborts `-bbox`/`-bbox-layout`/`-htmlmeta` on a PDF
|
||||
than inventing a layout failure. A broken extractor has two distinct causes: no `pdftotext`
|
||||
on PATH has the `-bbox` flag (Git for Windows ships an xpdf build without it, and the check
|
||||
skips past it to any Poppler further down PATH), and Poppler 26.0x before 26.05 aborts `-bbox`/`-bbox-layout`/`-htmlmeta` on a PDF
|
||||
whose Info dictionary carries an empty string in any field - which `hyperref` writes for
|
||||
every field it does not set, so any `lualatex`/`pdflatex` document built with `hyperref`
|
||||
and no `\\hypersetup{pdftitle=...}` triggers a real Poppler crashing on a legal PDF (#451).
|
||||
@@ -59,6 +59,8 @@ suite needs neither Poppler nor a compiled PDF.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -183,12 +185,53 @@ class Page:
|
||||
return max((tops[i + 1] - tops[i], tops[i]) for i in range(len(tops) - 1))
|
||||
|
||||
|
||||
@functools.cache
|
||||
def find_pdftotext() -> str | None:
|
||||
"""Find a pdftotext binary that supports Poppler's -bbox flag.
|
||||
|
||||
Git for Windows prepends Git's mingw64\\bin to PATH under Git Bash (used by
|
||||
Claude Code), shipping an xpdf pdftotext without -bbox. If a working Poppler
|
||||
is installed further down PATH (e.g. MiKTeX, Chocolatey, Scoop), probe each
|
||||
candidate with -h and pick the first that advertises -bbox (#655). Falls back
|
||||
to the first candidate if none advertises -bbox, so the caller keeps today's
|
||||
skip message on failure. Returns None if no pdftotext is found at all.
|
||||
|
||||
Cached at module level to avoid re-probing candidates with subprocesses on
|
||||
every parse_pdf call within a process.
|
||||
"""
|
||||
candidates: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for entry in os.get_exec_path():
|
||||
candidate = shutil.which("pdftotext", path=entry)
|
||||
if candidate and candidate not in seen:
|
||||
seen.add(candidate)
|
||||
candidates.append(candidate)
|
||||
if not candidates:
|
||||
return None
|
||||
for candidate in candidates:
|
||||
try:
|
||||
res = subprocess.run(
|
||||
[candidate, "-h"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
output = (res.stdout or "") + (res.stderr or "")
|
||||
if "-bbox" in output:
|
||||
return candidate
|
||||
except (subprocess.SubprocessError, OSError):
|
||||
continue
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def parse_pdf(path: Path) -> list[Page]:
|
||||
if not shutil.which("pdftotext"):
|
||||
binary = find_pdftotext()
|
||||
if not binary:
|
||||
raise RuntimeError("pdftotext (Poppler) not found; install poppler-utils")
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["pdftotext", "-bbox", "-enc", "UTF-8", str(path), "-"],
|
||||
[binary, "-bbox", "-enc", "UTF-8", str(path), "-"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
# pdftotext emits UTF-8; without this Windows decodes it as cp1252 and
|
||||
@@ -210,8 +253,8 @@ def parse_pdf(path: Path) -> list[Page]:
|
||||
detail = stderr_lines[0] if stderr_lines else f"exit {exc.returncode}"
|
||||
raise RuntimeError(
|
||||
f"pdftotext could not produce bounding boxes for {path} ({detail}); "
|
||||
"either the pdftotext first in PATH is an xpdf build with no -bbox flag "
|
||||
"(Git for Windows ships one ahead of Poppler), or Poppler aborted on this "
|
||||
"either no pdftotext on PATH has the -bbox flag (Git for Windows ships an "
|
||||
"xpdf build without it), or Poppler aborted on this "
|
||||
"document - Poppler 26.0x before 26.05 aborts on a PDF whose Info "
|
||||
"dictionary carries an empty string, as hyperref writes when pdftitle is unset"
|
||||
) from exc
|
||||
|
||||
Reference in new issue
Block a user