From 4f604591b415df19665b217f7e57817dae8788f7 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Thu, 13 Apr 2023 23:49:12 -0700 Subject: [PATCH] Convert to image size based --- src/ocrmypdf/builtin_plugins/tesseract_ocr.py | 4 +- src/ocrmypdf/imageops.py | 57 +++++++++---------- tests/test_imageops.py | 12 ++-- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/src/ocrmypdf/builtin_plugins/tesseract_ocr.py b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py index 533b9e8e..7de1bfca 100644 --- a/src/ocrmypdf/builtin_plugins/tesseract_ocr.py +++ b/src/ocrmypdf/builtin_plugins/tesseract_ocr.py @@ -172,10 +172,10 @@ def filter_ocr_image(page: PageContext, image: Image.Image) -> Image.Image: """ options = page.options if options.tesseract_downsample_large_images: - factor = calculate_downsample( + size = calculate_downsample( image, max_size=(32767, 32767), max_bytes=(2**31) - 1 ) - image = downsample_image(image, factor) + image = downsample_image(image, size) return image diff --git a/src/ocrmypdf/imageops.py b/src/ocrmypdf/imageops.py index 75ab7ffa..dcf56439 100644 --- a/src/ocrmypdf/imageops.py +++ b/src/ocrmypdf/imageops.py @@ -4,7 +4,7 @@ """OCR-related image manipulation.""" import logging -from math import ceil, sqrt +from math import ceil, floor, sqrt from PIL import Image @@ -31,12 +31,12 @@ def calculate_downsample( max_size: tuple[int, int] | None = None, max_pixels: int | None = None, max_bytes: int | None = None, -) -> float: +) -> tuple[int, int]: """ - Calculate the scaling factor required to downsample an image to fit within + Calculate the new image size required to downsample an image to fit within the given limits. - If no limit is exceeded, 1.0 is returned. + If no limit is exceeded, the input image's size is returned. Args: image: The image to downsample. @@ -46,41 +46,40 @@ def calculate_downsample( max_bytes: The maximum number of bytes in the image. RGB is counted as 4 bytes; all other modes are counted as 1 byte. """ - scaling_factor = 1.0 + size = image.size if max_size is not None: major_axis = max(image.size) - if major_axis > max(max_size): + size_factor = max(max_size) / major_axis + if size_factor < 1.0: log.debug("Resizing image to fit Tesseract image size limit") - scaling_factor = max(max_size) / major_axis + size = floor(size[0] * size_factor), floor(size[1] * size_factor) if max_pixels is not None: - if image.size[0] * image.size[1] * scaling_factor * scaling_factor > max_pixels: + if size[0] * size[1] > max_pixels: log.debug("Resizing image to fit image pixel limit") - scaling_factor *= sqrt( - max_pixels / (image.size[0] * image.size[1] * scaling_factor) - ) + pixels_factor = sqrt(max_pixels / (image.size[0] * image.size[1])) + size = floor(size[0] * pixels_factor), floor(size[1] * pixels_factor) if max_bytes is not None: bpp = bytes_per_pixel(image.mode) # stride = bytes per line - stride = ceil(image.size[0] * scaling_factor) * bpp - height = ceil(image.size[1] * scaling_factor) - size = stride * height - if size > max_bytes: + stride = size[0] * bpp + height = size[1] + if stride * height > max_bytes: log.debug("Resizing image to fit image byte size limit") - scaling_factor *= sqrt((max_bytes - 1) / size) - scaled_bytes_per_line = ceil(image.size[0] * scaling_factor) * bpp - height = ceil(image.size[1] * scaling_factor) - size = scaled_bytes_per_line * height - assert size <= max_bytes, f"{size} > {max_bytes}" + bytes_factor = sqrt((max_bytes) / (stride * height)) + scaled_stride = floor(stride * bytes_factor) + scaled_height = floor(height * bytes_factor) + size = ceil(scaled_stride / bpp), scaled_height + assert (size[0] * bpp * size[1]) <= max_bytes - return scaling_factor + return size def downsample_image( image: Image.Image, - scaling_factor: float, + new_size: tuple[int, int], *, resample_mode: Image.Resampling = Image.Resampling.BICUBIC, reducing_gap: int = 3, @@ -99,23 +98,19 @@ def downsample_image( reducing_gap: The reducing gap to use when downsampling (for larger reductions). """ - if scaling_factor == 1.0: + if new_size == image.size: return image - if scaling_factor > 1.0 or scaling_factor <= 0: - raise ValueError("scaling_factor must be <= 1.0 and > 0") + original_size = image.size original_dpi = image.info['dpi'] image = image.resize( - ( - ceil(image.size[0] * scaling_factor), - ceil(image.size[1] * scaling_factor), - ), + new_size, resample=resample_mode, reducing_gap=reducing_gap, ) image.info['dpi'] = ( - original_dpi[0] * scaling_factor, - original_dpi[1] * scaling_factor, + round(original_dpi[0] * new_size[0] / original_size[0]), + round(original_dpi[1] * new_size[1] / original_size[1]), ) log.debug(f"Rescaled image to {image.size} pixels and {image.info['dpi']} dpi") return image diff --git a/tests/test_imageops.py b/tests/test_imageops.py index 7f75a8fa..bb727755 100644 --- a/tests/test_imageops.py +++ b/tests/test_imageops.py @@ -16,13 +16,15 @@ def test_bytes_per_pixel(): def test_calculate_downsample(): im = Image.new('RGB', (100, 100)) - assert calculate_downsample(im, max_size=(50, 50)) == 0.5 - assert calculate_downsample(im, max_pixels=2500) == 0.5 - assert calculate_downsample(im, max_bytes=10000) == 0.25 + assert calculate_downsample(im, max_size=(50, 50)) == (50, 50) + assert calculate_downsample(im, max_pixels=2500) == (50, 50) + assert calculate_downsample(im, max_bytes=10000) == (50, 50) + assert calculate_downsample(im, max_bytes=100000) == (100, 100) def test_downsample_image(): im = Image.new('RGB', (100, 100)) im.info['dpi'] = (300, 300) - assert downsample_image(im, 0.5).size == (50, 50) - assert im.info['dpi'] == (150, 150) + ds = downsample_image(im, (50, 50)) + assert ds.size == (50, 50) + assert ds.info['dpi'] == (150, 150)