mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-15 10:26:42 +00:00
Feature: Generate PDF thumbnails via poppler utils (#14420)
* Chore: Generate PDF thumbnails with pdftoppm and Pillow instead of ImageMagick Replace the ImageMagick convert based PDF thumbnail path with pdftoppm and Pillow. Keeps thumbnails at 500px wide for small and odd sized pages, covers the qpdf repair and double failure paths, removes the unused run_convert helper, hardens thumbnail encoding, deprecates PAPERLESS_CONVERT_MEMORY_LIMIT and PAPERLESS_CONVERT_TMPDIR, and corrects the related docs and CI. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Chore: Supersample the PDF thumbnail render 2x Render the PDF page at twice the target size and downsample, with edge case handling for small and odd sized pages, simplified helpers and tests, and trimmed comments. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Fix: use a fractional thumbnail DPI so oversized pages render a bounded raster * Deprecate the unused PAPERLESS_GS_BINARY setting too * Warn at startup when pdftoppm is missing * Resample PDF thumbnails once instead of twice with LANCZOS, still never enlarging * Read PDF page size from pdfinfo so it matches pdftoppm * Mark the thumbnail constants Final * Comment each pdftoppm option
This commit is contained in:
1 parent
f730513e79
commit
a05afd654c
12 files changed
+916
-143
No files matched your search
@@ -10,7 +10,9 @@ from __future__ import annotations
|
||||
|
||||
import codecs
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -25,6 +27,15 @@ logger = logging.getLogger("paperless.parsers.utils")
|
||||
# Used by both the consumer (archive decision) and the tesseract parser (skip-OCR decision).
|
||||
PDF_TEXT_MIN_LENGTH: Final[int] = 50
|
||||
|
||||
_PDFINFO_PAGE_SIZE: Final[re.Pattern[str]] = re.compile(
|
||||
r"^Page\s+1 size:\s+([\d.eE+-]+) x ([\d.eE+-]+) pts",
|
||||
re.MULTILINE,
|
||||
)
|
||||
_PDFINFO_PAGE_ROTATION: Final[re.Pattern[str]] = re.compile(
|
||||
r"^Page\s+1 rot:\s+(\d+)",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def is_tagged_pdf(
|
||||
path: Path,
|
||||
@@ -265,6 +276,61 @@ def get_page_count_for_pdf(
|
||||
return None
|
||||
|
||||
|
||||
def get_pdf_first_page_size_points(
|
||||
path: Path,
|
||||
log: logging.Logger | None = None,
|
||||
) -> tuple[float, float] | None:
|
||||
"""Return the first page's (width, height) in PDF points, post-rotation.
|
||||
|
||||
Asks ``pdfinfo`` rather than reading the page boxes directly, so the size
|
||||
matches what ``pdftoppm -cropbox`` renders, including poppler's clipping of
|
||||
the CropBox to the MediaBox. pdfinfo reports the box size before rotation,
|
||||
so width and height are swapped for 90/270 rotation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path:
|
||||
Absolute path to the PDF file.
|
||||
log:
|
||||
Logger for warnings. Falls back to the module-level logger when omitted.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[float, float] | None
|
||||
``(width_points, height_points)``, or ``None`` if pdfinfo fails, the
|
||||
file has no pages, or the page size is degenerate.
|
||||
"""
|
||||
_log = log or logger
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["pdfinfo", "-f", "1", "-l", "1", str(path)],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
text=True,
|
||||
errors="ignore",
|
||||
# The labels parsed below are localized
|
||||
env={**os.environ, "LC_ALL": "C"},
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as e:
|
||||
_log.warning("Could not determine PDF page size for %s: %s", path, e)
|
||||
return None
|
||||
|
||||
size_match = _PDFINFO_PAGE_SIZE.search(proc.stdout)
|
||||
if size_match is None:
|
||||
_log.warning("Could not determine PDF page size for %s", path)
|
||||
return None
|
||||
|
||||
width = float(size_match.group(1))
|
||||
height = float(size_match.group(2))
|
||||
if width <= 0 or height <= 0:
|
||||
return None
|
||||
|
||||
rotation_match = _PDFINFO_PAGE_ROTATION.search(proc.stdout)
|
||||
if rotation_match is not None and int(rotation_match.group(1)) in (90, 270):
|
||||
width, height = height, width
|
||||
return width, height
|
||||
|
||||
|
||||
def extract_pdf_metadata(
|
||||
document_path: Path,
|
||||
log: logging.Logger | None = None,
|
||||
|
||||
Reference in new issue
Block a user