As you were

This commit is contained in:
James R. Barlow
2023-04-13 23:27:58 -07:00
parent b432770cfc
commit 01dc8e23ff
4 changed files with 200 additions and 10 deletions
@@ -8,10 +8,14 @@ from __future__ import annotations
import logging
import os
from PIL import Image
from ocrmypdf import hookimpl
from ocrmypdf._exec import tesseract
from ocrmypdf._jobcontext import PageContext
from ocrmypdf.cli import numeric, str_to_int
from ocrmypdf.helpers import clamp
from ocrmypdf.imageops import calculate_downsample, downsample_image
from ocrmypdf.pluginspec import OcrEngine
from ocrmypdf.subprocess import check_external_program
@@ -85,6 +89,19 @@ def add_options(parser):
"because these operations are not as expensive as OCR."
),
)
tess.add_argument(
'--tesseract-downsample-large-images',
action='store_true',
help=(
"Downsample large images before OCR. Tesseract has an upper limit on the "
"size images it will support. If this argument is given, OCRmyPDF will "
"downsample large images to fit Tesseract. This may reduce OCR quality, "
"on large images the most desirable text is usually larger. If this "
"parameter is not supplied, Tesseract will error out and produce no OCR "
"on the page in question. This argument should be used with a high value "
"of --tesseract-timeout to ensure Tesseract has enough to time."
),
)
tess.add_argument(
'--user-words',
metavar='FILE',
@@ -145,6 +162,23 @@ def validate(pdfinfo, options):
log.debug("Using Tesseract OpenMP thread limit %d", tess_threads)
@hookimpl
def filter_ocr_image(page: PageContext, image: Image.Image) -> Image.Image:
"""Filter the image before OCR.
Tesseract cannot handle images with more than 32767 pixels in either axis,
or more than 2**31 bytes. This function resizes the image to fit within
those limits.
"""
options = page.options
if options.tesseract_downsample_large_images:
factor = calculate_downsample(
image, max_size=(32767, 32767), max_bytes=(2**31) - 1
)
image = downsample_image(image, factor)
return image
class TesseractOcrEngine(OcrEngine):
"""Implements OCR with Tesseract."""
+121
View File
@@ -0,0 +1,121 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""OCR-related image manipulation."""
import logging
from math import ceil, sqrt
from PIL import Image
log = logging.getLogger(__name__)
def bytes_per_pixel(mode: str) -> int:
"""
Return the number of padded bytes per pixel for a given PIL image mode.
In RGB mode we assume 4 bytes per pixel, which is the case for most
consumers.
"""
if mode in ('1', 'L', 'P'):
return 1
if mode in ('LA', 'PA', 'La') or mode.startswith('I;16'):
return 2
return 4
def calculate_downsample(
image: Image.Image,
*,
max_size: tuple[int, int] | None = None,
max_pixels: int | None = None,
max_bytes: int | None = None,
) -> float:
"""
Calculate the scaling factor required to downsample an image to fit within
the given limits.
If no limit is exceeded, 1.0 is returned.
Args:
image: The image to downsample.
max_size: The maximum width and height of the image.
max_pixels: The maximum number of pixels in the image. Some image consumers
limit the total number of pixels as some value other than width*height.
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
if max_size is not None:
major_axis = max(image.size)
if major_axis > max(max_size):
log.debug("Resizing image to fit Tesseract image size limit")
scaling_factor = max(max_size) / major_axis
if max_pixels is not None:
if image.size[0] * image.size[1] * scaling_factor * scaling_factor > max_pixels:
log.debug("Resizing image to fit image pixel limit")
scaling_factor *= sqrt(
max_pixels / (image.size[0] * image.size[1] * scaling_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:
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}"
return scaling_factor
def downsample_image(
image: Image.Image,
scaling_factor: float,
*,
resample_mode: Image.Resampling = Image.Resampling.BICUBIC,
reducing_gap: int = 3,
) -> Image.Image:
"""
Downsample an image to fit within the given limits.
The DPI is adjusted to match the new size, which is how we can ensure the
OCR is positioned correctly.
Args:
image: The image to downsample
scaling_factor: The scaling factor to apply to the image, calculated using
calculate_downsample().
resample_mode: The resampling mode to use when downsampling.
reducing_gap: The reducing gap to use when downsampling (for larger
reductions).
"""
if scaling_factor == 1.0:
return image
if scaling_factor > 1.0 or scaling_factor <= 0:
raise ValueError("scaling_factor must be <= 1.0 and > 0")
original_dpi = image.info['dpi']
image = image.resize(
(
ceil(image.size[0] * scaling_factor),
ceil(image.size[1] * scaling_factor),
),
resample=resample_mode,
reducing_gap=reducing_gap,
)
image.info['dpi'] = (
original_dpi[0] * scaling_factor,
original_dpi[1] * scaling_factor,
)
log.debug(f"Rescaled image to {image.size} pixels and {image.info['dpi']} dpi")
return image
+17 -10
View File
@@ -228,19 +228,26 @@ def filter_ocr_image(page: PageContext, image: Image.Image) -> Image.Image:
"""Called to filter the image before it is sent to OCR.
This is the image that OCR sees, not what the user sees when they view the
PDF. If ``redo_ocr`` is enabled, portions of the image will be masked so
they are not shown to OCR. The main use of this hook is expected to be hiding
content from OCR.
PDF. In certain modes such as ``--redo-ocr``, portions of the image may be
masked out to hide them from OCR.
The main uses of this hook are expected to be hiding content from OCR,
conditioning images to OCR better with filters, and adjusting images to
match any constraints imposed by the OCR engine.
The input image may be color, grayscale, or monochrome, and the
output image may differ. The pixel width and height of the
output image must be identical to the input image, or misalignment between
the OCR text layer and visual position of the text will occur. Likewise,
the output must be a faithful representation of the input, or alignment
errors may occurs.
output image may differ. For example, if you know that a custom OCR engine
does not care about the color of the text, you could convert the image to
it to grayscale or monochrome.
Tesseract OCR only deals with monochrome images, and internally converts
non-monochrome images to OCR.
Generally speaking, the output image should be a faithful representation of
of the input image. You *may* change the pixel width and height of the
the input image, but you must not change the aspect ratio, and you must
calculate the DPI of the output image based on the new pixel width and
height or the OCR text layer will be misaligned with the visual position.
The built-in Tesseract OCR engine uses this hook itself to downsample
very large images to fit its constraints.
Note:
This hook will be called from child processes. Modifying global state