Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b59e761a14 | ||
|
|
17cd655752 | ||
|
|
b840b16c82 | ||
|
|
3834d1a0bf | ||
|
|
33e982b3fd | ||
|
|
43d650e78c | ||
|
|
b4c65c5781 | ||
|
|
d96867e6ab | ||
|
|
0a5108e704 | ||
|
|
58abb5785c | ||
|
|
509e75eaff | ||
|
|
0c50eedb2a | ||
|
|
c38ff90081 | ||
|
|
4c029e973f | ||
|
|
21cf9029e8 | ||
|
|
4a640b8dcd | ||
|
|
9471bc8921 | ||
|
|
7fe06c64fc | ||
|
|
d13d70fd56 | ||
|
|
58ec56180a | ||
|
|
32a88f1bad | ||
|
|
99ef42940c | ||
|
|
c152710617 | ||
|
|
8de0f9b86f | ||
|
|
23bc3d3a29 | ||
|
|
8307832ce9 | ||
|
|
dd1cf567db | ||
|
|
2490be8490 | ||
|
|
85e6c6669a | ||
|
|
00498282f5 | ||
|
|
e4cc9fcba7 | ||
|
|
a4555b1dae | ||
|
|
f35a2303bb | ||
|
|
82142fe5ef | ||
|
|
9be533b5f4 | ||
|
|
99653fcd32 | ||
|
|
5442c97ed8 | ||
|
|
0165255bd9 | ||
|
|
378e4dae3b | ||
|
|
cdf5afa753 | ||
|
|
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 |
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
What command line or API call were you trying to run?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ocrmypdf ...arguments... input.pdf output.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with verbosity or higher `-v1` to see more detailed logging. This information may be helpful.
|
||||||
|
|
||||||
|
**Example file**
|
||||||
|
Please include an example *input* PDF (or image). The input file is more helpful.
|
||||||
|
|
||||||
|
If possible, use an input file with no personal or confidential information. At your option you may GPG-encrypt the file for OCRmyPDF's author only.
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**System**
|
||||||
|
- OS: [e.g. Linux, Windows, macOS]
|
||||||
|
- OCRmyPDF Version: ``ocrmypdf --version``
|
||||||
|
- How did you install ocrmypdf? Did you use a system package manager, `pip`, or a Docker image?
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
-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
|
|
||||||
@@ -130,6 +130,7 @@ Press & Media
|
|||||||
- [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)
|
- [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](https://heise.de/-2279695): Detailed presentation of OCRmyPDF v1.0 in the leading German IT magazine c't
|
- [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 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
|
Business enquiries
|
||||||
------------------
|
------------------
|
||||||
|
|||||||
+7
-5
@@ -153,12 +153,13 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
Python37:
|
Python37:
|
||||||
python.version: "3.7"
|
python.version: "3.7"
|
||||||
Python38:
|
# Python38:
|
||||||
python.version: "3.8"
|
# python.version: "3.8"
|
||||||
steps:
|
steps:
|
||||||
- task: UsePythonVersion@0
|
# https://github.com/actions/virtual-environments/issues/664
|
||||||
inputs:
|
# - task: UsePythonVersion@0
|
||||||
versionSpec: "$(python.version)"
|
# inputs:
|
||||||
|
# versionSpec: "$(python.version)"
|
||||||
- bash: |
|
- bash: |
|
||||||
brew update
|
brew update
|
||||||
brew unlink python@2
|
brew unlink python@2
|
||||||
@@ -169,6 +170,7 @@ stages:
|
|||||||
leptonica \
|
leptonica \
|
||||||
openjpeg \
|
openjpeg \
|
||||||
pngquant \
|
pngquant \
|
||||||
|
python \
|
||||||
tesseract \
|
tesseract \
|
||||||
unpaper
|
unpaper
|
||||||
displayName: "Install system packages"
|
displayName: "Install system packages"
|
||||||
|
|||||||
Vendored
-6
@@ -95,12 +95,6 @@ Files: tests/resources/vector.pdf
|
|||||||
Copyright: (C) 2018 Catscratch
|
Copyright: (C) 2018 Catscratch
|
||||||
License: Expat
|
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
|
Files: src/ocrmypdf/data/sRGB.icc
|
||||||
Copyright: Kai-Uwe Behrmann <www.behrmann.name>
|
Copyright: Kai-Uwe Behrmann <www.behrmann.name>
|
||||||
Marti Maria <www.littlecms.com>
|
Marti Maria <www.littlecms.com>
|
||||||
|
|||||||
@@ -51,6 +51,14 @@ Forking a child process to call ``ocrmypdf.ocr()`` is suggested. That
|
|||||||
way your application will survive and remain interactive even if
|
way your application will survive and remain interactive even if
|
||||||
OCRmyPDF does not.
|
OCRmyPDF does not.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
On Windows, the script that calls ``ocrmypdf.ocr()`` must be protected
|
||||||
|
by an "ifmain" guard (``if __name__ == '__main__'``) or you must use
|
||||||
|
``ocrmypdf.ocr(...use_threads=True)``. If you do not take at least one
|
||||||
|
of these steps, Windows fork semantics will prevent OCRmyPDF from working
|
||||||
|
correct.
|
||||||
|
|
||||||
Logging
|
Logging
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|||||||
+57
-159
@@ -69,53 +69,8 @@ Sample script
|
|||||||
This user contributed script also provides an example of batch
|
This user contributed script also provides an example of batch
|
||||||
processing.
|
processing.
|
||||||
|
|
||||||
.. code-block:: python
|
.. literalinclude:: ../misc/batch.py
|
||||||
|
:caption: misc/batch.py
|
||||||
#!/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)
|
|
||||||
|
|
||||||
Synology DiskStations
|
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
|
adjustments might be needed to deal with the Synology's relatively
|
||||||
limited CPU and RAM.
|
limited CPU and RAM.
|
||||||
|
|
||||||
.. code-block:: python
|
.. literalinclude:: ../misc/synology.py
|
||||||
|
:caption: misc/synology.py - Sample script for Synology DiskStations
|
||||||
#!/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')
|
|
||||||
|
|
||||||
Huge batch jobs
|
Huge batch jobs
|
||||||
---------------
|
---------------
|
||||||
@@ -198,11 +99,45 @@ and all inquiries are appreciated.
|
|||||||
Hot (watched) folders
|
Hot (watched) folders
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
Watched folders with watcher.py
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
OCRmyPDF has a folder watcher called watcher.py, which is currently included in source
|
||||||
|
distributions but not part of the main program. It may be used natively or may run
|
||||||
|
in a Docker container. Native instances tend to give better performance. watcher.py
|
||||||
|
works on all platforms.
|
||||||
|
|
||||||
|
Users may need to customize the script to meet their requirements.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip3 install -r requirements/watcher.txt
|
||||||
|
|
||||||
|
env OCR_INPUT_DIRECTORY=/mnt/input-pdfs \
|
||||||
|
OCR_OUTPUT_DIRECTORY=/mnt/output-pdfs \
|
||||||
|
OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
||||||
|
python3 watcher.py
|
||||||
|
|
||||||
|
.. csv-table:: watcher.py environment variables
|
||||||
|
:header: "Environment variable", "Description"
|
||||||
|
:widths: 50, 50
|
||||||
|
|
||||||
|
"OCR_INPUT_DIRECTORY", "Set input directory to monitor (recursive)"
|
||||||
|
"OCR_OUTPUT_DIRECTORY", "Set output directory (should not be under input)"
|
||||||
|
"OCR_ON_SUCCESS_DELETE", "This will delete the input file if the exit code is 0 (OK)"
|
||||||
|
"OCR_OUTPUT_DIRECTORY_YEAR_MONTH", "This will place files in the output in ``{output}/{year}/{month}/{filename}``"
|
||||||
|
"OCR_DESKEW", "Apply deskew to crooked input PDFs"
|
||||||
|
"OCR_JSON_SETTINGS", "A JSON string specifying any other arguments for ``ocrmypdf.ocr``"
|
||||||
|
"OCR_POLL_NEW_FILE_SECONDS", "Polling interval"
|
||||||
|
"OCR_LOGLEVEL", "Level of log messages to report"
|
||||||
|
|
||||||
|
One could configure a networked scanner or scanning computer to drop files in the
|
||||||
|
watched folder.
|
||||||
|
|
||||||
Watched folders with Docker
|
Watched folders with Docker
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
The OCRmyPDF Docker image includes a watcher service. This service can
|
The watcher service is included in the OCRmyPDF Docker image. To run it:
|
||||||
be launched as follows:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
@@ -226,64 +161,21 @@ 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 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"
|
"``-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_OUTPUT_DIRECTORY_YEAR_MONTH=1``", "Define environment variable OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1"
|
||||||
"``-e OCR_ON_SUCCESS_DELETE=1``", "This will delete the input file if the exit code is 0 (OK)"
|
"``-e OCR_ON_SUCCESS_DELETE=1``", "Define environment variable"
|
||||||
"``-e OCR_DESKEW=1``", "This will enable deskew for crooked PDFs"
|
"``-e OCR_DESKEW=1``", "Define environment variable"
|
||||||
"``-e PYTHONBUFFERED=1``", "This will force STDOUT to be unbuffered and allow you to see messages in docker logs"
|
"``-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
|
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
|
may not be suitable for some environments, such as filesystems shared on a
|
||||||
slow network.
|
slow network.
|
||||||
|
|
||||||
Watched folders with watcher.py
|
A configuration manager such as Docker Compose could be used to ensure that the
|
||||||
-------------------------------
|
service is always available.
|
||||||
|
|
||||||
The watcher service may also be run natively.
|
.. literalinclude:: ../misc/docker-compose.example.yml
|
||||||
|
:language: yaml
|
||||||
.. code-block:: bash
|
:caption: misc/docker-compose.example.yml
|
||||||
|
|
||||||
pip3 install -r reqs/watcher.txt
|
|
||||||
|
|
||||||
env OCR_INPUT_DIRECTORY=/mnt/input-pdfs \
|
|
||||||
OCR_OUTPUT_DIRECTORY=/mnt/output-pdfs \
|
|
||||||
OCR_OUTPUT_DIRECTORY_YEAR_MONTH=1 \
|
|
||||||
python3 watcher.py
|
|
||||||
|
|
||||||
Watched folders with CLI
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
To set up a "hot folder" that will trigger OCR for every file inserted,
|
|
||||||
use a program like Python
|
|
||||||
`watchdog <https://pypi.python.org/pypi/watchdog>`__ (supports all major
|
|
||||||
OS).
|
|
||||||
|
|
||||||
One could then configure a scanner to automatically place scanned files
|
|
||||||
in a hot folder, so that they will be queued for OCR and copied to the
|
|
||||||
destination.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
pip install watchdog
|
|
||||||
|
|
||||||
watchdog installs the command line program ``watchmedo``, which can be
|
|
||||||
told to run ``ocrmypdf`` on any .pdf added to the current directory
|
|
||||||
(``.``) and place the result in the previously created ``out/`` folder.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
cd hot-folder
|
|
||||||
mkdir out
|
|
||||||
watchmedo shell-command \
|
|
||||||
--patterns="*.pdf" \
|
|
||||||
--ignore-directories \
|
|
||||||
--command='ocrmypdf "${watch_src_path}" "out/${watch_src_path}" ' \
|
|
||||||
. # don't forget the final dot
|
|
||||||
|
|
||||||
On file servers, you could configure watchmedo as a system service so it
|
|
||||||
will run all the time.
|
|
||||||
|
|
||||||
For more complex behavior you can write a Python script around to use
|
|
||||||
the watchdog API. You can refer to the watcher.py script as an example.
|
|
||||||
|
|
||||||
Caveats
|
Caveats
|
||||||
-------
|
-------
|
||||||
@@ -310,6 +202,13 @@ Alternatives
|
|||||||
- `Watchman <https://facebook.github.io/watchman/>`__ is a more
|
- `Watchman <https://facebook.github.io/watchman/>`__ is a more
|
||||||
powerful alternative to ``watchmedo``.
|
powerful alternative to ``watchmedo``.
|
||||||
|
|
||||||
|
AWS Lambda is not viable
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
AWS Lambda and its equivalents have low limits on execution time and payload
|
||||||
|
size, relative to OCRmyPDF's needs. As of this writing, the request/response
|
||||||
|
payload for AWS Lambda was 6 MB, which means many PDFs will not fit.
|
||||||
|
|
||||||
macOS Automator
|
macOS Automator
|
||||||
===============
|
===============
|
||||||
|
|
||||||
@@ -319,8 +218,7 @@ of Automator, the ``PATH`` may be set differently your Terminal's
|
|||||||
``PATH``; you may need to explicitly set the PATH to include
|
``PATH``; you may need to explicitly set the PATH to include
|
||||||
``ocrmypdf``. The following example may serve as a starting point:
|
``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.
|
You may customize the command sent to ocrmypdf.
|
||||||
|
|
||||||
.. |Example macOS Automator script| image:: images/macos-workflow.png
|
|
||||||
|
|||||||
@@ -237,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
|
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
|
Redo existing OCR
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
|||||||
+18
-18
@@ -56,52 +56,52 @@ See the Docker documentation for
|
|||||||
Using the Docker image on the command line
|
Using the Docker image on the command line
|
||||||
==========================================
|
==========================================
|
||||||
|
|
||||||
**Unlike typical Docker containers**, in this mode we are using the
|
**Unlike typical Docker containers**, in this section the OCRmyPDF Docker
|
||||||
OCRmyPDF Docker container is intended to be emphemeral – it runs for one
|
container is emphemeral – it runs for one OCR job and terminates, just like a
|
||||||
OCR job and then terminates, just like a command line program. We are
|
command line program. We are using Docker to deliver an application (as opposed
|
||||||
using Docker as a way of delivering an application, not a server.
|
to the more conventional case, where a Docker container runs as a server).
|
||||||
|
|
||||||
To start a Docker container (instance of the image):
|
To start a Docker container (instance of the image):
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker tag jbarlow83/ocrmypdf ocrmypdf
|
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
|
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
|
easier to send the input file as stdin and read the output from
|
||||||
stdout – this avoids the occasionally messy permission issues with
|
stdout – **this avoids the messy permission issues with Docker entirely**.
|
||||||
Docker entirely.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
alias ocrmypdf='docker run --rm -i ocrmypdf'
|
alias docker_ocrmypdf='docker run --rm -i ocrmypdf'
|
||||||
ocrmypdf --version # runs docker version
|
docker_ocrmypdf --version # runs docker version
|
||||||
ocrmypdf <input.pdf >output.pdf
|
docker_ocrmypdf - - <input.pdf >output.pdf
|
||||||
|
|
||||||
Or in the wonderful `fish shell <https://fishshell.com/>`__:
|
Or in the wonderful `fish shell <https://fishshell.com/>`__:
|
||||||
|
|
||||||
.. code-block:: fish
|
.. code-block:: fish
|
||||||
|
|
||||||
alias ocrmypdf 'docker run --rm ocrmypdf'
|
alias docker_ocrmypdf 'docker run --rm ocrmypdf'
|
||||||
funcsave ocrmypdf
|
funcsave docker_ocrmypdf
|
||||||
|
|
||||||
Alternately, you could mount the local current working directory as a
|
Alternately, you could mount the local current working directory as a
|
||||||
Docker volume:
|
Docker volume:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
docker run --rm -v $(pwd):/data ocrmypdf /data/input.pdf /data/output.pdf
|
alias docker_ocrmypdf='docker run --rm -i --user "$(id -u):$(id -g)" --workdir /data -v "$PWD:/data" ocrmypdf'
|
||||||
|
docker_ocrmypdf /data/input.pdf /data/output.pdf
|
||||||
|
|
||||||
.. _docker-lang-packs:
|
.. _docker-lang-packs:
|
||||||
|
|
||||||
Adding languages to the Docker image
|
Adding languages to the Docker image
|
||||||
====================================
|
====================================
|
||||||
|
|
||||||
By default the Docker image includes English, German and Simplified
|
By default the Docker image includes English, German, Simplified Chinese,
|
||||||
Chinese, the most popular languages for OCRmyPDF users based on
|
French, Portuguese and Spanish, the most popular languages for OCRmyPDF
|
||||||
feedback. You may add other languages by creating a new Dockerfile based
|
users based on feedback. You may add other languages by creating a new
|
||||||
on the public one:
|
Dockerfile based on the public one:
|
||||||
|
|
||||||
.. code-block:: dockerfile
|
.. code-block:: dockerfile
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
OCRmyPDF documentation
|
OCRmyPDF documentation
|
||||||
======================
|
======================
|
||||||
|
|
||||||
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
|
OCRmyPDF adds an optical charcter recognition (OCR) text layer to scanned PDF
|
||||||
be searched.
|
files, allowing them to be searched.
|
||||||
|
|
||||||
PDF is the best format for storing and exchanging scanned documents.
|
PDF is the best format for storing and exchanging scanned documents.
|
||||||
Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply
|
Unfortunately, PDFs can be difficult to modify. OCRmyPDF makes it easy to apply
|
||||||
|
|||||||
+82
-16
@@ -110,7 +110,7 @@ Fedora 29 or newer
|
|||||||
| |fedora-29| |fedora-30| |fedora-rawhide| |
|
| |fedora-29| |fedora-30| |fedora-rawhide| |
|
||||||
+-----------------------------------------------+
|
+-----------------------------------------------+
|
||||||
|
|
||||||
Users of Fedora 29 later may simply
|
Users of Fedora 29 or later may simply
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
@@ -136,11 +136,13 @@ from sources <#installing-head-revision-from-sources>`__.
|
|||||||
Installing the latest version on Ubuntu 18.04 LTS
|
Installing the latest version on Ubuntu 18.04 LTS
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
|
|
||||||
Ubuntu 18.04 includes ocrmypdf 6.1.2. To install a more recent version,
|
Ubuntu 18.04 includes ocrmypdf 6.1.2 - you can install that with ``apt``, but
|
||||||
first install the system version to get most of the dependencies:
|
it is quite old now. To install a more recent version, uninstall the old version
|
||||||
|
of ocrmypdf, and install the following dependencies:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
sudo apt-get -y remove ocrmypdf
|
||||||
sudo apt-get -y update
|
sudo apt-get -y update
|
||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
@@ -216,7 +218,8 @@ of ``pip`` at ``/usr/local/bin/pip``.
|
|||||||
**Install OCRmyPDF**
|
**Install OCRmyPDF**
|
||||||
|
|
||||||
OCRmyPDF requires the locale to be set for UTF-8. **On some minimal
|
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
|
.. code-block:: bash
|
||||||
|
|
||||||
@@ -230,7 +233,7 @@ environment variable contains ``$HOME/.local/bin``.
|
|||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
export PATH=$HOME/.local/bin:$PATH
|
export PATH=$HOME/.local/bin:$PATH
|
||||||
pip3 install --user ocrmypdf
|
pip3.6 install --user ocrmypdf
|
||||||
|
|
||||||
To add JBIG2 encoding, see :ref:`jbig2`.
|
To add JBIG2 encoding, see :ref:`jbig2`.
|
||||||
|
|
||||||
@@ -296,23 +299,86 @@ compiled by hand.
|
|||||||
|
|
||||||
To add JBIG2 encoding, see :ref:`jbig2`.
|
To add JBIG2 encoding, see :ref:`jbig2`.
|
||||||
|
|
||||||
ArchLinux (AUR)
|
Arch Linux (AUR)
|
||||||
---------------
|
----------------
|
||||||
|
|
||||||
.. image:: https://repology.org/badge/version-for-repo/aur/ocrmypdf.svg
|
.. image:: https://repology.org/badge/version-for-repo/aur/ocrmypdf.svg
|
||||||
:alt: ArchLinux
|
:alt: ArchLinux
|
||||||
:target: https://repology.org/metapackage/ocrmypdf
|
:target: https://repology.org/metapackage/ocrmypdf
|
||||||
|
|
||||||
There is an `ArchLinux User Repository package for
|
There is an `Arch User Repository (AUR) package for OCRmyPDF
|
||||||
ocrmypdf <https://aur.archlinux.org/packages/ocrmypdf/>`__. You can use
|
<https://aur.archlinux.org/packages/ocrmypdf/>`__.
|
||||||
the following command.
|
|
||||||
|
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
|
.. code-block:: bash
|
||||||
|
|
||||||
yaourt -S ocrmypdf
|
sudo pacman -S base-devel
|
||||||
|
|
||||||
If you have any difficulties with installation, check the repository
|
The OCRmyPDF package depends on `the python-pdfminer.six AUR package
|
||||||
package page.
|
<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
|
Alpine Linux
|
||||||
------------
|
------------
|
||||||
@@ -437,12 +503,12 @@ Installing on FreeBSD
|
|||||||
:alt: FreeBSD
|
:alt: FreeBSD
|
||||||
:target: https://repology.org/project/python:ocrmypdf/versions
|
: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.
|
versions likely work but have not been tested.
|
||||||
|
|
||||||
.. code-block:: bash
|
.. 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
|
To install a more recent version, you could attempt to first install the system
|
||||||
version with ``pkg``, then use ``pip install --user ocrmypdf``.
|
version with ``pkg``, then use ``pip install --user ocrmypdf``.
|
||||||
@@ -509,7 +575,7 @@ Installing on Windows Subsystem for Linux
|
|||||||
|
|
||||||
.. code-block:: powershell
|
.. code-block:: powershell
|
||||||
|
|
||||||
wsl sudo ln -s /home/user/.local/bin/ocrmypdf /usr/local/bin/ocrmypdf
|
wsl sudo ln -s /home/$USER/.local/bin/ocrmypdf /usr/local/bin/ocrmypdf
|
||||||
|
|
||||||
Then confirm that the expected version from PyPI (|latest|) is installed:
|
Then confirm that the expected version from PyPI (|latest|) is installed:
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ About OCR
|
|||||||
`Optical character
|
`Optical character
|
||||||
recognition <https://en.wikipedia.org/wiki/Optical_character_recognition>`__
|
recognition <https://en.wikipedia.org/wiki/Optical_character_recognition>`__
|
||||||
is technology that converts images of typed or handwritten text, such as
|
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
|
OCRmyPDF uses
|
||||||
`Tesseract <https://github.com/tesseract-ocr/tesseract>`__, the best
|
`Tesseract <https://github.com/tesseract-ocr/tesseract>`__, the best
|
||||||
|
|||||||
+11
-2
@@ -4,11 +4,20 @@
|
|||||||
Installing additional language packs
|
Installing additional language packs
|
||||||
====================================
|
====================================
|
||||||
|
|
||||||
OCRmyPDF uses Tesseract for OCR, and relies on its language packs for
|
OCRmyPDF uses Tesseract for OCR, and relies on its language packs for all languages.
|
||||||
languages other than English.
|
On most platforms, English is installed with Tesseract by default, but not always.
|
||||||
|
|
||||||
Tesseract supports `most
|
Tesseract supports `most
|
||||||
languages <https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages>`__.
|
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
|
For Linux users, you can often find packages that provide language
|
||||||
packs:
|
packs:
|
||||||
|
|||||||
@@ -13,6 +13,67 @@ Note that it is licensed under GPLv3, so scripts that
|
|||||||
``import ocrmypdf`` and are released publicly should probably also be
|
``import ocrmypdf`` and are released publicly should probably also be
|
||||||
licensed under GPLv3.
|
licensed under GPLv3.
|
||||||
|
|
||||||
|
v9.8.0
|
||||||
|
======
|
||||||
|
|
||||||
|
- Fixed issue where only the first PNG (FlateDecode) image in a file would be
|
||||||
|
considered for optimization. File sizes should be improved from here on.
|
||||||
|
- Fixed a startup crash when the chosen language was Japanese (#543).
|
||||||
|
- Added options to configure polling and log level to watcher.py.
|
||||||
|
|
||||||
|
v9.7.2
|
||||||
|
======
|
||||||
|
|
||||||
|
- Fixed an issue with ``ocrmypdf.ocr(...language=)`` not accepting a list of
|
||||||
|
languages as documented.
|
||||||
|
- Updated setup.py to confirm that pdfminer.six version 20200402 is supported.
|
||||||
|
|
||||||
|
v9.7.1
|
||||||
|
======
|
||||||
|
|
||||||
|
- Fixed version check failing when used with qpdf 10.0.0.
|
||||||
|
- Added some missing type annotations.
|
||||||
|
- Updated documentation to warn about need for "ifmain" guard and Windows.
|
||||||
|
|
||||||
|
v9.7.0
|
||||||
|
======
|
||||||
|
|
||||||
|
- Fixed an error in watcher.py if ``OCR_JSON_SETTINGS`` was not defined.
|
||||||
|
- Ghostscript 9.51 is now blacklisted, due to numerous problems with this version.
|
||||||
|
- Added a workaround for a problem with "txtwrite" in Ghostscript 9.52.
|
||||||
|
- Fixed an issue where the incorrect number of threads used was shown when
|
||||||
|
``OMP_THREAD_LIMIT`` was manipulated.
|
||||||
|
- Removed a possible performance bottlenecks for files that use hundreds to
|
||||||
|
thousands of images on the same page.
|
||||||
|
- Documentation improvements.
|
||||||
|
- Optimization will now be applied to some monochrome images that have a color
|
||||||
|
profile defined instead of only black and white.
|
||||||
|
- ICC profiles are consulted when determining the simplified colorspace of an
|
||||||
|
image.
|
||||||
|
|
||||||
|
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
|
v9.6.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()
|
_ocrmypdf()
|
||||||
{
|
{
|
||||||
local cur prev cword words split
|
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
|
case $prev in
|
||||||
--version|-h|--help)
|
--version|-h|--help)
|
||||||
@@ -65,25 +91,6 @@ _ocrmypdf()
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
$split && return
|
$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
|
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')
|
||||||
+22
-3
@@ -1,4 +1,5 @@
|
|||||||
# Copyright (C) 2019 Ian Alexander: https://github.com/ianalexander
|
# 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
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -13,8 +14,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -22,6 +25,7 @@ from pathlib import Path
|
|||||||
import pikepdf
|
import pikepdf
|
||||||
from watchdog.events import PatternMatchingEventHandler
|
from watchdog.events import PatternMatchingEventHandler
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.observers.polling import PollingObserver
|
||||||
|
|
||||||
import ocrmypdf
|
import ocrmypdf
|
||||||
|
|
||||||
@@ -32,8 +36,10 @@ OUTPUT_DIRECTORY = os.getenv('OCR_OUTPUT_DIRECTORY', '/output')
|
|||||||
OUTPUT_DIRECTORY_YEAR_MONTH = bool(os.getenv('OCR_OUTPUT_DIRECTORY_YEAR_MONTH', False))
|
OUTPUT_DIRECTORY_YEAR_MONTH = bool(os.getenv('OCR_OUTPUT_DIRECTORY_YEAR_MONTH', False))
|
||||||
ON_SUCCESS_DELETE = bool(os.getenv('OCR_ON_SUCCESS_DELETE', False))
|
ON_SUCCESS_DELETE = bool(os.getenv('OCR_ON_SUCCESS_DELETE', False))
|
||||||
DESKEW = bool(os.getenv('OCR_DESKEW', False))
|
DESKEW = bool(os.getenv('OCR_DESKEW', False))
|
||||||
|
OCR_JSON_SETTINGS = json.loads(os.getenv('OCR_JSON_SETTINGS', '{}'))
|
||||||
POLL_NEW_FILE_SECONDS = os.getenv('OCR_POLL_NEW_FILE_SECONDS', 1)
|
POLL_NEW_FILE_SECONDS = os.getenv('OCR_POLL_NEW_FILE_SECONDS', 1)
|
||||||
LOGLEVEL = os.environ.get('OCR_LOGLEVEL', 'INFO').upper()
|
USE_POLLING = bool(os.getenv('OCR_USE_POLLING', False))
|
||||||
|
LOGLEVEL = os.getenv('OCR_LOGLEVEL', 'INFO').upper()
|
||||||
PATTERNS = ['*.pdf']
|
PATTERNS = ['*.pdf']
|
||||||
|
|
||||||
log = logging.getLogger('ocrmypdf-watcher')
|
log = logging.getLogger('ocrmypdf-watcher')
|
||||||
@@ -86,7 +92,10 @@ def execute_ocrmypdf(file_path):
|
|||||||
return
|
return
|
||||||
log.info(f'Attempting to OCRmyPDF to: {output_path}')
|
log.info(f'Attempting to OCRmyPDF to: {output_path}')
|
||||||
exit_code = ocrmypdf.ocr(
|
exit_code = ocrmypdf.ocr(
|
||||||
input_file=file_path, output_file=output_path, deskew=DESKEW
|
input_file=file_path,
|
||||||
|
output_file=output_path,
|
||||||
|
deskew=DESKEW,
|
||||||
|
**OCR_JSON_SETTINGS,
|
||||||
)
|
)
|
||||||
if exit_code == 0 and ON_SUCCESS_DELETE:
|
if exit_code == 0 and ON_SUCCESS_DELETE:
|
||||||
log.info(f'OCR is done. Deleting: {file_path}')
|
log.info(f'OCR is done. Deleting: {file_path}')
|
||||||
@@ -105,6 +114,7 @@ def main():
|
|||||||
ocrmypdf.configure_logging(
|
ocrmypdf.configure_logging(
|
||||||
verbosity=ocrmypdf.Verbosity.default, manage_root_logger=True
|
verbosity=ocrmypdf.Verbosity.default, manage_root_logger=True
|
||||||
)
|
)
|
||||||
|
log.setLevel(LOGLEVEL)
|
||||||
log.info(
|
log.info(
|
||||||
f"Starting OCRmyPDF watcher with config:\n"
|
f"Starting OCRmyPDF watcher with config:\n"
|
||||||
f"Input Directory: {INPUT_DIRECTORY}\n"
|
f"Input Directory: {INPUT_DIRECTORY}\n"
|
||||||
@@ -117,12 +127,21 @@ def main():
|
|||||||
f"OUTPUT_DIRECTORY_YEAR_MONTH: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
|
f"OUTPUT_DIRECTORY_YEAR_MONTH: {OUTPUT_DIRECTORY_YEAR_MONTH}\n"
|
||||||
f"ON_SUCCESS_DELETE: {ON_SUCCESS_DELETE}\n"
|
f"ON_SUCCESS_DELETE: {ON_SUCCESS_DELETE}\n"
|
||||||
f"DESKEW: {DESKEW}\n"
|
f"DESKEW: {DESKEW}\n"
|
||||||
|
f"ARGS: {OCR_JSON_SETTINGS}\n"
|
||||||
f"POLL_NEW_FILE_SECONDS: {POLL_NEW_FILE_SECONDS}\n"
|
f"POLL_NEW_FILE_SECONDS: {POLL_NEW_FILE_SECONDS}\n"
|
||||||
|
f"USE_POLLING: {USE_POLLING}\n"
|
||||||
f"LOGLEVEL: {LOGLEVEL}\n"
|
f"LOGLEVEL: {LOGLEVEL}\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if 'input_file' in OCR_JSON_SETTINGS or 'output_file' in OCR_JSON_SETTINGS:
|
||||||
|
log.error('OCR_JSON_SETTINGS should not specify input file or output file')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
handler = HandleObserverEvent(patterns=PATTERNS)
|
handler = HandleObserverEvent(patterns=PATTERNS)
|
||||||
observer = Observer()
|
if USE_POLLING:
|
||||||
|
observer = PollingObserver()
|
||||||
|
else:
|
||||||
|
observer = Observer()
|
||||||
observer.schedule(handler, INPUT_DIRECTORY, recursive=True)
|
observer.schedule(handler, INPUT_DIRECTORY, recursive=True)
|
||||||
observer.start()
|
observer.start()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
twine >= 1.8.1
|
|
||||||
coverage >= 4.5
|
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
# setup.py lists a separate set of requirements that are looser to simplify
|
# setup.py lists a separate set of requirements that are looser to simplify
|
||||||
# installation
|
# installation
|
||||||
cffi == 1.14.0
|
cffi == 1.14.0
|
||||||
img2pdf == 0.3.3
|
img2pdf == 0.3.4
|
||||||
pdfminer.six == 20200124
|
pdfminer.six == 20200402
|
||||||
pikepdf == 1.10.1
|
pikepdf == 1.11.1
|
||||||
Pillow == 7.0.0
|
Pillow == 7.1.1
|
||||||
reportlab == 3.5.34
|
reportlab == 3.5.34
|
||||||
tqdm == 4.42.1
|
tqdm == 4.45.0
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ setup(
|
|||||||
'chardet >= 3.0.4, < 4', # unlisted requirement of pdfminer.six 20181108
|
'chardet >= 3.0.4, < 4', # unlisted requirement of pdfminer.six 20181108
|
||||||
'cffi >= 1.9.1', # must be a setup and install requirement
|
'cffi >= 1.9.1', # must be a setup and install requirement
|
||||||
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
|
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
|
||||||
'pdfminer.six >= 20181108, <= 20200124',
|
'pdfminer.six >= 20181108, <= 20200402',
|
||||||
'pikepdf >= 1.8.1, < 2',
|
'pikepdf >= 1.8.1, < 2',
|
||||||
'Pillow >= 6.2.0',
|
'Pillow >= 6.2.0',
|
||||||
'reportlab >= 3.3.0', # oldest released version with sane image handling
|
'reportlab >= 3.3.0', # oldest released version with sane image handling
|
||||||
|
|||||||
@@ -694,11 +694,18 @@ def convert_to_pdfa(input_pdf, input_ps_stub, context):
|
|||||||
# stamping them out as soon as possible.
|
# stamping them out as soon as possible.
|
||||||
modified = False
|
modified = False
|
||||||
with pikepdf.open(input_pdf) as pdf_file:
|
with pikepdf.open(input_pdf) as pdf_file:
|
||||||
if pdf_file.docinfo:
|
try:
|
||||||
for k, v in pdf_file.docinfo.items():
|
len(pdf_file.docinfo)
|
||||||
if b'\x00' in bytes(v):
|
except TypeError:
|
||||||
pdf_file.docinfo[k] = bytes(v).replace(b'\x00', b'')
|
context.log.error(
|
||||||
modified = True
|
"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:
|
if modified:
|
||||||
pdf_file.save(fix_docinfo_file)
|
pdf_file.save(fix_docinfo_file)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+12
-1
@@ -245,6 +245,10 @@ def exec_concurrent(context):
|
|||||||
if context.options.tesseract_env is None:
|
if context.options.tesseract_env is None:
|
||||||
context.options.tesseract_env = os.environ.copy()
|
context.options.tesseract_env = os.environ.copy()
|
||||||
context.options.tesseract_env.setdefault('OMP_THREAD_LIMIT', str(tess_threads))
|
context.options.tesseract_env.setdefault('OMP_THREAD_LIMIT', str(tess_threads))
|
||||||
|
try:
|
||||||
|
tess_threads = int(context.options.tesseract_env['OMP_THREAD_LIMIT'])
|
||||||
|
except ValueError: # OMP_THREAD_LIMIT initialized to non-numeric
|
||||||
|
context.log.error("Environment variable OMP_THREAD_LIMIT is not numeric")
|
||||||
if tess_threads > 1:
|
if tess_threads > 1:
|
||||||
context.log.info("Using Tesseract OpenMP thread limit %d", tess_threads)
|
context.log.info("Using Tesseract OpenMP thread limit %d", tess_threads)
|
||||||
|
|
||||||
@@ -357,10 +361,11 @@ def run_pipeline(options, api=False):
|
|||||||
options.jobs = available_cpu_count()
|
options.jobs = available_cpu_count()
|
||||||
|
|
||||||
work_folder = mkdtemp(prefix="com.github.ocrmypdf.")
|
work_folder = mkdtemp(prefix="com.github.ocrmypdf.")
|
||||||
|
debug_log_handler = None
|
||||||
if (options.keep_temporary_files or options.verbose >= 1) and not os.environ.get(
|
if (options.keep_temporary_files or options.verbose >= 1) and not os.environ.get(
|
||||||
'PYTEST_CURRENT_TEST', ''
|
'PYTEST_CURRENT_TEST', ''
|
||||||
):
|
):
|
||||||
configure_debug_logging(Path(work_folder) / "debug.log")
|
debug_log_handler = configure_debug_logging(Path(work_folder) / "debug.log")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
check_requested_output_file(options)
|
check_requested_output_file(options)
|
||||||
@@ -428,6 +433,12 @@ def run_pipeline(options, api=False):
|
|||||||
log.exception("An exception occurred while executing the pipeline")
|
log.exception("An exception occurred while executing the pipeline")
|
||||||
return ExitCode.other_error
|
return ExitCode.other_error
|
||||||
finally:
|
finally:
|
||||||
|
if debug_log_handler:
|
||||||
|
try:
|
||||||
|
debug_log_handler.close()
|
||||||
|
log.removeHandler(debug_log_handler)
|
||||||
|
except EnvironmentError as e:
|
||||||
|
print(e, file=sys.stderr)
|
||||||
cleanup_working_files(work_folder, options)
|
cleanup_working_files(work_folder, options)
|
||||||
|
|
||||||
return ExitCode.ok
|
return ExitCode.ok
|
||||||
|
|||||||
@@ -466,11 +466,12 @@ def check_dependency_versions(options):
|
|||||||
version_checker=ghostscript.version,
|
version_checker=ghostscript.version,
|
||||||
need_version='9.15', # limited by Travis CI / Ubuntu 14.04 backports
|
need_version='9.15', # limited by Travis CI / Ubuntu 14.04 backports
|
||||||
)
|
)
|
||||||
if ghostscript.version() == '9.24':
|
gs_version = ghostscript.version()
|
||||||
|
if gs_version in ('9.24', '9.51'):
|
||||||
raise MissingDependencyError(
|
raise MissingDependencyError(
|
||||||
"Ghostscript 9.24 contains serious regressions and is not "
|
f"Ghostscript {gs_version} contains serious regressions and is not "
|
||||||
"supported. Please upgrade to Ghostscript 9.25 or use an older "
|
"supported. Please upgrade to a newer version, or downgrade to the "
|
||||||
"version."
|
"previous version."
|
||||||
)
|
)
|
||||||
check_external_program(
|
check_external_program(
|
||||||
program='qpdf',
|
program='qpdf',
|
||||||
|
|||||||
+15
-5
@@ -21,7 +21,7 @@ import sys
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List
|
from typing import Dict, Iterable
|
||||||
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
@@ -67,7 +67,11 @@ class Verbosity(IntEnum):
|
|||||||
debug_all = 2 #: More detailed debugging from ocrmypdf and dependent modules
|
debug_all = 2 #: More detailed debugging from ocrmypdf and dependent modules
|
||||||
|
|
||||||
|
|
||||||
def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=False):
|
def configure_logging(
|
||||||
|
verbosity: Verbosity,
|
||||||
|
progress_bar_friendly: bool = True,
|
||||||
|
manage_root_logger: bool = False,
|
||||||
|
):
|
||||||
"""Set up logging.
|
"""Set up logging.
|
||||||
|
|
||||||
Library users may wish to use this function if they want their log output to be
|
Library users may wish to use this function if they want their log output to be
|
||||||
@@ -128,7 +132,7 @@ def configure_logging(verbosity, progress_bar_friendly=True, manage_root_logger=
|
|||||||
return log
|
return log
|
||||||
|
|
||||||
|
|
||||||
def create_options(*, input_file, output_file, **kwargs):
|
def create_options(*, input_file: os.PathLike, output_file: os.PathLike, **kwargs):
|
||||||
cmdline = []
|
cmdline = []
|
||||||
deferred = []
|
deferred = []
|
||||||
|
|
||||||
@@ -150,6 +154,12 @@ def create_options(*, input_file, output_file, **kwargs):
|
|||||||
cmdline.append(f"--{cmd_style_arg}")
|
cmdline.append(f"--{cmd_style_arg}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if isinstance(val, Iterable) and not isinstance(val, str):
|
||||||
|
for elem in val:
|
||||||
|
cmdline.append(f"--{cmd_style_arg}")
|
||||||
|
cmdline.append(elem)
|
||||||
|
continue
|
||||||
|
|
||||||
# We have a parameter
|
# We have a parameter
|
||||||
cmdline.append(f"--{cmd_style_arg}")
|
cmdline.append(f"--{cmd_style_arg}")
|
||||||
if isinstance(val, (int, float)):
|
if isinstance(val, (int, float)):
|
||||||
@@ -180,7 +190,7 @@ def ocr( # pylint: disable=unused-argument
|
|||||||
input_file: os.PathLike,
|
input_file: os.PathLike,
|
||||||
output_file: os.PathLike,
|
output_file: os.PathLike,
|
||||||
*,
|
*,
|
||||||
language: List[str] = None,
|
language: Iterable[str] = None,
|
||||||
image_dpi: int = None,
|
image_dpi: int = None,
|
||||||
output_type=None,
|
output_type=None,
|
||||||
sidecar: os.PathLike = None,
|
sidecar: os.PathLike = None,
|
||||||
@@ -210,7 +220,7 @@ def ocr( # pylint: disable=unused-argument
|
|||||||
jbig2_page_group_size: int = None,
|
jbig2_page_group_size: int = None,
|
||||||
pages: str = None,
|
pages: str = None,
|
||||||
max_image_mpixels: float = None,
|
max_image_mpixels: float = None,
|
||||||
tesseract_config: List[str] = None,
|
tesseract_config: Iterable[str] = None,
|
||||||
tesseract_pagesegmode: int = None,
|
tesseract_pagesegmode: int = None,
|
||||||
tesseract_oem: int = None,
|
tesseract_oem: int = None,
|
||||||
pdf_renderer=None,
|
pdf_renderer=None,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import argparse
|
|||||||
from ._version import PROGRAM_NAME as _PROGRAM_NAME
|
from ._version import PROGRAM_NAME as _PROGRAM_NAME
|
||||||
from ._version import __version__ as _VERSION
|
from ._version import __version__ as _VERSION
|
||||||
|
|
||||||
|
__all__ = ['parser']
|
||||||
|
|
||||||
|
|
||||||
def numeric(basetype, min_=None, max_=None):
|
def numeric(basetype, min_=None, max_=None):
|
||||||
"""Validator for numeric params"""
|
"""Validator for numeric params"""
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
from distutils.version import LooseVersion
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from subprocess import PIPE, STDOUT, CalledProcessError
|
from subprocess import PIPE, STDOUT, CalledProcessError
|
||||||
from subprocess import run as subprocess_run
|
from subprocess import run as subprocess_run
|
||||||
@@ -270,7 +271,15 @@ def check_external_program(
|
|||||||
raise MissingDependencyError()
|
raise MissingDependencyError()
|
||||||
return
|
return
|
||||||
|
|
||||||
if found_version < need_version:
|
def remove_leading_v(s):
|
||||||
|
if s.startswith('v'):
|
||||||
|
return s[1:]
|
||||||
|
return s
|
||||||
|
|
||||||
|
found_version = remove_leading_v(found_version)
|
||||||
|
need_version = remove_leading_v(need_version)
|
||||||
|
|
||||||
|
if LooseVersion(found_version) < LooseVersion(need_version):
|
||||||
_error_old_version(program, package, need_version, found_version, required_for)
|
_error_old_version(program, package, need_version, found_version, required_for)
|
||||||
if not recommended:
|
if not recommended:
|
||||||
raise MissingDependencyError()
|
raise MissingDependencyError()
|
||||||
|
|||||||
@@ -77,20 +77,14 @@ def has_textonly_pdf(tesseract_env=None, langs=None):
|
|||||||
args_tess = tess_base_args(langs, engine_mode=None) + ['--print-parameters', 'pdf']
|
args_tess = tess_base_args(langs, engine_mode=None) + ['--print-parameters', 'pdf']
|
||||||
params = ''
|
params = ''
|
||||||
try:
|
try:
|
||||||
proc = run(
|
# print-parameters can return non-UTF8 if the parameters are so initialized
|
||||||
args_tess,
|
proc = run(args_tess, check=True, stdout=PIPE, stderr=STDOUT, env=tesseract_env)
|
||||||
check=True,
|
|
||||||
universal_newlines=True,
|
|
||||||
stdout=PIPE,
|
|
||||||
stderr=STDOUT,
|
|
||||||
env=tesseract_env,
|
|
||||||
)
|
|
||||||
params = proc.stdout
|
params = proc.stdout
|
||||||
except CalledProcessError as e:
|
except CalledProcessError as e:
|
||||||
raise MissingDependencyError(
|
raise MissingDependencyError(
|
||||||
"Could not --print-parameters from tesseract"
|
"Could not --print-parameters from tesseract"
|
||||||
) from e
|
) from e
|
||||||
if 'textonly_pdf' in params:
|
if b'textonly_pdf' in params:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from pathlib import Path
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def safe_symlink(input_file, soft_link_name, *args, **kwargs):
|
def safe_symlink(input_file: os.PathLike, soft_link_name: os.PathLike, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Helper function: relinks soft symbolic link if necessary
|
Helper function: relinks soft symbolic link if necessary
|
||||||
"""
|
"""
|
||||||
@@ -76,12 +76,12 @@ def is_iterable_notstr(thing):
|
|||||||
return isinstance(thing, Iterable) and not isinstance(thing, str)
|
return isinstance(thing, Iterable) and not isinstance(thing, str)
|
||||||
|
|
||||||
|
|
||||||
def monotonic(L):
|
def monotonic(L: Iterable):
|
||||||
"""Does list increase monotonically?"""
|
"""Does list increase monotonically?"""
|
||||||
return all(b > a for a, b in zip(L, L[1:]))
|
return all(b > a for a, b in zip(L, L[1:]))
|
||||||
|
|
||||||
|
|
||||||
def page_number(input_file):
|
def page_number(input_file: os.PathLike):
|
||||||
"""Get one-based page number implied by filename (000002.pdf -> 2)"""
|
"""Get one-based page number implied by filename (000002.pdf -> 2)"""
|
||||||
return int(os.path.basename(os.fspath(input_file))[0:6])
|
return int(os.path.basename(os.fspath(input_file))[0:6])
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ def available_cpu_count():
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
def is_file_writable(test_file):
|
def is_file_writable(test_file: os.PathLike):
|
||||||
"""Intentionally racy test if target is writable.
|
"""Intentionally racy test if target is writable.
|
||||||
|
|
||||||
We intend to write to the output file if and only if we succeed and
|
We intend to write to the output file if and only if we succeed and
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -443,6 +443,12 @@ pixReadBarcodes(PIXA *pixa,
|
|||||||
SARRAY **psaw,
|
SARRAY **psaw,
|
||||||
l_int32 debugflag);
|
l_int32 debugflag);
|
||||||
|
|
||||||
|
PIX *
|
||||||
|
pixGenHalftoneMask(PIX *pixs,
|
||||||
|
PIX **ppixtext,
|
||||||
|
l_int32 *phtfound,
|
||||||
|
PIXA *pixadb);
|
||||||
|
|
||||||
l_int32
|
l_int32
|
||||||
l_generateCIDataForPdf(const char *fname,
|
l_generateCIDataForPdf(const char *fname,
|
||||||
PIX *pix,
|
PIX *pix,
|
||||||
|
|||||||
@@ -155,6 +155,17 @@ def extract_image_generic(*, pike, root, log, image, xref, options):
|
|||||||
# generating a PNG from compressed data
|
# generating a PNG from compressed data
|
||||||
pim.as_pil_image().save(png_name(root, xref))
|
pim.as_pil_image().save(png_name(root, xref))
|
||||||
return xref, '.png'
|
return xref, '.png'
|
||||||
|
elif (
|
||||||
|
not pim.indexed
|
||||||
|
and pim.colorspace == Name.ICCBased
|
||||||
|
and pim.bits_per_component == 1
|
||||||
|
and not options.jbig2_lossy
|
||||||
|
):
|
||||||
|
# We can losslessly optimize 1-bit images to CCITT or JBIG2 without
|
||||||
|
# paying any attention to the ICC profile, provided we're not doing
|
||||||
|
# lossy JBIG2
|
||||||
|
pim.as_pil_image().save(png_name(root, xref))
|
||||||
|
return xref, '.png'
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -410,9 +421,9 @@ def transcode_pngs(pike, images, image_name_fn, root, log, options):
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if compdata.type == leptonica.lept.L_FLATE_ENCODE:
|
if compdata.type == leptonica.lept.L_FLATE_ENCODE:
|
||||||
return rewrite_png(pike, im_obj, compdata, log)
|
rewrite_png(pike, im_obj, compdata, log)
|
||||||
elif compdata.type == leptonica.lept.L_G4_ENCODE:
|
elif compdata.type == leptonica.lept.L_G4_ENCODE:
|
||||||
return rewrite_png_as_g4(pike, im_obj, compdata, log)
|
rewrite_png_as_g4(pike, im_obj, compdata, log)
|
||||||
|
|
||||||
|
|
||||||
def rewrite_png_as_g4(pike, im_obj, compdata, log):
|
def rewrite_png_as_g4(pike, im_obj, compdata, log):
|
||||||
|
|||||||
@@ -18,11 +18,11 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from collections import namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from math import hypot, isclose
|
from math import hypot, isclose
|
||||||
from os import fspath
|
from os import PathLike, fspath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
@@ -30,17 +30,17 @@ import pikepdf
|
|||||||
from pikepdf import PdfMatrix
|
from pikepdf import PdfMatrix
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from ocrmypdf.exceptions import EncryptedPdfError, MissingDependencyError
|
from ocrmypdf.exceptions import EncryptedPdfError
|
||||||
|
from ocrmypdf.exec import ghostscript
|
||||||
from . import ghosttext
|
from ocrmypdf.pdfinfo import ghosttext
|
||||||
from .layout import get_page_analysis, get_text_boxes
|
from ocrmypdf.pdfinfo.layout import get_page_analysis, get_text_boxes
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
Colorspace = Enum('Colorspace', 'gray rgb cmyk lab icc index sep devn pattern jpeg2000')
|
Colorspace = Enum('Colorspace', 'gray rgb cmyk lab icc index sep devn pattern jpeg2000')
|
||||||
|
|
||||||
Encoding = Enum(
|
Encoding = Enum(
|
||||||
'Encoding', 'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate ' + 'runlength'
|
'Encoding', 'ccitt jpeg jpeg2000 jbig2 asciihex ascii85 lzw flate runlength'
|
||||||
)
|
)
|
||||||
|
|
||||||
FRIENDLY_COLORSPACE = {
|
FRIENDLY_COLORSPACE = {
|
||||||
@@ -98,7 +98,7 @@ XobjectSettings = namedtuple('XobjectSettings', ['name', 'shorthand', 'stack_dep
|
|||||||
InlineSettings = namedtuple('InlineSettings', ['iimage', 'shorthand', 'stack_depth'])
|
InlineSettings = namedtuple('InlineSettings', ['iimage', 'shorthand', 'stack_depth'])
|
||||||
|
|
||||||
ContentsInfo = namedtuple(
|
ContentsInfo = namedtuple(
|
||||||
'ContentsInfo', ['xobject_settings', 'inline_images', 'found_vector']
|
'ContentsInfo', ['xobject_settings', 'inline_images', 'found_vector', 'name_index']
|
||||||
)
|
)
|
||||||
|
|
||||||
TextboxInfo = namedtuple('TextboxInfo', ['bbox', 'is_visible', 'is_corrupt'])
|
TextboxInfo = namedtuple('TextboxInfo', ['bbox', 'is_visible', 'is_corrupt'])
|
||||||
@@ -151,6 +151,7 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
|
|||||||
ctm = PdfMatrix(initial_shorthand)
|
ctm = PdfMatrix(initial_shorthand)
|
||||||
xobject_settings = []
|
xobject_settings = []
|
||||||
inline_images = []
|
inline_images = []
|
||||||
|
name_index = defaultdict(lambda: [])
|
||||||
found_vector = False
|
found_vector = False
|
||||||
vector_ops = set('S s f F f* B B* b b*'.split())
|
vector_ops = set('S s f F f* B B* b b*'.split())
|
||||||
image_ops = set('BI ID EI q Q Do cm'.split())
|
image_ops = set('BI ID EI q Q Do cm'.split())
|
||||||
@@ -185,6 +186,7 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
|
|||||||
name=image_name, shorthand=ctm.shorthand, stack_depth=len(stack)
|
name=image_name, shorthand=ctm.shorthand, stack_depth=len(stack)
|
||||||
)
|
)
|
||||||
xobject_settings.append(settings)
|
xobject_settings.append(settings)
|
||||||
|
name_index[image_name].append(settings)
|
||||||
elif operator == 'INLINE IMAGE': # BI/ID/EI are grouped into this
|
elif operator == 'INLINE IMAGE': # BI/ID/EI are grouped into this
|
||||||
iimage = operands[0]
|
iimage = operands[0]
|
||||||
inline = InlineSettings(
|
inline = InlineSettings(
|
||||||
@@ -198,6 +200,7 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE):
|
|||||||
xobject_settings=xobject_settings,
|
xobject_settings=xobject_settings,
|
||||||
inline_images=inline_images,
|
inline_images=inline_images,
|
||||||
found_vector=found_vector,
|
found_vector=found_vector,
|
||||||
|
name_index=name_index,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -303,14 +306,22 @@ class ImageInfo:
|
|||||||
if self._enc == Encoding.jpeg2000:
|
if self._enc == Encoding.jpeg2000:
|
||||||
self._color = Colorspace.jpeg2000
|
self._color = Colorspace.jpeg2000
|
||||||
|
|
||||||
self._comp = FRIENDLY_COMP.get(self._color, '?')
|
if self._color == Colorspace.icc:
|
||||||
|
# Check the ICC profile to determine actual colorspace
|
||||||
|
pim_icc = pim.icc
|
||||||
|
if pim_icc.profile.xcolor_space == 'GRAY':
|
||||||
|
self._comp = 1
|
||||||
|
elif pim_icc.profile.xcolor_space == 'CMYK':
|
||||||
|
self._comp = 4
|
||||||
|
else:
|
||||||
|
self._comp = 3
|
||||||
|
else:
|
||||||
|
self._comp = FRIENDLY_COMP.get(self._color, '?')
|
||||||
|
|
||||||
# Bit of a hack... infer grayscale if component count is uncertain
|
# Bit of a hack... infer grayscale if component count is uncertain
|
||||||
# but encoding must be monochrome. This happens if a monochrome image
|
# but encoding only supports monochrome.
|
||||||
# has an ICC profile attached. Better solution would be to examine
|
if self._comp == '?' and self._enc in (Encoding.ccitt, Encoding.jbig2):
|
||||||
# the ICC profile.
|
self._comp = FRIENDLY_COMP[Colorspace.gray]
|
||||||
if self._comp == '?' and self._enc in (Encoding.ccitt, Encoding.jbig2):
|
|
||||||
self._comp = FRIENDLY_COMP[Colorspace.gray]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@@ -411,13 +422,9 @@ def _find_regular_images(container, contentsinfo):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
for pdfimage, xobj in _image_xobjects(container):
|
for pdfimage, xobj in _image_xobjects(container):
|
||||||
|
if xobj not in contentsinfo.name_index:
|
||||||
# For each image that is drawn on this, check if we drawing the
|
continue
|
||||||
# current image - yes this is O(n^2), but n == 1 almost always
|
for draw in contentsinfo.name_index[xobj]:
|
||||||
for draw in contentsinfo.xobject_settings:
|
|
||||||
if draw.name != xobj:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if draw.stack_depth == 0 and _is_unit_square(draw.shorthand):
|
if draw.stack_depth == 0 and _is_unit_square(draw.shorthand):
|
||||||
# At least one PDF in the wild (and test suite) draws an image
|
# At least one PDF in the wild (and test suite) draws an image
|
||||||
# when the graphics stack depth is 0, meaning that the image
|
# when the graphics stack depth is 0, meaning that the image
|
||||||
@@ -551,7 +558,7 @@ def simplify_textboxes(miner, textbox_getter):
|
|||||||
yield TextboxInfo(box.bbox, visible, corrupt)
|
yield TextboxInfo(box.bbox, visible, corrupt)
|
||||||
|
|
||||||
|
|
||||||
def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
|
def _pdf_get_pageinfo(pdf, pageno: int, infile: PathLike, xmltext: str):
|
||||||
pageinfo = {}
|
pageinfo = {}
|
||||||
pageinfo['pageno'] = pageno
|
pageinfo['pageno'] = pageno
|
||||||
pageinfo['images'] = []
|
pageinfo['images'] = []
|
||||||
@@ -611,25 +618,28 @@ def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
|
|||||||
|
|
||||||
def _pdf_get_all_pageinfo(infile, detailed_analysis=False, log=None, progbar=False):
|
def _pdf_get_all_pageinfo(infile, detailed_analysis=False, log=None, progbar=False):
|
||||||
pdf = pikepdf.open(infile) # Do not close in this function
|
pdf = pikepdf.open(infile) # Do not close in this function
|
||||||
if pdf.is_encrypted:
|
try:
|
||||||
pdf.close()
|
if pdf.is_encrypted:
|
||||||
raise EncryptedPdfError() # Triggered by encryption with empty passwd
|
raise EncryptedPdfError() # Triggered by encryption with empty passwd
|
||||||
if detailed_analysis:
|
if detailed_analysis:
|
||||||
pages_xml = None
|
pages_xml = None
|
||||||
else:
|
else:
|
||||||
pages_xml = ghosttext.extract_text_xml(infile, pdf, pageno=None, log=log)
|
pages_xml = ghosttext.extract_text_xml(infile, pdf, pageno=None, log=log)
|
||||||
|
|
||||||
pages = []
|
pages = []
|
||||||
for n, _ in tqdm(
|
for n, _ in tqdm(
|
||||||
enumerate(pdf.pages),
|
enumerate(pdf.pages),
|
||||||
total=len(pdf.pages),
|
total=len(pdf.pages),
|
||||||
desc="Scan",
|
desc="Scan",
|
||||||
unit='page',
|
unit='page',
|
||||||
disable=not progbar,
|
disable=not progbar,
|
||||||
):
|
):
|
||||||
page_xml = pages_xml[n] if pages_xml else None
|
page_xml = pages_xml[n] if pages_xml else None
|
||||||
page = PageInfo(pdf, n, infile, page_xml, detailed_analysis)
|
page = PageInfo(pdf, n, infile, page_xml, detailed_analysis)
|
||||||
pages.append(page)
|
pages.append(page)
|
||||||
|
except Exception:
|
||||||
|
pdf.close()
|
||||||
|
raise
|
||||||
|
|
||||||
return pages, pdf
|
return pages, pdf
|
||||||
|
|
||||||
@@ -750,6 +760,8 @@ class PdfInfo:
|
|||||||
|
|
||||||
def __init__(self, infile, detailed_page_analysis=False, log=logger, progbar=False):
|
def __init__(self, infile, detailed_page_analysis=False, log=logger, progbar=False):
|
||||||
self._infile = infile
|
self._infile = infile
|
||||||
|
if ghostscript.version() in ('9.52',):
|
||||||
|
detailed_page_analysis = True # txtwrite doesn't work in these versions
|
||||||
self._pages, pdf = _pdf_get_all_pageinfo(
|
self._pages, pdf = _pdf_get_all_pageinfo(
|
||||||
infile, detailed_page_analysis, log=log, progbar=progbar
|
infile, detailed_page_analysis, log=log, progbar=progbar
|
||||||
)
|
)
|
||||||
@@ -805,10 +817,14 @@ def main():
|
|||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('infile')
|
parser.add_argument('infile')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
info = _pdf_get_all_pageinfo(args.infile)
|
pagesinfo, pdfinfo = _pdf_get_all_pageinfo(args.infile)
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
|
||||||
pprint(info)
|
pprint(pdfinfo)
|
||||||
|
for page in pagesinfo:
|
||||||
|
pprint(page)
|
||||||
|
for im in page.images:
|
||||||
|
pprint(im)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
+40
-2
@@ -97,10 +97,48 @@ assert ast.parse(WINDOWS_SHIM_TEMPLATE.format(spoofer=repr(r"C:\\Temp\\file.py")
|
|||||||
def spoof(tmp_path_factory, **kwargs):
|
def spoof(tmp_path_factory, **kwargs):
|
||||||
"""Modify PATH to override subprocess executables
|
"""Modify PATH to override subprocess executables
|
||||||
|
|
||||||
spoof(program1='replacement', ...)
|
spoof(tmp_path_factory, program1='replacement', ...)
|
||||||
|
|
||||||
Creates temporary directory with symlinks to targets.
|
For the test suite we need a way override executables, so that we can
|
||||||
|
substitute desired results such as errors or just speed up OCR.
|
||||||
|
|
||||||
|
On POSIXish platforms we create a temporary folder with overrides that
|
||||||
|
are symlinks to the executables we want to run. We do not actually override
|
||||||
|
PATH. We also set an environment variable _OCRMYPDF_TEST_PATH, which
|
||||||
|
OCRmyPDF's subprocess wrapper will check before they use regular PATH. The
|
||||||
|
output is a folder full of executables we are overriding. We can override
|
||||||
|
multiple executables. The end result is a folder we can use in a PATH-style
|
||||||
|
lookup to override some executables:
|
||||||
|
|
||||||
|
/tmp/abcxyz/tesseract -> ocrmypdf/tests/resources/spoof/tesseract_crash.py
|
||||||
|
/tmp/abcxyz/gs -> ocrmypdf/tests/resources/spoof/gs_backflip.py
|
||||||
|
|
||||||
|
Windows needs extra help from us because usually, only the Administrator
|
||||||
|
can create symlinks. Instead we create small Python scripts that call
|
||||||
|
the programs we want, implementing the effect of a symlink. This is cleaner
|
||||||
|
than creating Windows executables or trying to use non-Python scripts.
|
||||||
|
The temporary folder generated for Windows could like:
|
||||||
|
|
||||||
|
%TEMP%\abcxyz\tesseract.py:
|
||||||
|
(script that runs ocrmypdf/tests/resources/spoof/tesseract_crash.py)
|
||||||
|
%TEMP%\abcxyz\gswin32c.py:
|
||||||
|
(script that runs ocrmypdf/tests/resources/spoof/gs_backflip.py)
|
||||||
|
%TEMP%\abcxyz\gswin64c.py:
|
||||||
|
(script that runs ocrmypdf/tests/resources/spoof/gs_backflip.py)
|
||||||
|
|
||||||
|
We also address one quirk here, that Ghostscript may be known as gswin32c
|
||||||
|
or gswin64c, depending on what the user installed (regardless of Windows
|
||||||
|
itself). On POSIX, Ghostscript is just 'gs'. We handle the special case here
|
||||||
|
too.
|
||||||
|
|
||||||
|
All of this is intimately dependent on the machinery in ocrmypdf.exec.run().
|
||||||
|
In particular, for Windows, that code has to know that if there is a .py
|
||||||
|
file, it needs to run it with Python, since Windows does not like being
|
||||||
|
asked to execute files.
|
||||||
|
|
||||||
|
We don't overload PATH directly because we have some tests where we call
|
||||||
|
ocrmypdf as a subprocess (to exercise the command line interface) and some
|
||||||
|
tests where we call it as an API.
|
||||||
"""
|
"""
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
slug = '-'.join(v.replace('.py', '') for v in sorted(kwargs.values()))
|
slug = '-'.join(v.replace('.py', '') for v in sorted(kwargs.values()))
|
||||||
|
|||||||
Binary file not shown.
@@ -1,80 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# © 2017 James R. Barlow: github.com/jbarlow83
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
||||||
# copy of this software and associated documentation files (the
|
|
||||||
# "Software"), to deal in the Software without restriction, including
|
|
||||||
# without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
# permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
# the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included
|
|
||||||
# in all copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
||||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
"""Tesseract bad utf8 spoof
|
|
||||||
|
|
||||||
In 'hocr' mode or 'pdf' mode, return error code 1 and some non-Unicode
|
|
||||||
text because tesseract seems to do that in some cases related to
|
|
||||||
language pack version mismatches
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
VERSION_STRING = '''tesseract 4.0.0
|
|
||||||
leptonica-1.77.0
|
|
||||||
libjpeg 9c : libpng 1.6.35 : libtiff 4.0.10 : zlib 1.2.11 : libopenjp2 2.3.0
|
|
||||||
Found AVX2
|
|
||||||
Found AVX
|
|
||||||
Found SSE
|
|
||||||
SPOOFED
|
|
||||||
'''
|
|
||||||
|
|
||||||
# Japanese "Invalid UTF-8" encoded in Shift JIS
|
|
||||||
BAD_UTF8 = b'\x96\xb3\x8c\xf8\x82\xc8UTF-8\x0a'
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if sys.argv[1] == '--version':
|
|
||||||
print(VERSION_STRING, file=sys.stderr)
|
|
||||||
sys.exit(0)
|
|
||||||
elif sys.argv[1] == '--list-langs':
|
|
||||||
print('List of available languages (1):\neng', file=sys.stderr)
|
|
||||||
sys.exit(0)
|
|
||||||
elif sys.argv[-2] == '--print-parameters':
|
|
||||||
print("Some parameters", file=sys.stderr)
|
|
||||||
print("textonly_pdf\t1\tSome help text")
|
|
||||||
sys.exit(0)
|
|
||||||
elif sys.argv[-2] in ('hocr', 'pdf'):
|
|
||||||
sys.stdout.buffer.write(BAD_UTF8)
|
|
||||||
sys.exit(1)
|
|
||||||
elif sys.argv[-1] == 'stdout':
|
|
||||||
# input file is at sys.argv[-2] but we don't look at it
|
|
||||||
print(
|
|
||||||
"""Orientation: 0
|
|
||||||
Orientation in degrees: 0
|
|
||||||
Orientation confidence: 100.00
|
|
||||||
Script: 1
|
|
||||||
Script confidence: 100.00""",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print("Spoof doesn't understand arguments", file=sys.stderr)
|
|
||||||
print(sys.argv, file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -59,3 +59,10 @@ def test_tqdm_console():
|
|||||||
|
|
||||||
log.info("done")
|
log.info("done")
|
||||||
assert not before_pbar("done")
|
assert not before_pbar("done")
|
||||||
|
|
||||||
|
|
||||||
|
def test_language_list():
|
||||||
|
with pytest.raises(
|
||||||
|
(ocrmypdf.exceptions.InputFileError, ocrmypdf.exceptions.MissingDependencyError)
|
||||||
|
):
|
||||||
|
ocrmypdf.ocr('doesnotexist.pdf', '_.pdf', language=['eng', 'deu'])
|
||||||
|
|||||||
+17
-12
@@ -25,25 +25,30 @@ import ocrmypdf
|
|||||||
|
|
||||||
|
|
||||||
def test_no_glyphless_graft(resources, outdir):
|
def test_no_glyphless_graft(resources, outdir):
|
||||||
pdf = pikepdf.open(resources / 'francais.pdf')
|
with pikepdf.open(resources / 'francais.pdf') as pdf, pikepdf.open(
|
||||||
pdf_aspect = pikepdf.open(resources / 'aspect.pdf')
|
resources / 'aspect.pdf'
|
||||||
pdf_cmyk = pikepdf.open(resources / 'cmyk.pdf')
|
) as pdf_aspect, pikepdf.open(resources / 'cmyk.pdf') as pdf_cmyk:
|
||||||
pdf.pages.extend(pdf_aspect.pages)
|
pdf.pages.extend(pdf_aspect.pages)
|
||||||
pdf.pages.extend(pdf_cmyk.pages)
|
pdf.pages.extend(pdf_cmyk.pages)
|
||||||
pdf.save(outdir / 'test.pdf')
|
pdf.save(outdir / 'test.pdf')
|
||||||
|
|
||||||
with patch('ocrmypdf._graft.MAX_REPLACE_PAGES', 2):
|
with patch('ocrmypdf._graft.MAX_REPLACE_PAGES', 2):
|
||||||
ocrmypdf.ocr(
|
ocrmypdf.ocr(
|
||||||
outdir / 'test.pdf', outdir / 'out.pdf', deskew=True, tesseract_timeout=0
|
outdir / 'test.pdf',
|
||||||
|
outdir / 'out.pdf',
|
||||||
|
deskew=True,
|
||||||
|
tesseract_timeout=0,
|
||||||
|
force_ocr=True,
|
||||||
)
|
)
|
||||||
|
# This test needs asserts
|
||||||
|
|
||||||
|
|
||||||
def test_links(resources, outpdf):
|
def test_links(resources, outpdf):
|
||||||
ocrmypdf.ocr(
|
ocrmypdf.ocr(
|
||||||
resources / 'link.pdf', outpdf, redo_ocr=True, oversample=200, output_type='pdf'
|
resources / 'link.pdf', outpdf, redo_ocr=True, oversample=200, output_type='pdf'
|
||||||
)
|
)
|
||||||
pdf = pikepdf.open(outpdf)
|
with pikepdf.open(outpdf) as pdf:
|
||||||
p1 = pdf.pages[0]
|
p1 = pdf.pages[0]
|
||||||
p2 = pdf.pages[1]
|
p2 = pdf.pages[1]
|
||||||
assert p1.Annots[0].A.D[0].objgen == p2.objgen
|
assert p1.Annots[0].A.D[0].objgen == p2.objgen
|
||||||
assert p2.Annots[0].A.D[0].objgen == p1.objgen
|
assert p2.Annots[0].A.D[0].objgen == p1.objgen
|
||||||
|
|||||||
+38
-8
@@ -31,8 +31,11 @@ import pytest
|
|||||||
from pikepdf.models.metadata import decode_pdf_date
|
from pikepdf.models.metadata import decode_pdf_date
|
||||||
|
|
||||||
from ocrmypdf._jobcontext import PDFContext
|
from ocrmypdf._jobcontext import PDFContext
|
||||||
|
from ocrmypdf._pipeline import convert_to_pdfa
|
||||||
|
from ocrmypdf.cli import parser
|
||||||
from ocrmypdf.exceptions import ExitCode
|
from ocrmypdf.exceptions import ExitCode
|
||||||
from ocrmypdf.pdfa import SRGB_ICC_PROFILE, file_claims_pdfa, generate_pdfa_ps
|
from ocrmypdf.pdfa import SRGB_ICC_PROFILE, file_claims_pdfa, generate_pdfa_ps
|
||||||
|
from ocrmypdf.pdfinfo import PdfInfo
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import fitz
|
import fitz
|
||||||
@@ -313,19 +316,21 @@ def test_metadata_fixup_warning(resources, outdir, caplog):
|
|||||||
|
|
||||||
|
|
||||||
def test_prevent_gs_invalid_xml(resources, outdir):
|
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')
|
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(
|
options = parser.parse_args(
|
||||||
args=['-j', '1', '--output-type', 'pdfa-2', 'a.pdf', 'b.pdf']
|
args=['-j', '1', '--output-type', 'pdfa-2', 'a.pdf', 'b.pdf']
|
||||||
)
|
)
|
||||||
pdfinfo = PdfInfo(resources / 'enron1.pdf')
|
pdfinfo = PdfInfo(outdir / 'layers.rendered.pdf')
|
||||||
context = PDFContext(options, outdir, resources / 'enron1.pdf', pdfinfo)
|
context = PDFContext(options, outdir, outdir / 'layers.rendered.pdf', pdfinfo)
|
||||||
|
|
||||||
convert_to_pdfa(
|
convert_to_pdfa(
|
||||||
str(outdir / 'layers.rendered.pdf'), str(outdir / 'pdfa.ps'), context
|
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_start = mm.find(XMP_MAGIC)
|
||||||
xmp_end = mm.rfind(b'<?xpacket end', xmp_start)
|
xmp_end = mm.rfind(b'<?xpacket end', xmp_start)
|
||||||
assert 0 < xmp_start < xmp_end
|
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'�', xmp_start, xmp_end) == -1, "found escaped nul"
|
||||||
assert mm.find(b'\x00', xmp_start, xmp_end) == -1
|
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
|
||||||
|
)
|
||||||
|
|||||||
+41
-1
@@ -18,10 +18,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from os import fspath
|
from os import fspath
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import img2pdf
|
||||||
import pikepdf
|
import pikepdf
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
from ocrmypdf import optimize as opt
|
from ocrmypdf import optimize as opt
|
||||||
from ocrmypdf.exec import jbig2enc, pngquant
|
from ocrmypdf.exec import jbig2enc, pngquant
|
||||||
@@ -130,3 +132,41 @@ def test_flate_to_jbig2(resources, outdir, spoof_tesseract_noop):
|
|||||||
pdf = pikepdf.open(outdir / 'out.pdf')
|
pdf = pikepdf.open(outdir / 'out.pdf')
|
||||||
pim = pikepdf.PdfImage(next(iter(pdf.pages[0].images.values())))
|
pim = pikepdf.PdfImage(next(iter(pdf.pages[0].images.values())))
|
||||||
assert pim.filters[0] == '/JBIG2Decode'
|
assert pim.filters[0] == '/JBIG2Decode'
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_pngs(resources, outdir, spoof_tesseract_noop):
|
||||||
|
with Path.open(outdir / 'in.pdf', 'wb') as inpdf:
|
||||||
|
img2pdf.convert(
|
||||||
|
fspath(resources / 'baiona_colormapped.png'),
|
||||||
|
fspath(resources / 'baiona_gray.png'),
|
||||||
|
with_pdfrw=False,
|
||||||
|
outputstream=inpdf,
|
||||||
|
)
|
||||||
|
|
||||||
|
def mockquant(input_file, output_file, _quality_min, _quality_max):
|
||||||
|
with Image.open(input_file) as im:
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
draw.rectangle((0, 0, im.width, im.height), fill=128)
|
||||||
|
im.save(output_file)
|
||||||
|
|
||||||
|
with patch('ocrmypdf.optimize.pngquant.quantize', new=mockquant):
|
||||||
|
check_ocrmypdf(
|
||||||
|
outdir / 'in.pdf',
|
||||||
|
outdir / 'out.pdf',
|
||||||
|
'--optimize',
|
||||||
|
'3',
|
||||||
|
'--jobs',
|
||||||
|
'1',
|
||||||
|
'--use-threads',
|
||||||
|
'--output-type',
|
||||||
|
'pdf',
|
||||||
|
env=spoof_tesseract_noop,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pikepdf.open(outdir / 'in.pdf') as inpdf, pikepdf.open(
|
||||||
|
outdir / 'out.pdf'
|
||||||
|
) as outpdf:
|
||||||
|
for n in range(len(inpdf.pages)):
|
||||||
|
inim = next(iter(inpdf.pages[n].images.values()))
|
||||||
|
outim = next(iter(outpdf.pages[n].images.values()))
|
||||||
|
assert len(outim.read_raw_bytes()) < len(inim.read_raw_bytes()), n
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from PIL import Image
|
|||||||
from reportlab.pdfgen.canvas import Canvas
|
from reportlab.pdfgen.canvas import Canvas
|
||||||
|
|
||||||
from ocrmypdf import pdfinfo
|
from ocrmypdf import pdfinfo
|
||||||
|
from ocrmypdf.exec import ghostscript
|
||||||
from ocrmypdf.pdfinfo import Colorspace, Encoding
|
from ocrmypdf.pdfinfo import Colorspace, Encoding
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
@@ -183,6 +184,9 @@ def test_ocr_detection(resources):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'testfile', ('truetype_font_nomapping.pdf', 'type3_font_nomapping.pdf')
|
'testfile', ('truetype_font_nomapping.pdf', 'type3_font_nomapping.pdf')
|
||||||
)
|
)
|
||||||
|
@pytest.mark.xfail(
|
||||||
|
ghostscript.version() in ('9.52',), reason="gs 9.52 txtwrite doesn't work"
|
||||||
|
)
|
||||||
def test_corrupt_font_detection(resources, testfile):
|
def test_corrupt_font_detection(resources, testfile):
|
||||||
filename = resources / testfile
|
filename = resources / testfile
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(NotImplementedError):
|
||||||
|
|||||||
@@ -33,11 +33,6 @@ run_ocrmypdf_api = pytest.helpers.run_ocrmypdf
|
|||||||
spoof = pytest.helpers.spoof
|
spoof = pytest.helpers.spoof
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def spoof_tess_bad_utf8(tmp_path_factory):
|
|
||||||
return spoof(tmp_path_factory, tesseract='tesseract_badutf8.py')
|
|
||||||
|
|
||||||
|
|
||||||
def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
def test_stdin(spoof_tesseract_noop, ocrmypdf_exec, resources, outpdf):
|
||||||
input_file = str(resources / 'francais.pdf')
|
input_file = str(resources / 'francais.pdf')
|
||||||
output_file = str(outpdf)
|
output_file = str(outpdf)
|
||||||
|
|||||||
@@ -176,3 +176,37 @@ def test_language_warning(caplog):
|
|||||||
vd.check_options_languages(opts)
|
vd.check_options_languages(opts)
|
||||||
assert opts.language == ['eng']
|
assert opts.language == ['eng']
|
||||||
assert 'assuming --language' in caplog.text
|
assert 'assuming --language' in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_comparison():
|
||||||
|
vd.check_external_program(
|
||||||
|
program="dummy_basic",
|
||||||
|
package="dummy",
|
||||||
|
version_checker=lambda: '9.0',
|
||||||
|
need_version='8.0.2',
|
||||||
|
)
|
||||||
|
vd.check_external_program(
|
||||||
|
program="dummy_doubledigit",
|
||||||
|
package="dummy",
|
||||||
|
version_checker=lambda: '10.0',
|
||||||
|
need_version='8.0.2',
|
||||||
|
)
|
||||||
|
vd.check_external_program(
|
||||||
|
program="tesseract",
|
||||||
|
package="tesseract",
|
||||||
|
version_checker=lambda: '4.0.0-beta.1',
|
||||||
|
need_version='4.0.0',
|
||||||
|
)
|
||||||
|
vd.check_external_program(
|
||||||
|
program="tesseract",
|
||||||
|
package="tesseract",
|
||||||
|
version_checker=lambda: 'v5.0.0-alpha.20200201',
|
||||||
|
need_version='4.0.0',
|
||||||
|
)
|
||||||
|
with pytest.raises(MissingDependencyError):
|
||||||
|
vd.check_external_program(
|
||||||
|
program="dummy_fails",
|
||||||
|
package="dummy",
|
||||||
|
version_checker=lambda: '1.0',
|
||||||
|
need_version='2.0',
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user