Compare commits

...
45 Commits
Author SHA1 Message Date
James R. Barlow 12769b96e5 Drop support for omitting pdfminer.six 2019-07-10 13:37:01 -07:00
James R. Barlow ee92ce8717 gitattributes: ensure afdesign is okay 2019-07-08 22:16:47 -07:00
James R. Barlow 2cff6ad2d1 Fixed blank pages produced when NULs removed from metadata 2019-07-03 02:22:50 -07:00
James R. Barlow 1cc4c45b7e docs: mention WSL works
[ci skip]
2019-07-03 00:49:06 -07:00
James R. Barlow 07d4fff3d4 docs: mention FreeBSD works 2019-05-14 02:13:56 -07:00
James R. Barlow 7e388f59af Move completions to better location/Homebrew compat 2019-05-13 00:22:52 -07:00
James R. Barlow 84ec83151d v8.3.0 notes: clarify 2019-05-12 03:36:41 -07:00
James R. Barlow 0a72c12ff0 weave: add new test for link consistency 2019-05-12 03:36:33 -07:00
James R. Barlow 5adbddd1f9 ghostscript: rendering threads has no effect on pdfwrite, so remove it
See
https://www.ghostscript.com/doc/current/Language.htm
2019-05-12 03:36:07 -07:00
James R. Barlow c2fecffdb4 Require pikepdf 1.3.0 2019-05-12 02:16:05 -07:00
James R. Barlow 7dea0a8362 v8.3.0 release notes in progress 2019-05-11 12:50:44 -07:00
James R. Barlow 482cb788ed Don't use MagicMock() as a dummy logger in pytest 2019-05-11 12:44:17 -07:00
James R. Barlow 15a988b999 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.
2019-05-11 12:40:25 -07:00
James R. Barlow 83398e54ea weave: fix corruption of certain high page count files
Corruption occurred when replacements was not incremented for multiple
consecutive pages.
2019-05-11 12:22:21 -07:00
James R. Barlow bcdd196699 ghostscript: remove unnecessary post-render resizing step 2019-05-11 12:10:50 -07:00
James R. Barlow 0cd576e701 Rename bash completions file 2019-05-11 10:52:42 -07:00
James R. Barlow 4d5e0eb749 docs: mention completions 2019-05-06 18:07:41 -07:00
Frankandjbarlow83 7ed0f8f50e Add bash completion (#384)
* Add bash completion

file must be copied to completion folder e.g. /usr/share/bash-completion/completions
2019-05-06 15:18:15 -07:00
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
James R. Barlow 6e49bb3588 v8.2.3 notes 2019-04-03 01:19:12 -07:00
James R. Barlow 427afc0616 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
2019-03-17 14:22:36 -07:00
James R. Barlow 9c7ee2bf23 Better help text for --verbose 2019-03-17 13:29:25 -07:00
James R. Barlow c5cfaa950b readme: tweaks 2019-03-16 14:09:19 -07:00
James R. Barlow 4e2a98ead4 leptonica: fix junkpixt harder 2019-03-16 14:08:58 -07:00
James R. Barlow 210f134b5b Merge branch 'master' of github.com:jbarlow83/OCRmyPDF 2019-03-08 15:38:31 -08:00
James R. Barlow 696c0721a0 docs: fix broken sphinx ref
[ci skip]
2019-03-08 15:38:26 -08:00
James R. Barlow aabab95418 docs: use images folder 2019-03-08 15:38:01 -08:00
James R. Barlow 7d614dd68b docs: explain Automator workflow 2019-03-08 15:37:42 -08:00
jumbliesandjbarlow83 f57dda7939 Update batch.rst (#362)
Added docker instructions for passing "find" filenames into container.  Obviates prior incorrect flag fix.
2019-03-08 12:46:50 -08:00
James R. Barlow 1b4542aa77 Further fixes to external program version testing 2019-03-07 14:27:16 -08:00
James R. Barlow 6c7fca57ec v8.2.1 notes 2019-03-06 22:22:50 -08:00
James R. Barlow 486dc7e22c Fix some test failures missed in prev commit 2019-03-06 13:28:50 -08:00
James R. Barlow dc616bb507 Fix test suite so --clean is not requested when unpaper is not installed 2019-03-05 22:33:13 -08:00
James R. Barlow 902bda43e3 main: fix version testing unnecessarily throwing exception to itself 2019-03-05 22:32:06 -08:00
James R. Barlow f7da63f68b main: fix redundant argument test 2019-03-05 22:29:29 -08:00
James R. Barlow 5da26e4c9c Convert most uses of subprocess.Popen to subprocess.run in test suite 2019-03-05 22:25:22 -08:00
James R. Barlow c19c852705 Fix exception while attempting to print error message for missing program 2019-03-05 16:32:48 -08:00
37 changed files with 605 additions and 452 deletions
+1
View File
@@ -9,5 +9,6 @@
*.png binary
*.jpg binary
*.bin binary
*.afdesign binary
.git_archival.txt export-subst
+4 -1
View File
@@ -3,9 +3,10 @@
.pylintrc
.pytest_cache/
.ruffus_history.sqlite
.venv/
.venv*/
*.pyc
*.sublime-*
*.DS_Store
# Package building
.eggs/
@@ -13,6 +14,7 @@
build/
dist/
wheelhouse/
pip-wheel-metadata/
# Automatically generated files
docs/_build/
@@ -41,3 +43,4 @@ tests/resources/private/
tmp/
/debug_tests.py
*.traineddata
/private
+1 -1
View File
@@ -3,4 +3,4 @@ repos:
rev: stable
hooks:
- id: black
language_version: python3.6
language_version: python3.7
+9 -4
View File
@@ -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
----------
-1
View File
@@ -85,7 +85,6 @@ For example, if you have a development build of Tesseract don't wish to use the
In this example ``TESSDATA_PREFIX`` is required to redirect Tesseract to an alternate folder for its "tessdata" files.
Overriding other support programs
"""""""""""""""""""""""""""""""""
+20 -7
View File
@@ -27,7 +27,13 @@ This will walk through a directory tree and run OCR on all files in place, print
.. code-block:: bash
find . --printf '%p' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
find . -printf '%p' -name '*.pdf' -exec ocrmypdf '{}' '{}' \;
Alternatively, with a docker container (mounts a volume to the container where the PDFs are stored):
.. code-block:: bash
find . -printf '%p' -name '*.pdf' -exec docker run --rm -v <host dir>:<container dir> jbarlow83/ocrmypdf-alpine '<container dir>/{}' '<container dir>/{}' \;
This only runs one ``ocrmypdf`` process at a time. This variation uses ``find`` to create a directory list and ``parallel`` to parallelize runs of ``ocrmypdf``, again updating files in place.
@@ -80,9 +86,9 @@ This user contributed script also provides an example of batch processing.
print(full_path)
cmd = ["ocrmypdf", "--deskew", filename, filename]
logging.info(cmd)
proc = subprocess.Popen(
proc = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
result = proc.stdout.read()
result = proc.stdout
if proc.returncode == 6:
print("Skipped document because it already contained text")
elif proc.returncode == 0:
@@ -151,7 +157,7 @@ This is only possible for x86-based Synology products. Some Synology products us
# the script is processed as root user via chron
cmd = ['docker', 'run', '--rm', '-v', docker_mount, '-u=1030:65538', 'jbarlow83/ocrmypdf', , '--deskew' , filename, filename_OCR]
logging.info(cmd)
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
result = proc.stdout.read()
logging.info(result)
full_path_OCR = dir_name + '/' + filename_OCR
@@ -163,14 +169,11 @@ This is only possible for x86-based Synology products. Some Synology products us
shutil.move(full_path, full_path_archive)
logging.info('Finished.\n')
Huge batch jobs
"""""""""""""""
If you have thousands of files to work with, contact the author. Consulting work related to OCRmyPDF helps fund this open source project and all inquiries are appreciated.
Hot (watched) folders
---------------------
@@ -210,3 +213,13 @@ Alternatives
""""""""""""
* `Watchman <https://facebook.github.io/watchman/>`_ is a more powerful alternative to ``watchmedo``.
macOS Automator
---------------
You can use the Automator app with macOS, to create a Workflow or Quick Action. Use a *Run Shell Script* action in your workflow. In the context of Automator, the ``PATH`` may be set differently your Terminal's ``PATH``; you may need to explicitly set the PATH to include ``ocrmypdf``. The following example may serve as a starting point:
.. image:: images/macos-workflow.png
:alt: Example macOS Automator script
You may customize the command sent to ocrmypdf.
+1 -1
View File
@@ -104,7 +104,7 @@ Clients must keep their open connection while waiting for OCR to complete. This
Unlike the rest of OCRmyPDF, this web service is licensed under the Affero GPLv3 (AGPLv3) since Ghostscript, a dependency of OCRmyPDF, is also licensed in this way.
In addition to the above, please read our :ref:`general remarks on using OCRmyPDF as a service <ocr-service>`_.
In addition to the above, please read our :ref:`general remarks on using OCRmyPDF as a service <ocr-service>`.
Legacy Ubuntu Docker images
---------------------------

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+36 -2
View File
@@ -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 <https://pikepdf.readthedocs.io/en/latest/installation.html#installing-on-freebsd-11-2>`_.
#. Install the equivalent list of dependencies for Linux.
Installing the Docker image
---------------------------
@@ -374,9 +384,19 @@ See `OCRmyPDF Docker Image <docker>`_ for more information.
Installing on Windows
---------------------
Direct installation on Windows is not possible. `Install the Docker <docker-install>`_ container as described above. Ensure that your command prompt can run the docker "hello world" container.
Direct installation on Windows is not possible, because there are a
POSIX dependencies. Your options are:
It would probably not be too difficult to port on Windows. The main reason this has been avoided is the difficulty of packaging and installing the various non-Python dependencies: Tesseract, QPDF, Ghostscript, Leptonica. Pull requests to add or improve Windows support would be quite welcome.
* Install Ubuntu 18.04 in Windows 10 Subsystem for Linux, then follow
the Ubuntu 18.04 procedure.
* `Install the Docker <docker-install>`__ container. Ensure that
your command prompt can run the docker "hello world" container.
It would probably not be too difficult to port on Windows. The main
reason this has been avoided is the difficulty of packaging and
installing the various non-Python dependencies: Tesseract, QPDF,
Ghostscript, Leptonica. Pull requests to add or improve Windows support
would be quite welcome.
Installing with Python pip
--------------------------
@@ -490,3 +510,17 @@ 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/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/completion/ocrmypdf.bash`` to
``/etc/bash_completion.d/ocrmypdf`` (rename the file).
To manually install the ``fish`` completion, copy ``misc/completion/ocrmypdf.fish`` to
``~/.config/fish/completions/ocrmypdf.fish``.
+1 -1
View File
@@ -16,7 +16,7 @@ About PDFs
PDFs are page description files that attempts to preserve a layout exactly. They contain `vector graphics <http://vector-conversions.com/vectorizing/raster_vs_vector.html>`_ that can contain raster objects such as scanned images. Because PDFs can contain multiple pages (unlike many image formats) and can contain fonts and text, it is a good formats for exchanging scanned documents.
.. image:: bitmap_vs_svg.svg
.. image:: images/bitmap_vs_svg.svg
A PDF page might contain multiple images, even if it only appears to have one image. Some scanners or scanning software will segment pages into monochromatic text and color regions for example, to improve the compression ratio and appearance of the page.
+60
View File
@@ -13,6 +13,66 @@ 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.3.2
------
- Dropped workaround for macOS that allowed it work without pdfminer.six,
now a proper sdist release of pdfminer.six is available.
- pikepdf 1.5.0 is now required.
v8.3.1
------
- Fixed an issue where PDFs with malformed metadata would be rendered as blank pages. `#398 <https://github.com/jbarlow83/OCRmyPDF/issues/398>`_.
v8.3.0
------
- 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.
- 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/completion``. Package maintainers, please install these so users can take advantage.
- Updated requirements.
- pikepdf 1.3.0 is now required.
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
------
- 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
------
- Fixed a regression from v8.2.0, an exception that occurred while attempting to report that ``unpaper`` or another optional dependency was unavailable.
- In some cases, ``ocrmypdf [-c|--clean]`` failed to exit with an error when ``unpaper`` is not installed.
v8.2.1
------
- This release was canceled.
v8.2.0
------
+87
View File
@@ -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
+1 -1
View File
@@ -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.5.0.post0
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
pycparser == 2.19
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-xdist
pytest-xdist == 1.28.0
pytest-cov >= 2.6.1
python-xmp-toolkit # requires apt-get install libexempi3
# or brew install exempi
+2 -3
View File
@@ -98,15 +98,14 @@ setup(
'chardet >= 3.0.4, < 4', # unlisted requirement of pdfminer.six 20181108
'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',
'pdfminer.six == 20181108',
'pikepdf >= 1.5.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
'reportlab >= 3.3.0', # oldest released version with sane image handling
'ruffus >= 2.7.0',
],
extras_require={'pdfminer': ['pdfminer.six == 20181108']},
tests_require=tests_require,
entry_points={'console_scripts': ['ocrmypdf = ocrmypdf.__main__:run_pipeline']},
package_data={'ocrmypdf': ['data/sRGB.icc']},
+34 -69
View File
@@ -46,6 +46,7 @@ from .exceptions import (
)
from .exec import (
ghostscript,
jbig2enc,
qpdf,
tesseract,
check_external_program,
@@ -65,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
@@ -230,7 +227,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(
@@ -602,43 +601,19 @@ def check_options_sidecar(options, log):
options.sidecar = options.output_file + '.txt'
def _optional_program_required(name, version_fn, min_version, for_argument):
try:
if version_fn() < min_version:
raise MissingDependencyError(
f"The installed '{name}' is not supported. "
f"Install version {min_version} or newer."
)
except (FileNotFoundError, MissingDependencyError):
raise MissingDependencyError(
f"Install the '{name}' program to use {for_argument}."
)
def _optional_program_recommended(name, version_fn, min_version, for_argument):
try:
if version_fn() < min_version:
raise MissingDependencyError(
f"The installed '{name}' is not supported. "
f"Install version {min_version} or newer."
)
except (FileNotFoundError, MissingDependencyError):
complain(
f"For best results, install the optional program '{name}' to use the "
f"argument {for_argument}."
)
def check_options_preprocessing(options, log):
if options.clean_final:
options.clean = True
if options.unpaper_args and not options.clean:
raise argparse.ArgumentError(None, "--clean is required for --unpaper-args")
if any((options.clean, options.clean_final)):
from .exec import unpaper
_optional_program_required(
'unpaper', unpaper.version, '6.1', '--clean, --clean-final'
if options.clean:
check_external_program(
log=log,
program='unpaper',
package='unpaper',
version_checker=unpaper.version,
need_version='6.1',
required_for=['--clean, --clean-final'],
)
try:
if options.unpaper_args:
@@ -664,19 +639,26 @@ def check_options_ocr_behavior(options, log):
def check_options_optimizing(options, log):
if options.optimize >= 2:
from .exec import pngquant, jbig2enc
_optional_program_required(
'pngquant', pngquant.version, '2.0.1', '--optimize {2,3}'
check_external_program(
log=log,
program='pngquant',
package='pngquant',
version_checker=pngquant.version,
need_version='2.0.1',
required_for='--optimize {2,3}',
)
if options.jbig2_lossy:
_optional_program_required('jbig2', jbig2enc.version, '0.28', '--jbig2-lossy')
elif options.optimize >= 2:
if options.optimize >= 2:
# Although we use JBIG2 for optimize=1, don't nag about it unless the
# user is asking for more optimization
_optional_program_recommended(
'jbig2', jbig2enc.version, '0.28', '--optimize {2,3}'
check_external_program(
log=log,
program='jbig2',
package='jbig2enc',
version_checker=jbig2enc.version,
need_version='0.28',
required_for='--optimize {2,3} | --jbig2-lossy',
recommended=True if not options.jbig2_lossy else False,
)
if options.optimize == 0 and any(
@@ -1027,7 +1009,7 @@ def report_output_file_size(options, _log, input_file, output_file):
)
def check_dependency_versions(log):
def check_dependency_versions(options, log):
check_external_program(
log=log,
program='tesseract',
@@ -1051,27 +1033,10 @@ def check_dependency_versions(log):
return ExitCode.missing_dependency
check_external_program(
log=log,
program='unpaper',
package='unpaper',
version_checker=unpaper.version,
need_version='6.1', # latest sane version
optional=True,
)
if os.environ.get('TRAVIS') != 'true': # Suppress for Ubuntu trusty
check_external_program(
log=log,
program='qpdf',
package='qpdf',
version_checker=qpdf.version,
need_version='8.0.2',
)
check_external_program(
log=log,
program='pngquant',
package='pngquant',
version_checker=pngquant.version,
need_version='2.0.0',
optional=True,
program='qpdf',
package='qpdf',
version_checker=qpdf.version,
need_version='8.0.2',
)
@@ -1091,7 +1056,7 @@ def run_pipeline(args=None):
)
preamble(_log)
check_options(options, _log)
check_dependency_versions(_log)
check_dependency_versions(options, _log)
# Any changes to options will not take effect for options that are already
# bound to function parameters in the pipeline. (For example
+12 -9
View File
@@ -816,16 +816,17 @@ 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
modified = False
with pikepdf.open(layers_file) as pdf_layers_file:
if pdf_layers_file.docinfo:
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
pdf_layers_file.save(layers_file + '_')
if modified:
os.replace(layers_file + '_', layers_file)
ps = next((ii for ii in input_files if ii.endswith('.ps')), None)
ghostscript.generate_pdfa(
@@ -880,6 +881,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):
+2 -2
View File
@@ -58,9 +58,9 @@ def verify_python3_env(): # pragma: no cover
if os.name == 'posix':
import subprocess
rv = subprocess.Popen(
rv = subprocess.run(
['locale', '-a'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
).communicate()[0]
).stdout
good_locales = set()
has_c_utf8 = False
+58 -145
View File
@@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from contextlib import suppress
from itertools import groupby
from pathlib import Path
import os
@@ -25,7 +26,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 +162,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,113 +171,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):
"""
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
@@ -317,21 +229,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 ]')
)
emplacements = 1
interim_count = 0
# Iterate rest
for page_num, layers in groups:
layers = list(layers)
@@ -344,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
pdf_image = pikepdf.open(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
# 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
# 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:
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
@@ -396,24 +298,35 @@ 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 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
# 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 = pikepdf.open(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(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)
pdf_base.close()
+57 -44
View File
@@ -21,7 +21,7 @@ import os
import re
import sys
from subprocess import run, STDOUT, PIPE, CalledProcessError
from ..exceptions import MissingDependencyError
from ..exceptions import MissingDependencyError, ExitCode
from collections.abc import Mapping
@@ -43,7 +43,7 @@ def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)'):
f"Could not find program '{program}' on the PATH"
) from e
except CalledProcessError as e:
if e.returncode < 0:
if e.returncode != 0:
raise MissingDependencyError(
f"Ran program '{program}' but it exited with an error:\n{e.output}"
) from e
@@ -66,10 +66,17 @@ The program '{program}' could not be executed or was not found on your
system PATH.
'''
unknown_version = '''
OCRmyPDF requires '{program}' {need_version} or higher. Your system has
'{program}' but we cannot tell what version is installed. Contact the
package maintainer.
missing_optional_program = '''
The program '{program}' could not be executed or was not found on your
system PATH. This program is required when you use the
{required_for} arguments. You could try omitting these arguments, or install
the package.
'''
missing_recommend_program = '''
The program '{program}' could not be executed or was not found on your
system PATH. This program is recommended when using the {required_for} arguments,
but not required, so we will proceed. For best results, install the program.
'''
old_version = '''
@@ -77,20 +84,15 @@ OCRmyPDF requires '{program}' {need_version} or higher. Your system appears
to have {found_version}. Please update this program.
'''
okay_its_optional = '''
This program is OPTIONAL, so installation of OCRmyPDF can proceed, but
some functionality may be missing.
'''
not_okay_its_required = '''
This program is REQUIRED for OCRmyPDF to work. Installation will abort.
old_version_required_for = '''
OCRmyPDF requires '{program}' {need_version} or higher when run with the
{required_for} arguments. If you omit these arguments, OCRmyPDF may be able to
proceed. For best results, install the program.
'''
osx_install_advice = '''
If you have homebrew installed, try these command to install the missing
packages:
brew update
brew upgrade
package:
brew install {package}
'''
@@ -105,7 +107,7 @@ installing the RPM for {program}.
'''
def get_platform():
def _get_platform():
if sys.platform.startswith('freebsd'):
return 'freebsd'
elif sys.platform.startswith('linux'):
@@ -113,48 +115,59 @@ def get_platform():
return sys.platform
def _error_trailer(log, program, package, optional, **kwargs):
if optional:
log.error(okay_its_optional.format(**locals()), file=sys.stderr)
else:
log.error(not_okay_its_required.format(**locals()), file=sys.stderr)
def _error_trailer(log, program, package, **kwargs):
if isinstance(package, Mapping):
package = package[get_platform()]
package = package[_get_platform()]
if get_platform() == 'darwin':
log.error(osx_install_advice.format(**locals()), file=sys.stderr)
elif get_platform() == 'linux':
log.error(linux_install_advice.format(**locals()), file=sys.stderr)
if _get_platform() == 'darwin':
log.info(osx_install_advice.format(**locals()))
elif _get_platform() == 'linux':
log.info(linux_install_advice.format(**locals()))
def error_missing_program(log, program, package, optional):
log.error(missing_program.format(**locals()), file=sys.stderr)
_error_trailer(log, **locals())
def _error_missing_program(log, program, package, required_for, recommended):
if required_for:
log.error(missing_optional_program.format(**locals()))
elif recommended:
log.info(missing_recommend_program.format(**locals()))
else:
log.error(missing_program.format(**locals()))
_error_trailer(**locals())
def error_unknown_version(log, program, package, optional, need_version):
log.error(unknown_version.format(**locals()), file=sys.stderr)
_error_trailer(log, **locals())
def error_old_version(log, program, package, optional, need_version, found_version):
log.error(old_version.format(**locals()), file=sys.stderr)
_error_trailer(log, **locals())
def _error_old_version(
log, program, package, need_version, found_version, required_for
):
if required_for:
log.error(old_version_required_for.format(**locals()))
else:
log.error(old_version.format(**locals()))
_error_trailer(**locals())
def check_external_program(
log, program, package, version_checker, need_version, optional=False
*,
log,
program,
package,
version_checker,
need_version,
required_for=None,
recommended=False,
):
try:
found_version = version_checker()
except (CalledProcessError, FileNotFoundError, MissingDependencyError):
error_missing_program(log, program, package, optional)
if not optional:
sys.exit(1)
_error_missing_program(log, program, package, required_for, recommended)
if not recommended:
sys.exit(ExitCode.missing_dependency)
return
if found_version < need_version:
error_old_version(log, program, package, optional, need_version, found_version)
_error_old_version(
log, program, package, need_version, found_version, required_for
)
if not recommended:
sys.exit(ExitCode.missing_dependency)
log.debug(f'Found {program} {found_version}')
+2 -19
View File
@@ -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
@@ -269,7 +253,6 @@ def generate_pdfa(
"-dBATCH",
"-dNOPAUSE",
"-dCompatibilityLevel=" + str(pdf_version),
"-dNumRenderingThreads=" + str(threads),
"-sDEVICE=pdfwrite",
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=" + strategy,
+24 -20
View File
@@ -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:
@@ -614,9 +617,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()
+15 -15
View File
@@ -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
+10 -10
View File
@@ -30,6 +30,7 @@ from pikepdf import PdfMatrix
import pikepdf
from . import ghosttext
from .layout import get_page_analysis, get_text_boxes
from ..exceptions import EncryptedPdfError, MissingDependencyError
@@ -564,14 +565,6 @@ def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
)
pageinfo['bboxes'] = bboxes
else:
# pdfminer required for this section
try:
from .layout import get_page_analysis, get_text_boxes
except ImportError:
raise MissingDependencyError(
"pdfminer is required for this feature. Your distribution "
"may not have installed it."
)
pscript5_mode = str(pdf.docinfo.get('/Creator')).startswith('PScript5')
miner = get_page_analysis(infile, pageno, pscript5_mode)
pageinfo['textboxes'] = list(simplify_textboxes(miner, get_text_boxes))
@@ -618,8 +611,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 +749,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):
+17 -19
View File
@@ -19,7 +19,7 @@ import os
import platform
import sys
from pathlib import Path
from subprocess import PIPE, Popen
from subprocess import PIPE, run
import pytest
@@ -63,13 +63,14 @@ def running_in_travis():
@pytest.helpers.register
def needs_pdfminer(fn):
def have_unpaper():
try:
import pdfminer
except ImportError:
skip = pytest.mark.skipif(True, reason="pdfminer not available")
return skip(fn)
return fn
from ocrmypdf.exec import unpaper
unpaper.version()
except Exception:
return False
return True
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
@@ -171,19 +172,16 @@ def run_ocrmypdf(input_file, output_file, *args, env=None, universal_newlines=Tr
if env is None:
env = os.environ
p_args = OCRMYPDF + [str(arg) for arg in args] + [str(input_file), str(output_file)]
p = Popen(
p_args,
close_fds=True,
stdout=PIPE,
stderr=PIPE,
universal_newlines=universal_newlines,
env=env,
p_args = (
OCRMYPDF
+ [str(arg) for arg in args if arg is not None]
+ [str(input_file), str(output_file)]
)
out, err = p.communicate()
# print(err)
return p, out, err
p = run(
p_args, stdout=PIPE, stderr=PIPE, universal_newlines=universal_newlines, env=env
)
# print(p.stderr)
return p, p.stdout, p.stderr
@pytest.helpers.register
Binary file not shown.
+81
View File
@@ -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 <http://www.gnu.org/licenses/>.
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])
+17
View File
@@ -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")
+15 -27
View File
@@ -21,14 +21,14 @@ import shutil
import sys
from math import isclose
from pathlib import Path
from subprocess import DEVNULL, PIPE, Popen
from subprocess import DEVNULL, PIPE, run, Popen
import PIL
import pytest
from PIL import Image
from ocrmypdf.exceptions import ExitCode, MissingDependencyError
from ocrmypdf.exec import ghostscript, qpdf, tesseract
from ocrmypdf.exec import ghostscript, qpdf, tesseract, unpaper
from ocrmypdf.leptonica import Pix
from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
@@ -164,7 +164,7 @@ def test_exotic_image(
check_ocrmypdf(
resources / pdf,
outfile,
'-dc',
'-dc' if pytest.helpers.have_unpaper() else '-d',
'-v',
'1',
'--output-type',
@@ -219,7 +219,6 @@ def test_skip_ocr(spoof_tesseract_cache, resources, outpdf):
assert pdfinfo[0].has_text
@pytest.helpers.needs_pdfminer
def test_redo_ocr(spoof_tesseract_cache, resources, outpdf):
in_ = resources / 'graph_ocred.pdf'
before = PdfInfo(in_, detailed_page_analysis=True)
@@ -282,8 +281,7 @@ def test_maximum_options(
resources / 'multipage.pdf',
outpdf,
'-d',
'-c',
'-i',
'-ci' if pytest.helpers.have_unpaper() else None,
'-f',
'-k',
'--oversample',
@@ -542,7 +540,6 @@ def test_jbig2_passthrough(spoof_tesseract_cache, resources, outpdf):
'hocr',
env=spoof_tesseract_cache,
)
out_pageinfo = PdfInfo(out)
assert out_pageinfo[0].images[0].enc == Encoding.jbig2
@@ -554,16 +551,13 @@ def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
# Runs: ocrmypdf - output.pdf < testfile.pdf
with open(input_file, 'rb') as input_stream:
p_args = ocrmypdf_exec + ['-', output_file]
p = Popen(
p = run(
p_args,
close_fds=True,
stdout=PIPE,
stderr=PIPE,
stdin=input_stream,
env=spoof_tesseract_noop,
)
out, err = p.communicate()
assert p.returncode == ExitCode.ok
@@ -574,16 +568,13 @@ def test_stdout(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
# Runs: ocrmypdf francais.pdf - > test_stdout.pdf
with open(output_file, 'wb') as output_stream:
p_args = ocrmypdf_exec + [input_file, '-']
p = Popen(
p = run(
p_args,
close_fds=True,
stdout=output_stream,
stderr=PIPE,
stdin=DEVNULL,
env=spoof_tesseract_noop,
)
out, err = p.communicate()
assert p.returncode == ExitCode.ok
assert qpdf.check(output_file, log=None)
@@ -781,10 +772,10 @@ def test_pagesize_consistency(renderer, resources, outpdf):
outpdf,
'--pdf-renderer',
renderer,
'--clean',
'--clean' if pytest.helpers.have_unpaper() else None,
'--deskew',
'--remove-background',
'--clean-final',
'--clean-final' if pytest.helpers.have_unpaper() else None,
)
after_dims = first_page_dimensions(outpdf)
@@ -852,22 +843,21 @@ def test_compression_preserved(
'-',
output_file,
]
p = Popen(
p = run(
p_args,
close_fds=True,
stdout=PIPE,
stderr=PIPE,
stdin=input_stream,
universal_newlines=True,
env=spoof_tesseract_noop,
)
out, err = p.communicate()
if im.mode in ('RGBA', 'LA'):
# If alpha image is input, expect an error
assert p.returncode != ExitCode.ok and b'alpha' in err
assert p.returncode != ExitCode.ok and 'alpha' in p.stderr
return
assert p.returncode == ExitCode.ok, err.decode('utf-8')
assert p.returncode == ExitCode.ok, p.stderr
pdfinfo = PdfInfo(output_file)
@@ -913,17 +903,15 @@ def test_compression_changed(
'-',
output_file,
]
p = Popen(
p = run(
p_args,
close_fds=True,
stdout=PIPE,
stderr=PIPE,
stdin=input_stream,
universal_newlines=True,
env=spoof_tesseract_noop,
)
out, err = p.communicate()
assert p.returncode == ExitCode.ok, err
assert p.returncode == ExitCode.ok, p.stderr
pdfinfo = PdfInfo(output_file)
+12 -10
View File
@@ -22,7 +22,7 @@ import logging
import mmap
from os import fspath
from pathlib import Path
from shutil import copyfile
from shutil import copyfile, move
from unittest.mock import MagicMock, patch
import pytest
@@ -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,15 +304,17 @@ 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')
with graph.open_metadata() as meta:
meta['prism2:publicationName'] = 'OCRmyPDF Test'
graph.save(outdir / 'graph.repaired.pdf')
with pikepdf.open(outdir / 'graph.repaired.pdf') as graph:
with graph.open_metadata() as meta:
meta['prism2:publicationName'] = 'OCRmyPDF Test'
graph.save(outdir / 'graph.repaired.modified.pdf')
move(outdir / 'graph.repaired.modified.pdf', outdir / 'graph.repaired.pdf')
log = MagicMock()
log = logging.getLogger()
context = MagicMock()
metadata_fixup(
input_files_groups=input_files,
@@ -320,7 +322,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):
-5
View File
@@ -183,12 +183,7 @@ def test_ocr_detection(resources):
@pytest.mark.parametrize(
'testfile', ('truetype_font_nomapping.pdf', 'type3_font_nomapping.pdf')
)
@pytest.helpers.needs_pdfminer # pylint: disable=e1101
def test_corrupt_font_detection(resources, testfile):
try:
import pdfminer
except ImportError:
pytest.skip("Needs pdfminer")
filename = resources / testfile
with pytest.raises(NotImplementedError):
pdf = pdfinfo.PdfInfo(filename)
+2 -2
View File
@@ -117,10 +117,10 @@ def test_pagesize_consistency_tess4(ensure_tess4, resources, outpdf):
outpdf,
'--pdf-renderer',
'sandwich',
'--clean',
'--clean' if pytest.helpers.have_unpaper() else None,
'--deskew',
'--remove-background',
'--clean-final',
'--clean-final' if pytest.helpers.have_unpaper() else None,
env=ensure_tess4,
)
+3 -2
View File
@@ -16,9 +16,10 @@
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
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):
+1 -9
View File
@@ -15,20 +15,12 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
import logging
import os
import shutil
from math import isclose
from subprocess import DEVNULL, PIPE, Popen, check_call, check_output
import PyPDF2 as pypdf
import pytest
from ocrmypdf import leptonica
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.exec import ghostscript
from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.pdfinfo import Colorspace, Encoding, PdfInfo
from ocrmypdf.pdfinfo import PdfInfo
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf = pytest.helpers.run_ocrmypdf
+18 -21
View File
@@ -15,35 +15,15 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
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')
@@ -53,7 +33,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',
@@ -62,3 +42,20 @@ def test_no_glyphless_weave(resources, outdir):
'0',
env=env,
)
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