diff --git a/src/ocrmypdf/_exec/ghostscript.py b/src/ocrmypdf/_exec/ghostscript.py index b48f2080..ec9017ca 100644 --- a/src/ocrmypdf/_exec/ghostscript.py +++ b/src/ocrmypdf/_exec/ghostscript.py @@ -127,6 +127,19 @@ def rasterize_pdf( if not page_dpi: page_dpi = raster_dpi + # Ghostscript may fail with very low DPI values (below 10). If the requested + # DPI is too low, use a minimum of 10 DPI and resize the output afterward. + MIN_RASTER_DPI = 10 + needs_low_dpi_resize = ( + raster_dpi.x < MIN_RASTER_DPI or raster_dpi.y < MIN_RASTER_DPI + ) + if needs_low_dpi_resize: + effective_dpi = Resolution( + max(raster_dpi.x, MIN_RASTER_DPI), max(raster_dpi.y, MIN_RASTER_DPI) + ) + else: + effective_dpi = raster_dpi + args_gs = ( [ GS, @@ -137,7 +150,7 @@ def rasterize_pdf( f'-sDEVICE={raster_device}', f'-dFirstPage={pageno}', f'-dLastPage={pageno}', - f'-r{raster_dpi.x:f}x{raster_dpi.y:f}', + f'-r{effective_dpi.x:f}x{effective_dpi.y:f}', ] + (['-dUseCropBox'] if use_cropbox else []) + (['-dFILTERVECTOR'] if filter_vector else []) @@ -173,6 +186,16 @@ def rasterize_pdf( try: with Image.open(output_file) as im: + if needs_low_dpi_resize: + # Resize to the dimensions that would have resulted from the + # original low DPI request + scale_x = raster_dpi.x / effective_dpi.x + scale_y = raster_dpi.y / effective_dpi.y + new_size = ( + max(1, int(round(im.width * scale_x))), + max(1, int(round(im.height * scale_y))), + ) + im = im.resize(new_size, Image.Resampling.LANCZOS) if rotation is not None: log.debug("Rotating output by %i", rotation) # rotation is a clockwise angle and Image.ROTATE_* is diff --git a/tests/test_ghostscript.py b/tests/test_ghostscript.py index 7d971216..f2fa34eb 100644 --- a/tests/test_ghostscript.py +++ b/tests/test_ghostscript.py @@ -81,6 +81,62 @@ def test_rasterize_rotated(francais, outdir, caplog): assert im.info['dpi'] == forced_dpi.flip_axis() +def test_rasterize_low_dpi(francais, outdir): + """Test that very low DPI values (below 10) produce correctly sized output. + + Ghostscript may fail with DPI values below 10. The workaround renders at + a minimum of 10 DPI and resizes the output to match the expected dimensions. + """ + path, pdf = francais + page_size_pts = (pdf.pages[0].mediabox[2], pdf.pages[0].mediabox[3]) + assert pdf.pages[0].mediabox[0] == pdf.pages[0].mediabox[1] == 0 + page_size = (float(page_size_pts[0]) / 72, float(page_size_pts[1]) / 72) + + # Request a very small output (DPI below 10 on both axes) + target_size = (5, 3) + forced_dpi = Resolution(72.0, 72.0) + + rasterize_pdf( + path, + outdir / 'out_low_dpi.png', + raster_device=GhostscriptRasterDevice.PNGMONO, + raster_dpi=Resolution( + target_size[0] / page_size[0], target_size[1] / page_size[1] + ), + page_dpi=forced_dpi, + ) + + with Image.open(outdir / 'out_low_dpi.png') as im: + assert im.size == target_size + assert im.info['dpi'] == forced_dpi + + +def test_rasterize_low_dpi_one_axis(francais, outdir): + """Test low DPI on only one axis produces correctly sized output.""" + path, pdf = francais + page_size_pts = (pdf.pages[0].mediabox[2], pdf.pages[0].mediabox[3]) + assert pdf.pages[0].mediabox[0] == pdf.pages[0].mediabox[1] == 0 + page_size = (float(page_size_pts[0]) / 72, float(page_size_pts[1]) / 72) + + # Request low DPI on X axis only (below 10), normal on Y axis + target_size = (5, 50) + forced_dpi = Resolution(72.0, 72.0) + + rasterize_pdf( + path, + outdir / 'out_low_dpi_x.png', + raster_device=GhostscriptRasterDevice.PNGMONO, + raster_dpi=Resolution( + target_size[0] / page_size[0], target_size[1] / page_size[1] + ), + page_dpi=forced_dpi, + ) + + with Image.open(outdir / 'out_low_dpi_x.png') as im: + assert im.size == target_size + assert im.info['dpi'] == forced_dpi + + def test_gs_render_failure(resources, outpdf, caplog): exitcode = run_ocrmypdf_api( resources / 'blank.pdf',