Files
OCRmyPDF/src/ocrmypdf/pluginspec.py
T

312 lines
12 KiB
Python

# © 2020 James R. Barlow: github.com/jbarlow83
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from abc import ABC, abstractmethod, abstractstaticmethod
from argparse import ArgumentParser, Namespace
from collections import namedtuple
from pathlib import Path
from typing import TYPE_CHECKING, AbstractSet, List, Optional
import pluggy
from ocrmypdf.helpers import Resolution
if TYPE_CHECKING:
from PIL import Image
from ocrmypdf._jobcontext import PageContext
from ocrmypdf.pdfinfo import PdfInfo
hookspec = pluggy.HookspecMarker('ocrmypdf')
# pylint: disable=unused-argument
@hookspec
def add_options(parser: ArgumentParser) -> None:
"""Allows the plugin to add its own command line and API arguments.
OCRmyPDF converts command line arguments to API arguments, so adding
arguments here will cause new arguments to be processed for API calls
to ``ocrmypdf.ocr``, or when invoked on the command line.
Note:
This hook will be called from the main process, and may modify global state
before child worker processes are forked.
"""
@hookspec
def check_options(options: Namespace) -> None:
"""Called to ask the plugin to check all of the options.
The plugin may check if options that it added are valid.
Warnings or other messages may be passed to the user by creating a logger
object using ``log = logging.getLogger(__name__)`` and logging to this.
The plugin may also modify the *options*. All objects that are in options
must be picklable so they can be marshalled to child worker processes.
Raises:
ocrmypdf.exceptions.ExitCodeException: If options are not acceptable
and the application should terminate gracefully with an informative
message and error code.
Note:
This hook will be called from the main process, and may modify global state
before child worker processes are forked.
"""
@hookspec
def validate(pdfinfo: 'PdfInfo', options: Namespace) -> None:
"""Called to give a plugin an opportunity to review *options* and *pdfinfo*.
*options* contains the "work order" to process a particular file. *pdfinfo*
contains information about the input file obtained after loading and
parsing. The plugin may modify the *options*. For example, you could decide
that a certain type of file should be treated with ``options.force_ocr = True``
based on information in its *pdfinfo*.
Raises:
ocrmypdf.exceptions.ExitCodeException: If options or pdfinfo are not acceptable
and the application should terminate gracefully with an informative
message and error code.
Note:
This hook will be called from the main process, and may modify global state
before child worker processes are forked.
"""
@hookspec(firstresult=True)
def rasterize_pdf_page(
input_file: Path,
output_file: Path,
raster_device: str,
raster_dpi: Resolution,
pageno: int,
page_dpi: Optional[Resolution],
rotation: Optional[int],
filter_vector: bool,
) -> Path:
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units.
The image is sized to match the integer pixels dimensions implied by
raster_dpi even if those numbers are noninteger. The image's DPI will
be overridden with the values in page_dpi.
Args:
input_file: The PDF to rasterize.
output_file: The desired name of the rasterized image.
raster_device: Type of image to produce at output_file
raster_dpi: Resolution at which to rasterize page
pageno: Page number to rasterize (beginning at page 1)
page_dpi: Resolution, overriding output image DPI
rotation: Cardinal angle, clockwise, to rotate page
filter_vector: If True, remove vector graphics objects
Returns:
Path: output_file if successful
Note:
This hook will be called from child processes. Modifying global state
will not affect the main process or other child processes.
Note:
This is a :ref:`firstresult hook<firstresult>`.
"""
@hookspec(firstresult=True)
def filter_ocr_image(page: 'PageContext', 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.
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.
Tesseract OCR only deals with monochrome images, and internally converts
non-monochrome images to OCR.
Note:
This hook will be called from child processes. Modifying global state
will not affect the main process or other child processes.
Note:
This is a :ref:`firstresult hook<firstresult>`.
"""
@hookspec(firstresult=True)
def filter_page_image(page: 'PageContext', image_filename: Path) -> Path:
"""Called to filter the whole page before it is inserted into the PDF.
A whole page image is only produced when preprocessing command line arguments
are issued or when ``--force-ocr`` is issued. If no whole page is image is
produced for a given page, this function will not be called. This is not
the image that will be shown to OCR.
If the function does not want to modify the image, it should return
``image_filename``. The hook may overwrite ``image_filename`` with a new file.
The output image should preserve the same physical unit dimensions, that is
(width * dpi_x, height * dpi_y). That is, if the image is resized, the DPI
must be adjusted by the reciprocal. If this is not preserved, the PDF page
will be resized and the OCR layer misaligned. OCRmyPDF does not nothing
to enforce these constraints; it is up to the plugin to do sensible things.
OCRmyPDF will create the PDF page based on the image format used. If you
convert the image to a JPEG, the output page will be created as a JPEG, etc.
If you change the colorspace, that change will be kept. Note that the
OCRmyPDF image optimization stage, if enabled, may ultimately chose a
different format.
If the return value is a file that does not exist, ``FileNotFoundError``
will occur. The return value should be a path to a file in the same folder
as ``image_filename``.
Implementation detail: If the value returned is falsy, OCRmyPDF will ignore
the return value and assume the input file was unmodified. This is deprecated.
To leave the image unmodified, ``image_filename`` should be returned.
Note:
This hook will be called from child processes. Modifying global state
will not affect the main process or other child processes.
Note:
This is a :ref:`firstresult hook<firstresult>`.
"""
OrientationConfidence = namedtuple('OrientationConfidence', ('angle', 'confidence'))
"""Expresses an OCR engine's confidence in page rotation.
Attributes:
angle (int): The clockwise angle (0, 90, 180, 270) that the page should be
rotated. 0 means no rotation.
confidence (float): How confident the OCR engine is that this the correct
rotation. 0 is not confident, 15 is very confident. Arbitrary units.
"""
class OcrEngine(ABC):
"""A class representing an OCR engine with capabilities similar to Tesseract OCR.
This could be used to create a plugin for another OCR engine instead of
Tesseract OCR.
"""
@abstractstaticmethod
def version() -> str:
"""Returns the version of the OCR engine."""
@abstractstaticmethod
def creator_tag(options: Namespace) -> str:
"""Returns the creator tag to identify this software's role in creating the PDF.
This tag will be inserted in the XMP metadata and DocumentInfo dictionary
as appropriate. Ideally you should include the name of the OCR engine and its
version. The text should not contain line breaks. This is to help developers
like yourself identify the software that produced this file.
OCRmyPDF will always prepend its name to this value.
"""
@abstractmethod
def __str__(self):
"""Returns name of OCR engine and version.
This is used when OCRmyPDF wants to mention the name of the OCR engine
to the user, usually in an error message.
"""
@abstractstaticmethod
def languages(options: Namespace) -> AbstractSet[str]:
"""Returns the set of all languages that are supported by the engine.
Languages are typically given in 3-letter ISO 3166-1 codes, but actually
can be any value understood by the OCR engine."""
@abstractstaticmethod
def get_orientation(input_file: Path, options: Namespace) -> OrientationConfidence:
"""Returns the orientation of the image."""
@abstractstaticmethod
def generate_hocr(
input_file: Path, output_hocr: Path, output_text: Path, options: Namespace
) -> None:
"""Called to produce a hOCR file and sidecar text file."""
@abstractstaticmethod
def generate_pdf(
input_file: Path, output_pdf: Path, output_text: Path, options: Namespace
) -> None:
"""Called to produce a text only PDF.
Args:
input_file: A page image on which to perform OCR.
output_pdf: The expected name of the output PDF, which must be
a single page PDF with no visible content of any kind, sized
to the dimensions implied by the input_file's width, height
and DPI. The image will be grafted onto the input PDF page.
"""
@hookspec(firstresult=True)
def get_ocr_engine() -> OcrEngine:
"""Returns an OcrEngine to use for processing this file.
The OcrEngine may be instantiated multiple times, by both the main process
and child process. As such, it must be obtain store any state in ``options``
or some common location.
Note:
This is a :ref:`firstresult hook<firstresult>`.
"""
@hookspec(firstresult=True)
def generate_pdfa(
pdf_pages: List[Path],
pdfmark: Path,
output_file: Path,
compression: str,
pdf_version: str,
pdfa_part: str,
) -> Path:
"""Generate a PDF/A.
This API strongly assumes a PDF/A generator with Ghostscript's semantics.
OCRmyPDF will modify the metadata and possibly linearize the PDF/A after it
is generated.
Arguments:
pdf_pages: A list of one or more filenames, will be merged into output_file.
pdfmark: A PostScript file intended for Ghostscript with details on
how to perform the PDF/A conversion.
output_file: The name of the desired output file.
compression: One of ``'jpeg'``, ``'lossless'``, ``''``. For ``'jpeg'``,
the PDF/A generator should convert all images to JPEG encoding where
possible. For lossless, all images should be converted to FlateEncode
(lossless PNG). If an empty string, the PDF generator should make its
own decisions about how to encode images.
pdf_version: The minimum PDF version that the output file should be.
At its own discretion, the PDF/A generator may raise the version,
but should not lower it.
pdfa_part: The desired PDF/A compliance level, such as ``'2B'``.
Returns:
Path: If successful, the hook should return ``output_file``.
Note:
This is a :ref:`firstresult hook<firstresult>`.
"""