Compare commits

...
15 Commits
Author SHA1 Message Date
James R. Barlow cb2f090c60 v16.3.1 release notes 2024-05-21 01:39:30 -07:00
James R. Barlow f3d6387bca Fix "OCR" progress bar not matching actual progress 2024-05-21 01:35:14 -07:00
James R. Barlow abf9729c61 Semfree test: accept pdfa conversion failed as a valid return code
Fixes #1316
2024-05-21 01:26:11 -07:00
James R. Barlow 442e9c9f0d Add missing codecov token where missed & drop unneeded brew openssl 2024-05-19 01:07:38 -07:00
James R. Barlow 397fad249d v16.3.0 release notes 2024-05-19 00:50:59 -07:00
James R. Barlow 9a3c5a3f7c Add progressbar for metadata_fixup
Might take time for big files. Pdf.open() potentially is expensive as well, but QPDF doesn't give us progress feedback for that.

Closes Show progress during postprocessing #1313
2024-05-19 00:46:50 -07:00
James R. Barlow 950c700274 Fix Ghostscript PDF/A progressbar not displaying 2024-05-19 00:44:21 -07:00
James R. Barlow 26432c38a9 Raise exception if rotate pages threshold adjusted without --rotate-pages
Fixes Make usage of --rotate-pages-threshold clearer #1309
2024-05-18 23:49:27 -07:00
James R. Barlow 28be50136c hocr: If a line box's coords are invalid, log and error and don't render
Addresses [Bug]: Crash on multiple .pdf files #1312

Not actually a fix, but at least it will get us better diagnostics. Appears old Tesseract 4.x generates bad line boxes at times.
2024-05-18 23:32:18 -07:00
James R. Barlow 0c62f2de5d Issue template: check for EOL OS 2024-05-17 19:51:15 -07:00
James R. Barlow 5caf654f22 Add new codecov token 2024-05-11 01:03:41 -07:00
James R. Barlow 205593445e Change test to run on macos x64 and arm64 2024-05-11 00:13:08 -07:00
James R. Barlow f25fb8c63a Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-05-08 00:39:27 -07:00
James R. Barlow 99c78650b6 Add better error message for PDFs with invalid CTMs
Closes #1303
2024-05-07 14:00:30 -07:00
Ahmed AbdouandGitHub 08e89e2dbe Adding language install docs for archlinux (#1296)
Adding language install docs for archlinux
2024-04-24 14:46:05 -07:00
14 changed files with 151 additions and 38 deletions
@@ -24,7 +24,7 @@ body:
- type: dropdown - type: dropdown
id: packaging-system id: packaging-system
attributes: attributes:
label: Where are you installing from? label: Where are you installing/running from?
multiple: true multiple: true
options: options:
- PyPI (pip, poetry, pipx, etc.) - PyPI (pip, poetry, pipx, etc.)
@@ -37,6 +37,11 @@ body:
- source build - source build
validations: validations:
required: true required: true
- type: input
id: version
attributes:
label: OCRmyPDF version
description: Paste "ocrmypdf --version" here
- type: dropdown - type: dropdown
id: operating-system id: operating-system
attributes: attributes:
@@ -47,6 +52,17 @@ body:
- Windows - Windows
- macOS - macOS
- BSD - BSD
- type: input
id: os_version
attributes:
label: Operating system details and version
- type: checkboxes
attributes:
label: Simple sanity checks
description: Select all that apply
options:
- label: Operating system is currently supported by its vendor (not end of life)
- label: Python version is compatible with OCRmyPDF
- type: textarea - type: textarea
id: logs id: logs
attributes: attributes:
+8 -2
View File
@@ -101,16 +101,19 @@ jobs:
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with: with:
files: ./coverage.xml files: ./coverage.xml
env_vars: OS,PYTHON env_vars: OS,PYTHON
test_macos: test_macos:
name: Test macOS name: Test macOS
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [macos-latest] os: [macos-latest, macos-13] # macos-latest is arm64, macos-13 is x86_64
python: ["3.10", "3.11", "3.12"] python: ["3.10", "3.11", "3.12"]
env: env:
@@ -131,7 +134,6 @@ jobs:
ghostscript \ ghostscript \
jbig2enc \ jbig2enc \
openjpeg \ openjpeg \
openssl \
pngquant \ pngquant \
tesseract tesseract
@@ -159,6 +161,8 @@ jobs:
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with: with:
files: ./coverage.xml files: ./coverage.xml
env_vars: OS,PYTHON env_vars: OS,PYTHON
@@ -202,6 +206,8 @@ jobs:
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with: with:
files: ./coverage.xml files: ./coverage.xml
env_vars: OS,PYTHON env_vars: OS,PYTHON
+16
View File
@@ -68,6 +68,22 @@ to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``. ``-l eng -l fra``.
Archlinux
------
.. code-block:: bash
# Display a list of all Tesseract language packs
pacman -Ss tesseract-data
# Install German language pack
pacman -S tesseract-data-deu
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as
to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
Gentoo Gentoo
------ ------
+15
View File
@@ -30,6 +30,21 @@ OCRmyPDF typically supports the three most recent Python versions.
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg .. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
v16.3.1
=======
- Fixed a test suite failure with Ghostscript 10.03.0+. :issue:`1316`
- Fixed an issue with the presentation of the "OCR" progress bar. :issue:`1313`
v16.3.0
=======
- Fixed progress bar not displaying for Ghostscript PDF/A conversion. :issue:`1313`
- Added progress bar for linearization. :issue:`1313`
- If `--rotate-pages-threshold` issued without `--rotate-pages` we now exit with
an error since the user likely intended to use `--rotate-pages`. :issue:`1309`
- If Tesseract hOCR gives an invalid line box, print an error message instead of
exiting with an error. :issue:`1312`
v16.2.0 v16.2.0
======= =======
+8
View File
@@ -0,0 +1,8 @@
# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# Enforce English hegemony
DEFAULT_LANGUAGE = 'eng'
# Default rotation threshold
DEFAULT_ROTATE_PAGES_THRESHOLD = 14.0
+18 -3
View File
@@ -177,6 +177,17 @@ class GhostscriptFollower:
self.progressbar_class = progressbar_class self.progressbar_class = progressbar_class
self.progressbar = None self.progressbar = None
def __enter__(self):
# We can't actually set up the progressbar here, because we don't know
# how many pages there are until the first __call__() happens. So we
# do it in __call__().
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.progressbar:
return self.progressbar.__exit__(exc_type, exc_value, traceback)
return False
def __call__(self, line): def __call__(self, line):
if not self.progressbar_class: if not self.progressbar_class:
return return
@@ -187,7 +198,8 @@ class GhostscriptFollower:
self.progressbar = self.progressbar_class( self.progressbar = self.progressbar_class(
total=self.count, desc="PDF/A conversion", unit='page' total=self.count, desc="PDF/A conversion", unit='page'
) )
return # Now that we know the count, we can set up the progressbar.
self.progressbar.__enter__()
else: else:
if self.re_page.match(line.strip()): if self.re_page.match(line.strip()):
self.progressbar.update() self.progressbar.update()
@@ -265,7 +277,10 @@ def generate_pdfa(
) )
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
try: try:
with Path(output_file).open('wb') as output: with (
Path(output_file).open('wb') as output,
GhostscriptFollower(progressbar_class) as pbar,
):
p = run_polling_stderr( p = run_polling_stderr(
args_gs, args_gs,
stdout=output, stdout=output,
@@ -274,7 +289,7 @@ def generate_pdfa(
text=True, text=True,
encoding='utf-8', encoding='utf-8',
errors='replace', errors='replace',
callback=GhostscriptFollower(progressbar_class), callback=pbar,
) )
except CalledProcessError as e: except CalledProcessError as e:
# Ghostscript does not change return code when it fails to create # Ghostscript does not change return code when it fails to create
+31 -4
View File
@@ -153,18 +153,45 @@ def _set_language(pdf: Pdf, languages: list[str]):
pdf.Root.Lang = iso639_2 pdf.Root.Lang = iso639_2
class MetadataProgress:
def __init__(self, progressbar_class):
self.progressbar_class = progressbar_class
self.progressbar = self.progressbar_class(
total=100, desc="Linearizing", unit='%'
)
def __enter__(self):
self.progressbar.__enter__()
return self
def __exit__(self, exc_type, exc_value, traceback):
return self.progressbar.__exit__(exc_type, exc_value, traceback)
def __call__(self, percent: int):
if not self.progressbar_class:
return
self.progressbar.update(completed=percent)
def metadata_fixup( def metadata_fixup(
working_file: Path, context: PdfContext, pdf_save_settings: dict[str, Any] working_file: Path, context: PdfContext, pdf_save_settings: dict[str, Any]
) -> Path: ) -> Path:
"""Fix certain metadata fields after Ghostscript PDF/A conversion. """Fix certain metadata fields whether PDF or PDF/A.
Override some of Ghostscript's metadata choices.
Also report on metadata in the input file that was not retained during Also report on metadata in the input file that was not retained during
PDF/A conversion. conversion.
""" """
output_file = context.get_path('metafix.pdf') output_file = context.get_path('metafix.pdf')
options = context.options options = context.options
with Pdf.open(context.origin) as original, Pdf.open(working_file) as pdf: pbar_class = context.plugin_manager.hook.get_progressbar_class()
with (
Pdf.open(context.origin) as original,
Pdf.open(working_file) as pdf,
MetadataProgress(pbar_class) as pbar,
):
docinfo = get_docinfo(original, context) docinfo = get_docinfo(original, context)
with ( with (
original.open_metadata( original.open_metadata(
@@ -182,6 +209,6 @@ def metadata_fixup(
report_on_metadata(options, meta_missing) report_on_metadata(options, meta_missing)
_set_language(pdf, options.languages) _set_language(pdf, options.languages)
pdf.save(output_file, **pdf_save_settings) pdf.save(output_file, progress=pbar, **pdf_save_settings)
return output_file return output_file
+3 -4
View File
@@ -103,14 +103,14 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
try: try:
set_thread_pageno(result.pageno + 1) set_thread_pageno(result.pageno + 1)
sidecars[result.pageno] = result.text sidecars[result.pageno] = result.text
pbar.update() pbar.update(0.5)
ocrgraft.graft_page( ocrgraft.graft_page(
pageno=result.pageno, pageno=result.pageno,
image=result.pdf_page_from_image, image=result.pdf_page_from_image,
textpdf=result.ocr, textpdf=result.ocr,
autorotate_correction=result.orientation_correction, autorotate_correction=result.orientation_correction,
) )
pbar.update() pbar.update(0.5)
finally: finally:
set_thread_pageno(None) set_thread_pageno(None)
@@ -118,10 +118,9 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
use_threads=options.use_threads, use_threads=options.use_threads,
max_workers=max_workers, max_workers=max_workers,
progress_kwargs=dict( progress_kwargs=dict(
total=(2 * len(context.pdfinfo)), total=len(context.pdfinfo),
desc='OCR' if options.tesseract_timeout > 0 else 'Image processing', desc='OCR' if options.tesseract_timeout > 0 else 'Image processing',
unit='page', unit='page',
unit_scale=0.5,
disable=not options.progress_bar, disable=not options.progress_bar,
), ),
worker_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS), worker_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS),
+11 -5
View File
@@ -66,7 +66,7 @@ class ProgressBar(Protocol):
def __exit__(self, *args): def __exit__(self, *args):
"""Exit a progress bar context.""" """Exit a progress bar context."""
def update(self, n=1): def update(self, n=1, *, completed=None):
"""Update the progress bar by an increment. """Update the progress bar by an increment.
For use within a progress bar context. For use within a progress bar context.
@@ -85,7 +85,7 @@ class NullProgressBar:
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
return False return False
def update(self, _arg=None): def update(self, _arg=None, *, completed=None):
return return
@@ -103,6 +103,7 @@ class RichProgressBar:
disable: bool = False, disable: bool = False,
**kwargs, **kwargs,
): ):
self._entered = False
self.progress = Progress( self.progress = Progress(
TextColumn( TextColumn(
"[progress.description]{task.description}", "[progress.description]{task.description}",
@@ -130,6 +131,7 @@ class RichProgressBar:
def __enter__(self): def __enter__(self):
self.progress.start() self.progress.start()
self._entered = True
return self return self
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
@@ -137,6 +139,10 @@ class RichProgressBar:
self.progress.stop() self.progress.stop()
return False return False
def update(self, value=None): def update(self, n=1, *, completed=None):
advance = self.unit_scale if value is None else value assert self._entered, "Progress bar must be entered before updating"
self.progress.update(self.progress_bar, advance=advance) if completed is None:
advance = self.unit_scale if n is None else n
self.progress.update(self.progress_bar, advance=advance)
else:
self.progress.update(self.progress_bar, completed=completed)
+6 -8
View File
@@ -20,6 +20,7 @@ import pikepdf
import PIL import PIL
from pluggy import PluginManager from pluggy import PluginManager
from ocrmypdf._defaults import DEFAULT_LANGUAGE, DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf._exec import unpaper from ocrmypdf._exec import unpaper
from ocrmypdf.exceptions import ( from ocrmypdf.exceptions import (
BadArgsError, BadArgsError,
@@ -30,17 +31,9 @@ from ocrmypdf.exceptions import (
from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink
from ocrmypdf.subprocess import check_external_program from ocrmypdf.subprocess import check_external_program
# -------------
# External dependencies
DEFAULT_LANGUAGE = 'eng' # Enforce English hegemony
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# --------
def check_platform() -> None: def check_platform() -> None:
if sys.maxsize <= 2**32: # pragma: no cover if sys.maxsize <= 2**32: # pragma: no cover
log.warning( log.warning(
@@ -130,6 +123,11 @@ def check_options_preprocessing(options: Namespace) -> None:
options.clean = True options.clean = True
if options.unpaper_args and not options.clean: if options.unpaper_args and not options.clean:
raise BadArgsError("--clean is required for --unpaper-args") raise BadArgsError("--clean is required for --unpaper-args")
if (
options.rotate_pages_threshold != DEFAULT_ROTATE_PAGES_THRESHOLD
and not options.rotate_pages
):
raise BadArgsError("--rotate-pages is required for --rotate-pages-threshold")
if options.clean: if options.clean:
check_external_program( check_external_program(
program='unpaper', program='unpaper',
+2 -1
View File
@@ -9,6 +9,7 @@ import argparse
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
from typing import Any, TypeVar from typing import Any, TypeVar
from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
from ocrmypdf._version import __version__ as _VERSION from ocrmypdf._version import __version__ as _VERSION
@@ -403,7 +404,7 @@ Online documentation is located at:
) )
advanced.add_argument( advanced.add_argument(
'--rotate-pages-threshold', '--rotate-pages-threshold',
default=14.0, default=DEFAULT_ROTATE_PAGES_THRESHOLD,
type=numeric(float, 0, 1000), type=numeric(float, 0, 1000),
metavar='CONFIDENCE', metavar='CONFIDENCE',
help="Only rotate pages when confidence is above this value (arbitrary " help="Only rotate pages when confidence is above this value (arbitrary "
+7 -7
View File
@@ -117,15 +117,12 @@ class HocrTransform:
# Stop after first div that has page coordinates # Stop after first div that has page coordinates
break break
def _get_element_text(self, element: Element): def _get_element_text(self, element: Element) -> str:
"""Return the textual content of the element and its children.""" """Return the textual content of the element and its children."""
text = '' text = element.text if element.text is not None else ''
if element.text is not None:
text += element.text
for child in element: for child in element:
text += self._get_element_text(child) text += self._get_element_text(child)
if element.tail is not None: text += element.tail if element.tail is not None else ''
text += element.tail
return text return text
@classmethod @classmethod
@@ -286,7 +283,10 @@ class HocrTransform:
line_box = self.element_coordinates(line) line_box = self.element_coordinates(line)
if not line_box: if not line_box:
return return
assert line_box.ury > line_box.lly # lly is top, ury is bottom if line_box.ury <= line_box.lly:
log.error("line box is invalid so we cannot render it: box=%s text=%s",
line_box, self._get_element_text(line))
return
self._debug_draw_line_bbox(canvas, line_box) self._debug_draw_line_bbox(canvas, line_box)
+7 -1
View File
@@ -239,7 +239,13 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
# to do. Just pretend nothing happened, keep calm and carry on. # to do. Just pretend nothing happened, keep calm and carry on.
warn("PDF graphics stack underflowed - PDF may be malformed") warn("PDF graphics stack underflowed - PDF may be malformed")
elif operator == 'cm': elif operator == 'cm':
ctm = Matrix(operands) @ ctm try:
ctm = Matrix(operands) @ ctm
except ValueError:
raise InputFileError(
"PDF content stream is corrupt - this PDF is malformed. "
"Use a PDF editor that is capable of visually inspecting the PDF."
)
elif operator == 'Do': elif operator == 'Do':
image_name = operands[0] image_name = operands[0]
settings = XobjectSettings( settings = XobjectSettings(
+1 -1
View File
@@ -23,4 +23,4 @@ def test_semfree(resources, outpdf):
'--plugin', '--plugin',
'tests/plugins/tesseract_noop.py', 'tests/plugins/tesseract_noop.py',
) )
assert exitcode == ExitCode.ok assert exitcode in (ExitCode.ok, ExitCode.pdfa_conversion_failed)