Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f255723e8d | ||
|
|
88b6c3df8f | ||
|
|
190bfe8859 | ||
|
|
0cc23b999c | ||
|
|
ce930aa16e | ||
|
|
1ce6c8daf3 | ||
|
|
a9cd5bf253 | ||
|
|
8102ca1075 | ||
|
|
6b5934ff4e | ||
|
|
e38b30af1c | ||
|
|
f64712322e | ||
|
|
1d09061130 | ||
|
|
a2203b2447 |
@@ -10,6 +10,26 @@ The OCRmyPDF package itself does not contain a public API, although it is fairly
|
|||||||
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
|
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
|
||||||
|
|
||||||
|
|
||||||
|
v6.2.5
|
||||||
|
------
|
||||||
|
|
||||||
|
- Backport compatibility fixes for Tesseract 4.0.0-rcN from v7.2.0
|
||||||
|
|
||||||
|
|
||||||
|
v6.2.4
|
||||||
|
------
|
||||||
|
|
||||||
|
- Backport Ghostscript 9.25 compatibility fixes, which removes support for setting Unicode metadata
|
||||||
|
- Backport blacklisting Ghostscript 9.24
|
||||||
|
- Older versions of Ghostscript are still supported
|
||||||
|
|
||||||
|
|
||||||
|
v6.2.3
|
||||||
|
------
|
||||||
|
|
||||||
|
- Fix compatibility with img2pdf >= 0.3.0 by rejecting input images that have an alpha channel
|
||||||
|
|
||||||
|
|
||||||
v6.2.2
|
v6.2.2
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|||||||
+31
-32
@@ -587,9 +587,27 @@ def do_ruffus_exception(ruffus_five_tuple, options, log):
|
|||||||
description of the error message that occurred."""
|
description of the error message that occurred."""
|
||||||
exit_code = None
|
exit_code = None
|
||||||
|
|
||||||
task_name, job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
|
_task_name, _job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
|
||||||
job_name = job_name # unused
|
|
||||||
if exc_name == 'builtins.SystemExit':
|
if isinstance(exc_name, type):
|
||||||
|
# ruffus is full of mystery... sometimes (probably when the process
|
||||||
|
# group leader is killed) exc_name is the class object of the exception,
|
||||||
|
# rather than a str. So reach into the object and get its name.
|
||||||
|
exc_name = exc_name.__name__
|
||||||
|
|
||||||
|
if exc_name.startswith('ocrmypdf.exceptions.'):
|
||||||
|
base_exc_name = exc_name.replace('ocrmypdf.exceptions.', '')
|
||||||
|
exc_class = getattr(ocrmypdf_exceptions, base_exc_name)
|
||||||
|
exit_code = getattr(exc_class, 'exit_code', ExitCode.other_error)
|
||||||
|
try:
|
||||||
|
if isinstance(exc_value, exc_class):
|
||||||
|
exc_msg = str(exc_value)
|
||||||
|
else:
|
||||||
|
exc_msg = str(exc_class())
|
||||||
|
except Exception:
|
||||||
|
exc_msg = "Unknown"
|
||||||
|
|
||||||
|
if exc_name in ('builtins.SystemExit', 'SystemExit'):
|
||||||
match = re.search(r"\.(.+?)\)", exc_value)
|
match = re.search(r"\.(.+?)\)", exc_value)
|
||||||
exit_code_name = match.groups()[0]
|
exit_code_name = match.groups()[0]
|
||||||
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
|
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
|
||||||
@@ -611,36 +629,9 @@ def do_ruffus_exception(ruffus_five_tuple, options, log):
|
|||||||
msg = "Error occurred while running this command:"
|
msg = "Error occurred while running this command:"
|
||||||
log.error(msg + '\n' + exc_value)
|
log.error(msg + '\n' + exc_value)
|
||||||
exit_code = ExitCode.child_process_error
|
exit_code = ExitCode.child_process_error
|
||||||
elif (exc_name == 'PyPDF2.utils.PdfReadError' and \
|
|
||||||
'not been decrypted' in exc_value) or \
|
|
||||||
(exc_name == 'ocrmypdf.exceptions.EncryptedPdfError'):
|
|
||||||
log.error(textwrap.dedent("""\
|
|
||||||
Input PDF is encrypted. The encryption must be removed to
|
|
||||||
perform OCR.
|
|
||||||
|
|
||||||
For information about this PDF's security use
|
|
||||||
qpdf --show-encryption infilename
|
|
||||||
|
|
||||||
You can remove the encryption using
|
|
||||||
qpdf --decrypt [--password=[password]] infilename
|
|
||||||
|
|
||||||
"""))
|
|
||||||
exit_code = ExitCode.encrypted_pdf
|
|
||||||
elif exc_name == 'ocrmypdf.exceptions.PdfMergeFailedError':
|
|
||||||
log.error(textwrap.dedent("""\
|
|
||||||
Failed to merge PDF image layer with OCR layer
|
|
||||||
|
|
||||||
Usually this happens because the input PDF file is mal-formed and
|
|
||||||
ocrmypdf cannot automatically correct the problem on its own.
|
|
||||||
|
|
||||||
Try using
|
|
||||||
ocrmypdf --pdf-renderer tesseract [..other args..]
|
|
||||||
"""))
|
|
||||||
exit_code = ExitCode.input_file
|
|
||||||
elif exc_name.startswith('ocrmypdf.exceptions.'):
|
elif exc_name.startswith('ocrmypdf.exceptions.'):
|
||||||
base_exc_name = exc_name.replace('ocrmypdf.exceptions.', '')
|
if exc_msg:
|
||||||
exc_class = getattr(ocrmypdf_exceptions, base_exc_name)
|
log.error(exc_msg)
|
||||||
exit_code = exc_class.exit_code
|
|
||||||
elif exc_name == 'PIL.Image.DecompressionBombError':
|
elif exc_name == 'PIL.Image.DecompressionBombError':
|
||||||
msg = cleanup_ruffus_error_message(exc_value)
|
msg = cleanup_ruffus_error_message(exc_value)
|
||||||
msg += ("\nUse the --max-image-mpixels argument to set increase the "
|
msg += ("\nUse the --max-image-mpixels argument to set increase the "
|
||||||
@@ -863,6 +854,14 @@ def run_pipeline():
|
|||||||
"security vulnerabilities with certain malformed PDFs. Consider "
|
"security vulnerabilities with certain malformed PDFs. Consider "
|
||||||
"upgrading to version 7.0.0 or newer.".format(qpdf.version()))
|
"upgrading to version 7.0.0 or newer.".format(qpdf.version()))
|
||||||
|
|
||||||
|
if ghostscript.version() == '9.24':
|
||||||
|
complain(
|
||||||
|
"Ghostscript 9.24 contains serious regressions and is not "
|
||||||
|
"supported. Please upgrade to Ghostscript 9.25 or use an older "
|
||||||
|
"version."
|
||||||
|
)
|
||||||
|
return ExitCode.missing_dependency
|
||||||
|
|
||||||
# Any changes to options will not take effect for options that are already
|
# Any changes to options will not take effect for options that are already
|
||||||
# bound to function parameters in the pipeline. (For example
|
# bound to function parameters in the pipeline. (For example
|
||||||
# options.input_file, options.pdf_renderer are already bound.)
|
# options.input_file, options.pdf_renderer are already bound.)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
class ExitCode(IntEnum):
|
class ExitCode(IntEnum):
|
||||||
ok = 0
|
ok = 0
|
||||||
@@ -35,6 +36,13 @@ class ExitCode(IntEnum):
|
|||||||
|
|
||||||
class ExitCodeException(Exception):
|
class ExitCodeException(Exception):
|
||||||
exit_code = ExitCode.other_error
|
exit_code = ExitCode.other_error
|
||||||
|
message = ""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
super_msg = super().__str__() # Don't do str(super())
|
||||||
|
if self.message:
|
||||||
|
return self.message.format(super_msg)
|
||||||
|
return super_msg
|
||||||
|
|
||||||
|
|
||||||
class BadArgsError(ExitCodeException):
|
class BadArgsError(ExitCodeException):
|
||||||
@@ -43,7 +51,15 @@ class BadArgsError(ExitCodeException):
|
|||||||
|
|
||||||
class PdfMergeFailedError(ExitCodeException):
|
class PdfMergeFailedError(ExitCodeException):
|
||||||
exit_code = ExitCode.input_file
|
exit_code = ExitCode.input_file
|
||||||
|
message = dedent('''\
|
||||||
|
Failed to merge PDF image layer with OCR layer
|
||||||
|
|
||||||
|
Usually this happens because the input PDF file is malformed and
|
||||||
|
ocrmypdf cannot automatically correct the problem on its own.
|
||||||
|
|
||||||
|
Try using
|
||||||
|
ocrmypdf --pdf-renderer sandwich [..other args..]
|
||||||
|
''')
|
||||||
|
|
||||||
class MissingDependencyError(ExitCodeException):
|
class MissingDependencyError(ExitCodeException):
|
||||||
exit_code = ExitCode.missing_dependency
|
exit_code = ExitCode.missing_dependency
|
||||||
@@ -75,7 +91,18 @@ class SubprocessOutputError(ExitCodeException):
|
|||||||
|
|
||||||
class EncryptedPdfError(ExitCodeException):
|
class EncryptedPdfError(ExitCodeException):
|
||||||
exit_code = ExitCode.encrypted_pdf
|
exit_code = ExitCode.encrypted_pdf
|
||||||
|
message = dedent('''\
|
||||||
|
Input PDF is encrypted. The encryption must be removed to
|
||||||
|
perform OCR.
|
||||||
|
|
||||||
|
For information about this PDF's security use
|
||||||
|
qpdf --show-encryption infilename
|
||||||
|
|
||||||
|
You can remove the encryption using
|
||||||
|
qpdf --decrypt [--password=[password]] infilename
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
class TesseractConfigError(ExitCodeException):
|
class TesseractConfigError(ExitCodeException):
|
||||||
exit_code = ExitCode.invalid_config
|
exit_code = ExitCode.invalid_config
|
||||||
|
message = "Error occurred while parsing a Tesseract configuration file"
|
||||||
|
|||||||
@@ -34,12 +34,20 @@ def version():
|
|||||||
|
|
||||||
def jpeg_passthrough_available():
|
def jpeg_passthrough_available():
|
||||||
"""
|
"""
|
||||||
Ghostscript 9.23 introduced JPEG passthrough but it seems to corrupt the
|
Returns True if the installed version of Ghostscript supports JPEG passthru
|
||||||
last two bytes of certain images, for now we disable it for 9.23 and
|
|
||||||
do not mention it for < 9.23.
|
|
||||||
|
|
||||||
|
Prior to 9.23, Ghostscript decode and re-encoded JPEGs internally. In 9.23
|
||||||
|
it gained the ability to keep JPEGs unmodified. However, the 9.23
|
||||||
|
implementation was buggy and would deletes the last two bytes of images in
|
||||||
|
some cases, as reported here.
|
||||||
https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
||||||
|
|
||||||
|
The issue was fixed for 9.24, hence that is the first version we consider
|
||||||
|
the feature available. (However, we don't use 9.24 at all, so the first
|
||||||
|
version that allows JPEG passthrough is 9.25.
|
||||||
|
|
||||||
|
Regardless, in ocrmypdf 6.x we are ignoring this new feature entirely to
|
||||||
|
avoid new behavior.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -144,10 +152,12 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
|
|||||||
# git commit fe1c025d.
|
# git commit fe1c025d.
|
||||||
strategy = 'RGB' if version() >= '9.19' else '/RGB'
|
strategy = 'RGB' if version() >= '9.19' else '/RGB'
|
||||||
|
|
||||||
if version() == '9.23':
|
if version() >= '9.23':
|
||||||
# 9.23: new feature JPEG passthrough is broken in some cases, best to
|
# 9.23: new feature JPEG passthrough is broken in some cases, best to
|
||||||
# disable it always
|
# disable it always
|
||||||
# https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
# https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
||||||
|
# fixed in 9.24, but to avoid changing expected behavior we disable it
|
||||||
|
# for ocrmypdf 6.x
|
||||||
compression_args.append('-dPassThroughJPEGImages=false')
|
compression_args.append('-dPassThroughJPEGImages=false')
|
||||||
|
|
||||||
with NamedTemporaryFile(delete=True) as gs_pdf:
|
with NamedTemporaryFile(delete=True) as gs_pdf:
|
||||||
|
|||||||
@@ -191,6 +191,14 @@ def tesseract_log_output(log, stdout, input_file):
|
|||||||
log.warning(prefix + "lots of diacritics - possibly poor OCR")
|
log.warning(prefix + "lots of diacritics - possibly poor OCR")
|
||||||
elif line.startswith('OSD: Weak margin'):
|
elif line.startswith('OSD: Weak margin'):
|
||||||
log.warning(prefix + "unsure about page orientation")
|
log.warning(prefix + "unsure about page orientation")
|
||||||
|
elif 'Error in pixScanForForeground' in line:
|
||||||
|
pass # Appears to be spurious/problem with nonwhite borders
|
||||||
|
elif 'Error in boxClipToRectangle' in line:
|
||||||
|
pass # Always appears with pixScanForForeground message
|
||||||
|
elif 'parameter not found: ' in line.lower():
|
||||||
|
log.error(prefix + line.strip())
|
||||||
|
problem = line.split('found: ')[1]
|
||||||
|
raise TesseractConfigError(problem)
|
||||||
elif 'error' in line.lower() or 'exception' in line.lower():
|
elif 'error' in line.lower() or 'exception' in line.lower():
|
||||||
log.error(prefix + line.strip())
|
log.error(prefix + line.strip())
|
||||||
elif 'warning' in line.lower():
|
elif 'warning' in line.lower():
|
||||||
@@ -264,8 +272,6 @@ def generate_hocr(input_file, output_files, language: list, engine_mode,
|
|||||||
_generate_null_hocr(output_hocr, output_sidecar, input_file)
|
_generate_null_hocr(output_hocr, output_sidecar, input_file)
|
||||||
except CalledProcessError as e:
|
except CalledProcessError as e:
|
||||||
tesseract_log_output(log, e.output, input_file)
|
tesseract_log_output(log, e.output, input_file)
|
||||||
if b'read_params_file: parameter not found' in e.output:
|
|
||||||
raise TesseractConfigError() from e
|
|
||||||
if b'Image too large' in e.output:
|
if b'Image too large' in e.output:
|
||||||
_generate_null_hocr(output_hocr, output_sidecar, input_file)
|
_generate_null_hocr(output_hocr, output_sidecar, input_file)
|
||||||
return
|
return
|
||||||
@@ -362,9 +368,6 @@ def generate_pdf(*, input_image, skip_pdf, output_pdf, output_text,
|
|||||||
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
|
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
|
||||||
except CalledProcessError as e:
|
except CalledProcessError as e:
|
||||||
tesseract_log_output(log, e.output, input_image)
|
tesseract_log_output(log, e.output, input_image)
|
||||||
if b'read_params_file: parameter not found' in e.output:
|
|
||||||
raise TesseractConfigError() from e
|
|
||||||
|
|
||||||
if b'Image too large' in e.output:
|
if b'Image too large' in e.output:
|
||||||
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
|
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from contextlib import suppress, contextmanager
|
from contextlib import suppress, contextmanager
|
||||||
|
|||||||
+71
-13
@@ -15,7 +15,21 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# Generate a PDFA_def.ps file for Ghostscript >= 9.14
|
"""
|
||||||
|
Generate a PDFMARK file for Ghostscript >= 9.14, for PDF/A conversion
|
||||||
|
|
||||||
|
pdfmark is an extension to the Postscript language that describes some PDF
|
||||||
|
features like bookmarks and annotations. It was originally specified Adobe
|
||||||
|
Distiller, for Postscript to PDF conversion:
|
||||||
|
https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
|
||||||
|
|
||||||
|
Ghostscript uses pdfmark for PDF to PDF/A conversion as well. To use Ghostscript
|
||||||
|
to create a PDF/A, we need to create a pdfmark file with the necessary metadata.
|
||||||
|
|
||||||
|
This takes care of the many version-specific bugs and pecularities in
|
||||||
|
Ghostscript's handling of pdfmark.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
from string import Template
|
from string import Template
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
@@ -78,7 +92,8 @@ def
|
|||||||
|
|
||||||
|
|
||||||
def encode_text_string(s: str) -> str:
|
def encode_text_string(s: str) -> str:
|
||||||
'''Encode text string to hex string for use in a PDF
|
"""
|
||||||
|
Encode text string to hex string for use in a PDF
|
||||||
|
|
||||||
From PDF 32000-1:2008 a string object may be included in hexademical form
|
From PDF 32000-1:2008 a string object may be included in hexademical form
|
||||||
if it is enclosed in angle brackets. For general Unicode the string should
|
if it is enclosed in angle brackets. For general Unicode the string should
|
||||||
@@ -86,7 +101,7 @@ def encode_text_string(s: str) -> str:
|
|||||||
ASCII strings could be encoded as PdfDocEncoding literals provided
|
ASCII strings could be encoded as PdfDocEncoding literals provided
|
||||||
that certain Postscript sequences are escaped. But it's far simpler to
|
that certain Postscript sequences are escaped. But it's far simpler to
|
||||||
encode everything as UTF-16.
|
encode everything as UTF-16.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# Sometimes lazy C programmers leave their NULs at the end of strings they
|
# Sometimes lazy C programmers leave their NULs at the end of strings they
|
||||||
# insert into PDFs
|
# insert into PDFs
|
||||||
@@ -102,8 +117,28 @@ def encode_text_string(s: str) -> str:
|
|||||||
return ascii_hex_str
|
return ascii_hex_str
|
||||||
|
|
||||||
|
|
||||||
|
def _encode_ascii(s: str) -> str:
|
||||||
|
"""
|
||||||
|
Aggressively strip non-ASCII and PDF escape sequences
|
||||||
|
|
||||||
|
Ghostscript 9.24+ lost support for UTF-16BE in pdfmark files for reasons
|
||||||
|
given in GhostPDL commit e997c683. Our temporary workaround is use ASCII
|
||||||
|
and drop all non-ASCII characters. A slightly improved alternative would
|
||||||
|
be to implement PdfDocEncoding in pikepdf and encode to that, or handle
|
||||||
|
metadata there.
|
||||||
|
"""
|
||||||
|
trans = str.maketrans({
|
||||||
|
'(': '',
|
||||||
|
')': '',
|
||||||
|
'\\': '',
|
||||||
|
'\0': ''
|
||||||
|
})
|
||||||
|
return s.translate(trans).encode('ascii', errors='replace').decode()
|
||||||
|
|
||||||
|
|
||||||
def encode_pdf_date(d: datetime) -> str:
|
def encode_pdf_date(d: datetime) -> str:
|
||||||
"""Encode Python datetime object as PDF date string
|
"""
|
||||||
|
Encode Python datetime object as PDF date string
|
||||||
|
|
||||||
From Adobe pdfmark manual:
|
From Adobe pdfmark manual:
|
||||||
(D:YYYYMMDDHHmmSSOHH'mm')
|
(D:YYYYMMDDHHmmSSOHH'mm')
|
||||||
@@ -137,6 +172,13 @@ def encode_pdf_date(d: datetime) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def decode_pdf_date(s: str) -> datetime:
|
def decode_pdf_date(s: str) -> datetime:
|
||||||
|
"""
|
||||||
|
Decode a pdfmark date to a Python datetime object
|
||||||
|
|
||||||
|
A pdfmark date is a string in a paritcular format. See the pdfmark
|
||||||
|
Reference for the specification.
|
||||||
|
|
||||||
|
"""
|
||||||
if s.startswith('D:'):
|
if s.startswith('D:'):
|
||||||
s = s[2:]
|
s = s[2:]
|
||||||
|
|
||||||
@@ -178,24 +220,41 @@ def _get_pdfmark_dates(pdfmark):
|
|||||||
yield ' {} null'.format(key)
|
yield ' {} null'.format(key)
|
||||||
|
|
||||||
|
|
||||||
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
|
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark, ascii_docinfo=False):
|
||||||
"""Create a Postscript file for Ghostscript. pdfmark contains the various
|
"""Create a Postscript pdfmark file for Ghostscript.
|
||||||
objects as strings; these must be encoded in ASCII, and dates have a
|
|
||||||
special format."""
|
pdfmark contains the various objects as strings; these must be encoded in
|
||||||
|
ASCII, and dates have a special format.
|
||||||
|
|
||||||
|
:param icc_profile: filename of the ICC profile to include in pdfmark
|
||||||
|
:param icc_identifier: ICC identifier such as 'sRGB'
|
||||||
|
:param pdfmark: a dictionary containing keys to include the pdfmark
|
||||||
|
:param ascii_docinfo: if True, the docinfo block must be encoded in pure
|
||||||
|
ASCII and may not contain UTF-16BE-BOM-hex encoded strings, as
|
||||||
|
required for Ghostscript 9.24+
|
||||||
|
|
||||||
|
:returns: a string containing the entire pdfmark
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
# Ghostscript <= 9.21 has a bug where null entries in DOCINFO might produce
|
# Ghostscript <= 9.21 has a bug where null entries in DOCINFO might produce
|
||||||
# ERROR: VMerror (-25) on closing pdfwrite device.
|
# ERROR: VMerror (-25) on closing pdfwrite device.
|
||||||
# https://bugs.ghostscript.com/show_bug.cgi?id=697684
|
# https://bugs.ghostscript.com/show_bug.cgi?id=697684
|
||||||
# Work around this by only adding keys that have a nontrivial value
|
# Work around this by only adding keys that have a nontrivial value
|
||||||
docinfo_keys = ('/Title', '/Author', '/Subject', '/Creator', '/Keywords')
|
docinfo_keys = ('/Title', '/Author', '/Subject', '/Creator', '/Keywords')
|
||||||
docinfo_line_template = ' {key} <{value}>'
|
|
||||||
|
|
||||||
def docinfo_gen():
|
def docinfo_gen():
|
||||||
|
if not ascii_docinfo:
|
||||||
|
docinfo_line_template = ' {key} <{value}>'
|
||||||
|
encode = encode_text_string
|
||||||
|
else:
|
||||||
|
docinfo_line_template = ' {key} ({value})'
|
||||||
|
encode = _encode_ascii
|
||||||
yield from _get_pdfmark_dates(pdfmark)
|
yield from _get_pdfmark_dates(pdfmark)
|
||||||
for key in docinfo_keys:
|
for key in docinfo_keys:
|
||||||
if key in pdfmark and pdfmark[key].strip() != '':
|
if key in pdfmark and pdfmark[key].strip() != '':
|
||||||
line = docinfo_line_template.format(
|
line = docinfo_line_template.format(
|
||||||
key=key, value=encode_text_string(pdfmark[key]))
|
key=key, value=encode(pdfmark[key]))
|
||||||
yield line
|
yield line
|
||||||
docinfo = '\n'.join(docinfo_gen())
|
docinfo = '\n'.join(docinfo_gen())
|
||||||
|
|
||||||
@@ -206,13 +265,13 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB'):
|
def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
|
||||||
if icc == 'sRGB':
|
if icc == 'sRGB':
|
||||||
icc_profile = SRGB_ICC_PROFILE
|
icc_profile = SRGB_ICC_PROFILE
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Only supporting sRGB")
|
raise NotImplementedError("Only supporting sRGB")
|
||||||
|
|
||||||
ps = _get_pdfa_def(icc_profile, icc, pdfmark)
|
ps = _get_pdfa_def(icc_profile, icc, pdfmark, ascii_docinfo=ascii_docinfo)
|
||||||
|
|
||||||
# We should have encoded everything to pure ASCII by this point, and
|
# We should have encoded everything to pure ASCII by this point, and
|
||||||
# to be safe, only allow ASCII in PostScript
|
# to be safe, only allow ASCII in PostScript
|
||||||
@@ -263,4 +322,3 @@ def file_claims_pdfa(filename):
|
|||||||
pdfa_dict['conformance'] = conformance
|
pdfa_dict['conformance'] = conformance
|
||||||
|
|
||||||
return pdfa_dict
|
return pdfa_dict
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from contextlib import suppress
|
|||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from io import BytesIO
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -144,6 +145,13 @@ def triage_image_file(input_file, output_file, log, options):
|
|||||||
"image was scanned and specify it using --image-dpi.")
|
"image was scanned and specify it using --image-dpi.")
|
||||||
raise DpiError()
|
raise DpiError()
|
||||||
|
|
||||||
|
if im.mode in ('RGBA', 'LA'):
|
||||||
|
log.error(
|
||||||
|
"The input image has an alpha channel. Remove the alpha "
|
||||||
|
"channel first."
|
||||||
|
)
|
||||||
|
raise UnsupportedImageFormatError()
|
||||||
|
|
||||||
if 'iccprofile' not in im.info:
|
if 'iccprofile' not in im.info:
|
||||||
if im.mode == 'RGB':
|
if im.mode == 'RGB':
|
||||||
log.info('Input image has no ICC profile, assuming sRGB')
|
log.info('Input image has no ICC profile, assuming sRGB')
|
||||||
@@ -918,7 +926,20 @@ def generate_postscript_stub(
|
|||||||
options = context.get_options()
|
options = context.get_options()
|
||||||
pdf = pypdf.PdfFileReader(input_file)
|
pdf = pypdf.PdfFileReader(input_file)
|
||||||
pdfmark = get_pdfmark(pdf, options)
|
pdfmark = get_pdfmark(pdf, options)
|
||||||
generate_pdfa_ps(output_file, pdfmark)
|
|
||||||
|
ascii_docinfo = False
|
||||||
|
if ghostscript.version() >= '9.24':
|
||||||
|
ascii_docinfo = True
|
||||||
|
try:
|
||||||
|
for v in pdfmark.values():
|
||||||
|
v.encode('ascii', errors='strict')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
log.warning(
|
||||||
|
"Ghostscript 9.24+ does not support Unicode strings in "
|
||||||
|
" metadata. These will be converted to ASCII if possible."
|
||||||
|
)
|
||||||
|
|
||||||
|
generate_pdfa_ps(output_file, pdfmark, ascii_docinfo=ascii_docinfo)
|
||||||
|
|
||||||
|
|
||||||
def skip_page(
|
def skip_page(
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 147 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
+10
-11
@@ -734,7 +734,7 @@ THIS FILE IS INVALID
|
|||||||
resources / 'ccitt.pdf', outdir / 'out.pdf',
|
resources / 'ccitt.pdf', outdir / 'out.pdf',
|
||||||
'--pdf-renderer', renderer,
|
'--pdf-renderer', renderer,
|
||||||
'--tesseract-config', cfg_file)
|
'--tesseract-config', cfg_file)
|
||||||
assert "parameter not found" in err, "No error message"
|
assert "parameter not found" in err.lower(), "No error message"
|
||||||
assert p.returncode == ExitCode.invalid_config
|
assert p.returncode == ExitCode.invalid_config
|
||||||
|
|
||||||
|
|
||||||
@@ -829,6 +829,7 @@ def test_no_contents(spoof_tesseract_noop, resources, outpdf):
|
|||||||
@pytest.mark.parametrize('image', [
|
@pytest.mark.parametrize('image', [
|
||||||
'baiona.png',
|
'baiona.png',
|
||||||
'baiona_gray.png',
|
'baiona_gray.png',
|
||||||
|
'baiona_alpha.png',
|
||||||
'congress.jpg'
|
'congress.jpg'
|
||||||
])
|
])
|
||||||
def test_compression_preserved(spoof_tesseract_noop, ocrmypdf_exec,
|
def test_compression_preserved(spoof_tesseract_noop, ocrmypdf_exec,
|
||||||
@@ -839,7 +840,6 @@ def test_compression_preserved(spoof_tesseract_noop, ocrmypdf_exec,
|
|||||||
output_file = str(outpdf)
|
output_file = str(outpdf)
|
||||||
|
|
||||||
im = Image.open(input_file)
|
im = Image.open(input_file)
|
||||||
|
|
||||||
# Runs: ocrmypdf - output.pdf < testfile
|
# Runs: ocrmypdf - output.pdf < testfile
|
||||||
with open(input_file, 'rb') as input_stream:
|
with open(input_file, 'rb') as input_stream:
|
||||||
p_args = ocrmypdf_exec + [
|
p_args = ocrmypdf_exec + [
|
||||||
@@ -849,7 +849,12 @@ def test_compression_preserved(spoof_tesseract_noop, ocrmypdf_exec,
|
|||||||
stdin=input_stream, env=spoof_tesseract_noop)
|
stdin=input_stream, env=spoof_tesseract_noop)
|
||||||
out, err = p.communicate()
|
out, err = p.communicate()
|
||||||
|
|
||||||
assert p.returncode == ExitCode.ok
|
if im.mode in ('RGBA', 'LA'):
|
||||||
|
# If alpha image is input, expect an error
|
||||||
|
assert p.returncode != ExitCode.ok and b'alpha' in err
|
||||||
|
return
|
||||||
|
|
||||||
|
assert p.returncode == ExitCode.ok, err.decode('utf-8')
|
||||||
|
|
||||||
pdfinfo = PdfInfo(output_file)
|
pdfinfo = PdfInfo(output_file)
|
||||||
|
|
||||||
@@ -894,7 +899,7 @@ def test_compression_changed(spoof_tesseract_noop, ocrmypdf_exec,
|
|||||||
stdin=input_stream, env=spoof_tesseract_noop)
|
stdin=input_stream, env=spoof_tesseract_noop)
|
||||||
out, err = p.communicate()
|
out, err = p.communicate()
|
||||||
|
|
||||||
assert p.returncode == ExitCode.ok
|
assert p.returncode == ExitCode.ok, err
|
||||||
|
|
||||||
pdfinfo = PdfInfo(output_file)
|
pdfinfo = PdfInfo(output_file)
|
||||||
|
|
||||||
@@ -903,13 +908,7 @@ def test_compression_changed(spoof_tesseract_noop, ocrmypdf_exec,
|
|||||||
if compression == "jpeg":
|
if compression == "jpeg":
|
||||||
assert pdfimage.enc == Encoding.jpeg
|
assert pdfimage.enc == Encoding.jpeg
|
||||||
else:
|
else:
|
||||||
if ghostscript.jpeg_passthrough_available():
|
assert pdfimage.enc not in (Encoding.jpeg, Encoding.jpeg2000)
|
||||||
# Ghostscript 9.23 adds JPEG passthrough, which allows a JPEG to be
|
|
||||||
# copied without transcoding - so report
|
|
||||||
if image.endswith('jpg'):
|
|
||||||
assert pdfimage.enc == Encoding.jpeg
|
|
||||||
else:
|
|
||||||
assert pdfimage.enc not in (Encoding.jpeg, Encoding.jpeg2000)
|
|
||||||
|
|
||||||
if im.mode.startswith('RGB') or im.mode.startswith('BGR'):
|
if im.mode.startswith('RGB') or im.mode.startswith('BGR'):
|
||||||
assert pdfimage.color == Colorspace.rgb, \
|
assert pdfimage.color == Colorspace.rgb, \
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ from datetime import timezone
|
|||||||
from ocrmypdf.pdfa import file_claims_pdfa, encode_pdf_date, decode_pdf_date
|
from ocrmypdf.pdfa import file_claims_pdfa, encode_pdf_date, decode_pdf_date
|
||||||
from ocrmypdf.exceptions import ExitCode
|
from ocrmypdf.exceptions import ExitCode
|
||||||
from ocrmypdf.lib import fitz
|
from ocrmypdf.lib import fitz
|
||||||
|
from ocrmypdf.helpers import fspath
|
||||||
|
from ocrmypdf.pdfa import (
|
||||||
|
file_claims_pdfa, encode_pdf_date, decode_pdf_date, generate_pdfa_ps,
|
||||||
|
SRGB_ICC_PROFILE
|
||||||
|
)
|
||||||
|
from ocrmypdf.exec import ghostscript
|
||||||
|
|
||||||
# pytest.helpers is dynamic
|
# pytest.helpers is dynamic
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
@@ -76,6 +82,9 @@ def test_override_metadata(spoof_tesseract_noop, output_type, resources,
|
|||||||
before = pypdf.PdfFileReader(str(input_file))
|
before = pypdf.PdfFileReader(str(input_file))
|
||||||
after = pypdf.PdfFileReader(outpdf)
|
after = pypdf.PdfFileReader(outpdf)
|
||||||
|
|
||||||
|
if ghostscript.version() >= '9.24':
|
||||||
|
pytest.xfail('Ghostscript 9.24+ does not support Unicode DOCINFO')
|
||||||
|
|
||||||
assert after.documentInfo['/Title'] == german
|
assert after.documentInfo['/Title'] == german
|
||||||
assert after.documentInfo['/Author'] == chinese
|
assert after.documentInfo['/Author'] == chinese
|
||||||
assert after.documentInfo.get('/Keywords', '') == ''
|
assert after.documentInfo.get('/Keywords', '') == ''
|
||||||
@@ -157,5 +166,3 @@ def test_creation_date_preserved(spoof_tesseract_noop, output_type, resources,
|
|||||||
date_after = decode_pdf_date(after['/ModDate'])
|
date_after = decode_pdf_date(after['/ModDate'])
|
||||||
assert seconds_between_dates(
|
assert seconds_between_dates(
|
||||||
date_after, datetime.datetime.now(timezone.utc)) < 1000
|
date_after, datetime.datetime.now(timezone.utc)) < 1000
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -29,8 +29,7 @@ from pathlib import Path
|
|||||||
spoof = pytest.helpers.spoof
|
spoof = pytest.helpers.spoof
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
def _ensure_tess4():
|
||||||
def ensure_tess4():
|
|
||||||
if tesseract.v4():
|
if tesseract.v4():
|
||||||
# "tesseract" on $PATH is already v4
|
# "tesseract" on $PATH is already v4
|
||||||
return os.environ.copy()
|
return os.environ.copy()
|
||||||
@@ -49,6 +48,11 @@ def ensure_tess4():
|
|||||||
raise EnvironmentError("Can't find Tesseract 4")
|
raise EnvironmentError("Can't find Tesseract 4")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ensure_tess4():
|
||||||
|
return _ensure_tess4()
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def modified_os_environ(env):
|
def modified_os_environ(env):
|
||||||
old_env = os.environ.copy()
|
old_env = os.environ.copy()
|
||||||
@@ -63,8 +67,8 @@ def tess4_available():
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# ensure_tess4 locates the tess4 binary we are going to check
|
# _ensure_tess4 locates the tess4 binary we are going to check
|
||||||
env = ensure_tess4()
|
env = _ensure_tess4()
|
||||||
with modified_os_environ(env):
|
with modified_os_environ(env):
|
||||||
# Now jump into this environment and make sure it really is Tess4
|
# Now jump into this environment and make sure it really is Tess4
|
||||||
return tesseract.v4() and tesseract.has_textonly_pdf()
|
return tesseract.v4() and tesseract.has_textonly_pdf()
|
||||||
|
|||||||
Reference in New Issue
Block a user