Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efa7ea4fde | ||
|
|
137a6e45f5 | ||
|
|
29116e1dec | ||
|
|
87193335b9 | ||
|
|
cfd4f8a850 | ||
|
|
eaa324939f | ||
|
|
ef70e538f7 | ||
|
|
b7b912e56a | ||
|
|
4615cf2f1e | ||
|
|
eaf772f80a | ||
|
|
96ba75eabd | ||
|
|
fdfe52c1ad | ||
|
|
932b2e2a29 | ||
|
|
57e489c957 | ||
|
|
17a3fa671c | ||
|
|
2659afb4f6 | ||
|
|
7392115507 | ||
|
|
c54d0c7eaa | ||
|
|
b95eefc65f | ||
|
|
686207ab7f | ||
|
|
517b385fe5 | ||
|
|
795019b0c1 | ||
|
|
3127a73822 | ||
|
|
069ee6c91f | ||
|
|
3aac3a98ca | ||
|
|
268859a304 | ||
|
|
a96710aa7b | ||
|
|
edcc58826a | ||
|
|
7077c8220a | ||
|
|
f7cbf68edd | ||
|
|
68a58ee8a5 | ||
|
|
3109ec5091 | ||
|
|
e0599fe8d7 | ||
|
|
a749240589 | ||
|
|
6decdaa062 | ||
|
|
4d5c9b8cdf |
+14
-21
@@ -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
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
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
@@ -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.
|
||||
+16
-12
@@ -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
|
||||
@@ -534,4 +538,4 @@ To install all of the development and test requirements:
|
||||
pip install -e .
|
||||
pip install -r dev_requirements.txt -r test_requirements.txt
|
||||
|
||||
To add JBIG2 encoding, see `Installing the JBIG2 encoder`_.
|
||||
To add JBIG2 encoding, see :ref:`jbig2`.
|
||||
|
||||
+3
-1
@@ -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.
|
||||
|
||||
|
||||
+52
-47
@@ -13,6 +13,45 @@ 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.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 +120,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 +394,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 +409,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 +416,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 +436,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 +443,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 +465,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 +491,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 +522,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 +539,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 +558,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 +607,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 +615,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 +628,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 +652,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 +671,7 @@ v4.0.2
|
||||
------
|
||||
|
||||
Fixes
|
||||
^^^^^
|
||||
|
||||
|
||||
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
|
||||
orientation information
|
||||
@@ -666,7 +683,7 @@ v4.0.1
|
||||
------
|
||||
|
||||
Fixes
|
||||
^^^^^
|
||||
|
||||
|
||||
- Fixed a KeyError if tesseract fails to find page orientation information
|
||||
|
||||
@@ -675,7 +692,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 +701,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 +709,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 +724,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 +733,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 +744,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 +753,6 @@ v3.1.1
|
||||
------
|
||||
|
||||
Changes
|
||||
^^^^^^^
|
||||
|
||||
- Fixed bug that caused incorrect page size and DPI calculations on documents with mixed page sizes
|
||||
|
||||
@@ -750,7 +760,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 +777,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 +799,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 +835,7 @@ Changes
|
||||
.. _Unpaper: https://github.com/Flameeyes/unpaper
|
||||
.. _JHOVE: http://jhove.sourceforge.net/
|
||||
|
||||
Release candidates
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
Release candidates^
|
||||
|
||||
- rc9:
|
||||
|
||||
@@ -897,12 +903,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
@@ -3,7 +3,7 @@
|
||||
# installation
|
||||
cffi == 1.11.5
|
||||
img2pdf == 0.3.0
|
||||
pikepdf == 0.3.0
|
||||
pikepdf == 0.3.3
|
||||
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
|
||||
pycparser == 2.18
|
||||
python-xmp-toolkit == 2.0.1
|
||||
|
||||
@@ -205,13 +205,14 @@ tests_require = open('test_requirements.txt').read().splitlines()
|
||||
|
||||
|
||||
def readme():
|
||||
with open('README.rst') as f:
|
||||
with open('README.md') 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'
|
||||
}
|
||||
)
|
||||
|
||||
+26
-10
@@ -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
|
||||
@@ -598,18 +597,24 @@ 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 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
|
||||
@@ -751,6 +756,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 +841,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 +866,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.)
|
||||
|
||||
@@ -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(
|
||||
|
||||
+64
-27
@@ -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,54 @@ 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:
|
||||
# or not isinstance(item, pikepdf.Dictionary):
|
||||
# # If there is garbage data, replace the key with an indirect
|
||||
# # ref to None. Kodak Capture Desktop produces keys like these.
|
||||
# log.error('Removing invalid reference from TOC: %s', repr(item))
|
||||
# node[key] = pdf_base.make_indirect(None)
|
||||
continue
|
||||
objgen = item.objgen
|
||||
if objgen not in visited:
|
||||
queue.add(objgen)
|
||||
|
||||
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 +211,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 +278,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, lambda *args: None, log)
|
||||
|
||||
procset = pdf_base.make_indirect(
|
||||
pikepdf.Object.parse(b'[ /PDF /Text /ImageB /ImageC /ImageI ]'))
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,7 +32,7 @@ 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):
|
||||
|
||||
+66
-14
@@ -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
@@ -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=""/> 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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user