Compare commits

...
51 Commits
Author SHA1 Message Date
James R. Barlow c96823a648 v4.1.3 release notes 2016-06-23 13:47:56 -07:00
James R. Barlow 3807b7d655 Merge branch 'feature/leptfun' into develop 2016-06-23 13:45:35 -07:00
James R. Barlow a45505cf1d Fix order of operations in matrix multiplication
Issue #73. The order of operations happens to not matter for scaling
but does matter for translation. We only need scaling to find the DPI,
so the error was not noticed. Mainly useful to other uses of this
library.
2016-06-23 13:36:23 -07:00
James R. Barlow b4a734fc0d Test case for "algorithm 4" test
Algorithm 4 -> PDF version 1.6
2016-06-23 13:21:26 -07:00
James R. Barlow bbd02926e1 Add helpful error message for PDFs that use algorithm 4 2016-06-23 13:13:17 -07:00
jbarlow83andGitHub 5022ded276 Update Windows directions 2016-06-16 15:15:46 -07:00
James R. Barlow c7612152ef leptonica: pillow interop 2016-06-06 23:55:23 -07:00
James R. Barlow af91642cd1 lept: fix __getstate/__setstate 2016-06-06 23:55:23 -07:00
James R. Barlow 9c66334c38 Leptonica - ortho rotate, background norm 2016-06-06 23:55:23 -07:00
James R. Barlow 8d79b94b84 cpix -> _pix 2016-06-06 23:55:23 -07:00
James R. Barlow d7f60b96c1 More leptonica functions for page manipulation 2016-06-06 23:55:23 -07:00
James R. Barlow b964999427 Update filename references from sRGB_IEC to sRGB 2016-05-10 21:58:04 -07:00
James R. Barlow 3473345ea6 Replace sRGB_IEC with MIT license compatible sRGB
New file is from Debian package icc-profiles-free
2016-05-10 21:48:32 -07:00
James R. Barlow 349ec5c81f Provide more helpful error message if pypdf can't merge pages 2016-04-28 14:02:12 -07:00
James R. Barlow ff78d7c56c v4.1 release notes 2016-04-28 00:46:16 -07:00
James R. Barlow ff092c8629 Fix race condition between these tests when run in parallel 2016-04-28 00:39:15 -07:00
James R. Barlow fe14cb57c0 Fix ruffus exception output
I found this issue in ruffus 2.6.3
https://github.com/bunbun/ruffus/issues/65
also discussed here
https://github.com/bunbun/ruffus/pull/67

ruffus 2.6.3 RethrownJobError don't follow the normal conventions and
so its exception causes problems when they cross process boundaries.
This change carefully examines the various forms of ruffus exception
objects that can appear in 2.6.3 and parses them more carefully. It
also removes any direct posting of the exception to the logger because
this triggers another serializing of the exception object, mutating it
further.
2016-04-28 00:38:50 -07:00
James R. Barlow 507fbc01d5 Refactor _find_page_images 2016-04-27 13:13:43 -07:00
James R. Barlow 325479e5be Fix test failure: inline images with multiple image filters specified 2016-04-27 13:09:41 -07:00
James R. Barlow e926ecb8b2 Fuzzing: check for graphics stack overflow
Very unlikely to occur
2016-04-18 13:08:35 -07:00
James R. Barlow d0cb6c0e92 Replace private hypotenuse formula with hypot() 2016-04-18 13:07:53 -07:00
James R. Barlow 5b7c8cf5d3 Remove check for /ImageMask
/ImageMask means the the image is a stencil mask for a grayscale or
color image. From issue #63 a user has a PDF apparently with only a
stencil mask and no other images. According to PDF spec you're supposed
to use masks to help draw other images rather than draw masks
explicitly, although drawing masks is not forbidden.

In the event that the image mask is higher resolution than the input
 image, the composite image should be drawn at the highest DPI to a
ccurately capture the effect of the mask. It looks like the right to
 do is take masks into account rather than ignore them (a holdover
from earlier, less robust versions), or don't treat it different.
2016-04-14 14:24:33 -07:00
James R. Barlow 40baab32ac Remove dead code "import stuff in testcase" 2016-04-14 14:22:34 -07:00
James R. Barlow e877d37ac8 --rotate-pages: Only apply rotation if we're reasonable confident
Take the threshold from tesseract's default value for -psm 1.
2016-04-14 13:49:44 -07:00
James R. Barlow 5a9f77e438 Merge commit '1605408c23fa1b9252c5d3f10f279b43733b0728' into develop 2016-04-14 13:07:10 -07:00
James R. Barlow 8ddd67d1e2 Check encoding of inline images 2016-03-24 15:11:10 -07:00
jbarlow83 1605408c23 README: add libffi-dev 2016-03-22 23:39:02 -07:00
James R. Barlow 2d3b1ebf6e Simplify DPI calculation with algebraic derivation
Needs testing
2016-03-20 12:35:41 -07:00
James R. Barlow c74eaab7f5 Update license: sRGB ICC 2016-03-20 12:34:34 -07:00
James R. Barlow c21d231388 Merge commit 'a73afc4e769202b916d35dee481d741cf6bb7224' 2016-03-20 12:33:15 -07:00
jbarlow83 a73afc4e76 Merge pull request #59 from spwhitton/apt-get
README: Debian and Ubuntu installation option
2016-03-14 00:19:59 -07:00
Sean Whitton 76c364150d README: Debian and Ubuntu installation option 2016-03-13 23:15:00 -07:00
James R. Barlow 94a3e447cc Add otsu threshold to leptonica 2016-03-12 00:09:20 -08:00
James R. Barlow 12868b461a Travis: install unpaper.deb instead of compiling from source 2016-03-11 21:16:01 -08:00
James R. Barlow 322085933b unpaper: fix check for missing and old versions, add test case 2016-03-10 15:37:09 -08:00
James R. Barlow 3fed94bb79 v4.0.7 2016-03-02 06:27:01 -08:00
James R. Barlow 8c877482bd Fix leptonica initializers 2016-03-02 06:26:25 -08:00
James R. Barlow b17d589e84 Don't set -sOutputICCProfile
Ghostscript dev advised against. It appears that this is for
creating target for a device that colors in a particular format.
2016-03-02 06:25:34 -08:00
James R. Barlow 368252a243 setuptools_scm_git_archive seems suddenly broken 2016-03-01 02:09:45 -08:00
James R. Barlow ccefda1bee v4.0.6 notes 2016-03-01 01:58:32 -08:00
James R. Barlow 3d0e8c9629 Provide our own sRGB profile instead of Ghostscript's 2016-03-01 01:27:40 -08:00
James R. Barlow 313bbbb94c setup_scm_git_archive: add additional files 2016-02-29 12:46:27 -08:00
James R. Barlow 0360f078de get_postscript_icc_path: don't check the same path multiple times 2016-02-29 12:45:58 -08:00
James R. Barlow c8901666c4 Merge branch 'master' of https://github.com/jbarlow83/OCRmyPDF 2016-02-29 00:06:07 -08:00
James R. Barlow 7430006596 Improve install instructions for OS X (unpaper) 2016-02-29 00:05:31 -08:00
James R. Barlow f3e06b2dbd Add bookmarks to file for more testing 2016-02-29 00:05:07 -08:00
jbarlow83 e97df307ff Merge pull request #54 from stweil/master
Replace broken link to c't article by permalink
2016-02-28 07:18:40 -08:00
Stefan Weil 1443354aa2 Replace broken link to c't article by permalink
Update also the 2nd article link to use a permalink, too.

Signed-off-by: Stefan Weil <sw@weilnetz.de>
2016-02-28 13:57:42 +01:00
James R. Barlow 250e68c1cd v4.0.5 release notes 2016-02-27 01:01:38 -08:00
James R. Barlow 6a380ee99c Fix temporary file placed in wrong folder 2016-02-27 00:51:47 -08:00
James R. Barlow 3c90bd96a9 Remove extraneous debug print() messages 2016-02-27 00:50:58 -08:00
19 changed files with 649 additions and 191 deletions
+1
View File
@@ -0,0 +1 @@
ref-names: $Format:%D$
+3 -1
View File
@@ -5,4 +5,6 @@
# (binary is a macro for -text -diff)
*.jar binary
*.pdf binary
*.PDF binary
*.PDF binary
.git_archival.txt export-subst
+5 -7
View File
@@ -5,7 +5,7 @@ cache: pip
cache:
directories:
- $HOME/.ccache
- tarballs
- packages
- tests/cache
python:
@@ -21,17 +21,15 @@ before_install:
- sudo add-apt-repository ppa:b-eltzner/qpdfview-exp -y # for QPDF 5
- sudo add-apt-repository ppa:itachi-san/ffmpeg -y # for libav 11.2 (for unpaper)
- sudo apt-get update -qq # must go after all add-apt-repo
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils gcc libavformat-dev libavcodec-dev libavutil-dev automake make pkg-config xsltproc libffi-dev
- sudo apt-get install -y ghostscript tesseract-ocr tesseract-ocr-deu tesseract-ocr-eng tesseract-ocr-fra qpdf poppler-utils libavformat56 libavcodec56 libavutil54 libffi-dev
# pip
- pip install --upgrade pip
# Download, make and install unpaper (using ccache)
- mkdir -p tarballs
- "[ -f tarballs/unpaper-6.1.tar.xz ] || wget -q https://www.flameeyes.eu/files/unpaper-6.1.tar.xz -O tarballs/unpaper-6.1.tar.xz"
- tar -xvf tarballs/unpaper-6.1.tar.xz
- export PATH="/usr/lib/ccache:$PATH"
- pushd unpaper-6.1 && ./configure --prefix=/usr && make -j && sudo make install && popd
- mkdir -p packages
- "[ -f packages/unpaper_6.1-1.deb ] || wget -q https://dl.dropboxusercontent.com/u/28971240/unpaper_6.1-1.deb -O packages/unpaper_6.1-1.deb"
- sudo dpkg -i packages/unpaper_6.1-1.deb
install:
- pip install -r requirements.txt
+18 -5
View File
@@ -48,6 +48,12 @@ Download OCRmyPDF here: https://github.com/jbarlow83/OCRmyPDF/releases
You can install it to a Python virtual environment or system-wide.
Debian and Ubuntu
~~~~~~~~~~~~~~~~~
Users of Debian 9 or later or Ubuntu 16.10 or later may simply
``apt-get install ocrmypdf``.
Installing the Docker image
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -116,8 +122,8 @@ Install or upgrade the required Homebrew packages, if any are missing::
brew install qpdf
brew install ghostscript
brew install python3
brew install libxml2
brew install leptonica
brew install libxml2 libffi leptonica
brew install unpaper # optional
brew install tesseract
Update the homebrew pip and install Pillow::
@@ -148,6 +154,7 @@ Install system dependencies::
sudo apt-get install \
zlib1g-dev \
libjpeg-dev \
libffi-dev \
ghostscript \
tesseract-ocr \
qpdf \
@@ -180,7 +187,13 @@ Ubuntu 14.04 only installs ``unpaper`` version 0.4.2, which is not supported by
Installing on Windows
~~~~~~~~~~~~~~~~~~~~~
Direct installation on Windows is not possible. Install the Docker container as described above.
Direct installation on Windows is not possible. Install the Docker container as described above. Ensure that your command prompt can run the docker "hello world" container.
The command line syntax to run ocrmypdf from a command prompt will resemble::
docker run -v /c/Users/sampleuser:/home/docker ocrmypdf --skip-text test.pdf output.pdf
where /c/Users/sampleuser is a Unix representation of the Windows path C:\Users\sampleuser, assuming a user named "sampleuser" is running ocrmypdf on a file in their home directory, and the files "test.pdf" and "output.pdf" are in the sampleuser folder. The Windows user must have read and write permissions.
Installing HEAD revision from sources
@@ -252,11 +265,11 @@ In case you detect an issue, please:
Press & Media
-------------
- `c't 1-2014, page 59 <http://www.heise.de/ct/inhalt/2014/1/58/>`__:
- `c't 1-2014, page 59 <http://heise.de/-2279695>`__:
Detailed presentation of OCRmyPDF v1.0 in the leading German IT
magazine c't
- `heise Open Source, 09/2014: Texterkennung mit
OCRmyPDF <http://www.heise.de/-2356670>`__
OCRmyPDF <http://heise.de/-2356670>`__
Disclaimer
----------
+45 -1
View File
@@ -6,15 +6,59 @@ Please always read this file before installing the package
Download software here: https://github.com/jbarlow83/OCRmyPDF/tags
v4.0.4:
v4.1.3:
=======
- More helpful error message for PDFs with version 4 security handler
- Update usage instructions for Windows/Docker users
- Fix order of operations for matrix multiplication (no effect on most users)
- Add a few leptonica wrapper functions (no effect on most users)
v4.1.2:
=======
- Replace IEC sRGB ICC profile with Debian's sRGB (from icc-profiles-free) which is more compatible with the MIT license
- More helpful error message for an error related to certain types of malformed PDFs
v4.1:
=====
- ``--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 with "RethrownJobError" errors during error handling that suppressed the useful error messages
v4.0.7:
=======
- Minor correction to Ghostscript output settings
v4.0.6:
=======
- Update install instructions
- Provide a sRGB profile instead of using Ghostscript's
v4.0.5:
=======
Fixes
-----
- Remove some verbose debug messages from v4.0.4
- Fixed temporary that wasn't being deleted
- DPI is now calculated correctly for cropped images, along with other image transformations
- Inline images are now checked during DPI calculation instead of rejecting the image
v4.0.4:
=======
Released with verbose debug message turned on. Do not use. Skip to v4.0.5.
v4.0.3:
=======
+1
View File
@@ -11,6 +11,7 @@ class ExitCode(IntEnum):
file_access_error = 5
already_done_ocr = 6
child_process_error = 7
encrypted_pdf = 8
other_error = 15
Binary file not shown.
+1 -1
View File
@@ -5,6 +5,7 @@ from tempfile import NamedTemporaryFile
from subprocess import Popen, PIPE, check_call
from shutil import copy
from . import get_program
from .pdfa import SRGB_ICC_PROFILE
def rasterize_pdf(input_file, output_file, xres, yres, raster_device, log,
@@ -52,7 +53,6 @@ def generate_pdfa(pdf_pages, output_file, threads=1):
"-dJPEGQ=95",
"-dPDFA=2",
"-sPDFACompatibilityPolicy=2",
"-sOutputICCProfile=srgb.icc",
"-sOutputFile=" + gs_pdf.name,
]
args_gs.extend(pdf_pages)
+226 -27
View File
@@ -17,6 +17,7 @@ from tempfile import TemporaryFile
from ctypes.util import find_library
from .lib._leptonica import ffi
from functools import lru_cache
from enum import Enum
lept = ffi.dlopen(find_library('lept'))
@@ -83,6 +84,13 @@ class LeptonicaIOError(LeptonicaError):
pass
class RemoveColormap(Enum):
to_binary = 0
to_grayscale = 1
to_full_color = 2
based_on_src = 3
class Pix:
"""Wrapper around leptonica's PIX object.
@@ -98,27 +106,31 @@ class Pix:
in a threadsafe manner if a Python threading.Lock protects the data.
"""
def __init__(self, cpix):
self.cpix = ffi.gc(cpix, Pix._pix_destroy)
def __init__(self, pix):
self._pix = ffi.gc(pix, Pix._pix_destroy)
def __repr__(self):
if self.cpix:
if self._pix:
s = "<leptonica.Pix image size={0}x{1} depth={2} at 0x{3:x}>"
return s.format(self.cpix.w, self.cpix.h, self.cpix.d,
int(ffi.cast("intptr_t", self.cpix)))
return s.format(self._pix.w, self._pix.h, self._pix.d,
int(ffi.cast("intptr_t", self._pix)))
else:
return "<leptonica.Pix image NULL>"
def __getstate__(self):
data = ffi.new('l_uint32 *[]', 1)
size = ffi.new('size_t *', 0)
data = ffi.new('l_uint32 **')
size = ffi.new('size_t *')
err = lept.pixSerializeToMemory(self.cpix, data, size)
err = lept.pixSerializeToMemory(self._pix, data, size)
if err != 0:
raise LeptonicaIOError("pixSerializeToMemory")
char_data = ffi.cast('char *', data[0])
# Copy from C bytes to python bytes()
data_bytes = ffi.buffer(char_data, size[0])[:]
# Can now free C bytes
lept.lept_free(char_data)
return dict(data=data_bytes)
@@ -126,16 +138,41 @@ class Pix:
cdata_bytes = ffi.new('char[]', state['data'])
cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes)
self.cpix = lept.pixDeserializeFromMemory(
pix = lept.pixDeserializeFromMemory(
cdata_uint32, len(state['data']))
Pix.__init__(self, pix)
@property
def width(self):
return self.cpix.w
return self._pix.w
@property
def height(self):
return self.cpix.h
return self._pix.h
@property
def depth(self):
return self._pix.d
@property
def size(self):
return (self._pix.w, self._pix.h)
@property
def info(self):
return {'dpi': (self._pix.xres, self._pix.yres)}
@property
def mode(self):
"Return mode like PIL.Image"
if self.depth == 1:
return '1'
elif self.depth >= 16:
return 'RGB'
elif not self._pix.colormap:
return 'L'
else:
return 'P'
@classmethod
def read(cls, filename):
@@ -158,7 +195,29 @@ class Pix:
with LeptonicaErrorTrap():
lept.pixWriteImpliedFormat(
filename.encode(sys.getfilesystemencoding()),
self.cpix, jpeg_quality, jpeg_progressive)
self._pix, jpeg_quality, jpeg_progressive)
def topil(self):
"Returns a PIL.Image version of this Pix"
from PIL import Image
with LeptonicaErrorTrap():
pix_swapped = Pix(lept.pixEndianByteSwapNew(self._pix))
size = (pix_swapped._pix.wpl * 4, pix_swapped._pix.h)
buf = ffi.buffer(pix_swapped._pix.data, size[0] * size[1])
im_raw = Image.frombytes(self.mode, size, buf, 'raw')
# Leptonica stores images in 32-bit words
# Need to crop the any trailing amount
box = (0, 0, self.width, self.height)
im = im_raw.crop(box)
return im
def show(self):
return self.topil().show()
def deskew(self, reduction_factor=0):
"""Returns the deskewed pix object.
@@ -170,16 +229,21 @@ class Pix:
for skew angle
"""
with LeptonicaErrorTrap():
return Pix(lept.pixDeskew(self.cpix, reduction_factor))
return Pix(lept.pixDeskew(self._pix, reduction_factor))
def scale(self, scalex, scaley):
"Returns the pix object rescaled according to the proportions given."
with LeptonicaErrorTrap():
return Pix(lept.pixScale(self.cpix, scalex, scaley))
return Pix(lept.pixScale(self._pix, scalex, scaley))
def rotate180(self):
with LeptonicaErrorTrap():
return Pix(lept.pixRotate180(ffi.NULL, self.cpix))
return Pix(lept.pixRotate180(ffi.NULL, self._pix))
def rotate_orth(self, quads):
"Orthographic rotation, quads: 0-3, number of clockwise rotations"
with LeptonicaErrorTrap():
return Pix(lept.pixRotateOrth(self._pix, quads))
def find_skew(self):
"""Returns a tuple (deskew angle in degrees, confidence value).
@@ -189,27 +253,125 @@ class Pix:
with LeptonicaErrorTrap():
angle = ffi.new('float *', 0.0)
confidence = ffi.new('float *', 0.0)
result = lept.pixFindSkew(self.cpix, angle, confidence)
result = lept.pixFindSkew(self._pix, angle, confidence)
if result == 0:
return (angle[0], confidence[0])
else:
return (None, None)
def convert_rgb_to_luminance(self):
with LeptonicaErrorTrap():
gray_pix = lept.pixConvertRGBToLuminance(self._pix)
if gray_pix:
return Pix(gray_pix)
return None
def remove_colormap(self, removal_type):
"""Remove a palette
removal_type - RemovalColormap()
"""
with LeptonicaErrorTrap():
return Pix(lept.pixRemoveColormap(self._pix, removal_type))
def otsu_adaptive_threshold(
self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1):
with LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
p_pix = ffi.new('PIX **')
result = lept.pixOtsuAdaptiveThreshold(
self._pix,
sx, sy,
smoothx, smoothy,
scorefract,
ffi.NULL,
p_pix)
if result == 0:
return Pix(p_pix[0])
else:
return None
def otsu_threshold_on_background_norm(
self, mask=None, tile_size=(10, 15), thresh=100, mincount=50,
bgval=255, kernel_size=(2, 2), scorefract=0.1):
with LeptonicaErrorTrap():
sx, sy = tile_size
smoothx, smoothy = kernel_size
if mask is None:
mask = ffi.NULL
if isinstance(mask, Pix):
mask = mask._pix
thresh_pix = lept.pixOtsuThreshOnBackgroundNorm(
self._pix,
mask,
sx, sy,
thresh, mincount, bgval,
smoothx, smoothy,
scorefract,
ffi.NULL
)
if thresh_pix == ffi.NULL:
return None
return Pix(thresh_pix)
def crop_to_foreground(
self, threshold=128, mindist=70, erasedist=30, pagenum=0,
showmorph=0, display=0, pdfdir=ffi.NULL):
with LeptonicaErrorTrap():
cropbox = Box(lept.pixFindPageForeground(
self._pix,
threshold,
mindist,
erasedist,
pagenum,
showmorph,
display,
pdfdir))
print(repr(cropbox))
cropped_pix = lept.pixClipRectangle(
self._pix,
cropbox._box,
ffi.NULL)
return Pix(cropped_pix)
def clean_background_to_white(
self, mask=None, grayscale=None, gamma=1.0, black=0, white=255):
with LeptonicaErrorTrap():
return Pix(lept.pixCleanBackgroundToWhite(
self._pix,
mask or ffi.NULL,
grayscale or ffi.NULL,
gamma,
black,
white))
@staticmethod
@lru_cache(maxsize=1)
def make_pixel_sum_tab8():
return lept.makePixelSumTab8()
@staticmethod
def correlation_binary(pix1, pix2):
if get_leptonica_version() < 'leptonica-1.72':
# Older versions of Leptonica (pre-1.72) have a buggy
# implementation of pixCorrelationBinary that overflows on larger
# images.
pix1_count = ffi.new('l_int32 *', 0)
pix2_count = ffi.new('l_int32 *', 0)
pixn_count = ffi.new('l_int32 *', 0)
tab8 = lept.makePixelSumTab8() # Small memory leak on each call
pix1_count = ffi.new('l_int32 *')
pix2_count = ffi.new('l_int32 *')
pixn_count = ffi.new('l_int32 *')
tab8 = Pix.make_pixel_sum_tab8()
lept.pixCountPixels(pix1.cpix, pix1_count, tab8)
lept.pixCountPixels(pix2.cpix, pix2_count, tab8)
pixn = Pix(lept.pixAnd(ffi.NULL, pix1.cpix, pix2.cpix))
lept.pixCountPixels(pixn.cpix, pixn_count, tab8)
lept.pixCountPixels(pix1._pix, pix1_count, tab8)
lept.pixCountPixels(pix2._pix, pix2_count, tab8)
pixn = Pix(lept.pixAnd(ffi.NULL, pix1._pix, pix2._pix))
lept.pixCountPixels(pixn._pix, pixn_count, tab8)
# Python converts these int32s to larger units as needed
# to avoid overflow. Overflow happens easily here.
@@ -220,7 +382,7 @@ class Pix:
return correlation
else:
correlation = ffi.new('float *', 0.0)
result = lept.pixCorrelationBinary(pix1.cpix, pix2.cpix,
result = lept.pixCorrelationBinary(pix1._pix, pix2._pix,
correlation)
if result != 0:
raise LeptonicaError("Correlation failed")
@@ -228,11 +390,48 @@ class Pix:
@staticmethod
def _pix_destroy(pix):
ptr_to_pix = ffi.new('PIX **', pix)
lept.pixDestroy(ptr_to_pix)
p_pix = ffi.new('PIX **', pix)
lept.pixDestroy(p_pix)
# print('pix destroy ' + repr(pix))
class Box:
"""Wrapper around Leptonica's BOX objects.
See class Pix for notes about reference counting.
"""
def __init__(self, box):
self._box = ffi.gc(box, Box._box_destroy)
def __repr__(self):
if self._box:
return '<leptonica.Box x={0} y={1} w={2} h={3}>'.format(
self.x, self.y, self.w, self.h)
return '<leptonica.Box NULL>'
@property
def x(self):
return self._box.x
@property
def y(self):
return self._box.y
@property
def w(self):
return self._box.w
@property
def h(self):
return self._box.h
@staticmethod
def _box_destroy(box):
p_box = ffi.new('BOX **', box)
lept.boxDestroy(p_box)
@lru_cache(maxsize=1)
def get_leptonica_version():
"""Get Leptonica version string.
+72
View File
@@ -42,6 +42,18 @@ struct PixColormap
l_int32 n; /* number of color entries used */
};
typedef struct PixColormap PIXCMAP;
struct Box
{
l_int32 x;
l_int32 y;
l_int32 w;
l_int32 h;
l_uint32 refcount; /* reference count (1 if no clones) */
};
typedef struct Box BOX;
""")
ffi.cdef("""
@@ -50,10 +62,17 @@ PIX * pixScale ( PIX *pixs, l_float32 scalex, l_float32 scaley );
l_int32 pixFindSkew ( PIX *pixs, l_float32 *pangle, l_float32 *pconf );
l_int32 pixWriteImpliedFormat ( const char *filename, PIX *pix, l_int32 quality, l_int32 progressive );
void pixDestroy ( PIX **ppix );
PIX *
pixEndianByteSwapNew(PIX *pixs);
PIX * pixDeskew ( PIX *pixs, l_int32 redsearch );
char * getLeptonicaVersion ( );
l_int32 pixCorrelationBinary(PIX *pix1, PIX *pix2, l_float32 *pval);
PIX *pixRotate180(PIX *pixd, PIX *pixs);
PIX *
pixRotateOrth(PIX *pixs,
l_int32 quads);
l_int32 pixCountPixels ( PIX *pix, l_int32 *pcount, l_int32 *tab8 );
PIX * pixAnd ( PIX *pixd, PIX *pixs1, PIX *pixs2 );
@@ -62,6 +81,59 @@ l_int32 * makePixelSumTab8 ( void );
PIX * pixDeserializeFromMemory ( const l_uint32 *data, size_t nbytes );
l_int32 pixSerializeToMemory ( PIX *pixs, l_uint32 **pdata, size_t *pnbytes );
PIX * pixConvertRGBToLuminance(PIX *pixs);
PIX * pixRemoveColormap(PIX *pixs, l_int32 type);
l_int32
pixOtsuAdaptiveThreshold(PIX *pixs,
l_int32 sx,
l_int32 sy,
l_int32 smoothx,
l_int32 smoothy,
l_float32 scorefract,
PIX **ppixth,
PIX **ppixd);
PIX *
pixOtsuThreshOnBackgroundNorm(PIX *pixs,
PIX *pixim,
l_int32 sx,
l_int32 sy,
l_int32 thresh,
l_int32 mincount,
l_int32 bgval,
l_int32 smoothx,
l_int32 smoothy,
l_float32 scorefract,
l_int32 *pthresh);
PIX *
pixCleanBackgroundToWhite(PIX *pixs,
PIX *pixim,
PIX *pixg,
l_float32 gamma,
l_int32 blackval,
l_int32 whiteval);
BOX *
pixFindPageForeground(PIX *pixs,
l_int32 threshold,
l_int32 mindist,
l_int32 erasedist,
l_int32 pagenum,
l_int32 showmorph,
l_int32 display,
const char *pdfdir);
PIX *
pixClipRectangle(PIX *pixs,
BOX *box,
BOX **pboxc);
void
boxDestroy(BOX **pbox);
void lept_free(void *ptr);
""")
+131 -44
View File
@@ -188,6 +188,10 @@ advanced.add_argument(
'--tesseract-timeout', default=180.0, type=float, metavar='SECONDS',
help='give up on OCR after the timeout, but copy the preprocessed page '
'into the final output')
advanced.add_argument(
'--rotate-pages-threshold', default=14.0, type=float, metavar='CONFIDENCE',
help="only rotate pages when confidence is above this value (arbitrary "
"units reported by tesseract)")
debugging = parser.add_argument_group(
"Debugging",
@@ -234,13 +238,18 @@ if options.pdf_renderer == 'tesseract' and tesseract.version() < '3.04.01' \
"Some PDF viewers will fail to find searchable text.\n"
"--pdf-renderer=tesseract is not recommended.")
if any((options.deskew, options.clean, options.clean_final)):
if any((options.clean, options.clean_final)):
try:
from . import unpaper
except ImportError:
if unpaper.version() < '6.1':
complain(
"The installed 'unpaper' is not supported. "
"Install version 6.1 or newer.")
sys.exit(ExitCode.missing_dependency)
except FileNotFoundError:
complain(
"Install the 'unpaper' program to use --deskew or --clean.")
sys.exit(ExitCode.bad_args)
sys.exit(ExitCode.missing_dependency)
else:
unpaper = None
@@ -378,7 +387,7 @@ def cleanup_working_files(*args):
@transform(
input=options.input_file,
filter=formatter('(?i)\.pdf'),
output=work_folder + '{basename[0]}.repaired.pdf',
output=os.path.join(work_folder, '{basename[0]}.repaired.pdf'),
extras=[_log, _pdfinfo, _pdfinfo_lock])
def repair_pdf(
input_file,
@@ -529,15 +538,29 @@ def orient_page(
270: ''
}
apply_correction = False
description = ''
if orient_conf.confidence >= options.rotate_pages_threshold:
if orient_conf.angle != 0:
apply_correction = True
description = ' - will rotate'
else:
description = ' - rotation appears correct'
else:
if orient_conf.angle != 0:
description = ' - confidence too low to rotate'
else:
description = ' - no change'
log.info(
'{0:4d}: page is facing {1}, confidence {2:.2f}{3}'.format(
page_number(preview),
direction.get(orient_conf.angle, '?'),
orient_conf.confidence,
' - correcting rotation' if orient_conf.angle != 0 else '')
description)
)
if orient_conf.angle == 0:
if not apply_correction:
re_symlink(page_pdf, output_file)
else:
writer = pypdf.PdfFileWriter()
@@ -774,6 +797,10 @@ def render_hocr_debug_page(
showBoundingboxes=True, invisibleText=False)
class PdfMergeFailedError(Exception):
pass
@active_if(options.pdf_renderer == 'hocr')
@collate(
input=[render_hocr_page, select_image_layer],
@@ -832,8 +859,13 @@ def add_text_layer(
log.info("{0:4d}: rotating image layer {1} degrees".format(
page_number(image), rotation, tx, ty))
page_text.mergeRotatedScaledTranslatedPage(
page_image, rotation, 1.0, tx, ty, expand=False)
try:
page_text.mergeRotatedScaledTranslatedPage(
page_image, rotation, 1.0, tx, ty, expand=False)
except (AttributeError, ValueError) as e:
if 'writeToStream' in str(e) or 'invalid literal' in str(e):
raise PdfMergeFailedError() from e
pdf_output = pypdf.PdfFileWriter()
pdf_output.addPage(page_text)
@@ -1010,51 +1042,106 @@ def cleanup_ruffus_error_message(msg):
return msg
def do_ruffus_exception(ruffus_five_tuple):
"""Replace the elaborate ruffus stack trace with a user friendly
description of the error message that occurred."""
task_name, job_name, exc_name, exc_value, exc_stack = ruffus_five_tuple
if exc_name == 'builtins.SystemExit':
match = re.search(r"\.(.+?)\)", exc_value)
exit_code_name = match.groups()[0]
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
return exit_code
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
_log.error(cleanup_ruffus_error_message(exc_value))
return ExitCode.input_file
elif exc_name == 'builtins.TypeError':
# Even though repair_pdf will fail, ruffus will still try
# to call split_pages with no input files, likely due to a bug
if task_name == 'split_pages':
_log.error("Input file '{0}' is not a valid PDF".format(
options.input_file))
return ExitCode.input_file
elif exc_name == 'subprocess.CalledProcessError':
# It's up to the subprocess handler to report something useful
msg = "Error occurred while running this command:"
_log.error(msg + '\n' + exc_value)
return ExitCode.child_process_error
elif exc_name == 'ocrmypdf.main.PdfMergeFailedError':
_log.error(textwrap.dedent("""\
Failed to merge PDF image layer with OCR layer
Usually this happens because the input PDF file is mal-formed and
ocrmypdf cannot automatically correct the problem on its own.
Try using
ocrmypdf --pdf-renderer tesseract [..other args..]
"""))
return ExitCode.input_file
elif exc_name == 'PyPDF2.utils.PdfReadError' and \
'not been decrypted' in exc_value:
_log.error(textwrap.dedent("""\
Input PDF uses either an encryption algorithm or a PDF security
handler that is not supported by ocrmypdf.
For information about this PDF's security use
qpdf --show-encryption [...input PDF...]
(Only algorithms "R = 1" and "R = 2" are supported.)
"""))
return ExitCode.encrypted_pdf
elif not options.verbose:
_log.error(exc_stack)
return ExitCode.other_error
def traverse_ruffus_exception(e):
"""Walk through a RethrownJobError and find the first exception.
The exit code will be based on this, even if multiple exceptions occurred
at the same time."""
if isinstance(e[0], str) and len(e) == 5:
return do_ruffus_exception(e)
elif hasattr(e, '__iter__'):
for exc in e:
return traverse_ruffus_exception(exc)
def run_pipeline():
if not options.jobs:
options.jobs = available_cpu_count()
try:
options.history_file = os.path.join(work_folder, 'ruffus_history.sqlite')
options.history_file = os.path.join(
work_folder, 'ruffus_history.sqlite')
cmdline.run(options)
except ruffus_exceptions.RethrownJobError as e:
if options.verbose:
_log.debug(e)
_log.debug(str(e)) # stringify exception so logger doesn't have to
# Yuck. Hunt through the ruffus exception to find out what the
# return code is supposed to be.
# Ruffus flattens the exception to a string, throwing away all kinds
# of helpful details
# task_name, job_name - ruffus status
# exc_name - class name of exception
# exc_value - irritating string that makes impossible to recover
# exception object
# exc_stack - string that contains traceback of exception
for exc in e.args:
task_name, job_name, exc_name, exc_value, exc_stack = exc
if exc_name == 'builtins.SystemExit':
match = re.search(r"\.(.+?)\)", exc_value)
exit_code_name = match.groups()[0]
exit_code = getattr(ExitCode, exit_code_name, 'other_error')
return exit_code
elif exc_name == 'ruffus.ruffus_exceptions.MissingInputFileError':
_log.error(cleanup_ruffus_error_message(exc_value))
return ExitCode.input_file
elif exc_name == 'builtins.TypeError':
# Even though repair_pdf will fail, ruffus will still try
# to call split_pages with no input files, likely due to a bug
if task_name == 'split_pages':
_log.error("Input file '{0}' is not a valid PDF".format(
options.input_file))
return ExitCode.input_file
elif exc_name == 'subprocess.CalledProcessError':
# It's up to the subprocess handler to report something useful
msg = "Error occurred while running this command:"
_log.error(msg + '\n' + exc_value)
return ExitCode.child_process_error
elif not options.verbose:
_log.error(e)
# Ruffus flattens exception to 5 element tuples. Because of a bug
# in <= 2.6.3 it may present either the single:
# (task, job, exc, value, stack)
# or something like:
# [[(task, job, exc, value, stack)]]
#
# Generally cross-process exception marshalling doesn't work well
# and ruffus doesn't support because BaseException has its own
# implementation of __reduce__ that attempts to reconstruct the
# exception based on e.__init__(e.args).
#
# Attempting to log the exception directly marshalls it to the logger
# which is probably in another process, so it's better to log only
# data from the exception at this point.
return ExitCode.other_error
exitcode = traverse_ruffus_exception(e.args)
if exitcode is None:
_log.error("Unexpected ruffus exception: " + str(e))
_log.error(repr(e))
return ExitCode.other_error
else:
return exitcode
except Exception as e:
_log.error(e)
return ExitCode.other_error
+55 -51
View File
@@ -3,6 +3,7 @@
from subprocess import Popen, PIPE
from decimal import Decimal, getcontext
from math import hypot
import re
import sys
import PyPDF2 as pypdf
@@ -34,7 +35,12 @@ FRIENDLY_ENCODING = {
'/JPXDecode': 'jpx',
'/JBIG2Decode': 'jbig2',
'/CCF': 'ccitt', # Abbreviations permitted in inline images
'/DCT': 'jpeg'
'/DCT': 'jpeg',
'/AHx': 'asciihex',
'/A85': 'ascii85',
'/LZW': 'lzw',
'/Fl': 'flate',
'/RL': 'runlength'
}
FRIENDLY_COMP = {
@@ -67,14 +73,9 @@ def _shorthand_from_matrix(matrix):
return tuple(map(float, (a, b, c, d, e, f)))
def euclidean_distance(rowvec1, rowvec2):
return ((rowvec1[0] - rowvec2[0]) ** 2
+ (rowvec1[1] - rowvec2[1]) ** 2) ** 0.5
ContentsInfo = namedtuple('ContentsInfo', ['raster_settings', 'inline_images'])
ContentsInfo = namedtuple('ContentsInfo',
['raster_settings', 'inline_images'])
def _interpret_contents(contentstream):
operations = contentstream.operations
stack = []
@@ -82,16 +83,17 @@ def _interpret_contents(contentstream):
image_raster_settings = []
inline_images = []
print(operations)
for op in operations:
operands, command = op
if command == b'q':
stack.append(ctm)
if len(stack) > 32:
raise RuntimeError("PDF graphics stack overflow")
elif command == b'Q':
ctm = stack.pop()
elif command == b'cm':
ctm = matrix_mult(
ctm, _matrix_from_shorthand(operands))
_matrix_from_shorthand(operands), ctm)
elif command == b'Do':
image_name = operands[0]
image_raster_settings.append(
@@ -120,21 +122,31 @@ def _get_dpi(ctm_shorthand, image_size):
it is not sufficient to assume that the image fills the page, even though
that is the most common case.
This code solves the general case where the image may be scaled (always),
cropped, translated (often), and rotated in place (occasionally) to an
arbitrary angle (rare). It will work as long as the image is a
parallelogram from the perspective of a rectilinear coordinate system.
It does not work for arbitrarily quadrilaterals that might be produced
by shearing, but by that point DPI becomes a linear gradient rather than
constant over the image.
A PDF image may be scaled (always), cropped, translated, rotated in place
to an arbitrary angle (rarely) and skewed. Only equal area mappings can
be expressed, that is, it is not necessary to consider distortions where
the effective DPI varies with position.
The transformation matrix describes the coordinate system at the time of
rendering. We transform the image corner locations into the coordinate
system and measure the width and height within the system, expressed in
PDF units. From there we can compare to the actual image dimensions.
To determine the image scale, transform an offset axis vector v0 (0, 0),
width-axis vector v0 (1, 0), height-axis vector vh (0, 1) with the matrix,
which gives the dimensions of the image in PDF units. From there we can
compare to actual image dimensions. PDF uses
row vector * matrix_tranposed unlike the traditional
matrix * column vector.
The offset, width and height vectors can be combined in a matrix and
multiplied by the transform matrix. Then we want to calculated
magnitude(width_vector - offset_vector)
and
magnitude(height_vector - offset_vector)
When the above is worked out algebraically, the effect of translation
cancels out, and the vector magnitudes become functions of the nonzero
transformation matrix indices. The results of the derivation are used
in this code.
pdfimages -list does calculate the DPI in some way that is not completely
naive, but it does not the DPI of rotated images right, so cannot be
naive, but it does not get the DPI of rotated images right, so cannot be
used anymore to validate this. Photoshop works, or using Acrobat to
rotate the image back to normal.
@@ -142,31 +154,12 @@ def _get_dpi(ctm_shorthand, image_size):
/MediaBox.
"""
matrix = _matrix_from_shorthand(ctm_shorthand)
# Corners of the image in untransformed unit space; last
# column is a dummy to assist matrix math
corners = [[0, 0, 1],
[1, 0, 1],
[0, 1, 1],
[1, 1, 1]]
a, b, c, d, _, _ = ctm_shorthand
# Rotate/translate/scale the corners into PDF coords (1/72")
# ordering of points may change, e.g. if rotation is 180 then
# the point (0, 0) may become the top right
# The row vectors can all be transformed together here by building
# a matrix of them
page_unit_corners = matrix_mult(corners, matrix)
# Calculate the width and height of the rotated image
# the transformation matrix so the corner that was originally
# (1, 1) can be ignored
image_drawn_width = euclidean_distance(
page_unit_corners[0], page_unit_corners[1])
image_drawn_height = euclidean_distance(
page_unit_corners[0], page_unit_corners[2])
# print((image_drawn_width, image_drawn_height))
# Calculate the width and height of the image in PDF units
image_drawn_width = hypot(a, b)
image_drawn_height = hypot(c, d)
# The scale of the image is pixels per PDF unit (1/72")
scale_w = image_size[0] / image_drawn_width
@@ -179,10 +172,10 @@ def _get_dpi(ctm_shorthand, image_size):
return (dpi_w, dpi_h)
def _find_page_images(page, pageinfo, contentsinfo):
def _find_page_inline_images(page, pageinfo, contentsinfo):
"Find inline images on the page"
for n, im in enumerate(contentsinfo.inline_images):
print(n)
settings, shorthand = im
image = {}
image['name'] = str('inline-%02d' % n)
@@ -191,12 +184,22 @@ def _find_page_images(page, pageinfo, contentsinfo):
image['bpc'] = settings['/BPC']
image['color'] = FRIENDLY_COLORSPACE.get(settings['/CS'], '-')
image['comp'] = FRIENDLY_COMP.get(image['color'], '?')
if '/F' in settings:
filter_ = settings['/F']
if isinstance(filter_, pypdf.generic.ArrayObject):
filter_ = filter_[0]
image['enc'] = FRIENDLY_ENCODING.get(filter_, 'image')
else:
image['enc'] = 'image'
dpi_w, dpi_h = _get_dpi(shorthand, (image['width'], image['height']))
image['dpi_w'], image['dpi_h'] = Decimal(dpi_w), Decimal(dpi_h)
yield image
# Look for XObject (out of line images)
def _find_page_regular_images(page, pageinfo, contentsinfo):
"Find images stored in XObject resources"
try:
page['/Resources']['/XObject']
except KeyError:
@@ -206,9 +209,6 @@ def _find_page_images(page, pageinfo, contentsinfo):
pdfimage = page['/Resources']['/XObject'][xobj]
if pdfimage['/Subtype'] != '/Image':
continue
if '/ImageMask' in pdfimage:
if pdfimage['/ImageMask']:
continue
image = {}
image['name'] = str(xobj)
image['width'] = pdfimage['/Width']
@@ -252,6 +252,11 @@ def _find_page_images(page, pageinfo, contentsinfo):
yield image
def _find_page_images(page, pageinfo, contentsinfo):
yield from _find_page_inline_images(page, pageinfo, contentsinfo)
yield from _find_page_regular_images(page, pageinfo, contentsinfo)
def _page_has_text(pdf, page):
# Simple test
text = page.extractText()
@@ -295,7 +300,6 @@ def _pdf_get_pageinfo(infile, pageno: int):
return pageinfo
contentsinfo = _interpret_contents(contentstream)
print(contentsinfo)
pageinfo['images'] = [im for im in _find_page_images(
page, pageinfo, contentsinfo)]
+7 -33
View File
@@ -5,10 +5,13 @@
from __future__ import print_function, absolute_import, division
from string import Template
from subprocess import Popen, PIPE
import os
import codecs
from . import get_program
import pkg_resources
ICC_PROFILE_RELPATH = 'data/sRGB.icc'
SRGB_ICC_PROFILE = pkg_resources.resource_filename(
'ocrmypdf', ICC_PROFILE_RELPATH)
# This is a template written in PostScript which is needed to create PDF/A
@@ -93,38 +96,9 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark):
return result
def _get_postscript_icc_path():
"Parse Ghostscript's help message to find where iccprofiles are stored"
p_gs = Popen([get_program('gs'), '--help'], close_fds=True,
universal_newlines=True,
stdout=PIPE, stderr=PIPE)
out, _ = p_gs.communicate()
lines = out.splitlines()
def search_paths(lines):
seeking = True
for line in lines:
if seeking:
if line.startswith('Search path'):
seeking = False
continue
else:
if line.strip().startswith('/'):
yield from (
path.strip() for path in line.split(':')
if path.strip() != '')
for root in search_paths(lines):
path = os.path.realpath(os.path.join(root, '../iccprofiles'))
if os.path.exists(path):
return path
raise FileNotFoundError("Could not find Ghostscript's iccprofiles")
def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'):
if icc == 'sRGB':
icc_profile = os.path.join(_get_postscript_icc_path(), 'srgb.icc')
icc_profile = SRGB_ICC_PROFILE
else:
raise NotImplementedError("Only supporting sRGB")
+15 -14
View File
@@ -3,7 +3,7 @@
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/master/doc/basic-concepts.md
from subprocess import Popen, PIPE
from subprocess import CalledProcessError, STDOUT, check_output, check_call
from tempfile import NamedTemporaryFile
import sys
import os
@@ -17,10 +17,9 @@ def version():
get_program('unpaper'),
'--version'
]
p_unpaper = Popen(args_unpaper, close_fds=True, universal_newlines=True,
stdout=PIPE, stderr=PIPE)
version, _ = p_unpaper.communicate(timeout=5)
version = check_output(
args_unpaper, close_fds=True, universal_newlines=True,
stderr=STDOUT, timeout=5)
return version.strip()
@@ -68,15 +67,17 @@ def run(input_file, output_file, dpi, log, mode_args):
os.unlink(output_pnm.name)
args_unpaper.extend([input_pnm.name, output_pnm.name])
p_unpaper = Popen(
args_unpaper, close_fds=True,
universal_newlines=True, stdout=PIPE, stderr=PIPE
)
out, err = p_unpaper.communicate()
log.debug(out)
log.debug(err)
Image.open(output_pnm.name).save(output_file)
try:
stdout = check_output(
args_unpaper, close_fds=True,
universal_newlines=True, stderr=STDOUT,
)
except CalledProcessError as e:
log.debug(e.output)
raise e from e
else:
log.debug(stdout)
Image.open(output_pnm.name).save(output_file)
def deskew(input_file, output_file, dpi, log):
+2 -2
View File
@@ -209,7 +209,6 @@ setup(
],
setup_requires=[
'setuptools_scm',
'setuptools_scm_git_archive',
'cffi>=1.5.0',
'pytest-runner'
],
@@ -218,7 +217,7 @@ setup(
'ocrmypdf/lib/compile_leptonica.py:ffi'
],
install_requires=[
'ruffus>=2.6.3',
'ruffus==2.6.3',
'Pillow>=3.0.0',
'reportlab>=3.1.44',
'PyPDF2>=1.25.1',
@@ -231,5 +230,6 @@ setup(
'ocrmypdf = ocrmypdf.main:run_pipeline'
],
},
package_data={'ocrmypdf': ['data/sRGB.icc']},
include_package_data=True,
zip_safe=False)
Binary file not shown.
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
import sys
def main():
if sys.argv[1] == '--version':
print('0.5')
sys.exit(0)
print("Only supports --version")
sys.exit(1)
if __name__ == '__main__':
main()
+53 -5
View File
@@ -325,11 +325,6 @@ def test_monochrome_correlation():
'tesseract',
])
def test_autorotate(spoof_tesseract_cache, renderer):
import ocrmypdf.ghostscript as ghostscript
import logging
gslog = logging.getLogger()
# cardinal.pdf contains four copies of an image rotated in each cardinal
# direction - these ones are "burned in" not tagged with /Rotate
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_%s.pdf' % renderer,
@@ -343,6 +338,36 @@ def test_autorotate(spoof_tesseract_cache, renderer):
assert correlation > 0.80
def test_autorotate_threshold_low(spoof_tesseract_cache):
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_threshold_low.pdf',
'--rotate-pages-threshold', '1',
'-r', '-v', '1', env=spoof_tesseract_cache)
# Low threshold -> always rotate -> expect high correlation between
# reference page and test page
correlation = check_monochrome_correlation(
reference_pdf=_infile('cardinal.pdf'),
reference_pageno=1,
test_pdf=out,
test_pageno=3)
assert correlation > 0.80
def test_autorotate_threshold_high(spoof_tesseract_cache):
out = check_ocrmypdf('cardinal.pdf', 'test_autorotate_threshold_high.pdf',
'--rotate-pages-threshold', '99',
'-r', '-v', '1', env=spoof_tesseract_cache)
# High threshold -> never rotate -> expect low correlation since
# test page will not be rotated
correlation = check_monochrome_correlation(
reference_pdf=_infile('cardinal.pdf'),
reference_pageno=1,
test_pdf=out,
test_pageno=3)
assert correlation < 0.10
@pytest.mark.parametrize('renderer', [
'hocr',
'tesseract',
@@ -493,6 +518,8 @@ def test_tesseract_crash_autorotate(spoof_tesseract_crash):
assert sh.returncode == ExitCode.child_process_error
assert not os.path.exists(_outfile('wontwork.pdf'))
assert "ERROR" in err
print(out)
print(err)
@pytest.mark.parametrize('renderer', [
@@ -503,3 +530,24 @@ def test_tesseract_image_too_big(renderer, spoof_tesseract_big_image_error):
check_ocrmypdf(
'hugemono.pdf', 'hugemono_%s.pdf' % renderer, '-r',
'--pdf-renderer', renderer, env=spoof_tesseract_big_image_error)
def test_no_unpaper():
env = os.environ.copy()
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/no_unpaper_here.py')
sh, out, err = run_ocrmypdf_env(
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
assert sh.returncode == ExitCode.missing_dependency
def test_old_unpaper():
env = os.environ.copy()
env['OCRMYPDF_UNPAPER'] = os.path.abspath('./spoof/unpaper_oldversion.py')
sh, out, err = run_ocrmypdf_env(
'c02-22.pdf', 'wont_be_created.pdf', '--clean', env=env)
assert sh.returncode == ExitCode.missing_dependency
def test_algo4():
sh, _, _ = run_ocrmypdf_env('encrypted_algo4.pdf', 'wontwork.pdf')
assert sh.returncode == ExitCode.encrypted_pdf