Fix: use a fractional thumbnail DPI so oversized pages render a bounded raster

This commit is contained in:
stumpylog committed 2026-10-15 09:05:33 -07:00
1 parent 4c5927a45f
commit 31c40207a8
2 files changed
+33 -20

No files matched your search

+10 -10
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
import logging
import math
import mimetypes
import shutil
import subprocess
@@ -89,7 +88,7 @@ def rasterize_pdf_page_to_png(
in_path: Path,
out_path: Path,
*,
dpi: int,
dpi: float,
logging_group=None,
) -> None:
"""
@@ -156,10 +155,15 @@ def encode_thumbnail_webp(
raise ParseError(f"Unable to encode thumbnail from {png_path}") from e
def _compute_thumbnail_dpi(in_path: Path, logging_group=None) -> tuple[int, int]:
def _compute_thumbnail_dpi(in_path: Path, logging_group=None) -> tuple[float, int]:
"""
Returns (dpi, supersample). Unknown geometry is not supersampled, since
the render size cannot be bounded.
The DPI is left fractional rather than floored at 1, so an oversized page
still renders at about the thumbnail size instead of an unbounded raster.
pdftoppm rounds pixel dimensions up, so the render is never short of the
target; the shrink-only clamp in encode_thumbnail_webp trims any excess.
"""
from paperless.parsers.utils import get_pdf_first_page_size_points
@@ -174,14 +178,10 @@ def _compute_thumbnail_dpi(in_path: Path, logging_group=None) -> tuple[int, int]
width_pts, height_pts = size
dpi_for_width = _THUMBNAIL_MAX_WIDTH * 72 / width_pts
dpi_for_height = _THUMBNAIL_MAX_HEIGHT * 72 / height_pts
# Round up so the downsampled render is never a few pixels short of the
# target; the shrink-only clamp in encode_thumbnail_webp trims the excess.
dpi = max(
1,
math.ceil(min(_THUMBNAIL_MAX_DPI, dpi_for_width, dpi_for_height)),
return (
min(_THUMBNAIL_MAX_DPI, dpi_for_width, dpi_for_height),
_THUMBNAIL_SUPERSAMPLE,
)
# At the 1 DPI floor the page is already oversized, so do not supersample
return dpi, 1 if dpi == 1 else _THUMBNAIL_SUPERSAMPLE
def _render_pdf_thumbnail(
+23 -10
View File
@@ -142,15 +142,23 @@ class TestComputeThumbnailDpi:
@pytest.mark.parametrize(
("size", "expected"),
[
pytest.param((612.0, 792.0), (59, 2), id="letter-width-bound"),
pytest.param((792.0, 612.0), (46, 2), id="landscape-rounded-up"),
pytest.param((612.0, 100000.0), (4, 2), id="tall-strip-height-bound"),
pytest.param((612.0, 792.0), (500 * 72 / 612, 2), id="letter-width-bound"),
pytest.param(
(792.0, 612.0),
(500 * 72 / 792, 2),
id="landscape-width-bound",
),
pytest.param(
(612.0, 100000.0),
(5000 * 72 / 100000, 2),
id="tall-strip-height-bound",
),
pytest.param((200.0, 300.0), (180, 2), id="small-page-width-bound"),
pytest.param((72.0, 72.0), (300, 2), id="tiny-page-capped-at-300"),
pytest.param(
(1000000.0, 1000000.0),
(1, 1),
id="huge-page-minimum-one-unsupersampled",
(500 * 72 / 1000000, 2),
id="huge-page-fractional",
),
pytest.param(None, (150, 1), id="unreadable-geometry-fallback"),
],
@@ -160,7 +168,7 @@ class TestComputeThumbnailDpi:
mocker: MockerFixture,
tmp_path: Path,
size: tuple[float, float] | None,
expected: tuple[int, int],
expected: tuple[float, int],
) -> None:
"""
GIVEN:
@@ -174,7 +182,7 @@ class TestComputeThumbnailDpi:
"paperless.parsers.utils.get_pdf_first_page_size_points",
return_value=size,
)
assert _compute_thumbnail_dpi(tmp_path / "doc.pdf") == expected
assert _compute_thumbnail_dpi(tmp_path / "doc.pdf") == pytest.approx(expected)
class TestMakeThumbnailFromPdf:
@@ -194,7 +202,12 @@ class TestMakeThumbnailFromPdf:
@pytest.mark.parametrize(
("size", "expected_dpi", "expected_supersample"),
[
pytest.param((612.0, 792.0), 118, 2, id="known-geometry-2x"),
pytest.param(
(612.0, 792.0),
2 * 500 * 72 / 612,
2,
id="known-geometry-2x",
),
pytest.param(None, 150, 1, id="unreadable-geometry-plain-fallback"),
],
)
@@ -204,7 +217,7 @@ class TestMakeThumbnailFromPdf:
tmp_path: Path,
work_dir: Path,
size: tuple[float, float] | None,
expected_dpi: int,
expected_dpi: float,
expected_supersample: int,
) -> None:
"""
@@ -224,7 +237,7 @@ class TestMakeThumbnailFromPdf:
make_thumbnail_from_pdf(tmp_path / "in.pdf", work_dir)
assert rasterize.call_args.kwargs["dpi"] == expected_dpi
assert rasterize.call_args.kwargs["dpi"] == pytest.approx(expected_dpi)
assert encode.call_args.kwargs["supersample"] == expected_supersample
@pytest.mark.parametrize(