From 8b8de7cc1d783023d2408923c8fbdfea805e33ea Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Wed, 12 Jun 2019 17:27:47 -0700 Subject: [PATCH] Add new --pages feature to limit OCR to only specific pages --- src/ocrmypdf/_pipeline.py | 8 ++++-- src/ocrmypdf/_validation.py | 37 +++++++++++++++++++++--- src/ocrmypdf/api.py | 1 + src/ocrmypdf/cli.py | 5 ++++ src/ocrmypdf/helpers.py | 9 ++++++ tests/test_page_numbers.py | 56 +++++++++++++++++++++++++++++++++++++ 6 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 tests/test_page_numbers.py diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index cba5de41..e7454a78 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -251,12 +251,14 @@ def is_ocr_required(page_context): ocr_required = True - if pageinfo.has_text: + if options.pages and pageinfo.pageno not in options.pages: + log.debug(f"skipped {pageinfo.pageno} as requested by --pages {options.pages}") + ocr_required = False + elif pageinfo.has_text: if not options.force_ocr and not (options.skip_text or options.redo_ocr): - log.error( + raise PriorOcrFoundError( "page already has text! - aborting (use --force-ocr to force OCR)" ) - raise PriorOcrFoundError() elif options.force_ocr: log.info("page already has text! - rasterizing text and running OCR anyway") ocr_required = True diff --git a/src/ocrmypdf/_validation.py b/src/ocrmypdf/_validation.py index eacff184..8069ae1b 100644 --- a/src/ocrmypdf/_validation.py +++ b/src/ocrmypdf/_validation.py @@ -41,7 +41,7 @@ from .exec import ( tesseract, unpaper, ) -from .helpers import is_file_writable, re_symlink +from .helpers import is_file_writable, re_symlink, is_iterable_notstr, monotonic # ------------- # External dependencies @@ -172,6 +172,33 @@ def check_options_preprocessing(options): raise BadArgsError(str(e)) +def _pages_from_ranges(ranges): + if is_iterable_notstr(ranges): + return set(ranges) + pages = [] + page_groups = ranges.replace(' ', '').split(',') + for g in page_groups: + if not g: + continue + try: + start, end = g.split('-') + except ValueError: + pages.append(int(g) - 1) + else: + pages.extend(range(int(start) - 1, int(end))) + + if not monotonic(pages): + log.warning( + "List of pages to process contains duplicate pages, or pages that are " + "out of order" + ) + if any(page < 0 for page in pages): + raise BadArgsError("pages refers to a page number less than 1") + + log.debug("OCRing only these pages: %s", pages) + return set(pages) + + def check_options_ocr_behavior(options): exclusive_options = sum( [ @@ -180,9 +207,11 @@ def check_options_ocr_behavior(options): ] ) if exclusive_options >= 2: - raise BadArgsError( - "Error: choose only one of --force-ocr, --skip-text, --redo-ocr." - ) + raise BadArgsError("Choose only one of --force-ocr, --skip-text, --redo-ocr.") + if options.pages and options.sidecar: + raise BadArgsError("--pages and --sidecar are mutually exclusive") + if options.pages: + options.pages = _pages_from_ranges(options.pages) def check_options_optimizing(options): diff --git a/src/ocrmypdf/api.py b/src/ocrmypdf/api.py index 3bddf47c..e108d587 100644 --- a/src/ocrmypdf/api.py +++ b/src/ocrmypdf/api.py @@ -188,6 +188,7 @@ def ocrmypdf( # pylint: disable=unused-argument png_quality=None, jbig2_lossy=None, jbig2_page_group_size=None, + pages=None, max_image_mpixels=None, tesseract_config=None, tesseract_pagesegmode=None, diff --git a/src/ocrmypdf/cli.py b/src/ocrmypdf/cli.py index cca69960..6fe45275 100644 --- a/src/ocrmypdf/cli.py +++ b/src/ocrmypdf/cli.py @@ -380,6 +380,11 @@ optimizing.add_argument( advanced = parser.add_argument_group( "Advanced", "Advanced options to control Tesseract's OCR behavior" ) +advanced.add_argument( + '--pages', + type=str, + help="Limit OCR to the specified pages (ranges or comma separated), skipping others", +) advanced.add_argument( '--max-image-mpixels', action='store', diff --git a/src/ocrmypdf/helpers.py b/src/ocrmypdf/helpers.py index 683cc815..8d52a822 100644 --- a/src/ocrmypdf/helpers.py +++ b/src/ocrmypdf/helpers.py @@ -66,6 +66,15 @@ def re_symlink(input_file, soft_link_name, *args, **kwargs): os.symlink(os.path.abspath(input_file), soft_link_name) +def is_iterable_notstr(thing): + return isinstance(thing, Iterable) and not isinstance(thing, str) + + +def monotonic(L): + """Does list increase monotonically?""" + return all(b > a for a, b in zip(L, L[1:])) + + def page_number(input_file): """Get one-based page number implied by filename (000002.pdf -> 2)""" return int(os.path.basename(os.fspath(input_file))[0:6]) diff --git a/tests/test_page_numbers.py b/tests/test_page_numbers.py new file mode 100644 index 00000000..a870a987 --- /dev/null +++ b/tests/test_page_numbers.py @@ -0,0 +1,56 @@ +# © 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 pytest + +from ocrmypdf import ocrmypdf as run +from ocrmypdf._validation import _pages_from_ranges +from ocrmypdf.pdfinfo import PdfInfo + + +def test_str_ranges(): + assert _pages_from_ranges('43') == {42} + assert _pages_from_ranges('1, 2, 3') == {0, 1, 2} + assert _pages_from_ranges('1-3') == {0, 1, 2} + assert _pages_from_ranges('1-3,5,7,42') == {0, 1, 2, 4, 6, 41} + assert _pages_from_ranges('3, 3, 3, 3,') == {2} + + +def test_nonmonotonic_warning(caplog): + pages = _pages_from_ranges('1, 3, 2') + assert pages == {0, 1, 2} + assert 'out of order' in caplog.text + + +def test_list_range(): + assert _pages_from_ranges([0, 1, 2]) == {0, 1, 2} + + +def test_limited_pages(resources, outpdf, spoof_tesseract_cache): + multi = resources / 'multipage.pdf' + run( + multi, + outpdf, + pages='5-6', + optimize=0, + output_type='pdf', + tesseract_env=spoof_tesseract_cache, + ) + pi = PdfInfo(outpdf) + assert not pi.pages[0].has_text + assert pi.pages[4].has_text + assert pi.pages[5].has_text