From d7f60b96c107cd5c385aec229b6b9b1fe4d7442b Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 12 Mar 2016 15:18:35 -0800 Subject: [PATCH 1/5] More leptonica functions for page manipulation --- ocrmypdf/leptonica.py | 72 +++++++++++++++++++++++++++++++ ocrmypdf/lib/compile_leptonica.py | 39 +++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/ocrmypdf/leptonica.py b/ocrmypdf/leptonica.py index 83c697dd..e1c435e4 100644 --- a/ocrmypdf/leptonica.py +++ b/ocrmypdf/leptonica.py @@ -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. @@ -137,6 +145,30 @@ class Pix: def height(self): return self.cpix.h + @property + def depth(self): + return self.cpix.d + + @property + def size(self): + return (self.cpix.w, self.cpix.h) + + @property + def info(self): + return {'dpi': (self.cpix.xres, self.cpix.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.cpix.colormap: + return 'L' + else: + return 'P' + @classmethod def read(cls, filename): """Load an image file into a PIX object. @@ -195,6 +227,22 @@ class Pix: else: return (None, None) + def convert_rgb_to_luminance(self): + with LeptonicaErrorTrap(): + gray_pix = lept.pixConvertRGBToLuminance(self.cpix) + 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.cpix, removal_type)) + def otsu_adaptive_threshold( self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1): with LeptonicaErrorTrap(): @@ -214,6 +262,30 @@ class Pix: 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.cpix + + thresh_pix = lept.pixOtsuThreshOnBackgroundNorm( + self.cpix, + mask, + sx, sy, + thresh, mincount, bgval, + smoothx, smoothy, + scorefract, + ffi.NULL + ) + if thresh_pix == ffi.NULL: + return None + return Pix(thresh_pix) + @staticmethod @lru_cache(maxsize=1) def make_pixel_sum_tab8(): diff --git a/ocrmypdf/lib/compile_leptonica.py b/ocrmypdf/lib/compile_leptonica.py index 6c013389..3a540f2d 100644 --- a/ocrmypdf/lib/compile_leptonica.py +++ b/ocrmypdf/lib/compile_leptonica.py @@ -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(""" @@ -62,6 +74,10 @@ 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, @@ -72,6 +88,29 @@ pixOtsuAdaptiveThreshold(PIX *pixs, 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); + +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); + void lept_free(void *ptr); """) From 8d79b94b8456742e8bebfe395af9e60e6ba0051a Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 12 Mar 2016 15:22:23 -0800 Subject: [PATCH 2/5] cpix -> _pix --- ocrmypdf/leptonica.py | 66 +++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/ocrmypdf/leptonica.py b/ocrmypdf/leptonica.py index e1c435e4..f1a1d632 100644 --- a/ocrmypdf/leptonica.py +++ b/ocrmypdf/leptonica.py @@ -106,14 +106,14 @@ 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 = "" - 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 "" @@ -121,7 +121,7 @@ class Pix: 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") @@ -134,28 +134,28 @@ class Pix: cdata_bytes = ffi.new('char[]', state['data']) cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes) - self.cpix = lept.pixDeserializeFromMemory( + self._pix = lept.pixDeserializeFromMemory( cdata_uint32, len(state['data'])) @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.cpix.d + return self._pix.d @property def size(self): - return (self.cpix.w, self.cpix.h) + return (self._pix.w, self._pix.h) @property def info(self): - return {'dpi': (self.cpix.xres, self.cpix.yres)} + return {'dpi': (self._pix.xres, self._pix.yres)} @property def mode(self): @@ -164,7 +164,7 @@ class Pix: return '1' elif self.depth >= 16: return 'RGB' - elif not self.cpix.colormap: + elif not self._pix.colormap: return 'L' else: return 'P' @@ -190,7 +190,7 @@ class Pix: with LeptonicaErrorTrap(): lept.pixWriteImpliedFormat( filename.encode(sys.getfilesystemencoding()), - self.cpix, jpeg_quality, jpeg_progressive) + self._pix, jpeg_quality, jpeg_progressive) def deskew(self, reduction_factor=0): """Returns the deskewed pix object. @@ -202,16 +202,16 @@ 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 find_skew(self): """Returns a tuple (deskew angle in degrees, confidence value). @@ -221,7 +221,7 @@ 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: @@ -229,7 +229,7 @@ class Pix: def convert_rgb_to_luminance(self): with LeptonicaErrorTrap(): - gray_pix = lept.pixConvertRGBToLuminance(self.cpix) + gray_pix = lept.pixConvertRGBToLuminance(self._pix) if gray_pix: return Pix(gray_pix) return None @@ -241,24 +241,24 @@ class Pix: """ with LeptonicaErrorTrap(): - return Pix(lept.pixRemoveColormap(self.cpix, removal_type)) + 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_cpix = ffi.new('PIX **') + p_pix = ffi.new('PIX **') result = lept.pixOtsuAdaptiveThreshold( - self.cpix, + self._pix, sx, sy, smoothx, smoothy, scorefract, ffi.NULL, - p_cpix) + p_pix) if result == 0: - return Pix(p_cpix[0]) + return Pix(p_pix[0]) else: return None @@ -271,10 +271,10 @@ class Pix: if mask is None: mask = ffi.NULL if isinstance(mask, Pix): - mask = mask.cpix + mask = mask._pix thresh_pix = lept.pixOtsuThreshOnBackgroundNorm( - self.cpix, + self._pix, mask, sx, sy, thresh, mincount, bgval, @@ -302,10 +302,10 @@ class Pix: 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. @@ -316,7 +316,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") @@ -324,8 +324,8 @@ 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)) From 9c66334c38f9fe738e4187de35c3d3b201fdee26 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 12 Mar 2016 23:26:31 -0800 Subject: [PATCH 3/5] Leptonica - ortho rotate, background norm --- ocrmypdf/leptonica.py | 76 +++++++++++++++++++++++++++++++ ocrmypdf/lib/compile_leptonica.py | 19 ++++++++ 2 files changed, 95 insertions(+) diff --git a/ocrmypdf/leptonica.py b/ocrmypdf/leptonica.py index f1a1d632..5d86fadd 100644 --- a/ocrmypdf/leptonica.py +++ b/ocrmypdf/leptonica.py @@ -213,6 +213,11 @@ class Pix: with LeptonicaErrorTrap(): 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). @@ -286,6 +291,40 @@ class Pix: 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(): @@ -329,6 +368,43 @@ class 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 ''.format( + self.x, self.y, self.w, self.h) + return '' + + @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. diff --git a/ocrmypdf/lib/compile_leptonica.py b/ocrmypdf/lib/compile_leptonica.py index 3a540f2d..6f9fde5e 100644 --- a/ocrmypdf/lib/compile_leptonica.py +++ b/ocrmypdf/lib/compile_leptonica.py @@ -66,6 +66,9 @@ 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 ); @@ -101,6 +104,14 @@ pixOtsuThreshOnBackgroundNorm(PIX *pixs, 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, @@ -111,6 +122,14 @@ pixFindPageForeground(PIX *pixs, l_int32 display, const char *pdfdir); +PIX * +pixClipRectangle(PIX *pixs, + BOX *box, + BOX **pboxc); + +void +boxDestroy(BOX **pbox); + void lept_free(void *ptr); """) From af91642cd177ba4f59fb3d0020b2cc0164bbd5ff Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 13 Mar 2016 14:44:27 -0700 Subject: [PATCH 4/5] lept: fix __getstate/__setstate --- ocrmypdf/leptonica.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ocrmypdf/leptonica.py b/ocrmypdf/leptonica.py index 5d86fadd..2f81dc4a 100644 --- a/ocrmypdf/leptonica.py +++ b/ocrmypdf/leptonica.py @@ -126,7 +126,11 @@ class Pix: 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) @@ -134,8 +138,9 @@ class Pix: cdata_bytes = ffi.new('char[]', state['data']) cdata_uint32 = ffi.cast('l_uint32 *', cdata_bytes) - self._pix = lept.pixDeserializeFromMemory( + pix = lept.pixDeserializeFromMemory( cdata_uint32, len(state['data'])) + Pix.__init__(self, pix) @property def width(self): From c7612152ef8096d32d0222abeb5a012bc13a3ac9 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 13 Mar 2016 18:17:58 -0700 Subject: [PATCH 5/5] leptonica: pillow interop --- ocrmypdf/leptonica.py | 22 ++++++++++++++++++++++ ocrmypdf/lib/compile_leptonica.py | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/ocrmypdf/leptonica.py b/ocrmypdf/leptonica.py index 2f81dc4a..f092e87a 100644 --- a/ocrmypdf/leptonica.py +++ b/ocrmypdf/leptonica.py @@ -197,6 +197,28 @@ class Pix: filename.encode(sys.getfilesystemencoding()), 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. diff --git a/ocrmypdf/lib/compile_leptonica.py b/ocrmypdf/lib/compile_leptonica.py index 6f9fde5e..dd8a836f 100644 --- a/ocrmypdf/lib/compile_leptonica.py +++ b/ocrmypdf/lib/compile_leptonica.py @@ -62,6 +62,10 @@ 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);