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 -1
View File
@@ -108,7 +108,7 @@ def main():
doc1 = pymupdf.open(os.path.join(d, "output1.pdf"))
doc2 = pymupdf.open(os.path.join(d, "output2.pdf"))
for i, page1_2 in enumerate(zip(doc1, doc2)):
for i, page1_2 in enumerate(zip(doc1, doc2, strict=False)):
st.write(f"Page {i+1}")
page1, page2 = page1_2
col1, col2 = st.columns(2)
+1 -1
View File
@@ -62,7 +62,7 @@ def main():
with st.expander("Text"):
doc1 = pymupdf.open(os.path.join(d, "1.pdf"))
doc2 = pymupdf.open(os.path.join(d, "2.pdf"))
for i, page1_2 in enumerate(zip(doc1, doc2)):
for i, page1_2 in enumerate(zip(doc1, doc2, strict=False)):
st.write(f"Page {i+1}")
page1, page2 = page1_2
col1, col2 = st.columns(2)
+15 -21
View File
@@ -17,7 +17,7 @@ dependencies = [
"img2pdf>=0.5",
"packaging>=20",
"pdfminer.six>=20220319",
"pi-heif", # Heif image format - maintainers: if this is removed, it will NOT break
"pi-heif", # Heif image format - maintainers: if this is removed, it will NOT break
"pikepdf>=10",
"Pillow>=10.0.1",
"pluggy>=1",
@@ -121,12 +121,17 @@ target-version = "py310"
[tool.ruff.lint]
"select" = [
"D", # pydocstyle
"E", # pycodestyle
"W", # pycodestyle
"F", # pyflakes
"I001", # isort
"UP", # pyupgrade
"D", # pydocstyle
"E", # pycodestyle
"W", # pycodestyle
"F", # pyflakes
"I", # isort
"UP", # pyupgrade
"SIM", # simplify
"B", # flake8-bugbear
]
ignore = [
"B028", # warn no explicit stacklevel
]
[tool.ruff.lint.isort]
@@ -146,10 +151,7 @@ quote-style = "preserve"
[dependency-groups]
# Developer-only tools - use `uv sync --group <name>` (NOT pip-installable)
dev = [
"mypy>=1.13.0",
"ipykernel>=6.29.5",
]
dev = ["mypy>=1.13.0", "ipykernel>=6.29.5"]
test = [
# Core testing framework
"coverage[toml]>=6.2",
@@ -166,13 +168,5 @@ test = [
# Extended test capabilities (merged from extended_test)
"pymupdf>=1.24.14",
]
docs = [
"myst-parser>=4.0.1",
"sphinx",
"sphinx-issues",
"sphinx-rtd-theme",
]
streamlit-dev = [
"streamlit>=1.40.2",
"streamlit-pdf-viewer>=0.0.19",
]
docs = ["myst-parser>=4.0.1", "sphinx", "sphinx-issues", "sphinx-rtd-theme"]
streamlit-dev = ["streamlit>=1.40.2", "streamlit-pdf-viewer>=0.0.19"]
+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
+1 -1
View File
@@ -133,7 +133,7 @@ def cached_run(options, run_args, **run_kwargs):
}
# Don't pass timeout=0 to the actual run call - it would timeout immediately
# A timeout of 0 means "use default/no timeout" in the caching context
if cache_kwargs.get('timeout', None) == 0.0:
if cache_kwargs.get('timeout') == 0.0:
cache_kwargs['timeout'] = None
if 'check' not in cache_kwargs:
cache_kwargs['check'] = True
+1 -1
View File
@@ -23,7 +23,7 @@ def acroform(resources):
def test_acroform_and_redo(acroform, no_outpdf):
with pytest.raises(
ocrmypdf.exceptions.InputFileError,
match='--redo-ocr (or --mode redo) is not currently possible',
match=r'.*--redo-ocr.*is not currently possible.*',
):
check_ocrmypdf(acroform, no_outpdf, '--redo-ocr')
+1 -1
View File
@@ -382,7 +382,7 @@ class TestGs106JpegCorruptionRepair:
repaired_bytes_list.append(obj.read_raw_bytes())
assert len(repaired_bytes_list) == len(original_bytes_list)
for orig, repaired_bytes in zip(original_bytes_list, repaired_bytes_list):
for orig, repaired_bytes in zip(original_bytes_list, repaired_bytes_list, strict=False):
assert orig == repaired_bytes, "Repaired bytes should match original"
# Check that error/warning was logged
-2
View File
@@ -13,8 +13,6 @@ import dataclasses
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from ocrmypdf import OcrElement
-2
View File
@@ -14,11 +14,9 @@ import pytest
from ocrmypdf.font import (
BuiltinFontProvider,
ChainedFontProvider,
FontManager,
SystemFontProvider,
)
# --- SystemFontProvider Platform Detection Tests ---
Generated
+41 -30
View File
@@ -474,6 +474,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
]
[[package]]
name = "cyclopts"
version = "4.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "docstring-parser" },
{ name = "rich" },
{ name = "rich-rst" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/c4/60b6068e703c78656d07b249919754f8f60e9e7da3325560574ee27b4e39/cyclopts-4.4.4.tar.gz", hash = "sha256:f30c591c971d974ab4f223e099f881668daed72de713713c984ca41479d393dd", size = 160046, upload-time = "2026-01-05T03:40:18.438Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/5b/0eceb9a5990de9025733a0d212ca43649ba9facd58b8552b6bf93c11439d/cyclopts-4.4.4-py3-none-any.whl", hash = "sha256:316f798fe2f2a30cb70e7140cfde2a46617bfbb575d31bbfdc0b2410a447bd83", size = 197398, upload-time = "2026-01-05T03:40:17.141Z" },
]
[[package]]
name = "debugpy"
version = "1.8.17"
@@ -545,6 +562,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" },
]
[[package]]
name = "docstring-parser"
version = "0.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
]
[[package]]
name = "docutils"
version = "0.21.2"
@@ -1415,8 +1441,8 @@ dependencies = [
[package.optional-dependencies]
watcher = [
{ name = "cyclopts" },
{ name = "python-dotenv" },
{ name = "typer-slim", extra = ["standard"] },
{ name = "watchdog" },
]
webservice = [
@@ -1454,6 +1480,7 @@ test = [
[package.metadata]
requires-dist = [
{ name = "cyclopts", marker = "extra == 'watcher'", specifier = ">=3" },
{ name = "deprecation", specifier = ">=2.1.0" },
{ name = "fpdf2", specifier = ">=2.8.0" },
{ name = "img2pdf", specifier = ">=0.5" },
@@ -1468,7 +1495,6 @@ requires-dist = [
{ name = "python-dotenv", marker = "extra == 'watcher'" },
{ name = "rich", specifier = ">=13" },
{ name = "streamlit", marker = "extra == 'webservice'", specifier = ">=1.41.0" },
{ name = "typer-slim", extras = ["standard"], marker = "extra == 'watcher'" },
{ name = "uharfbuzz", specifier = ">=0.53.2" },
{ name = "watchdog", marker = "extra == 'watcher'", specifier = ">=1.0.2" },
]
@@ -2429,6 +2455,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
]
[[package]]
name = "rich-rst"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docutils" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" },
]
[[package]]
name = "roman-numerals-py"
version = "3.1.0"
@@ -2560,15 +2599,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/d2/4a73b18821fd4669762c855fd1f4e80ceb66fb72d71162d14da58444a763/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f", size = 552199, upload-time = "2025-10-22T22:24:26.54Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "six"
version = "1.17.0"
@@ -2914,25 +2944,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
]
[[package]]
name = "typer-slim"
version = "0.20.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/45/81b94a52caed434b94da65729c03ad0fb7665fab0f7db9ee54c94e541403/typer_slim-0.20.0.tar.gz", hash = "sha256:9fc6607b3c6c20f5c33ea9590cbeb17848667c51feee27d9e314a579ab07d1a3", size = 106561, upload-time = "2025-10-20T17:03:46.642Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl", hash = "sha256:f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d", size = 47087, upload-time = "2025-10-20T17:03:44.546Z" },
]
[package.optional-dependencies]
standard = [
{ name = "rich" },
{ name = "shellingham" },
]
[[package]]
name = "types-humanfriendly"
version = "10.0.1.20250319"