Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2deee4920 | ||
|
|
1efa79cce2 | ||
|
|
b3b61c152c | ||
|
|
e429c3d729 | ||
|
|
8308b20096 | ||
|
|
8b41f60b6e | ||
|
|
d56f749017 | ||
|
|
9f31774aa9 | ||
|
|
7d55f6e01f | ||
|
|
c3bd2f296d | ||
|
|
e40c60d4d8 | ||
|
|
3960232ae0 | ||
|
|
5fbb3fc6ac | ||
|
|
0b1db8fccd | ||
|
|
0417610f9b | ||
|
|
43a23e3695 | ||
|
|
c4ca572afd | ||
|
|
e04e4565a9 | ||
|
|
2391fb0be0 | ||
|
|
c16f79d51b | ||
|
|
e3e888efde | ||
|
|
84f7e34ace | ||
|
|
32e2175891 | ||
|
|
975abfde9a | ||
|
|
d929ed76c2 | ||
|
|
4a27124eab | ||
|
|
683ffb84e8 | ||
|
|
2f2602357b | ||
|
|
a5f429f499 | ||
|
|
bdb7f92131 | ||
|
|
09f15ac4c0 | ||
|
|
4fdbf55c11 | ||
|
|
fe2b07652b | ||
|
|
f6d7aa6e33 | ||
|
|
a005d14f91 | ||
|
|
6f66232d44 | ||
|
|
b8a780d684 | ||
|
|
82f393dd09 | ||
|
|
4952af1604 | ||
|
|
bcf77375c0 | ||
|
|
3eab161771 | ||
|
|
b7f38e976b | ||
|
|
a6567f2ae4 | ||
|
|
e860c56b75 | ||
|
|
2e15d52895 | ||
|
|
ce97af5a79 | ||
|
|
3831c4cd4d | ||
|
|
61a2674317 | ||
|
|
9ad8cbf1f6 | ||
|
|
123fde174d | ||
|
|
fd991a2380 | ||
|
|
6f5d77d930 | ||
|
|
5169ac633b | ||
|
|
5b6ab1e003 | ||
|
|
8f984bf958 | ||
|
|
9c5f0d0ec6 | ||
|
|
32041c43e1 | ||
|
|
599028bebb | ||
|
|
6faa8f7221 | ||
|
|
a4dc5e365f | ||
|
|
e2a563cc76 | ||
|
|
1037d73efb | ||
|
|
aeb7b142a9 | ||
|
|
422ea9777e | ||
|
|
2f1c743227 | ||
|
|
96ee21aee9 | ||
|
|
4b759af6ff | ||
|
|
25d2b0cda4 | ||
|
|
16dd8b54a8 | ||
|
|
c4dc5269d2 | ||
|
|
c36e9950ae | ||
|
|
0c0d53b10f | ||
|
|
63de7e1677 | ||
|
|
b0e92760a2 |
+3
-3
@@ -1,5 +1,3 @@
|
||||
# Coverage isn't really compatible with subprocesses so results are unreliable
|
||||
|
||||
[paths]
|
||||
source =
|
||||
src
|
||||
@@ -8,9 +6,11 @@ source =
|
||||
[run]
|
||||
branch = true
|
||||
parallel = true
|
||||
concurrency =
|
||||
thread
|
||||
multiprocessing
|
||||
source =
|
||||
src/ocrmypdf
|
||||
tests
|
||||
omit =
|
||||
tests/spoof/*
|
||||
|
||||
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
branches:
|
||||
except:
|
||||
- azure
|
||||
|
||||
cache:
|
||||
pip: true
|
||||
directories:
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
dist: trusty
|
||||
sudo: required
|
||||
language: python
|
||||
python: "3.6"
|
||||
env:
|
||||
- DIST=trusty
|
||||
- MINIMAL=true
|
||||
addons:
|
||||
apt:
|
||||
update: true
|
||||
sources:
|
||||
- sourceline: "ppa:alex-p/tesseract-ocr"
|
||||
- sourceline: "ppa:vshn/ghostscript"
|
||||
packages:
|
||||
- ghostscript
|
||||
- libffi-dev
|
||||
- tesseract-ocr
|
||||
- tesseract-ocr-deu
|
||||
- tesseract-ocr-eng
|
||||
- tesseract-ocr-fra
|
||||
before_install: |
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade wheel
|
||||
- os: linux
|
||||
dist: trusty
|
||||
sudo: required
|
||||
language: python
|
||||
python: "3.6"
|
||||
env:
|
||||
- DIST=trusty
|
||||
addons:
|
||||
apt:
|
||||
update: true
|
||||
sources:
|
||||
- sourceline: "ppa:alex-p/tesseract-ocr"
|
||||
- sourceline: "ppa:heyarje/libav-11"
|
||||
- sourceline: "ppa:vshn/ghostscript"
|
||||
packages:
|
||||
- ghostscript
|
||||
- libavcodec56
|
||||
- libavformat56
|
||||
- libavutil54
|
||||
- libffi-dev
|
||||
- tesseract-ocr
|
||||
- tesseract-ocr-deu
|
||||
- tesseract-ocr-eng
|
||||
- tesseract-ocr-fra
|
||||
- libexempi3 # --- optional extras from here ---
|
||||
- pngquant
|
||||
- poppler-utils
|
||||
before_install: |
|
||||
mkdir -p bin packages
|
||||
pip3 install --upgrade pip
|
||||
pip3 install --upgrade wheel
|
||||
- os: linux
|
||||
dist: xenial
|
||||
sudo: required
|
||||
language: python
|
||||
python: "3.7"
|
||||
env:
|
||||
- DIST=xenial
|
||||
addons:
|
||||
apt:
|
||||
update: true
|
||||
sources:
|
||||
- sourceline: "ppa:alex-p/tesseract-ocr"
|
||||
packages:
|
||||
- ghostscript
|
||||
- libexempi3
|
||||
- libffi-dev
|
||||
- pngquant
|
||||
- poppler-utils
|
||||
- tesseract-ocr
|
||||
- tesseract-ocr-deu
|
||||
- tesseract-ocr-eng
|
||||
- tesseract-ocr-fra
|
||||
- unpaper
|
||||
- os: linux
|
||||
dist: bionic
|
||||
sudo: required
|
||||
language: python
|
||||
python: "3.8"
|
||||
env:
|
||||
- DIST=bionic
|
||||
addons:
|
||||
apt:
|
||||
update: true
|
||||
sources:
|
||||
- sourceline: "ppa:alex-p/tesseract-ocr"
|
||||
packages:
|
||||
- ghostscript
|
||||
- libexempi3
|
||||
- libffi-dev
|
||||
- pngquant
|
||||
- poppler-utils
|
||||
- tesseract-ocr
|
||||
- tesseract-ocr-deu
|
||||
- tesseract-ocr-eng
|
||||
- tesseract-ocr-fra
|
||||
- unpaper
|
||||
- os: osx
|
||||
language: generic
|
||||
addons:
|
||||
homebrew:
|
||||
update: true
|
||||
packages:
|
||||
- exempi
|
||||
- ghostscript
|
||||
- jbig2enc
|
||||
- leptonica
|
||||
- openjpeg
|
||||
- pngquant
|
||||
- python
|
||||
- qpdf
|
||||
- tesseract
|
||||
- unpaper
|
||||
before_install: |
|
||||
pip3 install --upgrade pip
|
||||
pip3 install wheel
|
||||
|
||||
before_cache:
|
||||
- rm -f $HOME/.cache/pip/log/debug.log
|
||||
|
||||
install:
|
||||
- mkdir -p bin
|
||||
- export PATH=$PWD/bin:$PATH
|
||||
- pip3 install -r requirements/main.txt -r requirements/test.txt .
|
||||
|
||||
script:
|
||||
- tesseract --version
|
||||
- pytest -n auto
|
||||
# deploy:
|
||||
# # release for main pypi
|
||||
# # 3.7 is considered the build leader and does the deploy, otherwise there is
|
||||
# # a race and all versions will try to deploy
|
||||
# # OTOH if we ever need separate binary wheels then each version needs its
|
||||
# # own deploy
|
||||
# - provider: pypi
|
||||
# user: ocrmypdf-travis
|
||||
# password:
|
||||
# secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
|
||||
# distributions: "sdist bdist_wheel"
|
||||
# on:
|
||||
# branch: master
|
||||
# tags: true
|
||||
# condition: $TRAVIS_PYTHON_VERSION == "3.7" && $TRAVIS_OS_NAME == "linux"
|
||||
# skip_upload_docs: true
|
||||
@@ -38,7 +38,7 @@ Main features
|
||||
- Keeps the exact resolution of the original embedded images
|
||||
- When possible, inserts OCR information as a "lossless" operation without disrupting any other content
|
||||
- Optimizes PDF images, often producing files smaller than the input file
|
||||
- If requested deskews and/or cleans the image before performing OCR
|
||||
- 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 to recognize more than [100 languages](https://github.com/tesseract-ocr/tessdata)
|
||||
@@ -50,7 +50,7 @@ For details: please consult the [documentation](https://ocrmypdf.readthedocs.io/
|
||||
Motivation
|
||||
----------
|
||||
|
||||
I searched the web for a free command line tool to OCR PDF files: I found many, but none of them were really satisfying.
|
||||
I searched the web for a free command line tool to OCR PDF files: I found many, but none of them were really satisfying:
|
||||
|
||||
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
|
||||
- Or they did not handle accents and multilingual characters
|
||||
@@ -97,7 +97,10 @@ OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux use
|
||||
apt-cache search tesseract-ocr
|
||||
|
||||
# Debian/Ubuntu users
|
||||
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
|
||||
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language pack
|
||||
|
||||
# Arch Linux users
|
||||
pacman -S tesseract-data-eng tesseract-data-deu # Example: Install the English and German language packs
|
||||
```
|
||||
|
||||
You can then pass the `-l LANG` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple languages can be requested.
|
||||
@@ -105,7 +108,7 @@ You can then pass the `-l LANG` argument to OCRmyPDF to give a hint as to what l
|
||||
Documentation and support
|
||||
-------------------------
|
||||
|
||||
Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via:
|
||||
Once OCRmyPDF is installed, the built-in help which explains the command syntax and options can be accessed via:
|
||||
|
||||
```bash
|
||||
ocrmypdf --help
|
||||
@@ -118,20 +121,21 @@ Please report issues on our [GitHub issues](https://github.com/jbarlow83/OCRmyPD
|
||||
Requirements
|
||||
------------
|
||||
|
||||
In addition to the required Python version (3.6+), OCRmyPDF requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings. OCRmyPDF works on pretty much everything: Linux, macOS, Windows and FreeBSD.
|
||||
In addition to the required Python version (3.6+), OCRmyPDF requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. OCRmyPDF is pure Python, but uses CFFI to portably generate library bindings. OCRmyPDF works on pretty much everything: Linux, macOS, Windows and FreeBSD.
|
||||
|
||||
Press & Media
|
||||
-------------
|
||||
|
||||
- [Going paperless with OCRmyPDF](https://medium.com/@ikirichenko/going-paperless-with-ocrmypdf-e2f36143f46a)
|
||||
- [Converting a scanned document into a compressed searchable PDF with redactions](https://medium.com/@treyharris/converting-a-scanned-document-into-a-compressed-searchable-pdf-with-redactions-63f61c34fe4c)
|
||||
- [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)
|
||||
- [c't 1-2014, page 59](https://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](https://heise.de/-2356670)
|
||||
- [heise Durchsuchbare PDF-Dokumente mit OCRmyPDF erstellen](https://www.heise.de/ratgeber/Durchsuchbare-PDF-Dokumente-mit-OCRmyPDF-erstellen-4607592.html)
|
||||
|
||||
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.
|
||||
OCRmyPDF would not be the software that it is today 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
|
||||
-------
|
||||
|
||||
Vendored
-6
@@ -95,12 +95,6 @@ Files: tests/resources/vector.pdf
|
||||
Copyright: (C) 2018 Catscratch
|
||||
License: Expat
|
||||
|
||||
Files: test/resources/enron*.pdf
|
||||
Copyright: EnronData.org
|
||||
License: CC-BY-3.0
|
||||
See: https://enrondata.readthedocs.io/en/latest/data/edo-enron-email-pst-dataset/
|
||||
Comment: Unprocessed.
|
||||
|
||||
Files: src/ocrmypdf/data/sRGB.icc
|
||||
Copyright: Kai-Uwe Behrmann <www.behrmann.name>
|
||||
Marti Maria <www.littlecms.com>
|
||||
|
||||
+21
-108
@@ -69,53 +69,8 @@ Sample script
|
||||
This user contributed script also provides an example of batch
|
||||
processing.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
#!/usr/bin/env python3
|
||||
# Walk through directory tree, replacing all files with OCR'd version
|
||||
# Original version by DeliciousPickle@github; modified
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
print(script_dir + '/ocr-tree.py: Start')
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
start_dir = sys.argv[1]
|
||||
else:
|
||||
start_dir = '.'
|
||||
|
||||
if len(sys.argv) > 2:
|
||||
log_file = sys.argv[2]
|
||||
else:
|
||||
log_file = script_dir + '/ocr-tree.log'
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format='%(asctime)s %(message)s',
|
||||
filename=log_file, filemode='w')
|
||||
|
||||
ocrmypdf.configure_logging(ocrmypdf.Verbosity.default)
|
||||
|
||||
for dir_name, subdirs, file_list in os.walk(start_dir):
|
||||
logging.info('\n')
|
||||
logging.info(dir_name + '\n')
|
||||
os.chdir(dir_name)
|
||||
for filename in file_list:
|
||||
file_ext = os.path.splitext(filename)[1]
|
||||
if file_ext == '.pdf':
|
||||
full_path = dir_name + '/' + filename
|
||||
print(full_path)
|
||||
result = ocrmypdf.ocr(filename, filename, deskew=True)
|
||||
if result == ocrmypdf.ExitCode.already_done_ocr:
|
||||
print("Skipped document because it already contained text")
|
||||
elif result == ocrmypdf.ExitCode.ok:
|
||||
print("OCR complete")
|
||||
logging.info(result)
|
||||
.. literalinclude:: ../misc/batch.py
|
||||
:caption: misc/batch.py
|
||||
|
||||
Synology DiskStations
|
||||
---------------------
|
||||
@@ -131,62 +86,8 @@ products use ARM or Power processors and do not support Docker. Further
|
||||
adjustments might be needed to deal with the Synology's relatively
|
||||
limited CPU and RAM.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
#!/bin/env python3
|
||||
# Contributed by github.com/Enantiomerie
|
||||
|
||||
# script needs 2 arguments
|
||||
# 1. source dir with *.pdf - default is location of script
|
||||
# 2. move dir where *.pdf and *_OCR.pdf are moved to
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
timestamp = time.strftime("%Y-%m-%d-%H%M_")
|
||||
log_file = script_dir + '/' + timestamp + 'ocrmypdf.log'
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s', filename=log_file, filemode='w')
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
start_dir = sys.argv[1]
|
||||
else:
|
||||
start_dir = '.'
|
||||
|
||||
for dir_name, subdirs, file_list in os.walk(start_dir):
|
||||
logging.info('\n')
|
||||
logging.info(dir_name + '\n')
|
||||
os.chdir(dir_name)
|
||||
for filename in file_list:
|
||||
file_ext = os.path.splitext(filename)[1]
|
||||
if file_ext == '.pdf':
|
||||
full_path = dir_name + '/' + filename
|
||||
file_noext = os.path.splitext(filename)[0]
|
||||
timestamp_OCR = time.strftime("%Y-%m-%d-%H%M_OCR_")
|
||||
filename_OCR = timestamp_OCR + file_noext + '.pdf'
|
||||
docker_mount = dir_name + ':/home/docker'
|
||||
# create string for pdf processing
|
||||
# diskstation needs a user:group docker:docker. find uid:gid of your diskstation docker:docker with id docker.
|
||||
# use this uid:gid in -u flag
|
||||
# rw rights for docker:docker at source dir are also necessary
|
||||
# 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.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
result = proc.stdout.read()
|
||||
logging.info(result)
|
||||
full_path_OCR = dir_name + '/' + filename_OCR
|
||||
os.chmod(full_path_OCR, 0o666)
|
||||
os.chmod(full_path, 0o666)
|
||||
full_path_OCR_archive = sys.argv[2]
|
||||
full_path_archive = sys.argv[2] + '/no_ocr'
|
||||
shutil.move(full_path_OCR,full_path_OCR_archive)
|
||||
shutil.move(full_path, full_path_archive)
|
||||
logging.info('Finished.\n')
|
||||
.. literalinclude:: ../misc/synology.py
|
||||
:caption: misc/synology.py - Sample script for Synology DiskStations
|
||||
|
||||
Huge batch jobs
|
||||
---------------
|
||||
@@ -210,6 +111,9 @@ be launched as follows:
|
||||
-v <path to files to convert>:/input \
|
||||
-v <path to store results>:/output \
|
||||
-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
||||
-e OCR_ON_SUCCESS_DELETE=1 \
|
||||
-e OCR_DESKEW=1 \
|
||||
-e PYTHONUNBUFFERED=1 \
|
||||
-it --entrypoint python3 \
|
||||
jbarlow83/ocrmypdf \
|
||||
watcher.py
|
||||
@@ -224,19 +128,29 @@ convert it to a OCRed PDF in ``/output/``. The parameters to this image are:
|
||||
"``-v <path to files to convert>:/input``", "Files placed in this location will be OCRed"
|
||||
"``-v <path to store results>:/output``", "This is where OCRed files will be stored"
|
||||
"``-e OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "This will place files in the output in {output}/{year}/{month}/{filename}"
|
||||
"``-e OCR_ON_SUCCESS_DELETE=1``", "This will delete the input file if the exit code is 0 (OK)"
|
||||
"``-e OCR_DESKEW=1``", "This will enable deskew for crooked PDFs"
|
||||
"``-e PYTHONBUFFERED=1``", "This will force STDOUT to be unbuffered and allow you to see messages in docker logs"
|
||||
|
||||
This service relies on polling to check for changes to the filesystem. It
|
||||
may not be suitable for some environments, such as filesystems shared on a
|
||||
slow network.
|
||||
|
||||
A configuration manager such as Docker Compose could be used to ensure that the
|
||||
service is always available.
|
||||
|
||||
.. literalinclude:: ../misc/docker-compose.example.yml
|
||||
:language: yaml
|
||||
:caption: misc/docker-compose.example.yml
|
||||
|
||||
Watched folders with watcher.py
|
||||
-------------------------------
|
||||
|
||||
The watcher service may also be run natively.
|
||||
The watcher service may also be run natively, without Docker:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install -r reqs/watcher.txt
|
||||
pip3 install -r requirements/watcher.txt
|
||||
|
||||
env OCR_INPUT_DIRECTORY=/mnt/input-pdfs \
|
||||
OCR_OUTPUT_DIRECTORY=/mnt/output-pdfs \
|
||||
@@ -313,8 +227,7 @@ 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:
|
||||
|
||||
|Example macOS Automator script|
|
||||
.. figure:: images/macos-workflow.png
|
||||
:alt: Example macOS Automator workflow
|
||||
|
||||
You may customize the command sent to ocrmypdf.
|
||||
|
||||
.. |Example macOS Automator script| image:: images/macos-workflow.png
|
||||
|
||||
@@ -89,6 +89,18 @@ This produces a file named "output.pdf" and a companion text file named
|
||||
|
||||
ocrmypdf --sidecar output.txt input.pdf output.pdf
|
||||
|
||||
.. note::
|
||||
|
||||
The sidecar file contains the **OCR text** found by OCRmyPDF. If the document
|
||||
contains pages that already have text, that text will not appear in the
|
||||
sidecar. If the option ``--pages`` is used, only those pages on which OCR
|
||||
was performed will be included in the sidecar. If certain pages were skipped
|
||||
because of options like ``--skip-big`` or ``--tesseract-timeout``, those pages
|
||||
will not be in the sidecar.
|
||||
|
||||
To extract all text from a PDF, whether generated from OCR or otherwise,
|
||||
use a program like Poppler's ``pdftotext`` or ``pdfgrep``.
|
||||
|
||||
OCR images, not PDFs
|
||||
--------------------
|
||||
|
||||
@@ -225,6 +237,32 @@ You can also optimize all images without performing any OCR:
|
||||
|
||||
ocrmypdf --tesseract-timeout=0 --optimize 3 --skip-text input.pdf output.pdf
|
||||
|
||||
Perform OCR only certain pages
|
||||
------------------------------
|
||||
|
||||
You can ask OCRmyPDF to only apply OCR to certain pages.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --pages 2,3,13-17 input.pdf output.pdf
|
||||
|
||||
Hyphens denote a range of pages and commas separate page numbers. If you prefer
|
||||
to use spaces, quote all of the page numbers: ``--pages '2, 3, 5, 7'``.
|
||||
|
||||
OCRmyPDF will warn if your list of page numbers contains duplicates or
|
||||
overlap pages. OCRmyPDF does not currently account for document page numbers,
|
||||
such as an introduction section of a book that uses Roman numerals. It simply
|
||||
counts the number of virtual pieces of paper since the start.
|
||||
|
||||
Regardless of the argument to ``--pages``, OCRmyPDF will optimize all pages in
|
||||
the file and convert it to PDF/A, unless you disable those options. In this
|
||||
example, we want to OCR only the title and otherwise change the PDF as little
|
||||
as possible:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ocrmypdf --pages 1 --output-type pdf --optimize 0 input.pdf output.pdf
|
||||
|
||||
Redo existing OCR
|
||||
=================
|
||||
|
||||
|
||||
+19
-17
@@ -56,35 +56,34 @@ See the Docker documentation for
|
||||
Using the Docker image on the command line
|
||||
==========================================
|
||||
|
||||
**Unlike typical Docker containers**, in this mode we are using the
|
||||
OCRmyPDF Docker container is intended to be emphemeral – it runs for one
|
||||
OCR job and then terminates, just like a command line program. We are
|
||||
using Docker as a way of delivering an application, not a server.
|
||||
**Unlike typical Docker containers**, in this section the OCRmyPDF Docker
|
||||
container is emphemeral – it runs for one OCR job and terminates, just like a
|
||||
command line program. We are using Docker to deliver an application (as opposed
|
||||
to the more conventional case, where a Docker container runs as a server).
|
||||
|
||||
To start a Docker container (instance of the image):
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
||||
docker run --rm -i ocrmypdf (... all other arguments here...)
|
||||
docker run --rm -i ocrmypdf (... all other arguments here...) - -
|
||||
|
||||
For convenience, create a shell alias to hide the Docker command. It is
|
||||
easier to send the input file to file stdin and read the output from
|
||||
stdout – this avoids the occasionally messy permission issues with
|
||||
Docker entirely.
|
||||
easier to send the input file as stdin and read the output from
|
||||
stdout – **this avoids the messy permission issues with Docker entirely**.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
alias ocrmypdf='docker run --rm -i ocrmypdf'
|
||||
ocrmypdf --version # runs docker version
|
||||
ocrmypdf <input.pdf >output.pdf
|
||||
alias docker_ocrmypdf='docker run --rm -i ocrmypdf'
|
||||
docker_ocrmypdf --version # runs docker version
|
||||
docker_ocrmypdf - - <input.pdf >output.pdf
|
||||
|
||||
Or in the wonderful `fish shell <https://fishshell.com/>`__:
|
||||
|
||||
.. code-block:: fish
|
||||
|
||||
alias ocrmypdf 'docker run --rm ocrmypdf'
|
||||
funcsave ocrmypdf
|
||||
alias docker_ocrmypdf 'docker run --rm ocrmypdf'
|
||||
funcsave docker_ocrmypdf
|
||||
|
||||
Alternately, you could mount the local current working directory as a
|
||||
Docker volume:
|
||||
@@ -93,15 +92,18 @@ Docker volume:
|
||||
|
||||
docker run --rm -v $(pwd):/data ocrmypdf /data/input.pdf /data/output.pdf
|
||||
|
||||
(However, when done this way, ``output.pdf`` may be owned by the root
|
||||
user.)
|
||||
|
||||
.. _docker-lang-packs:
|
||||
|
||||
Adding languages to the Docker image
|
||||
====================================
|
||||
|
||||
By default the Docker image includes English, German and Simplified
|
||||
Chinese, the most popular languages for OCRmyPDF users based on
|
||||
feedback. You may add other languages by creating a new Dockerfile based
|
||||
on the public one:
|
||||
By default the Docker image includes English, German, Simplified Chinese,
|
||||
French, Portuguese and Spanish, the most popular languages for OCRmyPDF
|
||||
users based on feedback. You may add other languages by creating a new
|
||||
Dockerfile based on the public one:
|
||||
|
||||
.. code-block:: dockerfile
|
||||
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
OCRmyPDF documentation
|
||||
======================
|
||||
|
||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
|
||||
be searched.
|
||||
OCRmyPDF adds an optical charcter recognition (OCR) text layer to scanned PDF
|
||||
files, allowing them to be searched.
|
||||
|
||||
PDF is the best format for storing and exchanging scanned documents.
|
||||
Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply
|
||||
|
||||
+92
-25
@@ -110,7 +110,7 @@ Fedora 29 or newer
|
||||
| |fedora-29| |fedora-30| |fedora-rawhide| |
|
||||
+-----------------------------------------------+
|
||||
|
||||
Users of Fedora 29 later may simply
|
||||
Users of Fedora 29 or later may simply
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -136,23 +136,26 @@ from sources <#installing-head-revision-from-sources>`__.
|
||||
Installing the latest version on Ubuntu 18.04 LTS
|
||||
-------------------------------------------------
|
||||
|
||||
Ubuntu 18.04 includes ocrmypdf 6.1.2. To install a more recent version,
|
||||
first install the system version to get most of the dependencies:
|
||||
Ubuntu 18.04 includes ocrmypdf 6.1.2 - you can install that with ``apt``, but
|
||||
it is quite old now. To install a more recent version, first install several
|
||||
system dependencies:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install \
|
||||
ocrmypdf
|
||||
|
||||
There are a few system dependency changes since ocrmypdf 6.1.2. Let's
|
||||
get these, too.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install \
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install \
|
||||
ghostscript \
|
||||
icc-profiles-free \
|
||||
liblept5 \
|
||||
libxml2 \
|
||||
pngquant
|
||||
pngquant \
|
||||
python3-cffi \
|
||||
python3-distutils \
|
||||
python3-pkg-resources \
|
||||
python3-reportlab \
|
||||
qpdf \
|
||||
tesseract-ocr \
|
||||
zlib1g
|
||||
|
||||
We will need a newer version of ``pip`` then was available for Ubuntu 18.04:
|
||||
|
||||
@@ -214,7 +217,8 @@ of ``pip`` at ``/usr/local/bin/pip``.
|
||||
**Install OCRmyPDF**
|
||||
|
||||
OCRmyPDF requires the locale to be set for UTF-8. **On some minimal
|
||||
Ubuntu installations systems**, it may be necessary to set the locale.
|
||||
Ubuntu installations**, such as the Ubuntu 16.04 Docker images it may be
|
||||
necessary to set the locale.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -294,23 +298,86 @@ compiled by hand.
|
||||
|
||||
To add JBIG2 encoding, see :ref:`jbig2`.
|
||||
|
||||
ArchLinux (AUR)
|
||||
---------------
|
||||
Arch Linux (AUR)
|
||||
----------------
|
||||
|
||||
.. image:: https://repology.org/badge/version-for-repo/aur/ocrmypdf.svg
|
||||
:alt: ArchLinux
|
||||
:target: https://repology.org/metapackage/ocrmypdf
|
||||
|
||||
There is an `ArchLinux User Repository package for
|
||||
ocrmypdf <https://aur.archlinux.org/packages/ocrmypdf/>`__. You can use
|
||||
the following command.
|
||||
There is an `Arch User Repository (AUR) package for OCRmyPDF
|
||||
<https://aur.archlinux.org/packages/ocrmypdf/>`__.
|
||||
|
||||
Installing AUR packages as root is not allowed, so you must first `setup a
|
||||
non-root user
|
||||
<https://wiki.archlinux.org/index.php/Users_and_groups#User_management>`__ and
|
||||
`configure sudo <https://wiki.archlinux.org/index.php/Sudo#Configuration>`__.
|
||||
The standard Docker image, ``archlinux/base:latest``, does **not** have a
|
||||
non-root user configured, so users of that image must follow these guides. If
|
||||
you are using a VM image, such as `the official Vagrant image
|
||||
<https://app.vagrantup.com/archlinux/boxes/archlinux>`__, this work may already
|
||||
be completed for you.
|
||||
|
||||
Next you should install the `base-devel package group
|
||||
<https://www.archlinux.org/groups/x86_64/base-devel/>`__. This includes the
|
||||
standard tooling needed to build packages, such as a compiler and binary tools.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
yaourt -S ocrmypdf
|
||||
sudo pacman -S base-devel
|
||||
|
||||
If you have any difficulties with installation, check the repository
|
||||
package page.
|
||||
The OCRmyPDF package depends on `the python-pdfminer.six AUR package
|
||||
<https://aur.archlinux.org/packages/python-pdfminer.six/>`__. Dependencies on
|
||||
AUR packages are not automatically resolved, so this package must be manually
|
||||
installed first.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -O https://aur.archlinux.org/cgit/aur.git/snapshot/python-pdfminer.six.tar.gz
|
||||
tar xvzf python-pdfminer.six.tar.gz
|
||||
cd python-pdfminer.six
|
||||
makepkg -sri
|
||||
|
||||
With that complete you can then repeat the same series of steps for the
|
||||
OCRmyPDF package.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -O https://aur.archlinux.org/cgit/aur.git/snapshot/ocrmypdf.tar.gz
|
||||
tar xvzf ocrmypdf.tar.gz
|
||||
cd ocrmypdf
|
||||
makepkg -sri
|
||||
|
||||
At this point you will have a working install of OCRmyPDF, but the Tesseract
|
||||
install won’t include any OCR language data. You can install `the
|
||||
tesseract-data package group
|
||||
<https://www.archlinux.org/groups/any/tesseract-data/>`__ to add all supported
|
||||
languages, or use that package listing to identify the appropriate package for
|
||||
your desired language.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pacman -S tesseract-data-eng
|
||||
|
||||
As an alternative to this manual procedure, consider using an `AUR helper
|
||||
<https://wiki.archlinux.org/index.php/AUR_helpers>`__. Such a tool will
|
||||
automatically fetch, build and install the AUR package, resolve dependencies
|
||||
(including dependencies on AUR packages), and ease the upgrade procedure.
|
||||
|
||||
If you have any difficulties with installation, check the repository package
|
||||
page.
|
||||
|
||||
.. note::
|
||||
|
||||
The OCRmyPDF AUR package currently omits the JBIG2 encoder. OCRmyPDF works
|
||||
fine without it but will produce larger output files. The encoder is
|
||||
available from `the jbig2enc-git AUR package
|
||||
<https://aur.archlinux.org/packages/jbig2enc-git/>`__ and may be installed
|
||||
using the same series of steps as for the installation of the pdfminer.six
|
||||
and OCRmyPDF AUR packages. Alternatively, it may be built manually from
|
||||
source following the instructions in `Installing the JBIG2 encoder
|
||||
<jbig2>`__. If JBIG2 is installed, OCRmyPDF 7.0.0 and later will
|
||||
automatically detect it.
|
||||
|
||||
Alpine Linux
|
||||
------------
|
||||
@@ -435,12 +502,12 @@ Installing on FreeBSD
|
||||
:alt: FreeBSD
|
||||
:target: https://repology.org/project/python:ocrmypdf/versions
|
||||
|
||||
FreeBSD 11.2, 11.3, 12.0-RELEASE and 13.0-CURRENT are supported. Other
|
||||
FreeBSD 11.3, 12.0, 12.1-RELEASE and 13.0-CURRENT are supported. Other
|
||||
versions likely work but have not been tested.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pkg install py36-ocrmypdf
|
||||
pkg install py37-ocrmypdf
|
||||
|
||||
To install a more recent version, you could attempt to first install the system
|
||||
version with ``pkg``, then use ``pip install --user ocrmypdf``.
|
||||
|
||||
@@ -10,7 +10,7 @@ About OCR
|
||||
`Optical character
|
||||
recognition <https://en.wikipedia.org/wiki/Optical_character_recognition>`__
|
||||
is technology that converts images of typed or handwritten text, such as
|
||||
in a scanned document, to computer text that can be searched and copied.
|
||||
in a scanned document, to computer text that can be selected, searched and copied.
|
||||
|
||||
OCRmyPDF uses
|
||||
`Tesseract <https://github.com/tesseract-ocr/tesseract>`__, the best
|
||||
|
||||
+11
-2
@@ -4,11 +4,20 @@
|
||||
Installing additional language packs
|
||||
====================================
|
||||
|
||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs for
|
||||
languages other than English.
|
||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs for all languages.
|
||||
On most platforms, English is installed with Tesseract by default, but not always.
|
||||
|
||||
Tesseract supports `most
|
||||
languages <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`__.
|
||||
Languages are identified by standardized three-letter codes (called ISO 639-2 Alpha-3).
|
||||
Tesseract's documentation also lists the three-letter code for your language.
|
||||
Some are anglicized, e.g. Spanish is ``spa`` rather than ``esp``, while others
|
||||
are not, e.g. German is ``deu``.
|
||||
|
||||
After you have installed a language pack, you can use it ``ocrmypdf -l <language>``,
|
||||
for example ``ocrmypdf -l spa``. For multilingual documents, you can specify
|
||||
all languages to be expected, e.g. ``ocrmypdf -l eng+fra`` for English and French.
|
||||
English is assumed by default unless other language(s) are specified.
|
||||
|
||||
For Linux users, you can often find packages that provide language
|
||||
packs:
|
||||
|
||||
@@ -13,6 +13,60 @@ Note that it is licensed under GPLv3, so scripts that
|
||||
``import ocrmypdf`` and are released publicly should probably also be
|
||||
licensed under GPLv3.
|
||||
|
||||
|
||||
v9.6.1
|
||||
======
|
||||
|
||||
- Documentation improvements - thanks to many users for their contributions!
|
||||
|
||||
- Fixed installation instructions for ArchLinux (@pigmonkey)
|
||||
- Updated installation instructions for FreeBSD and other OSes (@knobix)
|
||||
- Added instructions for using Docker Compose with watchdog (@ianalexander,
|
||||
@deisi)
|
||||
- Other miscellany (@mb720, @toy, @caiofacchinato)
|
||||
- Some scripts provided in the documentation have been migrated out so that
|
||||
they can be copied out as whole files, and to ensure syntax checking
|
||||
is maintained.
|
||||
|
||||
- Fixed an error that caused bash completions to fail on macOS. (#502, #504;
|
||||
@AlexanderWillner)
|
||||
- Fixed a rare case where OCRmyPDF threw an exception while processing a PDF
|
||||
with the wrong object type in its ``/Trailer /Info``. The error is now logged
|
||||
and incorrect object is ignored. (#497)
|
||||
- Removed potentially non-free file ``enron1.pdf`` and simplified the test that
|
||||
used it.
|
||||
- Removed potentially non-free file ``misc/media/logo.afdesign``.
|
||||
|
||||
v9.6.0
|
||||
======
|
||||
|
||||
- Fixed a regression with transferring metadata from the input PDF to the output
|
||||
PDF in certain situations.
|
||||
- pdfminer.six is now supported up to version 2020-01-24.
|
||||
- Messages are explaining page rotation decisions are now shown at the standard
|
||||
verbosity level again when ``--rotate-pages``. In some previous version they
|
||||
were set to debug level messages that only appeared with the parameter ``-v1``.
|
||||
- Improvements to ``misc/watcher.py``. Thanks to @ianalexander and @svenihoney.
|
||||
- Documentation improvements.
|
||||
|
||||
v9.5.0
|
||||
======
|
||||
|
||||
- Added API functions to measure OCR quality.
|
||||
- Modest improvements to handling PDFs with difficult/non compliant metadata.
|
||||
|
||||
v9.4.0
|
||||
======
|
||||
|
||||
- Updated recommended dependency versions.
|
||||
- Improvements to test coverage and changes to facilitate better measurement of
|
||||
test coverage, such as when tests run in subprocesses.
|
||||
- Improvements to error messages when Leptonica is not installed correctly.
|
||||
- Fixed use of pytest "session scope" that may have caused some intermittent
|
||||
CI failures.
|
||||
- When the argument ``--keep-temporary-files`` or verbosity is set to ``-v1``,
|
||||
a debug log file is generated in the working temporary folder.
|
||||
|
||||
v9.3.0
|
||||
======
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
# Original version by DeliciousPickle@github; modified
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
# pylint: disable=logging-format-interpolation
|
||||
# pylint: disable=logging-not-lazy
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
print(script_dir + '/batch.py: Start')
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
start_dir = sys.argv[1]
|
||||
else:
|
||||
start_dir = '.'
|
||||
|
||||
if len(sys.argv) > 2:
|
||||
log_file = sys.argv[2]
|
||||
else:
|
||||
log_file = script_dir + '/ocr-tree.log'
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(message)s',
|
||||
filename=log_file,
|
||||
filemode='w',
|
||||
)
|
||||
|
||||
ocrmypdf.configure_logging(ocrmypdf.Verbosity.default)
|
||||
|
||||
for dir_name, subdirs, file_list in os.walk(start_dir):
|
||||
logging.info(dir_name + '\n')
|
||||
os.chdir(dir_name)
|
||||
for filename in file_list:
|
||||
file_ext = os.path.splitext(filename)[1]
|
||||
if file_ext == '.pdf':
|
||||
full_path = dir_name + '/' + filename
|
||||
print(full_path)
|
||||
result = ocrmypdf.ocr(filename, filename, deskew=True)
|
||||
if result == ocrmypdf.ExitCode.already_done_ocr:
|
||||
print("Skipped document because it already contained text")
|
||||
elif result == ocrmypdf.ExitCode.ok:
|
||||
print("OCR complete")
|
||||
logging.info(result)
|
||||
@@ -5,7 +5,33 @@ set -o errexit
|
||||
_ocrmypdf()
|
||||
{
|
||||
local cur prev cword words split
|
||||
_init_completion -s || return
|
||||
|
||||
# Homebrew on Macs have version 1.3 of bash-completion which doesn't include - see #502
|
||||
if declare -F _init_completions >/dev/null 2>&1; then
|
||||
_init_completion -s || return
|
||||
else
|
||||
COMPREPLY=()
|
||||
_get_comp_words_by_ref cur prev words cword
|
||||
fi
|
||||
|
||||
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
|
||||
--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 --output-type
|
||||
--no-progress-bar --pages --fast-web-view' \
|
||||
-- "$cur" ) )
|
||||
return
|
||||
else
|
||||
_filedir
|
||||
return
|
||||
fi
|
||||
|
||||
case $prev in
|
||||
--version|-h|--help)
|
||||
@@ -65,25 +91,6 @@ _ocrmypdf()
|
||||
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
|
||||
--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 --output-type
|
||||
--no-progress-bar --pages --fast-web-view' \
|
||||
-- "$cur" ) )
|
||||
return
|
||||
else
|
||||
_filedir
|
||||
return
|
||||
fi
|
||||
} &&
|
||||
complete -F _ocrmypdf ocrmypdf
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
version: "3.3"
|
||||
services:
|
||||
ocrmypdf:
|
||||
restart: always
|
||||
container_name: ocrmypdf
|
||||
image: jbarlow83/ocrmypdf
|
||||
volumes:
|
||||
- "/media/scan:/input"
|
||||
- "/mnt/scan:/output"
|
||||
environment:
|
||||
- OCR_OUTPUT_DIRECTORY_YEAR_MONT=0
|
||||
user: "<SET TO YOUR USER ID>:<SET TO YOUR GROUP ID>"
|
||||
entrypoint: python3
|
||||
command: watcher.py
|
||||
Binary file not shown.
@@ -0,0 +1,72 @@
|
||||
#!/bin/env python3
|
||||
# Contributed by github.com/Enantiomerie
|
||||
|
||||
# This script must be edited to meet your needs.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
# pylint: disable=logging-format-interpolation
|
||||
# pylint: disable=logging-not-lazy
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
timestamp = time.strftime("%Y-%m-%d-%H%M_")
|
||||
log_file = script_dir + '/' + timestamp + 'ocrmypdf.log'
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(message)s',
|
||||
filename=log_file,
|
||||
filemode='w',
|
||||
)
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
start_dir = sys.argv[1]
|
||||
else:
|
||||
start_dir = '.'
|
||||
|
||||
for dir_name, subdirs, file_list in os.walk(start_dir):
|
||||
logging.info(dir_name)
|
||||
os.chdir(dir_name)
|
||||
for filename in file_list:
|
||||
file_stem, file_ext = os.path.splitext(filename)
|
||||
if file_ext != '.pdf':
|
||||
continue
|
||||
full_path = os.path.join(dir_name, filename)
|
||||
timestamp_ocr = time.strftime("%Y-%m-%d-%H%M_OCR_")
|
||||
filename_ocr = timestamp_ocr + file_stem + '.pdf'
|
||||
# create string for pdf processing
|
||||
# the script is processed as root user via chron
|
||||
cmd = [
|
||||
'docker',
|
||||
'run',
|
||||
'--rm',
|
||||
'-i',
|
||||
'jbarlow83/ocrmypdf',
|
||||
'--deskew',
|
||||
'-',
|
||||
'-',
|
||||
]
|
||||
logging.info(cmd)
|
||||
full_path_ocr = os.path.join(dir_name, filename_ocr)
|
||||
with open(filename, 'rb') as input_file, open(
|
||||
full_path_ocr, 'wb'
|
||||
) as output_file:
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
stdin=input_file,
|
||||
stdout=output_file,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
logging.info(proc.stderr.read())
|
||||
os.chmod(full_path_ocr, 0o664)
|
||||
os.chmod(full_path, 0o664)
|
||||
full_path_ocr_archive = sys.argv[2]
|
||||
full_path_archive = sys.argv[2] + '/no_ocr'
|
||||
shutil.move(full_path_ocr, full_path_ocr_archive)
|
||||
shutil.move(full_path, full_path_archive)
|
||||
logging.info('Finished.\n')
|
||||
+79
-11
@@ -1,4 +1,5 @@
|
||||
# Copyright (C) 2019 Ian Alexander: https://github.com/ianalexander
|
||||
# Copyright (C) 2020 James R Barlow: https://github.com/jbarlow83
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -13,51 +14,114 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pikepdf
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
# pylint: disable=logging-format-interpolation
|
||||
|
||||
INPUT_DIRECTORY = os.getenv('OCR_INPUT_DIRECTORY', '/input')
|
||||
OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
|
||||
OUTPUT_DIRECTORY_YEAR_MONTH = bool(os.getenv('OCR_OUTPUT_DIRECTORY_YEAR_MONTH', False))
|
||||
ON_SUCCESS_DELETE = bool(os.getenv('OCR_ON_SUCCESS_DELETE', False))
|
||||
DESKEW = bool(os.getenv('OCR_DESKEW', False))
|
||||
POLL_NEW_FILE_SECONDS = os.getenv('OCR_POLL_NEW_FILE_SECONDS', 1)
|
||||
LOGLEVEL = os.environ.get('OCR_LOGLEVEL', 'INFO').upper()
|
||||
PATTERNS = ['*.pdf']
|
||||
|
||||
log = logging.getLogger('ocrmypdf-watcher')
|
||||
|
||||
def execute_ocrmypdf(file_path):
|
||||
filename = Path(file_path).name
|
||||
|
||||
def get_output_dir(root, basename):
|
||||
if OUTPUT_DIRECTORY_YEAR_MONTH:
|
||||
today = datetime.today()
|
||||
output_directory_year_month = Path(
|
||||
f'{OUTPUT_DIRECTORY}/{today.year}/{today.month}'
|
||||
output_directory_year_month = (
|
||||
Path(root) / str(today.year) / f'{today.month:02d}'
|
||||
)
|
||||
if not output_directory_year_month.exists():
|
||||
output_directory_year_month.mkdir(parents=True, exist_ok=True)
|
||||
output_path = Path(output_directory_year_month) / filename
|
||||
output_path = Path(output_directory_year_month) / basename
|
||||
else:
|
||||
output_path = Path(OUTPUT_DIRECTORY) / filename
|
||||
print(f'New file: {file_path}.\nAttempting to OCRmyPDF to: {output_path}')
|
||||
ocrmypdf.ocr(file_path, output_path)
|
||||
output_path = Path(OUTPUT_DIRECTORY) / basename
|
||||
return output_path
|
||||
|
||||
|
||||
def wait_for_file_ready(file_path):
|
||||
# This loop waits to make sure that the file is completely loaded on
|
||||
# disk before attempting to read. Docker sometimes will publish the
|
||||
# watchdog event before the file is actually fully on disk, causing
|
||||
# pikepdf to fail.
|
||||
|
||||
retries = 5
|
||||
while retries:
|
||||
try:
|
||||
pdf = pikepdf.open(file_path)
|
||||
except (FileNotFoundError, pikepdf.PdfError) as e:
|
||||
log.info(f"File {file_path} is not ready yet")
|
||||
log.debug("Exception was", exc_info=e)
|
||||
time.sleep(POLL_NEW_FILE_SECONDS)
|
||||
retries -= 1
|
||||
else:
|
||||
pdf.close()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def execute_ocrmypdf(file_path):
|
||||
file_path = Path(file_path)
|
||||
output_path = get_output_dir(OUTPUT_DIRECTORY, file_path.name)
|
||||
|
||||
log.info("-" * 20)
|
||||
log.info(f'New file: {file_path}. Waiting until fully loaded...')
|
||||
if not wait_for_file_ready(file_path):
|
||||
log.info(f"Gave up waiting for {file_path} to become ready")
|
||||
return
|
||||
log.info(f'Attempting to OCRmyPDF to: {output_path}')
|
||||
exit_code = ocrmypdf.ocr(
|
||||
input_file=file_path, output_file=output_path, deskew=DESKEW
|
||||
)
|
||||
if exit_code == 0 and ON_SUCCESS_DELETE:
|
||||
log.info(f'OCR is done. Deleting: {file_path}')
|
||||
file_path.unlink()
|
||||
else:
|
||||
log.info('OCR is done')
|
||||
|
||||
|
||||
class HandleObserverEvent(PatternMatchingEventHandler):
|
||||
def on_any_event(self, event):
|
||||
if event.event_type in ['created', 'modified']:
|
||||
if event.event_type in ['created']:
|
||||
execute_ocrmypdf(event.src_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(
|
||||
def main():
|
||||
ocrmypdf.configure_logging(
|
||||
verbosity=ocrmypdf.Verbosity.default, manage_root_logger=True
|
||||
)
|
||||
log.info(
|
||||
f"Starting OCRmyPDF watcher with config:\n"
|
||||
f"Input Directory: {INPUT_DIRECTORY}\n"
|
||||
f"Output Directory: {OUTPUT_DIRECTORY}\n"
|
||||
f"Output Directory Year & Month: {OUTPUT_DIRECTORY_YEAR_MONTH}"
|
||||
)
|
||||
log.debug(
|
||||
f"INPUT_DIRECTORY: {INPUT_DIRECTORY}\n"
|
||||
f"OUTPUT_DIRECTORY: {OUTPUT_DIRECTORY}\n"
|
||||
f"OUTPUT_DIRECTORY_YEAR_MONTH: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
|
||||
f"ON_SUCCESS_DELETE: {ON_SUCCESS_DELETE}\n"
|
||||
f"DESKEW: {DESKEW}\n"
|
||||
f"POLL_NEW_FILE_SECONDS: {POLL_NEW_FILE_SECONDS}\n"
|
||||
f"LOGLEVEL: {LOGLEVEL}\n"
|
||||
)
|
||||
|
||||
handler = HandleObserverEvent(patterns=PATTERNS)
|
||||
observer = Observer()
|
||||
observer.schedule(handler, INPUT_DIRECTORY, recursive=True)
|
||||
@@ -68,3 +132,7 @@ if __name__ == "__main__":
|
||||
except KeyboardInterrupt:
|
||||
observer.stop()
|
||||
observer.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# requirements.txt can be used to replicate the developer's build environment
|
||||
# setup.py lists a separate set of requirements that are looser to simplify
|
||||
# installation
|
||||
cffi == 1.13.2
|
||||
cffi == 1.14.0
|
||||
img2pdf == 0.3.3
|
||||
pdfminer.six == 20191110
|
||||
pikepdf == 1.8.1
|
||||
Pillow >= 6.2.0
|
||||
reportlab == 3.5.32
|
||||
tqdm == 4.37.0
|
||||
pdfminer.six == 20200124
|
||||
pikepdf == 1.10.1
|
||||
Pillow == 7.0.0
|
||||
reportlab == 3.5.34
|
||||
tqdm == 4.42.1
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
pytest >= 5.0.0
|
||||
pytest-helpers-namespace >= 2019.1.8
|
||||
pytest-xdist >= 1.29.0 # For DumpError fix
|
||||
pytest-cov >= 2.6.1
|
||||
pytest-xdist >= 1.31.0
|
||||
pytest-cov >= 2.8.0
|
||||
python-xmp-toolkit == 2.0.1 # requires apt-get install libexempi3
|
||||
# or brew install exempi
|
||||
PyPDF2 >= 1.26.0
|
||||
#PyMuPDF == 1.13.4 # optional
|
||||
|
||||
@@ -23,7 +23,7 @@ force_grid_wrap=0
|
||||
use_parentheses=True
|
||||
line_length=88
|
||||
known_first_party = ocrmypdf
|
||||
known_third_party = PIL,PyPDF2,_cffi_backend,cffi,flask,gs,img2pdf,pdfminer,pikepdf,pkg_resources,pytest,reportlab,setuptools,sphinx_rtd_theme,tqdm,watchdog,werkzeug
|
||||
known_third_party = PIL,_cffi_backend,cffi,flask,gs,img2pdf,pdfminer,pikepdf,pkg_resources,pytest,reportlab,setuptools,sphinx_rtd_theme,tqdm,watchdog,werkzeug
|
||||
|
||||
[metadata]
|
||||
license_file = LICENSE
|
||||
|
||||
@@ -21,11 +21,12 @@ from __future__ import print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
if sys.version_info < (3, 6):
|
||||
print("Python 3.6 or newer is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
# pylint: disable=w0613
|
||||
|
||||
@@ -97,7 +98,7 @@ 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, <= 20191110',
|
||||
'pdfminer.six >= 20181108, <= 20200124',
|
||||
'pikepdf >= 1.8.1, < 2',
|
||||
'Pillow >= 6.2.0',
|
||||
'reportlab >= 3.3.0', # oldest released version with sane image handling
|
||||
|
||||
@@ -23,6 +23,7 @@ from .exceptions import (
|
||||
DpiError,
|
||||
EncryptedPdfError,
|
||||
ExitCode,
|
||||
ExitCodeException,
|
||||
InputFileError,
|
||||
MissingDependencyError,
|
||||
OutputFileAccessError,
|
||||
|
||||
+99
-85
@@ -19,6 +19,7 @@ import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from shutil import copyfileobj
|
||||
|
||||
import img2pdf
|
||||
@@ -123,7 +124,7 @@ def _pdf_guess_version(input_file, search_window=1024):
|
||||
return ''
|
||||
|
||||
|
||||
def triage(input_file, output_file, options, log):
|
||||
def triage(original_filename, input_file, output_file, options, log):
|
||||
try:
|
||||
if _pdf_guess_version(input_file):
|
||||
if options.image_dpi:
|
||||
@@ -135,8 +136,9 @@ def triage(input_file, output_file, options, log):
|
||||
safe_symlink(input_file, output_file)
|
||||
return output_file
|
||||
except EnvironmentError as e:
|
||||
log.error(e)
|
||||
raise InputFileError() from e
|
||||
log.debug(f"Temporary file was at: {input_file}")
|
||||
msg = str(e).replace(input_file, original_filename)
|
||||
raise InputFileError(msg) from e
|
||||
|
||||
triage_image_file(input_file, output_file, options, log)
|
||||
return output_file
|
||||
@@ -181,7 +183,7 @@ def validate_pdfinfo_options(context):
|
||||
)
|
||||
raise InputFileError()
|
||||
else:
|
||||
log.warn(
|
||||
log.warning(
|
||||
"This PDF has a fillable form. "
|
||||
"Chances are it is a pure digital "
|
||||
"document that does not need OCR."
|
||||
@@ -327,6 +329,35 @@ def rasterize_preview(input_file, page_context):
|
||||
return output_file
|
||||
|
||||
|
||||
def describe_rotation(page_context, orient_conf, correction):
|
||||
"""
|
||||
Describe the page rotation we are going to perform.
|
||||
"""
|
||||
direction = {0: '⇧', 90: '⇨', 180: '⇩', 270: '⇦'}
|
||||
turns = {0: ' ', 90: '⬏', 180: '↻', 270: '⬑'}
|
||||
|
||||
existing_rotation = page_context.pageinfo.rotation
|
||||
action = ''
|
||||
if orient_conf.confidence >= page_context.options.rotate_pages_threshold:
|
||||
if correction != 0:
|
||||
action = 'will rotate ' + turns[correction]
|
||||
else:
|
||||
action = 'rotation appears correct'
|
||||
else:
|
||||
if correction != 0:
|
||||
action = 'confidence too low to rotate'
|
||||
else:
|
||||
action = 'no change'
|
||||
|
||||
facing = ''
|
||||
|
||||
if existing_rotation != 0:
|
||||
facing = f"with existing rotation {direction.get(existing_rotation, '?')}, "
|
||||
facing += f"page is facing {direction.get(orient_conf.angle, '?')}"
|
||||
|
||||
return f"{facing}, confidence {orient_conf.confidence:.2f} - {action}"
|
||||
|
||||
|
||||
def get_orientation_correction(preview, page_context):
|
||||
"""
|
||||
Work out orientation correct for each page.
|
||||
@@ -353,44 +384,14 @@ def get_orientation_correction(preview, page_context):
|
||||
tesseract_env=page_context.options.tesseract_env,
|
||||
)
|
||||
|
||||
direction = {0: '⇧', 90: '⇨', 180: '⇩', 270: '⇦'}
|
||||
|
||||
existing_rotation = page_context.pageinfo.rotation
|
||||
|
||||
correction = orient_conf.angle % 360
|
||||
|
||||
apply_correction = False
|
||||
action = ''
|
||||
if orient_conf.confidence >= page_context.options.rotate_pages_threshold:
|
||||
if correction != 0:
|
||||
apply_correction = True
|
||||
action = ' - will rotate'
|
||||
else:
|
||||
action = ' - rotation appears correct'
|
||||
else:
|
||||
if correction != 0:
|
||||
action = ' - confidence too low to rotate'
|
||||
else:
|
||||
action = ' - no change'
|
||||
|
||||
facing = ''
|
||||
if existing_rotation != 0:
|
||||
facing = 'with existing rotation {}, '.format(
|
||||
direction.get(existing_rotation, '?')
|
||||
)
|
||||
facing += 'page is facing {}'.format(direction.get(orient_conf.angle, '?'))
|
||||
|
||||
page_context.log.debug(
|
||||
'{pagenum:4d}: {facing}, confidence {conf:.2f}{action}'.format(
|
||||
pagenum=page_context.pageinfo.pageno,
|
||||
facing=facing,
|
||||
conf=orient_conf.confidence,
|
||||
action=action,
|
||||
)
|
||||
)
|
||||
|
||||
if apply_correction:
|
||||
page_context.log.info(describe_rotation(page_context, orient_conf, correction))
|
||||
if (
|
||||
orient_conf.confidence >= page_context.options.rotate_pages_threshold
|
||||
and correction != 0
|
||||
):
|
||||
return correction
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -693,11 +694,18 @@ def convert_to_pdfa(input_pdf, input_ps_stub, context):
|
||||
# stamping them out as soon as possible.
|
||||
modified = False
|
||||
with pikepdf.open(input_pdf) as pdf_file:
|
||||
if pdf_file.docinfo:
|
||||
for k, v in pdf_file.docinfo.items():
|
||||
if b'\x00' in bytes(v):
|
||||
pdf_file.docinfo[k] = bytes(v).replace(b'\x00', b'')
|
||||
modified = True
|
||||
try:
|
||||
len(pdf_file.docinfo)
|
||||
except TypeError:
|
||||
context.log.error(
|
||||
"File contains a malformed DocumentInfo block - continuing anyway"
|
||||
)
|
||||
else:
|
||||
if pdf_file.docinfo:
|
||||
for k, v in pdf_file.docinfo.items():
|
||||
if b'\x00' in bytes(v):
|
||||
pdf_file.docinfo[k] = bytes(v).replace(b'\x00', b'')
|
||||
modified = True
|
||||
if modified:
|
||||
pdf_file.save(fix_docinfo_file)
|
||||
else:
|
||||
@@ -725,47 +733,53 @@ def should_linearize(working_file, context):
|
||||
def metadata_fixup(working_file, context):
|
||||
output_file = context.get_path('metafix.pdf')
|
||||
options = context.options
|
||||
original = pikepdf.open(context.origin)
|
||||
docinfo = get_docinfo(original, options)
|
||||
pdf = pikepdf.open(working_file)
|
||||
with pdf.open_metadata() as meta:
|
||||
meta.load_from_docinfo(docinfo, delete_missing=False)
|
||||
# If xmp:CreateDate is missing, set it to the modify date to
|
||||
# match Ghostscript, for consistency
|
||||
if 'xmp:CreateDate' not in meta:
|
||||
meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '')
|
||||
|
||||
meta_original = original.open_metadata()
|
||||
not_copied = set(meta_original.keys()) - set(meta.keys())
|
||||
if not_copied:
|
||||
if options.output_type.startswith('pdfa'):
|
||||
context.log.warning(
|
||||
"Some input metadata could not be copied because it is not "
|
||||
"permitted in PDF/A. You may wish to examine the output "
|
||||
"PDF's XMP metadata."
|
||||
)
|
||||
context.log.debug(
|
||||
"The following metadata fields were not copied: %r", not_copied
|
||||
)
|
||||
else:
|
||||
context.log.error(
|
||||
"Some input metadata could not be copied."
|
||||
"You may wish to examine the output PDF's XMP metadata."
|
||||
)
|
||||
context.log.info(
|
||||
"The following metadata fields were not copied: %r", not_copied
|
||||
)
|
||||
pdf.save(
|
||||
output_file,
|
||||
compress_streams=True,
|
||||
preserve_pdfa=True,
|
||||
object_stream_mode=pikepdf.ObjectStreamMode.generate,
|
||||
linearize=( # Don't linearize if optimize() will be linearizing too
|
||||
should_linearize(working_file, context) if options.optimize == 0 else False
|
||||
),
|
||||
)
|
||||
original.close()
|
||||
pdf.close()
|
||||
def report_on_metadata(missing):
|
||||
if not missing:
|
||||
return
|
||||
if options.output_type.startswith('pdfa'):
|
||||
context.log.warning(
|
||||
"Some input metadata could not be copied because it is not "
|
||||
"permitted in PDF/A. You may wish to examine the output "
|
||||
"PDF's XMP metadata."
|
||||
)
|
||||
context.log.debug(
|
||||
"The following metadata fields were not copied: %r", missing
|
||||
)
|
||||
else:
|
||||
context.log.error(
|
||||
"Some input metadata could not be copied."
|
||||
"You may wish to examine the output PDF's XMP metadata."
|
||||
)
|
||||
context.log.info(
|
||||
"The following metadata fields were not copied: %r", missing
|
||||
)
|
||||
|
||||
with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf:
|
||||
docinfo = get_docinfo(original, options)
|
||||
with pdf.open_metadata() as meta:
|
||||
meta.load_from_docinfo(docinfo, delete_missing=False, raise_failure=False)
|
||||
# If xmp:CreateDate is missing, set it to the modify date to
|
||||
# match Ghostscript, for consistency
|
||||
if 'xmp:CreateDate' not in meta:
|
||||
meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '')
|
||||
|
||||
meta_original = original.open_metadata()
|
||||
missing = set(meta_original.keys()) - set(meta.keys())
|
||||
report_on_metadata(missing)
|
||||
|
||||
pdf.save(
|
||||
output_file,
|
||||
compress_streams=True,
|
||||
preserve_pdfa=True,
|
||||
object_stream_mode=pikepdf.ObjectStreamMode.generate,
|
||||
linearize=( # Don't linearize if optimize() will be linearizing too
|
||||
should_linearize(working_file, context)
|
||||
if options.optimize == 0
|
||||
else False
|
||||
),
|
||||
)
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
|
||||
+60
-23
@@ -23,6 +23,7 @@ import signal
|
||||
import sys
|
||||
import threading
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
|
||||
import PIL
|
||||
@@ -230,7 +231,7 @@ def exec_concurrent(context):
|
||||
# Run exec_page_sync on every page context
|
||||
max_workers = min(len(context.pdfinfo), context.options.jobs)
|
||||
if max_workers > 1:
|
||||
context.log.info("Start processing %d pages concurrent", max_workers)
|
||||
context.log.info("Start processing %d pages concurrently", max_workers)
|
||||
|
||||
# Tesseract 4.x can be multithreaded, and we also run multiple workers. We want
|
||||
# to manage how many threads it uses to avoid creating total threads than cores.
|
||||
@@ -267,28 +268,43 @@ def exec_concurrent(context):
|
||||
unit='page',
|
||||
unit_scale=0.5,
|
||||
disable=not context.options.progress_bar,
|
||||
) as pbar, Pool(
|
||||
processes=max_workers,
|
||||
initializer=initializer,
|
||||
initargs=(log_queue, PIL.Image.MAX_IMAGE_PIXELS),
|
||||
) as pool:
|
||||
results = pool.imap_unordered(exec_page_sync, context.get_page_contexts())
|
||||
while True:
|
||||
try:
|
||||
page_result = results.next()
|
||||
sidecars[page_result.pageno] = page_result.text
|
||||
pbar.update()
|
||||
ocrgraft.graft_page(page_result)
|
||||
pbar.update()
|
||||
except StopIteration:
|
||||
break
|
||||
except (Exception, KeyboardInterrupt):
|
||||
) as pbar:
|
||||
pool = Pool(
|
||||
processes=max_workers,
|
||||
initializer=initializer,
|
||||
initargs=(log_queue, PIL.Image.MAX_IMAGE_PIXELS),
|
||||
)
|
||||
try:
|
||||
results = pool.imap_unordered(exec_page_sync, context.get_page_contexts())
|
||||
while True:
|
||||
try:
|
||||
page_result = results.next()
|
||||
sidecars[page_result.pageno] = page_result.text
|
||||
pbar.update()
|
||||
ocrgraft.graft_page(page_result)
|
||||
pbar.update()
|
||||
except StopIteration:
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
# Terminate pool so we exit instantly
|
||||
pool.terminate()
|
||||
# Don't try listener.join() here, will deadlock
|
||||
raise
|
||||
except Exception:
|
||||
if not os.environ.get("PYTEST_CURRENT_TEST", ""):
|
||||
# Unless inside pytest, exit immediately because no one wants
|
||||
# to wait for child processes to finalize results that will be
|
||||
# thrown away. Inside pytest, we want child processes to exit
|
||||
# cleanly so that they output an error messages or coverage data
|
||||
# we need from them.
|
||||
pool.terminate()
|
||||
log_queue.put_nowait(None) # Terminate log listener
|
||||
# Don't try listener.join() here, will deadlock
|
||||
raise
|
||||
raise
|
||||
finally:
|
||||
# Terminate log listener
|
||||
log_queue.put_nowait(None)
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
log_queue.put_nowait(None)
|
||||
listener.join()
|
||||
|
||||
# Output sidecar text
|
||||
@@ -320,6 +336,17 @@ def samefile(f1, f2):
|
||||
return os.path.samefile(f1, f2)
|
||||
|
||||
|
||||
def configure_debug_logging(log_filename, prefix=''):
|
||||
log_file_handler = logging.FileHandler(log_filename, delay=True)
|
||||
log_file_handler.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter(
|
||||
'[%(asctime)s] - %(name)s - %(levelname)7s - %(message)s'
|
||||
)
|
||||
log_file_handler.setFormatter(formatter)
|
||||
logging.getLogger(prefix).addHandler(log_file_handler)
|
||||
return log_file_handler
|
||||
|
||||
|
||||
def run_pipeline(options, api=False):
|
||||
log = make_logger(options, __name__)
|
||||
|
||||
@@ -330,13 +357,22 @@ def run_pipeline(options, api=False):
|
||||
options.jobs = available_cpu_count()
|
||||
|
||||
work_folder = mkdtemp(prefix="com.github.ocrmypdf.")
|
||||
if (options.keep_temporary_files or options.verbose >= 1) and not os.environ.get(
|
||||
'PYTEST_CURRENT_TEST', ''
|
||||
):
|
||||
configure_debug_logging(Path(work_folder) / "debug.log")
|
||||
|
||||
try:
|
||||
check_requested_output_file(options)
|
||||
start_input_file = create_input_file(options, work_folder)
|
||||
start_input_file, original_filename = create_input_file(options, work_folder)
|
||||
|
||||
# Triage image or pdf
|
||||
origin_pdf = triage(
|
||||
start_input_file, os.path.join(work_folder, 'origin.pdf'), options, log
|
||||
original_filename,
|
||||
start_input_file,
|
||||
os.path.join(work_folder, 'origin.pdf'),
|
||||
options,
|
||||
log,
|
||||
)
|
||||
|
||||
# Gather pdfinfo and create context
|
||||
@@ -345,6 +381,7 @@ def run_pipeline(options, api=False):
|
||||
detailed_page_analysis=options.redo_ocr,
|
||||
progbar=options.progress_bar,
|
||||
)
|
||||
|
||||
context = PDFContext(options, work_folder, origin_pdf, pdfinfo)
|
||||
|
||||
# Validate options are okay for this pdf
|
||||
|
||||
@@ -314,7 +314,7 @@ def check_options(options):
|
||||
check_dependency_versions(options)
|
||||
|
||||
|
||||
def check_closed_streams(options):
|
||||
def check_closed_streams(options): # pragma: no cover
|
||||
"""Work around Python issue with multiprocessing forking on closed streams
|
||||
|
||||
https://bugs.python.org/issue28326
|
||||
@@ -380,12 +380,12 @@ def create_input_file(options, work_folder):
|
||||
target = os.path.join(work_folder, 'stdin')
|
||||
with open(target, 'wb') as stream_buffer:
|
||||
copyfileobj(sys.stdin.buffer, stream_buffer)
|
||||
return target
|
||||
return target, "<stdin>"
|
||||
else:
|
||||
try:
|
||||
target = os.path.join(work_folder, 'origin')
|
||||
safe_symlink(options.input_file, target)
|
||||
return target
|
||||
return target, os.fspath(options.input_file)
|
||||
except FileNotFoundError:
|
||||
raise InputFileError(f"File not found - {options.input_file}")
|
||||
|
||||
|
||||
+18
-7
@@ -18,10 +18,10 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from contextlib import suppress
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
@@ -31,7 +31,15 @@ from .cli import parser
|
||||
|
||||
|
||||
class TqdmConsole:
|
||||
"""Wrapper to log messages in a way that is compatible with tqdm progress bar"""
|
||||
"""Wrapper to log messages in a way that is compatible with tqdm progress bar
|
||||
|
||||
This routes log messages through tqdm so that it can print them above the
|
||||
progress bar, and then refresh the progress bar, rather than overwriting
|
||||
it which looks messy.
|
||||
|
||||
For some reason Python 3.6 prints extra empty messages from time to time,
|
||||
so we suppress those.
|
||||
"""
|
||||
|
||||
def __init__(self, file):
|
||||
self.file = file
|
||||
@@ -46,7 +54,7 @@ class TqdmConsole:
|
||||
tqdm.write(msg.rstrip(), end='\n', file=self.file)
|
||||
|
||||
def flush(self):
|
||||
if hasattr(self.file, "flush"):
|
||||
with suppress(AttributeError):
|
||||
self.file.flush()
|
||||
|
||||
|
||||
@@ -80,11 +88,14 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
|
||||
overwrite the progress bar
|
||||
manage_root_logger (bool): Configure the process's root logger, to ensure
|
||||
all log output is sent through
|
||||
|
||||
Returns:
|
||||
The toplevel logger for ocrmypdf (or the root logger, if we are managing it).
|
||||
"""
|
||||
|
||||
prefix = '' if manage_root_logger else 'ocrmypdf'
|
||||
log = logging.getLogger(prefix)
|
||||
log.setLevel(logging.INFO)
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
if progress_bar_friendly:
|
||||
console = logging.StreamHandler(stream=TqdmConsole(sys.stderr))
|
||||
@@ -99,8 +110,6 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
|
||||
console.setLevel(logging.INFO)
|
||||
|
||||
formatter = logging.Formatter('%(levelname)7s - %(message)s')
|
||||
if verbosity >= 1:
|
||||
log.setLevel(logging.DEBUG)
|
||||
if verbosity >= 2:
|
||||
formatter = logging.Formatter('%(name)s - %(levelname)7s - %(message)s')
|
||||
|
||||
@@ -116,6 +125,8 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
|
||||
if manage_root_logger:
|
||||
logging.captureWarnings(True)
|
||||
|
||||
return log
|
||||
|
||||
|
||||
def create_options(*, input_file, output_file, **kwargs):
|
||||
cmdline = []
|
||||
|
||||
@@ -29,7 +29,7 @@ from subprocess import run as subprocess_run
|
||||
|
||||
from ..exceptions import ExitCode, MissingDependencyError
|
||||
|
||||
log = logging.Logger(__name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_program(args, env=None):
|
||||
@@ -77,12 +77,21 @@ def run(args, *, env=None, **kwargs):
|
||||
if new_args0:
|
||||
args[0] = new_args0
|
||||
|
||||
log.debug(args)
|
||||
process_log = log.getChild(os.path.basename(program))
|
||||
process_log.debug("Running: %s", args)
|
||||
if sys.version_info < (3, 7) and os.name == 'nt':
|
||||
# Can't use close_fds=True on Windows with Python 3.6 or older
|
||||
# https://bugs.python.org/issue19575, etc.
|
||||
kwargs['close_fds'] = False
|
||||
return subprocess_run(args, env=env, **kwargs)
|
||||
proc = subprocess_run(args, env=env, **kwargs)
|
||||
if process_log.isEnabledFor(logging.DEBUG):
|
||||
try:
|
||||
stderr = proc.stderr.decode('utf-8', 'replace')
|
||||
except AttributeError:
|
||||
stderr = proc.stderr
|
||||
if stderr:
|
||||
process_log.debug("stderr = %s", stderr)
|
||||
return proc
|
||||
|
||||
|
||||
def get_version(program, *, version_arg='--version', regex=r'(\d+(\.\d+)*)', env=None):
|
||||
|
||||
@@ -195,8 +195,6 @@ def rasterize_pdf(
|
||||
try:
|
||||
p = run(args_gs, stdout=PIPE, stderr=PIPE, check=True)
|
||||
except CalledProcessError as e:
|
||||
with suppress(OSError):
|
||||
Path(output_file).unlink() # no unfinished files
|
||||
log.error(e.stderr.decode(errors='replace'))
|
||||
raise SubprocessOutputError('Ghostscript rasterizing failed')
|
||||
else:
|
||||
@@ -313,15 +311,12 @@ def generate_pdfa(
|
||||
]
|
||||
)
|
||||
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
|
||||
log.debug(args_gs)
|
||||
try:
|
||||
with Path(output_file).open('wb') as output:
|
||||
p = run(args_gs, stdout=output, stderr=PIPE, check=True)
|
||||
except CalledProcessError as e:
|
||||
# Ghostscript does not change return code when it fails to create
|
||||
# PDF/A - check PDF/A status elsewhere
|
||||
with suppress(OSError):
|
||||
Path(output_file).unlink()
|
||||
log.error(e.stderr.decode(errors='replace'))
|
||||
raise SubprocessOutputError('Ghostscript PDF/A rendering failed')
|
||||
else:
|
||||
@@ -346,5 +341,3 @@ def generate_pdfa(
|
||||
"Ghostscript had to remove PDF 'overprinting' from the "
|
||||
"input file to complete PDF/A conversion. "
|
||||
)
|
||||
else:
|
||||
log.debug(stderr)
|
||||
|
||||
@@ -260,8 +260,8 @@ def generate_hocr(
|
||||
log,
|
||||
):
|
||||
|
||||
output_hocr = next(o for o in output_files if o.endswith('.hocr'))
|
||||
output_sidecar = next(o for o in output_files if o.endswith('.txt'))
|
||||
output_hocr = next(o for o in output_files if fspath(o).endswith('.hocr'))
|
||||
output_sidecar = next(o for o in output_files if fspath(o).endswith('.txt'))
|
||||
prefix = os.path.splitext(output_hocr)[0]
|
||||
|
||||
args_tesseract = tess_base_args(language, engine_mode)
|
||||
@@ -275,14 +275,10 @@ def generate_hocr(
|
||||
if user_patterns:
|
||||
args_tesseract.extend(['--user-patterns', user_patterns])
|
||||
|
||||
if user_words or user_patterns:
|
||||
args_tesseract.extend(['-c', 'lstm_use_matrix=1'])
|
||||
|
||||
# Reminder: test suite tesseract spoofers will break after any changes
|
||||
# to the number of order parameters here
|
||||
args_tesseract.extend([input_file, prefix, 'hocr', 'txt'] + tessconfig)
|
||||
try:
|
||||
log.debug(args_tesseract)
|
||||
p = run(
|
||||
args_tesseract,
|
||||
stdout=PIPE,
|
||||
@@ -381,7 +377,6 @@ def generate_pdf(
|
||||
|
||||
args_tesseract.extend([input_image, prefix, 'pdf', 'txt'] + tessconfig)
|
||||
try:
|
||||
log.debug(args_tesseract)
|
||||
p = run(
|
||||
args_tesseract,
|
||||
stdout=PIPE,
|
||||
|
||||
+29
-21
@@ -104,28 +104,36 @@ def is_file_writable(test_file):
|
||||
can replace it atomically. Before doing the OCR work, make sure
|
||||
the location is writable.
|
||||
"""
|
||||
p = Path(test_file)
|
||||
|
||||
if p.is_symlink():
|
||||
p = p.resolve(strict=False)
|
||||
|
||||
# p.is_file() throws an exception in some cases
|
||||
if p.exists() and p.is_file():
|
||||
return os.access(
|
||||
os.fspath(p),
|
||||
os.W_OK,
|
||||
effective_ids=(os.access in os.supports_effective_ids),
|
||||
)
|
||||
else:
|
||||
try:
|
||||
fp = p.open('wb')
|
||||
except OSError:
|
||||
return False
|
||||
try:
|
||||
if not isinstance(test_file, Path):
|
||||
p = Path(test_file)
|
||||
else:
|
||||
fp.close()
|
||||
with suppress(OSError):
|
||||
p.unlink()
|
||||
return True
|
||||
p = test_file
|
||||
|
||||
if p.is_symlink():
|
||||
p = p.resolve(strict=False)
|
||||
|
||||
# p.is_file() throws an exception in some cases
|
||||
if p.exists() and p.is_file():
|
||||
return os.access(
|
||||
os.fspath(p),
|
||||
os.W_OK,
|
||||
effective_ids=(os.access in os.supports_effective_ids),
|
||||
)
|
||||
else:
|
||||
try:
|
||||
fp = p.open('wb')
|
||||
except OSError:
|
||||
return False
|
||||
else:
|
||||
fp.close()
|
||||
with suppress(OSError):
|
||||
p.unlink()
|
||||
return True
|
||||
except (EnvironmentError, RuntimeError) as e:
|
||||
log.debug(e)
|
||||
log.error(str(e))
|
||||
return False
|
||||
|
||||
|
||||
def deprecated(func):
|
||||
|
||||
@@ -88,7 +88,7 @@ class HocrTransform:
|
||||
if self.width is None or self.height is None:
|
||||
raise HocrTransformError("hocr file is missing page dimensions")
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self): # pragma: no cover
|
||||
"""
|
||||
Return the textual content of the HTML body
|
||||
"""
|
||||
@@ -190,7 +190,7 @@ class HocrTransform:
|
||||
pt = self.pt_from_pixel(pxl_coords)
|
||||
|
||||
# draw the bbox border
|
||||
if showBoundingboxes:
|
||||
if showBoundingboxes: # pragma: no cover
|
||||
pdf.rect(
|
||||
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1, fill=1
|
||||
)
|
||||
@@ -231,7 +231,7 @@ class HocrTransform:
|
||||
pdf.save()
|
||||
|
||||
@classmethod
|
||||
def polyval(cls, poly, x):
|
||||
def polyval(cls, poly, x): # pragma: no cover
|
||||
return x * poly[0] + poly[1]
|
||||
|
||||
def _do_line(
|
||||
@@ -269,7 +269,7 @@ class HocrTransform:
|
||||
# of the line box
|
||||
baseline_y2 = self.height - (line_box.y2 + intercept)
|
||||
|
||||
if showBoundingboxes:
|
||||
if showBoundingboxes: # pragma: no cover
|
||||
# draw the baseline in magenta, dashed
|
||||
pdf.setDash()
|
||||
pdf.setStrokeColorRGB(0.95, 0.65, 0.95)
|
||||
@@ -318,7 +318,7 @@ class HocrTransform:
|
||||
font_width = pdf.stringWidth(elemtxt, fontname, fontsize)
|
||||
|
||||
# draw the bbox border
|
||||
if showBoundingboxes:
|
||||
if showBoundingboxes: # pragma: no cover
|
||||
pdf.rect(
|
||||
box.x1, self.height - line_box.y2, box_width, line_height, fill=0
|
||||
)
|
||||
|
||||
@@ -47,21 +47,32 @@ if os.name == 'nt':
|
||||
else:
|
||||
libname = 'lept'
|
||||
_libpath = find_library(libname)
|
||||
if not _libpath and os.name == 'nt':
|
||||
if not _libpath:
|
||||
raise MissingDependencyError(
|
||||
"""
|
||||
---------------------------------------------------------------------
|
||||
This error normally occurs when ocrmypdf can't find a file named
|
||||
liblept-5.dll (Leptonica). Please ensure Tesseract-OCR is installed
|
||||
and its location is added to the system PATH environment variable.
|
||||
This error normally occurs when ocrmypdf can't find the Leptonica
|
||||
library, which is usually installed with Tesseract OCR. It could be that
|
||||
Tesseract is not installed properly, we can't find the installation
|
||||
on your system PATH environment variable.
|
||||
|
||||
For details see:
|
||||
The library we are looking for is usually called:
|
||||
liblept-5.dll (Windows)
|
||||
liblept*.dylib (macOS)
|
||||
liblept*.so (Linux/BSD)
|
||||
|
||||
Please review our installation procedures to find a solution:
|
||||
https://ocrmypdf.readthedocs.io/en/latest/installation.html
|
||||
---------------------------------------------------------------------
|
||||
"""
|
||||
)
|
||||
lept = ffi.dlopen(_libpath)
|
||||
lept.setMsgSeverity(lept.L_SEVERITY_WARNING)
|
||||
try:
|
||||
lept = ffi.dlopen(_libpath)
|
||||
lept.setMsgSeverity(lept.L_SEVERITY_WARNING)
|
||||
except ffi.error as e:
|
||||
raise MissingDependencyError(
|
||||
f"Leptonica library found at {_libpath}, but we could not access it"
|
||||
) from e
|
||||
|
||||
|
||||
class _LeptonicaErrorTrap:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# © 2020 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 re
|
||||
from typing import Iterable
|
||||
|
||||
"""Utilities to measure OCR quality"""
|
||||
|
||||
|
||||
class OcrQualityDictionary:
|
||||
"""Manages a dictionary for simple OCR quality checks."""
|
||||
|
||||
def __init__(self, *, wordlist: Iterable[str] = []):
|
||||
"""Construct a dictionary from a list of words.
|
||||
|
||||
Words for which capitalization is important should be capitalized in the
|
||||
dictionary. Words that contain spaces or other punctuation will never match.
|
||||
"""
|
||||
self.dictionary = set()
|
||||
self.dictionary.update(w for w in wordlist)
|
||||
|
||||
def measure_words_matched(self, ocr_text: str) -> float:
|
||||
"""Check how many unique words in the OCR text match a dictionary.
|
||||
|
||||
Words with mixed capitalized are only considered a match if the test word
|
||||
matches that capitalization.
|
||||
|
||||
Returns:
|
||||
number of words that match / number
|
||||
"""
|
||||
text = re.sub(r"[0-9_]+", ' ', ocr_text)
|
||||
text = re.sub(r'\W+', ' ', text)
|
||||
text_words_list = re.split(r'\s+', text)
|
||||
text_words = {w for w in text_words_list if len(w) >= 3}
|
||||
|
||||
matches = 0
|
||||
for w in text_words:
|
||||
if w in self.dictionary or (
|
||||
w != w.lower() and w.lower() in self.dictionary
|
||||
):
|
||||
matches += 1
|
||||
if matches > 0:
|
||||
hit_ratio = matches / len(text_words)
|
||||
else:
|
||||
hit_ratio = 0.0
|
||||
return hit_ratio
|
||||
+19
-10
@@ -28,12 +28,6 @@ from ocrmypdf import api, cli
|
||||
|
||||
pytest_plugins = ['helpers_namespace']
|
||||
|
||||
try:
|
||||
from pytest_cov.embed import cleanup_on_sigterm
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
cleanup_on_sigterm()
|
||||
|
||||
# pylint: disable=E1101
|
||||
# pytest.helpers is dynamic so it confuses pylint
|
||||
@@ -137,12 +131,12 @@ def spoof(tmp_path_factory, **kwargs):
|
||||
return env
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_noop(tmp_path_factory):
|
||||
return spoof(tmp_path_factory, tesseract='tesseract_noop.py')
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_cache(tmp_path_factory):
|
||||
if running_in_docker():
|
||||
return os.environ.copy()
|
||||
@@ -201,7 +195,10 @@ def check_ocrmypdf(input_file, output_file, *args, env=None):
|
||||
|
||||
@pytest.helpers.register
|
||||
def run_ocrmypdf_api(input_file, output_file, *args, env=None):
|
||||
"Run ocrmypdf and let caller deal with results"
|
||||
"""Run ocrmypdf via API and let caller deal with results
|
||||
|
||||
Does not currently have a way to manipulate the PATH except for Tesseract.
|
||||
"""
|
||||
|
||||
options = cli.parser.parse_args(
|
||||
[str(input_file), str(output_file)]
|
||||
@@ -211,6 +208,10 @@ def run_ocrmypdf_api(input_file, output_file, *args, env=None):
|
||||
if env:
|
||||
options.tesseract_env = env.copy()
|
||||
options.tesseract_env['_OCRMYPDF_TEST_INFILE'] = os.fspath(input_file)
|
||||
first_path = env.get('_OCRMYPDF_TEST_PATH', '').split(os.pathsep)[0]
|
||||
if 'spoof' in first_path:
|
||||
assert 'gs' not in first_path, "use run_ocrmypdf() for gs"
|
||||
assert 'tesseract' in first_path
|
||||
if options.tesseract_env:
|
||||
assert all(isinstance(v, (str, bytes)) for v in options.tesseract_env.values())
|
||||
|
||||
@@ -222,13 +223,21 @@ def run_ocrmypdf(input_file, output_file, *args, env=None, universal_newlines=Tr
|
||||
"Run ocrmypdf and let caller deal with results"
|
||||
|
||||
if env is None:
|
||||
env = os.environ
|
||||
env = os.environ.copy()
|
||||
|
||||
p_args = (
|
||||
OCRMYPDF
|
||||
+ [str(arg) for arg in args if arg is not None]
|
||||
+ [str(input_file), str(output_file)]
|
||||
)
|
||||
|
||||
# Tell subprocess where to find coverage.py configuration
|
||||
# This has no effect except when coverage is running
|
||||
# Details: https://coverage.readthedocs.io/en/coverage-5.0/subprocess.html
|
||||
coverage_rc = Path(__file__).parent.parent / '.coveragerc'
|
||||
assert coverage_rc.exists()
|
||||
env['COVERAGE_PROCESS_START'] = os.fspath(coverage_rc)
|
||||
|
||||
p = run(
|
||||
p_args, stdout=PIPE, stderr=PIPE, universal_newlines=universal_newlines, env=env
|
||||
)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -35,13 +35,13 @@ def main():
|
||||
print('SPOOFED: ' + os.path.basename(__file__))
|
||||
sys.exit(0)
|
||||
|
||||
# For any rendering calls (device == pdfwrite) call real ghostscript
|
||||
if '-sDEVICE=pdfwrite' in sys.argv:
|
||||
# For non-image rastering calls, use real ghostscript
|
||||
if '-sDEVICE=pdfwrite' in sys.argv or '-sDEVICE=txtwrite' in sys.argv:
|
||||
real_ghostscript(sys.argv)
|
||||
return
|
||||
|
||||
# Fail
|
||||
print("ERROR: Ghost story archive not found")
|
||||
print("ERROR: Ghost story archive not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ def main():
|
||||
return
|
||||
|
||||
# Fail
|
||||
print("ERROR: Casper is not a friendly ghost")
|
||||
print("ERROR: Casper is not a friendly ghost", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
@@ -32,9 +32,10 @@ In orientation check mode, report the orientation is upright.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import img2pdf
|
||||
import PyPDF2 as pypdf
|
||||
import pikepdf
|
||||
from PIL import Image
|
||||
|
||||
VERSION_STRING = '''tesseract 4.0.0
|
||||
@@ -99,12 +100,10 @@ def main():
|
||||
pagesize = im.size[0] / dpi[0], im.size[1] / dpi[1]
|
||||
ptsize = pagesize[0] * 72, pagesize[1] * 72
|
||||
|
||||
pdf_out = pypdf.PdfFileWriter()
|
||||
pdf_out.addBlankPage(ptsize[0], ptsize[1])
|
||||
with open(output + '.pdf', 'wb') as f:
|
||||
pdf_out.write(f)
|
||||
with open(output + '.txt', 'w') as f:
|
||||
f.write('')
|
||||
pdf_out = pikepdf.new()
|
||||
pdf_out.add_blank_page(page_size=ptsize)
|
||||
pdf_out.save(Path(output).with_suffix('.pdf'), static_id=True)
|
||||
Path(output).with_suffix('.txt').write_text('')
|
||||
else:
|
||||
inputf = sys.argv[-4]
|
||||
output = sys.argv[-3]
|
||||
|
||||
+10
-1
@@ -15,6 +15,8 @@
|
||||
# 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 pytest
|
||||
|
||||
import ocrmypdf
|
||||
@@ -30,4 +32,11 @@ def acroform(resources):
|
||||
def test_acroform_and_redo(acroform, caplog, no_outpdf):
|
||||
with pytest.raises(ocrmypdf.exceptions.InputFileError):
|
||||
check_ocrmypdf(acroform, no_outpdf, '--redo-ocr')
|
||||
assert '--redo-ocr is not currently possible' in caplog.text
|
||||
assert '--redo-ocr is not currently possible' in caplog.text
|
||||
|
||||
|
||||
def test_acroform_message(acroform, caplog, spoof_tesseract_noop, outpdf):
|
||||
caplog.set_level(logging.INFO)
|
||||
check_ocrmypdf(acroform, outpdf, env=spoof_tesseract_noop)
|
||||
assert 'fillable form' in caplog.text
|
||||
assert '--force-ocr' in caplog.text
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# © 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 io import StringIO
|
||||
|
||||
import pytest
|
||||
from tqdm import tqdm
|
||||
|
||||
import ocrmypdf
|
||||
|
||||
|
||||
def test_raw_console():
|
||||
bio = StringIO()
|
||||
tqconsole = ocrmypdf.api.TqdmConsole(file=bio)
|
||||
tqconsole.write("Test")
|
||||
tqconsole.flush()
|
||||
assert "Test" in bio.getvalue()
|
||||
|
||||
|
||||
def test_tqdm_console():
|
||||
log = logging.getLogger()
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
formatter = logging.Formatter('%(message)s')
|
||||
|
||||
bio = StringIO()
|
||||
console = logging.StreamHandler(ocrmypdf.api.TqdmConsole(file=bio))
|
||||
console.setFormatter(formatter)
|
||||
|
||||
log.addHandler(console)
|
||||
|
||||
def before_pbar(message):
|
||||
# Ensure that log messages appear before the progress bar, even when
|
||||
# printed after the progress bar updates.
|
||||
v = bio.getvalue()
|
||||
pbar_start_marker = '|#'
|
||||
return v.index(message) < v.index(pbar_start_marker)
|
||||
|
||||
with tqdm(total=2, file=bio, disable=False) as pbar:
|
||||
pbar.update()
|
||||
msg = "1/2 above progress bar"
|
||||
log.info(msg)
|
||||
assert before_pbar(msg)
|
||||
|
||||
log.info("done")
|
||||
assert not before_pbar("done")
|
||||
+23
-23
@@ -31,28 +31,28 @@ run_ocrmypdf_api = pytest.helpers.run_ocrmypdf_api
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_gs_render_fail(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_render_failure.py'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_gs_raster_fail(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_raster_failure.py'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_no_pdfa(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_pdfa_failure.py'
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_no_tess_pdfa_warning(tmp_path_factory):
|
||||
return spoof(
|
||||
tmp_path_factory, tesseract='tesseract_noop.py', gs='gs_feature_elision.py'
|
||||
@@ -60,18 +60,18 @@ def spoof_no_tess_pdfa_warning(tmp_path_factory):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def linn(resources):
|
||||
path = resources / 'linn.pdf'
|
||||
def francais(resources):
|
||||
path = resources / 'francais.pdf'
|
||||
return path, pikepdf.open(path)
|
||||
|
||||
|
||||
def test_rasterize_size(linn, outdir, caplog):
|
||||
path, pdf = linn
|
||||
def test_rasterize_size(francais, outdir, caplog):
|
||||
path, pdf = francais
|
||||
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
|
||||
target_size = Decimal('50.0'), Decimal('30.0')
|
||||
forced_dpi = 42.0, 4242.0
|
||||
|
||||
log = logging.getLogger()
|
||||
rasterize_pdf(
|
||||
@@ -81,21 +81,21 @@ def test_rasterize_size(linn, outdir, caplog):
|
||||
target_size[1] / page_size[1],
|
||||
raster_device='pngmono',
|
||||
log=log,
|
||||
page_dpi=target_dpi,
|
||||
page_dpi=forced_dpi,
|
||||
)
|
||||
|
||||
with Image.open(outdir / 'out.png') as im:
|
||||
assert im.size == target_size
|
||||
assert im.info['dpi'] == target_dpi
|
||||
assert im.info['dpi'] == forced_dpi
|
||||
|
||||
|
||||
def test_rasterize_rotated(linn, outdir, caplog):
|
||||
path, pdf = linn
|
||||
def test_rasterize_rotated(francais, outdir, caplog):
|
||||
path, pdf = francais
|
||||
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
|
||||
target_size = Decimal('50.0'), Decimal('30.0')
|
||||
forced_dpi = 42.0, 4242.0
|
||||
|
||||
log = logging.getLogger()
|
||||
caplog.set_level(logging.DEBUG)
|
||||
@@ -106,34 +106,34 @@ def test_rasterize_rotated(linn, outdir, caplog):
|
||||
target_size[1] / page_size[1],
|
||||
raster_device='pngmono',
|
||||
log=log,
|
||||
page_dpi=target_dpi,
|
||||
page_dpi=forced_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])
|
||||
assert im.info['dpi'] == (forced_dpi[1], forced_dpi[0])
|
||||
|
||||
|
||||
def test_gs_render_failure(spoof_no_tess_gs_render_fail, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'blank.pdf', outpdf, env=spoof_no_tess_gs_render_fail
|
||||
)
|
||||
print(err)
|
||||
assert 'Casper is not a friendly ghost' in err
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
|
||||
|
||||
def test_gs_raster_failure(spoof_no_tess_gs_raster_fail, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_gs_raster_fail
|
||||
resources / 'francais.pdf', outpdf, env=spoof_no_tess_gs_raster_fail
|
||||
)
|
||||
print(err)
|
||||
assert 'Ghost story archive not found' in err
|
||||
assert p.returncode == ExitCode.child_process_error
|
||||
|
||||
|
||||
def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf):
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_no_pdfa
|
||||
resources / 'francais.pdf', outpdf, env=spoof_no_tess_no_pdfa
|
||||
)
|
||||
assert (
|
||||
p.returncode == ExitCode.pdfa_conversion_failed
|
||||
@@ -141,4 +141,4 @@ def test_ghostscript_pdfa_failure(spoof_no_tess_no_pdfa, resources, outpdf):
|
||||
|
||||
|
||||
def test_ghostscript_feature_elision(spoof_no_tess_pdfa_warning, resources, outpdf):
|
||||
check_ocrmypdf(resources / 'ccitt.pdf', outpdf, env=spoof_no_tess_pdfa_warning)
|
||||
check_ocrmypdf(resources / 'francais.pdf', outpdf, env=spoof_no_tess_pdfa_warning)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# © 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
|
||||
import multiprocessing
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import ocrmypdf.helpers as helpers
|
||||
|
||||
|
||||
class TestSafeSymlink:
|
||||
def test_safe_symlink_link_self(self, tmp_path, caplog):
|
||||
helpers.safe_symlink(tmp_path / 'self', tmp_path / 'self')
|
||||
assert caplog.record_tuples[0][1] == logging.WARNING
|
||||
|
||||
def test_safe_symlink_overwrite(self, tmp_path):
|
||||
(tmp_path / 'regular_file').touch()
|
||||
with pytest.raises(FileExistsError):
|
||||
helpers.safe_symlink(tmp_path / 'input', tmp_path / 'regular_file')
|
||||
|
||||
def test_safe_symlink_relink(self, tmp_path):
|
||||
(tmp_path / 'regular_file_a').touch()
|
||||
(tmp_path / 'regular_file_b').write_bytes(b'ABC')
|
||||
(tmp_path / 'link').symlink_to(tmp_path / 'regular_file_a')
|
||||
helpers.safe_symlink(tmp_path / 'regular_file_b', tmp_path / 'link')
|
||||
assert (tmp_path / 'link').samefile(tmp_path / 'regular_file_b') or (
|
||||
tmp_path / 'link'
|
||||
).read_bytes() == b'ABC'
|
||||
|
||||
|
||||
def test_no_cpu_count(monkeypatch):
|
||||
def cpu_count_raises():
|
||||
raise NotImplementedError()
|
||||
|
||||
monkeypatch.setattr(multiprocessing, 'cpu_count', cpu_count_raises)
|
||||
with pytest.warns(expected_warning=UserWarning):
|
||||
assert helpers.available_cpu_count() == 1
|
||||
|
||||
|
||||
def test_deprecated():
|
||||
@helpers.deprecated
|
||||
def old_function():
|
||||
return 42
|
||||
|
||||
with pytest.deprecated_call():
|
||||
assert old_function() == 42
|
||||
|
||||
|
||||
class TestFileIsWritable:
|
||||
@pytest.fixture
|
||||
def non_existent(self, tmp_path):
|
||||
return tmp_path / 'nofile'
|
||||
|
||||
@pytest.fixture
|
||||
def basic_file(self, tmp_path):
|
||||
basic = tmp_path / 'basic'
|
||||
basic.touch()
|
||||
return basic
|
||||
|
||||
def test_plain(self, non_existent):
|
||||
assert helpers.is_file_writable(non_existent)
|
||||
|
||||
def test_symlink_loop(self, tmp_path):
|
||||
loop = tmp_path / 'loop'
|
||||
loop.symlink_to(loop)
|
||||
assert not helpers.is_file_writable(loop)
|
||||
|
||||
def test_chmod(self, basic_file):
|
||||
assert helpers.is_file_writable(basic_file)
|
||||
basic_file.chmod(0o400)
|
||||
assert not helpers.is_file_writable(basic_file)
|
||||
basic_file.chmod(0o000)
|
||||
assert not helpers.is_file_writable(basic_file)
|
||||
|
||||
def test_permission_error(self, basic_file):
|
||||
pathmock = MagicMock(spec_set=basic_file)
|
||||
pathmock.is_symlink.return_value = False
|
||||
pathmock.exists.return_value = True
|
||||
pathmock.is_file.side_effect = PermissionError
|
||||
assert not helpers.is_file_writable(pathmock)
|
||||
+12
-2
@@ -45,12 +45,12 @@ spoof = pytest.helpers.spoof
|
||||
RENDERERS = ['hocr', 'sandwich']
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_crash(tmp_path_factory):
|
||||
return spoof(tmp_path_factory, tesseract='tesseract_crash.py')
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_tesseract_big_image_error(tmp_path_factory):
|
||||
return spoof(tmp_path_factory, tesseract='tesseract_big_image_error.py')
|
||||
|
||||
@@ -270,6 +270,16 @@ def test_input_file_not_found(caplog, no_outpdf):
|
||||
assert input_file in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == 'nt', reason="chmod")
|
||||
def test_input_file_not_readable(caplog, resources, outdir, no_outpdf):
|
||||
input_file = outdir / 'trivial.pdf'
|
||||
shutil.copy(resources / 'trivial.pdf', input_file)
|
||||
input_file.chmod(0o000)
|
||||
result = run_ocrmypdf_api(input_file, no_outpdf)
|
||||
assert result == ExitCode.input_file
|
||||
assert str(input_file) in caplog.text
|
||||
|
||||
|
||||
def test_input_file_not_a_pdf(caplog, no_outpdf):
|
||||
input_file = __file__ # Try to OCR this file
|
||||
result = run_ocrmypdf_api(input_file, no_outpdf)
|
||||
|
||||
+38
-8
@@ -31,8 +31,11 @@ import pytest
|
||||
from pikepdf.models.metadata import decode_pdf_date
|
||||
|
||||
from ocrmypdf._jobcontext import PDFContext
|
||||
from ocrmypdf._pipeline import convert_to_pdfa
|
||||
from ocrmypdf.cli import parser
|
||||
from ocrmypdf.exceptions import ExitCode
|
||||
from ocrmypdf.pdfa import SRGB_ICC_PROFILE, file_claims_pdfa, generate_pdfa_ps
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
|
||||
try:
|
||||
import fitz
|
||||
@@ -313,19 +316,21 @@ def test_metadata_fixup_warning(resources, outdir, caplog):
|
||||
|
||||
|
||||
def test_prevent_gs_invalid_xml(resources, outdir):
|
||||
from ocrmypdf.__main__ import parser
|
||||
from ocrmypdf._pipeline import convert_to_pdfa
|
||||
from ocrmypdf.pdfa import generate_pdfa_ps
|
||||
from ocrmypdf.pdfinfo import PdfInfo
|
||||
|
||||
generate_pdfa_ps(outdir / 'pdfa.ps')
|
||||
copyfile(resources / 'enron1.pdf', outdir / 'layers.rendered.pdf')
|
||||
copyfile(resources / 'trivial.pdf', outdir / 'layers.rendered.pdf')
|
||||
|
||||
# Inject a string with a trailing nul character into the DocumentInfo
|
||||
# dictionary of this PDF, as often occurs in practice.
|
||||
with pikepdf.open(outdir / 'layers.rendered.pdf') as pike:
|
||||
pike.Root.DocumentInfo = pikepdf.Dictionary(
|
||||
Title=b'String with trailing nul\x00'
|
||||
)
|
||||
|
||||
options = parser.parse_args(
|
||||
args=['-j', '1', '--output-type', 'pdfa-2', 'a.pdf', 'b.pdf']
|
||||
)
|
||||
pdfinfo = PdfInfo(resources / 'enron1.pdf')
|
||||
context = PDFContext(options, outdir, resources / 'enron1.pdf', pdfinfo)
|
||||
pdfinfo = PdfInfo(outdir / 'layers.rendered.pdf')
|
||||
context = PDFContext(options, outdir, outdir / 'layers.rendered.pdf', pdfinfo)
|
||||
|
||||
convert_to_pdfa(
|
||||
str(outdir / 'layers.rendered.pdf'), str(outdir / 'pdfa.ps'), context
|
||||
@@ -339,5 +344,30 @@ def test_prevent_gs_invalid_xml(resources, outdir):
|
||||
xmp_start = mm.find(XMP_MAGIC)
|
||||
xmp_end = mm.rfind(b'<?xpacket end', xmp_start)
|
||||
assert 0 < xmp_start < xmp_end
|
||||
# Ensure we did not carry the nul forward.
|
||||
assert mm.find(b'�', xmp_start, xmp_end) == -1, "found escaped nul"
|
||||
assert mm.find(b'\x00', xmp_start, xmp_end) == -1
|
||||
|
||||
|
||||
def test_malformed_docinfo(caplog, resources, outdir):
|
||||
generate_pdfa_ps(outdir / 'pdfa.ps')
|
||||
# copyfile(resources / 'trivial.pdf', outdir / 'layers.rendered.pdf')
|
||||
|
||||
with pikepdf.open(resources / 'trivial.pdf') as pike:
|
||||
pike.trailer.Info = pikepdf.Stream(pike, b"<xml></xml>")
|
||||
pike.save(outdir / 'layers.rendered.pdf', fix_metadata_version=False)
|
||||
|
||||
options = parser.parse_args(
|
||||
args=['-j', '1', '--output-type', 'pdfa-2', 'a.pdf', 'b.pdf']
|
||||
)
|
||||
pdfinfo = PdfInfo(outdir / 'layers.rendered.pdf')
|
||||
context = PDFContext(options, outdir, outdir / 'layers.rendered.pdf', pdfinfo)
|
||||
|
||||
convert_to_pdfa(
|
||||
str(outdir / 'layers.rendered.pdf'), str(outdir / 'pdfa.ps'), context
|
||||
)
|
||||
|
||||
print(caplog.records)
|
||||
assert any(
|
||||
'malformed DocumentInfo block' in record.message for record in caplog.records
|
||||
)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# © 2020 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 pytest
|
||||
|
||||
import ocrmypdf.quality as qual
|
||||
|
||||
|
||||
def test_quality_measurement():
|
||||
oqd = qual.OcrQualityDictionary(
|
||||
wordlist=["words", "words", "quick", "brown", "fox", "dog", "lazy"]
|
||||
)
|
||||
assert len(oqd.dictionary) == 6 # 6 unique
|
||||
|
||||
assert (
|
||||
oqd.measure_words_matched("The quick brown fox jumps quickly over the lazy dog")
|
||||
== 0.5
|
||||
)
|
||||
assert oqd.measure_words_matched("12345 10% _f 7fox -brown | words") == 1.0
|
||||
|
||||
assert oqd.measure_words_matched("quick quick quick") == 1.0
|
||||
+7
-1
@@ -33,7 +33,7 @@ run_ocrmypdf_api = pytest.helpers.run_ocrmypdf
|
||||
spoof = pytest.helpers.spoof
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
@pytest.fixture
|
||||
def spoof_tess_bad_utf8(tmp_path_factory):
|
||||
return spoof(tmp_path_factory, tesseract='tesseract_badutf8.py')
|
||||
|
||||
@@ -56,6 +56,9 @@ def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||
|
||||
|
||||
def test_stdout(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||
if 'COV_CORE_DATAFILE' in spoof_tesseract_noop:
|
||||
pytest.skip(msg="Coverage uses stdout")
|
||||
|
||||
input_file = str(resources / 'francais.pdf')
|
||||
output_file = str(outpdf)
|
||||
|
||||
@@ -121,6 +124,9 @@ def test_bad_locale():
|
||||
reason="Windows does not like this; not sure how to fix",
|
||||
)
|
||||
def test_dev_null(spoof_tesseract_noop, resources):
|
||||
if 'COV_CORE_DATAFILE' in spoof_tesseract_noop:
|
||||
pytest.skip(msg="Coverage uses stdout")
|
||||
|
||||
p, out, err = run_ocrmypdf(
|
||||
resources / 'trivial.pdf', os.devnull, '--force-ocr', env=spoof_tesseract_noop
|
||||
)
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
# 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 subprocess
|
||||
from contextlib import contextmanager
|
||||
from os import fspath
|
||||
from pathlib import Path
|
||||
@@ -81,3 +83,95 @@ def test_no_languages(tmp_path):
|
||||
|
||||
with pytest.raises(MissingDependencyError):
|
||||
tesseract.languages(tesseract_env=env)
|
||||
|
||||
|
||||
def test_image_too_large_hocr(monkeypatch, resources, outdir):
|
||||
log = logging.getLogger('test_image_too_large_hocr')
|
||||
|
||||
def dummy_run(args, *, env=None, **kwargs):
|
||||
raise subprocess.CalledProcessError(1, 'tesseract', output=b'Image too large')
|
||||
|
||||
monkeypatch.setattr(tesseract, 'run', dummy_run)
|
||||
tesseract.generate_hocr(
|
||||
input_file=resources / 'crom.png',
|
||||
output_files=[outdir / 'out.hocr', outdir / 'out.txt'],
|
||||
language=['eng'],
|
||||
engine_mode=None,
|
||||
tessconfig=[],
|
||||
timeout=180.0,
|
||||
pagesegmode=None,
|
||||
log=log,
|
||||
user_words=None,
|
||||
user_patterns=None,
|
||||
tesseract_env=None,
|
||||
)
|
||||
assert "name='ocr-capabilities'" in Path(outdir / 'out.hocr').read_text()
|
||||
|
||||
|
||||
def test_image_too_large_pdf(monkeypatch, resources, outdir):
|
||||
log = logging.getLogger('test_image_too_large_pdf')
|
||||
|
||||
def dummy_run(args, *, env=None, **kwargs):
|
||||
raise subprocess.CalledProcessError(1, 'tesseract', output=b'Image too large')
|
||||
|
||||
monkeypatch.setattr(tesseract, 'run', dummy_run)
|
||||
tesseract.generate_pdf(
|
||||
input_image=resources / 'crom.png',
|
||||
skip_pdf=resources / 'blank.pdf',
|
||||
output_pdf=outdir / 'pdf.pdf',
|
||||
output_text=outdir / 'txt.txt',
|
||||
language=['eng'],
|
||||
engine_mode=None,
|
||||
text_only=False,
|
||||
tessconfig=[],
|
||||
timeout=180.0,
|
||||
pagesegmode=None,
|
||||
log=log,
|
||||
user_words=None,
|
||||
user_patterns=None,
|
||||
tesseract_env=None,
|
||||
)
|
||||
assert Path(outdir / 'txt.txt').read_text() == '[skipped page]'
|
||||
if os.name != 'nt': # different semantics
|
||||
assert Path(outdir / 'pdf.pdf').samefile(resources / 'blank.pdf')
|
||||
|
||||
|
||||
def test_timeout(caplog):
|
||||
log = logging.getLogger('test_timeout')
|
||||
tesseract.page_timedout(log, '123456.png', 5)
|
||||
assert "123456" in caplog.text
|
||||
assert "took too long" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'in_, logged',
|
||||
[
|
||||
(b'Tesseract Open Source', ''),
|
||||
(b'lots of diacritics blah blah', 'diacritics'),
|
||||
(b'Warning in pixReadMem', ''),
|
||||
(b'OSD: Weak margin', 'unsure about page orientation'),
|
||||
(b'Error in pixScanForForeground', ''),
|
||||
(b'Error in boxClipToRectangle', ''),
|
||||
(b'an unexpected error', 'an unexpected error'),
|
||||
(b'a dire warning', 'a dire warning'),
|
||||
(b'read_params_file something', 'read_params_file'),
|
||||
(b'an innocent message', 'innocent'),
|
||||
(b'\x7f\x7f\x80innocent unicode failure', 'innocent'),
|
||||
],
|
||||
)
|
||||
def test_tesseract_log_output(caplog, in_, logged):
|
||||
log = logging.getLogger('tesseract_log_output')
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
tesseract.tesseract_log_output(log, in_, 'dummy')
|
||||
if logged == '':
|
||||
assert caplog.text == ''
|
||||
else:
|
||||
assert logged in caplog.text
|
||||
|
||||
|
||||
def test_tesseract_log_output_raises(caplog):
|
||||
log = logging.getLogger('tesseract_log_output')
|
||||
with pytest.raises(tesseract.TesseractConfigError):
|
||||
tesseract.tesseract_log_output(log, b'parameter not found: moo', 'dummy')
|
||||
assert 'not found' in caplog.text
|
||||
|
||||
@@ -43,7 +43,7 @@ def have_unpaper():
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@pytest.fixture
|
||||
def spoof_unpaper_oldversion(tmp_path_factory):
|
||||
return spoof(tmp_path_factory, unpaper="unpaper_oldversion.py")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user