Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efa7ea4fde | ||
|
|
137a6e45f5 | ||
|
|
29116e1dec | ||
|
|
87193335b9 | ||
|
|
cfd4f8a850 | ||
|
|
eaa324939f | ||
|
|
ef70e538f7 | ||
|
|
b7b912e56a | ||
|
|
4615cf2f1e | ||
|
|
eaf772f80a | ||
|
|
96ba75eabd | ||
|
|
fdfe52c1ad | ||
|
|
932b2e2a29 | ||
|
|
57e489c957 | ||
|
|
17a3fa671c | ||
|
|
2659afb4f6 | ||
|
|
7392115507 |
+14
-21
@@ -1,47 +1,40 @@
|
|||||||
# Development environment
|
# Development environment
|
||||||
|
.bash_history
|
||||||
|
.pylintrc
|
||||||
|
.pytest_cache/
|
||||||
|
.ruffus_history.sqlite
|
||||||
|
.venv/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.sublime-*
|
*.sublime-*
|
||||||
venv*/
|
|
||||||
.venv/
|
|
||||||
pyvenv.cfg
|
|
||||||
tasks.py
|
|
||||||
.bash_history
|
|
||||||
.ruffus_history.sqlite
|
|
||||||
.idea/
|
|
||||||
.pytest_cache/
|
|
||||||
.pylintrc
|
|
||||||
|
|
||||||
# Package building
|
# Package building
|
||||||
*.egg-info/
|
|
||||||
.cache/
|
|
||||||
.eggs/
|
.eggs/
|
||||||
|
*.egg-info/
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
wheelhouse/
|
wheelhouse/
|
||||||
|
|
||||||
# Automatically generated files
|
# Automatically generated files
|
||||||
ocrmypdf/lib/_*.py
|
|
||||||
ocrmypdf/version.py
|
|
||||||
docs/_build/
|
docs/_build/
|
||||||
docs/_static/
|
docs/_static/
|
||||||
docs/_templates/
|
docs/_templates/
|
||||||
docs/Makefile
|
docs/Makefile
|
||||||
|
ocrmypdf/lib/_*.py
|
||||||
|
|
||||||
# Code coverage
|
# Code coverage
|
||||||
.coverage
|
.coverage
|
||||||
htmlcov/
|
htmlcov/
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
log/
|
.ipynb_checkpoints/
|
||||||
|
.vscode/
|
||||||
|
*.ipynb
|
||||||
|
*.profile
|
||||||
/*.pdf
|
/*.pdf
|
||||||
/*.qdf
|
/*.qdf
|
||||||
*.ipynb
|
/scratch.py
|
||||||
.ipynb_checkpoints/
|
IDEAS
|
||||||
|
log/
|
||||||
tests/output/
|
tests/output/
|
||||||
tests/resources/private/
|
tests/resources/private/
|
||||||
tmp/
|
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.
|
|
||||||
@@ -13,6 +13,27 @@ Note that it is licensed under GPLv3, so scripts that ``import ocrmypdf`` and ar
|
|||||||
find: [^`]\#([0-9]{1,3})[^0-9]
|
find: [^`]\#([0-9]{1,3})[^0-9]
|
||||||
replace: `#$1 <https://github.com/jbarlow83/OCRmyPDF/issues/$1>`_
|
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
|
v7.0.5
|
||||||
------
|
------
|
||||||
|
|
||||||
@@ -99,6 +120,13 @@ 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>`_.
|
+ 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
|
v6.2.3
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
# installation
|
# installation
|
||||||
cffi == 1.11.5
|
cffi == 1.11.5
|
||||||
img2pdf == 0.3.0
|
img2pdf == 0.3.0
|
||||||
pikepdf == 0.3.2
|
pikepdf == 0.3.3
|
||||||
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
|
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
|
||||||
pycparser == 2.18
|
pycparser == 2.18
|
||||||
python-xmp-toolkit == 2.0.1
|
python-xmp-toolkit == 2.0.1
|
||||||
|
|||||||
@@ -205,13 +205,14 @@ tests_require = open('test_requirements.txt').read().splitlines()
|
|||||||
|
|
||||||
|
|
||||||
def readme():
|
def readme():
|
||||||
with open('README.rst') as f:
|
with open('README.md') as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='ocrmypdf',
|
name='ocrmypdf',
|
||||||
description='OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched',
|
description='OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched',
|
||||||
long_description=readme(),
|
long_description=readme(),
|
||||||
|
long_description_content_type='text/markdown',
|
||||||
url='https://github.com/jbarlow83/OCRmyPDF',
|
url='https://github.com/jbarlow83/OCRmyPDF',
|
||||||
author='James R. Barlow',
|
author='James R. Barlow',
|
||||||
author_email='jim@purplerock.ca',
|
author_email='jim@purplerock.ca',
|
||||||
@@ -250,7 +251,7 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
'cffi >= 1.9.1', # must be a setup and install requirement
|
'cffi >= 1.9.1', # must be a setup and install requirement
|
||||||
'img2pdf >= 0.2.4, < 0.4', # pure Python, so track HEAD closely
|
'img2pdf >= 0.2.4, < 0.4', # pure Python, so track HEAD closely
|
||||||
'pikepdf >= 0.3.2, < 0.4',
|
'pikepdf >= 0.3.3, < 0.4',
|
||||||
'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"',
|
'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"',
|
||||||
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
|
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
|
||||||
# block 5.1.0, broken wheels
|
# block 5.1.0, broken wheels
|
||||||
@@ -269,4 +270,10 @@ setup(
|
|||||||
},
|
},
|
||||||
package_data={'ocrmypdf': ['data/sRGB.icc']},
|
package_data={'ocrmypdf': ['data/sRGB.icc']},
|
||||||
include_package_data=True,
|
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/>.
|
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
from collections.abc import Sequence
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -36,7 +35,7 @@ import ruffus.proxy_logger as proxy_logger
|
|||||||
from ._jobcontext import JobContext, JobContextManager, cleanup_working_files
|
from ._jobcontext import JobContext, JobContextManager, cleanup_working_files
|
||||||
from ._pipeline import build_pipeline
|
from ._pipeline import build_pipeline
|
||||||
from .pdfa import file_claims_pdfa
|
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
|
available_cpu_count
|
||||||
from .exec import tesseract, qpdf, ghostscript
|
from .exec import tesseract, qpdf, ghostscript
|
||||||
from . import PROGRAM_NAME, VERSION
|
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."""
|
description of the error message that occurred."""
|
||||||
exit_code = None
|
exit_code = None
|
||||||
|
|
||||||
task_name, job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
|
_task_name, _job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
|
||||||
task_name = task_name # unused
|
|
||||||
job_name = job_name # unused
|
if isinstance(exc_name, type):
|
||||||
if exc_name == 'builtins.SystemExit':
|
# 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)
|
match = re.search(r"\.(.+?)\)", exc_value)
|
||||||
exit_code_name = match.groups()[0]
|
exit_code_name = match.groups()[0]
|
||||||
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
|
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
|
||||||
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
|
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
|
||||||
log.error(cleanup_ruffus_error_message(exc_value))
|
log.error(cleanup_ruffus_error_message(exc_value))
|
||||||
exit_code = ExitCode.input_file
|
exit_code = ExitCode.input_file
|
||||||
elif exc_name == 'builtins.KeyboardInterrupt':
|
elif exc_name in ('builtins.KeyboardInterrupt', 'KeyboardInterrupt'):
|
||||||
log.error("Interrupted by user")
|
# 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
|
exit_code = ExitCode.ctrl_c
|
||||||
elif exc_name == 'subprocess.CalledProcessError':
|
elif exc_name == 'subprocess.CalledProcessError':
|
||||||
# It's up to the subprocess handler to report something useful
|
# It's up to the subprocess handler to report something useful
|
||||||
@@ -751,6 +756,7 @@ def preamble(_log):
|
|||||||
_log.debug('ocrmypdf ' + VERSION)
|
_log.debug('ocrmypdf ' + VERSION)
|
||||||
_log.debug('tesseract ' + tesseract.version())
|
_log.debug('tesseract ' + tesseract.version())
|
||||||
_log.debug('qpdf ' + qpdf.version())
|
_log.debug('qpdf ' + qpdf.version())
|
||||||
|
_log.debug('gs ' + ghostscript.version())
|
||||||
|
|
||||||
|
|
||||||
def check_environ(options, _log):
|
def check_environ(options, _log):
|
||||||
@@ -835,9 +841,11 @@ def report_output_file_size(options, _log, input_file, output_file):
|
|||||||
""".format(ratio, explanation)))
|
""".format(ratio, explanation)))
|
||||||
|
|
||||||
|
|
||||||
def run_pipeline():
|
def run_pipeline(args=None):
|
||||||
options = parser.parse_args()
|
options = parser.parse_args(args=args)
|
||||||
options.verbose_abbreviated_path = 1
|
options.verbose_abbreviated_path = 1
|
||||||
|
if os.environ.get('_OCRMYPDF_THREADS'):
|
||||||
|
options.use_threads = True
|
||||||
|
|
||||||
if not check_closed_streams(options):
|
if not check_closed_streams(options):
|
||||||
return ExitCode.bad_args
|
return ExitCode.bad_args
|
||||||
@@ -858,6 +866,14 @@ def run_pipeline():
|
|||||||
"security vulnerabilities with certain malformed PDFs. Consider "
|
"security vulnerabilities with certain malformed PDFs. Consider "
|
||||||
"upgrading to version 7.0.0 or newer.".format(qpdf.version()))
|
"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
|
# Any changes to options will not take effect for options that are already
|
||||||
# bound to function parameters in the pipeline. (For example
|
# bound to function parameters in the pipeline. (For example
|
||||||
# options.input_file, options.pdf_renderer are already bound.)
|
# options.input_file, options.pdf_renderer are already bound.)
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ def repair_and_parse_pdf(
|
|||||||
copyfile(input_file, output_file)
|
copyfile(input_file, output_file)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pdfinfo = PdfInfo(output_file)
|
pdfinfo = PdfInfo(output_file, log=log)
|
||||||
except pikepdf.PasswordError as e:
|
except pikepdf.PasswordError as e:
|
||||||
raise EncryptedPdfError()
|
raise EncryptedPdfError()
|
||||||
except pikepdf.PdfError as e:
|
except pikepdf.PdfError as e:
|
||||||
@@ -789,8 +789,8 @@ def generate_postscript_stub(
|
|||||||
v.encode('ascii', errors='strict')
|
v.encode('ascii', errors='strict')
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
log.warning(
|
log.warning(
|
||||||
"Ghostscript 9.24 does not support Unicode strings in metadata."
|
"Ghostscript 9.24+ does not support Unicode strings in "
|
||||||
" These will be converted to ASCII if possible."
|
" metadata. These will be converted to ASCII if possible."
|
||||||
)
|
)
|
||||||
|
|
||||||
generate_pdfa_ps(output_file, pdfmark, ascii_docinfo=ascii_docinfo)
|
generate_pdfa_ps(output_file, pdfmark, ascii_docinfo=ascii_docinfo)
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ def version():
|
|||||||
|
|
||||||
|
|
||||||
def jpeg_passthrough_available():
|
def jpeg_passthrough_available():
|
||||||
"""
|
"""Returns True if the installed version of Ghostscript supports JPEG passthru
|
||||||
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
|
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
|
it gained the ability to keep JPEGs unmodified. However, the 9.23
|
||||||
@@ -42,7 +41,8 @@ def jpeg_passthrough_available():
|
|||||||
https://bugs.ghostscript.com/show_bug.cgi?id=699216
|
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 issue was fixed for 9.24, hence that is the first version we consider
|
||||||
the feature available.
|
the feature available. (However, we don't use 9.24 at all, so the first
|
||||||
|
version that allows JPEG passthrough is 9.25.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return version() >= '9.24'
|
return version() >= '9.24'
|
||||||
@@ -53,8 +53,7 @@ def _gs_error_reported(stream):
|
|||||||
|
|
||||||
|
|
||||||
def extract_text(input_file, pageno=1):
|
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
|
For details on options of -dTextFormat see
|
||||||
https://www.ghostscript.com/doc/current/VectorDevices.htm#TXT
|
https://www.ghostscript.com/doc/current/VectorDevices.htm#TXT
|
||||||
@@ -65,10 +64,18 @@ def extract_text(input_file, pageno=1):
|
|||||||
<span bbox="left top right bottom" font="..." size="...">
|
<span bbox="left top right bottom" font="..." size="...">
|
||||||
<char bbox="...." c="X"/>
|
<char bbox="...." c="X"/>
|
||||||
|
|
||||||
|
:param pageno: number of page to extract, or all pages if None
|
||||||
:return: XML-ish text representation in bytes
|
:return: XML-ish text representation in bytes
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if pageno is not None:
|
||||||
|
pages = [
|
||||||
|
'-dFirstPage=%i' % pageno,
|
||||||
|
'-dLastPage=%i' % pageno
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
pages = []
|
||||||
|
|
||||||
args_gs = [
|
args_gs = [
|
||||||
'gs',
|
'gs',
|
||||||
'-dQUIET',
|
'-dQUIET',
|
||||||
@@ -77,10 +84,9 @@ def extract_text(input_file, pageno=1):
|
|||||||
'-dNOPAUSE',
|
'-dNOPAUSE',
|
||||||
'-sDEVICE=txtwrite',
|
'-sDEVICE=txtwrite',
|
||||||
'-dTextFormat=0',
|
'-dTextFormat=0',
|
||||||
'-dFirstPage=%i' % pageno,
|
] + pages + [
|
||||||
'-dLastPage=%i' % pageno,
|
|
||||||
'-o', '-',
|
'-o', '-',
|
||||||
input_file
|
fspath(input_file)
|
||||||
]
|
]
|
||||||
|
|
||||||
p = run(args_gs, stdout=PIPE, stderr=PIPE)
|
p = run(args_gs, stdout=PIPE, stderr=PIPE)
|
||||||
@@ -96,8 +102,7 @@ def extract_text(input_file, pageno=1):
|
|||||||
|
|
||||||
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
||||||
pageno=1, page_dpi=None, rotation=None):
|
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
|
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
|
(xres, yres) even if those numbers are noninteger. The image's DPI will
|
||||||
@@ -179,6 +184,23 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
|
|||||||
|
|
||||||
def generate_pdfa(pdf_pages, output_file, compression, log,
|
def generate_pdfa(pdf_pages, output_file, compression, log,
|
||||||
threads=1, pdf_version='1.5', pdfa_part='2'):
|
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 = []
|
compression_args = []
|
||||||
if compression == 'jpeg':
|
if compression == 'jpeg':
|
||||||
compression_args = [
|
compression_args = [
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ def _encode_ascii(s: str) -> str:
|
|||||||
'(': '',
|
'(': '',
|
||||||
')': '',
|
')': '',
|
||||||
'\\': '',
|
'\\': '',
|
||||||
|
'\0': ''
|
||||||
})
|
})
|
||||||
return s.translate(trans).encode('ascii', errors='replace').decode()
|
return s.translate(trans).encode('ascii', errors='replace').decode()
|
||||||
|
|
||||||
@@ -284,7 +285,7 @@ def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
|
|||||||
hex_icc_profile = hexlify(bytes_icc_profile)
|
hex_icc_profile = hexlify(bytes_icc_profile)
|
||||||
icc_profile = '<' + hex_icc_profile.decode('ascii') + '>'
|
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
|
# We should have encoded everything to pure ASCII by this point, and
|
||||||
# to be safe, only allow ASCII in PostScript
|
# to be safe, only allow ASCII in PostScript
|
||||||
|
|||||||
+49
-29
@@ -16,12 +16,14 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with OCRmyPDF. If not, see <http://www.gnu.org/licenses/>.
|
# 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 collections import namedtuple
|
||||||
from pathlib import Path
|
from decimal import Decimal
|
||||||
from enum import Enum
|
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 .exec import ghostscript
|
||||||
from .helpers import fspath
|
from .helpers import fspath
|
||||||
@@ -486,27 +488,13 @@ def _find_images(*, pdf, container, shorthand=None):
|
|||||||
yield from _find_form_xobject_images(pdf, container, contentsinfo)
|
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"""
|
"""Smarter text detection"""
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
gstext = ghostscript.extract_text(infile, pageno+1)
|
root = xmltext
|
||||||
|
if not hasattr(xmltext, 'findall'):
|
||||||
# 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() == '':
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
try:
|
|
||||||
root = ET.fromstring(gstext)
|
|
||||||
except ET.ParseError as e:
|
|
||||||
return [] # If we can't parse, assume none...
|
|
||||||
|
|
||||||
def blocks():
|
def blocks():
|
||||||
for span in root.findall('.//span'):
|
for span in root.findall('.//span'):
|
||||||
bbox_str = span.attrib['bbox']
|
bbox_str = span.attrib['bbox']
|
||||||
@@ -565,14 +553,15 @@ def _page_has_text(text_blocks, page_width, page_height):
|
|||||||
return has_text
|
return has_text
|
||||||
|
|
||||||
|
|
||||||
def _pdf_get_pageinfo(pdf, pageno: int, infile):
|
def _pdf_get_pageinfo(pdf, pageno: int, infile, xmltext):
|
||||||
pageinfo = {}
|
pageinfo = {}
|
||||||
pageinfo['pageno'] = pageno
|
pageinfo['pageno'] = pageno
|
||||||
pageinfo['images'] = []
|
pageinfo['images'] = []
|
||||||
|
|
||||||
page = pdf.pages[pageno]
|
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()]
|
mediabox = [Decimal(d) for d in page.MediaBox.as_list()]
|
||||||
width_pt = mediabox[2] - mediabox[0]
|
width_pt = mediabox[2] - mediabox[0]
|
||||||
@@ -609,16 +598,47 @@ def _pdf_get_pageinfo(pdf, pageno: int, infile):
|
|||||||
return pageinfo
|
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)
|
pdf = pikepdf.open(infile)
|
||||||
return [PageInfo(pdf, n, infile) for n in range(len(pdf.pages))], pdf
|
|
||||||
|
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:
|
class PageInfo:
|
||||||
def __init__(self, pdf, pageno, infile):
|
def __init__(self, pdf, pageno, infile, xmltext):
|
||||||
self._pageno = pageno
|
self._pageno = pageno
|
||||||
self._infile = infile
|
self._infile = infile
|
||||||
self._pageinfo = _pdf_get_pageinfo(pdf, pageno, infile)
|
self._pageinfo = _pdf_get_pageinfo(pdf, pageno, infile, xmltext)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pageno(self):
|
def pageno(self):
|
||||||
@@ -695,9 +715,9 @@ class PdfInfo:
|
|||||||
"""Get summary information about a PDF
|
"""Get summary information about a PDF
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, infile):
|
def __init__(self, infile, log=None):
|
||||||
self._infile = infile
|
self._infile = infile
|
||||||
self._pages, pdf = _pdf_get_all_pageinfo(infile)
|
self._pages, pdf = _pdf_get_all_pageinfo(infile, log=log)
|
||||||
self._needs_rendering = pdf.root.get('/NeedsRendering', False)
|
self._needs_rendering = pdf.root.get('/NeedsRendering', False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -20,11 +20,11 @@
|
|||||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
"""Replicate Ghostscript render failure while allowing rasterizing"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
"""Replicate Ghostscript render failure while allowing rasterizing"""
|
|
||||||
|
|
||||||
|
|
||||||
def real_ghostscript(argv):
|
def real_ghostscript(argv):
|
||||||
|
|||||||
Reference in New Issue
Block a user