ruff lint and format
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user