diff --git a/docs/api.rst b/docs/api.rst index ce6583fc..735354f6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -106,7 +106,3 @@ Reference :undoc-members: .. autofunction:: ocrmypdf.configure_logging - -.. automodule:: ocrmypdf.exceptions - :members: - :undoc-members: diff --git a/docs/apiref.rst b/docs/apiref.rst new file mode 100644 index 00000000..caeb277a --- /dev/null +++ b/docs/apiref.rst @@ -0,0 +1,43 @@ +============= +API Reference +============= + +This page summarizes the rest of the public API. Generally speaking this +should mainly of interest to plugin developers. + +ocrmypdf.exceptions +=================== + +.. automodule:: ocrmypdf.exceptions + :members: + :undoc-members: + +ocrmypdf.helpers +================ + +.. automodule:: ocrmypdf.helpers + :members: + +ocrmypdf.hocrtransform +====================== + +.. automodule:: ocrmypdf.hocrtransform + :members: + +ocrmypdf.pdfa +============= + +.. automodule:: ocrmypdf.pdfa + :members: + +ocrmypdf.quality +================ + +.. automodule:: ocrmypdf.quality + :members: + +ocrmypdf.subprocess +=================== + +.. automodule:: ocrmypdf.subprocess + :members: diff --git a/docs/conf.py b/docs/conf.py index b78cc050..4a6dc0ac 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # ocrmypdf documentation build configuration file, created by # sphinx-quickstart on Sun Sep 4 14:29:43 2016. @@ -21,6 +20,8 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) +"""isort:skip_file""" + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -32,6 +33,8 @@ # ones. extensions = ['sphinx.ext.napoleon'] +napoleon_use_rtype = False + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -51,7 +54,7 @@ master_doc = 'index' # General information about the project. project = 'ocrmypdf' copyright = ( - '2019, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.' + '2020, James R. Barlow. Licensed under Creative Commons Attribution-ShareAlike 4.0.' ) author = 'James R. Barlow' @@ -90,6 +93,7 @@ from pkg_resources import get_distribution, DistributionNotFound release = get_distribution('ocrmypdf').version version = '.'.join(release.split('.')[:2]) + # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # @@ -174,7 +178,7 @@ html_theme_options = {'display_version': False} # The name of an image file (relative to this directory) to place at the top # of the sidebar. # -# html_logo = None +# html_logo = "images/logo.svg" # looks bad # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 diff --git a/docs/index.rst b/docs/index.rst index 2591b1b9..217f5411 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,6 +36,7 @@ image processing and OCR to existing PDFs. api plugins + apiref contributing Indices and tables diff --git a/docs/plugins.rst b/docs/plugins.rst index 9dd145c2..d55a01f8 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -76,5 +76,43 @@ A plugin may provide the following hooks. Hooks should be decorated with The following is a complete list of hooks that may be installed and when they are called. -.. automodule:: ocrmypdf.pluginspec +Custom command line arguments +----------------------------- + +.. autofunction:: ocrmypdf.pluginspec.add_options + +.. autofunction:: ocrmypdf.pluginspec.check_options + +Applying special behavior before processing +------------------------------------------- + +.. autofunction:: ocrmypdf.pluginspec.validate + +PDF page to image +----------------- + +.. autofunction:: ocrmypdf.pluginspec.rasterize_pdf_page + +Modifying intermediate images +----------------------------- + +.. autofunction:: ocrmypdf.pluginspec.filter_ocr_image + +.. autofunction:: ocrmypdf.pluginspec.filter_page_image + +OCR engine +---------- + +.. autofunction:: ocrmypdf.pluginspec.get_ocr_engine + +.. autoclass:: ocrmypdf.pluginspec.OcrEngine :members: + + .. automethod:: __str__ + +.. autoclass:: ocrmypdf.pluginspec.OrientationConfidence + +PDF/A production +---------------- + +.. autofunction:: ocrmypdf.pluginspec.generate_pdfa diff --git a/src/ocrmypdf/helpers.py b/src/ocrmypdf/helpers.py index c457ab8b..d2523496 100644 --- a/src/ocrmypdf/helpers.py +++ b/src/ocrmypdf/helpers.py @@ -35,6 +35,8 @@ log = logging.getLogger(__name__) class Resolution(namedtuple('Resolution', ('x', 'y'))): + """The number of pixels per inch in each 2D direction.""" + __slots__ = () def round(self, ndigits: int): @@ -128,6 +130,7 @@ def page_number(input_file: os.PathLike) -> int: def available_cpu_count() -> int: + """Returns number of CPUs in the system.""" try: return multiprocessing.cpu_count() except NotImplementedError: @@ -174,6 +177,10 @@ def is_file_writable(test_file: os.PathLike) -> bool: def check_pdf(input_file: Path) -> bool: + """Check if a PDF complies with the PDF specification. + + Checks for proper formatting and proper linearization. + """ pdf = None try: pdf = pikepdf.open(input_file) @@ -211,6 +218,7 @@ T = TypeVar('T') def clamp(n: T, smallest: T, largest: T) -> T: + """Clamps the value of n to between smallest and largest.""" return max(smallest, min(n, largest)) diff --git a/src/ocrmypdf/hocrtransform.py b/src/ocrmypdf/hocrtransform.py index 606a2850..7e2ef142 100755 --- a/src/ocrmypdf/hocrtransform.py +++ b/src/ocrmypdf/hocrtransform.py @@ -35,7 +35,7 @@ from collections import namedtuple from itertools import chain from math import atan, cos, sin from pathlib import Path -from typing import Union +from typing import Optional, Tuple, Union from xml.etree import ElementTree from reportlab.lib.colors import black, cyan, magenta, red @@ -133,7 +133,7 @@ class HocrTransform: return out @classmethod - def baseline(cls, element): + def baseline(cls, element) -> Tuple[float, float]: """ Returns a tuple containing the baseline slope and intercept. """ @@ -143,7 +143,7 @@ class HocrTransform: return float(matches.group(1)), int(matches.group(2)) return (0.0, 0.0) - def pt_from_pixel(self, pxl): + def pt_from_pixel(self, pxl) -> Rect: """ Returns the quantity in PDF units (pt) given quantity in pixels """ @@ -156,11 +156,11 @@ class HocrTransform: return xpath @classmethod - def replace_unsupported_chars(cls, s: str): + def replace_unsupported_chars(cls, s: str) -> str: """ Given an input string, returns the corresponding string that: - - is available in the helvetica facetype - - does not contain any ligature (to allow easy search in the PDF file) + * is available in the Helvetica facetype + * does not contain any ligature (to allow easy search in the PDF file) """ return s.translate(cls.ligatures) @@ -172,12 +172,12 @@ class HocrTransform: def to_pdf( self, out_filename: Path, - image_filename: Path = None, + image_filename: Optional[Path] = None, show_bounding_boxes: bool = False, fontname: str = "Helvetica", invisible_text: bool = False, interword_spaces: bool = False, - ): + ) -> None: """ Creates a PDF file with an image superimposed on top of the text. Text is positioned according to the bounding box of the lines in @@ -185,6 +185,20 @@ class HocrTransform: The image need not be identical to the image used to create the hOCR file. It can have a lower resolution, different color mode, etc. + + Arguments: + out_filename: Path of PDF to write. + image_filename: Image to use for this file. If omitted, the OCR text + is shown. + show_bounding_boxes: Show bounding boxes around various text regions, + for debugging. + fontname: Name of font to use. + invisible_text: If True, text is rendered invisible so that is + selectable but never drawn. If False, text is visible and may + be seen if the image is skipped or deleted in Acrobat. + interword_spaces: If True, insert spaces between words rather than + drawing each word without spaces. Generally this improves text + extraction. """ # create the PDF file # page size in points (1/72 in.) diff --git a/src/ocrmypdf/pdfa.py b/src/ocrmypdf/pdfa.py index 3aeb7612..7fd42194 100644 --- a/src/ocrmypdf/pdfa.py +++ b/src/ocrmypdf/pdfa.py @@ -16,19 +16,7 @@ # along with OCRmyPDF. If not, see . """ -Generate a PDFMARK file for Ghostscript >= 9.14, for PDF/A conversion - -pdfmark is an extension to the Postscript language that describes some PDF -features like bookmarks and annotations. It was originally specified Adobe -Distiller, for Postscript to PDF conversion: -https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf - -Ghostscript uses pdfmark for PDF to PDF/A conversion as well. To use Ghostscript -to create a PDF/A, we need to create a pdfmark file with the necessary metadata. - -This takes care of the many version-specific bugs and pecularities in -Ghostscript's handling of pdfmark. - +Utilities for PDF/A production and confirmation with Ghostspcript. """ import base64 @@ -68,20 +56,28 @@ def """ -def generate_pdfa_ps(target_filename, icc='sRGB'): - """Create a Postscript pdfmark file for Ghostscript PDF/A conversion +def generate_pdfa_ps(target_filename: Path, icc: str = 'sRGB'): + """Create a Postscript PDFMARK file for Ghostscript PDF/A conversion - A pdfmark file is a small Postscript program that provides some information - Ghostscript needs to perform PDF/A conversion. The only information we put - in specifies that we want the file to be a PDF/A, and we want to Ghostscript - to convert objects to the sRGB colorspace if it runs into any object that - it decides must be converted. + pdfmark is an extension to the Postscript language that describes some PDF + features like bookmarks and annotations. It was originally specified Adobe + Distiller, for Postscript to PDF conversion. - See the Adobe pdfmark Reference for details: - https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf + Ghostscript uses pdfmark for PDF to PDF/A conversion as well. To use Ghostscript + to create a PDF/A, we need to create a pdfmark file with the necessary metadata. - :param target_filename: filename to save - :param icc: ICC identifier such as 'sRGB' + This function takes care of the many version-specific bugs and pecularities in + Ghostscript's handling of pdfmark. + + The only information we put in specifies that we want the file to be a + PDF/A, and we want to Ghostscript to convert objects to the sRGB colorspace + if it runs into any object that it decides must be converted. + + Arguments: + target_filename: filename to save + icc: ICC identifier such as 'sRGB' + References: + Adobe PDFMARK Reference: https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf """ if icc == 'sRGB': icc_profile = SRGB_ICC_PROFILE @@ -102,7 +98,7 @@ def generate_pdfa_ps(target_filename, icc='sRGB'): return target_filename -def file_claims_pdfa(filename): +def file_claims_pdfa(filename: Path): """Determines if the file claims to be PDF/A compliant This only checks if the XMP metadata contains a PDF/A marker. It does not diff --git a/src/ocrmypdf/pluginspec.py b/src/ocrmypdf/pluginspec.py index a96178f9..9371aede 100644 --- a/src/ocrmypdf/pluginspec.py +++ b/src/ocrmypdf/pluginspec.py @@ -22,13 +22,13 @@ from pathlib import Path from typing import TYPE_CHECKING, AbstractSet, List, Optional import pluggy -from PIL import Image from ocrmypdf.helpers import Resolution if TYPE_CHECKING: from ocrmypdf._jobcontext import PageContext from ocrmypdf.pdfinfo import PdfInfo + from PIL import Image hookspec = pluggy.HookspecMarker('ocrmypdf') @@ -118,7 +118,7 @@ def rasterize_pdf_page( rotation: Cardinal angle, clockwise, to rotate page filter_vector: If True, remove vector graphics objects Returns: - output_file + 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. @@ -126,7 +126,7 @@ def rasterize_pdf_page( @hookspec(firstresult=True) -def filter_ocr_image(page: 'PageContext', image: Image) -> Image: +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 @@ -159,20 +159,46 @@ def filter_page_image(page: 'PageContext', image_filename: Path) -> Path: 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.""" + """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.""" + """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]: @@ -248,5 +274,5 @@ def generate_pdfa( pdfa_part: The desired PDF/A compliance level, such as ``'2B'``. Returns: - output_file: If successful, the hook should return ``output_file``. + Path: If successful, the hook should return ``output_file``. """ diff --git a/src/ocrmypdf/subprocess.py b/src/ocrmypdf/subprocess.py index 5cc540fa..9e4db351 100644 --- a/src/ocrmypdf/subprocess.py +++ b/src/ocrmypdf/subprocess.py @@ -36,18 +36,12 @@ log = logging.getLogger(__name__) def run(args, *, env=None, **kwargs): - """Wrapper around subprocess.run() - - The main purpose of this wrapper is to log subprocess output. - - Secondly we have to account for behavioral differences in Windows in particular. - Creating symbolic links in Windows requires administrator privileges and - may not work if for some reason we're using a FAT file system or the temporary - folder is on a different drive from the working folder. The test suite - works around this by creating shim Python scripts that perform the same function - as a symbolic link, but those shims require support on this side, to ensure - we call them with Python. + """Wrapper around :py:func:`subprocess.run` + The main purpose of this wrapper is to log subprocess output in an orderly + fashion that indentifies the responsible subprocess. An additional + task is that this function goes to greater lengths to find possible Windows + locations of our dependencies when they are not on the system PATH. """ if not env: env = os.environ @@ -106,8 +100,18 @@ def _fix_windows_args(program, args, env): @lru_cache(maxsize=None) -def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env=None): - """Get the version of the specified program""" +def get_version( + program: str, *, version_arg: str = '--version', regex=r'(\d+(\.\d+)*)', env=None +): + """Get the version of the specified program + + Arguments: + program: The program to version check. + version_arg: The argument needed to ask for its version, e.g. ``--version``. + regex: A regular expression to parse the program's output and obtain the + version. + env: Custom ``os.environ`` in which to run program. + """ args_prog = [program, version_arg] try: proc = run(