Compare commits

...
25 Commits
Author SHA1 Message Date
James R. Barlow fae2119b1e v4.5.6 release notes 2017-05-01 15:55:02 -07:00
James R. Barlow aa859a4139 Fix #156 - NoneType has no ‘getObject’ for pages with no /Contents 2017-05-01 15:46:15 -07:00
James R. Barlow b9b12e2879 Ensure that ocrmypdf stops and reports an error if Ghostscript fails
Past behavior was to continue and let ruffus puke eventually
2017-05-01 15:44:21 -07:00
James R. Barlow cf643c9f43 Fix argparse.ArgumentError needs two positional args 2017-05-01 14:55:47 -07:00
James R. Barlow 5b1a7880a9 Switch to Travis triggering Docker build to skip race condition with PyPI 2017-04-28 17:18:01 -07:00
James R. Barlow 474b6b0500 v4.5.5 release notes 2017-04-28 15:27:41 -07:00
James R. Barlow 6c8c1d8173 Fix #154: KeyError ‘/Contents’ on blank pages with /Contents record 2017-04-28 15:22:05 -07:00
James R. Barlow 6a91fa637f Squash merge improvements to auto-homebrewing macOS version 2017-04-28 15:17:38 -07:00
Jakub Wilkandjbarlow83 2846fb4e31 Remove misplaced flags from re.sub() call (#153)
The 4th argument of re.sub() is maximum number of substitutions,
not flags.

Moreover, re.MULTILINE affects only semantics of ^ and $,
so it wouldn't have any effect on this regular expression.
2017-04-20 00:38:23 -07:00
James R. Barlow a1033cdc64 osx_brew: show output before letting “brew audit” check it 2017-04-18 23:38:07 -07:00
James R. Barlow 204336e1a5 Move release notes into the rest of documentation 2017-04-18 18:26:31 -07:00
James R. Barlow 8954e6c3b9 v4.5.4 Update release notes 2017-04-18 18:09:15 -07:00
James R. Barlow fee22b6b0b Fix #151, cannot write mode P as JPEG
all(<empty generator>) is True.
2017-04-18 18:08:45 -07:00
James R. Barlow 2b82c31b85 Update documentation 2017-04-18 18:07:19 -07:00
James R. Barlow 9a4813089c autobrew: remove homebrew dependency “zlib”, causes audit failure 2017-04-18 15:28:30 -07:00
James R. Barlow 554fcc8b9d Add test case for #152 2017-04-18 15:20:25 -07:00
Tomandjbarlow83 345256ee99 Fix --skip-big when there are no images in pdf (#152)
* fixed skip-big when there are no images in pdf

* added only_text pdf

* updated only_text pdf

* Delete only_text.pdf
2017-04-18 15:19:24 -07:00
James R. Barlow 58d1042147 v4.5.3 release notes update 2017-03-29 23:50:31 -07:00
James R. Barlow 7b7e3a3e03 Enable lossless reconstruction for —pdf-renderer tess4 where appropriate 2017-03-29 23:44:12 -07:00
James R. Barlow 1e7fbd4202 Fix issues with —pdf-renderer tess4 page skipping
If tess4 renderer needed to skip OCR on a page it would end up
duplicating the page contents onto the new page, rather than creating
a blank OCR layer and placing it on the output page. This created
duplicated content in output files.
2017-03-29 23:43:26 -07:00
James R. Barlow 4a9e9e9db2 v4.5.3 release notes 2017-03-29 13:19:34 -07:00
James R. Barlow 88ef2718f1 Reject high Unicode metadata at command line
Ghostscript 9.21 does not seem to accept Unicode above U+FFFF. Previous
versions did, but it now exits with a rangecheck error (-15).

Reject on the command line for now. Complete fix would also need to
check input PDF’s metadata.
2017-03-28 11:08:38 -07:00
James R. Barlow e71e8ca3ad Workaround for GS VMerror -25 bug
Avoid inserting docinfo keys that would be translated to null strings,
to avoid running afoul of
https://bugs.ghostscript.com/show_bug.cgi?id=697684
2017-03-28 11:05:43 -07:00
James R. Barlow 45e9257d6e Don’t use filename “pdfa_def.ps” for GS file
At recommendation of Artifex people, don’t use the filename pdfa_def.ps
because if given without a full path Ghostscript will prioritize
searching its own resources over the current directory, and will pick up
 the example file pdfa_def.ps instead.  We used the full path anyway,
but this is a prudent defensive change to avoid odd Ghostscript behavior.
2017-03-28 11:02:46 -07:00
James R. Barlow 2954e72652 Some examples of Ghostscript and Tesseract warnings/errors were not tagged properly 2017-03-28 10:59:53 -07:00
23 changed files with 503 additions and 147 deletions
+1
View File
@@ -6,6 +6,7 @@ pyvenv.cfg
tasks.py tasks.py
.bash_history .bash_history
.ruffus_history.sqlite .ruffus_history.sqlite
.idea/
# Package building # Package building
*.egg-info/ *.egg-info/
+33 -11
View File
@@ -21,16 +21,23 @@ matrix:
sudo: required sudo: required
language: python language: python
python: 3.6 python: 3.6
env:
- secure: "oyX5xesoHD7qcDXKIxMyGZPi+H/WxcvfFkaviEmq84K1DDyHk48+9e92IKgrw8/lcTADnEo/AgVKfnhCPflFimk1xTkgaK4sUg1WLI2YjmaHcwl5SlBHa2rN3uGBwy1hyP92qyv/mMc9R59NtRJ8u76lbn6eN9wi7lkFWdE6BTw=" # DOCKERHUB_OCRMYPDF_TOKEN
- os: osx - os: osx
osx_image: xcode8 osx_image: xcode8
language: generic language: generic
env:
- secure: "hsf6MT+n2x3OiDM2fQyJZdV0/PWYmv81LdVqC6cfnHBE/8N3DloJRqQ7WfO14TxhiK9PEC7MpyCj0lSabUHEO7gSH6Vks6I1asoSkt8S9/bSMlhT4hei+pwVpeGEiU5xHVATNjY+D919VC3IFvc3XmjT74h/2SLhaZ+jhEmDggM=" # HOMEBREW_OCRMYPDF_TOKEN
before_cache: before_cache:
- rm -f $HOME/.cache/pip/log/debug.log - rm -f $HOME/.cache/pip/log/debug.log
before_install: before_install: |
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then bash .travis/linux_before_install.sh ; fi if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then bash .travis/osx_before_install.sh ; fi bash .travis/linux_before_install.sh
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
bash .travis/osx_before_install.sh
fi
install: install:
- pip3 install . - pip3 install .
@@ -41,20 +48,35 @@ script:
- pytest - pytest
- mv dont_import_this_ocrmypdf ocrmypdf - mv dont_import_this_ocrmypdf ocrmypdf
after_success:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then bash .travis/osx_brew.sh ; fi
# See https://www.appneta.com/blog/pypi-deployment-with-travis-ci/ for
# steps to set up testpypi deploy for untagged builds if desired
deploy: deploy:
provider: pypi # release for main pypi
- provider: pypi
user: ocrmypdf-travis user: ocrmypdf-travis
password: password:
secure: DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo= secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
distributions: "sdist bdist_wheel" distributions: "sdist bdist_wheel"
on: on:
branch: master branch: master
tags: true tags: true
condition: $TRAVIS_PYTHON_VERSION == "3.6" && $TRAVIS_OS_NAME == "linux" condition: $TRAVIS_PYTHON_VERSION == "3.6" && $TRAVIS_OS_NAME == "linux"
skip_upload_docs: true skip_upload_docs: true
# test pypi
- provider: pypi
server: https://testpypi.python.org/pypi
user: ocrmypdf-travis
password:
secure: "DTFOmmNL6olA0+yXvp4u9jXZlZeqrJsJ0526jzqf4a3gZ6jnGTq5UI6WzRsslSyoMMfXKtHQebqHM6ogSgCZinyZ3ufHJo8fn9brxbEc2gsiWkbj5o3bGwdWMT1vNNE7XW0VCpw87rZ1EEwjl4FJHFudMlPR1yfU5+uq0k0PACo="
distributions: "sdist"
on:
branch: develop
tags: false
condition: $TRAVIS_OS_NAME == "osx"
skip_upload_docs: true
after_deploy: |
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
bash .travis/osx_brew.sh
elif [[ "$TRAVIS_PYTHON_VERSION" == "3.6" && "$TRAVIS_OS_NAME" == "linux" ]]; then
curl -H "Content-Type: application/json" --data '{"build": true}' -X POST https://registry.hub.docker.com/u/jbarlow83/ocrmypdf/trigger/$DOCKERHUB_OCRMYPDF_TOKEN/
fi
-1
View File
@@ -13,7 +13,6 @@ class Ocrmypdf < Formula
depends_on :python3 depends_on :python3
depends_on "pkg-config" => :build depends_on "pkg-config" => :build
depends_on "zlib"
depends_on "libffi" depends_on "libffi"
depends_on "tesseract" depends_on "tesseract"
depends_on "ghostscript" depends_on "ghostscript"
+13 -1
View File
@@ -4,5 +4,17 @@ set -x
pip3 install homebrew-pypi-poet pip3 install homebrew-pypi-poet
python3 .travis/autobrew.py python3 .travis/autobrew.py
brew audit ocrmypdf.rb
cat ocrmypdf.rb cat ocrmypdf.rb
brew audit ocrmypdf.rb
# Important: disable debug output so token is hidden
set +x
git clone https://$HOMEBREW_OCRMYPDF_TOKEN@github.com/jbarlow83/homebrew-ocrmypdf.git
set -x
pushd homebrew-ocrmypdf
cp ../ocrmypdf.rb Formula/ocrmypdf.rb
git add Formula/ocrmypdf.rb
git commit -m "homebrew-ocrmypdf: automatic release $TRAVIS_BUILD_NUMBER $TRAVIS_TAG"
git push origin master
popd
+1 -1
View File
@@ -35,7 +35,7 @@ Main features
- Supports more than `100 languages <https://github.com/tesseract-ocr/tessdata>`_ recognized by Tesseract - 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 - Battle-tested on thousands of PDFs, a test suite and continuous integration
For details: please consult the `release notes <RELEASE_NOTES.rst>`_. For details: please consult the `documentation <https://ocrmypdf.readthedocs.io/en/latest/>`_.
Motivation Motivation
---------- ----------
+7 -4
View File
@@ -11,14 +11,18 @@ be searched.
PDFs are the best format for scanned documents. Unfortunately, PDFs can be difficult to work with. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs. PDFs are the best format for scanned documents. Unfortunately, PDFs can be difficult to work with. OCRmyPDF makes it easy to apply image processing and OCR to existing PDFs.
Contents:
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 1
introduction introduction
release_notes
installation installation
languages languages
.. toctree::
:caption: Usage
:maxdepth: 2
cookbook cookbook
advanced advanced
batch batch
@@ -26,7 +30,6 @@ Contents:
errors errors
Indices and tables Indices and tables
================== ==================
+26 -1
View File
@@ -53,6 +53,20 @@ By default, OCRmyPDF will convert the file to a PDF/A. This behavior can be dis
Depending on the settings selected, OCRmyPDF may "graft" the OCR layer into the existing PDF, or reconstruct a visually equivalent new PDF. Depending on the settings selected, OCRmyPDF may "graft" the OCR layer into the existing PDF, or reconstruct a visually equivalent new PDF.
Why you shouldn't do this manually
----------------------------------
There are two routes to manually applying OCR to an existing PDF, both of which destroy information in the original PDF.
1. Rasterize each page as an image, OCR the images, and combine the output into a PDF. This preserves the appearance of each page, but resamples all images (possibly losing quality, increasing file size, introducing compression artifacts, etc.)
2. Extract each image, OCR, and combine the output into a PDF. This loses the context in which images are used in the PDF, meaning that cropping, rotation and scaling of pages may be lost. Some PDFs use multiple images per page with stencil masks, which would quite difficult to reassemble correctly. This also loses and text or vector art on any pages in a PDF with both scanned and pure digital content.
In the case of a PDF that is nothing other than a container of images (no rotation, scaling, cropping, one image per page), the second approach is can be lossless.
OCRmyPDF uses several strategies depending on input options and the input PDF itself, but generally speaking it rasterizes a page for OCR and then grafts the OCR back onto the original. As such it can handle complex PDFs and still preserve their contents as much as possible.
Limitations Limitations
----------- -----------
@@ -73,4 +87,15 @@ Ghostscript also imposes some limitations:
* PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled. * PDFs containing JBIG2-encoded content will be converted to CCITT Group4 encoding, which has lower compression ratios, if Ghostscript PDF/A is enabled.
OCRmyPDF is currently not designed to be used as a Python API; it is designed to be run as a command line tool. ``import ocrmypf`` currently attempts to process the command line on ``sys.argv`` at import time so it has side effects that will interfere with its use as a package. The API it presents should not be considered stable. OCRmyPDF is currently not designed to be used as a Python API; it is designed to be run as a command line tool. ``import ocrmypf`` currently attempts to process the command line on ``sys.argv`` at import time so it has side effects that will interfere with its use as a package. The API it presents should not be considered stable.
Similar programs
----------------
To the author's knowledge, OCRmyPDF is the most feature-rich and thoroughly tested command line OCR PDF conversion tool. If it doesn't meet your needs, consider of these similar open source programs:
* pdf2pdfocr
* pdfsandwich
* pypdfocr
* pdfbeads
+85 -54
View File
@@ -1,11 +1,42 @@
RELEASE NOTES Release notes
============= =============
OCRmyPDF uses `semantic versioning <http://semver.org/>`_. OCRmyPDF uses `semantic versioning <http://semver.org/>`_.
v4.5.6
------
- Fixed issue #156, 'NoneType' object has no attribute 'getObject' on pages with no optional /Contents record. This should resolve all issues related to pages with no /Contents record.
- Fixed issue #158, ocrmypdf now stops and terminates if Ghostscript fails on an intermediate step, as it is not possible to proceed.
- Fixed issue #160, exception thrown on certain invalid arguments instead of error message
v4.5.5
------
- Automated update of macOS homebrew tap
- Fixed issue #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) (thanks to @TomRaz)
- Fix an issue where pages with no images might trigger "cannot write mode P as JPEG" (#151)
v4.5.3
------
- Added a workaround for Ghostscript 9.21 and probably earlier versions would fail with the error message "VMerror -25", due to a Ghostscript bug in XMP metadata handling
- High Unicode characters (U+10000 and up) are no longer accepted for setting metadata on the command line, as Ghostscript may not handle them correctly.
- 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 v4.5.2
====== ------
- Fix issue #147. ``--pdf-renderer tess4 --clean`` will produce an oversized page containing the original image in the bottom left corner, due to loss DPI information. - Fix issue #147. ``--pdf-renderer tess4 --clean`` will produce an oversized page containing the original image in the bottom left corner, due to loss DPI information.
- Make "using Tesseract 4.0" warning less ominous - Make "using Tesseract 4.0" warning less ominous
@@ -13,13 +44,13 @@ v4.5.2
v4.5.1 v4.5.1
====== ------
- Fix issue #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 - Fix issue #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 v4.5
==== ----
- Exotic PDFs containing "Form XObjects" are now supported (issue #134; PDF reference manual 8.10), and images they contain are taken into account when determining the resolution for rasterizing - Exotic PDFs containing "Form XObjects" are now supported (issue #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 - The Tesseract 4 Docker image no longer includes all languages, because it took so long to build something would tend to fail
@@ -27,7 +58,7 @@ v4.5
v4.4.2 v4.4.2
====== ------
- The Docker images (ocrmypdf, ocrmypdf-polyglot, ocrmypdf-tess4) are now based on Ubuntu 16.10 instead of Debian stretch - The Docker images (ocrmypdf, ocrmypdf-polyglot, ocrmypdf-tess4) are now based on Ubuntu 16.10 instead of Debian stretch
@@ -38,7 +69,7 @@ v4.4.2
v4.4.1 v4.4.1
====== ------
- To prevent a `TIFF output error <https://github.com/python-pillow/Pillow/issues/2206>`_ caused by img2pdf >= 0.2.1 and Pillow <= 3.4.2, dependencies have been tightened - To prevent a `TIFF output error <https://github.com/python-pillow/Pillow/issues/2206>`_ caused by img2pdf >= 0.2.1 and Pillow <= 3.4.2, dependencies have been tightened
- The Tesseract 4.00 simultaneous process limit was increased from 1 to 2, since it was observed that 1 lowers performance - The Tesseract 4.00 simultaneous process limit was increased from 1 to 2, since it was observed that 1 lowers performance
@@ -47,7 +78,7 @@ v4.4.1
- Tweaks to setup.py to deal with issues in the v4.4 release - Tweaks to setup.py to deal with issues in the v4.4 release
v4.4 v4.4
==== ----
- Tesseract 4.00 is now supported on an experimental basis. - Tesseract 4.00 is now supported on an experimental basis.
@@ -65,32 +96,32 @@ v4.4
v4.3.5 v4.3.5
====== ------
- Update documentation to confirm Python 3.6.0 compatibility. No code changes were needed, so many earlier versions are likely supported. - Update documentation to confirm Python 3.6.0 compatibility. No code changes were needed, so many earlier versions are likely supported.
v4.3.4 v4.3.4
====== ------
- Fixed "decimal.InvalidOperation: quantize result has too many digits" for high DPI images - Fixed "decimal.InvalidOperation: quantize result has too many digits" for high DPI images
v4.3.3 v4.3.3
====== ------
- Fixed PDF/A creation with Ghostscript 9.20 properly - Fixed PDF/A creation with Ghostscript 9.20 properly
- Fixed an exception on inline stencil masks with a missing optional parameter - Fixed an exception on inline stencil masks with a missing optional parameter
v4.3.2 v4.3.2
====== ------
- Fixed a PDF/A creation issue with Ghostscript 9.20 (note: this fix did not actually work) - Fixed a PDF/A creation issue with Ghostscript 9.20 (note: this fix did not actually work)
v4.3.1 v4.3.1
====== ------
- Fixed an issue where pages produced by the "hocr" renderer after a Tesseract timeout would be rotated incorrectly if the input page was rotated with a /Rotate marker - Fixed an issue where pages produced by the "hocr" renderer after a Tesseract timeout would be rotated incorrectly if the input page was rotated with a /Rotate marker
- Fixed a file handle leak in LeptonicaErrorTrap that would cause a "too many open files" error for files around hundred pages of pages long when ``--deskew`` or ``--remove-background`` or other Leptonica based image processing features were in use, depending on the system value of ``ulimit -n`` - Fixed a file handle leak in LeptonicaErrorTrap that would cause a "too many open files" error for files around hundred pages of pages long when ``--deskew`` or ``--remove-background`` or other Leptonica based image processing features were in use, depending on the system value of ``ulimit -n``
@@ -101,7 +132,7 @@ v4.3.1
v4.3 v4.3
==== ----
- New feature ``--remove-background`` to detect and erase the background of color and grayscale images - New feature ``--remove-background`` to detect and erase the background of color and grayscale images
- Better documentation - Better documentation
@@ -112,20 +143,20 @@ v4.3
+ Some output validation is disabled in this mode + Some output validation is disabled in this mode
v4.2.5 v4.2.5
====== ------
- Fixed an issue (#100) with PDFs that omit the optional /BitsPerComponent parameter on images - Fixed an issue (#100) with PDFs that omit the optional /BitsPerComponent parameter on images
- Removed non-free file milk.pdf - Removed non-free file milk.pdf
v4.2.4 v4.2.4
====== ------
- Fixed an error (#90) caused by PDFs that use stencil masks properly - Fixed an error (#90) caused by PDFs that use stencil masks properly
- Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI) - Fixed handling of PDFs that try to draw images or stencil masks without properly setting up the graphics state (such images are now ignored for the purposes of calculating DPI)
v4.2.3 v4.2.3
====== ------
- Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object - Fixed an issue with PDFs that store page rotation (/Rotate) in an indirect object
- Integrated a few fixes to simplify downstream packaging (Debian) - Integrated a few fixes to simplify downstream packaging (Debian)
@@ -139,20 +170,20 @@ v4.2.3
v4.2.2 v4.2.2
====== ------
- Improvements to documentation - Improvements to documentation
v4.2.1 v4.2.1
====== ------
- Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort - Fixed an issue where PDF pages that contained stencil masks would report an incorrect DPI and cause Ghostscript to abort
- Implemented stdin streaming - Implemented stdin streaming
v4.2 v4.2
==== ----
- ocrmypdf will now try to convert single image files to PDFs if they are provided as input (#15) - ocrmypdf will now try to convert single image files to PDFs if they are provided as input (#15)
@@ -185,13 +216,13 @@ v4.2
- Ghostscript now runs in "safer" mode where possible - Ghostscript now runs in "safer" mode where possible
v4.1.4 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 - 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 v4.1.3
====== ------
- More helpful error message for PDFs with version 4 security handler - More helpful error message for PDFs with version 4 security handler
- Update usage instructions for Windows/Docker users - Update usage instructions for Windows/Docker users
@@ -200,14 +231,14 @@ v4.1.3
v4.1.2 v4.1.2
====== ------
- Replace IEC sRGB ICC profile with Debian's sRGB (from icc-profiles-free) which is more compatible with the MIT license - 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 - More helpful error message for an error related to certain types of malformed PDFs
v4.1 v4.1
==== ----
- ``--rotate-pages`` now only rotates pages when reasonably confidence in the orientation. This behavior can be adjusted with the new argument ``--rotate-pages-threshold`` - ``--rotate-pages`` now only rotates pages when reasonably confidence in the orientation. This behavior can be adjusted with the new argument ``--rotate-pages-threshold``
- Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time - Fixed problems in error checking if ``unpaper`` is uninstalled or missing at run-time
@@ -215,20 +246,20 @@ v4.1
v4.0.7 v4.0.7
====== ------
- Minor correction to Ghostscript output settings - Minor correction to Ghostscript output settings
v4.0.6 v4.0.6
====== ------
- Update install instructions - Update install instructions
- Provide a sRGB profile instead of using Ghostscript's - Provide a sRGB profile instead of using Ghostscript's
v4.0.5 v4.0.5
====== ------
- Remove some verbose debug messages from v4.0.4 - Remove some verbose debug messages from v4.0.4
- Fixed temporary that wasn't being deleted - Fixed temporary that wasn't being deleted
@@ -236,22 +267,22 @@ v4.0.5
- Inline images are now checked during DPI calculation instead of rejecting the image - Inline images are now checked during DPI calculation instead of rejecting the image
v4.0.4 v4.0.4
====== ------
Released with verbose debug message turned on. Do not use. Skip to v4.0.5. Released with verbose debug message turned on. Do not use. Skip to v4.0.5.
v4.0.3 v4.0.3
====== ------
New features New features
------------ ^^^^^^^^^^^^
- Page orientations detected are now reported in a summary comment - Page orientations detected are now reported in a summary comment
Fixes Fixes
----- ^^^^^
- Show stack trace if unexpected errors occur - Show stack trace if unexpected errors occur
- Treat "too few characters" error message from Tesseract as a reason to skip that page rather than - Treat "too few characters" error message from Tesseract as a reason to skip that page rather than
@@ -260,10 +291,10 @@ Fixes
v4.0.2 v4.0.2
====== ------
Fixes Fixes
----- ^^^^^
- Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting - Fixed compatibility with Tesseract 3.04.01 release, particularly its different way of outputting
orientation information orientation information
@@ -272,19 +303,19 @@ Fixes
v4.0.1 v4.0.1
====== ------
Fixes Fixes
----- ^^^^^
- Fixed a KeyError if tesseract fails to find page orientation information - Fixed a KeyError if tesseract fails to find page orientation information
v4.0 v4.0
==== ----
New features New features
------------ ^^^^^^^^^^^^
- Automatic page rotation (``-r``) is now available. It uses ignores any prior rotation information - 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 on PDFs and sets rotation based on the dominant orientation of detectable text. This feature is
@@ -294,7 +325,7 @@ New features
Fixes Fixes
----- ^^^^^
- Fixed an issue where lossless reconstruction could cause some pages to be appear incorrectly - 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) if the page was rotated by the user in Acrobat after being scanned (specifically if it a /Rotate tag)
@@ -303,7 +334,7 @@ Fixes
Changes Changes
------- ^^^^^^^
- Logging output is now much easier to read - Logging output is now much easier to read
- ``--deskew`` is now performed by Leptonica instead of unpaper (#25) - ``--deskew`` is now performed by Leptonica instead of unpaper (#25)
@@ -316,20 +347,20 @@ Changes
v3.2.1 v3.2.1
====== ------
Changes Changes
------- ^^^^^^^
- Fixed issue #47 "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2 - Fixed issue #47 "convert() got and unexpected keyword argument 'dpi'" by upgrading to img2pdf 0.2
- Tweaked the Dockerfiles - Tweaked the Dockerfiles
v3.2 v3.2
==== ----
New features New features
------------ ^^^^^^^^^^^^
- Lossless reconstruction: when possible, OCRmyPDF will inject text layers without - 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 otherwise manipulating the content and layout of a PDF page. For example, a PDF containing a mix
@@ -341,25 +372,25 @@ New features
for the polyglots among us. It is much larger. for the polyglots among us. It is much larger.
Changes Changes
------- ^^^^^^^
- JPEG transcoding quality is now 95 instead of the default 75. Bigger file sizes for less degradation. - JPEG transcoding quality is now 95 instead of the default 75. Bigger file sizes for less degradation.
v3.1.1 v3.1.1
====== ------
Changes Changes
------- ^^^^^^^
- Fixed bug that caused incorrect page size and DPI calculations on documents with mixed page sizes - Fixed bug that caused incorrect page size and DPI calculations on documents with mixed page sizes
v3.1 v3.1
==== ----
Changes Changes
------- ^^^^^^^
- Default output format is now PDF/A-2b instead of PDF/A-1b - 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 - Python 3.5 and macOS El Capitan are now supported platforms - no changes were
@@ -374,10 +405,10 @@ Changes
- Set up Travis CI automatic integration testing - Set up Travis CI automatic integration testing
v3.0 v3.0
==== ----
New features New features
------------ ^^^^^^^^^^^^
- Easier installation with a Docker container or Python's ``pip`` package manager - Easier installation with a Docker container or Python's ``pip`` package manager
- Eliminated many external dependencies, so it's easier to setup - Eliminated many external dependencies, so it's easier to setup
@@ -400,7 +431,7 @@ New features
- Multiple images on the same PDF page are now supported - Multiple images on the same PDF page are now supported
Changes Changes
------- ^^^^^^^
- New, robust rewrite in Python 3.4+ with ruffus_ pipelines - New, robust rewrite in Python 3.4+ with ruffus_ pipelines
- Now uses Ghostscript 9.14's improved color conversion model to preserve PDF colors - Now uses Ghostscript 9.14's improved color conversion model to preserve PDF colors
@@ -438,7 +469,7 @@ Changes
.. _JHOVE: http://jhove.sourceforge.net/ .. _JHOVE: http://jhove.sourceforge.net/
Release candidates Release candidates
------------------ ^^^^^^^^^^^^^^^^^^
- rc9: - rc9:
@@ -506,12 +537,12 @@ where ``settings.txt`` contains *one argument per line*, for example:
Fixes Fixes
----- ^^^^^
- Handling of filenames containing spaces: fixed - Handling of filenames containing spaces: fixed
Notes and known issues Notes and known issues
---------------------- ^^^^^^^^^^^^^^^^^^^^^^
- Some dependencies may work with lower versions than tested, so try - Some dependencies may work with lower versions than tested, so try
overriding dependencies if they are "in the way" to see if they work. overriding dependencies if they are "in the way" to see if they work.
@@ -524,7 +555,7 @@ Notes and known issues
v2.2-stable (2014-09-29) v2.2-stable (2014-09-29)
======================== ------------------------
OCRmyPDF versions 1 and 2 were implemented as shell scripts. OCRmyPDF 3.0+ is a fork that gradually replaced all shell scripts with Python while maintaining the existing command line arguments. No one is maintaining old versions. OCRmyPDF versions 1 and 2 were implemented as shell scripts. OCRmyPDF 3.0+ is a fork that gradually replaced all shell scripts with Python while maintaining the existing command line arguments. No one is maintaining old versions.
+1 -1
View File
@@ -1,4 +1,4 @@
PDF Security Issues PDF security issues
=================== ===================
OCRmyPDF should only be used on PDFs you trust. It is not designed to protect you against malware. OCRmyPDF should only be used on PDFs you trust. It is not designed to protect you against malware.
+26 -4
View File
@@ -46,6 +46,10 @@ def complain(message):
print(*textwrap.wrap(message), file=sys.stderr) print(*textwrap.wrap(message), file=sys.stderr)
# Hack to help debugger context find /usr/local/bin
if 'IDE_PROJECT_ROOTS' in os.environ:
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
if tesseract.version() < MINIMUM_TESS_VERSION: if tesseract.version() < MINIMUM_TESS_VERSION:
complain( complain(
"Please install tesseract {0} or newer " "Please install tesseract {0} or newer "
@@ -53,7 +57,6 @@ if tesseract.version() < MINIMUM_TESS_VERSION:
MINIMUM_TESS_VERSION, tesseract.version())) MINIMUM_TESS_VERSION, tesseract.version()))
sys.exit(ExitCode.missing_dependency) sys.exit(ExitCode.missing_dependency)
# ------------- # -------------
# Parser # Parser
@@ -265,7 +268,7 @@ def check_options_languages(options, _log):
"data for the following requested languages: \n") "data for the following requested languages: \n")
for lang in (set(options.language) - tesseract.languages()): for lang in (set(options.language) - tesseract.languages()):
msg += lang + '\n' msg += lang + '\n'
raise argparse.ArgumentError(msg) raise argparse.ArgumentError(None, msg)
def check_options_output(options, log): def check_options_output(options, log):
@@ -291,7 +294,7 @@ def check_options_output(options, log):
"--pdf-renderer=tesseract.") "--pdf-renderer=tesseract.")
lossless_reconstruction = False lossless_reconstruction = False
if options.pdf_renderer == 'hocr': if options.pdf_renderer in ('hocr', 'tess4'):
if not any((options.deskew, options.clean_final, options.force_ocr, if not any((options.deskew, options.clean_final, options.force_ocr,
options.remove_background)): options.remove_background)):
lossless_reconstruction = True lossless_reconstruction = True
@@ -321,6 +324,7 @@ def check_options_preprocessing(options, log):
def check_options_ocr_behavior(options, log): def check_options_ocr_behavior(options, log):
if options.force_ocr and options.skip_text: if options.force_ocr and options.skip_text:
raise argparse.ArgumentError( raise argparse.ArgumentError(
None,
"Error: --force-ocr and --skip-text are mutually incompatible.") "Error: --force-ocr and --skip-text are mutually incompatible.")
if set(options.language) & {'chi_sim', 'chi_tra'} and \ if set(options.language) & {'chi_sim', 'chi_tra'} and \
@@ -344,13 +348,31 @@ def check_options_advanced(options, log):
"commit 3d9fb3b or later") "commit 3d9fb3b or later")
def check_options_metadata(options, log):
import unicodedata
metadata = [options.title, options.author, options.keywords,
options.subject]
for s in (m for m in metadata if m):
for c in s:
if unicodedata.category(c) == 'Co' or ord(c) >= 0x10000:
raise ValueError(
"One of the metadata strings contains "
"an unsupported Unicode character: '{}' (U+{})".format(
c, hex(ord(c))[2:].upper()
))
def check_options(options, log): def check_options(options, log):
try: try:
check_options_languages(options, log) check_options_languages(options, log)
check_options_metadata(options, log)
check_options_output(options, log) check_options_output(options, log)
check_options_preprocessing(options, log) check_options_preprocessing(options, log)
check_options_ocr_behavior(options, log) check_options_ocr_behavior(options, log)
check_options_advanced(options, log) check_options_advanced(options, log)
except ValueError as e:
log.error(e)
sys.exit(ExitCode.bad_args)
except argparse.ArgumentError as e: except argparse.ArgumentError as e:
log.error(e) log.error(e)
sys.exit(ExitCode.bad_args) sys.exit(ExitCode.bad_args)
@@ -399,7 +421,7 @@ def available_cpu_count():
def cleanup_ruffus_error_message(msg): def cleanup_ruffus_error_message(msg):
msg = re.sub(r'\s+', r' ', msg, re.MULTILINE) msg = re.sub(r'\s+', r' ', msg)
msg = re.sub(r"\((.+?)\)", r'\1', msg) msg = re.sub(r"\((.+?)\)", r'\1', msg)
msg = msg.strip() msg = msg.strip()
return msg return msg
+1 -1
View File
@@ -48,7 +48,7 @@ class InputFileError(ExitCodeException):
class SubprocessOutputError(ExitCodeException): class SubprocessOutputError(ExitCodeException):
exit_code = ExitCode.other_error exit_code = ExitCode.child_process_error
class EncryptedPdfError(ExitCodeException): class EncryptedPdfError(ExitCodeException):
+13 -6
View File
@@ -6,8 +6,9 @@ from subprocess import Popen, PIPE, STDOUT, check_call, CalledProcessError, \
check_output check_output
from shutil import copy from shutil import copy
from functools import lru_cache from functools import lru_cache
import re
from . import get_program from . import get_program
from ..pdfa import SRGB_ICC_PROFILE from ..exceptions import SubprocessOutputError
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
@@ -28,6 +29,10 @@ def version():
return version.strip() return version.strip()
def _gs_error_reported(stream):
return re.search(r'error', stream, flags=re.IGNORECASE)
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): pageno=1):
with NamedTemporaryFile(delete=True) as tmp: with NamedTemporaryFile(delete=True) as tmp:
@@ -48,15 +53,16 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=STDOUT, p = Popen(args_gs, close_fds=True, stdout=PIPE, stderr=STDOUT,
universal_newlines=True) universal_newlines=True)
stdout, _ = p.communicate() stdout, _ = p.communicate()
if 'error' in stdout: if _gs_error_reported(stdout):
log.error(stdout) # Ghostscript puts errors in stdout log.error(stdout)
else: else:
log.debug(stdout) log.debug(stdout)
if p.returncode == 0: if p.returncode == 0:
copy(tmp.name, output_file) copy(tmp.name, output_file)
else: else:
log.error('Ghostscript rendering failed') log.error('Ghostscript rasterizing failed')
raise SubprocessOutputError()
def generate_pdfa(pdf_pages, output_file, log, threads=1): def generate_pdfa(pdf_pages, output_file, log, threads=1):
@@ -81,7 +87,7 @@ def generate_pdfa(pdf_pages, output_file, log, threads=1):
universal_newlines=True) universal_newlines=True)
stdout, _ = p.communicate() stdout, _ = p.communicate()
if 'error' in stdout: if _gs_error_reported(stdout):
log.error(stdout) log.error(stdout)
elif 'overprint mode not set' in stdout: elif 'overprint mode not set' in stdout:
# Unless someone is going to print PDF/A documents on a # Unless someone is going to print PDF/A documents on a
@@ -99,4 +105,5 @@ def generate_pdfa(pdf_pages, output_file, log, threads=1):
# PDF/A - check PDF/A status elsewhere # PDF/A - check PDF/A status elsewhere
copy(gs_pdf.name, output_file) copy(gs_pdf.name, output_file)
else: else:
log.error('Ghostscript PDF/A failed') log.error('Ghostscript PDF/A rendering failed')
raise SubprocessOutputError()
+23 -2
View File
@@ -11,6 +11,7 @@ from ..helpers import page_number
from . import get_program from . import get_program
from collections import namedtuple from collections import namedtuple
from textwrap import dedent from textwrap import dedent
import PyPDF2 as pypdf
from subprocess import Popen, PIPE, CalledProcessError, \ from subprocess import Popen, PIPE, CalledProcessError, \
TimeoutExpired, check_output, STDOUT, DEVNULL TimeoutExpired, check_output, STDOUT, DEVNULL
@@ -186,6 +187,8 @@ def tesseract_log_output(log, stdout, input_file):
log.warning(prefix + "unsure about page orientation") log.warning(prefix + "unsure about page orientation")
elif 'error' in line.lower() or 'exception' in line.lower(): elif 'error' in line.lower() or 'exception' in line.lower():
log.error(prefix + line.strip()) log.error(prefix + line.strip())
elif 'warning' in line.lower():
log.warning(prefix + line.strip())
elif 'read_params_file' in line.lower(): elif 'read_params_file' in line.lower():
log.error(prefix + line.strip()) log.error(prefix + line.strip())
else: else:
@@ -270,6 +273,24 @@ def generate_hocr(input_file, output_hocr, language: list, engine_mode,
f_out.write(line) f_out.write(line)
def use_skip_page(text_only, skip_pdf, output_pdf):
if not text_only:
os.symlink(skip_pdf, output_pdf)
return
# For text only we must create a blank page with dimensions identical
# to the skip page because this is equivalent to a page with no text
pdf_in = pypdf.PdfFileReader(skip_pdf)
page0 = pdf_in.pages[0]
with open(output_pdf, 'wb') as out:
pdf_out = pypdf.PdfFileWriter()
w, h = page0.mediaBox.getWidth(), page0.mediaBox.getHeight()
pdf_out.addBlankPage(w, h)
pdf_out.write(out)
def generate_pdf(input_image, skip_pdf, output_pdf, language: list, def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
engine_mode, text_only: bool, engine_mode, text_only: bool,
tessconfig: list, timeout: float, pagesegmode: int, log): tessconfig: list, timeout: float, pagesegmode: int, log):
@@ -307,14 +328,14 @@ def generate_pdf(input_image, skip_pdf, output_pdf, language: list,
universal_newlines=True, timeout=timeout) universal_newlines=True, timeout=timeout)
except TimeoutExpired: except TimeoutExpired:
page_timedout(log, input_image) page_timedout(log, input_image)
shutil.copy(skip_pdf, output_pdf) use_skip_page(text_only, skip_pdf, output_pdf)
except CalledProcessError as e: except CalledProcessError as e:
tesseract_log_output(log, e.output, input_image) tesseract_log_output(log, e.output, input_image)
if 'read_params_file: parameter not found' in e.output: if 'read_params_file: parameter not found' in e.output:
raise TesseractConfigError() from e raise TesseractConfigError() from e
if 'Image too large' in e.output: if 'Image too large' in e.output:
shutil.copy(skip_pdf, output_pdf) use_skip_page(text_only, skip_pdf, output_pdf)
return return
raise e from e raise e from e
else: else:
+4 -1
View File
@@ -430,7 +430,7 @@ def _find_images(pdf, container, shorthand=None):
""" """
if container.get('/Type') == '/Page': if container.get('/Type') == '/Page' and '/Contents' in container:
# For a /Page the content stream is attached to the page's /Contents # For a /Page the content stream is attached to the page's /Contents
page = container page = container
contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf) contentstream = pypdf.pdf.ContentStream(page.getContents(), pdf)
@@ -464,6 +464,9 @@ def _find_images(pdf, container, shorthand=None):
def _page_has_text(pdf, page): def _page_has_text(pdf, page):
if not '/Contents' in page:
return False
# Simple test # Simple test
text = page.extractText() text = page.extractText()
if text.strip() != '': if text.strip() != '':
+22 -12
View File
@@ -26,11 +26,7 @@ pdfa_def_template = u"""%!
/ICCProfile ($icc_profile) /ICCProfile ($icc_profile)
def def
[ /Title <$title> [$docinfo
/Author <$author>
/Subject <$subject>
/Keywords <$keywords>
/Creator <$creator>
/DOCINFO pdfmark /DOCINFO pdfmark
% Define an ICC profile : % Define an ICC profile :
@@ -89,20 +85,30 @@ def encode_text_string(s: str) -> str:
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark): def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
pdfmark_utf16 = {k: encode_text_string(v) for k, v in pdfmark.items()} # Ghostscript <= 9.21 has a bug where null entries in DOCINFO might produce
# ERROR: VMerror (-25) on closing pdfwrite device.
# 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():
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]))
yield line
docinfo = '\n'.join(docinfo_gen())
t = Template(pdfa_def_template) t = Template(pdfa_def_template)
result = t.substitute(icc_profile=icc_profile, result = t.substitute(icc_profile=icc_profile,
icc_identifier=icc_identifier, icc_identifier=icc_identifier,
title=pdfmark_utf16.get('/Title', ''), docinfo=docinfo)
author=pdfmark_utf16.get('/Author', ''),
subject=pdfmark_utf16.get('/Subject', ''),
creator=pdfmark_utf16.get('/Creator', ''),
keywords=pdfmark_utf16.get('/Keywords', ''))
return result return result
def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'): def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB'):
if icc == 'sRGB': if icc == 'sRGB':
icc_profile = SRGB_ICC_PROFILE icc_profile = SRGB_ICC_PROFILE
else: else:
@@ -116,6 +122,10 @@ def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'):
f.write(ps) f.write(ps)
# The old name is generate_pdfa_def -- now deprecated
generate_pdfa_def = generate_pdfa_ps
def file_claims_pdfa(filename): def file_claims_pdfa(filename):
"""Determines if the file claims to be PDF/A compliant """Determines if the file claims to be PDF/A compliant
+19 -16
View File
@@ -23,7 +23,7 @@ from ruffus import formatter, regex, Pipeline, suffix
from .hocrtransform import HocrTransform from .hocrtransform import HocrTransform
from .pageinfo import pdf_get_all_pageinfo from .pageinfo import pdf_get_all_pageinfo
from .pdfa import generate_pdfa_def, file_claims_pdfa from .pdfa import generate_pdfa_ps, file_claims_pdfa
from .helpers import re_symlink, is_iterable_notstr, page_number from .helpers import re_symlink, is_iterable_notstr, page_number
from .exec import ghostscript, tesseract, qpdf from .exec import ghostscript, tesseract, qpdf
from .exceptions import * from .exceptions import *
@@ -247,7 +247,7 @@ def is_ocr_required(pageinfo, log, options):
"skipping all processing on this page")) "skipping all processing on this page"))
ocr_required = False ocr_required = False
if ocr_required and options.skip_big: if ocr_required and options.skip_big and pageinfo['images']:
pixel_count = pageinfo['width_pixels'] * pageinfo['height_pixels'] pixel_count = pageinfo['width_pixels'] * pageinfo['height_pixels']
if pixel_count > (options.skip_big * 1000000): if pixel_count > (options.skip_big * 1000000):
ocr_required = False ocr_required = False
@@ -393,15 +393,16 @@ def rasterize_with_ghostscript(
pageinfo = get_pageinfo(input_file, context) pageinfo = get_pageinfo(input_file, context)
device = 'png16m' # 24-bit device = 'png16m' # 24-bit
if all(image['comp'] == 1 for image in pageinfo['images']): if pageinfo['images']:
if all(image['bpc'] == 1 for image in pageinfo['images']): if all(image['comp'] == 1 for image in pageinfo['images']):
device = 'pngmono' if all(image['bpc'] == 1 for image in pageinfo['images']):
elif all(image['bpc'] > 1 and image['color'] == 'index' device = 'pngmono'
for image in pageinfo['images']): elif all(image['bpc'] > 1 and image['color'] == 'index'
device = 'png256' for image in pageinfo['images']):
elif all(image['bpc'] > 1 and image['color'] == 'gray' device = 'png256'
for image in pageinfo['images']): elif all(image['bpc'] > 1 and image['color'] == 'gray'
device = 'pnggray' for image in pageinfo['images']):
device = 'pnggray'
log.debug("Rasterize {0} with {1}".format( log.debug("Rasterize {0} with {1}".format(
os.path.basename(input_file), device)) os.path.basename(input_file), device))
@@ -517,7 +518,8 @@ def select_visible_page_image(
image = next(ii for ii in infiles if ii.endswith(image_suffix)) image = next(ii for ii in infiles if ii.endswith(image_suffix))
pageinfo = get_pageinfo(image, context) pageinfo = get_pageinfo(image, context)
if all(orig_image['enc'] == 'jpeg' for orig_image in pageinfo['images']): if pageinfo['images'] and \
all(im['enc'] == 'jpeg' for im in pageinfo['images']):
# If all images were JPEGs originally, produce a JPEG as output # If all images were JPEGs originally, produce a JPEG as output
im = Image.open(image) im = Image.open(image)
@@ -767,13 +769,14 @@ def generate_postscript_stub(
options = context.get_options() options = context.get_options()
pdf = pypdf.PdfFileReader(input_file) pdf = pypdf.PdfFileReader(input_file)
pdfmark = get_pdfmark(pdf, options) pdfmark = get_pdfmark(pdf, options)
generate_pdfa_def(output_file, pdfmark) generate_pdfa_ps(output_file, pdfmark)
def skip_page( def skip_page(
input_file, input_file,
output_file, output_file,
log): log,
context):
# The purpose of this step is its filter to forward only the skipped # The purpose of this step is its filter to forward only the skipped
# files (.skip.oriented.pdf) while disregarding the processed ones # files (.skip.oriented.pdf) while disregarding the processed ones
# (.ocr.oriented.pdf). Alternative would be for merge_pages to filter # (.ocr.oriented.pdf). Alternative would be for merge_pages to filter
@@ -1036,7 +1039,7 @@ def build_pipeline(options, work_folder, log, context):
task_func=generate_postscript_stub, task_func=generate_postscript_stub,
input=task_repair_pdf, input=task_repair_pdf,
filter=formatter(r'\.repaired\.pdf'), filter=formatter(r'\.repaired\.pdf'),
output=os.path.join(work_folder, 'pdfa_def.ps'), output=os.path.join(work_folder, 'pdfa.ps'),
extras=[log, context]) extras=[log, context])
task_generate_postscript_stub.active_if(options.output_type == 'pdfa') task_generate_postscript_stub.active_if(options.output_type == 'pdfa')
@@ -1048,7 +1051,7 @@ def build_pipeline(options, work_folder, log, context):
filter=suffix('.skip.oriented.pdf'), filter=suffix('.skip.oriented.pdf'),
output='.done.pdf', output='.done.pdf',
output_dir=work_folder, output_dir=work_folder,
extras=[log]) extras=[log, context])
# Merge pages # Merge pages
task_merge_pages_ghostscript = main_pipeline.merge( task_merge_pages_ghostscript = main_pipeline.merge(
+3
View File
@@ -85,6 +85,9 @@ under the terms of the license in LICENSE.rst.
* - overlay.pdf * - overlay.pdf
- @maxandersen - @maxandersen
- PDF file generated by PDFPen pro that triggered content stream parse errors - PDF file generated by PDFPen pro that triggered content stream parse errors
* - no_conentes.pdf
- @jbarlow83
- synthetic PDF with a blank page that has no /Contents entry
Assemblies Assemblies
========== ==========
+21
View File
@@ -0,0 +1,21 @@
%PDF-1.3
%¿÷¢þ
1 0 obj
<< /Pages 2 0 R /Type /Catalog >>
endobj
2 0 obj
<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
endobj
3 0 obj
<< /MediaBox [ 0 0 720 720 ] /Parent 2 0 R /Resources << >> /Type /Page >>
endobj
xref
0 4
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000123 00000 n
trailer << /Root 1 0 R /Size 4 /ID [<52bba3c78160d0c6e851b59110e5d076><52bba3c78160d0c6e851b59110e5d076>] >>
startxref
213
%%EOF
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
import sys
import os
"""Replicate Ghostscript raster failure while allowing rendering"""
def real_ghostscript(argv):
gs_args = ['gs'] + argv[1:]
os.execvp("gs", gs_args)
return # Not reachable
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__filename__))
sys.exit(0)
# For any rendering calls (device == pdfwrite) call real ghostscript
if '-sDEVICE=pdfwrite' in sys.argv:
real_ghostscript(sys.argv)
return
# Fail
print("ERROR: Ghost story archive not found")
sys.exit(1)
if __name__ == '__main__':
main()
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
import sys
import os
"""Replicate Ghostscript render failure while allowing rasterizing"""
def real_ghostscript(argv):
gs_args = ['gs'] + argv[1:]
os.execvp("gs", gs_args)
return # Not reachable
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__filename__))
sys.exit(0)
# For any rasterize calls (device != pdfwrite) call real ghostscript
if '-sDEVICE=pdfwrite' not in sys.argv:
real_ghostscript(sys.argv)
return
# Fail
print("ERROR: Casper is not a friendly ghost")
sys.exit(1)
if __name__ == '__main__':
main()
+59 -26
View File
@@ -51,6 +51,16 @@ def spoof_no_tess_pdfa_warning():
return spoof(tesseract='tesseract_noop.py', gs='gs_feature_elision.py') return spoof(tesseract='tesseract_noop.py', gs='gs_feature_elision.py')
@pytest.fixture
def spoof_no_tess_gs_render_fail():
return spoof(tesseract='tesseract_noop.py', gs='gs_render_failure.py')
@pytest.fixture
def spoof_no_tess_gs_raster_fail():
return spoof(tesseract='tesseract_noop.py', gs='gs_raster_failure.py')
@pytest.fixture @pytest.fixture
def spoof_qpdf_always_error(): def spoof_qpdf_always_error():
return spoof(qpdf='qpdf_dummy_return2.py') return spoof(qpdf='qpdf_dummy_return2.py')
@@ -167,55 +177,50 @@ def test_preserve_metadata(spoof_tesseract_noop, output_type,
assert pdfa_info['output'] == output_type assert pdfa_info['output'] == output_type
@pytest.mark.skipif(
pytest.helpers.is_linux() and not pytest.helpers.running_in_docker(),
reason="likely to fail if Linux locale is not configured correctly")
@pytest.mark.skipif(
pytest.helpers.is_macos() and pytest.helpers.running_in_travis(),
reason="save Travis the trouble of installing poppler")
@pytest.mark.xfail(
ghostscript.version() == '9.21',
reason="gs 9.21 has a regression that affects this"
)
@pytest.mark.parametrize("output_type", [ @pytest.mark.parametrize("output_type", [
'pdfa', 'pdf' 'pdfa', 'pdf'
]) ])
def test_override_metadata(spoof_tesseract_noop, output_type, resources, def test_override_metadata(spoof_tesseract_noop, output_type, resources,
outpdf): outpdf):
input_file = resources / 'c02-22.pdf' input_file = resources / 'c02-22.pdf'
german = 'Du siehst den Wald vor lauter Bäumen nicht.' german = 'Du siehst den Wald vor lauter Bäumen nicht.'
chinese = '孔子' chinese = '孔子'
high_unicode = 'U+1030C is: 𐌌'
p, out, err = run_ocrmypdf( p, out, err = run_ocrmypdf(
input_file, outpdf, input_file, outpdf,
'--title', german, '--title', german,
'--author', chinese, '--author', chinese,
'--subject', high_unicode,
'--output-type', output_type, '--output-type', output_type,
env=spoof_tesseract_noop) env=spoof_tesseract_noop)
assert p.returncode == ExitCode.ok, err assert p.returncode == ExitCode.ok, err
pdf = str(outpdf) reader = pypdf.PdfFileReader(outpdf)
out_pdfinfo = check_output(['pdfinfo', pdf], universal_newlines=True) assert reader.documentInfo['/Title'] == german
lines_pdfinfo = out_pdfinfo.splitlines() assert reader.documentInfo['/Author'] == chinese
pdfinfo = {} assert reader.documentInfo.get('/Keywords', '') == ''
for line in lines_pdfinfo:
k, v = line.strip().split(':', maxsplit=1)
pdfinfo[k.strip()] = v.strip()
assert pdfinfo['Title'] == german pdfa_info = file_claims_pdfa(outpdf)
assert pdfinfo['Author'] == chinese
assert pdfinfo['Subject'] == high_unicode
assert pdfinfo.get('Keywords', '') == ''
pdfa_info = file_claims_pdfa(pdf)
assert pdfa_info['output'] == output_type assert pdfa_info['output'] == output_type
def test_high_unicode(spoof_tesseract_noop, resources, no_outpdf):
# Ghostscript doesn't support high Unicode, so neither do we, to be
# safe
input_file = resources / 'c02-22.pdf'
high_unicode = 'U+1030C is: 𐌌'
p, out, err = run_ocrmypdf(
input_file, no_outpdf,
'--subject', high_unicode,
'--output-type', 'pdfa',
env=spoof_tesseract_noop)
assert p.returncode == ExitCode.bad_args, err
@pytest.mark.parametrize('renderer', [ @pytest.mark.parametrize('renderer', [
'hocr', 'hocr',
'tesseract', 'tesseract',
@@ -842,3 +847,31 @@ def test_pagesize_consistency(renderer, resources, outpdf):
assert isclose(before_dims[0], after_dims[0]) assert isclose(before_dims[0], after_dims[0])
assert isclose(before_dims[1], after_dims[1]) assert isclose(before_dims[1], after_dims[1])
def test_skip_big_with_no_images(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(resources / 'blank.pdf', outpdf,
'--skip-big', '5',
'--force-ocr',
env=spoof_tesseract_noop)
def test_gs_render_failure(spoof_no_tess_gs_render_fail, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'blank.pdf', outpdf,
env=spoof_no_tess_gs_render_fail)
print(err)
assert p.returncode == ExitCode.child_process_error
def test_gs_raster_failure(spoof_no_tess_gs_raster_fail, resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'ccitt.pdf', outpdf,
env=spoof_no_tess_gs_raster_fail)
print(err)
assert p.returncode == ExitCode.child_process_error
def test_no_contents(spoof_tesseract_noop, resources, outpdf):
check_ocrmypdf(resources / 'no_contents.pdf', outpdf, '--force-ocr',
env=spoof_tesseract_noop)
+8
View File
@@ -108,3 +108,11 @@ def test_form_xobject(resources):
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename)) pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
pdfimage = pdfinfo[0]['images'][0] pdfimage = pdfinfo[0]['images'][0]
assert pdfimage['width'] == 50 assert pdfimage['width'] == 50
def test_no_contents(resources):
filename = resources / 'no_contents.pdf'
pdfinfo = pageinfo.pdf_get_all_pageinfo(str(filename))
assert len(pdfinfo[0]['images']) == 0
assert pdfinfo[0]['has_text'] == False
+75 -5
View File
@@ -6,11 +6,42 @@ from ocrmypdf.exceptions import ExitCode
from ocrmypdf.exec import tesseract from ocrmypdf.exec import tesseract
from ocrmypdf import pageinfo from ocrmypdf import pageinfo
import sys import sys
import os
import PyPDF2 as pypdf
spoof = pytest.helpers.spoof
def tess4_possible_location():
"""The location of tesseract 4 may be OCRMYPDF_TESS4, OCRMYPDF_TESSERACT,
or the installed version on PATH."""
return os.environ.get('OCRMYPDF_TESS4') or \
os.environ.get('OCRMYPDF_TESSERACT') or \
'tesseract'
@pytest.fixture
def ensure_tess4():
return spoof(tesseract=tess4_possible_location())
def tess4_available():
"""Check if a tesseract 4 binary is available, even if it's not the
official "tesseract" on PATH
"""
old_environ = os.environ.copy()
try:
os.environ['OCRMYPDF_TESSERACT'] = tess4_possible_location()
return tesseract.v4() and tesseract.has_textonly_pdf()
finally:
os.environ = old_environ
# Skip all tests in this file if not tesseract 4 # Skip all tests in this file if not tesseract 4
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.skipif(
not (tesseract.v4() and tesseract.has_textonly_pdf()), not tess4_available(),
reason="tesseract 4.0 with textonly_pdf feature required") reason="tesseract 4.0 with textonly_pdf feature required")
check_ocrmypdf = pytest.helpers.check_ocrmypdf check_ocrmypdf = pytest.helpers.check_ocrmypdf
@@ -18,14 +49,15 @@ run_ocrmypdf = pytest.helpers.run_ocrmypdf
spoof = pytest.helpers.spoof spoof = pytest.helpers.spoof
def test_textonly_pdf(resources, outdir): def test_textonly_pdf(ensure_tess4, resources, outdir):
check_ocrmypdf( check_ocrmypdf(
resources / 'linn.pdf', resources / 'linn.pdf',
outdir / 'linn_textonly.pdf', '--pdf-renderer', 'tess4') outdir / 'linn_textonly.pdf', '--pdf-renderer', 'tess4',
env=ensure_tess4)
@pytest.mark.skipif(sys.version_info < (3, 5), reason="needs math.isclose") @pytest.mark.skipif(sys.version_info < (3, 5), reason="needs math.isclose")
def test_pagesize_consistency_tess4(resources, outpdf): def test_pagesize_consistency_tess4(ensure_tess4, resources, outpdf):
from math import isclose from math import isclose
infile = resources / 'linn.pdf' infile = resources / 'linn.pdf'
@@ -35,9 +67,47 @@ def test_pagesize_consistency_tess4(resources, outpdf):
check_ocrmypdf( check_ocrmypdf(
infile, infile,
outpdf, '--pdf-renderer', 'tess4', outpdf, '--pdf-renderer', 'tess4',
'--clean', '--deskew', '--remove-background', '--clean-final') '--clean', '--deskew', '--remove-background', '--clean-final',
env=ensure_tess4)
after_dims = pytest.helpers.first_page_dimensions(outpdf) after_dims = pytest.helpers.first_page_dimensions(outpdf)
assert isclose(before_dims[0], after_dims[0]) assert isclose(before_dims[0], after_dims[0])
assert isclose(before_dims[1], after_dims[1]) assert isclose(before_dims[1], after_dims[1])
@pytest.mark.parametrize('basename', ['graph_ocred.pdf', 'cardinal.pdf'])
def test_skip_pages_does_not_replicate(
ensure_tess4, resources, basename, outdir):
infile = resources / basename
outpdf = outdir / basename
check_ocrmypdf(
infile,
outpdf, '--pdf-renderer', 'tess4', '--force-ocr',
'--tesseract-timeout', '0',
env=ensure_tess4
)
info_in = pageinfo.pdf_get_all_pageinfo(str(infile))
info = pageinfo.pdf_get_all_pageinfo(str(outpdf))
for page in info:
assert len(page['images']) == 1, "skipped page was replicated"
for n in range(len(info_in)):
assert info[n]['width_inches'] == info_in[n]['width_inches']
def test_content_preservation(ensure_tess4, resources, outpdf):
infile = resources / 'masks.pdf'
check_ocrmypdf(
infile,
outpdf, '--pdf-renderer', 'tess4', '--tesseract-timeout', '0',
env=ensure_tess4
)
info = pageinfo.pdf_get_all_pageinfo(str(outpdf))
page = info[0]
assert len(page['images']) > 1, "masked were rasterized"