diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index 43031e73..44bd88c9 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -33,6 +33,7 @@ from ocrmypdf.exceptions import ( EncryptedPdfError, InputFileError, PriorOcrFoundError, + TaggedPDFError, UnsupportedImageFormatError, ) from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink @@ -218,6 +219,16 @@ def validate_pdfinfo_options(context: PdfContext) -> None: "form and all filled form fields. The output PDF will be " "'flattened' and will no longer be fillable." ) + if pdfinfo.is_tagged: + if options.force_ocr or options.skip_text or options.redo_ocr: + log.warning( + "This PDF is marked as a Tagged PDF. This often indicates " + "that the PDF was generated from an office document and does " + "not need OCR. PDF pages processed by OCRmyPDF may not be " + "tagged correctly." + ) + else: + raise TaggedPDFError() context.plugin_manager.hook.validate(pdfinfo=pdfinfo, options=options) diff --git a/src/ocrmypdf/exceptions.py b/src/ocrmypdf/exceptions.py index 7c1a6414..6f549286 100644 --- a/src/ocrmypdf/exceptions.py +++ b/src/ocrmypdf/exceptions.py @@ -108,10 +108,16 @@ class EncryptedPdfError(ExitCodeException): ) -class DigitalSignatureError(ExitCodeException): +class TesseractConfigError(ExitCodeException): + """Tesseract config can't be parsed.""" + + exit_code = ExitCode.invalid_config + message = "Error occurred while parsing a Tesseract configuration file" + + +class DigitalSignatureError(InputFileError): """PDF has a digital signature.""" - exit_code = ExitCode.input_file message = dedent( """\ Input PDF has a digital signature. OCR would alter the document, @@ -120,8 +126,14 @@ class DigitalSignatureError(ExitCodeException): ) -class TesseractConfigError(ExitCodeException): - """Tesseract config can't be parsed.""" +class TaggedPDFError(InputFileError): + """PDF is tagged.""" - exit_code = ExitCode.invalid_config - message = "Error occurred while parsing a Tesseract configuration file" + message = dedent( + """\ + This PDF is marked as a Tagged PDF. This often indicates + that the PDF was generated from an office document and does + not need OCR. Use --force-ocr, --skip-text or --redo-ocr to + override this error. + """ + ) diff --git a/src/ocrmypdf/pdfinfo/info.py b/src/ocrmypdf/pdfinfo/info.py index a14656c8..472967ab 100644 --- a/src/ocrmypdf/pdfinfo/info.py +++ b/src/ocrmypdf/pdfinfo/info.py @@ -1038,7 +1038,11 @@ DEFAULT_EXECUTOR = SerialExecutor() class PdfInfo: - """Get summary information about a PDF.""" + """Extract summary information about a PDF without retaining the PDF itself. + + Crucially this lets us get the information in a pure Python format so that + it can be pickled and passed to a worker process. + """ _has_acroform: bool = False _has_signature: bool = False @@ -1078,6 +1082,9 @@ class PdfInfo: elif 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( + pdf.Root.get(Name.MarkInfo, {}).get(Name.Marked, False) + ) @property def pages(self) -> Sequence[PageInfo | None]: @@ -1105,6 +1112,11 @@ class PdfInfo: """Return True if the document annotations has a digital signature.""" return self._has_signature + @property + def is_tagged(self) -> bool: + """Return True if the document catalog indicates this is a Tagged PDF.""" + return self._is_tagged + @property def filename(self) -> str | Path: """Return filename of PDF.""" diff --git a/tests/resources/tagged.odt b/tests/resources/tagged.odt new file mode 100644 index 00000000..1ef80969 Binary files /dev/null and b/tests/resources/tagged.odt differ diff --git a/tests/resources/tagged.pdf b/tests/resources/tagged.pdf new file mode 100644 index 00000000..574cfeaa Binary files /dev/null and b/tests/resources/tagged.pdf differ diff --git a/tests/test_tagged.py b/tests/test_tagged.py new file mode 100644 index 00000000..e36709ae --- /dev/null +++ b/tests/test_tagged.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2023 James R. Barlow +# SPDX-License-Identifier: MPL-2.0 + +from __future__ import annotations + +import argparse + +import pytest + +import ocrmypdf + + +def test_block_tagged(resources): + with pytest.raises(ocrmypdf.exceptions.TaggedPDFError): + ocrmypdf.ocr(resources / 'tagged.pdf', '_.pdf') + + +def test_force_tagged_warns(resources, outpdf, caplog): + caplog.set_level('WARNING') + ocrmypdf.ocr( + resources / 'tagged.pdf', + outpdf, + force_ocr=True, + plugins=['tests/plugins/tesseract_noop.py'], + ) + assert 'marked as a Tagged PDF' in caplog.text