ruff: more fixes, mainly missing docstrings

This commit is contained in:
James R. Barlow
2023-04-14 02:16:38 -07:00
parent 4924b11b6b
commit 33b70be7d5
10 changed files with 149 additions and 16 deletions
+9 -1
View File
@@ -2,9 +2,17 @@
# SPDX-FileCopyrightText: 2016 findingorder <https://github.com/findingorder>
# SPDX-License-Identifier: MIT
"""Example of using ocrmypdf as a library in a script.
This script will recursively search a directory for PDF files and run OCR on
them. It will log the results. It runs OCR on every file, even if it already
has text. OCRmyPDF will detect files that already have text.
You should edit this script to meet your needs.
"""
from __future__ import annotations
# This script must be edited to meet your needs.
import logging
import sys
from pathlib import Path
+2 -1
View File
@@ -205,4 +205,5 @@ convention = "google"
[tool.ruff.per-file-ignores]
"docs/conf.py" = ["D100", "D101", "D105"]
"tests/*.py" = ["D100", "D101", "D102", "D103", "D105"]
"misc/*.py" = ["D103", "D101", "D102"]
"misc/*.py" = ["D103", "D101", "D102"]
"src/ocrmypdf/builtin_plugins/*.py" = ["D103", "D102", "D105"]
+2 -1
View File
@@ -203,7 +203,7 @@ def create_options(
return options
def ocr( # pylint: disable=unused-argument
def ocr( # noqa: ruff: disable=D417
input_file: PathOrIO,
output_file: PathOrIO,
*,
@@ -242,6 +242,7 @@ def ocr( # pylint: disable=unused-argument
tesseract_thresholding: int = None,
pdf_renderer=None,
tesseract_timeout: float = None,
tesseract_non_ocr_timeout: float = None,
rotate_pages_threshold: float = None,
pdfa_image_compression=None,
user_words: os.PathLike = None,
+2 -4
View File
@@ -1,8 +1,6 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
"""Plugins in this package are automatically loaded by ocrmypdf."""
# This file exists only mark builtin_plugins as a package.
# The plugin manager will not load it, so anything defined here may not be
# processed as a module.
from __future__ import annotations
+11
View File
@@ -57,13 +57,21 @@ class ArgumentParser(argparse.ArgumentParser):
"""
def __init__(self, *args, **kwargs):
"""Initialize the parser."""
super().__init__(*args, **kwargs)
self._api_mode = False
def enable_api_mode(self):
"""Enable API mode.
When set, the parser will not call sys.exit() on error. OCRmyPDF was originally
a command line program, but now it has an API. The API works by synthesizing
command line arguments.
"""
self._api_mode = True
def error(self, message):
"""Override the default argparse error behavior."""
if not self._api_mode:
super().error(message)
return
@@ -74,11 +82,13 @@ class LanguageSetAction(argparse.Action):
"""Manages a list of languages."""
def __init__(self, option_strings, dest, default=None, **kwargs):
"""Initialize the action."""
if default is None:
default = set()
super().__init__(option_strings, dest, default=default, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
"""Add a language to the set."""
dest = getattr(namespace, self.dest)
if '+' in values:
dest.update(lang for lang in values.split('+'))
@@ -87,6 +97,7 @@ class LanguageSetAction(argparse.Action):
def get_parser():
"""Get the main CLI parser."""
parser = ArgumentParser(
prog=_PROGRAM_NAME,
allow_abbrev=True,
+9
View File
@@ -486,6 +486,12 @@ def _deflate_jpeg(args: tuple[Pdf, threading.Lock, Xref, int]) -> tuple[Xref, by
def deflate_jpegs(pike: Pdf, root: Path, options, executor: Executor) -> None:
"""Apply FlateDecode to JPEGs.
This is a lossless compression method that is supported by all PDF viewers,
and generally results in a smaller file size compared to straight DCTDecode
images.
"""
jpegs = []
for _pageno, xref_ext in extract_images(pike, root, options, _find_deflatable_jpeg):
xref = xref_ext.xref
@@ -573,6 +579,7 @@ def transcode_pngs(
options,
executor,
) -> None:
"""Apply lossy transcoding to PNGs."""
modified: MutableSet[Xref] = set()
if options.optimize >= 2:
png_quality = (
@@ -619,6 +626,7 @@ def optimize(
save_settings,
executor: Executor = DEFAULT_EXECUTOR,
) -> Path:
"""Optimize images in a PDF file."""
options = context.options
if options.optimize == 0:
safe_symlink(input_file, output_file)
@@ -675,6 +683,7 @@ def optimize(
def main(infile, outfile, level, jobs=1):
"""Entry point for direct optimization of a file."""
from shutil import copy # pylint: disable=import-outside-toplevel
from tempfile import TemporaryDirectory # pylint: disable=import-outside-toplevel
+69 -5
View File
@@ -322,7 +322,12 @@ def _get_dpi(ctm_shorthand, image_size) -> Resolution:
class ImageInfo:
"""Information about an image found in a PDF."""
"""Information about an image found in a PDF.
This gathers information from pikepdf and pdfminer.six, and is pickle-able
so that it can be passed to a worker process, unlike objects from those
libraries.
"""
DPI_PREC = Decimal('1.000')
@@ -337,6 +342,7 @@ class ImageInfo:
inline: PdfInlineImage | None = None,
shorthand=None,
):
"""Initialize an ImageInfo."""
self._name = str(name)
self._shorthand = shorthand
@@ -403,45 +409,62 @@ class ImageInfo:
@property
def name(self):
"""Name of the image as it appears in the PDF."""
return self._name
@property
def type_(self):
"""Type of image, either 'image' or 'stencil'."""
return self._type
@property
def width(self):
"""Width of the image in pixels."""
return self._width
@property
def height(self):
"""Height of the image in pixels."""
return self._height
@property
def bpc(self):
"""Bits per component."""
return self._bpc
@property
def color(self):
"""Colorspace of the image."""
return self._color if self._color is not None else '?'
@property
def comp(self):
"""Number of components/channels in the image."""
return self._comp if self._comp is not None else '?'
@property
def enc(self):
"""Encoding of the image."""
return self._enc if self._enc is not None else 'image'
@property
def renderable(self):
def renderable(self) -> bool:
"""Whether the image is renderable.
Some PDFs in the wild have invalid images that are not renderable.
"""
return self.dpi.is_finite and self.width >= 0 and self.height >= 0
@property
def dpi(self):
"""Dots per inch of the image.
Calculated based on where and how the image is drawn in the PDF.
"""
return _get_dpi(self._shorthand, (self._width, self._height))
def __repr__(self):
"""Return a string representation of the image."""
return (
f"<ImageInfo '{self.name}' {self.type_} {self.width}x{self.height} "
f"{self.color} {self.comp} {self.bpc} {self.enc} {self.dpi}>"
@@ -449,7 +472,7 @@ class ImageInfo:
def _find_inline_images(contentsinfo: ContentsInfo) -> Iterator[ImageInfo]:
"Find inline images in the contentstream."
"""Find inline images in the contentstream."""
for n, inline in enumerate(contentsinfo.inline_images):
yield ImageInfo(
name=f'inline-{n:02d}', shorthand=inline.shorthand, inline=inline.iimage
@@ -601,7 +624,9 @@ def _page_has_text(text_blocks: Iterable[FloatRect], page_width, page_height) ->
)
def rects_intersect(a: FloatRect, b: FloatRect) -> bool:
"""Where (a,b) are 4-tuple rects (left-0, top-1, right-2, bottom-3)
"""Check if two 4-tuple rects intersect.
Where (a,b) are 4-tuple rects (left-0, top-1, right-2, bottom-3)
https://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other
Formula assumes all boxes are in first quadrant.
"""
@@ -732,6 +757,7 @@ class PageInfo:
check_pages: Container[int],
detailed_analysis: bool = False,
):
"""Initialize a PageInfo object."""
self._pageno = pageno
self._infile = infile
self._detailed_analysis = detailed_analysis
@@ -808,40 +834,56 @@ class PageInfo:
@property
def pageno(self) -> int:
"""Return page number (0-based)."""
return self._pageno
@property
def has_text(self) -> bool:
"""Return True if page has text, False if not or unknown."""
return bool(self._has_text)
@property
def has_corrupt_text(self) -> bool:
"""Return True if page has corrupt text, False if not or unknown."""
if not self._detailed_analysis:
raise NotImplementedError('Did not do detailed analysis')
return any(tbox.is_corrupt for tbox in self._textboxes)
@property
def has_vector(self) -> bool:
"""Return True if page has vector graphics, False if not or unknown.
Vector graphics are sometimes used to draw fonts, so it may not be
obvious on visual inspection whether a page has text or not.
"""
return bool(self._has_vector)
@property
def width_inches(self) -> Decimal:
"""Return width of page in inches."""
return self._width_inches
@property
def height_inches(self) -> Decimal:
"""Return height of page in inches."""
return self._height_inches
@property
def width_pixels(self) -> int:
"""Return width of page in pixels."""
return int(round(float(self.width_inches) * self.dpi.x))
@property
def height_pixels(self) -> int:
"""Return height of page in pixels."""
return int(round(float(self.height_inches) * self.dpi.y))
@property
def rotation(self) -> int:
"""Return rotation of page in degrees.
Will only be a multiple of 90.
"""
return self._rotate
@rotation.setter
@@ -852,10 +894,13 @@ class PageInfo:
raise ValueError("rotation must be a cardinal angle")
@property
def images(self):
def images(self) -> list[ImageInfo]:
"""Return images."""
return self._images
def get_textareas(self, visible: bool | None = None, corrupt: bool | None = None):
"""Return textareas bounding boxes in PDF coordinates on the page."""
def predicate(obj, want_visible, want_corrupt):
result = True
if want_visible is not None:
@@ -875,22 +920,26 @@ class PageInfo:
@property
def dpi(self) -> Resolution:
"""Return DPI needed to render all images on the page."""
if self._dpi is None:
return Resolution(0.0, 0.0)
return self._dpi
@property
def userunit(self) -> Decimal:
"""Return user unit of page."""
return self._userunit
@property
def min_version(self) -> str:
"""Return minimum PDF version needed to render this page."""
if self.userunit is not None:
return '1.6'
else:
return '1.5'
def __repr__(self):
"""Return string representation."""
return (
f'<PageInfo '
f'pageno={self.pageno} {self.width_inches}"x{self.height_inches}" '
@@ -914,6 +963,7 @@ class PdfInfo:
check_pages=None,
executor: Executor = DEFAULT_EXECUTOR,
):
"""Initialize."""
self._infile = infile
if check_pages is None:
check_pages = range(0, 1_000_000_000)
@@ -940,42 +990,56 @@ class PdfInfo:
@property
def pages(self) -> Sequence[PageInfo | None]:
"""Return list of PageInfo objects, one per page in the PDF."""
return self._pages
@property
def min_version(self) -> str:
"""Return minimum PDF version needed to render this PDF."""
# The minimum PDF is the maximum version that any particular page needs
return max(page.min_version for page in self.pages if page)
@property
def has_userunit(self) -> bool:
"""Return True if any page has a user unit."""
return any(page.userunit != 1.0 for page in self.pages if page)
@property
def has_acroform(self) -> bool:
"""Return True if any page has an AcroForm."""
return self._has_acroform
@property
def filename(self) -> str | Path:
"""Return filename of PDF."""
if not isinstance(self._infile, (str, Path)):
raise NotImplementedError("can't get filename from stream")
return self._infile
@property
def needs_rendering(self) -> bool:
"""Return True if PDF contains XFA forms.
XFA forms are not supported by most standard PDF renderers, so we
need to detect and suppress them.
"""
return self._needs_rendering
def __getitem__(self, item) -> PageInfo:
"""Return PageInfo object for page number `item`."""
return self._pages[item]
def __len__(self):
"""Return number of pages in PDF."""
return len(self._pages)
def __repr__(self):
"""Return string representation."""
return f"<PdfInfo('...'), page count={len(self)}>"
def main():
"""Run as a script."""
import argparse # pylint: disable=import-outside-toplevel
from pprint import pprint # pylint: disable=import-outside-toplevel
+38 -2
View File
@@ -29,12 +29,16 @@ original_pdfsimplefont_init = PDFSimpleFont.__init__
def pdfsimplefont__init__(self, descriptor, widths, spec):
"""Monkeypatch pdfminer.six PDFSimpleFont.__init__.
If there is no ToUnicode and no Encoding, pdfminer.six assumes that Unicode
conversion is possible. This is incorrect, according to PDF Reference Manual
9.10.2. This patch fixes that.
"""
# Font encoding is specified either by a name of
# built-in encoding or a dictionary that describes
# the differences.
original_pdfsimplefont_init(self, descriptor, widths, spec)
# pdfminer is incorrect. If there is no ToUnicode and no Encoding, do not
# assume Unicode conversion is possible. RM 9.10.2
if not self.unicode_map and 'Encoding' not in spec:
self.cid2unicode = {}
return
@@ -48,6 +52,13 @@ PDFSimpleFont.__init__ = pdfsimplefont__init__
def pdftype3font__pscript5_get_height(self):
"""Monkeypatch for PScript5.dll PDFs.
The height of Type3 fonts is known to be incorrect in PScript5.dll
generated PDFs. This patch attempts to correct the height by
using the bbox height if it is available, otherwise using the
ascent and descent.
"""
h = self.bbox[3] - self.bbox[1]
if h == 0:
h = self.ascent - self.descent
@@ -55,10 +66,22 @@ def pdftype3font__pscript5_get_height(self):
def pdftype3font__pscript5_get_descent(self):
"""Monkeypatch for PScript5.dll PDFs.
The descent of Type3 fonts is known to be incorrect in PScript5.dll
generated PDFs. This patch attempts to correct the descent by
using the vscale.
"""
return self.descent * copysign(1.0, self.vscale)
def pdftype3font__pscript5_get_ascent(self):
"""Monkeypatch for PScript5.dll PDFs.
The ascent of Type3 fonts is known to be incorrect in PScript5.dll
generated PDFs. This patch attempts to correct the ascent by
using the vscale.
"""
return self.ascent * copysign(1.0, self.vscale)
@@ -96,6 +119,7 @@ class LTStateAwareChar(LTChar):
graphicstate,
textstate,
):
"""Initialize."""
super().__init__(
matrix,
font,
@@ -129,11 +153,13 @@ class LTStateAwareChar(LTChar):
return False
def get_text(self):
"""Get text from this character."""
if isinstance(self._text, tuple):
return '\ufffd' # standard 'Unknown symbol'
return self._text
def __repr__(self):
"""Return a string representation of this object."""
return (
f"<{self.__class__.__name__} "
f"{bbox2str(self.bbox)} "
@@ -149,16 +175,19 @@ class TextPositionTracker(PDFLayoutAnalyzer):
"""A page layout analyzer that pays attention to text visibility."""
def __init__(self, rsrcmgr, pageno=1, laparams=None):
"""Initialize the layout analyzer."""
super().__init__(rsrcmgr, pageno, laparams)
self.textstate = None
self.result = None
self.cur_item = None # not defined in pdfminer code as it should be
def begin_page(self, page, ctm):
"""Begin processing of a page."""
super().begin_page(page, ctm)
self.cur_item = LTPage(self.pageno, page.mediabox)
def end_page(self, page):
"""End processing of a page."""
assert not self._stack, str(len(self._stack))
assert isinstance(self.cur_item, LTPage), str(type(self.cur_item))
if self.laparams is not None:
@@ -167,12 +196,14 @@ class TextPositionTracker(PDFLayoutAnalyzer):
self.receive_layout(self.cur_item)
def render_string(self, textstate, seq, ncs, graphicstate):
"""Respond to render string event by updating text state."""
self.textstate = textstate.copy()
super().render_string(self.textstate, seq, ncs, graphicstate)
def render_char(
self, matrix, font, fontsize, scaling, rise, cid, ncs, graphicstate
):
"""Respond to render char event by updating text state."""
try:
text = font.to_unichr(cid)
assert isinstance(text, str), str(type(text))
@@ -197,17 +228,21 @@ class TextPositionTracker(PDFLayoutAnalyzer):
return item.adv
def handle_undefined_char(self, font, cid):
"""Handle undefined character."""
# log.info('undefined: %r, %r', font, cid)
return (font.fontname, cid)
def receive_layout(self, ltpage):
"""Receive layout handler."""
self.result = ltpage
def get_result(self):
"""Get the result of the analysis."""
return self.result
def get_page_analysis(infile, pageno, pscript5_mode):
"""Get the page analysis for a given page."""
rman = pdfminer.pdfinterp.PDFResourceManager(caching=True)
disable_boxes_flow = None
dev = TextPositionTracker(
@@ -248,6 +283,7 @@ def get_page_analysis(infile, pageno, pscript5_mode):
def get_text_boxes(obj):
"""Get the text boxes attached to the current node."""
for child in obj:
if isinstance(child, (LTTextBox)):
yield child
+6 -1
View File
@@ -43,11 +43,16 @@ def run(
Arguments should be identical to ``subprocess.run``, except for following:
Arguments:
Args:
args: Positional arguments to pass to ``subprocess.run``.
env: A set of environment variables. If None, the OS environment is used.
logs_errors_to_stdout: If True, indicates that the process writes its error
messages to stdout rather than stderr, so stdout should be logged
if there is an error. If False, stderr is logged. Could be used with
stderr=STDOUT, stdout=PIPE for example.
check: If True, raise an exception if the process exits with a non-zero
status code. If False, the return value will indicate success or failure.
kwargs: Additional arguments to pass to ``subprocess.run``.
"""
args, env, process_log, _text = _fix_process_args(args, env, kwargs)
+1 -1
View File
@@ -32,7 +32,7 @@ else:
spec=['HKEYType', 'EnumKey', 'EnumValue', 'HKEY_LOCAL_MACHINE', 'OpenKey']
)
# mypy does not understand winreg.HKeyType where winreg is a Mock (fair enough!)
HKEYType: TypeAlias = Any
HKEYType: TypeAlias = Any # type: ignore
log = logging.getLogger(__name__)