diff --git a/ocrmypdf/leptonica.py b/ocrmypdf/leptonica.py index a03a1af5..7b3fb67a 100644 --- a/ocrmypdf/leptonica.py +++ b/ocrmypdf/leptonica.py @@ -10,11 +10,14 @@ from __future__ import print_function, absolute_import, division import argparse -import ctypes as C import sys import os import logging from tempfile import TemporaryFile +from ctypes.util import find_library +from .lib._leptonica import ffi + +lept = ffi.dlopen(find_library('lept')) logger = logging.getLogger(__name__) @@ -25,67 +28,6 @@ def stderr(*objs): print("leptonica.py:", *objs, file=sys.stderr) -from ctypes.util import find_library -lept_lib = find_library('lept') -if not lept_lib: - stderr("Could not find the Leptonica library") - sys.exit(3) -try: - lept = C.cdll.LoadLibrary(lept_lib) -except Exception: - stderr("Could not load the Leptonica library from %s", lept_lib) - sys.exit(3) - - -class _PIXCOLORMAP(C.Structure): - """struct PixColormap from Leptonica src/pix.h - """ - - _fields_ = [ - ("array", C.c_void_p), - ("depth", C.c_int32), - ("nalloc", C.c_int32), - ("n", C.c_int32) - ] - - -class _PIX(C.Structure): - """struct Pix from Leptonica src/pix.h - """ - - _fields_ = [ - ("w", C.c_uint32), - ("h", C.c_uint32), - ("d", C.c_uint32), - ("wpl", C.c_uint32), - ("refcount", C.c_uint32), - ("xres", C.c_int32), - ("yres", C.c_int32), - ("informat", C.c_int32), - ("text", C.POINTER(C.c_char)), - ("colormap", C.POINTER(_PIXCOLORMAP)), - ("data", C.POINTER(C.c_uint32)) - ] - - -PIX = C.POINTER(_PIX) - -lept.pixRead.argtypes = [C.c_char_p] -lept.pixRead.restype = PIX -lept.pixScale.argtypes = [PIX, C.c_float, C.c_float] -lept.pixScale.restype = PIX -lept.pixDeskew.argtypes = [PIX, C.c_int32] -lept.pixDeskew.restype = PIX -lept.pixFindSkew.argtypes = [PIX, C.POINTER(C.c_float), C.POINTER(C.c_float)] -lept.pixFindSkew.restype = C.c_int32 -lept.pixWriteImpliedFormat.argtypes = [C.c_char_p, PIX, C.c_int32, C.c_int32] -lept.pixWriteImpliedFormat.restype = C.c_int32 -lept.pixDestroy.argtypes = [C.POINTER(PIX)] -lept.pixDestroy.restype = None -lept.getLeptonicaVersion.argtypes = [] -lept.getLeptonicaVersion.restype = C.c_char_p - - class LeptonicaErrorTrap(object): """Context manager to trap errors reported by Leptonica. @@ -140,6 +82,12 @@ class LeptonicaIOError(LeptonicaError): pass +def _pix_destroy(pix): + ptr_to_pix = ffi.new('PIX **', pix) + lept.pixDestroy(ptr_to_pix) + print('pix destroy ' + repr(pix)) + + def pixRead(filename): """Load an image file into a PIX object. @@ -148,7 +96,8 @@ def pixRead(filename): """ with LeptonicaErrorTrap(): - return lept.pixRead(filename.encode(sys.getfilesystemencoding())) + pix = lept.pixRead(filename.encode(sys.getfilesystemencoding())) + return ffi.gc(pix, _pix_destroy) def pixScale(pix, scalex, scaley): @@ -168,7 +117,8 @@ def pixDeskew(pix, reduction_factor=0): """ with LeptonicaErrorTrap(): - return lept.pixDeskew(pix, reduction_factor) + deskewed = lept.pixDeskew(pix, reduction_factor) + return ffi.gc(deskewed, _pix_destroy) def pixFindSkew(pix): @@ -178,11 +128,11 @@ def pixFindSkew(pix): """ with LeptonicaErrorTrap(): - angle = C.c_float(0.0) - confidence = C.c_float(0.0) - result = lept.pixFindSkew(pix, C.byref(angle), C.byref(confidence)) + angle = ffi.new('float *', 0.0) + confidence = ffi.new('float *', 0.0) + result = lept.pixFindSkew(pix, angle, confidence) if result == 0: - return (angle.value, confidence.value) + return (angle[0], confidence[0]) else: return (None, None) @@ -212,17 +162,6 @@ def pixWriteImpliedFormat(filename, pix, jpeg_quality=0, jpeg_progressive=0): move(filename, filename[:-4]) # Remove .pnm suffix -def pixDestroy(pix): - """Destroy the pix object. - - Function signature is pixDestroy(struct Pix **), hence C.byref() to pass - the address of the pointer. - - """ - with LeptonicaErrorTrap(): - lept.pixDestroy(C.byref(pix)) - - def getLeptonicaVersion(): """Get Leptonica version string. @@ -231,12 +170,13 @@ def getLeptonicaVersion(): a pointless effort to reclaim 100 bytes of memory. """ - return lept.getLeptonicaVersion().decode() + return ffi.string(lept.getLeptonicaVersion()).decode() def deskew(infile, outfile, dpi): try: pix_source = pixRead(infile) + print(repr(pix_source)) except LeptonicaIOError: raise LeptonicaIOError("Failed to open file: %s" % infile) @@ -245,13 +185,12 @@ def deskew(infile, outfile, dpi): else: reduction_factor = 0 # Use default pix_deskewed = pixDeskew(pix_source, reduction_factor) + print(repr(pix_deskewed)) try: pixWriteImpliedFormat(outfile, pix_deskewed) except LeptonicaIOError: raise LeptonicaIOError("Failed to open destination file: %s" % outfile) - pixDestroy(pix_source) - pixDestroy(pix_deskewed) if __name__ == '__main__': @@ -325,7 +264,6 @@ def test_skew_angle(): rotated_im.save(tmpfile) pix = pixRead(tmpfile.name) angle, confidence = pixFindSkew(pix) - pixDestroy(pix) print('{0} {1} {2}'.format(rotate_angle, angle, confidence), file=sys.stderr) diff --git a/ocrmypdf/lib/compile_leptonica.py b/ocrmypdf/lib/compile_leptonica.py new file mode 100644 index 00000000..427ffbb8 --- /dev/null +++ b/ocrmypdf/lib/compile_leptonica.py @@ -0,0 +1,58 @@ +from cffi import FFI + +ffi = FFI() +ffi.set_source("ocrmypdf.lib._leptonica", None) +ffi.cdef(""" +typedef signed char l_int8; +typedef unsigned char l_uint8; +typedef short l_int16; +typedef unsigned short l_uint16; +typedef int l_int32; +typedef unsigned int l_uint32; +typedef float l_float32; +typedef double l_float64; +typedef long long l_int64; +typedef unsigned long long l_uint64; + +struct Pix +{ + l_uint32 w; /* width in pixels */ + l_uint32 h; /* height in pixels */ + l_uint32 d; /* depth in bits (bpp) */ + l_uint32 spp; /* number of samples per pixel */ + l_uint32 wpl; /* 32-bit words/line */ + l_uint32 refcount; /* reference count (1 if no clones) */ + l_int32 xres; /* image res (ppi) in x direction */ + /* (use 0 if unknown) */ + l_int32 yres; /* image res (ppi) in y direction */ + /* (use 0 if unknown) */ + l_int32 informat; /* input file format, IFF_* */ + l_int32 special; /* special instructions for I/O, etc */ + char *text; /* text string associated with pix */ + struct PixColormap *colormap; /* colormap (may be null) */ + l_uint32 *data; /* the image data */ +}; +typedef struct Pix PIX; + +struct PixColormap +{ + void *array; /* colormap table (array of RGBA_QUAD) */ + l_int32 depth; /* of pix (1, 2, 4 or 8 bpp) */ + l_int32 nalloc; /* number of color entries allocated */ + l_int32 n; /* number of color entries used */ +}; +typedef struct PixColormap PIXCMAP; +""") + +ffi.cdef(""" +PIX * pixRead ( const char *filename ); +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 * pixDeskew ( PIX *pixs, l_int32 redsearch ); +char * getLeptonicaVersion ( ); +""") + +if __name__ == '__main__': + ffi.compile() diff --git a/ocrmypdf/main.py b/ocrmypdf/main.py index 9240bdf5..35eb924d 100755 --- a/ocrmypdf/main.py +++ b/ocrmypdf/main.py @@ -187,6 +187,9 @@ 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( + '--deskewer', choices=['leptonica', 'unpaper'], default='leptonica', + help='choose deskew provider') debugging = parser.add_argument_group( "Debugging", @@ -585,7 +588,11 @@ def preprocess_deskew( pageinfo = get_pageinfo(input_file, pdfinfo, pdfinfo_lock) dpi = int(pageinfo['xres']) - unpaper.deskew(input_file, output_file, dpi, log) + if options.deskewer == 'unpaper': + unpaper.deskew(input_file, output_file, dpi, log) + else: + from . import leptonica + leptonica.deskew(input_file, output_file, dpi) @transform( diff --git a/requirements.txt b/requirements.txt index 18cb6b81..9d220ffb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ Pillow==3.1.1 reportlab==3.2.0 PyPDF2==1.25.1 img2pdf==0.2 +cffi>=1.5.0 diff --git a/setup.py b/setup.py index ae49a947..49d7fb26 100644 --- a/setup.py +++ b/setup.py @@ -208,15 +208,20 @@ setup( "Topic :: Text Processing :: Linguistic", ], setup_requires=[ - 'setuptools_scm' + 'setuptools_scm', + 'cffi>=1.0.0' ], use_scm_version={'version_scheme': 'post-release'}, + cffi_modules=[ + 'ocrmypdf/lib/compile_leptonica.py:ffi' + ], install_requires=[ 'ruffus>=2.6.3', 'Pillow>=2.4.0', 'reportlab>=3.1.44', 'PyPDF2>=1.25.1', - 'img2pdf>=0.2.0' + 'img2pdf>=0.2.0', + 'cffi>=1.5.0' ], tests_require=tests_require, entry_points={