ruff lint and format

This commit is contained in:
James R. Barlow
2026-01-13 01:50:57 -08:00
parent 7bfe3ecd5b
commit 5acf21651f
29 changed files with 188 additions and 202 deletions
+1 -3
View File
@@ -287,9 +287,7 @@ def tesseract_log_output(stream: bytes) -> None:
lines = text.splitlines()
for line in lines:
if line.startswith("Tesseract Open Source"):
continue
elif line.startswith("Warning in pixReadMem"):
if line.startswith("Tesseract Open Source") or line.startswith("Warning in pixReadMem"):
continue
elif 'diacritics' in line:
tlog.warning("lots of diacritics - possibly poor OCR")
+24 -23
View File
@@ -181,12 +181,9 @@ def strip_invisible_text(pdf: Pdf, page: Page):
render_mode_stack.append(render_mode)
if operator == Operator('Q'):
try:
# IndexError is raised if stack is empty; try to carry on
with suppress(IndexError):
render_mode = render_mode_stack.pop()
except IndexError:
# Stack underflow: content stream is malformed
# but try to carry on
pass
if not in_text_obj:
if operator == Operator('BT'):
@@ -314,9 +311,9 @@ class OcrGrafter:
def finalize(self):
# Can have hocr OR parsed pages OR neither (no OCR), but not both
assert not (self.fpdf2_hocr_pages and self.fpdf2_parsed_pages), (
"Can't have both hocr and ocrtree pages"
)
assert not (
self.fpdf2_hocr_pages and self.fpdf2_parsed_pages
), "Can't have both hocr and ocrtree pages"
if self.fpdf2_hocr_pages:
# Render all pages with fpdf2, then graft
@@ -376,7 +373,8 @@ class OcrGrafter:
multi_font_manager = MultiFontManager(font_dir)
# Build renderer input as (pageno, ocr_tree, dpi) tuples
renderer_pages_data = [
(parsed.pageno, parsed.ocr_tree, parsed.dpi) for parsed in self.fpdf2_parsed_pages
(parsed.pageno, parsed.ocr_tree, parsed.dpi)
for parsed in self.fpdf2_parsed_pages
]
renderer = Fpdf2MultiPageRenderer(
pages_data=renderer_pages_data,
@@ -398,9 +396,7 @@ class OcrGrafter:
parsed.autorotate_correction,
parsed.emplaced_page,
)
self._graft_fpdf2_text_layer(
parsed.pageno, text_page, text_misaligned
)
self._graft_fpdf2_text_layer(parsed.pageno, text_page, text_misaligned)
page_rotation = _compute_page_rotation(
content_rotation,
@@ -414,9 +410,7 @@ class OcrGrafter:
with suppress(FileNotFoundError):
multi_page_pdf_path.unlink()
def _graft_fpdf2_text_layer(
self, pageno: int, text_page: Page, text_rotation: int
):
def _graft_fpdf2_text_layer(self, pageno: int, text_page: Page, text_rotation: int):
"""Graft a single text page onto the base PDF.
Similar to existing _graft_text_layer but works with
@@ -479,14 +473,17 @@ class OcrGrafter:
# Build transformation matrix for rotation and scaling
ctm = _build_text_layer_ctm(
wt, ht, wp, hp, float(base_mediabox[0]), float(base_mediabox[1]),
text_rotation
wt,
ht,
wp,
hp,
float(base_mediabox[0]),
float(base_mediabox[1]),
text_rotation,
)
if ctm is not None:
pdf_draw_xobj = (
(b'q %s cm\n' % ctm.encode())
+ (b'%s Do\n' % text_xobj_name)
+ b'Q\n'
(b'q %s cm\n' % ctm.encode()) + (b'%s Do\n' % text_xobj_name) + b'Q\n'
)
else:
pdf_draw_xobj = b'q\n' + (b'%s Do\n' % text_xobj_name) + b'\nQ\n'
@@ -552,9 +549,13 @@ class OcrGrafter:
# Build transformation matrix for rotation and scaling
ctm = _build_text_layer_ctm(
wt, ht, wp, hp,
float(base_mediabox[0]), float(base_mediabox[1]),
text_rotation
wt,
ht,
wp,
hp,
float(base_mediabox[0]),
float(base_mediabox[1]),
text_rotation,
)
log.debug("Grafting with ctm %r", ctm)
+6 -9
View File
@@ -47,9 +47,9 @@ def get_docinfo(base_pdf: Pdf, context: PdfContext) -> dict[str, str]:
if options.subject:
pdfmark['/Subject'] = options.subject
creator_tag = context.plugin_manager.get_ocr_engine(
options=options
).creator_tag(options)
creator_tag = context.plugin_manager.get_ocr_engine(options=options).creator_tag(
options
)
pdfmark['/Creator'] = f'{PROGRAM_NAME} {OCRMYPF_VERSION} / {creator_tag}'
pdfmark['/Producer'] = f'pikepdf {PIKEPDF_VERSION}'
@@ -100,9 +100,7 @@ def should_linearize(working_file: Path, context: PdfContext) -> bool:
For smaller files, linearization is not worth the effort.
"""
filesize = os.stat(working_file).st_size
if filesize > (context.options.fast_web_view * 1_000_000):
return True
return False
return filesize > (context.options.fast_web_view * 1_000_000)
def _fix_metadata(meta_original: PdfMetadata, meta_pdf: PdfMetadata):
@@ -110,12 +108,11 @@ def _fix_metadata(meta_original: PdfMetadata, meta_pdf: PdfMetadata):
# ensure consistency with Ghostscript.
if 'xmp:CreateDate' not in meta_pdf:
meta_pdf['xmp:CreateDate'] = meta_pdf.get('xmp:ModifyDate', '')
if meta_pdf.get('dc:title') == 'Untitled':
if meta_pdf.get('dc:title') == 'Untitled' and ('dc:title' not in meta_original):
# Ghostscript likes to set title to Untitled if omitted from input.
# Reverse this, because PDF/A TechNote 0003:Metadata in PDF/A-1
# and the XMP Spec do not make this recommendation.
if 'dc:title' not in meta_original:
del meta_pdf['dc:title']
del meta_pdf['dc:title']
def _unset_empty_metadata(meta: PdfMetadata, options):
+9 -8
View File
@@ -374,12 +374,13 @@ class OcrOptions(BaseModel):
@model_validator(mode='after')
def validate_redo_ocr_options(self):
"""Validate options compatible with redo mode."""
if self.mode == ProcessingMode.redo:
if self.deskew or self.clean_final or self.remove_background:
raise ValueError(
"--redo-ocr (or --mode redo) is not currently compatible with "
"--deskew, --clean-final, and --remove-background"
)
if self.mode == ProcessingMode.redo and (
self.deskew or self.clean_final or self.remove_background
):
raise ValueError(
"--redo-ocr (or --mode redo) is not currently compatible with "
"--deskew, --clean-final, and --remove-background"
)
return self
@model_validator(mode='after')
@@ -559,13 +560,13 @@ class OcrOptions(BaseModel):
elif namespace == 'optimize' and field_name == 'level':
# 'optimize' field maps to 'level' in OptimizeOptions
if 'optimize' in OcrOptions.model_fields:
value = getattr(self, 'optimize')
value = self.optimize
if value is not None:
kwargs[field_name] = _convert_value(value)
elif namespace == 'optimize' and field_name == 'jpeg_quality':
# jpg_quality maps to jpeg_quality
if 'jpg_quality' in OcrOptions.model_fields:
value = getattr(self, 'jpg_quality')
value = self.jpg_quality
if value is not None:
kwargs[field_name] = _convert_value(value)
+4 -15
View File
@@ -431,10 +431,7 @@ def describe_rotation(
else:
action = 'rotation appears correct'
else:
if correction != 0:
action = 'confidence too low to rotate'
else:
action = 'no change'
action = "confidence too low to rotate" if correction != 0 else "no change"
facing = ''
@@ -1093,10 +1090,7 @@ def _is_safe_pdfa(input_pdf: Path, options) -> bool:
return True
# Safe if we rewrote the PDF with force mode
if options.mode == ProcessingMode.force:
return True
return False
return options.mode == ProcessingMode.force
def should_linearize(working_file: Path, context: PdfContext) -> bool:
@@ -1105,9 +1099,7 @@ def should_linearize(working_file: Path, context: PdfContext) -> bool:
For smaller files, linearization is not worth the effort.
"""
filesize = os.stat(working_file).st_size
if filesize > (context.options.fast_web_view * 1_000_000):
return True
return False
return filesize > (context.options.fast_web_view * 1_000_000)
def get_pdf_save_settings(output_type: str) -> dict[str, Any]:
@@ -1225,10 +1217,7 @@ def merge_sidecars(txt_files: Iterable[Path | None], context: PdfContext) -> Pat
# others don't. Remove it if it exists, since we add one manually.
stream.write(txt.removesuffix('\f'))
else:
if from_ != to_:
pages = f'{from_}-{to_}'
else:
pages = f'{from_}'
pages = f"{from_}-{to_}" if from_ != to_ else f"{from_}"
stream.write(f'[OCR skipped on page(s) {pages}]')
return output_file
+2 -1
View File
@@ -123,7 +123,8 @@ class HOCRResultEncoder(json.JSONEncoder):
class HOCRResultDecoder(json.JSONDecoder):
def __init__(self, *args, **kwargs):
super().__init__(object_hook=self.dict_to_object, *args, **kwargs)
kwargs['object_hook'] = self.dict_to_object
super().__init__(*args, **kwargs)
def dict_to_object(self, d):
if 'Path' in d:
+7 -3
View File
@@ -50,7 +50,8 @@ class ProgressBar(Protocol):
unit (str | None):
A short label for the type of work being tracked (e.g. "page", "%", "image").
disable (bool):
If ``True``, progress updates are suppressed (no output). Defaults to ``False``.
If ``True``, progress updates are suppressed (no output).
Defaults to ``False``.
**kwargs:
Future or extra parameters that OCRmyPDF might pass. Implementations
should accept and ignore unrecognized keywords gracefully.
@@ -64,7 +65,8 @@ class ProgressBar(Protocol):
from ocrmypdf import hookimpl
class ConsoleProgressBar(ProgressBar):
def __init__(self, *, total=None, desc=None, unit=None, disable=False, **kwargs):
def __init__(self, *, total=None, desc=None, unit=None, disable=False,
**kwargs):
self.total = total
self.desc = desc
self.unit = unit
@@ -73,7 +75,9 @@ class ProgressBar(Protocol):
def __enter__(self):
if not self.disable:
print(f"Starting {self.desc or 'an OCR task'} (total={self.total} {self.unit})")
print(f"Starting {self.desc or 'an OCR task'} "
f"(total={self.total} {self.unit})"
)
return self
def __exit__(self, exc_type, exc_value, traceback):
+9 -8
View File
@@ -78,8 +78,8 @@ class ValidationCoordinator:
DENIED_LANGUAGES = {'equ', 'osd'}
if DENIED_LANGUAGES & set(options.languages):
raise BadArgsError(
"The following languages are for Tesseract's internal use and should not "
"be issued explicitly: "
"The following languages are for Tesseract's internal use and "
"should not be issued explicitly: "
f"{', '.join(DENIED_LANGUAGES & set(options.languages))}\n"
"Remove them from the -l/--language argument."
)
@@ -109,12 +109,13 @@ class ValidationCoordinator:
# by the ProcessingMode enum - only one mode can be active at a time.
# Validate redo mode compatibility
if options.mode == ProcessingMode.redo:
if options.deskew or options.clean_final or options.remove_background:
raise ValueError(
"--redo-ocr (or --mode redo) is not currently compatible with "
"--deskew, --clean-final, and --remove-background"
)
if options.mode == ProcessingMode.redo and (
options.deskew or options.clean_final or options.remove_background
):
raise ValueError(
"--redo-ocr (or --mode redo) is not currently compatible with "
"--deskew, --clean-final, and --remove-background"
)
# Validate output type compatibility
if options.output_type == 'none' and str(options.output_file) not in (
+4 -2
View File
@@ -61,7 +61,8 @@ class GhostscriptOptions(BaseModel):
Args:
parser: The argument parser to add arguments to
namespace: The namespace prefix for argument names (not used for ghostscript for backward compatibility)
namespace: The namespace prefix for argument names (not used for ghostscript
for backward compatibility)
"""
gs = parser.add_argument_group("Ghostscript", "Advanced control of Ghostscript")
gs.add_argument(
@@ -173,7 +174,8 @@ def rasterize_pdf_page(
"""Rasterize a single page of a PDF file using Ghostscript."""
# Check if user explicitly requested a different rasterizer
if options is not None and options.rasterizer == 'pypdfium':
return None # Let pypdfium handle it (it will error in check_options if unavailable)
# Let pypdfium handle it (it will error in check_options if unavailable)
return None
ghostscript.rasterize_pdf(
input_file,
+5 -6
View File
@@ -127,19 +127,18 @@ class OptimizeOptions(BaseModel):
@model_validator(mode='after')
def validate_optimization_consistency(self):
"""Validate optimization options are consistent."""
if self.level == 0 and any([
self.png_quality > 0,
self.jpeg_quality > 0
]):
if self.level == 0 and any([self.png_quality > 0, self.jpeg_quality > 0]):
log.warning(
"The arguments --png-quality and --jpeg-quality "
"will be ignored because --optimize=0."
)
return self
def validate_with_context(self, external_programs_available: dict[str, bool]) -> None:
def validate_with_context(
self, external_programs_available: dict[str, bool]
) -> None:
"""Validate options that require external context.
Args:
external_programs_available: Dict of program name -> availability
"""
+19 -20
View File
@@ -90,10 +90,7 @@ def _render_page_to_bitmap(
# Calculate crop to render the appropriate box
# Default (use_cropbox=False) renders MediaBox for consistency with Ghostscript
if use_cropbox:
crop = (0, 0, 0, 0) # No crop adjustment, use default CropBox
else:
crop = _calculate_mediabox_crop(page) # Expand to MediaBox
crop = (0, 0, 0, 0) if use_cropbox else _calculate_mediabox_crop(page)
bitmap = page.render(
scale=scale,
@@ -155,9 +152,12 @@ def _process_image_for_output(
def _save_image(pil_image, output_file: Path, format_name: str):
"""Save PIL image to file with appropriate DPI metadata."""
save_kwargs = {}
if format_name in ('PNG', 'TIFF') and 'dpi' in pil_image.info:
save_kwargs['dpi'] = pil_image.info['dpi']
elif format_name == 'JPEG' and 'dpi' in pil_image.info:
if (
format_name in ('PNG', 'TIFF')
and 'dpi' in pil_image.info
or format_name == 'JPEG'
and 'dpi' in pil_image.info
):
save_kwargs['dpi'] = pil_image.info['dpi']
pil_image.save(output_file, format=format_name, **save_kwargs)
@@ -190,19 +190,18 @@ def rasterize_pdf_page(
return None # Fall back to Ghostscript
# Acquire lock to ensure thread-safe access to pypdfium2
with _pdfium_lock:
# Open the PDF document and get the specific page (pypdfium2 uses 0-based indexing)
with (
closing(_open_pdf_document(input_file)) as pdf,
closing(pdf[pageno - 1]) as page,
):
# Render the page to a bitmap
bitmap = _render_page_to_bitmap(
page, raster_device, raster_dpi, rotation, use_cropbox
)
with closing(bitmap):
# Convert to PIL Image
pil_image = bitmap.to_pil()
with (
_pdfium_lock,
closing(_open_pdf_document(input_file)) as pdf,
closing(pdf[pageno - 1]) as page,
):
# Render the page to a bitmap
bitmap = _render_page_to_bitmap(
page, raster_device, raster_dpi, rotation, use_cropbox
)
with closing(bitmap):
# Convert to PIL Image
pil_image = bitmap.to_pil()
# Process and save image outside the lock (PIL operations are thread-safe)
pil_image, format_name = _process_image_for_output(
@@ -216,8 +216,8 @@ class TesseractOptions(BaseModel):
default=32767,
dest=f'{namespace}_downsample_above',
help=(
"Downsample images larger than this size pixel size in either dimension "
f"before OCR. --{namespace}-downsample-large-images downsamples only when "
"Downsample images larger than this size pixel size (either dimension) "
f"before OCR. --{namespace}-downsample-large-images downsamples when "
"an image exceeds Tesseract's internal limits. This argument causes "
"downsampling to occur when an image exceeds the given size. This may "
"reduce OCR quality, but on large images the most desirable text is "
@@ -280,8 +280,8 @@ class TesseractOptions(BaseModel):
DENIED_LANGUAGES = {'equ', 'osd'}
if DENIED_LANGUAGES & set(languages):
raise BadArgsError(
"The following languages are for Tesseract's internal use and should not "
"be issued explicitly: "
"The following languages are for Tesseract's internal use "
"and should not be issued explicitly: "
f"{', '.join(DENIED_LANGUAGES & set(languages))}\n"
"Remove them from the -l/--language argument."
)
+2 -4
View File
@@ -200,7 +200,7 @@ def is_iterable_notstr(thing: Any) -> bool:
def monotonic(seq: Sequence) -> bool:
"""Does this sequence increase monotonically?"""
return all(b > a for a, b in zip(seq, seq[1:]))
return all(b > a for a, b in zip(seq, seq[1:], strict=False))
def page_number(input_file: os.PathLike) -> int:
@@ -298,9 +298,7 @@ def check_pdf(input_file: Path) -> bool:
if linearize_msgs:
log.warning(linearize_msgs)
if success and not linearize_msgs:
return True
return False
return bool(success and not linearize_msgs)
def clamp(n: T, smallest: T, largest: T) -> T:
+4 -5
View File
@@ -60,11 +60,10 @@ def _calculate_downsample(
elif size[1] == 0:
size = min(size[0], max_size[0]), 1
if max_pixels is not None:
if size[0] * size[1] > max_pixels:
log.debug("Resizing image to fit image pixel limit")
pixels_factor = sqrt(max_pixels / (size[0] * size[1]))
size = floor(size[0] * pixels_factor), floor(size[1] * pixels_factor)
if max_pixels is not None and size[0] * size[1] > max_pixels:
log.debug("Resizing image to fit image pixel limit")
pixels_factor = sqrt(max_pixels / (size[0] * size[1]))
size = floor(size[0] * pixels_factor), floor(size[1] * pixels_factor)
if max_bytes is not None:
bpp = bytes_per_pixel
+3 -5
View File
@@ -194,11 +194,9 @@ def extract_image_jbig2(
def _should_optimize_jpeg(options, filtdp):
if options.optimize >= 2:
return True
if options.optimize < 2 and ghostscript.version() >= Version('10.6.0'):
# Ghostscript 10.6.0+ introduced some sort of JPEG encoding issue.
# To resolve this, re-optimize the JPEG anyway.
return True
return False
# Ghostscript 10.6.0+ introduced some sort of JPEG encoding issue.
# To resolve this, re-optimize the JPEG anyway.
return options.optimize < 2 and ghostscript.version() >= Version('10.6.0')
def extract_image_generic(
+3 -3
View File
@@ -63,7 +63,7 @@ class TextMarker:
def _is_unit_square(shorthand):
"""Check if the shorthand represents a unit square transformation."""
values = map(float, shorthand)
pairwise = zip(values, UNIT_SQUARE)
pairwise = zip(values, UNIT_SQUARE, strict=False)
return all(isclose(a, b, rel_tol=1e-3) for a, b in pairwise)
@@ -138,11 +138,11 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
elif operator == 'cm':
try:
ctm = Matrix(operands) @ ctm
except ValueError:
except ValueError as e:
raise InputFileError(
"PDF content stream is corrupt - this PDF is malformed. "
"Use a PDF editor that is capable of visually inspecting the PDF."
)
) from e
elif operator == 'Do':
image_name = operands[0]
settings = XobjectSettings(
+10 -8
View File
@@ -79,23 +79,25 @@ class ImageInfo:
self._width = pim.width
self._height = pim.height
if (smask := pim.obj.get(Name.SMask, None)) is not None:
if (smask := pim.obj.get(Name.SMask, None)) is not None and isinstance(
smask, Stream | Dictionary
):
# SMask is pretty much an alpha channel, but in PDF it's possible
# for channel to have different dimensions than the image
# itself. Some PDF writers use this to create a grayscale stencil
# mask. For our purposes, the effective size is the size of the
# larger component (image or smask).
if isinstance(smask, Stream | Dictionary):
self._width = max(smask.get(Name.Width, 0), self._width)
self._height = max(smask.get(Name.Height, 0), self._height)
if (mask := pim.obj.get(Name.Mask, None)) is not None:
self._width = max(smask.get(Name.Width, 0), self._width)
self._height = max(smask.get(Name.Height, 0), self._height)
if (mask := pim.obj.get(Name.Mask, None)) is not None and isinstance(
mask, Stream | Dictionary
):
# If the image has a /Mask entry, it has an explicit mask.
# /Mask can be a Stream or an Array. If it's a Stream,
# use its /Width and /Height if they are larger than the main
# image's.
if isinstance(mask, Stream | Dictionary):
self._width = max(mask.get(Name.Width, 0), self._width)
self._height = max(mask.get(Name.Height, 0), self._height)
self._width = max(mask.get(Name.Width, 0), self._width)
self._height = max(mask.get(Name.Height, 0), self._height)
# If /ImageMask is true, then this image is a stencil mask
# (Images that draw with this stencil mask will have a reference to
+8 -9
View File
@@ -304,12 +304,10 @@ class PageInfo:
obj: TextboxInfo, want_visible: bool | None, want_corrupt: bool | None
) -> bool:
result = True
if want_visible is not None:
if obj.is_visible != want_visible:
result = False
if want_corrupt is not None:
if obj.is_corrupt != want_corrupt:
result = False
if want_visible is not None and obj.is_visible != want_visible:
result = False
if want_corrupt is not None and obj.is_corrupt != want_corrupt:
result = False
return result
if not self._textboxes:
@@ -442,9 +440,10 @@ class PdfInfo:
)
self._needs_rendering = pdf.Root.get(Name.NeedsRendering, False)
if Name.AcroForm in pdf.Root:
if len(pdf.Root.AcroForm.get(Name.Fields, [])) > 0:
self._has_acroform = True
elif Name.XFA in pdf.Root.AcroForm:
if (
len(pdf.Root.AcroForm.get(Name.Fields, [])) > 0
or Name.XFA in pdf.Root.AcroForm
):
self._has_acroform = True
self._has_signature = bool(pdf.Root.AcroForm.get(Name.SigFlags, 0) & 1)
self._is_tagged = bool(
+2 -2
View File
@@ -58,7 +58,7 @@ def pdfsimplefont__init__(
return
setattr(PDFSimpleFont, '__init__', pdfsimplefont__init__)
PDFSimpleFont.__init__ = pdfsimplefont__init__
# Patch pdfminer.six buffer size
# The parser doesn't properly handle keyword tokens are split across the end of the
@@ -363,7 +363,7 @@ class PdfMinerState:
except StopIteration:
raise InputFileError(
f"pdfminer did not find page {pageno} in the input file."
)
) from None
page = self.page_cache[pageno]
if not page:
raise InputFileError(
+1 -4
View File
@@ -40,8 +40,5 @@ class OcrQualityDictionary:
w != w.lower() and w.lower() in self.dictionary
):
matches += 1
if matches > 0:
hit_ratio = matches / len(text_words)
else:
hit_ratio = 0.0
hit_ratio = matches / len(text_words) if matches > 0 else 0.0
return hit_ratio