Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79c84eefa3 | ||
|
|
5398003160 | ||
|
|
58b2bed99d | ||
|
|
58c29ffb5c | ||
|
|
f615b6f0e8 | ||
|
|
e0c8dadcce | ||
|
|
9a86f53109 | ||
|
|
91cb092aa0 | ||
|
|
922a107b7f |
@@ -13,6 +13,7 @@
|
|||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
wheelhouse/
|
wheelhouse/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
|
||||||
# Automatically generated files
|
# Automatically generated files
|
||||||
docs/_build/
|
docs/_build/
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ Note that it is licensed under GPLv3, so scripts that ``import ocrmypdf`` and ar
|
|||||||
find: [^`]\#([0-9]{1,3})[^0-9]
|
find: [^`]\#([0-9]{1,3})[^0-9]
|
||||||
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
|
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
|
||||||
|
|
||||||
|
v8.2.4
|
||||||
|
------
|
||||||
|
|
||||||
|
- Fixed a false positive while checking for a certain type of PDF that only Acrobat can read. We now more accurately detect Acrobat-only PDFs.
|
||||||
|
|
||||||
|
- OCRmyPDF holds fewer open file handles and is more prompt about releasing those it no longer needs.
|
||||||
|
|
||||||
|
- Minor optimization: we no longer traverse the table of contents to ensure all references in it are resolved, as changes to libqpdf have made this unnecessary.
|
||||||
|
|
||||||
|
- pikepdf 1.2.0 is now required
|
||||||
|
|
||||||
v8.2.3
|
v8.2.3
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ chardet == 3.0.4
|
|||||||
cffi == 1.12.2
|
cffi == 1.12.2
|
||||||
img2pdf == 0.3.3
|
img2pdf == 0.3.3
|
||||||
pdfminer.six == 20181108
|
pdfminer.six == 20181108
|
||||||
pikepdf == 1.1.0
|
pikepdf == 1.2.0
|
||||||
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
|
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
|
||||||
pycparser == 2.19
|
pycparser == 2.19
|
||||||
python-xmp-toolkit == 2.0.1
|
python-xmp-toolkit == 2.0.1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pytest == 4.3.0
|
pytest >= 4.4.1, < 5
|
||||||
pytest-helpers-namespace >= 2019.1.8
|
pytest-helpers-namespace >= 2019.1.8
|
||||||
pytest-xdist
|
pytest-xdist == 1.28.0
|
||||||
pytest-cov >= 2.6.1
|
pytest-cov >= 2.6.1
|
||||||
python-xmp-toolkit # requires apt-get install libexempi3
|
python-xmp-toolkit # requires apt-get install libexempi3
|
||||||
# or brew install exempi
|
# or brew install exempi
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ setup(
|
|||||||
'cffi >= 1.9.1', # must be a setup and install requirement
|
'cffi >= 1.9.1', # must be a setup and install requirement
|
||||||
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
|
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
|
||||||
'pdfminer.six == 20181108 ; sys_platform != "darwin"',
|
'pdfminer.six == 20181108 ; sys_platform != "darwin"',
|
||||||
'pikepdf >= 1.1.0, < 2',
|
'pikepdf >= 1.2.0, < 2',
|
||||||
'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"',
|
'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"',
|
||||||
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
|
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
|
||||||
# block 5.1.0, broken wheels
|
# block 5.1.0, broken wheels
|
||||||
|
|||||||
@@ -66,10 +66,6 @@ def complain(message):
|
|||||||
print(*textwrap.wrap(message), file=sys.stderr)
|
print(*textwrap.wrap(message), file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
# Hack to help debugger context find /usr/local/bin
|
|
||||||
if 'IDE_PROJECT_ROOTS' in os.environ:
|
|
||||||
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
|
|
||||||
|
|
||||||
# --------
|
# --------
|
||||||
# Critical environment tests
|
# Critical environment tests
|
||||||
|
|
||||||
|
|||||||
+11
-10
@@ -816,16 +816,15 @@ def convert_to_pdfa(input_files_groups, output_file, log, context):
|
|||||||
# NULs in DocumentInfo seem to be common since older Acrobats included them.
|
# NULs in DocumentInfo seem to be common since older Acrobats included them.
|
||||||
# pikepdf can deal with this, but we make the world a better place by
|
# pikepdf can deal with this, but we make the world a better place by
|
||||||
# stamping them out as soon as possible.
|
# stamping them out as soon as possible.
|
||||||
pdf_layers_file = pikepdf.open(layers_file)
|
with pikepdf.open(layers_file) as pdf_layers_file:
|
||||||
if pdf_layers_file.docinfo:
|
if pdf_layers_file.docinfo:
|
||||||
modified = False
|
modified = False
|
||||||
for k, v in pdf_layers_file.docinfo.items():
|
for k, v in pdf_layers_file.docinfo.items():
|
||||||
if b'\x00' in bytes(v):
|
if b'\x00' in bytes(v):
|
||||||
pdf_layers_file.docinfo[k] = bytes(v).replace(b'\x00', b'')
|
pdf_layers_file.docinfo[k] = bytes(v).replace(b'\x00', b'')
|
||||||
modified = True
|
modified = True
|
||||||
if modified:
|
if modified:
|
||||||
pdf_layers_file.save(layers_file)
|
pdf_layers_file.save(layers_file)
|
||||||
del pdf_layers_file
|
|
||||||
|
|
||||||
ps = next((ii for ii in input_files if ii.endswith('.ps')), None)
|
ps = next((ii for ii in input_files if ii.endswith('.ps')), None)
|
||||||
ghostscript.generate_pdfa(
|
ghostscript.generate_pdfa(
|
||||||
@@ -880,6 +879,8 @@ def metadata_fixup(input_files_groups, output_file, log, context):
|
|||||||
compress_streams=True,
|
compress_streams=True,
|
||||||
object_stream_mode=pikepdf.ObjectStreamMode.generate,
|
object_stream_mode=pikepdf.ObjectStreamMode.generate,
|
||||||
)
|
)
|
||||||
|
original.close()
|
||||||
|
pdf.close()
|
||||||
|
|
||||||
|
|
||||||
def optimize_pdf(input_file, output_file, log, context):
|
def optimize_pdf(input_file, output_file, log, context):
|
||||||
|
|||||||
+31
-32
@@ -25,7 +25,7 @@ from .exec import tesseract
|
|||||||
from .helpers import flatten_groups, page_number
|
from .helpers import flatten_groups, page_number
|
||||||
|
|
||||||
|
|
||||||
MAX_OPEN_PAGE_PDFS = int(os.environ.get('_OCRMYPDF_MAX_OPEN_PAGE_PDFS', 100))
|
MAX_REPLACE_PAGES = int(os.environ.get('_OCRMYPDF_MAX_REPLACE_PAGES', 100))
|
||||||
|
|
||||||
|
|
||||||
def _update_page_resources(*, page, font, font_key, procset):
|
def _update_page_resources(*, page, font, font_key, procset):
|
||||||
@@ -161,6 +161,7 @@ def _weave_layers_graft(
|
|||||||
_update_page_resources(
|
_update_page_resources(
|
||||||
page=base_page, font=font, font_key=font_key, procset=procset
|
page=base_page, font=font, font_key=font_key, procset=procset
|
||||||
)
|
)
|
||||||
|
pdf_text.close()
|
||||||
|
|
||||||
|
|
||||||
def _find_font(text, pdf_base):
|
def _find_font(text, pdf_base):
|
||||||
@@ -169,20 +170,23 @@ def _find_font(text, pdf_base):
|
|||||||
font, font_key = None, None
|
font, font_key = None, None
|
||||||
possible_font_names = ('/f-0-0', '/F1')
|
possible_font_names = ('/f-0-0', '/F1')
|
||||||
try:
|
try:
|
||||||
pdf_text = pikepdf.open(text)
|
with pikepdf.open(text) as pdf_text:
|
||||||
pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {})
|
try:
|
||||||
except Exception:
|
pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {})
|
||||||
|
except (AttributeError, IndexError, KeyError):
|
||||||
|
return None, None
|
||||||
|
for f in possible_font_names:
|
||||||
|
pdf_text_font = pdf_text_fonts.get(f, None)
|
||||||
|
if pdf_text_font is not None:
|
||||||
|
font_key = f
|
||||||
|
break
|
||||||
|
if pdf_text_font:
|
||||||
|
font = pdf_base.copy_foreign(pdf_text_font)
|
||||||
|
return font, font_key
|
||||||
|
except (FileNotFoundError, pikepdf.PdfError):
|
||||||
|
# PdfError occurs if a 0-length file is written e.g. due to OCR timeout
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
for f in possible_font_names:
|
|
||||||
pdf_text_font = pdf_text_fonts.get(f, None)
|
|
||||||
if pdf_text_font is not None:
|
|
||||||
font_key = f
|
|
||||||
break
|
|
||||||
if pdf_text_font:
|
|
||||||
font = pdf_base.copy_foreign(pdf_text_font)
|
|
||||||
return font, font_key
|
|
||||||
|
|
||||||
|
|
||||||
def _traverse_toc(pdf_base, visitor_fn, log):
|
def _traverse_toc(pdf_base, visitor_fn, log):
|
||||||
"""
|
"""
|
||||||
@@ -317,21 +321,16 @@ def weave_layers(infiles, output_file, log, context):
|
|||||||
base = list(basegroup)[0]
|
base = list(basegroup)[0]
|
||||||
path_base = Path(base).resolve()
|
path_base = Path(base).resolve()
|
||||||
pdf_base = pikepdf.open(path_base)
|
pdf_base = pikepdf.open(path_base)
|
||||||
keep_open = []
|
|
||||||
font, font_key, procset = None, None, None
|
font, font_key, procset = None, None, None
|
||||||
pdfinfo = context.get_pdfinfo()
|
pdfinfo = context.get_pdfinfo()
|
||||||
pagerefs = {}
|
pagerefs = {}
|
||||||
|
|
||||||
# Walk the table of contents first, to trigger pikepdf/qpdf to resolve all
|
|
||||||
# page references in the table of contents. Some PDF generators put invalid
|
|
||||||
# references in the ToC, so we want to resolve them to null before we
|
|
||||||
# create any references, or the ToC will be corrupted
|
|
||||||
_traverse_toc(pdf_base, None, log)
|
|
||||||
|
|
||||||
procset = pdf_base.make_indirect(
|
procset = pdf_base.make_indirect(
|
||||||
pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]')
|
pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replacements = 0
|
||||||
|
|
||||||
# Iterate rest
|
# Iterate rest
|
||||||
for page_num, layers in groups:
|
for page_num, layers in groups:
|
||||||
layers = list(layers)
|
layers = list(layers)
|
||||||
@@ -354,10 +353,10 @@ def weave_layers(infiles, output_file, log, context):
|
|||||||
log.debug("Replace")
|
log.debug("Replace")
|
||||||
old_objgen = pdf_base.pages[page_num - 1].objgen
|
old_objgen = pdf_base.pages[page_num - 1].objgen
|
||||||
|
|
||||||
pdf_image = pikepdf.open(image)
|
with pikepdf.open(image) as pdf_image:
|
||||||
keep_open.append(pdf_image)
|
replacements += 1
|
||||||
image_page = pdf_image.pages[0]
|
image_page = pdf_image.pages[0]
|
||||||
pdf_base.pages[page_num - 1] = image_page
|
pdf_base.pages[page_num - 1] = image_page
|
||||||
|
|
||||||
# We're adding a new page, which will get a new objgen number pair,
|
# We're adding a new page, which will get a new objgen number pair,
|
||||||
# so we need to update any references to it. qpdf did not like
|
# so we need to update any references to it. qpdf did not like
|
||||||
@@ -396,20 +395,19 @@ def weave_layers(infiles, output_file, log, context):
|
|||||||
content_rotation - autorotate_correction
|
content_rotation - autorotate_correction
|
||||||
) % 360
|
) % 360
|
||||||
|
|
||||||
if len(keep_open) > MAX_OPEN_PAGE_PDFS:
|
if replacements % MAX_REPLACE_PAGES == 0:
|
||||||
# qpdf limitations require us to keep files open when we intend
|
# Periodically save and reload the Pdf object. This will keep a
|
||||||
# to copy content from them before saving. However, we want to keep
|
# lid on our memory usage for very large files. Attach the font to
|
||||||
# a lid on file handles and memory usage, so for big files we're
|
# page 1 even if page 1 doesn't use it, so we have a way to get it
|
||||||
# going to stop and save periodically. Attach the font to page 1
|
# back.
|
||||||
# even if page 1 doesn't use it, so we have a way to get it back.
|
# TODO refactor this to outside the loop
|
||||||
page0 = pdf_base.pages[0]
|
page0 = pdf_base.pages[0]
|
||||||
_update_page_resources(
|
_update_page_resources(
|
||||||
page=page0, font=font, font_key=font_key, procset=procset
|
page=page0, font=font, font_key=font_key, procset=procset
|
||||||
)
|
)
|
||||||
interim = output_file + f'_working{page_num}.pdf'
|
interim = output_file + f'_working{page_num}.pdf'
|
||||||
pdf_base.save(interim)
|
pdf_base.save(interim)
|
||||||
del pdf_base
|
pdf_base.close()
|
||||||
keep_open = []
|
|
||||||
|
|
||||||
pdf_base = pikepdf.open(interim)
|
pdf_base = pikepdf.open(interim)
|
||||||
procset = pdf_base.pages[0].Resources.ProcSet
|
procset = pdf_base.pages[0].Resources.ProcSet
|
||||||
@@ -417,3 +415,4 @@ def weave_layers(infiles, output_file, log, context):
|
|||||||
|
|
||||||
_fix_toc(pdf_base, pagerefs, log)
|
_fix_toc(pdf_base, pagerefs, log)
|
||||||
pdf_base.save(output_file)
|
pdf_base.save(output_file)
|
||||||
|
pdf_base.close()
|
||||||
|
|||||||
+15
-15
@@ -131,19 +131,19 @@ def file_claims_pdfa(filename):
|
|||||||
do full PDF/A validation.
|
do full PDF/A validation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pdf = pikepdf.open(filename)
|
with pikepdf.open(filename) as pdf:
|
||||||
pdfmeta = pdf.open_metadata()
|
pdfmeta = pdf.open_metadata()
|
||||||
if not pdfmeta.pdfa_status:
|
if not pdfmeta.pdfa_status:
|
||||||
return {
|
return {
|
||||||
'pass': False,
|
'pass': False,
|
||||||
'output': 'pdf',
|
'output': 'pdf',
|
||||||
'conformance': 'No PDF/A metadata in XMP',
|
'conformance': 'No PDF/A metadata in XMP',
|
||||||
}
|
}
|
||||||
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||||
conformance = f'PDF/A-{pdfmeta.pdfa_status}'
|
conformance = f'PDF/A-{pdfmeta.pdfa_status}'
|
||||||
pdfa_dict = {}
|
pdfa_dict = {}
|
||||||
if pdfmeta.pdfa_status in valid_part_conforms:
|
if pdfmeta.pdfa_status in valid_part_conforms:
|
||||||
pdfa_dict['pass'] = True
|
pdfa_dict['pass'] = True
|
||||||
pdfa_dict['output'] = 'pdfa'
|
pdfa_dict['output'] = 'pdfa'
|
||||||
pdfa_dict['conformance'] = conformance
|
pdfa_dict['conformance'] = conformance
|
||||||
return pdfa_dict
|
return pdfa_dict
|
||||||
|
|||||||
@@ -618,8 +618,9 @@ def _pdf_get_all_pageinfo(infile, detailed_analysis=False, log=None):
|
|||||||
if not log:
|
if not log:
|
||||||
log = Mock()
|
log = Mock()
|
||||||
|
|
||||||
pdf = pikepdf.open(infile)
|
pdf = pikepdf.open(infile) # Do not close in this function
|
||||||
if pdf.is_encrypted:
|
if pdf.is_encrypted:
|
||||||
|
pdf.close()
|
||||||
raise EncryptedPdfError() # Triggered by encryption with empty passwd
|
raise EncryptedPdfError() # Triggered by encryption with empty passwd
|
||||||
if detailed_analysis:
|
if detailed_analysis:
|
||||||
pages_xml = None
|
pages_xml = None
|
||||||
@@ -755,7 +756,13 @@ class PdfInfo:
|
|||||||
infile, detailed_page_analysis, log=log
|
infile, detailed_page_analysis, log=log
|
||||||
)
|
)
|
||||||
self._needs_rendering = pdf.root.get('/NeedsRendering', False)
|
self._needs_rendering = pdf.root.get('/NeedsRendering', False)
|
||||||
self._has_acroform = '/AcroForm' in pdf.root
|
self._has_acroform = False
|
||||||
|
if '/AcroForm' in pdf.root:
|
||||||
|
if len(pdf.root.AcroForm.get('/Fields', [])) > 0:
|
||||||
|
self._has_acroform = True
|
||||||
|
elif '/XFA' in pdf.root.AcroForm:
|
||||||
|
self._has_acroform = True
|
||||||
|
pdf.close()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pages(self):
|
def pages(self):
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ def test_no_glyphless_weave(resources, outdir):
|
|||||||
pdf.save(outdir / 'test.pdf')
|
pdf.save(outdir / 'test.pdf')
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env['_OCRMYPDF_MAX_OPEN_PAGE_PDFS'] = '2'
|
env['_OCRMYPDF_MAX_REPLACE_PAGES'] = '2'
|
||||||
check_ocrmypdf(
|
check_ocrmypdf(
|
||||||
outdir / 'test.pdf',
|
outdir / 'test.pdf',
|
||||||
outdir / 'out.pdf',
|
outdir / 'out.pdf',
|
||||||
|
|||||||
Reference in New Issue
Block a user