Add stop on soft render errors and option to override

This commit is contained in:
James R. Barlow
2023-06-01 23:49:34 -07:00
parent 22d35c199d
commit e8ed510543
13 changed files with 68 additions and 20 deletions
+12 -1
View File
@@ -104,7 +104,7 @@ was requested, the preprocessed image layer will be inserted.
If you want to adjust the amount of time spent on OCR, change
``--tesseract-timeout``. You can also automatically skip images that
exceed a certain number of megapixels with ``--skip-big``. (A 300 DPI,
8.5×11" page is 8.4 megapixels.)
8.5×11" page image is 8.4 megapixels.)
.. code-block:: bash
@@ -241,6 +241,17 @@ PDF.js viewer.
This works in all versions of Tesseract.
Rendering and rasterizing options
=================================
.. versionadded:: 14.3.0
The ``--continue-on-soft-render-error`` option allows OCRmyPDF to
proceed if a page cannot be rasterized rendered. This is useful if you are
trying to get the best possible OCR from a PDF that is not well-formed,
and you are willing to accept some pages that may not visually match the
input, and that may not OCR well.
Return code policy
==================
+7 -11
View File
@@ -29,13 +29,9 @@ attack vectors.
In short, PDFs `may contain
viruses <https://security.stackexchange.com/questions/64052/can-a-pdf-file-contain-a-virus>`__.
This
`article <https://theinvisiblethings.blogspot.ca/2013/02/converting-untrusted-pdfs-into-trusted.html>`__
describes a high-paranoia method which allows potentially hostile PDFs
to be viewed and rasterized safely in a disposable virtual machine. A
trusted PDF created in this manner is converted to images and loses all
information making it searchable and losing all compression. OCRmyPDF
could be used to restore searchability.
If you do not trust a PDF or its source, do not open it or use OCRmyPDF
on it. Consider using a Docker container or virtual machine to isolate
an untrusted PDF from your system.
How OCRmyPDF processes PDFs
===========================
@@ -43,11 +39,11 @@ How OCRmyPDF processes PDFs
OCRmyPDF must open and interpret your PDF in order to insert an OCR
layer. First, it runs all PDFs through
`pikepdf <https://github.com/pikepdf/pikepdf>`__, a library based on
`qpdf <https://github.com/qpdf/qpdf>`__, a program that repairs PDFs
`QPDF <https://github.com/qpdf/qpdf>`__, a program that repairs PDFs
with syntax errors. This is done because, in the author's experience, a
significant number of PDFs in the wild, especially those created by
scanners, are not well-formed files. qpdf makes it more likely that
OCRmyPDF will succeed, but offers no security guarantees. qpdf is also
scanners, are not well-formed files. QPDF makes it more likely that
OCRmyPDF will succeed, but offers no security guarantees. QPDF is also
used to split the PDF into single page PDFs.
Finally, OCRmyPDF rasterizes each page of the PDF using
@@ -133,7 +129,7 @@ The author also provides professional services that include OCR and
building databases around PDFs, and is happy to provide consultation.
Abbyy Cloud OCR is viable commercial alternative with a web services
API. Amazon Textract, Google Cloud Vision, and Microsoft Azure
API. Amazon Textract, Google Cloud Vision, and Microsoft Azure
Computer Vision provide advanced OCR but have less PDF rendering capability.
Password protection, digital signatures and certification
+5 -1
View File
@@ -30,7 +30,7 @@ except AttributeError:
log = logging.getLogger(__name__)
# Most reliable what to get the bitness of Python interpreter, according to Python docs
# Most reliable way to get the bitness of Python interpreter, according to Python docs
_IS_64BIT = sys.maxsize > 2**32
_GSWIN = None
@@ -63,6 +63,7 @@ def rasterize_pdf(
page_dpi: Resolution | None = None,
rotation: int | None = None,
filter_vector: bool = False,
stop_on_error: bool = False,
):
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units."""
raster_dpi = raster_dpi.round(6)
@@ -83,6 +84,7 @@ def rasterize_pdf(
f'-r{raster_dpi.x:f}x{raster_dpi.y:f}',
]
+ (['-dFILTERVECTOR'] if filter_vector else [])
+ (['-dPDFSTOPONERROR'] if stop_on_error else [])
+ [
'-o',
'-',
@@ -161,6 +163,7 @@ def generate_pdfa(
pdf_version: str = '1.5',
pdfa_part: str = '2',
progressbar_class=None,
stop_on_error: bool = False,
):
# Ghostscript's compression is all or nothing. We can either force all images
# to JPEG, force all to Flate/PNG, or let it decide how to encode the images.
@@ -207,6 +210,7 @@ def generate_pdfa(
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=" + strategy,
]
+ (['-dPDFSTOPONERROR'] if stop_on_error else [])
+ compression_args
+ [
"-dJPEGQ=95",
+3
View File
@@ -343,6 +343,7 @@ def rasterize_preview(input_file: Path, page_context: PageContext) -> Path:
page_dpi=page_dpi,
rotation=0,
filter_vector=False,
stop_on_soft_error=not page_context.options.continue_on_soft_render_error,
)
return output_file
@@ -455,6 +456,7 @@ def rasterize(
pageno=pageinfo.pageno + 1,
rotation=correction,
filter_vector=remove_vectors,
stop_on_soft_error=not page_context.options.continue_on_soft_render_error,
)
return output_file
@@ -736,6 +738,7 @@ def convert_to_pdfa(input_pdf: Path, input_ps_stub: Path, context: PdfContext) -
if options.progress_bar
else None
),
stop_on_soft_error=not options.continue_on_soft_render_error,
)
return output_file
+1
View File
@@ -248,6 +248,7 @@ def ocr( # noqa: ruff: disable=D417
user_words: os.PathLike | None = None,
user_patterns: os.PathLike | None = None,
fast_web_view: float | None = None,
continue_on_soft_render_error: bool | None = None,
plugins: Iterable[StrPath] | None = None,
plugin_manager=None,
keep_temporary_files: bool | None = None,
@@ -45,6 +45,7 @@ def rasterize_pdf_page(
page_dpi,
rotation,
filter_vector,
stop_on_soft_error,
):
"""Rasterize a single page of a PDF file using Ghostscript."""
ghostscript.rasterize_pdf(
@@ -56,6 +57,7 @@ def rasterize_pdf_page(
page_dpi=page_dpi,
rotation=rotation,
filter_vector=filter_vector,
stop_on_error=stop_on_soft_error,
)
return output_file
@@ -69,6 +71,7 @@ def generate_pdfa(
pdf_version,
pdfa_part,
progressbar_class,
stop_on_soft_error,
):
"""Generate a PDF/A from the list of PDF pages and PDF/A metadata."""
ghostscript.generate_pdfa(
@@ -78,5 +81,6 @@ def generate_pdfa(
pdf_version=pdf_version,
pdfa_part=pdfa_part,
progressbar_class=progressbar_class,
stop_on_error=stop_on_soft_error,
)
return output_file
+8
View File
@@ -420,6 +420,14 @@ Online documentation is located at:
"which do not benefit. If the threshold is 0 it will be apply to all files. "
"Set the threshold very high to disable.",
)
advanced.add_argument(
'--continue-on-soft-render-error',
action='store_true',
help="Continue processing pages after a recoverable PDF rendering error. "
"A recoverable error is one that does not prevent the page from being "
"rendered, but may result in visual differences compared to the input "
"file. Missing fonts are a typical source of these errors.",
)
advanced.add_argument(
'--plugin',
dest='plugins',
+22 -7
View File
@@ -203,6 +203,7 @@ def rasterize_pdf_page(
page_dpi: Resolution | None,
rotation: int | None,
filter_vector: bool,
stop_on_soft_error: bool,
) -> Path:
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units.
@@ -213,19 +214,26 @@ def rasterize_pdf_page(
Args:
input_file: The PDF to rasterize.
output_file: The desired name of the rasterized image.
raster_device: Type of image to produce at output_file
raster_dpi: Resolution at which to rasterize page
pageno: Page number to rasterize (beginning at page 1)
page_dpi: Resolution, overriding output image DPI
rotation: Cardinal angle, clockwise, to rotate page
filter_vector: If True, remove vector graphics objects
raster_device: Type of image to produce at output_file.
raster_dpi: Resolution in dots per inch at which to rasterize page.
pageno: Page number to rasterize (beginning at page 1).
page_dpi: Resolution, overriding output image DPI.
rotation: Cardinal angle, clockwise, to rotate page.
filter_vector: If True, remove vector graphics objects.
stop_on_soft_error: If there is an "soft error" such that PDF page image
generation can proceed, but may visually differ from the original,
the implementer of this hook should raise a detailed exception. If
``False``, continue processing and report by logging it. If the hook
cannot proceed, it should always raise an exception, regardless of
this setting. One "soft error" would be a missing font that is
required to properly rasterize the PDF.
Returns:
Path: output_file if successful
Note:
This hook will be called from child processes. Modifying global state
will not affect the main process or other child processes.
Note:
This is a :ref:`firstresult hook<firstresult>`.
"""
@@ -462,6 +470,7 @@ def generate_pdfa(
pdf_version: str,
pdfa_part: str,
progressbar_class,
stop_on_soft_error: bool,
) -> Path:
"""Generate a PDF/A.
@@ -492,6 +501,12 @@ def generate_pdfa(
and the name of the work units ("page"). Then ``instance.update()``
will be called when a work unit is completed. If ``None``, no
progress information is reported.
stop_on_soft_error: If there is an "soft error" such that PDF/A generation
can proceed and produce a valid PDF/A, but output may be invalid or
may not visually resemble the original, the implementer of this hook
should raise a detailed exception. If ``False``, continue processing
and report by logging it. If the hook cannot proceed, it should always
raise an exception, regardless of this setting.
Returns:
Path: If successful, the hook should return ``output_file``.
+1
View File
@@ -31,6 +31,7 @@ def generate_pdfa(pdf_pages, pdfmark, output_file, compression, pdf_version, pdf
pdf_version=pdf_version,
pdfa_part=pdfa_part,
progressbar_class=None,
stop_on_soft_error=True,
)
mock.assert_called_once()
return output_file
+1
View File
@@ -33,6 +33,7 @@ def generate_pdfa(pdf_pages, pdfmark, output_file, compression, pdf_version, pdf
pdf_version=pdf_version,
pdfa_part=pdfa_part,
progressbar_class=None,
stop_on_soft_error=True,
)
mock.assert_called()
return output_file
+1
View File
@@ -39,6 +39,7 @@ def rasterize_pdf_page(
page_dpi=page_dpi,
rotation=rotation,
filter_vector=filter_vector,
stop_on_soft_error=True,
)
mock.assert_called()
return output_file
+1
View File
@@ -28,6 +28,7 @@ def generate_pdfa(pdf_pages, pdfmark, output_file, compression, pdf_version, pdf
pdf_version=pdf_version,
pdfa_part=pdfa_part,
progressbar_class=None,
stop_on_soft_error=True,
)
mock.assert_called()
return output_file
+2
View File
@@ -275,6 +275,7 @@ def test_rasterize_rotates(resources, tmp_path):
pageno=1,
rotation=90,
filter_vector=False,
stop_on_soft_error=True,
)
with Image.open(img) as im:
assert im.size == (83, 200), "Image not rotated"
@@ -289,6 +290,7 @@ def test_rasterize_rotates(resources, tmp_path):
pageno=1,
rotation=180,
filter_vector=False,
stop_on_soft_error=True,
)
assert Image.open(img).size == (200, 83), "Image not rotated"