Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cff6ad2d1 | ||
|
|
1cc4c45b7e | ||
|
|
07d4fff3d4 | ||
|
|
7e388f59af | ||
|
|
84ec83151d | ||
|
|
0a72c12ff0 | ||
|
|
5adbddd1f9 | ||
|
|
c2fecffdb4 | ||
|
|
7dea0a8362 | ||
|
|
482cb788ed | ||
|
|
15a988b999 | ||
|
|
83398e54ea | ||
|
|
bcdd196699 | ||
|
|
0cd576e701 | ||
|
|
4d5e0eb749 | ||
|
|
7ed0f8f50e | ||
|
|
79c84eefa3 | ||
|
|
5398003160 | ||
|
|
58b2bed99d | ||
|
|
58c29ffb5c | ||
|
|
f615b6f0e8 | ||
|
|
e0c8dadcce | ||
|
|
9a86f53109 | ||
|
|
91cb092aa0 | ||
|
|
922a107b7f | ||
|
|
6e49bb3588 | ||
|
|
427afc0616 | ||
|
|
9c7ee2bf23 | ||
|
|
c5cfaa950b | ||
|
|
4e2a98ead4 | ||
|
|
210f134b5b | ||
|
|
696c0721a0 | ||
|
|
aabab95418 | ||
|
|
7d614dd68b | ||
|
|
f57dda7939 | ||
|
|
1b4542aa77 | ||
|
|
6c7fca57ec | ||
|
|
486dc7e22c | ||
|
|
dc616bb507 | ||
|
|
902bda43e3 | ||
|
|
f7da63f68b | ||
|
|
5da26e4c9c | ||
|
|
c19c852705 |
+4
-1
@@ -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
|
||||
|
||||
@@ -3,4 +3,4 @@ repos:
|
||||
rev: stable
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.6
|
||||
language_version: python3.7
|
||||
|
||||
@@ -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
|
||||
----------
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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``.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -13,6 +13,58 @@ 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.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
|
||||
------
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.3.0
|
||||
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
|
||||
pycparser == 2.19
|
||||
python-xmp-toolkit == 2.0.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pytest == 4.3.0
|
||||
pytest >= 4.4.1, < 5
|
||||
pytest-helpers-namespace >= 2019.1.8
|
||||
pytest-xdist
|
||||
pytest-xdist == 1.28.0
|
||||
pytest-cov >= 2.6.1
|
||||
python-xmp-toolkit # requires apt-get install libexempi3
|
||||
# or brew install exempi
|
||||
|
||||
@@ -99,7 +99,7 @@ setup(
|
||||
'cffi >= 1.9.1', # must be a setup and install requirement
|
||||
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
|
||||
'pdfminer.six == 20181108 ; sys_platform != "darwin"',
|
||||
'pikepdf >= 1.1.0, < 2',
|
||||
'pikepdf >= 1.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
|
||||
|
||||
+34
-69
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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
@@ -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
@@ -131,19 +131,19 @@ def file_claims_pdfa(filename):
|
||||
do full PDF/A validation.
|
||||
"""
|
||||
|
||||
pdf = pikepdf.open(filename)
|
||||
pdfmeta = pdf.open_metadata()
|
||||
if not pdfmeta.pdfa_status:
|
||||
return {
|
||||
'pass': False,
|
||||
'output': 'pdf',
|
||||
'conformance': 'No PDF/A metadata in XMP',
|
||||
}
|
||||
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||
conformance = f'PDF/A-{pdfmeta.pdfa_status}'
|
||||
pdfa_dict = {}
|
||||
if pdfmeta.pdfa_status in valid_part_conforms:
|
||||
pdfa_dict['pass'] = True
|
||||
pdfa_dict['output'] = 'pdfa'
|
||||
pdfa_dict['conformance'] = conformance
|
||||
with pikepdf.open(filename) as pdf:
|
||||
pdfmeta = pdf.open_metadata()
|
||||
if not pdfmeta.pdfa_status:
|
||||
return {
|
||||
'pass': False,
|
||||
'output': 'pdf',
|
||||
'conformance': 'No PDF/A metadata in XMP',
|
||||
}
|
||||
valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'}
|
||||
conformance = f'PDF/A-{pdfmeta.pdfa_status}'
|
||||
pdfa_dict = {}
|
||||
if pdfmeta.pdfa_status in valid_part_conforms:
|
||||
pdfa_dict['pass'] = True
|
||||
pdfa_dict['output'] = 'pdfa'
|
||||
pdfa_dict['conformance'] = conformance
|
||||
return pdfa_dict
|
||||
|
||||
@@ -618,8 +618,9 @@ def _pdf_get_all_pageinfo(infile, detailed_analysis=False, log=None):
|
||||
if not log:
|
||||
log = Mock()
|
||||
|
||||
pdf = pikepdf.open(infile)
|
||||
pdf = pikepdf.open(infile) # Do not close in this function
|
||||
if pdf.is_encrypted:
|
||||
pdf.close()
|
||||
raise EncryptedPdfError() # Triggered by encryption with empty passwd
|
||||
if detailed_analysis:
|
||||
pages_xml = None
|
||||
@@ -755,7 +756,13 @@ class PdfInfo:
|
||||
infile, detailed_page_analysis, log=log
|
||||
)
|
||||
self._needs_rendering = pdf.root.get('/NeedsRendering', False)
|
||||
self._has_acroform = '/AcroForm' in pdf.root
|
||||
self._has_acroform = False
|
||||
if '/AcroForm' in pdf.root:
|
||||
if len(pdf.root.AcroForm.get('/Fields', [])) > 0:
|
||||
self._has_acroform = True
|
||||
elif '/XFA' in pdf.root.AcroForm:
|
||||
self._has_acroform = True
|
||||
pdf.close()
|
||||
|
||||
@property
|
||||
def pages(self):
|
||||
|
||||
+21
-13
@@ -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
|
||||
|
||||
@@ -72,6 +72,17 @@ def needs_pdfminer(fn):
|
||||
return fn
|
||||
|
||||
|
||||
@pytest.helpers.register
|
||||
def have_unpaper():
|
||||
try:
|
||||
from ocrmypdf.exec import unpaper
|
||||
|
||||
unpaper.version()
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
SPOOF_PATH = os.path.join(TESTS_ROOT, 'spoof')
|
||||
PROJECT_ROOT = os.path.dirname(TESTS_ROOT)
|
||||
@@ -171,19 +182,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.
@@ -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,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
-26
@@ -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',
|
||||
@@ -282,8 +282,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 +541,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 +552,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 +569,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 +773,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 +844,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 +904,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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
+2
-2
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
+19
-21
@@ -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,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
|
||||
|
||||
Reference in New Issue
Block a user