diff --git a/src/documents/parsers.py b/src/documents/parsers.py index 75256bbed..a669c20ac 100644 --- a/src/documents/parsers.py +++ b/src/documents/parsers.py @@ -79,7 +79,8 @@ _THUMBNAIL_MAX_WIDTH = 500 _THUMBNAIL_MAX_HEIGHT = 5000 # Used only when the page geometry cannot be read _THUMBNAIL_FALLBACK_DPI = 150 -# Matches the density the thumbnail was previously rendered at before scaling +# Cap on the target density, applied before supersampling: a capped tiny page +# is rasterized at twice this and halved to the size a 300 DPI render gave _THUMBNAIL_MAX_DPI = 300 # Pages with known geometry are rendered at this multiple of the computed DPI # and downsampled with Lanczos, which keeps text noticeably crisper than @@ -196,14 +197,16 @@ def _compute_thumbnail_dpi(in_path: Path, logging_group=None) -> tuple[int, int] dpi_for_height = _THUMBNAIL_MAX_HEIGHT * 72 / height_pts # The old pipeline rendered at 300 DPI and then shrank to fit, so only # pages too small to reach the thumbnail size even at 300 DPI end up - # smaller than it. Rounding up keeps the render at or above the target, - # so the shrink-only clamp in encode_thumbnail_webp trims it to exactly - # the thumbnail size instead of leaving it a few pixels short. + # smaller than it. Rounding up keeps the downsampled render at or above + # the target, so the shrink-only clamp in encode_thumbnail_webp trims it + # to exactly the thumbnail size instead of leaving it a few pixels short. dpi = max( 1, math.ceil(min(_THUMBNAIL_MAX_DPI, dpi_for_width, dpi_for_height)), ) - return dpi, _THUMBNAIL_SUPERSAMPLE + # A page so large it hits the 1 DPI floor is already oversized, so + # doubling it would only quadruple the pixel count for no benefit + return dpi, 1 if dpi == 1 else _THUMBNAIL_SUPERSAMPLE def _render_pdf_thumbnail( diff --git a/src/documents/tests/test_parsers.py b/src/documents/tests/test_parsers.py index 3531cec92..23155b60e 100644 --- a/src/documents/tests/test_parsers.py +++ b/src/documents/tests/test_parsers.py @@ -147,7 +147,11 @@ class TestComputeThumbnailDpi: pytest.param((612.0, 100000.0), (4, 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, 2), id="huge-page-minimum-one"), + pytest.param( + (1000000.0, 1000000.0), + (1, 1), + id="huge-page-minimum-one-unsupersampled", + ), pytest.param(None, (150, 1), id="unreadable-geometry-fallback"), ], ) @@ -166,7 +170,8 @@ class TestComputeThumbnailDpi: - The thumbnail DPI is computed THEN: - The DPI is rounded up so the render reaches 500x5000, never - exceeds 300, is at least 1, and is supersampled 2x; an unknown + exceeds 300, is at least 1, and is supersampled 2x (except at the + 1 DPI floor); an unknown size gives the plain 150 DPI fallback without supersampling """ mocker.patch( @@ -491,6 +496,7 @@ class TestEncodeThumbnailWebp: pytest.param((1001, 1401), 2, (500, 700), id="2x-odd-rounded"), pytest.param((1000, 1400), 1, (500, 700), id="no-supersample-clamped"), pytest.param((600, 800), 2, (300, 400), id="2x-small-not-enlarged"), + pytest.param((900, 1200), 2, (450, 600), id="2x-below-clamp"), ], ) def test_supersampled_render_downsampled(