ruff lint and format
This commit is contained in:
@@ -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
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,8 +13,6 @@ import dataclasses
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from ocrmypdf import OcrElement
|
||||
|
||||
|
||||
|
||||
@@ -14,11 +14,9 @@ import pytest
|
||||
from ocrmypdf.font import (
|
||||
BuiltinFontProvider,
|
||||
ChainedFontProvider,
|
||||
FontManager,
|
||||
SystemFontProvider,
|
||||
)
|
||||
|
||||
|
||||
# --- SystemFontProvider Platform Detection Tests ---
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user