Compare commits

...
9 Commits
Author SHA1 Message Date
James R. Barlow 79c84eefa3 Fix main.txt 2019-04-23 02:21:31 -07:00
James R. Barlow 5398003160 Fix test.txt 2019-04-23 00:42:40 -07:00
James R. Barlow 58b2bed99d v8.2.4 notes 2019-04-23 00:07:12 -07:00
James R. Barlow 58c29ffb5c weave: use explicit pdf.close(), drastically reduce open file handles
With the new pikepdf 1.2.0 we no longer need to hold file handles
open because of the "copy to memory" functionality. We retain
the behavior of closing/reopening the output PDF every 100 pages as
a way to limit memory usage.
2019-04-18 15:12:48 -07:00
James R. Barlow f615b6f0e8 pdfinfo: be more specific about detecting XFA we can't render 2019-04-18 15:07:25 -07:00
James R. Barlow e0c8dadcce Explicitly close most pikepdf.Pdf when done with them 2019-04-18 15:02:12 -07:00
James R. Barlow 9a86f53109 Ignore pip-wheel-metadata folder
https://github.com/pypa/pip/issues/6213
2019-04-18 10:42:10 -07:00
James R. Barlow 91cb092aa0 Remove PyCharm debugger hack 2019-04-18 10:15:02 -07:00
James R. Barlow 922a107b7f Remove safety traversal of PDF table of contents
qpdf fixed the danging reference issue (qpdf #240) in 8.3.0, which is
required by pikepdf 1.1.0. We no
longer need the workaround.
2019-04-13 00:24:03 -07:00
11 changed files with 83 additions and 68 deletions
+1
View File
@@ -13,6 +13,7 @@
build/ build/
dist/ dist/
wheelhouse/ wheelhouse/
pip-wheel-metadata/
# Automatically generated files # Automatically generated files
docs/_build/ docs/_build/
+11
View File
@@ -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
------ ------
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
-4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+9 -2
View File
@@ -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
View File
@@ -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',