Compare commits

..
66 Commits
Author SHA1 Message Date
James R. Barlow 1b18dbecf5 Fix filename test.txt 2018-10-11 16:03:25 -07:00
James R. Barlow 9f82c0eb6e v7.2.1 release notes 2018-10-11 15:55:01 -07:00
James R. Barlow 68bac1b177 Fix compatibility with pikepdf 0.3.5 API change 2018-10-11 15:51:34 -07:00
James R. Barlow 1495b78330 Remove cruft to support leptonica < 1.72 in test suite 2018-10-11 01:37:32 -07:00
James R. Barlow 6f777d2848 Include Debian copyright file 2018-10-10 23:55:48 -07:00
James R. Barlow 5650eba848 Cleanup MANIFEST.in, reorg requirements/*.txt, fix non-Unicode readme 2018-10-10 23:53:08 -07:00
James R. Barlow 5bc5dc93f3 v7.2.0 release notes update 2018-10-05 01:27:00 -07:00
James R. Barlow c1e18bb825 optimize: Exclude soft masks (SMasks) from optimization
Soft masks are only allowed to be of colorspace DeviceGray so we
shouldn't use pngquant on them. For now, avoid this exceptional
case by excluded soft masks from optimization.
2018-10-05 01:23:26 -07:00
James R. Barlow 58282ea0fb optimize: more refactoring
Now properly generalized/specialized where it should be
2018-10-04 13:44:51 -07:00
James R. Barlow 891da7834c optimize: refactor image extraction 2018-10-04 12:34:22 -07:00
James R. Barlow 5c229d48d5 optimize: Reorganize so JBIG2 can be performed on images reduced to 1bpp
Closes #297
2018-10-04 11:53:11 -07:00
James R. Barlow 53f660cf35 Travis: use newer macos image 2018-10-04 08:59:40 -07:00
James R. Barlow 7b66ca68f2 ...and document lossy JBIG2 2018-10-04 01:31:53 -07:00
James R. Barlow ba71c3ffbd requirements: request pikepdf 0.3.4 2018-10-04 01:22:03 -07:00
James R. Barlow 6707ad427a v7.2.0 release notes 2018-10-04 01:21:17 -07:00
James R. Barlow 5b84549716 Change JBIG2 lossy mode to require --jbig2-lossy 2018-10-04 01:20:49 -07:00
James R. Barlow c74f2ee6e8 Refactor the detailed error messages 2018-10-04 00:10:59 -07:00
James R. Barlow b32dd9f9d3 Fix lossless JBIG2 when there are multiple JBIG2 images on a single page 2018-10-03 17:40:26 -07:00
James R. Barlow fb8b161f6c Fix suppression of tesseract config error messages 2018-10-03 17:39:50 -07:00
James R. Barlow baddd6d233 Remove libtiff from Brewfile
For some reason, brew complains about it now.
2018-10-03 16:17:59 -07:00
James R. Barlow 6f554c6ae8 tesseract: account for behavior changes when params are missing
Tesseract 4.0-rc1 now accepts invalid parameters in config and
won't return an error anymore. We prefer to raise an error if this
occurs.

See: https://github.com/tesseract-ocr/tesseract/commit/741ea00d7059d8ff7c55797ffc525a461d7f3ced
2018-10-03 15:11:34 -07:00
James R. Barlow a71e4488b3 test: fix pytest warning about direct use of a fixture 2018-10-03 15:04:46 -07:00
James R. Barlow 72156b5653 Degrade more gracefully when --optimize is set but JBIG2 is not present 2018-10-03 14:24:20 -07:00
James R. Barlow 9fa471e053 Test: send stderr to stderr, why don't we? 2018-10-03 14:23:34 -07:00
James R. Barlow 31ef2fe907 test: this error message changed case in newer Tesseract 2018-10-03 13:58:20 -07:00
James R. Barlow 9a8ec4b210 optimize: only enable lossy JBIG2 for -O3 2018-10-03 00:38:58 -07:00
James R. Barlow 75aad4cc79 optimize: Refactor convert_to_jbig2 2018-10-02 23:42:12 -07:00
James R. Barlow 4b27feca98 optimize: Disable JBIG2 lossy mode, use lossless instead 2018-10-01 12:28:54 -07:00
James R. Barlow 45522cd15f weave: clarify comment about garbage data in ToC 2018-09-27 13:48:35 -07:00
James R. Barlow 677d9a4e76 Remove some unhelpful lambdas 2018-09-27 13:48:12 -07:00
James R. Barlow efa7ea4fde Fix log.error where log is None 2018-09-19 23:01:27 -07:00
James R. Barlow 137a6e45f5 ghostscript: fix missing fspath for py3.5 2018-09-19 22:57:20 -07:00
James R. Barlow 29116e1dec Change to README.md 2018-09-19 21:01:24 -07:00
James R. Barlow 87193335b9 v7.1.0 notes 2018-09-19 20:57:18 -07:00
James R. Barlow cfd4f8a850 Improve error handling for improvements to Ghostscript text extraction 2018-09-19 20:29:18 -07:00
James R. Barlow eaa324939f Upgrade to pikepdf 0.3.3
Closes #231
2018-09-19 15:30:54 -07:00
James R. Barlow ef70e538f7 Improve error message on handling KeyboardInterrupt
Closes #301
2018-09-19 01:40:26 -07:00
James R. Barlow b7b912e56a Fix test suite and blank pages 2018-09-17 01:12:58 -07:00
James R. Barlow 4615cf2f1e First cut at improving text extraction speed 2018-09-16 23:34:18 -07:00
James R. Barlow eaf772f80a Merge v6.2.4 release notes 2018-09-16 15:45:38 -07:00
James R. Barlow 96ba75eabd Ghostscript: fix issues in strict ASCII implementation 2018-09-16 15:41:54 -07:00
James R. Barlow fdfe52c1ad main: add debug option to force threads 2018-09-15 00:01:45 -07:00
James R. Barlow 932b2e2a29 main: print Ghostscript version too 2018-09-14 23:58:06 -07:00
James R. Barlow 57e489c957 main: Cleanup; support overriding sys.args in run_pipeline 2018-09-14 23:57:35 -07:00
James R. Barlow 17a3fa671c ghostscript: API docs update 2018-09-14 23:51:52 -07:00
James R. Barlow 2659afb4f6 Cleanup gitignore 2018-09-14 21:02:22 -07:00
James R. Barlow 7392115507 Blacklist Ghostscript 9.24 due to regressions
As per issue #291. Forced push to remove a copyrighted test file that was
accidentally included.
2018-09-14 20:41:13 -07:00
James R. Barlow c54d0c7eaa v7.0.5 release notes 2018-09-13 23:29:54 -07:00
James R. Barlow b95eefc65f Fix pikepdf version for Travis 2018-09-13 22:08:19 -07:00
James R. Barlow 686207ab7f Check for and reject Adobe LiveCycle Designer PDFs
These are the ones that display a "Please wait..." message.

Closes #296
2018-09-13 21:50:51 -07:00
James R. Barlow 517b385fe5 Work around loss of Unicode DOCINFO in Ghostscript 9.24+
Ghostscript no longer supports UTF-16-BE-hex strings as a way of
supplying Unicode data in pdfmark so we have lost this functionality too:
http://git.ghostscript.com/?p=ghostpdl.git;a=commit;h=e997c6836d243ab37fe3a5f0d57974af95eb5eac

For users this means setting --title, --author, etc. will not work if gs
9.24 is installed, but if the file has existing metadata it might work.

For now we enforce police-state-strict ASCII, until there's time to
implement proper metadata editing. Relevant tests set to xfail.
2018-09-13 21:33:39 -07:00
James R. Barlow 795019b0c1 Work around invalid TOC entries
Kodak Capture Desktop and probably other software creates a
/Outlines entry with /First being set to an invalid indirect reference to
an object that hasn't been created. This is legal in the PDF spec but
problematic for qpdf. The objgen will be (max valid object ID + 1, 0).
Because we create new objects in _weave, some TOC entries will end
up assigned to new objects we create. Typically /ProcSet.

We solve the issue by refactoring page traversal and then doing it
twice, once to resolve all references (eliminating the null
reference problem) and a second pass to make our changes.
2018-09-11 14:44:16 -07:00
James R. Barlow 3127a73822 Ghostscript: no need to specify ProcessColorModel when ColorConversionStrategy 2018-09-11 11:56:05 -07:00
James R. Barlow 069ee6c91f ghostscript: fix for 9.24 having jpeg passthrough available 2018-09-10 23:09:51 -07:00
James R. Barlow 3aac3a98ca tests: Migrate metadata tests to pikepdf
For some reason PyPDF2 has begun to trigger internal errors in
pytest on macOS alone. Not sure why, but nothing is wrong that I can
see. Seemed like an opportune time to switch to pikepdf; found some
new issues in the process anyway.
2018-09-10 16:06:01 -07:00
James R. Barlow 268859a304 Merge branch 'master' of github.com:jbarlow83/OCRmyPDF: docs 2018-09-10 11:52:04 -07:00
James R. Barlow a96710aa7b leptonica: update comments 2018-09-10 11:47:38 -07:00
James R. Barlow edcc58826a pdfinfo: remove some dead code 2018-09-10 11:47:00 -07:00
James R. Barlow 7077c8220a Fix rst formatting in release notes 2018-09-10 11:46:17 -07:00
Mateus Seenem Tavaresandjbarlow83 f7cbf68edd Updating Arch Linux instalation (#288)
* Updating Arch Linux instalation

And adding a workaround to a wrong dependencies definition on https://aur.archlinux.org/packages/python-pikepdf/

* Remove comment about temporary workaround
2018-08-31 12:30:18 -07:00
James R. Barlow 68a58ee8a5 docs: fix hyperlinking of jbig2 page (again) and cleanup release notes 2018-08-27 01:25:30 -07:00
James R. Barlow 3109ec5091 v7.0.4 notes 2018-08-24 12:41:53 -07:00
James R. Barlow e0599fe8d7 Require pikepdf 0.3.2 2018-08-24 12:41:43 -07:00
James R. Barlow a749240589 docs: mention pikepdf install more clearly 2018-08-22 03:19:46 -07:00
James R. Barlow 6decdaa062 Try setuptools_scm_git_archive again 2018-08-20 15:45:51 -07:00
James R. Barlow 4d5c9b8cdf Fix error in optimize.py on PNGs at -O2
Error was
TypeError: unsupported operand type(s) for -: 'tuple' and 'int'
2018-08-20 15:45:34 -07:00
39 changed files with 2522 additions and 570 deletions
+1 -1
View File
@@ -51,7 +51,7 @@ RUN . /appenv/bin/activate; \
# Do this now to make the best use of Docker cache.
COPY . /application
RUN . /appenv/bin/activate; \
pip install -r /application/test_requirements.txt
pip install -r /application/requirements/test.txt
# Remove the junk, including the source version of application since it was
# already installed
+14 -21
View File
@@ -1,47 +1,40 @@
# Development environment
.bash_history
.pylintrc
.pytest_cache/
.ruffus_history.sqlite
.venv/
*.pyc
*.sublime-*
venv*/
.venv/
pyvenv.cfg
tasks.py
.bash_history
.ruffus_history.sqlite
.idea/
.pytest_cache/
.pylintrc
# Package building
*.egg-info/
.cache/
.eggs/
*.egg-info/
build/
dist/
wheelhouse/
# Automatically generated files
ocrmypdf/lib/_*.py
ocrmypdf/version.py
docs/_build/
docs/_static/
docs/_templates/
docs/Makefile
ocrmypdf/lib/_*.py
# Code coverage
.coverage
htmlcov/
# Testing
log/
.ipynb_checkpoints/
.vscode/
*.ipynb
*.profile
/*.pdf
/*.qdf
*.ipynb
.ipynb_checkpoints/
/scratch.py
IDEAS
log/
tests/output/
tests/resources/private/
tmp/
pdfbox-app*.jar
.vscode/
IDEAS
_Dockerfile.local
/scratch.py
+4 -4
View File
@@ -66,7 +66,7 @@ matrix:
- tesseract-ocr-fra
- unpaper
- os: osx
osx_image: xcode8
osx_image: xcode9.2
language: generic
before_cache:
@@ -83,7 +83,7 @@ before_install: |
sudo dpkg -i packages/unpaper_6.1-1.deb
fi
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
brew update && brew bundle --file=.travis/Brewfile
brew update --quiet && brew bundle --file=.travis/Brewfile --quiet
pip3 install --upgrade pip
pip3 install wheel
fi
@@ -91,9 +91,9 @@ before_install: |
install:
- export PATH=$PWD/bin:$PATH
- pip3 install pycparser # py3.7 workaround for https://github.com/eliben/pycparser/issues/251
- pip3 install -r requirements.txt
- pip3 install -r requirements/main.txt
- pip3 install --no-deps .
- pip3 install -r test_requirements.txt
- pip3 install -r requirements/test.txt
script:
- tesseract --version
-1
View File
@@ -4,7 +4,6 @@ brew 'exempi'
brew 'ghostscript'
brew 'jbig2enc'
brew 'leptonica'
brew 'libtiff'
brew 'openjpeg'
brew 'pngquant'
brew 'qpdf'
+6 -12
View File
@@ -1,7 +1,5 @@
# requirements
include requirements.txt
include test_requirements.txt
include dev_requirements.txt
recursive-include requirements *
# git
include .git_archival.txt
@@ -12,21 +10,20 @@ recursive-include .docker *
# tests
include .coveragerc
recursive-include tests *.bin
recursive-include tests *.jpg
recursive-include tests *.jsonl
recursive-include tests *.png
recursive-include tests *.pdf
recursive-include tests *.py
recursive-include tests *.rst
recursive-include tests *.txt
recursive-include tests/cache *
recursive-exclude tests/output *
recursive-exclude tests/output_pageinfo *
recursive-exclude tests/resources/private *
# documentation
include LICENSE
include *.rst
recursive-exclude .github *
recursive-exclude .github *
recursive-include docs *.py
recursive-include docs *.rst
recursive-include docs *.svg
@@ -42,8 +39,5 @@ exclude .travis*
# code
recursive-include src/ocrmypdf *.py
exclude ocrmypdf/lib/_leptonica.py
exclude src/ocrmypdf/lib/_leptonica.py
exclude scratch.py
+132
View File
@@ -0,0 +1,132 @@
OCRmyPDF
========
![image](https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master%0A%20:target:%20https://travis-ci.org/jbarlow83/OCRmyPDF)
![image](https://img.shields.io/pypi/v/ocrmypdf.svg%0A%20:target:%20https://pypi.org/project/ocrmypdf/)
![image](https://img.shields.io/homebrew/v/ocrmypdf.svg%0A%20:alt:%20homebrew%0A%20:target:%20http://brewformulas.org/Ocrmypdf)
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched or copy-pasted.
```bash
ocrmypdf # it's a scriptable command line program
-l eng+fra # it supports multiple languages
--rotate-pages # it can fix pages that are misrotated
--deskew # it can deskew crooked PDFs!
--title "My PDF" # it can change output metadata
--jobs 4 # it uses multiple cores by default
--output-type pdfa # it produces PDF/A by default
input_scanned.pdf # takes PDF input (or images)
output_searchable.pdf # produces validated PDF output
```
Main features
-------------
- Generates a searchable [PDF/A](https://en.wikipedia.org/?title=PDF/A) file from a regular PDF
- Places OCR text accurately below the image to ease copy / paste
- Keeps the exact resolution of the original embedded images
- When possible, inserts OCR information as a "lossless" operation without disrupting any other content
- Optimizes PDF images, often producing files smaller than the input file
- If requested deskews and/or cleans the image before performing OCR
- Validates input and output files
- Distributes work across all available CPU cores
- Uses [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) engine
- Supports more than [100 languages](https://github.com/tesseract-ocr/tessdata) recognized by Tesseract
- Battle-tested on thousands of PDFs, a test suite and continuous integration
For details: please consult the [documentation](https://ocrmypdf.readthedocs.io/en/latest/).
Motivation
----------
I searched the web for a free command line tool to OCR PDF files on Linux/UNIX: I found many, but none of them were really satisfying.
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
- Or they did not handle accents and multilingual characters
- Or they changed the resolution of the embedded images
- Or they generated ridiculously large PDF files
- Or they crashed when trying to OCR
- Or they did not produce valid PDF files
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
...so I decided to develop my own tool.
Installation
------------
Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows.
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
```bash
apt-get install ocrmypdf
```
and macOS users with Homebrew may simply
```bash
brew install ocrmypdf
```
For everyone else, [see our documentation](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for installation steps.
Languages
---------
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users, you can often find packages that provide language packs:
```bash
# Display a list of all Tesseract language packs
apt-cache search tesseract-ocr
# Debian/Ubuntu users
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
```
You can then pass the `-l LANG` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple languages can be requested.
Documentation and support
-------------------------
Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via:
```bash
ocrmypdf --help
```
Our [documentation is served on Read the Docs](https://ocrmypdf.readthedocs.io/en/latest/index.html).
If you detect an issue, please:
- Check whether your issue is already known
- If no problem report exists on github, please create one here: <https://github.com/jbarlow83/OCRmyPDF/issues>
- Describe your problem thoroughly
- Append the console output of the script when running the debug mode (`-v 1` option)
- If possible provide your input PDF file as well as the content of the temporary folder (using a file sharing service like Dropbox)
Requirements
------------
Runs on CPython 3.5, 3.6 and 3.7. Requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings.
Press & Media
-------------
- [c't 1-2014, page 59](http://heise.de/-2279695): Detailed presentation of OCRmyPDF v1.0 in the leading German IT magazine c't
- [heise Open Source, 09/2014: Texterkennung mit OCRmyPDF](http://heise.de/-2356670)
License
-------
The OCRmyPDF software is licensed under the GNU GPLv3. Certain files are covered by other licenses, as noted in their source files.
The license for each test file varies, and is noted in tests/resources/README.rst. The documentation is licensed under Creative Commons Attribution-ShareAlike 4.0 (CC-BY-SA 4.0).
OCRmyPDF versions prior to 6.0 were licensed under the MIT License.
Disclaimer
----------
The software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-150
View File
@@ -1,150 +0,0 @@
OCRmyPDF
========
.. image:: https://travis-ci.org/jbarlow83/OCRmyPDF.svg?branch=master
:target: https://travis-ci.org/jbarlow83/OCRmyPDF
.. image:: https://img.shields.io/pypi/v/ocrmypdf.svg
:target: https://pypi.org/project/ocrmypdf/
.. image:: https://img.shields.io/homebrew/v/ocrmypdf.svg
:alt: homebrew
:target: http://brewformulas.org/Ocrmypdf
OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to
be searched or copy-pasted.
.. code-block:: bash
ocrmypdf # it's a scriptable command line program
-l eng+fra # it supports multiple languages
--rotate-pages # it can fix pages that are misrotated
--deskew # it can deskew crooked PDFs!
--title "My PDF" # it can change output metadata
--jobs 4 # it uses multiple cores by default
--output-type pdfa # it produces PDF/A by default
input_scanned.pdf # takes PDF input (or images)
output_searchable.pdf # produces validated PDF output
Main features
-------------
- Generates a searchable
`PDF/A <https://en.wikipedia.org/?title=PDF/A>`_ file from a regular PDF
- Places OCR text accurately below the image to ease copy / paste
- Keeps the exact resolution of the original embedded images
- When possible, inserts OCR information as a "lossless" operation without disrupting any other content
- Optimizes PDF images, often producing files smaller than the input file
- If requested deskews and/or cleans the image before performing OCR
- Validates input and output files
- Distributes work across all available CPU cores
- Uses `Tesseract OCR <https://github.com/tesseract-ocr/tesseract>`_ engine
- Supports more than `100 languages <https://github.com/tesseract-ocr/tessdata>`_ recognized by Tesseract
- Battle-tested on thousands of PDFs, a test suite and continuous integration
For details: please consult the `documentation <https://ocrmypdf.readthedocs.io/en/latest/>`_.
Motivation
----------
I searched the web for a free command line tool to OCR PDF files on
Linux/UNIX: I found many, but none of them were really satisfying.
- Either they produced PDF files with misplaced text under the image (making copy/paste impossible)
- Or they did not handle accents and multilingual characters
- Or they changed the resolution of the embedded images
- Or they generated ridiculously large PDF files
- Or they crashed when trying to OCR
- Or they did not produce valid PDF files
- On top of that none of them produced PDF/A files (format dedicated for long time storage)
...so I decided to develop my own tool.
Installation
------------
Linux, UNIX, and macOS are supported. Windows is not directly supported but there is a Docker image available that runs on Windows.
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
.. code-block:: bash
apt-get install ocrmypdf
and macOS users with Homebrew may simply
.. code-block:: bash
brew install ocrmypdf
For everyone else, `see our documentation <https://ocrmypdf.readthedocs.io/en/latest/installation.html>`_ for installation steps.
Languages
---------
OCRmyPDF uses Tesseract for OCR, and relies on its language packs. For Linux users,
you can often find packages that provide language packs:
.. code-block:: bash
# Display a list of all Tesseract language packs
apt-cache search tesseract-ocr
# Debian/Ubuntu users
apt-get install tesseract-ocr-chi-sim # Example: Install Chinese Simplified language back
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as to what languages it should search for. Multiple
languages can be requested.
Documentation and support
-------------------------
Once ocrmypdf is installed, the built-in help which explains the command syntax and options can be accessed via:
.. code-block:: bash
ocrmypdf --help
Our `documentation is served on Read the Docs <https://ocrmypdf.readthedocs.io/en/latest/index.html>`_.
If you detect an issue, please:
- Check whether your issue is already known
- If no problem report exists on github, please create one here:
https://github.com/jbarlow83/OCRmyPDF/issues
- Describe your problem thoroughly
- Append the console output of the script when running the debug mode
(``-v 1`` option)
- If possible provide your input PDF file as well as the content of the
temporary folder (using a file sharing service like Dropbox)
Requirements
------------
Runs on CPython 3.5, 3.6 and 3.7. Requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings.
Press & Media
-------------
- `c't 1-2014, page 59 <http://heise.de/-2279695>`_:
Detailed presentation of OCRmyPDF v1.0 in the leading German IT
magazine c't
- `heise Open Source, 09/2014: Texterkennung mit
OCRmyPDF <http://heise.de/-2356670>`_
License
-------
The OCRmyPDF software is licensed under the GNU GPLv3. Certain files are covered by other licenses, as noted in their source files.
The license for each test file varies, and is noted in tests/resources/README.rst. The documentation is licensed under Creative Commons Attribution-ShareAlike 4.0 (CC-BY-SA 4.0).
OCRmyPDF versions prior to 6.0 were licensed under the MIT License.
Disclaimer
----------
The software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
+1488
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -213,4 +213,6 @@ Currently optimization attempts to find more efficient encodings for images. The
.. code-block:: bash
ocrmypdf --optimize 3 in.pdf out.pdf # Make it as small as possible
ocrmypdf --optimize 3 in.pdf out.pdf # Make it small
Some users may consider enabling lossy JBIG2. See: :ref:`jbig2-lossy`.
+17 -13
View File
@@ -55,13 +55,13 @@ Users of Debian 9 ("stretch") or later or Ubuntu 16.10 or later may simply
apt-get install ocrmypdf
For full details on version availability, check the `Debian Package Tracker <https://tracker.debian.org/pkg/ocrmypdf>`_ or `Ubuntu launchpad.net <https://launchpad.net/ocrmypdf>`_.
As indicated in the table above, Debian and Ubuntu releases may lag behind the latest version. If the version available for your platform is out of date, you could opt to install the latest version from source. See `Installing HEAD revision from sources`_.
If the version available for your platform is out of date, you could opt to install the latest version from source. See `Installing HEAD revision from sources`_.
For full details on version availability for your platform, check the `Debian Package Tracker <https://tracker.debian.org/pkg/ocrmypdf>`_ or `Ubuntu launchpad.net <https://launchpad.net/ocrmypdf>`_.
.. note::
OCRmyPDF for Debian and Ubuntu currently omit the JBIG2 encoder. OCRmyPDF works fine without it but will produce larger output files. If you build jbig2enc from source, ocrmypdf 7.0.0 and later will automatically detect it on the ``PATH``. To add JBIG2 encoding, see `Installing the JBIG2 encoder`_.
OCRmyPDF for Debian and Ubuntu currently omit the JBIG2 encoder. OCRmyPDF works fine without it but will produce larger output files. If you build jbig2enc from source, ocrmypdf 7.0.0 and later will automatically detect it (specifically the ``jbig2`` binary) on the ``PATH``. To add JBIG2 encoding, see :ref:`jbig2`.
Installing the latest version on Ubuntu 18.04 LTS
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -92,7 +92,7 @@ Then install the most recent ocrmypdf for the local user and set the user's ``PA
export PATH=$HOME/.local/bin:$PATH
pip3 install --user ocrmypdf
To add JBIG2 encoding, see `Installing the JBIG2 encoder`_.
To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 16.04 LTS
^^^^^^^^^^^^^^^^
@@ -135,7 +135,7 @@ At your option, you may upgrade Ubuntu 16.04 LTS to Tesseract 4.0 for improved O
sudo apt-get update
sudo apt-get upgrade tesseract-ocr
To add JBIG2 encoding, see `Installing the JBIG2 encoder`_.
To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 14.04 LTS
^^^^^^^^^^^^^^^^
@@ -198,7 +198,7 @@ These installation instructions omit the optional dependency ``unpaper``, which
wget -q 'https://www.dropbox.com/s/vaq0kbwi6e6au80/unpaper_6.1-1.deb?raw=1' -O unpaper_6.1-1.deb
sudo dpkg -i unpaper_6.1-1.deb
To add JBIG2 encoding, see `Installing the JBIG2 encoder`_.
To add JBIG2 encoding, see :ref:`jbig2`.
ArchLinux
^^^^^^^^^
@@ -207,11 +207,13 @@ ArchLinux
:alt: ArchLinux
:target: https://repology.org/metapackage/ocrmypdf
The author is aware of an `ArchLinux package for ocrmypdf <https://aur.archlinux.org/packages/ocrmypdf/>`_. It seems like the following command might work.
The author is aware of an `ArchLinux User Repository package for ocrmypdf <https://aur.archlinux.org/packages/ocrmypdf/>`_. You can use the following command.
.. code-block:: bash
pacman -S ocrmypdf
yaourt -S ocrmypdf
If you have any difficulties with installation, check the repository package page.
Other Linux packages
^^^^^^^^^^^^^^^^^^^^
@@ -447,12 +449,12 @@ Since ``pip3 install --user`` does not work correctly on some platforms, notably
Requirements for pip and HEAD install
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OCRmyPDF currently requires these external programs and libraries to be installed:
OCRmyPDF currently requires these external programs and libraries to be installed, and must be satisfied using the operating system package manager. ``pip`` cannot provide them.
- Python 3.5 or newer
- Ghostscript 9.15 or newer
- libexempi3 2.2.0 or newer
- qpdf 7.0.0 or newer
- qpdf 8.1.0 or newer
- Tesseract 3.04 or newer
As of ocrmypdf 7.0.0, the following versions are recommended:
@@ -468,7 +470,7 @@ As of ocrmypdf 7.0.0, the following versions are recommended:
jbig2enc, pngquant, and unpaper are optional. If missing certain features are disabled. OCRmyPDF will discover them as soon as they are available.
**jbig2enc**, if present, will be used to optimize the encoding of monochrome images. This can significantly reduce the file size of the output file. It is not required. `jbig2enc <https://github.com/agl/jbig2enc>`_ is not generally available for Ubuntu or Debian due to lingering concerns about patent issues, but can easily be built from source. To add JBIG2 encoding, see `Optional: installing the JBIG2 encoder`_.
**jbig2enc**, if present, will be used to optimize the encoding of monochrome images. This can significantly reduce the file size of the output file. It is not required. `jbig2enc <https://github.com/agl/jbig2enc>`_ is not generally available for Ubuntu or Debian due to lingering concerns about patent issues, but can easily be built from source. To add JBIG2 encoding, see :ref:`jbig2`.
**pngquant**, if present, is optionally used to optimize the encoding of PNG-style images in PDFs (actually, any that are that losslessly encoded) by lossily quantizing to a smaller color palette. It is only activated then the ``--optimize`` argument is ``2`` or ``3``.
@@ -484,6 +486,8 @@ Installing HEAD revision from sources
If you have ``git`` and Python 3.5 or newer installed, you can install from source. When the ``pip`` installer runs, it will alert you if dependencies are missing.
If you prefer to build every from source, you will need to `build pikepdf from source <https://pikepdf.readthedocs.io/en/latest/installation.html#building-from-source>`_. First ensure you can build and install pikepdf.
To install the HEAD revision from sources in the current Python 3 environment:
.. code-block:: bash
@@ -532,6 +536,6 @@ To install all of the development and test requirements:
source venv/bin/activate
cd OCRmyPDF
pip install -e .
pip install -r dev_requirements.txt -r test_requirements.txt
pip install -r requirements/dev.txt -r requirements/test.txt
To add JBIG2 encoding, see `Installing the JBIG2 encoder`_.
To add JBIG2 encoding, see :ref:`jbig2`.
+16 -1
View File
@@ -1,5 +1,7 @@
.. _jbig2:
Installing the JBIG2 encoder
==========================
============================
Most Linux distributions do not include a JBIG2 encoder since JBIG2 encoding was patented for a long time. All known JBIG2 US patents have expired as of 2017, but it is possible that unknown patents exist.
@@ -18,3 +20,16 @@ For all other Linux, you must build a JBIG2 encoder from source:
./autogen.sh
./configure && make
[sudo] make install
.. _jbig2-lossy:
Lossy mode JBIG2
----------------
OCRmyPDF provides lossy mode JBIG2 as an advanced feature. Users should `review the technical concerns with JBIG2 in lossy mode <https://abbyy.technology/en:kb:tip:jbig2_compression_and_ocr>`_ and decide if this feature is acceptable for their use case.
JBIG2 lossy mode does achieve higher compression ratios than any other monochrome (bitonal) compression technology; for large text documents the savings are considerable. JBIG2 lossless still gives great compression ratios and is a major improvement over the older CCITT G4 standard. As explained above, there is some risk of substitution errors.
To turn on JBIG2 lossy mode, add the argument ``--jbig2-lossy``. ``--optimize {1,2,3}`` are necessary for the argument to take effect also required. Also, a JBIG2 encoder must be installed as described in the previous section.
*ocrmypdf v7.0 and v7.1 used lossy mode by default.*
+84 -47
View File
@@ -13,6 +13,77 @@ Note that it is licensed under GPLv3, so scripts that ``import ocrmypdf`` and ar
find: [^`]\#([0-9]{1,3})[^0-9]
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
v7.2.1
------
- Fix compatibility with an API change in pikepdf 0.3.5.
- A kludge to support Leptonica versions older than 1.72 in the test suite was dropped. Older versions of Leptonica are likely still compatible. The only impact is that a portion of the test suite will be skipped.
v7.2.0
------
**Lossy JBIG2 behavior change**
A user reported that ocrmypdf was in fact using JBIG2 in **lossy** compression mode. This was not the intended behavior. Users should `review the technical concerns with JBIG2 in lossy mode <https://abbyy.technology/en:kb:tip:jbig2_compression_and_ocr>`_ and decide if this is a concern for their use case.
JBIG2 lossy mode does achieve higher compression ratios than any other monochrome compression technology; for large text documents the savings are considerable. JBIG2 lossless still gives great compression ratios and is a major improvement over the older CCITT G4 standard.
Only users who have reviewed the concerns with JBIG2 in lossy mode should opt-in. As such, lossy mode JBIG2 is only turned on when the new argument ``--jbig2-lossy`` is issued. This is independent of the setting for ``--optimize``.
Users who did not install an optional JBIG2 encoder are unaffected.
(Thanks to user 'bsdice' for reporting this issue.)
**Other issues**
- When the image optimizer quantizes an image to 1 bit per pixel, it will now attempt to further optimize that image as CCITT or JBIG2, instead of keeping it in the "flate" encoding which is not efficient for 1 bpp images. (`#297 <https://github.com/jbarlow83/OCRmyPDF/issues/297>`_)
- Images in PDFs that are used as soft masks (i.e. transparency masks or alpha channels) are now excluded from optimization.
- Fixed handling of Tesseract 4.0-rc1 which now accepts invalid Tesseract configuration files, which broke the test suite.
v7.1.0
------
- Improve the performance of initial text extraction, which is done to determine if a file contains existing text of some kind or not. On large files, this initial processing is now about 20x times faster. (`#299 <https://github.com/jbarlow83/OCRmyPDF/issues/299>`_)
- pikepdf 0.3.3 is now required.
- Fixed issue `#231 <https://github.com/jbarlow83/OCRmyPDF/issues/231>`_, a problem with JPEG2000 images where image metadata was only available inside the JPEG2000 file.
- Fixed some additional Ghostscript 9.25 compatibility issues.
- Improved handling of KeyboardInterrupt error messages. (`#301 <https://github.com/jbarlow83/OCRmyPDF/issues/301>`_)
- README.md is now served in GitHub markdown instead of reStructuredText.
v7.0.6
------
- Blacklist Ghostscript 9.24, now that 9.25 is available and fixes many regressions in 9.24.
v7.0.5
------
- Improve capability with Ghostscript 9.24, and enable the JPEG passthrough feature when this version in installed.
- Ghostscript 9.24 lost the ability to set PDF title, author, subject and keyword metadata to Unicode strings. OCRmyPDF will set ASCII strings and warn when Unicode is suppressed. Other software may be used to update metadata. This is a short term work around.
- PDFs generated by Kodak Capture Desktop, or generally PDFs that contain indirect references to null objects in their table of contents, would have an invalid table of contents after processing by OCRmyPDF that might interfere with other viewers. This has been fixed.
- Detect PDFs generated by Adobe LiveCycle, which can only be displayed in Adobe Acrobat and Reader currently. When these are encountered, exit with an error instead of performing OCR on the "Please wait" error message page.
v7.0.4
------
- Fix exception thrown when trying to optimize a certain type of PNG embedded in a PDF with the ``-O2``
- Update to pikepdf 0.3.2, to gain support for optimizing some additional image types that were previously excluded from optimization (CMYK and grayscale). Fixes `#285 <https://github.com/jbarlow83/OCRmyPDF/issues/285>`_.
v7.0.3
------
@@ -81,10 +152,18 @@ v7.0.0
+ It may be necessary to separately ``pip install pycparser`` to avoid `another Python 3.7 issue <https://github.com/eliben/pycparser/pull/135>`_.
v6.2.4
------
- Backport Ghostscript 9.25 compatibility fixes, which removes support for setting Unicode metadata
- Backport blacklisting Ghostscript 9.24
- Older versions of Ghostscript are still supported
v6.2.3
------
- Fix compatibility with img2pdf >= 0.3.0 by rejecting input images that have an alpha channel
- This version will be included in Ubuntu 18.10
v6.2.2
------
@@ -347,7 +426,6 @@ v5.0.1
- Fixed issue `#169 <https://github.com/jbarlow83/OCRmyPDF/issues/169>`_, exception due to failure to create sidecar text files on some versions of Tesseract 3.04, including the jbarlow83/ocrmypdf Docker image
v5.0
----
@@ -363,7 +441,6 @@ v5.0
- Fixed issue `#164 <https://github.com/jbarlow83/OCRmyPDF/issues/164>`_, a typo
- Removed the command line parameters ``-n`` and ``--just-print`` since they have not worked for some time (reported as Ubuntu bug `#1687308 <https://bugs.launchpad.net/ubuntu/+source/ocrmypdf/+bug/1687308>`_)
v4.5.6
------
@@ -371,21 +448,18 @@ v4.5.6
- Fixed issue `#158 <https://github.com/jbarlow83/OCRmyPDF/issues/158>`_, ocrmypdf now stops and terminates if Ghostscript fails on an intermediate step, as it is not possible to proceed.
- Fixed issue `#160 <https://github.com/jbarlow83/OCRmyPDF/issues/160>`_, exception thrown on certain invalid arguments instead of error message
v4.5.5
------
- Automated update of macOS homebrew tap
- Fixed issue `#154 <https://github.com/jbarlow83/OCRmyPDF/issues/154>`_, KeyError '/Contents' when searching for text on blank pages that have no /Contents record. Note: incomplete fix for this issue.
v4.5.4
------
- Fix ``--skip-big`` raising an exception if a page contains no images (`#152 <https://github.com/jbarlow83/OCRmyPDF/issues/152>`_) (thanks to @TomRaz)
- Fix an issue where pages with no images might trigger "cannot write mode P as JPEG" (`#151 <https://github.com/jbarlow83/OCRmyPDF/issues/151>`_)
v4.5.3
------
@@ -394,7 +468,6 @@ v4.5.3
- Fixed an issue where the ``tess4`` renderer would duplicate content onto output pages if tesseract failed or timed out
- Fixed ``tess4`` renderer not recognized when lossless reconstruction is possible
v4.5.2
------
@@ -402,21 +475,18 @@ v4.5.2
- Make "using Tesseract 4.0" warning less ominous
- Set up machinery for homebrew OCRmyPDF tap
v4.5.1
------
- Fix issue `#137 <https://github.com/jbarlow83/OCRmyPDF/issues/137>`_, proportions of images with a non-square pixel aspect ratio would be distorted in output for ``--force-ocr`` and some other combinations of flags
v4.5
----
- Exotic PDFs containing "Form XObjects" are now supported (issue `#134 <https://github.com/jbarlow83/OCRmyPDF/issues/134>`_; PDF reference manual 8.10), and images they contain are taken into account when determining the resolution for rasterizing
- PDFs containing "Form XObjects" are now supported (issue `#134 <https://github.com/jbarlow83/OCRmyPDF/issues/134>`_; PDF reference manual 8.10), and images they contain are taken into account when determining the resolution for rasterizing
- The Tesseract 4 Docker image no longer includes all languages, because it took so long to build something would tend to fail
- OCRmyPDF now warns about using ``--pdf-renderer tesseract`` with Tesseract 3.04 or lower due to issues with Ghostscript corrupting the OCR text in these cases
v4.4.2
------
@@ -427,7 +497,6 @@ v4.4.2
- OCRmyPDF now prevents running the Tesseract 4 renderer with Tesseract 3.04, which was permitted in v4.4 and v4.4.1 but will not work
v4.4.1
------
@@ -454,32 +523,27 @@ v4.4
+ However, OCRmyPDF's dependency "ruffus" is not re-entrant, so no Python API is available. Scripts should continue to use the command line interface.
v4.3.5
------
- Update documentation to confirm Python 3.6.0 compatibility. No code changes were needed, so many earlier versions are likely supported.
v4.3.4
------
- Fixed "decimal.InvalidOperation: quantize result has too many digits" for high DPI images
v4.3.3
------
- Fixed PDF/A creation with Ghostscript 9.20 properly
- Fixed an exception on inline stencil masks with a missing optional parameter
v4.3.2
------
- Fixed a PDF/A creation issue with Ghostscript 9.20 (note: this fix did not actually work)
v4.3.1
------
@@ -490,7 +554,6 @@ v4.3.1
- Cleaned up debug output
- Tesseract caching in test cases is now more cautious about false cache hits and reproducing exact output, not that any problems were observed
v4.3
----
@@ -508,7 +571,6 @@ v4.2.5
- Fixed an issue (`#100 <https://github.com/jbarlow83/OCRmyPDF/issues/100>`_) with PDFs that omit the optional /BitsPerComponent parameter on images
- Removed non-free file milk.pdf
v4.2.4
------
@@ -528,20 +590,17 @@ v4.2.3
- Added a test case for indirect objects and linearized PDFs
- Deprecated the OCRmyPDF.sh shell script
v4.2.2
------
- Improvements to documentation
v4.2.1
------
- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort
- Implemented stdin streaming
v4.2
----
@@ -580,7 +639,6 @@ v4.1.4
- Bug fix: monochrome images with an ICC profile attached were incorrectly converted to full color images if lossless reconstruction was not possible due to other settings; consequence was increased file size for these images
v4.1.3
------
@@ -589,14 +647,12 @@ v4.1.3
- Fix order of operations for matrix multiplication (no effect on most users)
- Add a few leptonica wrapper functions (no effect on most users)
v4.1.2
------
- Replace IEC sRGB ICC profile with Debian's sRGB (from icc-profiles-free) which is more compatible with the MIT license
- More helpful error message for an error related to certain types of malformed PDFs
v4.1
----
@@ -604,20 +660,17 @@ v4.1
- Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time
- Fixed problems with "RethrownJobError" errors during error handling that suppressed the useful error messages
v4.0.7
------
- Minor correction to Ghostscript output settings
v4.0.6
------
- Update install instructions
- Provide a sRGB profile instead of using Ghostscript's
v4.0.5
------
@@ -631,18 +684,14 @@ v4.0.4
Released with verbose debug message turned on. Do not use. Skip to v4.0.5.
v4.0.3
------
New features
^^^^^^^^^^^^
- Page orientations detected are now reported in a summary comment
Fixes
^^^^^
- Show stack trace if unexpected errors occur
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
@@ -654,7 +703,7 @@ v4.0.2
------
Fixes
^^^^^
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
orientation information
@@ -666,7 +715,7 @@ v4.0.1
------
Fixes
^^^^^
- Fixed a KeyError if tesseract fails to find page orientation information
@@ -675,7 +724,6 @@ v4.0
----
New features
^^^^^^^^^^^^
- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information
on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is
@@ -685,7 +733,6 @@ New features
Fixes
^^^^^
- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly
if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag)
@@ -694,7 +741,6 @@ Fixes
Changes
^^^^^^^
- Logging output is now much easier to read
- ``--deskew`` is now performed by Leptonica instead of unpaper (`#25 <https://github.com/jbarlow83/OCRmyPDF/issues/25>`_)
@@ -710,7 +756,6 @@ v3.2.1
------
Changes
^^^^^^^
- Fixed issue `#47 <https://github.com/jbarlow83/OCRmyPDF/issues/47>`_ "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2
- Tweaked the Dockerfiles
@@ -720,7 +765,6 @@ v3.2
----
New features
^^^^^^^^^^^^
- Lossless reconstruction: when possible, OCRmyPDF will inject text layers without
otherwise manipulating the content and layout of a PDF page. For example, a PDF containing a mix
@@ -732,7 +776,6 @@ New features
for the polyglots among us. It is much larger.
Changes
^^^^^^^
- JPEG transcoding quality is now 95 instead of the default 75. Bigger file sizes for less degradation.
@@ -742,7 +785,6 @@ v3.1.1
------
Changes
^^^^^^^
- Fixed bug that caused incorrect page size and DPI calculations on documents with mixed page sizes
@@ -750,7 +792,6 @@ v3.1
----
Changes
^^^^^^^
- Default output format is now PDF/A-2b instead of PDF/A-1b
- Python 3.5 and macOS El Capitan are now supported platforms - no changes were
@@ -768,7 +809,6 @@ v3.0
----
New features
^^^^^^^^^^^^
- Easier installation with a Docker container or Python's ``pip`` package manager
- Eliminated many external dependencies, so it's easier to setup
@@ -791,7 +831,6 @@ New features
- Multiple images on the same PDF page are now supported
Changes
^^^^^^^
- New, robust rewrite in Python 3.4+ with ruffus_ pipelines
- Now uses Ghostscript 9.14's improved color conversion model to preserve PDF colors
@@ -828,8 +867,7 @@ Changes
.. _Unpaper: https://github.com/Flameeyes/unpaper
.. _JHOVE: http://jhove.sourceforge.net/
Release candidates
^^^^^^^^^^^^^^^^^^
Release candidates^
- rc9:
@@ -897,12 +935,11 @@ where ``settings.txt`` contains *one argument per line*, for example:
Fixes
^^^^^
- Handling of filenames containing spaces: fixed
Notes and known issues
^^^^^^^^^^^^^^^^^^^^^^
- Some dependencies may work with lower versions than tested, so try
overriding dependencies if they are "in the way" to see if they work.
+1 -1
View File
@@ -3,7 +3,7 @@
# installation
cffi == 1.11.5
img2pdf == 0.3.0
pikepdf == 0.3.0
pikepdf == 0.3.4
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
pycparser == 2.18
python-xmp-toolkit == 2.0.1
+12 -4
View File
@@ -201,17 +201,18 @@ if 'upload' in sys.argv[1:]:
print('Use twine to upload the package - setup.py upload is insecure')
sys.exit(1)
tests_require = open('test_requirements.txt').read().splitlines()
tests_require = open('requirements/test.txt', encoding='utf-8').read().splitlines()
def readme():
with open('README.rst') as f:
with open('README.md', encoding='utf-8') as f:
return f.read()
setup(
name='ocrmypdf',
description='OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched',
long_description=readme(),
long_description_content_type='text/markdown',
url='https://github.com/jbarlow83/OCRmyPDF',
author='James R. Barlow',
author_email='jim@purplerock.ca',
@@ -241,6 +242,7 @@ setup(
'cffi >= 1.9.1', # to build the leptonica module
'pytest-runner', # to enable python setup.py test
'setuptools_scm', # so that version will work
'setuptools_scm_git_archive' # enable version from github tarballs
],
use_scm_version={'version_scheme': 'post-release'},
cffi_modules=[
@@ -249,7 +251,7 @@ setup(
install_requires=[
'cffi >= 1.9.1', # must be a setup and install requirement
'img2pdf >= 0.2.4, < 0.4', # pure Python, so track HEAD closely
'pikepdf >= 0.3.1, < 0.4',
'pikepdf >= 0.3.3, < 0.4',
'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"',
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
# block 5.1.0, broken wheels
@@ -268,4 +270,10 @@ setup(
},
package_data={'ocrmypdf': ['data/sRGB.icc']},
include_package_data=True,
zip_safe=False)
zip_safe=False,
project_urls={
'Documentation': 'https://ocrmypdf.readthedocs.io/',
'Source': 'https://github.com/jbarlow83/ocrmypdf',
'Tracker': 'https://github.com/jbarlow83/ocrmypdf/issues'
}
)
+91 -47
View File
@@ -17,7 +17,6 @@
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from tempfile import mkdtemp
from collections.abc import Sequence
from pathlib import Path
import sys
import os
@@ -36,7 +35,7 @@ import ruffus.proxy_logger as proxy_logger
from ._jobcontext import JobContext, JobContextManager, cleanup_working_files
from ._pipeline import build_pipeline
from .pdfa import file_claims_pdfa
from .helpers import is_iterable_notstr, re_symlink, is_file_writable, \
from .helpers import re_symlink, is_file_writable, \
available_cpu_count
from .exec import tesseract, qpdf, ghostscript
from . import PROGRAM_NAME, VERSION
@@ -270,17 +269,17 @@ optimizing = parser.add_argument_group(
optimizing.add_argument(
'-O', '--optimize', type=int, choices=range(0, 4), default=1,
help=("Control how PDF is optimized after processing:"
"0 - do not optimize;"
"1 - do safe, lossless optimizations (default);"
"2 - do lossy optimizations; "
"3 - do aggressive lossy optimizations"
"0 - do not optimize; "
"1 - do safe, lossless optimizations (default); "
"2 - do some lossy optimizations; "
"3 - do aggressive lossy optimizations (including lossy JBIG2)"
)
)
optimizing.add_argument(
'--jpeg-quality', type=numeric(int, 0, 100), default=0, metavar='Q',
help=("Adjust JPEG quality level for JPEG optimization. "
"100 is best quality and largest output size; "
"1 is lowest quality and smallest output"
"1 is lowest quality and smallest output; "
"0 uses the default."
)
)
@@ -295,6 +294,18 @@ optimizing.add_argument(
"Values have same meaning as with --jpeg-quality"
)
)
optimizing.add_argument(
'--jbig2-lossy', action='store_true',
help=("Enable JBIG2 lossy mode (better compression, not suitable for some "
"use cases - see documentation)."
)
)
optimizing.add_argument(
'--jbig2-page-group-size', type=numeric(int, 1, 10000), default=0,
metavar='N',
# Adjust number of pages to consider at once for JBIG2 compression
help=argparse.SUPPRESS
)
advanced = parser.add_argument_group(
"Advanced",
@@ -467,21 +478,34 @@ def check_options_sidecar(options, log):
options.sidecar = options.output_file + '.txt'
def _optional_program_check(name, version_fn, min_version, for_argument):
def _optional_program_required(name, version_fn, min_version, for_argument):
try:
if version_fn() < min_version:
raise MissingDependencyError(
"The installed '{}' is not supported. "
"Install version {} or newer.".format(name, min_version))
except FileNotFoundError:
except (FileNotFoundError, MissingDependencyError):
raise MissingDependencyError(
"Install the '{}' program to use {}.".format(name, for_argument))
def _optional_program_recommended(name, version_fn, min_version, for_argument):
try:
if version_fn() < min_version:
raise MissingDependencyError(
"The installed '{}' is not supported. "
"Install version {} or newer.".format(name, min_version))
except (FileNotFoundError, MissingDependencyError):
complain(
"For best results, install the optional program '{}' to use the "
"argument {}.".format(name, for_argument)
)
def check_options_preprocessing(options, log):
if any((options.clean, options.clean_final)):
from .exec import unpaper
_optional_program_check(
_optional_program_required(
'unpaper', unpaper.version, '6.1', '--clean, --clean-final'
)
@@ -496,13 +520,29 @@ def check_options_ocr_behavior(options, log):
def check_options_optimizing(options, log):
if options.optimize >= 2:
from .exec import pngquant, jbig2enc
_optional_program_check(
_optional_program_required(
'pngquant', pngquant.version, '2.0.1', '--optimize {2,3}'
)
_optional_program_check(
if options.jbig2_lossy:
_optional_program_required(
'jbig2', jbig2enc.version, '0.28', '--jbig2-lossy'
)
elif options.optimize >= 2:
# Although we use JBIG2 for optimize=1, don't nag about it unless the
# user is asking for more optimization
_optional_program_recommended(
'jbig2', jbig2enc.version, '0.28', '--optimize {2,3}'
)
if options.optimize == 0 and any([
options.jbig2_lossy, options.png_quality, options.jpeg_quality
]):
log.warning(
"The arguments --jbig2-lossy, --png-quality, and --jpeg-quality "
"will be ignored because --optimize=0."
)
def check_options_advanced(options, log):
if options.tesseract_oem and not tesseract.v4():
@@ -598,52 +638,45 @@ def do_ruffus_exception(ruffus_five_tuple, options, log):
description of the error message that occurred."""
exit_code = None
task_name, job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
task_name = task_name # unused
job_name = job_name # unused
if exc_name == 'builtins.SystemExit':
_task_name, _job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
if isinstance(exc_name, type):
# ruffus is full of mystery... sometimes (probably when the process
# group leader is killed) exc_name is the class object of the exception,
# rather than a str. So reach into the object and get its name.
exc_name = exc_name.__name__
if exc_name.startswith('ocrmypdf.exceptions.'):
base_exc_name = exc_name.replace('ocrmypdf.exceptions.', '')
exc_class = getattr(ocrmypdf_exceptions, base_exc_name)
exit_code = getattr(exc_class, 'exit_code', ExitCode.other_error)
try:
if isinstance(exc_value, exc_class):
exc_msg = str(exc_value)
else:
exc_msg = str(exc_class())
except Exception:
exc_msg = "Unknown"
if exc_name in ('builtins.SystemExit', 'SystemExit'):
match = re.search(r"\.(.+?)\)", exc_value)
exit_code_name = match.groups()[0]
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
log.error(cleanup_ruffus_error_message(exc_value))
exit_code = ExitCode.input_file
elif exc_name == 'builtins.KeyboardInterrupt':
log.error("Interrupted by user")
elif exc_name in ('builtins.KeyboardInterrupt', 'KeyboardInterrupt'):
# We have to print in this case because the log daemon might be toast
print("Interrupted by user", file=sys.stderr)
exit_code = ExitCode.ctrl_c
elif exc_name == 'subprocess.CalledProcessError':
# It's up to the subprocess handler to report something useful
msg = "Error occurred while running this command:"
log.error(msg + '\n' + exc_value)
exit_code = ExitCode.child_process_error
elif exc_name == 'ocrmypdf.exceptions.EncryptedPdfError':
log.error(textwrap.dedent("""\
Input PDF is encrypted. The encryption must be removed to
perform OCR.
For information about this PDF's security use
qpdf --show-encryption infilename
You can remove the encryption using
qpdf --decrypt [--password=[password]] infilename
"""))
exit_code = ExitCode.encrypted_pdf
elif exc_name == 'ocrmypdf.exceptions.PdfMergeFailedError':
log.error(textwrap.dedent("""\
Failed to merge PDF image layer with OCR layer
Usually this happens because the input PDF file is mal-formed and
ocrmypdf cannot automatically correct the problem on its own.
Try using
ocrmypdf --pdf-renderer sandwich [..other args..]
"""))
exit_code = ExitCode.input_file
elif exc_name.startswith('ocrmypdf.exceptions.'):
base_exc_name = exc_name.replace('ocrmypdf.exceptions.', '')
exc_class = getattr(ocrmypdf_exceptions, base_exc_name)
exit_code = exc_class.exit_code
if exc_msg:
log.error(exc_msg)
elif exc_name == 'PIL.Image.DecompressionBombError':
msg = cleanup_ruffus_error_message(exc_value)
msg += ("\nUse the --max-image-mpixels argument to set increase the "
@@ -751,6 +784,7 @@ def preamble(_log):
_log.debug('ocrmypdf ' + VERSION)
_log.debug('tesseract ' + tesseract.version())
_log.debug('qpdf ' + qpdf.version())
_log.debug('gs ' + ghostscript.version())
def check_environ(options, _log):
@@ -835,9 +869,11 @@ def report_output_file_size(options, _log, input_file, output_file):
""".format(ratio, explanation)))
def run_pipeline():
options = parser.parse_args()
def run_pipeline(args=None):
options = parser.parse_args(args=args)
options.verbose_abbreviated_path = 1
if os.environ.get('_OCRMYPDF_THREADS'):
options.use_threads = True
if not check_closed_streams(options):
return ExitCode.bad_args
@@ -858,6 +894,14 @@ def run_pipeline():
"security vulnerabilities with certain malformed PDFs. Consider "
"upgrading to version 7.0.0 or newer.".format(qpdf.version()))
if ghostscript.version() == '9.24':
complain(
"Ghostscript 9.24 contains serious regressions and is not "
"supported. Please upgrade to Ghostscript 9.25 or use an older "
"version."
)
return ExitCode.missing_dependency
# Any changes to options will not take effect for options that are already
# bound to function parameters in the pipeline. (For example
# options.input_file, options.pdf_renderer are already bound.)
+29 -3
View File
@@ -163,13 +163,20 @@ def repair_and_parse_pdf(
copyfile(input_file, output_file)
try:
pdfinfo = PdfInfo(output_file)
pdfinfo = PdfInfo(output_file, log=log)
except pikepdf.PasswordError as e:
raise EncryptedPdfError()
except pikepdf.PdfError as e:
log.error(e)
raise InputFileError()
if pdfinfo.needs_rendering:
log.error(
"This PDF contains dynamic XFA forms created by Adobe LiveCycle "
"Designer and can only be read by Adobe Acrobat or Adobe Reader."
)
raise InputFileError()
if pdfinfo.has_userunit and options.output_type.startswith('pdfa'):
log.error(
"This input file uses a PDF feature that is not supported "
@@ -773,7 +780,20 @@ def generate_postscript_stub(
options = context.get_options()
pdf = pikepdf.open(input_file)
pdfmark = get_pdfmark(pdf, options)
generate_pdfa_ps(output_file, pdfmark)
ascii_docinfo = False
if ghostscript.version() >= '9.24':
ascii_docinfo = True
try:
for v in pdfmark.values():
v.encode('ascii', errors='strict')
except UnicodeEncodeError:
log.warning(
"Ghostscript 9.24+ does not support Unicode strings in "
" metadata. These will be converted to ASCII if possible."
)
generate_pdfa_ps(output_file, pdfmark, ascii_docinfo=ascii_docinfo)
def metadata_fixup(
@@ -810,7 +830,13 @@ def metadata_fixup(
pdfmark = get_pdfmark(metadata, options)
pdf = pikepdf.open(layers_file)
pdf.metadata = pdf.make_indirect(pikepdf.Dictionary(pdfmark))
pdf.save(output_file, stream_data_mode=pikepdf.StreamDataMode.compress)
try:
pdf.save(output_file, compress_streams=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate)
except AttributeError:
# pikepdf <= 0.3.4
pdf.save(output_file,
stream_data_mode=pikepdf.StreamDataMode.compress)
def optimize_pdf(
+63 -27
View File
@@ -137,14 +137,9 @@ def _find_font(text, pdf_base):
return font, font_key
def _fix_toc(pdf_base, pageref_remap, log):
"""Repair the table of contents
Whenever we replace a page wholesale, it gets assigned a new objgen number
and other references to it within the PDF become invalid, most notably in
the table of contents (/Outlines in PDF-speak). In weave_layers we collect
pageref_remap, a mapping that describes the new objgen number given an old
one. (objgen is a tuple, and the gen is almost always zero.)
def _traverse_toc(pdf_base, visitor_fn, log):
"""
Walk the table of contents, calling visitor_fn() at each node
The /Outlines data structure is a messy data structure, but rather than
navigating hierarchically we just track unique nodes. Enqueue nodes when
@@ -152,8 +147,6 @@ def _fix_toc(pdf_base, pageref_remap, log):
the two types of object in the table of contents that can be page bookmarks
and update the page entry.
It may ultimately be better to find a way to rebuild a page in place.
"""
visited = set()
@@ -162,10 +155,53 @@ def _fix_toc(pdf_base, pageref_remap, log):
if not '/Outlines' in pdf_base.root:
return
queue.add(pdf_base.root.Outlines.objgen)
while queue:
objgen = queue.pop()
visited.add(objgen)
node = pdf_base.get_object(objgen)
log.debug('fix toc: exploring outline entries at %r', objgen)
# Enumerate other nodes we could visit from here
for key in link_keys:
if key not in node:
continue
item = node[key]
if not item.is_indirect:
# Direct references are not allowed here, but it's not clear
# what we should do if we find any. Removing them is an option:
# node[key] = pdf_base.make_indirect(None)
continue
objgen = item.objgen
if objgen not in visited:
queue.add(objgen)
if visitor_fn:
visitor_fn(pdf_base, node, log)
def _fix_toc(pdf_base, pageref_remap, log):
"""Repair the table of contents
Whenever we replace a page wholesale, it gets assigned a new objgen number
and other references to it within the PDF become invalid, most notably in
the table of contents (/Outlines in PDF-speak). In weave_layers we collect
pageref_remap, a mapping that describes the new objgen number given an old
one. (objgen is a tuple, and the gen is almost always zero.)
It may ultimately be better to find a way to rebuild a page in place.
"""
if not pageref_remap:
return
def remap_dest(dest_node):
"""
Inner helper function: change the objgen for any page from the old we
invalidated to its new one.
"""
if not isinstance(dest_node, pikepdf.Array):
return
pageref = dest_node[0]
@@ -174,30 +210,24 @@ def _fix_toc(pdf_base, pageref_remap, log):
new_objgen = pageref_remap[pageref.objgen]
dest_node[0] = pdf_base.get_object(new_objgen)
queue.add(pdf_base.root.Outlines.objgen)
while queue:
objgen = queue.pop()
visited.add(objgen)
node = pdf_base.get_object(objgen)
log.debug('fix toc: visiting %r', objgen)
# Enumerate other nodes we could visit from here
for key in link_keys:
if key not in node:
continue
item = node[key]
if not item.is_indirect:
continue
objgen = item.objgen
if objgen not in visited:
queue.add(objgen)
def visit_remap_dest(pdf_base, node, log):
"""
Visitor function to fix ToC entries
Test for the two types of references to pages that can occur in ToCs.
Both types have the same final format (an indirect reference to the
target page).
"""
if '/Dest' in node:
# /Dest reference to another page (old method)
remap_dest(node['/Dest'])
elif '/A' in node:
# /A (action) command set to "GoTo" (newer method)
if '/S' in node['/A'] and node['/A']['/S'] == '/GoTo':
remap_dest(node['/A']['/D'])
_traverse_toc(pdf_base, visit_remap_dest, log)
def weave_layers(
infiles,
@@ -247,6 +277,12 @@ def weave_layers(
pdfinfo = context.get_pdfinfo()
pagerefs = {}
# Walk the table of contents first, to trigger pikepdf/qpdf to resolve all
# page references in the table of contents. Some PDF generators put invalid
# references in the ToC, so we want to resolve them to null before we
# create any references, or the ToC will be corrupted
_traverse_toc(pdf_base, None, log)
procset = pdf_base.make_indirect(
pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]'))
+27
View File
@@ -17,6 +17,7 @@
from enum import IntEnum
from textwrap import dedent
class ExitCode(IntEnum):
ok = 0
@@ -36,6 +37,13 @@ class ExitCode(IntEnum):
class ExitCodeException(Exception):
exit_code = ExitCode.other_error
message = ""
def __str__(self):
super_msg = super().__str__() # Don't do str(super())
if self.message:
return self.message.format(super_msg)
return super_msg
class BadArgsError(ExitCodeException):
@@ -44,7 +52,15 @@ class BadArgsError(ExitCodeException):
class PdfMergeFailedError(ExitCodeException):
exit_code = ExitCode.input_file
message = dedent('''\
Failed to merge PDF image layer with OCR layer
Usually this happens because the input PDF file is malformed and
ocrmypdf cannot automatically correct the problem on its own.
Try using
ocrmypdf --pdf-renderer sandwich [..other args..]
''')
class MissingDependencyError(ExitCodeException):
exit_code = ExitCode.missing_dependency
@@ -76,7 +92,18 @@ class SubprocessOutputError(ExitCodeException):
class EncryptedPdfError(ExitCodeException):
exit_code = ExitCode.encrypted_pdf
message = dedent('''\
Input PDF is encrypted. The encryption must be removed to
perform OCR.
For information about this PDF's security use
qpdf --show-encryption infilename
You can remove the encryption using
qpdf --decrypt [--password=[password]] infilename
''')
class TesseractConfigError(ExitCodeException):
exit_code = ExitCode.invalid_config
message = "Error occurred while parsing a Tesseract configuration file"
-1
View File
@@ -24,7 +24,6 @@ from subprocess import run, STDOUT, PIPE, CalledProcessError
from ..exceptions import MissingDependencyError
def get_version(program, *,
version_arg='--version', regex=r'(\d+(\.\d+)*)'):
"Get the version of the specified program"
+44 -15
View File
@@ -32,15 +32,20 @@ def version():
def jpeg_passthrough_available():
"""
Ghostscript 9.23 introduced JPEG passthrough but it seems to corrupt the
last two bytes of certain images, for now we disable it for 9.23 and
do not mention it for < 9.23.
"""Returns True if the installed version of Ghostscript supports JPEG passthru
Prior to 9.23, Ghostscript decode and re-encoded JPEGs internally. In 9.23
it gained the ability to keep JPEGs unmodified. However, the 9.23
implementation was buggy and would deletes the last two bytes of images in
some cases, as reported here.
https://bugs.ghostscript.com/show_bug.cgi?id=699216
The issue was fixed for 9.24, hence that is the first version we consider
the feature available. (However, we don't use 9.24 at all, so the first
version that allows JPEG passthrough is 9.25.
"""
return False
return version() >= '9.24'
def _gs_error_reported(stream):
@@ -48,8 +53,7 @@ def _gs_error_reported(stream):
def extract_text(input_file, pageno=1):
"""
Use the txtwrite device to get text layout information out
"""Use the txtwrite device to get text layout information out
For details on options of -dTextFormat see
https://www.ghostscript.com/doc/current/VectorDevices.htm#TXT
@@ -60,10 +64,18 @@ def extract_text(input_file, pageno=1):
<span bbox="left top right bottom" font="..." size="...">
<char bbox="...." c="X"/>
:param pageno: number of page to extract, or all pages if None
:return: XML-ish text representation in bytes
"""
if pageno is not None:
pages = [
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno
]
else:
pages = []
args_gs = [
'gs',
'-dQUIET',
@@ -72,10 +84,9 @@ def extract_text(input_file, pageno=1):
'-dNOPAUSE',
'-sDEVICE=txtwrite',
'-dTextFormat=0',
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno,
] + pages + [
'-o', '-',
input_file
fspath(input_file)
]
p = run(args_gs, stdout=PIPE, stderr=PIPE)
@@ -91,8 +102,7 @@ def extract_text(input_file, pageno=1):
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
pageno=1, page_dpi=None, rotation=None):
"""
Rasterize one page of a PDF at resolution (xres, yres) in canvas units.
"""Rasterize one page of a PDF at resolution (xres, yres) in canvas units.
The image is sized to match the integer pixels dimensions implied by
(xres, yres) even if those numbers are noninteger. The image's DPI will
@@ -174,6 +184,23 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
def generate_pdfa(pdf_pages, output_file, compression, log,
threads=1, pdf_version='1.5', pdfa_part='2'):
"""Generate a PDF/A.
The pdf_pages, a list files, will be merged into output_file. One or more
PDF files may be merged. One of the files in this list must be a pdfmark
file that provides Ghostscript with details on how to perform the PDF/A
conversion. By default with we pick PDF/A-2b, but this works for 1 or 3.
compression can be 'jpeg', 'lossless', or an empty string. In 'jpeg',
Ghostscript is instructed to convert color and grayscale images to DCT
(JPEG encoding). In 'lossless' Ghostscript is told to convert images to
Flate (lossless/PNG). If the parameter is omitted Ghostscript is left to
make its own decisions about how to encode images; it appears to use a
heuristic to decide how to encode images. As of Ghostscript 9.25, we
support passthrough JPEG which allows Ghostscript to avoid transcoding
images entirely. (The feature was added in 9.23 but broken, and the 9.24
release of Ghostscript had regressions, so we don't support it until 9.25.)
"""
compression_args = []
if compression == 'jpeg':
compression_args = [
@@ -207,6 +234,9 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
compression_args.append('-dPassThroughJPEGImages=false')
with NamedTemporaryFile(delete=True) as gs_pdf:
# nb no need to specify ProcessColorModel when ColorConversionStrategy
# is set; see:
# https://bugs.ghostscript.com/show_bug.cgi?id=699392
args_gs = [
"gs",
"-dQUIET",
@@ -216,8 +246,7 @@ def generate_pdfa(pdf_pages, output_file, compression, log,
"-dNumRenderingThreads=" + str(threads),
"-sDEVICE=pdfwrite",
"-dAutoRotatePages=/None",
"-sColorConversionStrategy=" + strategy,
"-sProcessColorModel=DeviceRGB"
"-sColorConversionStrategy=" + strategy
] + compression_args + [
"-dJPEGQ=95",
"-dPDFA=" + pdfa_part,
+16 -3
View File
@@ -30,7 +30,6 @@ def version():
return get_version('jbig2', regex=r'jbig2enc (\d+(\.\d+)*).*')
@lru_cache(maxsize=1)
def available():
try:
version()
@@ -44,10 +43,24 @@ def convert_group(*, cwd, infiles, out_prefix):
'jbig2',
'-b',
out_prefix,
'-s',
'-s', # symbol mode (lossy)
# '-r', # refinement mode (lossless symbol mode, currently disabled in
# jbig2)
'-p',
]
args.extend(infiles)
proc = run(args, cwd=cwd, stdout=PIPE, stderr=PIPE)
proc.check_returncode()
return proc
return proc
def convert_single(*, cwd, infile, outfile):
args = [
'jbig2',
'-p',
infile
]
with open(outfile, 'wb') as fstdout:
proc = run(args, cwd=cwd, stdout=fstdout, stderr=PIPE)
proc.check_returncode()
return proc
+10 -2
View File
@@ -23,7 +23,7 @@ import os
import shutil
from . import get_version
from ..exceptions import ExitCode
from ..exceptions import ExitCode, MissingDependencyError
@lru_cache(maxsize=1)
@@ -31,6 +31,14 @@ def version():
return get_version('pngquant', regex=r'(\d+(\.\d+)*).*')
def available():
try:
version()
except MissingDependencyError:
return False
return True
def quantize(input_file, output_file, quality_min, quality_max):
args = [
'pngquant',
@@ -42,4 +50,4 @@ def quantize(input_file, output_file, quality_min, quality_max):
input_file
]
proc = run(args)
proc.check_returncode()
proc.check_returncode()
+4 -5
View File
@@ -190,6 +190,10 @@ def tesseract_log_output(log, stdout, input_file):
pass # Appears to be spurious/problem with nonwhite borders
elif 'Error in boxClipToRectangle' in line:
pass # Always appears with pixScanForForeground message
elif 'parameter not found: ' in line.lower():
log.error(prefix + line.strip())
problem = line.split('found: ')[1]
raise TesseractConfigError(problem)
elif 'error' in line.lower() or 'exception' in line.lower():
log.error(prefix + line.strip())
elif 'warning' in line.lower():
@@ -263,8 +267,6 @@ def generate_hocr(input_file, output_files, language: list, engine_mode,
_generate_null_hocr(output_hocr, output_sidecar, input_file)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
if b'read_params_file: parameter not found' in e.output:
raise TesseractConfigError() from e
if b'Image too large' in e.output:
_generate_null_hocr(output_hocr, output_sidecar, input_file)
return
@@ -350,9 +352,6 @@ def generate_pdf(*, input_image, skip_pdf=None, output_pdf, output_text,
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_image)
if b'read_params_file: parameter not found' in e.output:
raise TesseractConfigError() from e
if b'Image too large' in e.output:
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
return
+16 -30
View File
@@ -32,7 +32,7 @@ from functools import lru_cache
from .lib._leptonica import ffi
from .helpers import fspath
# pylint: disable=w0212
# pylint: disable=protected-access
lept = ffi.dlopen(find_library('lept'))
@@ -45,12 +45,13 @@ def stderr(*objs):
class _LeptonicaErrorTrap:
"""Context manager to trap errors reported by Leptonica.
"""
Context manager to trap errors reported by Leptonica.
Leptonica's error return codes are unreliable to the point of being
almost useless. It does, however, write errors to stderr provided that is
not disabled at its compile time. Fortunately this is done using error
macros so it is very self-consistent.
Leptonica's error return codes don't provide much informatino about what
went wrong. Leptonica does, however, write more detailed errors to stderr
(provided this is not disabled at compile time). The Leptonica source
code is very consistent in its use of macros to generate errors.
This context manager redirects stderr to a temporary file which is then
read and parsed for error messages. As a side benefit, debug messages
@@ -116,7 +117,8 @@ class LeptonicaIOError(LeptonicaError):
class Pix:
"""Wrapper around leptonica's PIX object.
"""
Wrapper around leptonica's PIX object.
Leptonica uses referencing counting on PIX objects. Also, many Leptonica
functions return the original object with an increased reference count
@@ -462,30 +464,14 @@ class Pix:
# implementation of pixCorrelationBinary that overflows on larger
# images. Ubuntu 14.04/trusty has 1.70. Ubuntu PPA
# ppa:alex-p/tesseract-ocr has leptonlib 1.75.
pix1_count = ffi.new('l_int32 *')
pix2_count = ffi.new('l_int32 *')
pixn_count = ffi.new('l_int32 *')
tab8 = Pix.make_pixel_sum_tab8()
raise LeptonicaError("Leptonica version is too old")
lept.pixCountPixels(pix1._pix, pix1_count, tab8)
lept.pixCountPixels(pix2._pix, pix2_count, tab8)
pixn = Pix(lept.pixAnd(ffi.NULL, pix1._pix, pix2._pix))
lept.pixCountPixels(pixn._pix, pixn_count, tab8)
# Python converts these int32s to larger units as needed
# to avoid overflow. Overflow happens easily here.
correlation = (
(pixn_count[0] * pixn_count[0]) /
(pix1_count[0] * pix2_count[0])
)
return correlation
else:
correlation = ffi.new('float *', 0.0)
result = lept.pixCorrelationBinary(pix1._pix, pix2._pix,
correlation)
if result != 0:
raise LeptonicaError("Correlation failed")
return correlation[0]
correlation = ffi.new('float *', 0.0)
result = lept.pixCorrelationBinary(pix1._pix, pix2._pix,
correlation)
if result != 0:
raise LeptonicaError("Correlation failed")
return correlation[0]
def generate_pdf_ci_data(self, type_, quality):
"Convert to PDF data, with transcoding"
+174 -80
View File
@@ -30,9 +30,8 @@ from . import leptonica
from .helpers import re_symlink, fspath
from .exec import pngquant, jbig2enc
PAGE_GROUP_SIZE = 10
DEFAULT_JPEG_QUALITY = 75
DEFAULT_PNG_QUALITY = (65, 75)
DEFAULT_PNG_QUALITY = 70
def img_name(root, xref, ext):
@@ -51,26 +50,34 @@ def tif_name(root, xref):
return img_name(root, xref, '.tif')
def extract_image(*, pike, root, log, image, xref, jbig2s,
pngs, jpegs, options):
def extract_image_filter(pike, root, log, image, xref):
if image.Subtype != '/Image':
return False
return None
if image.Length < 100:
log.debug("Skipping small image, xref {}".format(xref))
return False
log.debug("Skipping small image, xref %s", xref)
return None
pim = pikepdf.PdfImage(image)
if len(pim.filter_decodeparms) > 1:
log.debug("Skipping multiply filtered, xref {}".format(xref))
return False
log.debug("Skipping multiply filtered, xref %s", xref)
return None
filtdp = pim.filter_decodeparms[0]
if pim.bits_per_component > 8:
return False # Don't mess with wide gamut images
return None # Don't mess with wide gamut images
if filtdp[0] == '/JPXDecode':
return False # Don't do JPEG2000
return None # Don't do JPEG2000
return pim, filtdp
def extract_image_jbig2(*, pike, root, log, image, xref, options):
result = extract_image_filter(pike, root, log, image, xref)
if result is None:
return None
pim, filtdp = result
if pim.bits_per_component == 1 \
and filtdp != '/JBIG2Decode' \
@@ -81,9 +88,18 @@ def extract_image(*, pike, root, log, image, xref, jbig2s,
ext = pim.extract_to(stream=f)
imgname.rename(imgname.with_suffix(ext))
except pikepdf.UnsupportedImageTypeError:
return False
jbig2s.append((xref, ext))
elif filtdp[0] == '/DCTDecode' \
return None
return xref, ext
return None
def extract_image_generic(*, pike, root, log, image, xref, options):
result = extract_image_filter(pike, root, log, image, xref)
if result is None:
return None
pim, filtdp = result
if filtdp[0] == '/DCTDecode' \
and options.optimize >= 2:
# This is a simple heuristic derived from some training data, that has
# about a 70% chance of guessing whether the JPEG is high quality,
@@ -92,7 +108,7 @@ def extract_image(*, pike, root, log, image, xref, jbig2s,
# bytes_per_pixel = int(raw_jpeg.Length) / (w * h)
# jpeg_quality_estimate = 117.0 * (bytes_per_pixel ** 0.213)
# if jpeg_quality_estimate < 65:
# return False
# return None
# We could get the ICC profile here, but there's no need to look at it
# for quality transcoding
@@ -107,88 +123,111 @@ def extract_image(*, pike, root, log, image, xref, jbig2s,
ext = pim.extract_to(stream=f)
imgname.rename(imgname.with_suffix(ext))
except pikepdf.UnsupportedImageTypeError:
return False
jpegs.append(xref)
return None
return xref, ext
elif pim.indexed \
and pim.colorspace in pim.SIMPLE_COLORSPACES \
and options.optimize >= 3:
# Try to improve on indexed images - these are far from low hanging
# fruit in most cases
pim.as_pil_image().save(png_name(root, xref))
pngs.append(xref)
return xref, '.png'
elif not pim.indexed and pim.colorspace in pim.SIMPLE_COLORSPACES:
# An optimization opportunity here, not currently taken, is directly
# generating a PNG from compressed data
pim.as_pil_image().save(png_name(root, xref))
pngs.append(xref)
return xref, '.png'
else:
return False
return None
return True
def extract_images(pike, root, log, options):
# Extract images we can improve
changed_xrefs = set()
jbig2_groups = defaultdict(lambda: [])
jpegs = []
pngs = []
def extract_images(pike, root, log, options, extract_fn):
"""Extract image using extract_fn
extract_fn decides where the image is interesting in this case
"""
include_xrefs = set()
exclude_xrefs = set()
errors = 0
for pageno, page in enumerate(pike.pages):
group, _ = divmod(pageno, PAGE_GROUP_SIZE)
try:
xobjs = page.Resources.XObject
except AttributeError:
continue
for imname, image in dict(xobjs).items():
for _imname, image in dict(xobjs).items():
if image.objgen[1] != 0:
continue # Ignore images in an incremental PDF
xref = image.objgen[0]
if xref in changed_xrefs:
continue # Don't improve same image twice
try:
result = extract_image(
pike=pike, root=root, log=log, image=image,
xref=xref, jbig2s=jbig2_groups[group], pngs=pngs,
jpegs=jpegs, options=options
)
if result:
changed_xrefs.add(xref)
except Exception as e:
log.debug("Image {} xref {}".format(imname, xref))
log.debug(repr(e))
errors += 1
if hasattr(image, 'SMask'):
# Ignore soft masks
smask_xref = image.SMask.objgen[0]
exclude_xrefs.add(smask_xref)
include_xrefs.add(xref)
working_xrefs = include_xrefs - exclude_xrefs
for xref in working_xrefs:
image = pike.get_object((xref, 0))
try:
result = extract_fn(
pike=pike, root=root, log=log, image=image,
xref=xref, options=options
)
except Exception as e:
log.debug("Image xref %s", xref)
log.debug(repr(e))
errors += 1
else:
if result:
_, ext = result
yield pageno, xref, ext
def extract_images_generic(pike, root, log, options):
"""Extract any >=2bpp image we think we can improve"""
jpegs = []
pngs = []
for _, xref, ext in extract_images(
pike, root, log, options, extract_image_generic):
log.debug('xref = %s ext = %s', xref, ext)
if ext == '.png':
pngs.append(xref)
elif ext == '.jpg':
jpegs.append(xref)
log.debug(
"Optimizable images: "
"JPEGs: %s PNGs: %s", len(jpegs), len(pngs)
)
return jpegs, pngs
def extract_images_jbig2(pike, root, log, options):
"""Extract any bitonal image that we think we can improve as JBIG2"""
jbig2_groups = defaultdict(list)
for pageno, xref, ext in extract_images(
pike, root, log, options, extract_image_jbig2):
group = pageno // options.jbig2_page_group_size
jbig2_groups[group].append((xref, ext))
# Elide empty groups
jbig2_groups = {group: xrefs for group, xrefs in jbig2_groups.items()
if len(xrefs) > 0}
log.debug(
"Optimizable images: "
"JBIG2 groups: {} JPEGs: {} PNGs: {} Errors: {}".format(
len(jbig2_groups), len(jpegs), len(pngs), errors
))
return jbig2_groups, jpegs, pngs
"JBIG2 groups: %s", (len(jbig2_groups),)
)
return jbig2_groups
def convert_to_jbig2(pike, jbig2_groups, root, log, options):
"""
Convert a group of JBIG2 images and insert into PDF.
def _produce_jbig2_images(jbig2_groups, root, log, options):
"""Produce JBIG2 images from their groups"""
We use a group because JBIG2 works best with a symbol dictionary that spans
multiple pages. When inserted back into the PDF, each JBIG2 must reference
the symbol dictionary it is associated with. So convert a group at a time,
and replace their streams with a parameter set that points to the
appropriate dictionary.
If too many pages shared the same dictionary JBIG2 encoding becomes more
expensive and less efficient.
"""
with concurrent.futures.ThreadPoolExecutor(
max_workers=options.jobs) as executor:
futures = []
for group, xref_exts in jbig2_groups.items():
def jbig2_group_futures(executor, root, groups):
for group, xref_exts in groups.items():
prefix = 'group{:08d}'.format(group)
future = executor.submit(
jbig2enc.convert_group,
@@ -196,15 +235,64 @@ def convert_to_jbig2(pike, jbig2_groups, root, log, options):
infiles=(img_name(root, xref, ext) for xref, ext in xref_exts),
out_prefix=prefix
)
futures.append(future)
yield future
def jbig2_single_futures(executor, root, groups):
for group, xref_exts in groups.items():
prefix = 'group{:08d}'.format(group)
# Second loop is to ensure multiple images per page are unpacked
for n, xref_ext in enumerate(xref_exts):
xref, ext = xref_ext
future = executor.submit(
jbig2enc.convert_single,
cwd=fspath(root),
infile=img_name(root, xref, ext),
outfile=root / ('{}.{:04d}'.format(prefix, n))
)
yield future
if options.jbig2_page_group_size > 1:
jbig2_futures = jbig2_group_futures
else:
jbig2_futures = jbig2_single_futures
with concurrent.futures.ThreadPoolExecutor(
max_workers=options.jobs) as executor:
futures = jbig2_futures(executor, root, jbig2_groups)
for future in concurrent.futures.as_completed(futures):
proc = future.result()
log.debug(proc.stderr.decode())
def convert_to_jbig2(pike, jbig2_groups, root, log, options):
"""Convert images to JBIG2 and insert into PDF.
When the JBIG2 page group size is > 1 we do several JBIG2 images at once
and build a symbol dictionary that will span several pages. Each JBIG2
image must reference to its symbol dictionary. If too many pages shared the
same dictionary JBIG2 encoding becomes more expensive and less efficient.
The default value of 10 was determined through testing. Currently this
must be lossy encoding since jbig2enc does not support refinement coding.
When the JBIG2 symbolic coder is not used, each JBIG2 stands on its own
and needs no dictionary. Currently this is must be lossless JBIG2.
"""
_produce_jbig2_images(jbig2_groups, root, log, options)
for group, xref_exts in jbig2_groups.items():
prefix = 'group{:08d}'.format(group)
jbig2_globals_data = (root / (prefix + '.sym')).read_bytes()
jbig2_globals = pikepdf.Stream(pike, jbig2_globals_data)
jbig2_symfile = root / (prefix + '.sym')
if jbig2_symfile.exists():
jbig2_globals_data = jbig2_symfile.read_bytes()
jbig2_globals = pikepdf.Stream(pike, jbig2_globals_data)
jbig2_globals_dict = pikepdf.Dictionary({
'/JBIG2Globals': jbig2_globals
})
elif options.jbig2_page_group_size == 1:
jbig2_globals_dict = None
else:
raise FileNotFoundError(jbig2_symfile)
for n, xref_ext in enumerate(xref_exts):
xref, _ = xref_ext
@@ -213,9 +301,7 @@ def convert_to_jbig2(pike, jbig2_groups, root, log, options):
im_obj = pike.get_object(xref, 0)
im_obj.write(
jbig2_im_data, pikepdf.Name('/JBIG2Decode'),
pikepdf.Dictionary({
'/JBIG2Globals': jbig2_globals
})
jbig2_globals_dict
)
@@ -232,9 +318,9 @@ def transcode_jpegs(pike, jpegs, root, log, options):
im.save(fspath(opt_jpg),
optimize=True,
quality=options.jpeg_quality)
# pylint: disable=E1101
# pylint: disable=no-member
if opt_jpg.stat().st_size > in_jpg.stat().st_size:
log.debug("xref {}, jpeg, made larger - skip".format(xref))
log.debug("xref %s, jpeg, made larger - skip", xref)
continue
compdata = leptonica.CompressedData.open(opt_jpg)
@@ -326,21 +412,25 @@ def optimize(
if options.png_quality == 0:
options.png_quality = \
DEFAULT_PNG_QUALITY if options.optimize < 3 else 30
if options.jbig2_page_group_size == 0:
options.jbig2_page_group_size = \
10 if options.jbig2_lossy else 1
pike = pikepdf.Pdf.open(input_file)
root = Path(output_file).parent / 'images'
root.mkdir(exist_ok=True) # pylint: disable=E1101
jbig2_groups, jpegs, pngs = extract_images(
pike, root, log, options)
root.mkdir(exist_ok=True) # pylint: disable=no-member
convert_to_jbig2(pike, jbig2_groups, root, log, options)
jpegs, pngs = extract_images_generic(pike, root, log, options)
transcode_jpegs(pike, jpegs, root, log, options)
transcode_pngs(pike, pngs, root, log, options)
# Not object_stream_mode + preserve_pdfa generates noncompliant PDFs
jbig2_groups = extract_images_jbig2(pike, root, log, options)
convert_to_jbig2(pike, jbig2_groups, root, log, options)
target_file = Path(output_file).with_suffix('.opt.pdf')
pike.save(target_file, preserve_pdfa=True)
pike.save(target_file, preserve_pdfa=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate)
input_size = Path(input_file).stat().st_size
output_size = Path(target_file).stat().st_size
@@ -363,11 +453,14 @@ def main(infile, outfile, level, jobs=1):
class OptimizeOptions:
"""Emulate ocrmypdf's options"""
def __init__(self, jobs, optimize, jpeg_quality, png_quality):
def __init__(
self, jobs, optimize, jpeg_quality, png_quality, jb2lossy):
self.jobs = jobs
self.optimize = optimize
self.jpeg_quality = jpeg_quality
self.png_quality = png_quality
self.jbig2_page_group_size = 0
self.jbig2_lossy = jb2lossy
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger()
@@ -377,7 +470,8 @@ def main(infile, outfile, level, jobs=1):
jobs=jobs,
optimize=int(level),
jpeg_quality=0, # Use default
png_quality=0
png_quality=0,
jb2lossy=False
)
ctx.set_options(options)
+66 -14
View File
@@ -15,7 +15,21 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
# Generate a PDFA_def.ps file for Ghostscript >= 9.14
"""
Generate a PDFMARK file for Ghostscript >= 9.14, for PDF/A conversion
pdfmark is an extension to the Postscript language that describes some PDF
features like bookmarks and annotations. It was originally specified Adobe
Distiller, for Postscript to PDF conversion:
https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
Ghostscript uses pdfmark for PDF to PDF/A conversion as well. To use Ghostscript
to create a PDF/A, we need to create a pdfmark file with the necessary metadata.
This takes care of the many version-specific bugs and pecularities in
Ghostscript's handling of pdfmark.
"""
from binascii import hexlify
from datetime import datetime
@@ -78,7 +92,8 @@ def
def encode_text_string(s: str) -> str:
'''Encode text string to hex string for use in a PDF
"""
Encode text string to hex string for use in a PDF
From PDF 32000-1:2008 a string object may be included in hexademical form
if it is enclosed in angle brackets. For general Unicode the string should
@@ -86,7 +101,7 @@ def encode_text_string(s: str) -> str:
ASCII strings could be encoded as PdfDocEncoding literals provided
that certain Postscript sequences are escaped. But it's far simpler to
encode everything as UTF-16.
'''
"""
# Sometimes lazy C programmers leave their NULs at the end of strings they
# insert into PDFs
@@ -102,8 +117,28 @@ def encode_text_string(s: str) -> str:
return ascii_hex_str
def _encode_ascii(s: str) -> str:
"""
Aggressively strip non-ASCII and PDF escape sequences
Ghostscript 9.24+ lost support for UTF-16BE in pdfmark files for reasons
given in GhostPDL commit e997c683. Our temporary workaround is use ASCII
and drop all non-ASCII characters. A slightly improved alternative would
be to implement PdfDocEncoding in pikepdf and encode to that, or handle
metadata there.
"""
trans = str.maketrans({
'(': '',
')': '',
'\\': '',
'\0': ''
})
return s.translate(trans).encode('ascii', errors='replace').decode()
def encode_pdf_date(d: datetime) -> str:
"""Encode Python datetime object as PDF date string
"""
Encode Python datetime object as PDF date string
From Adobe pdfmark manual:
(D:YYYYMMDDHHmmSSOHH'mm')
@@ -137,6 +172,13 @@ def encode_pdf_date(d: datetime) -> str:
def decode_pdf_date(s: str) -> datetime:
"""
Decode a pdfmark date to a Python datetime object
A pdfmark date is a string in a paritcular format. See the pdfmark
Reference for the specification.
"""
if s.startswith('D:'):
s = s[2:]
@@ -153,11 +195,13 @@ def decode_pdf_date(s: str) -> datetime:
def _get_pdfmark_dates(pdfmark):
"""Encode dates for pdfmark Postscript. The best way to deal with a
missing date entry is set it to null, because if the key is omitted
Ghostscript will set it to now - we do not want to erase the fact that
the value was unknown. Setting to an empty string breaks Ghostscript
9.22 as reported here:
"""
Encode dates in the expected format for pdfmark Postscript
The best way to deal with amissing date entry is set it to null, because if
the key is omitted Ghostscript will set it to now - we do not want to erase
the fact that the value was unknown. Setting to an empty string breaks
Ghostscript 9.22 as reported here:
https://bugs.ghostscript.com/show_bug.cgi?id=699182
"""
@@ -178,7 +222,7 @@ def _get_pdfmark_dates(pdfmark):
yield ' {} null'.format(key)
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark, ascii_docinfo=False):
"""
Create a Postscript pdfmark file for Ghostscript.
@@ -188,6 +232,9 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
:param icc_profile: filename of the ICC profile to include in pdfmark
:param icc_identifier: ICC identifier such as 'sRGB'
:param pdfmark: a dictionary containing keys to include the pdfmark
:param ascii_docinfo: if True, the docinfo block must be encoded in pure
ASCII and may not contain UTF-16BE-BOM-hex encoded strings, as
required for Ghostscript 9.24+
:returns: a string containing the entire pdfmark
@@ -198,14 +245,19 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
# https://bugs.ghostscript.com/show_bug.cgi?id=697684
# Work around this by only adding keys that have a nontrivial value
docinfo_keys = ('/Title', '/Author', '/Subject', '/Creator', '/Keywords')
docinfo_line_template = ' {key} <{value}>'
def docinfo_gen():
if not ascii_docinfo:
docinfo_line_template = ' {key} <{value}>'
encode = encode_text_string
else:
docinfo_line_template = ' {key} ({value})'
encode = _encode_ascii
yield from _get_pdfmark_dates(pdfmark)
for key in docinfo_keys:
if key in pdfmark and pdfmark[key].strip() != '':
line = docinfo_line_template.format(
key=key, value=encode_text_string(pdfmark[key]))
key=key, value=encode(pdfmark[key]))
yield line
docinfo = '\n'.join(docinfo_gen())
@@ -216,7 +268,7 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
return result
def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB'):
def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
if icc == 'sRGB':
icc_profile = SRGB_ICC_PROFILE
else:
@@ -233,7 +285,7 @@ def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB'):
hex_icc_profile = hexlify(bytes_icc_profile)
icc_profile = '<' + hex_icc_profile.decode('ascii') + '>'
ps = _get_pdfa_def(icc_profile, icc, pdfmark)
ps = _get_pdfa_def(icc_profile, icc, pdfmark, ascii_docinfo=ascii_docinfo)
# We should have encoded everything to pure ASCII by this point, and
# to be safe, only allow ASCII in PostScript
+54 -60
View File
@@ -16,12 +16,14 @@
# You should have received a copy of the GNU General Public License
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
from decimal import Decimal
from math import hypot, isclose
import re
from collections import namedtuple
from pathlib import Path
from decimal import Decimal
from enum import Enum
from math import hypot, isclose
from pathlib import Path
from unittest.mock import Mock
import re
import xml.etree.ElementTree as ET
from .exec import ghostscript
from .helpers import fspath
@@ -90,28 +92,6 @@ FRIENDLY_COMP = {
UNIT_SQUARE = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
def _matrix_from_shorthand(shorthand):
"""Convert from PDF matrix shorthand to full matrix
PDF 1.7 spec defines a shorthand for describing the entries of a matrix
since the last column is always (0, 0, 1).
"""
a, b, c, d, e, f = map(float, shorthand)
return ((a, b, 0),
(c, d, 0),
(e, f, 1))
def _shorthand_from_matrix(matrix):
"""Convert from transformation matrix to PDF shorthand."""
a, b = matrix[0][0], matrix[0][1]
c, d = matrix[1][0], matrix[1][1]
e, f = matrix[2][0], matrix[2][1]
return tuple(map(float, (a, b, c, d, e, f)))
def _is_unit_square(shorthand):
values = map(float, shorthand)
pairwise = zip(values, UNIT_SQUARE)
@@ -508,27 +488,13 @@ def _find_images(*, pdf, container, shorthand=None):
yield from _find_form_xobject_images(pdf, container, contentsinfo)
def _page_get_textblocks(infile, pageno):
def _page_get_textblocks(infile, pageno, xmltext):
"""Smarter text detection"""
import xml.etree.ElementTree as ET
gstext = ghostscript.extract_text(infile, pageno+1)
# Remove all <char /> tags, because they might contain invalid XML entities
# like <char bbox="348 596 348 596" c="&#x1;"/> which chokes on the
# inclusion of U+0001. Understandably.
# Just remove the whole <char /> tag since we don't use it at all, and they
# are only generated as innermost self-closing tags.
gstext = regex_remove_char_tags.sub(b' ', gstext)
if gstext.strip() == '':
root = xmltext
if not hasattr(xmltext, 'findall'):
return []
try:
root = ET.fromstring(gstext)
except ET.ParseError as e:
return [] # If we can't parse, assume none...
def blocks():
for span in root.findall('.//span'):
bbox_str = span.attrib['bbox']
@@ -587,14 +553,15 @@ def _page_has_text(text_blocks, page_width, page_height):
return has_text
def _pdf_get_pageinfo(pdf, pageno: int, infile):
def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
pageinfo = {}
pageinfo['pageno'] = pageno
pageinfo['images'] = []
page = pdf.pages[pageno]
pageinfo['textinfo'] = _page_get_textblocks(fspath(infile), pageno)
pageinfo['textinfo'] = _page_get_textblocks(
fspath(infile), pageno, xmltext=xmltext)
mediabox = [Decimal(d) for d in page.MediaBox.as_list()]
width_pt = mediabox[2] - mediabox[0]
@@ -631,16 +598,47 @@ def _pdf_get_pageinfo(pdf, pageno: int, infile):
return pageinfo
def _pdf_get_all_pageinfo(infile):
def _pdf_get_all_pageinfo(infile, log=None):
if not log:
log = Mock()
pdf = pikepdf.open(infile)
return [PageInfo(pdf, n, infile) for n in range(len(pdf.pages))]
existing_text = ghostscript.extract_text(infile, pageno=None)
existing_text = regex_remove_char_tags.sub(b' ', existing_text)
try:
root = ET.fromstringlist([
b'<document>\n', existing_text, b'</document>\n'
])
page_xml = root.findall('page')
except ET.ParseError as e:
log.error(
"An error occurred while attempting to retrieve existing text in "
"the input file. Will attempt to continue assuming that there is "
"no existing text in the file. The error was:")
log.error(e)
page_xml = [None] * len(pdf.pages)
page_count_difference = len(pdf.pages) - len(page_xml)
if page_count_difference != 0:
log.error("The number of pages in the input file is inconsistent.")
if page_count_difference > 0:
page_xml.extend([None] * page_count_difference)
pages = []
for n in range(len(pdf.pages)):
page = PageInfo(pdf, n, infile, page_xml[n])
pages.append(page)
return pages, pdf
class PageInfo:
def __init__(self, pdf, pageno, infile):
def __init__(self, pdf, pageno, infile, xmltext):
self._pageno = pageno
self._infile = infile
self._pageinfo = _pdf_get_pageinfo(pdf, pageno, infile)
self._pageinfo = _pdf_get_pageinfo(pdf, pageno, infile, xmltext)
@property
def pageno(self):
@@ -717,9 +715,10 @@ class PdfInfo:
"""Get summary information about a PDF
"""
def __init__(self, infile):
def __init__(self, infile, log=None):
self._infile = infile
self._pages = _pdf_get_all_pageinfo(infile)
self._pages, pdf = _pdf_get_all_pageinfo(infile, log=log)
self._needs_rendering = pdf.root.get('/NeedsRendering', False)
@property
def pages(self):
@@ -740,6 +739,10 @@ class PdfInfo:
raise NotImplementedError("can't get filename from stream")
return self._infile
@property
def needs_rendering(self):
return self._needs_rendering
def __getitem__(self, item):
return self._pages[item]
@@ -749,15 +752,6 @@ class PdfInfo:
def __repr__(self):
return "<PdfInfo('...'), page count={}>".format(len(self))
# def __getstate__(self):
# state = {'_infile': self._infile}
# return state
#
# def __setstate__(self, state):
# self._infile = state['_infile']
# self._pages = _pdf_get_all_pageinfo(self._infile)
def main():
import argparse
parser = argparse.ArgumentParser()
+2 -1
View File
@@ -134,7 +134,8 @@ def check_ocrmypdf(input_file, output_file, *args, env=None):
"Run ocrmypdf and confirmed that a valid file was created"
p, out, err = run_ocrmypdf(input_file, output_file, *args, env=env)
print(err) # ensure py.test collects the output, use -s to view
# ensure py.test collects the output, use -s to view
print(err, file=sys.stderr)
assert p.returncode == 0
assert os.path.exists(str(output_file)), "Output file not created"
assert os.stat(str(output_file)).st_size > 100, "PDF too small or empty"
+10
View File
@@ -104,6 +104,16 @@ licensed under the specified license.
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - kcs.pdf
- PDF file generated by Kodak Capture Desktop Software 1.2; has invalid table of contents
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - livecycle.pdf
- a minimal PDF that claims to use dynamic XFA forms
- @jbarlow83
- @jbarlow83
- CC-BY-SA 4.0
* - masks.pdf
- file containing explicit masks and a stencil mask drawn without a proper transformation matrix; printout of a German Wikipedia article (CC-BY-SA)
- @supergrobi
BIN
View File
Binary file not shown.
+28
View File
@@ -0,0 +1,28 @@
%PDF-1.7
%¿÷¢þ
1 0 obj
<< /NeedsRendering true /Pages 2 0 R /Type /Catalog >>
endobj
2 0 obj
<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
endobj
3 0 obj
<< /Contents 4 0 R /MediaBox [ 0 0 300 300 ] /Parent 2 0 R /Resources << >> /Type /Page >>
endobj
4 0 obj
<< /Length 0 >>
stream
endstream
endobj
xref
0 5
0000000000 65535 f
0000000015 00000 n
0000000085 00000 n
0000000144 00000 n
0000000250 00000 n
trailer << /Root 1 0 R /Size 5 /ID [<8088c23e0edc07ef4fbed3daa55f52ed><9d36eee510d20004f32255f5800b6fd7>] >>
startxref
299
%%EOF
+1 -1
View File
@@ -20,11 +20,11 @@
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Replicate Ghostscript render failure while allowing rasterizing"""
import sys
import os
"""Replicate Ghostscript render failure while allowing rasterizing"""
def real_ghostscript(argv):
+10 -1
View File
@@ -595,7 +595,7 @@ THIS FILE IS INVALID
resources / 'ccitt.pdf', outdir / 'out.pdf',
'--pdf-renderer', renderer,
'--tesseract-config', cfg_file)
assert "parameter not found" in err, "No error message"
assert "parameter not found" in err.lower(), "No error message"
assert p.returncode == ExitCode.invalid_config
@@ -925,3 +925,12 @@ def test_output_is_symlink(spoof_tesseract_noop, resources, outdir):
)
assert p.returncode == ExitCode.ok, err
assert (outdir / 'out.pdf').stat().st_size > 0, 'target file not created'
def test_livecycle(resources, no_outpdf):
p, _, err = run_ocrmypdf(
resources / 'livecycle.pdf',
no_outpdf
)
assert p.returncode == ExitCode.input_file, err
+38 -17
View File
@@ -17,7 +17,6 @@
import pytest
import PyPDF2 as pypdf
from datetime import timezone
from pathlib import Path
@@ -25,12 +24,15 @@ from shutil import copyfile
from unittest.mock import patch
import datetime
import pikepdf
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.helpers import fspath
from ocrmypdf.pdfa import (
file_claims_pdfa, encode_pdf_date, decode_pdf_date, generate_pdfa_ps,
SRGB_ICC_PROFILE
)
from ocrmypdf.exec import ghostscript
try:
import fitz
@@ -51,17 +53,17 @@ spoof = pytest.helpers.spoof
])
def test_preserve_metadata(spoof_tesseract_noop, output_type,
resources, outpdf):
pdf_before = pypdf.PdfFileReader(str(resources / 'graph.pdf'))
pdf_before = pikepdf.open(resources / 'graph.pdf')
output = check_ocrmypdf(
resources / 'graph.pdf', outpdf,
'--output-type', output_type,
env=spoof_tesseract_noop)
pdf_after = pypdf.PdfFileReader(str(output))
pdf_after = pikepdf.open(output)
for key in ('/Title', '/Author'):
assert pdf_before.documentInfo[key] == pdf_after.documentInfo[key]
assert pdf_before.metadata[key] == pdf_after.metadata[key]
pdfa_info = file_claims_pdfa(str(output))
assert pdfa_info['output'] == output_type
@@ -85,15 +87,18 @@ def test_override_metadata(spoof_tesseract_noop, output_type, resources,
assert p.returncode == ExitCode.ok, err
before = pypdf.PdfFileReader(str(input_file))
after = pypdf.PdfFileReader(outpdf)
before = pikepdf.open(input_file)
after = pikepdf.open(outpdf)
assert after.documentInfo['/Title'] == german
assert after.documentInfo['/Author'] == chinese
assert after.documentInfo.get('/Keywords', '') == ''
if ghostscript.version() >= '9.24':
pytest.xfail('Ghostscript 9.24+ does not support Unicode DOCINFO')
before_date = decode_pdf_date(before.documentInfo['/CreationDate'])
after_date = decode_pdf_date(after.documentInfo['/CreationDate'])
assert after.metadata.Title == german, after.metadata
assert after.metadata.Author == chinese, after.metadata
assert after.metadata.get('/Keywords', '') == ''
before_date = decode_pdf_date(str(before.metadata.CreationDate))
after_date = decode_pdf_date(str(after.metadata.CreationDate))
assert before_date == after_date
pdfa_info = file_claims_pdfa(outpdf)
@@ -146,26 +151,30 @@ def test_creation_date_preserved(spoof_tesseract_noop, output_type, resources,
infile, outpdf):
input_file = resources / infile
before = pypdf.PdfFileReader(str(input_file)).getDocumentInfo()
check_ocrmypdf(
input_file, outpdf, '--output-type', output_type,
env=spoof_tesseract_noop)
after = pypdf.PdfFileReader(str(outpdf)).getDocumentInfo()
pdf_before = pikepdf.open(input_file)
pdf_after = pikepdf.open(outpdf)
before = pdf_before.trailer.get('/Info', {})
after = pdf_after.trailer.get('/Info', {})
if not before:
# If there was input creation date, none should be output
# because of Ghostscript quirks we set it to null
# This test would be better if we had a test file with /DocumentInfo but
# no /CreationDate, which we don't
assert not after.get('/CreationDate')
assert after.get('/CreationDate', '') == ''
else:
# We expect that the creation date stayed the same
date_before = decode_pdf_date(before['/CreationDate'])
date_after = decode_pdf_date(after['/CreationDate'])
date_before = decode_pdf_date(str(before['/CreationDate']))
date_after = decode_pdf_date(str(after['/CreationDate']))
assert seconds_between_dates(date_before, date_after) < 1000
# We expect that the modified date is quite recent
date_after = decode_pdf_date(after['/ModDate'])
date_after = decode_pdf_date(str(after['/ModDate']))
assert seconds_between_dates(
date_after, datetime.datetime.now(timezone.utc)) < 1000
@@ -260,3 +269,15 @@ def test_srgb_in_unicode_path(tmpdir):
with patch('ocrmypdf.pdfa.SRGB_ICC_PROFILE', new=str(dst)):
generate_pdfa_ps(dstdir / 'out.ps', {})
def test_kodak_toc(resources, outpdf, spoof_tesseract_noop):
output = check_ocrmypdf(
resources / 'kcs.pdf', outpdf,
'--output-type', 'pdf',
env=spoof_tesseract_noop)
p = pikepdf.open(outpdf)
if pikepdf.Name.First in p.root.Outlines:
assert isinstance(p.root.Outlines.First, pikepdf.Dictionary)
+48 -3
View File
@@ -22,9 +22,11 @@ import logging
from PIL import Image
import pikepdf
from ocrmypdf import optimize as opt
from ocrmypdf.exec.ghostscript import rasterize_pdf
from ocrmypdf.exec import jbig2enc
from ocrmypdf.exec import jbig2enc, pngquant
from ocrmypdf.helpers import fspath
@@ -53,10 +55,53 @@ def test_mono_not_inverted(resources, outdir):
assert im.getpixel((0, 0)) == 255, "Expected white background"
@pytest.mark.skipif(not jbig2enc.available(), reason='need jbig2enc')
def test_jpg_png_params(resources, outpdf, spoof_tesseract_noop):
check_ocrmypdf(
resources / 'crom.png', outpdf, '--image-dpi', '200',
'--optimize', '2', '--jpg-quality', '50', '--png-quality', '20',
'--optimize', '3', '--jpg-quality', '50', '--png-quality', '20',
env=spoof_tesseract_noop
)
@pytest.mark.skipif(not jbig2enc.available(), reason='need jbig2enc')
@pytest.mark.parametrize('lossy', [False, True])
def test_jbig2_lossy(lossy, resources, outpdf, spoof_tesseract_noop):
args = [
resources / 'ccitt.pdf', outpdf, '--image-dpi', '200',
'--optimize', 3, '--jpg-quality', '50', '--png-quality', '20'
]
if lossy:
args.append('--jbig2-lossy')
check_ocrmypdf(*args, env=spoof_tesseract_noop)
pdf = pikepdf.open(outpdf)
pim = pikepdf.PdfImage(next(iter(pdf.pages[0].images.values())))
assert pim.filters[0] == '/JBIG2Decode'
if lossy:
assert '/JBIG2Globals' in pim.decode_parms[0]
else:
assert len(pim.decode_parms) == 0
@pytest.mark.skipif(not jbig2enc.available() or not pngquant.available(),
reason='need jbig2enc and pngquant')
def test_flate_to_jbig2(resources, outdir, spoof_tesseract_noop):
# This test requires an image that pngquant is capable of converting to
# to 1bpp - so use an existing 1bpp image, convert up, confirm it can
# convert down
im = Image.open(fspath(resources / 'typewriter.png'))
assert im.mode in ('1', 'P')
im = im.convert('L')
im.save(fspath(outdir / 'type8.png'))
check_ocrmypdf(
outdir / 'type8.png', outdir / 'out.pdf',
'--image-dpi', '100', '--png-quality', '10', '--optimize', '3',
env=spoof_tesseract_noop
)
pdf = pikepdf.open(outdir / 'out.pdf')
pim = pikepdf.PdfImage(next(iter(pdf.pages[0].images.values())))
assert pim.filters[0] == '/JBIG2Decode'
+5
View File
@@ -33,6 +33,11 @@ from ocrmypdf.helpers import fspath
# pylint: disable=no-member
# pylint: disable=w0612
pytestmark = pytest.mark.skipif(
leptonica.get_leptonica_version() < 'leptonica-1.72',
reason="Leptonica is too old, correlation doesn't work"
)
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf = pytest.helpers.run_ocrmypdf
+8 -4
View File
@@ -29,8 +29,7 @@ from pathlib import Path
spoof = pytest.helpers.spoof
@pytest.fixture
def ensure_tess4():
def _ensure_tess4():
if tesseract.v4():
# "tesseract" on $PATH is already v4
return os.environ.copy()
@@ -49,6 +48,11 @@ def ensure_tess4():
raise EnvironmentError("Can't find Tesseract 4")
@pytest.fixture
def ensure_tess4():
return _ensure_tess4()
@contextmanager
def modified_os_environ(env):
old_env = os.environ.copy()
@@ -63,8 +67,8 @@ def tess4_available():
"""
try:
# ensure_tess4 locates the tess4 binary we are going to check
env = ensure_tess4()
# _ensure_tess4 locates the tess4 binary we are going to check
env = _ensure_tess4()
with modified_os_environ(env):
# Now jump into this environment and make sure it really is Tess4
return tesseract.v4() and tesseract.has_textonly_pdf()