From 4e2a98ead4f23001f8c62cbb90b0f74de8edbb6c Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 16 Mar 2019 14:08:58 -0700 Subject: [PATCH 01/28] leptonica: fix junkpixt harder --- src/ocrmypdf/leptonica.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ocrmypdf/leptonica.py b/src/ocrmypdf/leptonica.py index 0cc8b643..55de5b9b 100644 --- a/src/ocrmypdf/leptonica.py +++ b/src/ocrmypdf/leptonica.py @@ -614,9 +614,10 @@ class Pix(LeptonicaObject): except (LeptonicaError, ValueError, IndexError): return finally: - with suppress(FileNotFoundError): - os.unlink('junkpixt.png') # leptonica may produce this - os.unlink('junkpixt') + leptonica_junk = ('junkpixt.png', 'junkpixt') + for junk in leptonica_junk: + with suppress(FileNotFoundError): + os.unlink(junk) # leptonica may produce this for n, s in enumerate(sarray): decoded = s.decode() From c5cfaa950b2076de44cc93dc57759627594ee84e Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 16 Mar 2019 14:09:19 -0700 Subject: [PATCH 02/28] readme: tweaks --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 75415401..3509fb71 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ Main features - If requested deskews and/or cleans the image before performing OCR - Validates input and output files - Distributes work across all available CPU cores -- Uses [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) engine -- Supports more than [100 languages](https://github.com/tesseract-ocr/tessdata) recognized by Tesseract -- Battle-tested on thousands of PDFs, a test suite and continuous integration +- Uses [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) engine to recognize more than [100 languages](https://github.com/tesseract-ocr/tessdata) +- Scales properly to handle files with thousands of pages +- Battle-tested on millions of PDFs For details: please consult the [documentation](https://ocrmypdf.readthedocs.io/en/latest/). @@ -131,6 +131,11 @@ Press & Media - [c't 1-2014, page 59](http://heise.de/-2279695): Detailed presentation of OCRmyPDF v1.0 in the leading German IT magazine c't - [heise Open Source, 09/2014: Texterkennung mit OCRmyPDF](http://heise.de/-2356670) +Business enquiries +------------------ + +OCRmyPDF would not be the software that it is today is without companies and users choosing to provide support for feature development and consulting enquiries. We are happy to discuss all enquiries, whether for extending the existing feature set, or integrating OCRmyPDF into a larger system. + License ------- @@ -138,7 +143,7 @@ The OCRmyPDF software is licensed under the GNU GPLv3. Certain files are covered The license for each test file varies, and is noted in tests/resources/README.rst. The documentation is licensed under Creative Commons Attribution-ShareAlike 4.0 (CC-BY-SA 4.0). -OCRmyPDF versions prior to 6.0 were licensed under the MIT License. +OCRmyPDF versions prior to 6.0 were distributed under the MIT License. Disclaimer ---------- From 9c7ee2bf23acb117a8ecd08062fc8bd2985c5464 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 17 Mar 2019 13:29:25 -0700 Subject: [PATCH 03/28] Better help text for --verbose --- src/ocrmypdf/__main__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ocrmypdf/__main__.py b/src/ocrmypdf/__main__.py index 3f2042df..f685823e 100755 --- a/src/ocrmypdf/__main__.py +++ b/src/ocrmypdf/__main__.py @@ -231,7 +231,9 @@ jobcontrol.add_argument( default=[], nargs='?', action="append", - help="Print more verbose messages for each additional verbose level", + help="Print more verbose messages for each additional verbose level. Use " + "`-v 1` typically for much more detailed logging. Higher numbers " + "are probably only useful in debugging.", ) metadata = parser.add_argument_group( From 427afc0616103195677facb19aa58884a43b11e1 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 17 Mar 2019 14:22:36 -0700 Subject: [PATCH 04/28] Fix LeptonicaErrorTrap when a sys.stderr.fileno() is not available The LeptonicaErrorTrap was problematic for Celery and other libraries that mess with stderr. Closes #359 --- src/ocrmypdf/leptonica.py | 37 ++++++++++++++++++++----------------- tests/test_lept.py | 17 +++++++++++++++++ 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/ocrmypdf/leptonica.py b/src/ocrmypdf/leptonica.py index 55de5b9b..1cdec312 100644 --- a/src/ocrmypdf/leptonica.py +++ b/src/ocrmypdf/leptonica.py @@ -43,11 +43,6 @@ lept = ffi.dlopen(find_library('lept')) lept.setMsgSeverity(lept.L_SEVERITY_WARNING) -def stderr(*objs): - """Shorthand print to stderr.""" - print("leptonica.py:", *objs, file=sys.stderr) - - class _LeptonicaErrorTrap: """ Context manager to trap errors reported by Leptonica. @@ -66,6 +61,7 @@ class _LeptonicaErrorTrap: def __init__(self): self.tmpfile = None self.copy_of_stderr = -1 + self.no_stderr = False def __enter__(self): from io import UnsupportedOperation @@ -73,33 +69,40 @@ class _LeptonicaErrorTrap: self.tmpfile = TemporaryFile() # Save the old stderr, and redirect stderr to temporary file - sys.stderr.flush() + with suppress(AttributeError): + sys.stderr.flush() try: self.copy_of_stderr = os.dup(sys.stderr.fileno()) os.dup2(self.tmpfile.fileno(), sys.stderr.fileno(), inheritable=False) + except AttributeError: + # We are in some unusual context where our Python process does not + # have a sys.stderr. Leptonica still expects to write to file + # descriptor 2, so we are going to ensure it is redirected. + self.copy_of_stderr = None + self.no_stderr = True + os.dup2(self.tmpfile.fileno(), 2, inheritable=False) except UnsupportedOperation: self.copy_of_stderr = None return def __exit__(self, exc_type, exc_value, traceback): # Restore old stderr - sys.stderr.flush() + with suppress(AttributeError): + sys.stderr.flush() + if self.copy_of_stderr is not None: os.dup2(self.copy_of_stderr, sys.stderr.fileno()) os.close(self.copy_of_stderr) + if self.no_stderr: + os.close(2) - # Get data from tmpfile (in with block to ensure it is closed) - with self.tmpfile as tmpfile: - tmpfile.seek(0) # Cursor will be at end, so move back to beginning - leptonica_output = tmpfile.read().decode(errors='replace') - - assert self.tmpfile.closed - assert not sys.stderr.closed - - # If there are Python errors, let them bubble up + # Get data from tmpfile + self.tmpfile.seek(0) # Cursor will be at end, so move back to beginning + leptonica_output = self.tmpfile.read().decode(errors='replace') + self.tmpfile.close() + # If there are Python errors, record them if exc_type: logger.warning(leptonica_output) - return False # If there are Leptonica errors, wrap them in Python excpetions if 'Error' in leptonica_output: diff --git a/tests/test_lept.py b/tests/test_lept.py index 142fc702..5fe68109 100644 --- a/tests/test_lept.py +++ b/tests/test_lept.py @@ -17,7 +17,9 @@ from os import fspath +import os from pickle import dumps, loads +from unittest.mock import patch import pytest from PIL import Image, ImageChops @@ -85,3 +87,18 @@ def test_leptonica_compile(tmpdir): # existing compiled library. Also compile in API mode so that we test # the interfaces, even though we use it ABI mode. ffibuilder.compile(tmpdir=fspath(tmpdir), target=fspath(tmpdir / 'lepttest.*')) + + +def test_with_stderr(capsys): + # pytest redirects stderr too; we must disable this for the test to be valid + with capsys.disabled(): + with pytest.raises(FileNotFoundError): + lept.Pix.open("does_not_exist1") + + +def test_without_stderr(capsys): + # pytest redirects stderr too; we must disable this for the test to be valid + with capsys.disabled(): + with patch('sys.stderr', new=None): + with pytest.raises(FileNotFoundError): + lept.Pix.open("does_not_exist2") From 6e49bb358892999b455a689555bd132f5caffd94 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Wed, 3 Apr 2019 01:19:12 -0700 Subject: [PATCH 05/28] v8.2.3 notes --- docs/release_notes.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 70f4c434..861ce2dd 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -13,6 +13,15 @@ Note that it is licensed under GPLv3, so scripts that ``import ocrmypdf`` and ar find: [^`]\#([0-9]{1,3})[^0-9] replace: `#$1 `_ +v8.2.3 +------ + +- Fixed that ``--mask-barcodes`` would occasionally leave a unwanted temporary file named ``junkpixt`` in the current working folder. + +- Fixed (hopefully) handling of Leptonica errors in an environment where a non-standard ``sys.stderr`` is present. + +- Improved help text for ``--verbose``. + v8.2.2 ------ From 922a107b7ff124979546e0613fea494896d19280 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 13 Apr 2019 00:24:03 -0700 Subject: [PATCH 06/28] 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. --- src/ocrmypdf/_weave.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/ocrmypdf/_weave.py b/src/ocrmypdf/_weave.py index 4ea07e4c..86119ef9 100644 --- a/src/ocrmypdf/_weave.py +++ b/src/ocrmypdf/_weave.py @@ -322,12 +322,6 @@ def weave_layers(infiles, output_file, log, context): 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 ]') ) From 91cb092aa0a0bca56b9b31215e56eefcba0d13ee Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Thu, 18 Apr 2019 10:15:02 -0700 Subject: [PATCH 07/28] Remove PyCharm debugger hack --- src/ocrmypdf/__main__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ocrmypdf/__main__.py b/src/ocrmypdf/__main__.py index f685823e..d60c243b 100755 --- a/src/ocrmypdf/__main__.py +++ b/src/ocrmypdf/__main__.py @@ -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 From 9a86f531093713f9b9372ed845272a0880e53b3c Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Thu, 18 Apr 2019 10:42:10 -0700 Subject: [PATCH 08/28] Ignore pip-wheel-metadata folder https://github.com/pypa/pip/issues/6213 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 93954ec3..b2881fb4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ build/ dist/ wheelhouse/ +pip-wheel-metadata/ # Automatically generated files docs/_build/ From e0c8dadccef0b2a4c64793911f6f3594a5b06288 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Thu, 18 Apr 2019 11:27:00 -0700 Subject: [PATCH 09/28] Explicitly close most pikepdf.Pdf when done with them --- src/ocrmypdf/_pipeline.py | 21 ++++++++++--------- src/ocrmypdf/_weave.py | 36 ++++++++++++++++++-------------- src/ocrmypdf/pdfa.py | 30 +++++++++++++------------- src/ocrmypdf/pdfinfo/__init__.py | 5 +++-- 4 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index b283a988..ab11da6e 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -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): diff --git a/src/ocrmypdf/_weave.py b/src/ocrmypdf/_weave.py index 86119ef9..17e37144 100644 --- a/src/ocrmypdf/_weave.py +++ b/src/ocrmypdf/_weave.py @@ -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): """ @@ -348,10 +352,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: + keep_open.append(pdf_image) + 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 diff --git a/src/ocrmypdf/pdfa.py b/src/ocrmypdf/pdfa.py index 9bc3b729..684254e7 100644 --- a/src/ocrmypdf/pdfa.py +++ b/src/ocrmypdf/pdfa.py @@ -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 diff --git a/src/ocrmypdf/pdfinfo/__init__.py b/src/ocrmypdf/pdfinfo/__init__.py index 1dd94e60..4f2ba168 100644 --- a/src/ocrmypdf/pdfinfo/__init__.py +++ b/src/ocrmypdf/pdfinfo/__init__.py @@ -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,7 @@ class PdfInfo: infile, detailed_page_analysis, log=log ) self._needs_rendering = pdf.root.get('/NeedsRendering', False) - self._has_acroform = '/AcroForm' in pdf.root + pdf.close() @property def pages(self): From f615b6f0e87de8ccac2d8af6f26d060256395da0 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Thu, 18 Apr 2019 15:07:25 -0700 Subject: [PATCH 10/28] pdfinfo: be more specific about detecting XFA we can't render --- src/ocrmypdf/pdfinfo/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ocrmypdf/pdfinfo/__init__.py b/src/ocrmypdf/pdfinfo/__init__.py index 4f2ba168..aaad8ebe 100644 --- a/src/ocrmypdf/pdfinfo/__init__.py +++ b/src/ocrmypdf/pdfinfo/__init__.py @@ -756,6 +756,12 @@ class PdfInfo: infile, detailed_page_analysis, log=log ) self._needs_rendering = pdf.root.get('/NeedsRendering', False) + 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 From 58c29ffb5cc037aeb8f00e09237a5933ea1e31f0 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Thu, 18 Apr 2019 15:12:48 -0700 Subject: [PATCH 11/28] 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. --- setup.py | 2 +- src/ocrmypdf/_weave.py | 23 ++++++++++++----------- tests/test_weave.py | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/setup.py b/setup.py index 560434f8..81003662 100644 --- a/setup.py +++ b/setup.py @@ -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 diff --git a/src/ocrmypdf/_weave.py b/src/ocrmypdf/_weave.py index 17e37144..a66244a2 100644 --- a/src/ocrmypdf/_weave.py +++ b/src/ocrmypdf/_weave.py @@ -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): @@ -321,7 +321,6 @@ 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 = {} @@ -330,6 +329,8 @@ def weave_layers(infiles, output_file, log, context): pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]') ) + replacements = 0 + # Iterate rest for page_num, layers in groups: layers = list(layers) @@ -353,7 +354,7 @@ def weave_layers(infiles, output_file, log, context): old_objgen = pdf_base.pages[page_num - 1].objgen with pikepdf.open(image) as pdf_image: - keep_open.append(pdf_image) + replacements += 1 image_page = pdf_image.pages[0] pdf_base.pages[page_num - 1] = image_page @@ -394,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 @@ -415,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() diff --git a/tests/test_weave.py b/tests/test_weave.py index 34452215..372eddca 100644 --- a/tests/test_weave.py +++ b/tests/test_weave.py @@ -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', From 58b2bed99d92a939dea4554068a93f85c648ae35 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Tue, 23 Apr 2019 00:07:12 -0700 Subject: [PATCH 12/28] v8.2.4 notes --- docs/release_notes.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 861ce2dd..26d62161 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -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 `_ +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 ------ From 5398003160934ad245a8ae8655de57c70cf4e822 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Tue, 23 Apr 2019 00:42:40 -0700 Subject: [PATCH 13/28] Fix test.txt --- requirements/test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/test.txt b/requirements/test.txt index 90919da7..ad5ec593 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -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 From 79c84eefa353632a3d7ccddbd398c6678c1c1777 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Tue, 23 Apr 2019 02:21:31 -0700 Subject: [PATCH 14/28] Fix main.txt --- requirements/main.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/main.txt b/requirements/main.txt index b2ebc357..95dfc51b 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -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 From 7ed0f8f50eb81e31dfb59d11a46340b662a4655e Mon Sep 17 00:00:00 2001 From: Frank <50119297+FPille@users.noreply.github.com> Date: Tue, 7 May 2019 00:18:15 +0200 Subject: [PATCH 15/28] Add bash completion (#384) * Add bash completion file must be copied to completion folder e.g. /usr/share/bash-completion/completions --- misc/ocrmypdf | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 misc/ocrmypdf diff --git a/misc/ocrmypdf b/misc/ocrmypdf new file mode 100644 index 00000000..50d4a25e --- /dev/null +++ b/misc/ocrmypdf @@ -0,0 +1,87 @@ +# ocrmypdf completion -*- shell-script -*- + +_ocrmypdf() +{ + local cur prev cword words split + _init_completion -s || return + + case $prev in + --version|-h|--help) + return + ;; + --user-words|--user-patterns|--tesseract-config) + _filedir + return + ;; + --output-type) + COMPREPLY=( $( compgen -W 'pdfa pdf pdfa-1 pdfa-2 pdfa-3' -- \ + "$cur" ) ) + return + ;; + --pdf-renderer) + COMPREPLY=( $( compgen -W 'auto hocr sandwich' -- "$cur" ) ) + return + ;; + --pdfa-image-compression) + COMPREPLY=( $( compgen -W 'auto jpeg lossless' -- "$cur" ) ) + return + ;; + -O|--optimize|--tesseract-oem) + COMPREPLY=( $( compgen -W '{0..3}' -- "$cur" ) ) + return + ;; + --jpeg-quality|--png-quality) + COMPREPLY=( $( compgen -W '{0..100}' -- "$cur" ) ) + return + ;; + -l|--language) + COMPREPLY=$( command tesseract --list-langs 2>/dev/null ) + COMPREPLY=( $( compgen -W '${COMPREPLY[@]##*:}' -- "$cur" ) ) + return + ;; + --image-dpi|--oversample|--skip-big|--max-image-mpixels|\ + --tesseract-timeout|--rotate-pages-threshold) + COMPREPLY=( $( compgen -P "$cur" -W '{0..9}' ) ) + return + ;; + -j|--jobs) + COMPREPLY=( $( compgen -W '{1..'$( _ncpus )'}' -- "$cur" ) ) + return + ;; + -v|--verbose) + COMPREPLY=( $( compgen -W '{1..9}' -- "$cur" ) ) # max level ? + return + ;; + --tesseract-pagesegmode) + COMPREPLY=( $( compgen -W '{1..13}' -- "$cur" ) ) + return + ;; + --sidecar|--title|--author|--subject|--keywords|--unpaper-args) + # argument required but no completions available + return + ;; + esac + + $split && return + + if [[ $cur == -* ]]; then + COMPREPLY=( $( compgen -W '--language --image-dpi --output-type + --sidecar --version --jobs --quiet --verbose --title --author + --subject --keywords --rotate-pages --remove-background --deskew + --clean --clean-final --unpaper-args --oversample --remove-vectors + --mask-barcodes --threshold --force-ocr --skip-text --redo-ocr + --skip-big --jpeg-quality --png-quality --jbig2-lossy + --max-image-mpixels --tesseract-config --tesseract-pagesegmode + --help --tesseract-oem --pdf-renderer --tesseract-timeout + --rotate-pages-threshold --pdfa-image-compression --user-words + --user-patterns --keep-temporary-files --flowchart --output-type' \ + -- "$cur" ) ) + return + else + _filedir + return + fi +} && +complete -F _ocrmypdf ocrmypdf + +# ex: filetype=sh From 4d5e0eb749d82cb7a115b88c30a4c7c2b1c670a5 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Mon, 6 May 2019 18:07:41 -0700 Subject: [PATCH 16/28] docs: mention completions --- .pre-commit-config.yaml | 2 +- docs/installation.rst | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b9a75aff..b268b628 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,4 +3,4 @@ repos: rev: stable hooks: - id: black - language_version: python3.6 + language_version: python3.7 diff --git a/docs/installation.rst b/docs/installation.rst index 555ad0d5..014f769f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -490,3 +490,16 @@ To install all of the development and test requirements: pip install -r requirements/dev.txt -r requirements/test.txt To add JBIG2 encoding, see :ref:`jbig2`. + +Shell completions +----------------- + +Completions for ``bash`` and ``fish`` are available in the project's ``misc/`` folder. +The ``bash`` completions are likely ``zsh`` compatible but this has not been confirmed. +Package maintainers, please install these at the appropriate locations for your system. + +To manually install the ``bash`` completion, copy ``misc/ocrmypdf`` to +``/etc/bash_completion.d/``. + +To manually install the ``fish`` completion, copy ``misc/completions.fish`` to +``~/.config/fish/completions/ocrmypdf.fish``. From 0cd576e701550add496f3b063bc223ca87556af9 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 11 May 2019 10:52:42 -0700 Subject: [PATCH 17/28] Rename bash completions file --- misc/{ocrmypdf => completions.bash} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename misc/{ocrmypdf => completions.bash} (100%) diff --git a/misc/ocrmypdf b/misc/completions.bash similarity index 100% rename from misc/ocrmypdf rename to misc/completions.bash From bcdd19669917aa0b1e3d4aca945ebf569d83c8f3 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 11 May 2019 12:10:50 -0700 Subject: [PATCH 18/28] ghostscript: remove unnecessary post-render resizing step --- src/ocrmypdf/exec/ghostscript.py | 20 +------- tests/test_ghostscript.py | 81 ++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 tests/test_ghostscript.py diff --git a/src/ocrmypdf/exec/ghostscript.py b/src/ocrmypdf/exec/ghostscript.py index af91960b..7973885a 100644 --- a/src/ocrmypdf/exec/ghostscript.py +++ b/src/ocrmypdf/exec/ghostscript.py @@ -129,8 +129,7 @@ def rasterize_pdf( :param filter_vector: if True, remove vector graphics objects :return: """ - res = xres, yres - int_res = round(xres), round(yres) + res = round(xres, 6), round(yres, 6) if not page_dpi: page_dpi = res @@ -145,7 +144,7 @@ def rasterize_pdf( f'-sDEVICE={raster_device}', f'-dFirstPage={pageno}', f'-dLastPage={pageno}', - f'-r{str(int_res[0])}x{str(int_res[1])}', + f'-r{res[0]:f}x{res[1]:f}', ] + (['-dFILTERVECTOR'] if filter_vector else []) + [ @@ -168,23 +167,8 @@ def rasterize_pdf( log.error('Ghostscript rasterizing failed') raise SubprocessOutputError() - # Ghostscript only accepts integers for output resolution - # if the resolution happens to be fractional, then the discrepancy - # would change the size of the output page, especially if the DPI - # is quite low. Resize the image to the expected size - tmp.seek(0) with Image.open(tmp) as im: - expected_size = ( - round(im.size[0] / int_res[0] * res[0]), - round(im.size[1] / int_res[1] * res[1]), - ) - if expected_size != im.size or page_dpi != (xres, yres): - log.debug( - f"Ghostscript: resize output image {im.size} -> {expected_size}" - ) - im = im.resize(expected_size) - if rotation is not None: log.debug("Rotating output by %i", rotation) # rotation is a clockwise angle and Image.ROTATE_* is diff --git a/tests/test_ghostscript.py b/tests/test_ghostscript.py new file mode 100644 index 00000000..8756e8a8 --- /dev/null +++ b/tests/test_ghostscript.py @@ -0,0 +1,81 @@ +# © 2019 James R. Barlow: github.com/jbarlow83 +# +# This file is part of OCRmyPDF. +# +# OCRmyPDF is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# OCRmyPDF is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with OCRmyPDF. If not, see . + +import logging +from decimal import Decimal + +import pikepdf +import pytest +from PIL import Image + +from ocrmypdf.exec.ghostscript import rasterize_pdf + + +@pytest.fixture +def linn(resources): + path = resources / 'linn.pdf' + return path, pikepdf.open(path) + + +def test_rasterize_size(linn, outdir, caplog): + path, pdf = linn + page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3]) + assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0 + page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72)) + target_size = Decimal('200.0'), Decimal('150.0') + target_dpi = 42.0, 4242.0 + + log = logging.getLogger() + rasterize_pdf( + path, + outdir / 'out.png', + target_size[0] / page_size[0], + target_size[1] / page_size[1], + raster_device='pngmono', + log=log, + page_dpi=target_dpi, + ) + + with Image.open(outdir / 'out.png') as im: + assert im.size == target_size + assert im.info['dpi'] == target_dpi + + +def test_rasterize_rotated(linn, outdir, caplog): + path, pdf = linn + page_size_pts = (pdf.pages[0].MediaBox[2], pdf.pages[0].MediaBox[3]) + assert pdf.pages[0].MediaBox[0] == pdf.pages[0].MediaBox[1] == 0 + page_size = (page_size_pts[0] / Decimal(72), page_size_pts[1] / Decimal(72)) + target_size = Decimal('200.0'), Decimal('150.0') + target_dpi = 42.0, 4242.0 + + log = logging.getLogger() + caplog.set_level(logging.DEBUG) + rasterize_pdf( + path, + outdir / 'out.png', + target_size[0] / page_size[0], + target_size[1] / page_size[1], + raster_device='pngmono', + log=log, + page_dpi=target_dpi, + rotation=90, + ) + + with Image.open(outdir / 'out.png') as im: + assert im.size == (target_size[1], target_size[0]) + assert im.info['dpi'] == (target_dpi[1], target_dpi[0]) From 83398e54eaf6307436d51f74d228bfe2b49917f1 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 11 May 2019 12:22:21 -0700 Subject: [PATCH 19/28] weave: fix corruption of certain high page count files Corruption occurred when replacements was not incremented for multiple consecutive pages. --- src/ocrmypdf/_weave.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/ocrmypdf/_weave.py b/src/ocrmypdf/_weave.py index a66244a2..fb5f3c22 100644 --- a/src/ocrmypdf/_weave.py +++ b/src/ocrmypdf/_weave.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with OCRmyPDF. If not, see . +from contextlib import suppress from itertools import groupby from pathlib import Path import os @@ -329,7 +330,8 @@ def weave_layers(infiles, output_file, log, context): pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]') ) - replacements = 0 + replacements = 1 + interim_count = 0 # Iterate rest for page_num, layers in groups: @@ -405,13 +407,25 @@ def weave_layers(infiles, output_file, log, context): _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) + + # We cannot read and write the same file, that will corrupt it + # but we don't to keep more copies than we need to. Delete intermediates. + # {interim_count} is the opened file we were updateing + # {interim_count - 1} can be deleted + # {interim_count + 1} is the new file will produce and open + old_file = output_file + f'_working{interim_count - 1}.pdf' + if not context.get_options().keep_temporary_files: + with suppress(FileNotFoundError): + os.unlink(old_file) + + next_file = output_file + f'_working{interim_count + 1}.pdf' + pdf_base.save(next_file) pdf_base.close() - pdf_base = pikepdf.open(interim) + pdf_base = pikepdf.open(next_file) procset = pdf_base.pages[0].Resources.ProcSet - font, font_key = None, None # Reacquire this information + font, font_key = None, None # Ensure we reacquire this information + interim_count += 1 _fix_toc(pdf_base, pagerefs, log) pdf_base.save(output_file) From 15a988b99960ce3249db032b030ff312104adf2d Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 11 May 2019 12:40:25 -0700 Subject: [PATCH 20/28] weave: use emplacement method, scrap TOC repair The new emplacement method updates page objects in place without generating new objgen numbers, meaning we no longer need to update the table of contents to preserve links. --- src/ocrmypdf/_weave.py | 128 +++++------------------------------------ tests/test_weave.py | 20 ------- 2 files changed, 14 insertions(+), 134 deletions(-) diff --git a/src/ocrmypdf/_weave.py b/src/ocrmypdf/_weave.py index fb5f3c22..6b3c8cdd 100644 --- a/src/ocrmypdf/_weave.py +++ b/src/ocrmypdf/_weave.py @@ -189,99 +189,6 @@ def _find_font(text, pdf_base): return None, None -def _traverse_toc(pdf_base, visitor_fn, log): - """ - Walk the table of contents, calling visitor_fn() at each node - - The /Outlines data structure is a messy data structure, but rather than - navigating hierarchically we just track unique nodes. Enqueue nodes when - we find them, and never visit them again. set() is awesome. We look for - the two types of object in the table of contents that can be page bookmarks - and update the page entry. - - """ - - visited = set() - queue = set() - link_keys = ('/Parent', '/First', '/Last', '/Prev', '/Next') - - if not '/Outlines' in pdf_base.root: - return - - queue.add(pdf_base.root.Outlines.objgen) - while queue: - objgen = queue.pop() - visited.add(objgen) - node = pdf_base.get_object(objgen) - log.debug('fix toc: exploring outline entries at %r', objgen) - - # Enumerate other nodes we could visit from here - for key in link_keys: - if key not in node: - continue - item = node[key] - if not item.is_indirect: - # Direct references are not allowed here, but it's not clear - # what we should do if we find any. Removing them is an option: - # node[key] = pdf_base.make_indirect(None) - continue - objgen = item.objgen - if objgen not in visited: - queue.add(objgen) - - if visitor_fn: - visitor_fn(pdf_base, node, log) - - -def _fix_toc(pdf_base, pageref_remap, log): - """Repair the table of contents - - Whenever we replace a page wholesale, it gets assigned a new objgen number - and other references to it within the PDF become invalid, most notably in - the table of contents (/Outlines in PDF-speak). In weave_layers we collect - pageref_remap, a mapping that describes the new objgen number given an old - one. (objgen is a tuple, and the gen is almost always zero.) - - It may ultimately be better to find a way to rebuild a page in place. - - """ - - if not pageref_remap: - return - - def remap_dest(dest_node): - """ - Inner helper function: change the objgen for any page from the old we - invalidated to its new one. - """ - try: - pageref = dest_node[0] - if pageref['/Type'] == '/Page' and pageref.objgen in pageref_remap: - new_objgen = pageref_remap[pageref.objgen] - dest_node[0] = pdf_base.get_object(new_objgen) - except (IndexError, TypeError) as e: - log.warning("This file may contain invalid table of contents entries") - log.debug(e) - - def visit_remap_dest(pdf_base, node, log): - """ - Visitor function to fix ToC entries - - Test for the two types of references to pages that can occur in ToCs. - Both types have the same final format (an indirect reference to the - target page). - """ - if '/Dest' in node: - # /Dest reference to another page (old method) - remap_dest(node['/Dest']) - elif '/A' in node: - # /A (action) command set to "GoTo" (newer method) - if '/S' in node['/A'] and node['/A']['/S'] == '/GoTo': - remap_dest(node['/A']['/D']) - - _traverse_toc(pdf_base, visit_remap_dest, log) - - def weave_layers(infiles, output_file, log, context): """Apply text layer and/or image layer changes to baseline file @@ -324,13 +231,12 @@ def weave_layers(infiles, output_file, log, context): pdf_base = pikepdf.open(path_base) font, font_key, procset = None, None, None pdfinfo = context.get_pdfinfo() - pagerefs = {} procset = pdf_base.make_indirect( pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]') ) - replacements = 1 + emplacements = 1 interim_count = 0 # Iterate rest @@ -345,30 +251,25 @@ def weave_layers(infiles, output_file, log, context): if text and not font: font, font_key = _find_font(text, pdf_base) - replacing = False + emplaced_page = False content_rotation = pdfinfo[page_num - 1].rotation path_image = Path(image).resolve() if image else None if path_image is not None and path_image != path_base: - # We are replacing the old page with a rasterized PDF of the new - # page - log.debug("Replace") - old_objgen = pdf_base.pages[page_num - 1].objgen - + # We are updating the old page with a rasterized PDF of the new + # page (without changing objgen, to preserve references) + log.debug("Emplacement update") 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 - # my attempt to update the old object in place, but that is an - # option to consider - pagerefs[old_objgen] = pdf_base.pages[page_num - 1].objgen - replacing = True + emplacements += 1 + foreign_image_page = pdf_image.pages[0] + pdf_base.pages.append(foreign_image_page) + local_image_page = pdf_base.pages[-1] + pdf_base.pages[page_num - 1].emplace(local_image_page) + del pdf_base.pages[-1] + emplaced_page = True autorotate_correction = context.get_rotation(page_num - 1) - if replacing: + if emplaced_page: content_rotation = autorotate_correction text_rotation = autorotate_correction text_misaligned = (text_rotation - content_rotation) % 360 @@ -397,7 +298,7 @@ def weave_layers(infiles, output_file, log, context): content_rotation - autorotate_correction ) % 360 - if replacements % MAX_REPLACE_PAGES == 0: + if emplacements % 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 @@ -427,6 +328,5 @@ def weave_layers(infiles, output_file, log, context): font, font_key = None, None # Ensure we reacquire this information interim_count += 1 - _fix_toc(pdf_base, pagerefs, log) pdf_base.save(output_file) pdf_base.close() diff --git a/tests/test_weave.py b/tests/test_weave.py index 372eddca..a603673b 100644 --- a/tests/test_weave.py +++ b/tests/test_weave.py @@ -15,35 +15,15 @@ # You should have received a copy of the GNU General Public License # along with OCRmyPDF. If not, see . -from unittest.mock import MagicMock -import logging import os import pytest import pikepdf -from ocrmypdf._weave import _fix_toc, _update_page_resources check_ocrmypdf = pytest.helpers.check_ocrmypdf -def test_invalid_toc(resources, outdir, caplog): - pdf = pikepdf.open(resources / 'toc.pdf') - - # Corrupt a TOC entry - pdf.Root.Outlines.Last.Dest = pikepdf.Array([None, 0.0, 0.1, 0.2]) - pdf.save(outdir / 'test.pdf') - - pdf = pikepdf.open(outdir / 'test.pdf') - remap = {} - remap[pdf.pages[0].objgen] = pdf.pages[0].objgen # Dummy remap - - # Confirm we complain about the TOC and don't throw an exception - log = logging.getLogger() - _fix_toc(pdf, remap, log) - assert 'invalid table of contents entries' in caplog.text - - def test_no_glyphless_weave(resources, outdir): pdf = pikepdf.open(resources / 'francais.pdf') pdf_aspect = pikepdf.open(resources / 'aspect.pdf') From 482cb788ed79e13df462d0b990a7b6317b679d9e Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 11 May 2019 12:44:17 -0700 Subject: [PATCH 21/28] Don't use MagicMock() as a dummy logger in pytest --- tests/test_metadata.py | 11 ++++++----- tests/test_unpaper.py | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index be1d952c..442c71a1 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -285,7 +285,7 @@ def test_kodak_toc(resources, outpdf, spoof_tesseract_noop): assert isinstance(p.root.Outlines.First, pikepdf.Dictionary) -def test_metadata_fixup_warning(resources, outdir): +def test_metadata_fixup_warning(resources, outdir, caplog): from ocrmypdf._pipeline import metadata_fixup input_files = [ @@ -296,7 +296,7 @@ def test_metadata_fixup_warning(resources, outdir): for f in input_files: copyfile(resources / 'graph.pdf', f) - log = MagicMock() + log = logging.getLogger() context = MagicMock() metadata_fixup( input_files_groups=input_files, @@ -304,7 +304,8 @@ def test_metadata_fixup_warning(resources, outdir): log=log, context=context, ) - log.warning.assert_not_called() + for record in caplog.records: + assert record.levelname != 'WARNING' # Now add some metadata that will not be copyable graph = pikepdf.open(outdir / 'graph.repaired.pdf') @@ -312,7 +313,7 @@ def test_metadata_fixup_warning(resources, outdir): meta['prism2:publicationName'] = 'OCRmyPDF Test' graph.save(outdir / 'graph.repaired.pdf') - log = MagicMock() + log = logging.getLogger() context = MagicMock() metadata_fixup( input_files_groups=input_files, @@ -320,7 +321,7 @@ def test_metadata_fixup_warning(resources, outdir): log=log, context=context, ) - log.warning.assert_called_once() + assert any(record.levelname == 'WARNING' for record in caplog.records) def test_prevent_gs_invalid_xml(resources, outdir): diff --git a/tests/test_unpaper.py b/tests/test_unpaper.py index 23f6d698..3133f63c 100644 --- a/tests/test_unpaper.py +++ b/tests/test_unpaper.py @@ -16,9 +16,10 @@ # along with OCRmyPDF. If not, see . import argparse +import logging from os import fspath from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest @@ -57,7 +58,7 @@ def test_no_unpaper(resources, no_outpdf): with patch("ocrmypdf.exec.unpaper.version") as mock_unpaper_version: mock_unpaper_version.side_effect = FileNotFoundError("unpaper") with pytest.raises(SystemExit): - main.check_options(options, log=MagicMock()) + main.check_options(options, log=logging.getLogger()) def test_old_unpaper(spoof_unpaper_oldversion, resources, no_outpdf): From 7dea0a836259741dab7242d669655ba9456605de Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 11 May 2019 12:50:44 -0700 Subject: [PATCH 22/28] v8.3.0 release notes in progress --- docs/release_notes.rst | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 26d62161..46634f79 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -13,6 +13,21 @@ Note that it is licensed under GPLv3, so scripts that ``import ocrmypdf`` and ar find: [^`]\#([0-9]{1,3})[^0-9] replace: `#$1 `_ +v8.3.0 +------ + +- Improved the strategy for replacing pages, emplacing the contents in an existing page. This preserves any types of references to the original page, and eliminates the need to manually fix the table of contents. + +- For PDFs with more than 100 pages and a sequence where one PDF page was replaced and one or more subsequent ones were skipped, an intermediate file would be corrupted while grafting OCR text, causing processing to fail. + +- Previously, we resized the images produced by Ghostscript by a small number of pixels to ensure the output image size was an exactly what we wanted. Having discovered a way to get Ghostscript to produce the exact image sizes we require, we eliminated the resizing step. + +- Command line completions for ``bash`` are now available, in addition to ``fish``, both in ``misc/``. Package maintainers, please install these so users can take advantage. + +- Updated requirements. + +- pikepdf 1.3.0 is now required. + v8.2.4 ------ @@ -22,7 +37,7 @@ v8.2.4 - 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 +- pikepdf 1.2.0 is now required. v8.2.3 ------ From c2fecffdb4f406f2572cb065eac5da213c016da6 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 12 May 2019 02:16:05 -0700 Subject: [PATCH 23/28] Require pikepdf 1.3.0 --- requirements/main.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/main.txt b/requirements/main.txt index 95dfc51b..383d7189 100644 --- a/requirements/main.txt +++ b/requirements/main.txt @@ -5,7 +5,7 @@ chardet == 3.0.4 cffi == 1.12.2 img2pdf == 0.3.3 pdfminer.six == 20181108 -pikepdf == 1.2.0 +pikepdf == 1.3.0 Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin" pycparser == 2.19 python-xmp-toolkit == 2.0.1 diff --git a/setup.py b/setup.py index 81003662..b5c959ea 100644 --- a/setup.py +++ b/setup.py @@ -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.2.0, < 2', + 'pikepdf >= 1.3.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 From 5adbddd1f9c2fe979b3786ba106ac0cee1a0c86c Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 12 May 2019 03:36:07 -0700 Subject: [PATCH 24/28] ghostscript: rendering threads has no effect on pdfwrite, so remove it See https://www.ghostscript.com/doc/current/Language.htm --- src/ocrmypdf/exec/ghostscript.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ocrmypdf/exec/ghostscript.py b/src/ocrmypdf/exec/ghostscript.py index 7973885a..70497092 100644 --- a/src/ocrmypdf/exec/ghostscript.py +++ b/src/ocrmypdf/exec/ghostscript.py @@ -253,7 +253,6 @@ def generate_pdfa( "-dBATCH", "-dNOPAUSE", "-dCompatibilityLevel=" + str(pdf_version), - "-dNumRenderingThreads=" + str(threads), "-sDEVICE=pdfwrite", "-dAutoRotatePages=/None", "-sColorConversionStrategy=" + strategy, From 0a72c12ff00e8328f7bfcb488d7c2a5d5b7453f1 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 12 May 2019 03:36:33 -0700 Subject: [PATCH 25/28] weave: add new test for link consistency --- tests/resources/link.pdf | Bin 0 -> 72954 bytes tests/test_weave.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 tests/resources/link.pdf diff --git a/tests/resources/link.pdf b/tests/resources/link.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2633d975066f3f5a50cbbfb228d57f5290a4ad59 GIT binary patch literal 72954 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�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 literal 0 HcmV?d00001 diff --git a/tests/test_weave.py b/tests/test_weave.py index a603673b..06fa2a0a 100644 --- a/tests/test_weave.py +++ b/tests/test_weave.py @@ -42,3 +42,21 @@ def test_no_glyphless_weave(resources, outdir): '0', env=env, ) + + +@pytest.helpers.needs_pdfminer +def test_links(resources, outpdf): + check_ocrmypdf( + resources / 'link.pdf', + outpdf, + '--redo-ocr', + '--oversample', + '200', + '--output-type', + 'pdf', + ) + pdf = pikepdf.open(outpdf) + p1 = pdf.pages[0] + p2 = pdf.pages[1] + assert p1.Annots[0].A.D[0].objgen == p2.objgen + assert p2.Annots[0].A.D[0].objgen == p1.objgen From 84ec83151d31728e9bd10556648287b0b8071053 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 12 May 2019 03:36:41 -0700 Subject: [PATCH 26/28] v8.3.0 notes: clarify --- docs/release_notes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 46634f79..e646d038 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -16,7 +16,7 @@ Note that it is licensed under GPLv3, so scripts that ``import ocrmypdf`` and ar v8.3.0 ------ -- Improved the strategy for replacing pages, emplacing the contents in an existing page. This preserves any types of references to the original page, and eliminates the need to manually fix the table of contents. +- Improved the strategy for updating pages when a new image of the page was produced. We know attempt to preserve more content from the original file, for annotations in particular. - For PDFs with more than 100 pages and a sequence where one PDF page was replaced and one or more subsequent ones were skipped, an intermediate file would be corrupted while grafting OCR text, causing processing to fail. From 7e388f59af13aa5907233e7b9b84d9d0c7cdf021 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Mon, 13 May 2019 00:22:52 -0700 Subject: [PATCH 27/28] Move completions to better location/Homebrew compat --- docs/installation.rst | 13 +++++++------ docs/release_notes.rst | 2 +- misc/{completions.bash => completion/ocrmypdf.bash} | 0 misc/{completions.fish => completion/ocrmypdf.fish} | 0 4 files changed, 8 insertions(+), 7 deletions(-) rename misc/{completions.bash => completion/ocrmypdf.bash} (100%) rename misc/{completions.fish => completion/ocrmypdf.fish} (100%) diff --git a/docs/installation.rst b/docs/installation.rst index 014f769f..59f9d58e 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -494,12 +494,13 @@ To add JBIG2 encoding, see :ref:`jbig2`. Shell completions ----------------- -Completions for ``bash`` and ``fish`` are available in the project's ``misc/`` folder. -The ``bash`` completions are likely ``zsh`` compatible but this has not been confirmed. -Package maintainers, please install these at the appropriate locations for your system. +Completions for ``bash`` and ``fish`` are available in the project's +``misc/completion`` folder. The ``bash`` completions are likely ``zsh`` +compatible but this has not been confirmed. Package maintainers, please install +these at the appropriate locations for your system. -To manually install the ``bash`` completion, copy ``misc/ocrmypdf`` to -``/etc/bash_completion.d/``. +To manually install the ``bash`` completion, copy ``misc/completion/ocrmypdf.bash`` to +``/etc/bash_completion.d/ocrmypdf`` (rename the file). -To manually install the ``fish`` completion, copy ``misc/completions.fish`` to +To manually install the ``fish`` completion, copy ``misc/completion/ocrmypdf.fish`` to ``~/.config/fish/completions/ocrmypdf.fish``. diff --git a/docs/release_notes.rst b/docs/release_notes.rst index e646d038..6af0f263 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -22,7 +22,7 @@ v8.3.0 - Previously, we resized the images produced by Ghostscript by a small number of pixels to ensure the output image size was an exactly what we wanted. Having discovered a way to get Ghostscript to produce the exact image sizes we require, we eliminated the resizing step. -- Command line completions for ``bash`` are now available, in addition to ``fish``, both in ``misc/``. Package maintainers, please install these so users can take advantage. +- Command line completions for ``bash`` are now available, in addition to ``fish``, both in ``misc/completion``. Package maintainers, please install these so users can take advantage. - Updated requirements. diff --git a/misc/completions.bash b/misc/completion/ocrmypdf.bash similarity index 100% rename from misc/completions.bash rename to misc/completion/ocrmypdf.bash diff --git a/misc/completions.fish b/misc/completion/ocrmypdf.fish similarity index 100% rename from misc/completions.fish rename to misc/completion/ocrmypdf.fish From 07d4fff3d43f9eaab3f83e5ebb83b8fa683e1d05 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Tue, 14 May 2019 02:13:56 -0700 Subject: [PATCH 28/28] docs: mention FreeBSD works --- docs/installation.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index 59f9d58e..73380839 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -364,6 +364,16 @@ The command line program should now be available: ocrmypdf --help +Installing on FreeBSD +--------------------- + +FreeBSD 11.2 is known to work. Other versions likely work but have not been tested. + +In general it should work to: + +#. `Install and build pikepdf `_. +#. Install the equivalent list of dependencies for Linux. + Installing the Docker image ---------------------------