Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79c84eefa3 | ||
|
|
5398003160 | ||
|
|
58b2bed99d | ||
|
|
58c29ffb5c | ||
|
|
f615b6f0e8 | ||
|
|
e0c8dadcce | ||
|
|
9a86f53109 | ||
|
|
91cb092aa0 | ||
|
|
922a107b7f |
@@ -13,6 +13,7 @@
|
||||
build/
|
||||
dist/
|
||||
wheelhouse/
|
||||
pip-wheel-metadata/
|
||||
|
||||
# Automatically generated files
|
||||
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]
|
||||
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
|
||||
------
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ chardet == 3.0.4
|
||||
cffi == 1.12.2
|
||||
img2pdf == 0.3.3
|
||||
pdfminer.six == 20181108
|
||||
pikepdf == 1.1.0
|
||||
pikepdf == 1.2.0
|
||||
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
|
||||
pycparser == 2.19
|
||||
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-xdist
|
||||
pytest-xdist == 1.28.0
|
||||
pytest-cov >= 2.6.1
|
||||
python-xmp-toolkit # requires apt-get install libexempi3
|
||||
# or brew install exempi
|
||||
|
||||
@@ -99,7 +99,7 @@ setup(
|
||||
'cffi >= 1.9.1', # must be a setup and install requirement
|
||||
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
|
||||
'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 has BytesIO/TIFF bug w/img2pdf 0.2.3
|
||||
# block 5.1.0, broken wheels
|
||||
|
||||
@@ -66,10 +66,6 @@ def complain(message):
|
||||
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
|
||||
|
||||
|
||||
+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.
|
||||
# pikepdf can deal with this, but we make the world a better place by
|
||||
# stamping them out as soon as possible.
|
||||
pdf_layers_file = pikepdf.open(layers_file)
|
||||
if pdf_layers_file.docinfo:
|
||||
modified = False
|
||||
for k, v in pdf_layers_file.docinfo.items():
|
||||
if b'\x00' in bytes(v):
|
||||
pdf_layers_file.docinfo[k] = bytes(v).replace(b'\x00', b'')
|
||||
modified = True
|
||||
if modified:
|
||||
pdf_layers_file.save(layers_file)
|
||||
del pdf_layers_file
|
||||
with pikepdf.open(layers_file) as pdf_layers_file:
|
||||
if pdf_layers_file.docinfo:
|
||||
modified = False
|
||||
for k, v in pdf_layers_file.docinfo.items():
|
||||
if b'\x00' in bytes(v):
|
||||
pdf_layers_file.docinfo[k] = bytes(v).replace(b'\x00', b'')
|
||||
modified = True
|
||||
if modified:
|
||||
pdf_layers_file.save(layers_file)
|
||||
|
||||
ps = next((ii for ii in input_files if ii.endswith('.ps')), None)
|
||||
ghostscript.generate_pdfa(
|
||||
@@ -880,6 +879,8 @@ def metadata_fixup(input_files_groups, output_file, log, context):
|
||||
compress_streams=True,
|
||||
object_stream_mode=pikepdf.ObjectStreamMode.generate,
|
||||
)
|
||||
original.close()
|
||||
pdf.close()
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
@@ -161,6 +161,7 @@ def _weave_layers_graft(
|
||||
_update_page_resources(
|
||||
page=base_page, font=font, font_key=font_key, procset=procset
|
||||
)
|
||||
pdf_text.close()
|
||||
|
||||
|
||||
def _find_font(text, pdf_base):
|
||||
@@ -169,20 +170,23 @@ def _find_font(text, pdf_base):
|
||||
font, font_key = None, None
|
||||
possible_font_names = ('/f-0-0', '/F1')
|
||||
try:
|
||||
pdf_text = pikepdf.open(text)
|
||||
pdf_text_fonts = pdf_text.pages[0].Resources.get('/Font', {})
|
||||
except Exception:
|
||||
with pikepdf.open(text) as pdf_text:
|
||||
try:
|
||||
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
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -317,21 +321,16 @@ def weave_layers(infiles, output_file, log, context):
|
||||
base = list(basegroup)[0]
|
||||
path_base = Path(base).resolve()
|
||||
pdf_base = pikepdf.open(path_base)
|
||||
keep_open = []
|
||||
font, font_key, procset = None, None, None
|
||||
pdfinfo = context.get_pdfinfo()
|
||||
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(
|
||||
pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]')
|
||||
)
|
||||
|
||||
replacements = 0
|
||||
|
||||
# Iterate rest
|
||||
for page_num, layers in groups:
|
||||
layers = list(layers)
|
||||
@@ -354,10 +353,10 @@ def weave_layers(infiles, output_file, log, context):
|
||||
log.debug("Replace")
|
||||
old_objgen = pdf_base.pages[page_num - 1].objgen
|
||||
|
||||
pdf_image = pikepdf.open(image)
|
||||
keep_open.append(pdf_image)
|
||||
image_page = pdf_image.pages[0]
|
||||
pdf_base.pages[page_num - 1] = image_page
|
||||
with pikepdf.open(image) as pdf_image:
|
||||
replacements += 1
|
||||
image_page = pdf_image.pages[0]
|
||||
pdf_base.pages[page_num - 1] = image_page
|
||||
|
||||
# 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
|
||||
@@ -396,20 +395,19 @@ def weave_layers(infiles, output_file, log, context):
|
||||
content_rotation - autorotate_correction
|
||||
) % 360
|
||||
|
||||
if len(keep_open) > MAX_OPEN_PAGE_PDFS:
|
||||
# qpdf limitations require us to keep files open when we intend
|
||||
# to copy content from them before saving. However, we want to keep
|
||||
# a lid on file handles and memory usage, so for big files we're
|
||||
# going to stop and save periodically. Attach the font to page 1
|
||||
# even if page 1 doesn't use it, so we have a way to get it back.
|
||||
if replacements % MAX_REPLACE_PAGES == 0:
|
||||
# Periodically save and reload the Pdf object. This will keep a
|
||||
# lid on our memory usage for very large files. Attach the font to
|
||||
# page 1 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]
|
||||
_update_page_resources(
|
||||
page=page0, font=font, font_key=font_key, procset=procset
|
||||
)
|
||||
interim = output_file + f'_working{page_num}.pdf'
|
||||
pdf_base.save(interim)
|
||||
del pdf_base
|
||||
keep_open = []
|
||||
pdf_base.close()
|
||||
|
||||
pdf_base = pikepdf.open(interim)
|
||||
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)
|
||||
pdf_base.save(output_file)
|
||||
pdf_base.close()
|
||||
|
||||
+15
-15
@@ -131,19 +131,19 @@ def file_claims_pdfa(filename):
|
||||
do full PDF/A validation.
|
||||
"""
|
||||
|
||||
pdf = pikepdf.open(filename)
|
||||
pdfmeta = pdf.open_metadata()
|
||||
if not pdfmeta.pdfa_status:
|
||||
return {
|
||||
'pass': False,
|
||||
'output': 'pdf',
|
||||
'conformance': 'No PDF/A metadata in XMP',
|
||||
}
|
||||
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||
conformance = f'PDF/A-{pdfmeta.pdfa_status}'
|
||||
pdfa_dict = {}
|
||||
if pdfmeta.pdfa_status in valid_part_conforms:
|
||||
pdfa_dict['pass'] = True
|
||||
pdfa_dict['output'] = 'pdfa'
|
||||
pdfa_dict['conformance'] = conformance
|
||||
with pikepdf.open(filename) as pdf:
|
||||
pdfmeta = pdf.open_metadata()
|
||||
if not pdfmeta.pdfa_status:
|
||||
return {
|
||||
'pass': False,
|
||||
'output': 'pdf',
|
||||
'conformance': 'No PDF/A metadata in XMP',
|
||||
}
|
||||
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||
conformance = f'PDF/A-{pdfmeta.pdfa_status}'
|
||||
pdfa_dict = {}
|
||||
if pdfmeta.pdfa_status in valid_part_conforms:
|
||||
pdfa_dict['pass'] = True
|
||||
pdfa_dict['output'] = 'pdfa'
|
||||
pdfa_dict['conformance'] = conformance
|
||||
return pdfa_dict
|
||||
|
||||
@@ -618,8 +618,9 @@ def _pdf_get_all_pageinfo(infile, detailed_analysis=False, log=None):
|
||||
if not log:
|
||||
log = Mock()
|
||||
|
||||
pdf = pikepdf.open(infile)
|
||||
pdf = pikepdf.open(infile) # Do not close in this function
|
||||
if pdf.is_encrypted:
|
||||
pdf.close()
|
||||
raise EncryptedPdfError() # Triggered by encryption with empty passwd
|
||||
if detailed_analysis:
|
||||
pages_xml = None
|
||||
@@ -755,7 +756,13 @@ class PdfInfo:
|
||||
infile, detailed_page_analysis, log=log
|
||||
)
|
||||
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
|
||||
def pages(self):
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ def test_no_glyphless_weave(resources, outdir):
|
||||
pdf.save(outdir / 'test.pdf')
|
||||
|
||||
env = os.environ.copy()
|
||||
env['_OCRMYPDF_MAX_OPEN_PAGE_PDFS'] = '2'
|
||||
env['_OCRMYPDF_MAX_REPLACE_PAGES'] = '2'
|
||||
check_ocrmypdf(
|
||||
outdir / 'test.pdf',
|
||||
outdir / 'out.pdf',
|
||||
|
||||
Reference in New Issue
Block a user