Compare commits

..
52 Commits
Author SHA1 Message Date
James R. Barlow 019513696b Ghostscript spoof scripts did not report their --version correctly 2018-01-10 17:08:14 -08:00
James R. Barlow ad7a4476db hugemono.pdf needs --max-image-mpixels to pass with Pillow 5.0 2018-01-10 16:55:18 -08:00
James R. Barlow 4812b20fb2 Fix tesseract_noop.py generating wrong size of output PDF in tests
This caused trouble before with test_deskew
2018-01-10 16:35:31 -08:00
James R. Barlow f5e07c9427 Fix Ghostscript parameter order 2018-01-10 16:33:26 -08:00
James R. Barlow 75dcb90621 Niceties: when environment variable overload is used clarify we're not checking the PATH 2018-01-10 16:33:03 -08:00
James R. Barlow dfc0434cc2 Update requirements to set Pillow to 5.0 2018-01-10 15:45:23 -08:00
James R. Barlow 882fc2257c Add --max-image-mpixels argument to support Pillow 5.0 2018-01-10 15:43:59 -08:00
James R. Barlow 41e83b52fc Document process for redoing OCR 2018-01-10 15:39:58 -08:00
James R. Barlow 47758b4d8f Reactivate autobrew 2018-01-10 15:39:36 -08:00
James R. Barlow 6bf1f970a0 Fix some parameter validation for --output-type pdfa-1 and pdfa-2 2018-01-10 11:50:08 -08:00
James R. Barlow 7d451f101f Detect old versions of Ghostscript and warn about them (#208) 2018-01-10 11:47:39 -08:00
James R. Barlow 7edbfe0e40 Update autobrew template 2018-01-09 12:36:14 -08:00
James R. Barlow 91b42cbfa8 Fix issue in sandwich renderer when skipping OCR on a rotated and deskewed page
If OCR is skipped due to --tesseract-timeout or similar, and the skip page is rotated with /Rotate, and the skip page was deskewed or had other image processing, then the skip page was created with the wrong dimensions causing the output page to be cropped.
2018-01-09 00:17:53 -08:00
James R. Barlow 6907df41b4 Disable autobrew until homebrew accepts the official release 2018-01-08 23:26:58 -08:00
James R. Barlow 2cebd90cbd Fix brew audit --strict warnings 2017-12-09 12:14:28 -08:00
James R. Barlow 376a121aaa Re-enable macos 2017-11-29 15:06:19 -08:00
James R. Barlow da11fd17ee qpdf dummy: needs to return version now 2017-11-29 14:35:37 -08:00
James R. Barlow a40689a0ff tesseract: handle return of bytes properly in error cases 2017-11-29 14:35:26 -08:00
James R. Barlow 44a45fc3fb Add "bad UTF8 output from Tesseract" test 2017-11-29 14:08:07 -08:00
James R. Barlow ec4bb5359a Read tesseract's output as binary to avoid UnicodeDecodeErrors if it messes up 2017-11-29 13:44:40 -08:00
James R. Barlow d2217632df Rename _verify_python3_env 2017-11-29 13:43:18 -08:00
James R. Barlow 2cc044feed Move qpdf complaint to after options checking so that it won't break ocrmypdf --version 2017-11-29 13:42:55 -08:00
James R. Barlow 64fd0cb54f Remove test_qpdf.py only from travis 2017-11-29 12:54:48 -08:00
James R. Barlow c5a1d22e81 That fixed it. Complain about old versions of qpdf now 2017-11-29 12:53:34 -08:00
James R. Barlow a7b307af04 Looks like issue was negzero.pdf with qpdf 5.1.1 on travis, which is why osx passes
Reorganize and see if this is better now
2017-11-29 12:47:09 -08:00
James R. Barlow 3269eba16c Is it negzero.pdf? 2017-11-29 12:02:37 -08:00
James R. Barlow d472860e3b Try to diagnose travis-only failure of qpdf test 2017-11-29 11:41:06 -08:00
James R. Barlow 731c9ea55e Set timeouts on the tests that seem to be stalling on travis (but not elsewhere) 2017-11-27 14:46:10 -08:00
James R. Barlow 0b6af8d965 Clarify hocrtransform license/copyright 2017-11-27 13:41:46 -08:00
James R. Barlow 92ca9e954c Fix test warning/failures, hopefully 2017-11-27 13:41:32 -08:00
James R. Barlow 67aa00e2c5 Remove py3.5 deprecation
It'll be in Debian stable for a long time, so maybe not such a good idea
2017-11-27 01:10:34 -08:00
James R. Barlow a1bf161eb7 Update v5.4.4 release notes 2017-11-27 01:09:35 -08:00
James R. Barlow 56614fcaa4 Add support and tests for handling page count > ulimit - fixes issue #181 2017-11-27 00:32:35 -08:00
James R. Barlow 4d9169e15f Add merge ulimit test case 2017-11-26 23:34:36 -08:00
James R. Barlow 3a167af2c4 Nearly smallest possible PDF-1.3 with all required fields 2017-11-26 23:32:21 -08:00
James R. Barlow 64b9005030 v5.4.4 release notes; mention deprecation of 3.5 2017-11-26 23:08:55 -08:00
James R. Barlow 965de3a235 Test case for issue #200 2017-11-26 22:52:53 -08:00
James R. Barlow 2040ae4856 Fix issue #200, uncommon but valid decimal syntax treated as error
Also replace check_output() calls with run() in qpdf.py
2017-11-26 22:52:43 -08:00
James R. Barlow 31a1f9f5ea pytest-runner should be a setup requirement 2017-11-26 22:51:51 -08:00
James R. Barlow 7eadd82048 Update docker script based on uid issue
issue #180, fix reported by @Atredis76
2017-11-26 15:11:17 -08:00
James R. Barlow 31d0eaac8e Ensure intermediate metadata holder PDF has same version as its input file
While not known to cause problems, the absence of this could confuse parsers
2017-11-24 00:09:13 -08:00
James R. Barlow f45d012635 Update docs: security, pip install --user 2017-11-21 16:50:14 -08:00
James R. Barlow 5a777ee9bc v5.4.3 release notes 2017-11-17 02:28:02 -08:00
James R. Barlow 7bbf6bc7f4 Travis didn't like LANG, use LC_ALL 2017-11-16 20:37:30 -08:00
James R. Barlow 9bfc45702d Update copyright in license 2017-11-16 17:18:29 -08:00
James R. Barlow 40aa82ab41 Check that the locale is sane before allowing OCR to proceed 2017-11-16 17:18:02 -08:00
James R. Barlow 5d2c67c62b Declare in yet another place that this shouldn't run on py2 2017-11-16 14:54:28 -08:00
James R. Barlow a00ef4836d autobrew: declare conflict with mactex
As reported in https://github.com/jbarlow83/homebrew-ocrmypdf/issues/4#issuecomment-345029899
2017-11-16 14:53:42 -08:00
James R. Barlow 3ef766bb93 Remove bare 'except:' 2017-11-01 01:52:13 -07:00
James R. Barlow 44b5a18462 Declare our __version__ properly 2017-11-01 01:49:51 -07:00
James R. Barlow 4df716f0e8 Update requirements.txt 2017-11-01 01:49:35 -07:00
James R. Barlow fcbf34a4d3 Refactor obtaining version from subprocesses
Issue #196 raised the need to deal with linker warnings on --version.
2017-11-01 01:44:36 -07:00
35 changed files with 817 additions and 181 deletions
+2 -1
View File
@@ -44,7 +44,8 @@ install:
script:
- mv ocrmypdf dont_import_this_ocrmypdf
- tesseract --version
- pytest -n auto
- qpdf --version
- pytest
- mv dont_import_this_ocrmypdf ocrmypdf
deploy:
+55 -25
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# © 2016 James R. Barlow: github.com/jbarlow83
# © 2016-7 James R. Barlow: github.com/jbarlow83
from string import Template
from subprocess import run, PIPE
@@ -14,39 +14,69 @@ class Ocrmypdf < Formula
${ocrmypdf_url}
${ocrmypdf_sha256}
depends_on :python3
depends_on :x11 # Pillow needs XQuartz
depends_on "pkg-config" => :build
depends_on "libffi"
depends_on "tesseract"
depends_on "ghostscript"
depends_on "unpaper"
depends_on "qpdf"
# For Pillow source install
depends_on "openjpeg"
depends_on "freetype"
depends_on "ghostscript"
depends_on "jpeg"
depends_on "libpng"
depends_on "libjpeg"
depends_on "webp"
depends_on "little-cms2"
depends_on "zlib"
depends_on "pkg-config" => :build
depends_on "python3"
depends_on "qpdf"
depends_on "tesseract"
depends_on "unpaper"
${resources}
def install
ENV.append ["SETUPTOOLS_SCM_PRETEND_VERSION"], "v${ocrmypdf_version}"
ENV.each do |key, value|
puts "#{key}:#{value}"
venv = virtualenv_create(libexec, "python3")
resource("Pillow").stage do
inreplace "setup.py" do |s|
sdkprefix = MacOS::CLT.installed? ? "" : MacOS.sdk_path
s.gsub! "openjpeg.h", "probably_not_a_header_called_this_eh.h"
s.gsub! "ZLIB_ROOT = None", "ZLIB_ROOT = ('#{sdkprefix}/usr/lib', '#{sdkprefix}/usr/include')"
s.gsub! "JPEG_ROOT = None", "JPEG_ROOT = ('#{Formula["jpeg"].opt_prefix}/lib', '#{Formula["jpeg"].opt_prefix}/include')"
s.gsub! "FREETYPE_ROOT = None", "FREETYPE_ROOT = ('#{Formula["freetype"].opt_prefix}/lib', '#{Formula["freetype"].opt_prefix}/include')"
end
# avoid triggering "helpful" distutils code that doesn't recognize Xcode 7 .tbd stubs
ENV.append "CFLAGS", "-I#{MacOS.sdk_path}/System/Library/Frameworks/Tk.framework/Versions/8.5/Headers" unless MacOS::CLT.installed?
venv.pip_install Pathname.pwd
end
virtualenv_install_with_resources
res = resources.map(&:name).to_set - ["Pillow"]
res.each do |r|
venv.pip_install resource(r)
end
venv.pip_install_and_link buildpath
end
test do
# `test do` will create, run in and delete a temporary directory.
#
# The installed folder is not in the path, so use the entire path to any
# executables being tested: `system "#{bin}/program", "do", "something"`.
system "#{bin}/ocrmypdf", "--version"
# Since we use Python 3, we require a UTF-8 locale
ENV["LC_ALL"] = "en_US.UTF-8"
# One page Postscript with the wording "Testing" on the page
# This is more compact than including a test PDF
(testpath/"test.ps").write(
<<~EOS
%!PS
/Times-Roman findfont
20 scalefont
setfont
gsave
newpath
200 400 moveto
(Testing) show
closepath
stroke
showpage
EOS
)
system "#{Formula["ghostscript"].opt_bin}/ps2pdf", testpath/"test.ps", testpath/"test.pdf"
# Use ocrmypdf -f to rasterize the PDF to image before doing OCR
system "#{bin}/ocrmypdf", "-f", "-q", "--deskew", testpath/"test.pdf", testpath/"ocr.pdf"
end
end
""")
+4 -1
View File
@@ -1,4 +1,7 @@
Copyright (c) 2013-2016, The OCRmyPDF Authors
Copyright (c) 2014-2017, James R. Barlow
Copyright (c) 2013-2014, Julien Pfefferkorn
Copyright (c) 2013-2017, The OCRmyPDF Authors
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
+7
View File
@@ -122,6 +122,13 @@ If you detect an issue, please:
- 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.6, and requires external program installations of Ghostscript, Tesseract OCR, QPDF, and Leptonica. ocrmypdf is pure Python, but uses CFFI to portably generate library bindings.
Python 3.5 is fully supported, but support will end at the end of 2017, to allow f-strings in the code base.
Press & Media
-------------
+1 -1
View File
@@ -147,7 +147,7 @@ This is only possible for x86-based Synology products. Some Synology products us
# use this uid:gid in -u flag
# rw rights for docker:docker at source dir are also necessary
# the script is processed as root user via chron
cmd = ['docker', 'run', '--rm', '-v', docker_mount, '-u="1030:65538"', 'jbarlow83/ocrmypdf', , '--deskew' , filename, filename_OCR]
cmd = ['docker', 'run', '--rm', '-v', docker_mount, '-u=1030:65538', 'jbarlow83/ocrmypdf', , '--deskew' , filename, filename_OCR]
logging.info(cmd)
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
result = proc.stdout.read()
+23
View File
@@ -157,6 +157,29 @@ If you set ``--tesseract-timeout 0`` OCRmyPDF will apply its image processing wi
ocrmypdf --tesseract-timeout=0 --remove-background input.pdf output.pdf
Redo OCR
""""""""
To redo OCR on a file OCRed with other OCR software or a previous version of OCRmyPDF and/or Tesseract, you may use the ``--force-ocr`` argument. Normally, OCRmyPDF does not modify files that already appear to contain OCR text.
.. code-block:: bash
ocrmypdf --force-ocr input.pdf output.pdf
Note that the method above will force rasterization of all pages, potentially reducing quality or losing vector content.
To ensure quality is preserved, one could extract all of the images and rebuild the PDF for a lossless transformation. This recipe does not work when PDFs contain multiple images per page, as many do in practice. It will also lose any page rotation information.
.. code-block:: bash
pdfimages -all old-ocr.pdf prefix # extract all images
img2pdf -o temp.pdf prefix* # construct new PDF from the images
# review the new PDF to ensure it visually matches the old one
ocrmypdf --output-type pdf temp.pdf new-ocr.pdf
``--output-type pdf`` is used here to avoid using Ghostscript which will also rasterize images.
Improving OCR quality
---------------------
+18 -4
View File
@@ -125,6 +125,9 @@ Or in the wonderful `fish shell <https://fishshell.com/>`_:
alias ocrmypdf 'docker run --rm -v (pwd):/home/docker ocrmypdf'
funcsave ocrmypdf
.. note::
The ocrmypdf Docker containers are designed to be used for a single OCR job. The ``docker run --rm`` argument tells Docker to delete temporary storage associated with container when it is done executing.
Manual installation on macOS
----------------------------
@@ -171,7 +174,13 @@ Update the homebrew pip and install Pillow:
pip3 install --upgrade pip
pip3 install --upgrade pillow
You can then install OCRmyPDF from PyPI:
You can then install OCRmyPDF from PyPI, for the current user:
.. code-block:: bash
pip3 install --user ocrmypdf
or system-wide:
.. code-block:: bash
@@ -200,8 +209,13 @@ No package is currently available for Ubuntu 16.04, but you can install the depe
python3-pip \
python3-cffi
If you wish install OCRmyPDF to the system Python, then install as follows (note this installs new packages
into your system Python, which could interfere with other programs):
If you wish install OCRmyPDF for the current user:
.. code-block:: bash
pip3 install --user ocrmypdf
Alternately, system-wide. Note that this may modify the system Python environment:
.. code-block:: bash
@@ -221,7 +235,7 @@ Installing on Ubuntu 14.04 LTS
------------------------------
Installing on Ubuntu 14.04 LTS (trusty) is more difficult than some other options,
because it is older.
because it is older and does not provide ``pip``.
Update apt-get:
+30
View File
@@ -5,6 +5,36 @@ OCRmyPDF uses `semantic versioning <http://semver.org/>`_ for its command line i
The OCRmyPDF package itself does not contain a public API, although it is fairly stable and breaking changes are usually timed with a major release. A future release will clearly define the stable public API.
v5.5
----
- Add new argument ``--max-image-mpixels``. Pillow 5.0 now raises an exception when images may be decompression bombs. This argument can be used to override the limit Pillow sets.
- Fix output page cropped when using the sandwich renderer and OCR is skipped on a rotated and image-processed page
- A warning is now issued when old versions of Ghostscript are used in cases known to cause issues with non-Latin characters
- Fix a few parameter validation checks for ``-output-type pdfa-1`` and ``pdfa-2``
v5.4.4
------
- Fix issue #181: fix final merge failure for PDFs with more pages than the system file handle limit (``ulimit -n``)
- Fix issue #200: an uncommon syntax for formatting decimal numbers in a PDF would cause qpdf to issue a warning, which ocrmypdf treated as an error. Now this the warning is relayed.
- Fix an issue where intermediate PDFs would be created at version 1.3 instead of the version of the original file. It's possible but unlikely this had side effects.
- A warning is now issued when older versions of qpdf are used since issues like #200 cause qpdf to infinite-loop
- Address issue #140: if Tesseract outputs invalid UTF-8, escape it and print its message instead of aborting with a Unicode error
- Adding previously unlisted setup requirement, pytest-runner
- Update documentation: fix an error in the example script for Synology with Docker images, improved security guidance, advised ``pip install --user``
v5.4.3
------
- If a subprocess fails to report its version when queried, exit cleanly with an error instead of throwing an exception
- Added test to confirm that the system locale is Unicode-aware and fail early if it's not
- Clarified some copyright information
- Updated pinned requirements.txt so the homebrew formula captures more recent versions
v5.4.2
------
+34 -7
View File
@@ -10,7 +10,7 @@ The disclaimer applies: this software has no warranties of any kind.
PDFs may contain malware
------------------------
PDF is a rich, complex file format. The official PDF 1.7 specification, ISO 32000:2008, is hundreds of packages long and references several annexes each of which are similar in length. PDFs can contain video, audio, JavaScript and other programming, and forms. In some cases, they can open internet connections to pre-selected URLs. All of these possible attack vectors.
PDF is a rich, complex file format. The official PDF 1.7 specification, ISO 32000:2008, is hundreds of pages long and references several annexes each of which are similar in length. PDFs can contain video, audio, XML, JavaScript and other programming, and forms. In some cases, they can open internet connections to pre-selected URLs. All of these possible attack vectors.
In short, PDFs `may contain viruses <https://security.stackexchange.com/questions/64052/can-a-pdf-file-contain-a-virus>`_.
@@ -21,18 +21,45 @@ How OCRmyPDF processes PDFs
OCRmyPDF must open and interpret your PDF in order to insert an OCR layer. First, it runs all PDFs through `qpdf <https://github.com/qpdf/qpdf>`_, a program that repairs PDFs with syntax errors. This is done because, in the author's experience, a significant number of PDFs in the wild especially those created by scanners are not well-formed files. qpdf makes it more likely that OCRmyPDF will succeed, but offers no security guarantees. qpdf is also used to split the PDF into single page PDFs.
After qpdf, OCRmyPDF examines each page using `PyPDF2 <https://github.com/mstamy2/PyPDF2>`_. This library also has no warranties or guarantees.
After qpdf, OCRmyPDF examines each page using `PyPDF2 <https://github.com/mstamy2/PyPDF2>`_. This library also has no warranties or guarantees. OCRmyPDF works with qpdf 5.0 and up, but version 7.0 is recommended because of known security vulnerabilities in early versions.
Finally, OCRmyPDF rasterizes each page of the PDF using `Ghostscript <http://ghostscript.com/>`_ in ``-dSAFER`` mode.
Finally, OCRmyPDF rasterizes each page of the PDF using `Ghostscript <http://ghostscript.com/>`_ in ``-dSAFER`` mode.
Depending on the options specified, OCRmyPDF may graft the OCR layer into the existing PDF or it may essentially reconstruct ("re-fry") a visually identical PDF that may be quite different at the binary level. That said, OCRmyPDF is not a tool designed for sanitizing PDFs.
Using OCRmyPDF online
---------------------
Using OCRmyPDF online or as a service
-------------------------------------
OCRmyPDF is not designed to be deployed "as a service", in a setting where a user/attacker could upload a file for OCR processing online. It is not designed to be secure in this case.
OCRmyPDF should not be deployed as a public-facing service, like a website where a potential attacker could upload a PDF of their choice for OCR. OCRmyPDF is not designed to be secure against PDF malware. Another concern is PDFs specifically designed to be a denial of service attack: PDFs can contain recursive data structures that sometimes send parsers into infinite loops, and issue complex graphics drawing commands.
OCRmyPDF should be relatively safe to use in a trusted intranet, with some considerations:
Limiting CPU usage
^^^^^^^^^^^^^^^^^^
OCRmyPDF will attempt to use all available CPUs and storage, so executing ``nice ocrmypdf`` or limiting the number of jobs with the ``-j`` argument may ensure the server remains available. Another option would be run OCRmyPDF jobs inside a Docker container or virtual machine, which can impose its own limits on CPU usage.
Temporary storage requirements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OCRmyPDF will use a large amount of temporary storage for its work, proportional to the total number of pixels needed to rasterize the PDF. The raster image of a 8.5×11" color page at 300 DPI takes 25 MB uncompressed; OCRmyPDF saves its intermediates as PNG, but that still means it requires about 9 MB per intermediate based on average compression ratios. Multiple intermediates per page are also required, depending on the command line given. A rule of thumb would be to allow 100 MB of temporary storage per page in a file meaning that a small cloud servers or small VM partitions should be provisioned with plenty of extra space, if say, a 500 page file might be sent.
To check temporary storage usage on actual files, run ``ocrmypdf -k ...`` which will preserve and print the path to temporary storage when the job is done.
To change where temporary files are stored, change the ``TMPDIR`` environment variable for ocrmypdf's environment. (Python's ``tempfile.gettempdir()`` returns the root directory in which temporary files will be stored.)
Timeouts
^^^^^^^^
To prevent excessively long OCR jobs consider setting ``--tesseract-timeout`` and/or ``--skip-big`` arguments. ``--skip-big`` is particularly helpful if your PDFs include documents such as reports on standard page sizes with large images attached - often large images are not worth OCR'ing anyway.
Commercial alternatives
^^^^^^^^^^^^^^^^^^^^^^^
The author also provides professional services that include OCR and building databases around PDFs, and is happy to provide consultation.
Abbyy Cloud OCR is a viable commercial alternative with a web services API.
Abbyy Cloud OCR is a viable commercial alternative with a web services API. The author also provides professional services that include OCR and building databases around PDFs, and is happy to provide consultation.
Password protection, digital signatures and certification
---------------------------------------------------------
+3 -2
View File
@@ -4,6 +4,7 @@ import pkg_resources
PROGRAM_NAME = 'ocrmypdf'
VERSION = pkg_resources.get_distribution('ocrmypdf').version
# Official PEP 396
__version__ = pkg_resources.get_distribution('ocrmypdf').version
VERSION = __version__
+51 -8
View File
@@ -14,6 +14,7 @@ import logging
import argparse
import PyPDF2 as pypdf
import PIL
import ruffus.ruffus_exceptions as ruffus_exceptions
import ruffus.cmdline as cmdline
@@ -23,11 +24,12 @@ from .pipeline import JobContext, JobContextManager, \
cleanup_working_files, build_pipeline
from .pdfa import file_claims_pdfa
from .helpers import is_iterable_notstr, re_symlink, is_file_writable
from .exec import tesseract, qpdf
from .exec import tesseract, qpdf, ghostscript
from . import PROGRAM_NAME, VERSION
from .exceptions import *
from . import exceptions as ocrmypdf_exceptions
from ._unicodefun import verify_python3_env
warnings.simplefilter('ignore', pypdf.utils.PdfReadWarning)
@@ -49,6 +51,11 @@ def complain(message):
if 'IDE_PROJECT_ROOTS' in os.environ:
os.environ['PATH'] = '/usr/local/bin:' + os.environ['PATH']
# --------
# Critical environment tests
verify_python3_env()
if tesseract.version() < MINIMUM_TESS_VERSION:
complain(
"Please install tesseract {0} or newer "
@@ -235,6 +242,11 @@ ocrsettings.add_argument(
advanced = parser.add_argument_group(
"Advanced",
"Advanced options to control Tesseract's OCR behavior")
advanced.add_argument(
'--max-image-mpixels', action='store', type=float, metavar='MPixels',
help="Set maximum number of pixels to unpack before treating an image as a "
"decompression bomb",
default=128.0)
advanced.add_argument(
'--tesseract-config', action='append', metavar='CFG', default=[],
help="Additional Tesseract configuration files -- see documentation")
@@ -315,11 +327,12 @@ def check_options_languages(options, _log):
if '+' in options.language[0]:
options.language = options.language[0].split('+')
if not set(options.language).issubset(tesseract.languages()):
languages = set(options.language)
if not languages.issubset(tesseract.languages()):
msg = (
"The installed version of tesseract does not have language "
"data for the following requested languages: \n")
for lang in (set(options.language) - tesseract.languages()):
for lang in (languages - tesseract.languages()):
msg += lang + '\n'
raise MissingDependencyError(msg)
@@ -342,7 +355,8 @@ def check_options_output(options, log):
options.pdf_renderer = 'sandwich'
if options.pdf_renderer == 'tesseract':
if tesseract.version() < '3.05' and options.output_type == 'pdfa':
if tesseract.version() < '3.05' and \
options.output_type.startswith('pdfa'):
log.warning(
"For best results use --pdf-renderer=tesseract "
"--output-type=pdf to disable PDF/A generation via "
@@ -406,9 +420,9 @@ def check_options_ocr_behavior(options, log):
# if options.redo_ocr and (options.skip_text or options.force_ocr):
# raise argparse.ArgumentError(
# "Error: --redo-ocr and other OCR options are incompatible.")
languages = set(options.language)
if options.pdf_renderer == 'hocr' and \
not set(options.language).issubset(HOCR_OK_LANGS):
not languages.issubset(HOCR_OK_LANGS):
msg = (
"The 'hocr' PDF renderer is known to cause problems with one "
"or more of the languages in your document. ")
@@ -421,6 +435,15 @@ def check_options_ocr_behavior(options, log):
"Use --pdf-renderer tesseract --output-type pdf to avoid "
"this issue")
log.warning(msg)
elif ghostscript.version() < '9.20' and \
not languages.issubset(HOCR_OK_LANGS) \
and options.output_type != 'pdf':
msg = (
"The installed version of Ghostscript does not work correctly "
"with the OCR languages you specified. Use --output-type pdf or "
"upgrade to Ghostscript 9.20 or later to avoid this issue.")
msg += "Found Ghostscript {}".format(ghostscript.version())
log.warning(msg)
def check_options_advanced(options, log):
@@ -435,10 +458,10 @@ def check_options_advanced(options, log):
"--pdf-renderer tess4 requires Tesseract 4.x "
"commit 3d9fb3b or later")
if options.pdfa_image_compression != 'auto' and \
options.output_type != 'pdfa':
options.output_type.startswith('pdfa'):
log.warning(
"--pdfa-image-compression argument has no effect when "
"--output-type is not 'pdfa'"
"--output-type is not 'pdfa', 'pdfa-1', or 'pdfa-2'"
)
@@ -581,6 +604,12 @@ def do_ruffus_exception(ruffus_five_tuple, options, log):
"""))
exit_code = ExitCode.encrypted_pdf
elif exc_name == 'PIL.Image.DecompressionBombError':
msg = cleanup_ruffus_error_message(exc_value)
msg += ("\nUse the --max-image-mpixels argument to set increase the "
"maximum number of megapixels to accept.")
log.error(msg)
exit_code = ExitCode.input_file
if exit_code is not None:
return exit_code
@@ -676,9 +705,23 @@ def run_pipeline():
logging_factory, __name__, logger_args)
_log.debug('ocrmypdf ' + VERSION)
_log.debug('tesseract ' + tesseract.version())
_log.debug('qpdf ' + qpdf.version())
check_options(options, _log)
PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1000000)
if PIL.Image.MAX_IMAGE_PIXELS == 0:
PIL.Image.MAX_IMAGE_PIXELS = None
# Complain about qpdf version < 7.0.0
# Suppress the warning if in the test suite, since there are no PPAs
# for qpdf 7.0.0 for Ubuntu trusty (i.e. Travis)
if qpdf.version() < '7.0.0' and not os.environ.get('PYTEST_CURRENT_TEST'):
complain(
"You are using qpdf version {0} which has known issues including "
"security vulnerabilities with certain malformed PDFs. Consider "
"upgrading to version 7.0.0 or newer.".format(qpdf.version()))
# Any changes to options will not take effect for options that are already
# bound to function parameters in the pipeline. (For example
# options.input_file, options.pdf_renderer are already bound.)
+108
View File
@@ -0,0 +1,108 @@
# Copyright (c) 2014, Armin Ronacher
#
# Copyright (c) 2017, James R Barlow
#
# Some rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# * The names of the contributors may not be used to endorse or
# promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
import sys
import codecs
def verify_python3_env():
"""Ensures that the environment is good for unicode on Python 3."""
try:
import locale
fs_enc = codecs.lookup(locale.getpreferredencoding()).name
except Exception:
fs_enc = 'ascii'
if fs_enc != 'ascii':
return
extra = ''
if os.name == 'posix':
import subprocess
rv = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE,
stderr=subprocess.PIPE).communicate()[0]
good_locales = set()
has_c_utf8 = False
# Make sure we're operating on text here.
if isinstance(rv, bytes):
rv = rv.decode('ascii', 'replace')
for line in rv.splitlines():
locale = line.strip()
if locale.lower().endswith(('.utf-8', '.utf8')):
good_locales.add(locale)
if locale.lower() in ('c.utf8', 'c.utf-8'):
has_c_utf8 = True
extra += '\n\n'
if not good_locales:
extra += (
'Additional information: on this system no suitable UTF-8\n'
'locales were discovered. This most likely requires resolving\n'
'by reconfiguring the locale system.'
)
elif has_c_utf8:
extra += (
'This system supports the C.UTF-8 locale which is recommended.\n'
'You might be able to resolve your issue by exporting the\n'
'following environment variables:\n\n'
' export LC_ALL=C.UTF-8\n'
' export LANG=C.UTF-8'
)
else:
extra += (
'This system lists a couple of UTF-8 supporting locales that\n'
'you can pick from. The following suitable locales were\n'
'discovered: %s'
) % ', '.join(sorted(good_locales))
bad_locale = None
for locale in os.environ.get('LC_ALL'), os.environ.get('LANG'):
if locale and locale.lower().endswith(('.utf-8', '.utf8')):
bad_locale = locale
if locale is not None:
break
if bad_locale is not None:
extra += (
'\n\ocrmypdf discovered that you exported a UTF-8 locale\n'
'but the locale system could not pick up from it because\n'
'it does not exist. The exported locale is "%s" but it\n'
'is not supported'
) % bad_locale
raise RuntimeError('ocrmypdf will abort further execution because Python 3 '
'was configured to use ASCII as encoding for the '
'environment.' + extra)
+37
View File
@@ -4,9 +4,46 @@
"""Wrappers to manage subprocess calls"""
import os
import re
import sys
from subprocess import run, STDOUT, PIPE, CalledProcessError
from ..exceptions import MissingDependencyError
def get_program(name):
"Check environment variables for overrides to this program"
envvar = 'OCRMYPDF_' + name.upper()
return os.environ.get(envvar, name)
def get_version(program, *,
version_arg='--version', regex=r'(\d+(\.\d+)*)'):
"Get the version of the specified program, "
args_prog = [
get_program(program),
version_arg
]
try:
proc = run(
args_prog, close_fds=True, universal_newlines=True,
stdout=PIPE, stderr=STDOUT, check=True)
output = proc.stdout
except CalledProcessError as e:
if get_program(program) == program:
raise MissingDependencyError(
"Could not find program '{}' on the PATH".format(
program)) from e
else:
raise MissingDependencyError(
"Could not find program '{}'".format(
get_program(program))) from e
try:
version = re.match(regex, output.strip()).group(1)
except AttributeError as e:
raise MissingDependencyError(
("The program '{}' did not report its version. "
"Message was:\n{}").format(program, output)
)
return version
+6 -19
View File
@@ -8,28 +8,14 @@ from functools import lru_cache
import re
import sys
from PIL import Image
from . import get_program
from . import get_program, get_version
from ..exceptions import SubprocessOutputError, MissingDependencyError
from ..helpers import fspath
@lru_cache(maxsize=1)
def version():
args_gs = [
get_program('gs'),
'--version'
]
try:
proc = run(
args_gs, close_fds=True, universal_newlines=True,
stdout=PIPE, stderr=STDOUT, check=True)
ver = proc.stdout
except CalledProcessError as e:
print("Could not find Ghostscript executable on system PATH.",
file=sys.stderr)
raise MissingDependencyError from e
return ver.strip()
return get_version('gs')
def _gs_error_reported(stream):
@@ -51,7 +37,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
:param yres:
:param raster_device:
:param log:
:param pageno: page number to rasterize
:param pageno: page number to rasterize (beginning at page 1)
:param page_dpi: resolution tuple (x, y) overriding output image DPI
:return:
"""
@@ -69,11 +55,11 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
'-sDEVICE=%s' % raster_device,
'-dFirstPage=%i' % pageno,
'-dLastPage=%i' % pageno,
'-o', tmp.name,
'-r{0}x{1}'.format(str(int_res[0]), str(int_res[1])),
'-o', tmp.name,
fspath(input_file)
]
p = run(args_gs, stdout=PIPE, stderr=STDOUT,
universal_newlines=True)
if _gs_error_reported(p.stdout):
@@ -89,6 +75,7 @@ def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
# if the resolution happens to be fractional, then the discrepancy
# would change the size of the output page, especially if the DPI
# is quite low. Resize the image to the expected size
tmp.seek(0)
with Image.open(tmp) as im:
expected_size = round(im.size[0] / int_res[0] * res[0]), \
+93 -24
View File
@@ -1,33 +1,22 @@
#!/usr/bin/env python3
# © 2017 James R. Barlow: github.com/jbarlow83
from subprocess import CalledProcessError, STDOUT, PIPE, run, check_output
from subprocess import CalledProcessError, STDOUT, PIPE, run
from functools import lru_cache
import sys
import os
import re
import resource
from ..exceptions import InputFileError, SubprocessOutputError, \
MissingDependencyError, EncryptedPdfError
from . import get_program
from . import get_program, get_version
from ..helpers import re_symlink
@lru_cache(maxsize=1)
def version():
args_qpdf = [
get_program('qpdf'),
'--version'
]
try:
p = run(args_qpdf, universal_newlines=True, stderr=STDOUT,
stdout=PIPE)
except CalledProcessError as e:
print("Could not find qpdf executable on system PATH.",
file=sys.stderr)
raise MissingDependencyError() from e
qpdf_version = re.match(r'qpdf version (.+)', p.stdout).group(1)
return qpdf_version
return get_version('qpdf', regex=r'qpdf version (.+)')
def check(input_file, log=None):
@@ -41,7 +30,8 @@ def check(input_file, log=None):
import logging as log
try:
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
run(args_qpdf, stderr=STDOUT, stdout=PIPE, universal_newlines=True,
check=True)
except CalledProcessError as e:
if e.returncode == 2:
log.error("{0}: not a valid PDF, and could not repair it.".format(
@@ -70,11 +60,11 @@ def repair(input_file, output_file, log):
get_program('qpdf'), input_file, output_file
]
try:
check_output(args_qpdf, stderr=STDOUT, universal_newlines=True)
run(args_qpdf, stderr=STDOUT, stdout=PIPE, universal_newlines=True,
check=True)
except CalledProcessError as e:
if e.returncode == 3 and e.output.find("operation succeeded"):
log.debug('qpdf found and fixed errors: ' + e.output)
log.debug(e.output)
return
if _probably_encrypted(e):
@@ -95,9 +85,9 @@ def repair(input_file, output_file, log):
def get_npages(input_file, log):
try:
pages = check_output(
pages = run(
[get_program('qpdf'), '--show-npages', input_file],
universal_newlines=True, close_fds=True)
universal_newlines=True, check=True, stdout=PIPE, stderr=STDOUT)
except CalledProcessError as e:
if e.returncode == 2 and e.output.find('No such file'):
log.error(e.output)
@@ -120,18 +110,97 @@ def split_pages(input_file, work_folder, npages):
run(args_qpdf, check=True)
def merge(input_files, output_file, min_version=None):
def _merge_inner(input_files, output_file, min_version=None, log=None):
"""Merge the list of input files (all filenames) into the output file.
The input files may contain one or more pages.
"""
# Single page 'merges' should still be attempted to that the same error
# checking is applied to single page case
version_arg = ['--min-version={}'.format(min_version)] \
if min_version else []
if log is None:
import logging as log
args_qpdf = [
get_program('qpdf')
] + version_arg + [
input_files[0], '--pages'
] + input_files + ['--', output_file]
run(args_qpdf, check=True)
try:
run(args_qpdf, check=True, stderr=PIPE, universal_newlines=True)
except CalledProcessError as e:
if e.returncode == 3 and \
e.stderr.find("unknown token while reading object") and \
e.stderr.find("operation succeeded"):
# Only whitelist the 'unknown token' problem (decimal/string issue)
# qpdf issue #165
log.warning('qpdf found and fixed errors: ' + e.stderr)
return
raise e from e
def merge(input_files, output_file, min_version=None, log=None, max_files=None):
"""Merge the list of input files (all filenames) into the output file.
The input files may contain one or more pages.
"""
# qpdf requires that every file that contributes to the output has a file
# descriptor that remains open. That means, given our approach of one
# intermediate PDF per, we can practically hit the number of file
# descriptors.
if max_files is None or max_files < 2:
# Find out how many open file descriptors we can get away with
ulimits = resource.getrlimit(resource.RLIMIT_NOFILE)
max_open_files = ulimits[0]
max_files = max_open_files // 2 # Conservative guess
# We'll write things alongside the output file
output_dir = os.path.dirname(output_file)
import random
import string
def randstr():
return ''.join(random.sample(string.ascii_lowercase, 6))
# How many files to grab at once, merging all their contents
step_size = max_files
workqueue = input_files.copy()
counter = 1
next_workqueue = []
while len(workqueue) > 1 or len(next_workqueue) > 0:
# Take n files out of the queue
n = min(step_size, len(workqueue))
job = workqueue[0:n]
del workqueue[0:n]
print('merging ' + repr(job))
# Merge them into 1 file, which will contain n^depth pages
merge_file = os.path.join(
output_dir, "merge-{:06d}-{}.pdf".format(counter, randstr()))
counter += 1
_merge_inner(job, merge_file, min_version=min_version, log=log)
# On the next
next_workqueue.append(merge_file)
print('next_workqueue ' + repr(next_workqueue))
# If we're out of things to do in this queue, move on to the next
# queue. On the counter-th pass of the workqueue we can chew through
# (step_size)**N pages, so on most systems the second pass finishes
# the job.
if len(workqueue) == 0:
workqueue = next_workqueue
next_workqueue = []
re_symlink(workqueue.pop(), output_file)
+30 -34
View File
@@ -14,7 +14,7 @@ from subprocess import PIPE, CalledProcessError, \
from ..exceptions import MissingDependencyError, TesseractConfigError
from ..helpers import page_number
from . import get_program
from . import get_program, get_version
OrientationConfidence = namedtuple(
'OrientationConfidence',
@@ -40,21 +40,7 @@ HOCR_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
@lru_cache(maxsize=1)
def version():
args_tess = [
get_program('tesseract'),
'--version'
]
try:
versions = check_output(
args_tess, close_fds=True, universal_newlines=True,
stderr=STDOUT)
except CalledProcessError as e:
print("Could not find Tesseract executable on system PATH.",
file=sys.stderr)
raise MissingDependencyError from e
tesseract_version = re.match(r'tesseract\s(.+)', versions).group(1)
return tesseract_version
return get_version('tesseract', regex=r'tesseract\s(.+)')
def v4():
@@ -76,8 +62,7 @@ def has_textonly_pdf():
params = ''
try:
params = check_output(
args_tess, close_fds=True, universal_newlines=True,
stderr=STDOUT)
args_tess, universal_newlines=True, stderr=STDOUT)
except CalledProcessError as e:
print("Could not --print-parameters from tesseract",
file=sys.stderr)
@@ -100,8 +85,7 @@ def languages():
]
try:
langs = check_output(
args_tess, close_fds=True, universal_newlines=True,
stderr=STDOUT)
args_tess, universal_newlines=True, stderr=STDOUT)
except CalledProcessError as e:
msg = dedent("""Tesseract failed to report available languages.
Output from Tesseract:
@@ -134,19 +118,18 @@ def get_orientation(input_file, language: list, engine_mode, timeout: float,
try:
stdout = check_output(
args_tesseract, close_fds=True, stderr=STDOUT,
universal_newlines=True, timeout=timeout)
args_tesseract, stderr=STDOUT, timeout=timeout)
except TimeoutExpired:
return OrientationConfidence(angle=0, confidence=0.0)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
if ('Too few characters. Skipping this page' in e.output or
'Image too large' in e.output):
if (b'Too few characters. Skipping this page' in e.output or
b'Image too large' in e.output):
return OrientationConfidence(0, 0)
raise e from e
else:
osd = {}
for line in stdout.splitlines():
for line in stdout.decode().splitlines():
line = line.strip()
parts = line.split(':', maxsplit=2)
if len(parts) == 2:
@@ -171,8 +154,17 @@ def get_orientation(input_file, language: list, engine_mode, timeout: float,
def tesseract_log_output(log, stdout, input_file):
lines = stdout.splitlines()
prefix = "{0:4d}: [tesseract] ".format(page_number(input_file))
try:
text = stdout.decode()
except UnicodeDecodeError as e:
log.error(prefix + "command line output was not utf-8. " +
"This usually means Tesseract's language packs do not match "
"the installed version of Tesseract.")
text = stdout.decode('utf-8', 'backslashreplace')
lines = text.splitlines()
for line in lines:
if line.startswith("Tesseract Open Source"):
continue
@@ -245,8 +237,8 @@ def generate_hocr(input_file, output_files, language: list, engine_mode,
try:
log.debug(args_tesseract)
stdout = check_output(
args_tesseract, close_fds=True, stderr=STDOUT,
universal_newlines=True, timeout=timeout)
args_tesseract, stderr=STDOUT,
timeout=timeout)
except TimeoutExpired:
# Generate a HOCR file with no recognized text if tesseract times out
# Temporary workaround to hocrTransform not being able to function if
@@ -255,9 +247,9 @@ def generate_hocr(input_file, output_files, language: list, engine_mode,
_generate_null_hocr(output_hocr, output_sidecar, input_file)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_file)
if 'read_params_file: parameter not found' in e.output:
if b'read_params_file: parameter not found' in e.output:
raise TesseractConfigError() from e
if 'Image too large' in e.output:
if b'Image too large' in e.output:
_generate_null_hocr(output_hocr, output_sidecar, input_file)
return
@@ -287,6 +279,10 @@ def use_skip_page(text_only, skip_pdf, output_pdf, output_text):
with open(output_pdf, 'wb') as out:
pdf_out = pypdf.PdfFileWriter()
w, h = page0.mediaBox.getWidth(), page0.mediaBox.getHeight()
# If skip page has a /Rotate key, replicate the rotation
rotation = int(page0.get('/Rotate', 0))
if rotation % 180 == 90:
w, h = h, w
pdf_out.addBlankPage(w, h)
pdf_out.write(out)
@@ -338,8 +334,8 @@ def generate_pdf(*, input_image, skip_pdf, output_pdf, output_text,
try:
log.debug(args_tesseract)
stdout = check_output(
args_tesseract, close_fds=True, stderr=STDOUT,
universal_newlines=True, timeout=timeout)
args_tesseract, stderr=STDOUT,
timeout=timeout)
if os.path.exists(prefix + '.txt'):
shutil.move(prefix + '.txt', output_text)
except TimeoutExpired:
@@ -347,10 +343,10 @@ def generate_pdf(*, input_image, skip_pdf, output_pdf, output_text,
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
except CalledProcessError as e:
tesseract_log_output(log, e.output, input_image)
if 'read_params_file: parameter not found' in e.output:
if b'read_params_file: parameter not found' in e.output:
raise TesseractConfigError() from e
if 'Image too large' in e.output:
if b'Image too large' in e.output:
use_skip_page(text_only, skip_pdf, output_pdf, output_text)
return
raise e from e
+6 -13
View File
@@ -9,19 +9,7 @@ import sys
import os
from functools import lru_cache
from ..exceptions import MissingDependencyError
from . import get_program
@lru_cache(maxsize=1)
def version():
args_unpaper = [
get_program('unpaper'),
'--version'
]
ver = check_output(
args_unpaper, close_fds=True, universal_newlines=True,
stderr=STDOUT, timeout=5)
return ver.strip()
from . import get_program, get_version
try:
@@ -31,6 +19,11 @@ except ImportError:
raise
@lru_cache(maxsize=1)
def version():
return get_version('unpaper')
def run(input_file, output_file, dpi, log, mode_args):
args_unpaper = [
get_program('unpaper'),
+1 -1
View File
@@ -32,7 +32,7 @@ def re_symlink(input_file, soft_link_name, log=None):
"%s exists and is not a link" % soft_link_name)
try:
os.unlink(soft_link_name)
except:
except OSError:
prdebug("Can't unlink %s" % (soft_link_name))
if not os.path.exists(input_file):
+28 -7
View File
@@ -1,12 +1,33 @@
#!/usr/bin/env python3
##############################################################################
# Copyright (c) 2013-14: fritz-hh from Github
# (https://github.com/fritz-hh)
#
# Copyright (c) 2010: Jonathan Brinley from Github
# (https://github.com/jbrinley/HocrConverter)
# Initial version by Jonathan Brinley, jonathanbrinley@gmail.com
##############################################################################
# Copyright (c) 2010, Jonathan Brinley
# Original version from: https://github.com/jbrinley/HocrConverter
#
# Copyright (c) 2013-14, Julien Pfefferkorn
# Modifications
#
# Copyright (c) 2015-16, James R. Barlow
# Set text to transparent
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.units import inch
from xml.etree import ElementTree
+26 -10
View File
@@ -6,7 +6,7 @@ import sys
import os
import shutil
import img2pdf
import re
import PyPDF2 as pypdf
from PIL import Image
@@ -149,6 +149,22 @@ def triage_image_file(input_file, output_file, log, options):
raise UnsupportedImageFormatError() from e
def _pdf_guess_version(input_file, search_window=1024):
"""Try to find version signature at start of file.
Not robust enough to deal with appended files.
Returns empty string if not found, indicating file is probably not PDF.
"""
with open(input_file, 'rb') as f:
signature = f.read(1024)
m = re.search(b'%PDF-(\d\.\d)', signature)
if m:
return m.group(1)
return ''
def triage(
input_file,
output_file,
@@ -157,14 +173,12 @@ def triage(
options = context.get_options()
try:
with open(input_file, 'rb') as f:
signature = f.read(1024)
if b'%PDF' in signature:
if options.image_dpi:
log.warning("Argument --image-dpi ignored because the "
"input file is a PDF, not an image.")
re_symlink(input_file, output_file, log)
return
if _pdf_guess_version(input_file):
if options.image_dpi:
log.warning("Argument --image-dpi ignored because the "
"input file is a PDF, not an image.")
re_symlink(input_file, output_file, log)
return
except EnvironmentError as e:
log.error(e)
raise InputFileError() from e
@@ -914,6 +928,7 @@ def merge_pages_qpdf(
first_page = pypdf.PdfFileReader(pdf_pages[0])
writer = pypdf.PdfFileWriter()
writer._header = b'%PDF-' + _pdf_guess_version(pdf_pages[0]) # copy version from source
writer.appendPagesFromReader(first_page)
writer.addMetadata(pdfmark)
writer_file = pdf_pages[0].replace('.pdf', '.metadata.pdf')
@@ -923,7 +938,8 @@ def merge_pages_qpdf(
pdf_pages[0] = writer_file
qpdf.merge(input_files=pdf_pages, output_file=output_file,
min_version=context.get_pdfinfo().min_version)
min_version=context.get_pdfinfo().min_version,
log=log)
def merge_sidecars(
+3 -3
View File
@@ -2,8 +2,8 @@
# setup.py lists a separate set of requirements that are looser to simplify
# installation
ruffus == 2.6.3
Pillow == 4.1.1
Pillow == 5.0.0
reportlab == 3.4.0
PyPDF2 == 1.26.0
img2pdf == 0.2.3
cffi == 1.10.0
img2pdf == 0.2.4
cffi == 1.11.2
+3 -1
View File
@@ -217,9 +217,11 @@ setup(
"Topic :: Text Processing :: Indexing",
"Topic :: Text Processing :: Linguistic",
],
python_requires='>=3.5',
setup_requires=[
'setuptools_scm', # so that version will work
'cffi>=1.9.1' # to build the leptonica module
'cffi>=1.9.1', # to build the leptonica module
'pytest-runner' # to enable python setup.py test
],
use_scm_version={'version_scheme': 'post-release'},
cffi_modules=[
+3 -2
View File
@@ -1,4 +1,5 @@
pytest >= 3.0
pytest >= 3.2
pytest-helpers-namespace
pytest-xdist
pytest-cov
pytest-cov
pytest-timeout
+9 -3
View File
@@ -61,6 +61,9 @@ under the terms of the license in LICENSE.rst.
* - cmyk.pdf
- @jbarlow83
- a CMYK image created in Photoshop
* - crom.png
- @jbarlow83
- test for non-dictionary words
* - enormous.pdf
- @jbarlow83
- very large PDF page
@@ -69,7 +72,7 @@ under the terms of the license in LICENSE.rst.
- a linearized PDF containing some unusual indirect objects, created by an Epson printer; printout of a Wikipedia article (CC BY-SA)
* - formxobject.pdf
- @jbarlow83
- hand-crafted exotic PDF containing an image inside a Form XObject
- hand-crafted PDF containing an image inside a Form XObject
* - francais.pdf
- @jbarlow83
- a page containing French accents (diacritics)
@@ -88,12 +91,15 @@ under the terms of the license in LICENSE.rst.
* - overlay.pdf
- @maxandersen
- PDF file generated by PDFPen pro that triggered content stream parse errors
* - negzero.pdf
- @jbarlow83
- copy of formxobject.pdf with token that qpdf doesn't like
* - no_contents.pdf
- @jbarlow83
- synthetic PDF with a blank page that has no /Contents entry
* - crom.png
* - trivial.pdf
- @jbarlow83
- test for non-dictionary words
- smallest possible valid PDF-1.3 with all required fields
Assemblies
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
%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
<< /Contents 4 0 R /MediaBox [ 0 0 300 300 ] /Parent 2 0 R /Resources << >> /Type /Page >>
endobj
4 0 obj
<< /Length 0 >>
stream
endstream
endobj
xref
0 5
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000123 00000 n
0000000229 00000 n
trailer << /Root 1 0 R /Size 5 /ID [<8088c23e0edc07ef4fbed3daa55f52ed><8088c23e0edc07ef4fbed3daa55f52ed>] >>
startxref
277
%%EOF
+1 -1
View File
@@ -22,7 +22,7 @@ not permitted in PDF/A-2, overprint mode not set"""
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__filename__))
print('SPOOFED: ' + os.path.basename(__file__))
sys.exit(0)
gs_args = ['gs'] + sys.argv[1:]
+1 -1
View File
@@ -17,7 +17,7 @@ def real_ghostscript(argv):
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__filename__))
print('SPOOFED: ' + os.path.basename(__file__))
sys.exit(0)
# Unless some argument is calling for PDFA generation, forward to
+1 -1
View File
@@ -16,7 +16,7 @@ def real_ghostscript(argv):
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__filename__))
print('SPOOFED: ' + os.path.basename(__file__))
sys.exit(0)
# For any rendering calls (device == pdfwrite) call real ghostscript
+1 -1
View File
@@ -16,7 +16,7 @@ def real_ghostscript(argv):
def main():
if '--version' in sys.argv:
print('9.20')
print('SPOOFED: ' + os.path.basename(__filename__))
print('SPOOFED: ' + os.path.basename(__file__))
sys.exit(0)
# For any rasterize calls (device != pdfwrite) call real ghostscript
+3
View File
@@ -5,6 +5,9 @@ import sys
def main():
if sys.argv[-1] == '--version':
print('qpdf version 7.0.0')
sys.exit(0)
print('qpdf dummy')
sys.exit(2)
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
# © 2017 James R. Barlow: github.com/jbarlow83
import sys
import img2pdf
import PyPDF2 as pypdf
from PIL import Image
"""Tesseract bad utf8 spoof
In 'hocr' mode or 'pdf' mode, return error code 1 and some non-Unicode
text because tesseract seems to do that in some cases related to
language pack version mismatches
"""
VERSION_STRING = '''tesseract 3.05.01
leptonica-1.72
libjpeg 8d : libpng 1.6.19 : libtiff 4.0.6 : zlib 1.2.5
SPOOFED
'''
# Japanese "Invalid UTF-8" encoded in Shift JIS
BAD_UTF8 = b'\x96\xb3\x8c\xf8\x82\xc8UTF-8\x0a'
def main():
if sys.argv[1] == '--version':
print(VERSION_STRING, file=sys.stderr)
sys.exit(0)
elif sys.argv[1] == '--list-langs':
print('List of available languages (1):\neng', file=sys.stderr)
sys.exit(0)
elif sys.argv[1] == '--print-parameters':
print("Some parameters", file=sys.stderr)
print("textonly_pdf\t1\tSome help text")
sys.exit(0)
elif sys.argv[-2] in ('hocr', 'pdf'):
sys.stdout.buffer.write(BAD_UTF8)
sys.exit(1)
elif sys.argv[-1] == 'stdout':
inputf = sys.argv[-2]
print("""Orientation: 0
Orientation in degrees: 0
Orientation confidence: 100.00
Script: 1
Script confidence: 100.00""", file=sys.stderr)
else:
print("Spoof doesn't understand arguments", file=sys.stderr)
print(sys.argv, file=sys.stderr)
sys.exit(1)
sys.exit(0)
if __name__ == '__main__':
main()
+3 -2
View File
@@ -73,10 +73,11 @@ def main():
output = sys.argv[-3]
with Image.open(inputf) as im:
dpi = im.info['dpi']
imsize = im.size[0] * dpi[0] / 72, im.size[1] * dpi[1] / 72
pagesize = im.size[0] / dpi[0], im.size[1] / dpi[1]
ptsize = pagesize[0] * 72, pagesize[1] * 72
pdf_out = pypdf.PdfFileWriter()
pdf_out.addBlankPage(imsize[0], imsize[1])
pdf_out.addBlankPage(ptsize[0], ptsize[1])
with open(output + '.pdf', 'wb') as f:
pdf_out.write(f)
with open(output + '.txt', 'w') as f:
+86 -9
View File
@@ -1,19 +1,21 @@
#!/usr/bin/env python3
# © 2015 James R. Barlow: github.com/jbarlow83
# © 2015-17 James R. Barlow: github.com/jbarlow83
from subprocess import Popen, PIPE, check_output, check_call, DEVNULL
import os
import shutil
import resource
import pytest
from ocrmypdf.pdfinfo import PdfInfo, Colorspace, Encoding
import PyPDF2 as pypdf
from ocrmypdf.exceptions import ExitCode
from ocrmypdf import leptonica
from ocrmypdf.pdfa import file_claims_pdfa
from ocrmypdf.exec import ghostscript, tesseract
from ocrmypdf.exec import ghostscript, tesseract, qpdf
import logging
from math import isclose
import PIL
check_ocrmypdf = pytest.helpers.check_ocrmypdf
run_ocrmypdf = pytest.helpers.run_ocrmypdf
@@ -55,6 +57,11 @@ def spoof_no_tess_gs_raster_fail():
return spoof(tesseract='tesseract_noop.py', gs='gs_raster_failure.py')
@pytest.fixture
def spoof_tess_bad_utf8():
return spoof(tesseract='tesseract_badutf8.py')
@pytest.fixture
def spoof_qpdf_always_error():
return spoof(qpdf='qpdf_dummy_return2.py')
@@ -67,7 +74,7 @@ def test_quick(spoof_tesseract_cache, resources, outpdf):
def test_deskew(spoof_tesseract_noop, resources, outdir):
# Run with deskew
deskewed_pdf = check_ocrmypdf(
resources / 'skew.pdf', outdir / 'skew.pdf', '-d', '-v', '1',
resources / 'skew.pdf', outdir / 'skew.pdf', '-d',
env=spoof_tesseract_noop)
# Now render as an image again and use Leptonica to find the skew angle
@@ -82,7 +89,8 @@ def test_deskew(spoof_tesseract_noop, resources, outdir):
xres=150,
yres=150,
raster_device='pngmono',
log=log)
log=log,
pageno=1)
from ocrmypdf.leptonica import Pix
pix = Pix.read(str(deskewed_png))
@@ -121,8 +129,8 @@ def test_remove_background(spoof_tesseract_noop, resources, outdir):
xres=100,
yres=100,
raster_device='png16m',
log=log)
log=log,
pageno=1)
# The output image should contain pure white and black
im = Image.open(output_png)
@@ -515,7 +523,9 @@ def test_tesseract_image_too_big(renderer, spoof_tesseract_big_image_error,
resources, outpdf):
check_ocrmypdf(
resources / 'hugemono.pdf', outpdf, '-r',
'--pdf-renderer', renderer, env=spoof_tesseract_big_image_error)
'--pdf-renderer', renderer,
'--max-image-mpixels', '0',
env=spoof_tesseract_big_image_error)
def test_no_unpaper(resources, no_outpdf):
@@ -1004,4 +1014,71 @@ def test_pdfa_1(spoof_tesseract_cache, resources, outpdf):
)
pdfa_info = file_claims_pdfa(outpdf)
assert pdfa_info['conformance'] == 'PDF/A-1B'
assert pdfa_info['conformance'] == 'PDF/A-1B'
def test_bad_locale():
env = os.environ.copy()
env['LC_ALL'] = 'C'
p, out, err = run_ocrmypdf(
'a', 'b', env=env
)
assert out == '', "stdout not clean"
assert p.returncode != 0
assert 'configured to use ASCII as encoding' in err, "should whine"
@pytest.mark.parametrize('renderer', RENDERERS)
def test_bad_utf8(spoof_tess_bad_utf8, renderer, resources, no_outpdf):
p, out, err = run_ocrmypdf(
resources / 'ccitt.pdf', no_outpdf,
'--pdf-renderer', renderer,
env=spoof_tess_bad_utf8
)
assert out == '', "stdout not clean"
assert p.returncode != 0
assert 'not utf-8' in err, "should whine about utf-8"
assert '\\x96' in err, 'should repeat backslash encoded output'
@pytest.mark.skipif(
not tesseract.has_textonly_pdf(),
reason="issue only affects sandwich")
def test_rotate_deskew_timeout(resources, outdir):
check_ocrmypdf(
resources / 'rotated_skew.pdf',
outdir / 'deskewed.pdf',
'--deskew',
'--tesseract-timeout', '0',
'--pdf-renderer', 'sandwich'
)
correlation = check_monochrome_correlation(
outdir,
reference_pdf=resources / 'ccitt.pdf',
reference_pageno=1,
test_pdf=outdir / 'deskewed.pdf',
test_pageno=1)
# Confirm that the page still got deskewed
assert correlation > 0.50
@pytest.mark.skipif(
PIL.PILLOW_VERSION < '5.0.0',
reason="Pillow < 5.0.0 doesn't raise the exception")
def test_decompression_bomb(resources, outpdf):
p, out, err = run_ocrmypdf(
resources / 'hugemono.pdf',
outpdf
)
assert 'decompression bomb' in err
p, out, err = run_ocrmypdf(
resources / 'hugemono.pdf',
outpdf,
'--max-image-mpixels', '2000'
)
assert p.returncode == 0
+55
View File
@@ -0,0 +1,55 @@
# © 2017 James R. Barlow: github.com/jbarlow83
import logging
import resource
import pytest
from ocrmypdf.exec import ghostscript, tesseract, qpdf
from ocrmypdf.pdfinfo import PdfInfo
@pytest.mark.skipif(
qpdf.version() < '7.0.0',
reason="negzero.pdf crashes earlier versions")
def test_qpdf_negative_zero(resources, outpdf):
negzero = resources / 'negzero.pdf'
hugemono = resources / 'hugemono.pdf'
# raises exception on err
qpdf.merge([str(negzero), str(hugemono)], outpdf, log=logging.getLogger())
@pytest.mark.timeout(15)
@pytest.mark.parametrize('max_files,skip', [
(2, 0), # Can we merge correctly without opening more than 2 files at once?
(16, 0), # And does this work properly when we can one-shot it?
(2, 1), # Or playing with even/odd
(3, 0) # Or odd step size
])
def test_qpdf_merge_correctness(resources, outpdf, max_files, skip):
# All of these must be only one page long
inputs = [
'2400dpi.pdf', 'aspect.pdf', 'blank.pdf', 'ccitt.pdf',
'linn.pdf', 'masks.pdf', 'poster.pdf', 'overlay.pdf',
'skew.pdf', 'trivial.pdf']
input_files = [str(resources / f) for f in inputs]
qpdf.merge(
input_files[skip:], outpdf, log=logging.getLogger(),
max_files=max_files)
assert len(PdfInfo(outpdf).pages) == len(input_files[skip:])
@pytest.mark.timeout(15)
@pytest.mark.skipif(
True,
reason='qpdf binary cannot open multiple files multiple times')
def test_page_merge_ulimit(resources, outpdf):
# Ensure we can merge pages without opening one file descriptor per page
ulimits = resource.getrlimit(resource.RLIMIT_NOFILE)
page_count = ulimits[0]
print(page_count)
input_files = [str(resources / 'trivial.pdf')] * page_count
qpdf.merge(input_files, outpdf, log=logging.getLogger())