Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9cd5bf253 | ||
|
|
8102ca1075 | ||
|
|
6b5934ff4e | ||
|
|
e38b30af1c | ||
|
|
f64712322e | ||
|
|
1d09061130 | ||
|
|
a2203b2447 |
@@ -10,6 +10,20 @@ 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.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
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|||||||
@@ -863,6 +863,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.)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -52,20 +60,20 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
|||||||
pageno=1, page_dpi=None):
|
pageno=1, page_dpi=None):
|
||||||
"""
|
"""
|
||||||
Rasterize one page of a PDF at resolution (xres, yres) in canvas units.
|
Rasterize one page of a PDF at resolution (xres, yres) in canvas units.
|
||||||
|
|
||||||
The image is sized to match the integer pixels dimensions implied by
|
The image is sized to match the integer pixels dimensions implied by
|
||||||
(xres, yres) even if those numbers are noninteger. The image's DPI will
|
(xres, yres) even if those numbers are noninteger. The image's DPI will
|
||||||
be overridden with the values in page_dpi.
|
be overridden with the values in page_dpi.
|
||||||
|
|
||||||
:param input_file: pathlike
|
:param input_file: pathlike
|
||||||
:param output_file: pathlike
|
:param output_file: pathlike
|
||||||
:param xres: resolution at which to rasterize page
|
:param xres: resolution at which to rasterize page
|
||||||
:param yres:
|
:param yres:
|
||||||
:param raster_device:
|
:param raster_device:
|
||||||
:param log:
|
:param log:
|
||||||
:param pageno: page number to rasterize (beginning at page 1)
|
:param pageno: page number to rasterize (beginning at page 1)
|
||||||
:param page_dpi: resolution tuple (x, y) overriding output image DPI
|
:param page_dpi: resolution tuple (x, y) overriding output image DPI
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
res = xres, yres
|
res = xres, yres
|
||||||
int_res = round(xres), round(yres)
|
int_res = round(xres), round(yres)
|
||||||
@@ -85,7 +93,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
|||||||
'-o', tmp.name,
|
'-o', tmp.name,
|
||||||
fspath(input_file)
|
fspath(input_file)
|
||||||
]
|
]
|
||||||
|
|
||||||
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
|
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
|
||||||
universal_newlines=True)
|
universal_newlines=True)
|
||||||
if _gs_error_reported(p.stdout):
|
if _gs_error_reported(p.stdout):
|
||||||
@@ -139,15 +147,17 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
|
|||||||
"-dAutoFilterGrayImages=true",
|
"-dAutoFilterGrayImages=true",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Older versions of Ghostscript expect a leading slash in
|
# Older versions of Ghostscript expect a leading slash in
|
||||||
# sColorConversionStrategy, newer ones should not have it. See Ghostscript
|
# sColorConversionStrategy, newer ones should not have it. See Ghostscript
|
||||||
# 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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -103,7 +105,7 @@ def is_file_writable(test_file):
|
|||||||
|
|
||||||
if p.is_symlink():
|
if p.is_symlink():
|
||||||
# Python 3.5 does not accept parameters for Path.resolve() and behaves
|
# Python 3.5 does not accept parameters for Path.resolve() and behaves
|
||||||
# as if strict=True (throws an exception on failure). Python 3.6
|
# as if strict=True (throws an exception on failure). Python 3.6
|
||||||
# defaults to strict=False. This implements strict=False like behavior
|
# defaults to strict=False. This implements strict=False like behavior
|
||||||
# for Python 3.5.
|
# for Python 3.5.
|
||||||
if sys.version_info[0:2] <= (3, 5):
|
if sys.version_info[0:2] <= (3, 5):
|
||||||
|
|||||||
+77
-19
@@ -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,10 +117,30 @@ def encode_text_string(s: str) -> str:
|
|||||||
return ascii_hex_str
|
return ascii_hex_str
|
||||||
|
|
||||||
|
|
||||||
def encode_pdf_date(d: datetime) -> str:
|
def _encode_ascii(s: str) -> str:
|
||||||
"""Encode Python datetime object as PDF date string
|
"""
|
||||||
|
Aggressively strip non-ASCII and PDF escape sequences
|
||||||
|
|
||||||
From Adobe pdfmark manual:
|
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:
|
||||||
|
"""
|
||||||
|
Encode Python datetime object as PDF date string
|
||||||
|
|
||||||
|
From Adobe pdfmark manual:
|
||||||
(D:YYYYMMDDHHmmSSOHH'mm')
|
(D:YYYYMMDDHHmmSSOHH'mm')
|
||||||
D: is an optional prefix. YYYY is the year. All fields after the year are
|
D: is an optional prefix. YYYY is the year. All fields after the year are
|
||||||
optional. MM is the month (01-12), DD is the day (01-31), HH is the
|
optional. MM is the month (01-12), DD is the day (01-31), HH is the
|
||||||
@@ -137,16 +172,23 @@ 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:]
|
||||||
|
|
||||||
# Literal Z00'00', is incorrect but found in the wild,
|
# Literal Z00'00', is incorrect but found in the wild,
|
||||||
# probably made by OS X Quartz -- standardize
|
# probably made by OS X Quartz -- standardize
|
||||||
if s.endswith("Z00'00'"):
|
if s.endswith("Z00'00'"):
|
||||||
s = s.replace("Z00'00'", '+0000')
|
s = s.replace("Z00'00'", '+0000')
|
||||||
elif s.endswith('Z'):
|
elif s.endswith('Z'):
|
||||||
s = s.replace('Z', '+0000')
|
s = s.replace('Z', '+0000')
|
||||||
|
|
||||||
s = s.replace("'", "") # Remove apos from PDF time strings
|
s = s.replace("'", "") # Remove apos from PDF time strings
|
||||||
|
|
||||||
return datetime.strptime(s, r'%Y%m%d%H%M%S%z')
|
return datetime.strptime(s, r'%Y%m%d%H%M%S%z')
|
||||||
@@ -154,7 +196,7 @@ def decode_pdf_date(s: str) -> datetime:
|
|||||||
|
|
||||||
def _get_pdfmark_dates(pdfmark):
|
def _get_pdfmark_dates(pdfmark):
|
||||||
"""Encode dates for pdfmark Postscript. The best way to deal with a
|
"""Encode dates for pdfmark Postscript. The best way to deal with a
|
||||||
missing date entry is set it to null, because if the key is omitted
|
missing date entry is set it to null, because if the key is omitted
|
||||||
Ghostscript will set it to now - we do not want to erase the fact that
|
Ghostscript will set it to now - we do not want to erase the fact that
|
||||||
the value was unknown. Setting to an empty string breaks Ghostscript
|
the value was unknown. Setting to an empty string breaks Ghostscript
|
||||||
9.22 as reported here:
|
9.22 as reported here:
|
||||||
@@ -172,30 +214,47 @@ def _get_pdfmark_dates(pdfmark):
|
|||||||
date_str = date_str[2:]
|
date_str = date_str[2:]
|
||||||
try:
|
try:
|
||||||
yield ' {} (D:{})'.format(
|
yield ' {} (D:{})'.format(
|
||||||
key,
|
key,
|
||||||
encode_pdf_date(decode_pdf_date(date_str)))
|
encode_pdf_date(decode_pdf_date(date_str)))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
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')
|
||||||
@@ -172,7 +180,7 @@ def triage_image_file(input_file, output_file, log, options):
|
|||||||
|
|
||||||
def _pdf_guess_version(input_file, search_window=1024):
|
def _pdf_guess_version(input_file, search_window=1024):
|
||||||
"""Try to find version signature at start of file.
|
"""Try to find version signature at start of file.
|
||||||
|
|
||||||
Not robust enough to deal with appended files.
|
Not robust enough to deal with appended files.
|
||||||
|
|
||||||
Returns empty string if not found, indicating file is probably not PDF.
|
Returns empty string if not found, indicating file is probably not PDF.
|
||||||
@@ -231,7 +239,7 @@ def repair_and_parse_pdf(
|
|||||||
"output these files.) Use --output-type=pdf instead."
|
"output these files.) Use --output-type=pdf instead."
|
||||||
)
|
)
|
||||||
raise InputFileError()
|
raise InputFileError()
|
||||||
|
|
||||||
if len(pdfinfo.pages) > 2000 and sys.version_info[0:2] <= (3, 5):
|
if len(pdfinfo.pages) > 2000 and sys.version_info[0:2] <= (3, 5):
|
||||||
log.warning(
|
log.warning(
|
||||||
"Performance regressions are known occur with Python 3.5 for "
|
"Performance regressions are known occur with Python 3.5 for "
|
||||||
@@ -298,7 +306,7 @@ def is_ocr_required(pageinfo, log, options):
|
|||||||
# We found a page with no images and no text. That means it may
|
# We found a page with no images and no text. That means it may
|
||||||
# have vector art that the user wants to OCR. If we determined
|
# have vector art that the user wants to OCR. If we determined
|
||||||
# lossless reconstruction is not possible then we have to rasterize
|
# lossless reconstruction is not possible then we have to rasterize
|
||||||
# the image. So if OCR is being forced, take that to mean YES, go
|
# the image. So if OCR is being forced, take that to mean YES, go
|
||||||
# ahead and rasterize. If not forced, then pretend there's no text
|
# ahead and rasterize. If not forced, then pretend there's no text
|
||||||
# on the page at all so we don't lose anything.
|
# on the page at all so we don't lose anything.
|
||||||
# This could be made smarter by explicitly searching for vector art.
|
# This could be made smarter by explicitly searching for vector art.
|
||||||
@@ -884,7 +892,7 @@ def get_pdfmark(base_pdf, options):
|
|||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
pdfmark = {k: from_document_info(k) for k in
|
pdfmark = {k: from_document_info(k) for k in
|
||||||
('/Title', '/Author', '/Keywords', '/Subject', '/CreationDate')}
|
('/Title', '/Author', '/Keywords', '/Subject', '/CreationDate')}
|
||||||
if options.title:
|
if options.title:
|
||||||
pdfmark['/Title'] = options.title
|
pdfmark['/Title'] = options.title
|
||||||
@@ -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(
|
||||||
@@ -1075,7 +1096,7 @@ def merge_sidecars(
|
|||||||
if txt_file:
|
if txt_file:
|
||||||
with open(txt_file, 'r', encoding="utf-8") as in_:
|
with open(txt_file, 'r', encoding="utf-8") as in_:
|
||||||
txt = in_.read()
|
txt = in_.read()
|
||||||
# Tesseract v4 alpha started adding form feeds in
|
# Tesseract v4 alpha started adding form feeds in
|
||||||
# commit aa6eb6b
|
# commit aa6eb6b
|
||||||
# No obvious way to detect what binaries will do this, so
|
# No obvious way to detect what binaries will do this, so
|
||||||
# for consistency just ignore its form feeds and insert our
|
# for consistency just ignore its form feeds and insert our
|
||||||
@@ -1109,7 +1130,7 @@ def copy_final(
|
|||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
else:
|
else:
|
||||||
# At this point we overwrite the output_file specified by the user
|
# At this point we overwrite the output_file specified by the user
|
||||||
# use copyfileobj because then we use open() to create the file and
|
# use copyfileobj because then we use open() to create the file and
|
||||||
# get the appropriate umask, ownership, etc.
|
# get the appropriate umask, ownership, etc.
|
||||||
with open(output_file, 'wb') as output_stream:
|
with open(output_file, 'wb') as output_stream:
|
||||||
copyfileobj(input_stream, output_stream)
|
copyfileobj(input_stream, output_stream)
|
||||||
@@ -1284,7 +1305,7 @@ def build_pipeline(options, work_folder, log, context):
|
|||||||
output=os.path.join(work_folder, r'\1.rendered.pdf'),
|
output=os.path.join(work_folder, r'\1.rendered.pdf'),
|
||||||
extras=[log, context])
|
extras=[log, context])
|
||||||
task_combine_layers.graphviz(fillcolor='"#00cc66"')
|
task_combine_layers.graphviz(fillcolor='"#00cc66"')
|
||||||
task_combine_layers.active_if(options.pdf_renderer == 'hocr' or
|
task_combine_layers.active_if(options.pdf_renderer == 'hocr' or
|
||||||
options.pdf_renderer == 'sandwich')
|
options.pdf_renderer == 'sandwich')
|
||||||
|
|
||||||
# Tesseract OCR+PDF
|
# Tesseract OCR+PDF
|
||||||
|
|||||||
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 |
+9
-10
@@ -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, \
|
||||||
|
|||||||
+10
-3
@@ -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', '') == ''
|
||||||
@@ -136,7 +145,7 @@ def test_creation_date_preserved(spoof_tesseract_noop, output_type, resources,
|
|||||||
|
|
||||||
before = pypdf.PdfFileReader(str(input_file)).getDocumentInfo()
|
before = pypdf.PdfFileReader(str(input_file)).getDocumentInfo()
|
||||||
check_ocrmypdf(
|
check_ocrmypdf(
|
||||||
input_file, outpdf, '--output-type', output_type,
|
input_file, outpdf, '--output-type', output_type,
|
||||||
env=spoof_tesseract_noop)
|
env=spoof_tesseract_noop)
|
||||||
after = pypdf.PdfFileReader(str(outpdf)).getDocumentInfo()
|
after = pypdf.PdfFileReader(str(outpdf)).getDocumentInfo()
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user