From 806daf42846de7cefc176f05bbb42472bc3cd4de Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Tue, 6 Nov 2018 20:33:40 -0800 Subject: [PATCH] leptonica: reduce boilerplate for PIX (2/2) --- src/ocrmypdf/leptonica.py | 121 +++++++++++++++++++++----------------- 1 file changed, 66 insertions(+), 55 deletions(-) diff --git a/src/ocrmypdf/leptonica.py b/src/ocrmypdf/leptonica.py index 252dcb80..80f52b27 100644 --- a/src/ocrmypdf/leptonica.py +++ b/src/ocrmypdf/leptonica.py @@ -121,6 +121,23 @@ class LeptonicaIOError(LeptonicaError): class LeptonicaObject: + """General wrapper for Leptonica objects + + When Leptonica returns an object, we bundled it in a wrapper class, which + manages its memory. The wrapper class assumes that it will be calling some + sort of lept.thingDestroy() function when the instance is deleted. Most + Leptonica objects are reference counted, and destroy decrements the + refcount. + + Most of the time, when Leptonica returns something, we wrap and it the job + is done. When wrapping objects that came from a Leptonica container, like + a PIXA returning PIX, the subclass must clone the object before passing it + here, to maintain the reference count. + + CFFI ensures that the destroy function is called at garbage collection time + so we do not need to mess with __del__. + """ + cdata_destroy = lambda cdata: None LEPTONICA_TYPENAME = '' @@ -131,11 +148,15 @@ class LeptonicaObject: @classmethod def _destroy(cls, cdata): + """Destroy some cdata""" + # Leptonica API uses double-pointers for its destroy APIs to prevent + # dangling pointers. This means we need to put our single pointer, + # cdata, in a temporary CDATA**. pp = ffi.new('{} **'.format(cls.LEPTONICA_TYPENAME), cdata) cls.cdata_destroy(pp) -class Pix: +class Pix(LeptonicaObject): """ Wrapper around leptonica's PIX object. @@ -154,20 +175,17 @@ class Pix: modified objects. This allows convenient chaining: >>> Pix.open('filename.jpg').scale((0.5, 0.5)).deskew().show() - """ - def __init__(self, pix): - if not pix: - raise ValueError('NULL pix') - self._pix = ffi.gc(pix, Pix._destroy) + LEPTONICA_TYPENAME = "PIX" + cdata_destroy = lept.pixDestroy def __repr__(self): - if self._pix: + if self._cdata: s = "" - return s.format(self._pix.w, self._pix.h, self._pix.d, - int(ffi.cast('intptr_t', self._pix)), - '(colormapped)' if self._pix.colormap else '') + return s.format(self._cdata.w, self._cdata.h, self._cdata.d, + int(ffi.cast('intptr_t', self._cdata)), + '(colormapped)' if self._cdata.colormap else '') else: return "" @@ -180,7 +198,7 @@ class Pix: data = ffi.new('l_uint8 **') size = ffi.new('size_t *') - err = lept.pixWriteMemPng(data, size, self._pix, 0) + err = lept.pixWriteMemPng(data, size, self._cdata, 0) if err != 0: raise LeptonicaIOError("pixWriteMemPng") @@ -191,7 +209,7 @@ class Pix: data = ffi.new('l_uint32 **') size = ffi.new('size_t *') - err = lept.pixSerializeToMemory(self._pix, data, size) + err = lept.pixSerializeToMemory(self._cdata, data, size) if err != 0: raise LeptonicaIOError("pixSerializeToMemory") @@ -216,30 +234,30 @@ class Pix: return NotImplemented same = ffi.new('l_int32 *', 0) with _LeptonicaErrorTrap(): - err = lept.pixEqual(self._pix, other._pix, same) + err = lept.pixEqual(self._cdata, other._cdata, same) if err: raise TypeError() return bool(same[0]) @property def width(self): - return self._pix.w + return self._cdata.w @property def height(self): - return self._pix.h + return self._cdata.h @property def depth(self): - return self._pix.d + return self._cdata.d @property def size(self): - return (self._pix.w, self._pix.h) + return (self._cdata.w, self._cdata.h) @property def info(self): - return {'dpi': (self._pix.xres, self._pix.yres)} + return {'dpi': (self._cdata.xres, self._cdata.yres)} @property def mode(self): @@ -248,7 +266,7 @@ class Pix: return '1' elif self.depth >= 16: return 'RGB' - elif not self._pix.colormap: + elif not self._cdata.colormap: return 'L' else: return 'P' @@ -280,7 +298,7 @@ class Pix: with _LeptonicaErrorTrap(): lept.pixWriteImpliedFormat( os.fsencode(filename), - self._pix, jpeg_quality, jpeg_progressive) + self._cdata, jpeg_quality, jpeg_progressive) def topil(self): "Returns a PIL.Image version of this Pix" @@ -296,17 +314,17 @@ class Pix: raw_mode = 'ABGR' elif self.mode == '1': raw_mode = '1;I' - pix = Pix(lept.pixEndianByteSwapNew(pix._pix)) + pix = Pix(lept.pixEndianByteSwapNew(pix._cdata)) else: raw_mode = self.mode - pix = Pix(lept.pixEndianByteSwapNew(pix._pix)) + pix = Pix(lept.pixEndianByteSwapNew(pix._cdata)) else: raw_mode = self.mode # no endian swap needed - size = (pix._pix.w, pix._pix.h) - bytecount = pix._pix.wpl * 4 * pix._pix.h - buf = ffi.buffer(pix._pix.data, bytecount) - stride = pix._pix.wpl * 4 + size = (pix._cdata.w, pix._cdata.h) + bytecount = pix._cdata.wpl * 4 * pix._cdata.h + buf = ffi.buffer(pix._cdata.data, bytecount) + stride = pix._cdata.wpl * 4 im = Image.frombytes(self.mode, size, buf, 'raw', raw_mode, stride) @@ -325,21 +343,21 @@ class Pix: for skew angle """ with _LeptonicaErrorTrap(): - return Pix(lept.pixDeskew(self._pix, reduction_factor)) + return Pix(lept.pixDeskew(self._cdata, reduction_factor)) def scale(self, scale_xy): "Returns the pix object rescaled according to the proportions given." with _LeptonicaErrorTrap(): - return Pix(lept.pixScale(self._pix, scale_xy[0], scale_xy[1])) + return Pix(lept.pixScale(self._cdata, scale_xy[0], scale_xy[1])) def rotate180(self): with _LeptonicaErrorTrap(): - return Pix(lept.pixRotate180(ffi.NULL, self._pix)) + return Pix(lept.pixRotate180(ffi.NULL, self._cdata)) def rotate_orth(self, quads): "Orthographic rotation, quads: 0-3, number of clockwise rotations" with _LeptonicaErrorTrap(): - return Pix(lept.pixRotateOrth(self._pix, quads)) + return Pix(lept.pixRotateOrth(self._cdata, quads)) def find_skew(self): """Returns a tuple (deskew angle in degrees, confidence value). @@ -349,7 +367,7 @@ class Pix: with _LeptonicaErrorTrap(): angle = ffi.new('float *', 0.0) confidence = ffi.new('float *', 0.0) - result = lept.pixFindSkew(self._pix, angle, confidence) + result = lept.pixFindSkew(self._cdata, angle, confidence) if result == 0: return (angle[0], confidence[0]) else: @@ -357,7 +375,7 @@ class Pix: def convert_rgb_to_luminance(self): with _LeptonicaErrorTrap(): - gray_pix = lept.pixConvertRGBToLuminance(self._pix) + gray_pix = lept.pixConvertRGBToLuminance(self._cdata) if gray_pix: return Pix(gray_pix) return None @@ -371,7 +389,7 @@ class Pix: """ with _LeptonicaErrorTrap(): return Pix(lept.pixRemoveColormapGeneral( - self._pix, removal_type, lept.L_COPY)) + self._cdata, removal_type, lept.L_COPY)) def otsu_adaptive_threshold( self, tile_size=(300, 300), kernel_size=(4, 4), scorefract=0.1): @@ -381,7 +399,7 @@ class Pix: p_pix = ffi.new('PIX **') result = lept.pixOtsuAdaptiveThreshold( - self._pix, + self._cdata, sx, sy, smoothx, smoothy, scorefract, @@ -401,10 +419,10 @@ class Pix: if mask is None: mask = ffi.NULL if isinstance(mask, Pix): - mask = mask._pix + mask = mask._cdata thresh_pix = lept.pixOtsuThreshOnBackgroundNorm( - self._pix, + self._cdata, mask, sx, sy, thresh, mincount, bgval, @@ -425,10 +443,10 @@ class Pix: if mask is None: mask = ffi.NULL if isinstance(mask, Pix): - mask = mask._pix + mask = mask._cdata new_pix = lept.pixMaskedThreshOnBackgroundNorm( - self._pix, + self._cdata, mask, sx, sy, thresh, mincount, @@ -445,7 +463,7 @@ class Pix: showmorph=0, display=0, pdfdir=ffi.NULL): with _LeptonicaErrorTrap(): cropbox = Box(lept.pixFindPageForeground( - self._pix, + self._cdata, threshold, mindist, erasedist, @@ -455,7 +473,7 @@ class Pix: pdfdir)) cropped_pix = lept.pixClipRectangle( - self._pix, + self._cdata, cropbox._cdata, ffi.NULL) @@ -465,7 +483,7 @@ class Pix: self, mask=None, grayscale=None, gamma=1.0, black=0, white=255): with _LeptonicaErrorTrap(): return Pix(lept.pixCleanBackgroundToWhite( - self._pix, + self._cdata, mask or ffi.NULL, grayscale or ffi.NULL, gamma, @@ -476,7 +494,7 @@ class Pix: with _LeptonicaErrorTrap(): return Pix(lept.pixGammaTRC( ffi.NULL, - self._pix, + self._cdata, gamma, minval, maxval @@ -489,7 +507,7 @@ class Pix: target_pix = self.remove_colormap(lept.REMOVE_CMAP_BASED_ON_SRC) with _LeptonicaErrorTrap(): return Pix(lept.pixBackgroundNorm( - target_pix._pix, + target_pix._cdata, mask or ffi.NULL, grayscale or ffi.NULL, tile_size[0], @@ -516,7 +534,7 @@ class Pix: raise LeptonicaError("Leptonica version is too old") correlation = ffi.new('float *', 0.0) - result = lept.pixCorrelationBinary(pix1._pix, pix2._pix, + result = lept.pixCorrelationBinary(pix1._cdata, pix2._cdata, correlation) if result != 0: raise LeptonicaError("Correlation failed") @@ -525,19 +543,19 @@ class Pix: def generate_pdf_ci_data(self, type_, quality): "Convert to PDF data, with transcoding" p_compdata = ffi.new('L_COMP_DATA **') - result = lept.pixGenerateCIData(self._pix, type_, quality, 0, + result = lept.pixGenerateCIData(self._cdata, type_, quality, 0, p_compdata) if result != 0: raise LeptonicaError("Generate PDF data failed") return CompressedData(p_compdata[0]) def invert(self): - return Pix(lept.pixInvert(ffi.NULL, self._pix)) + return Pix(lept.pixInvert(ffi.NULL, self._cdata)) def locate_barcodes(self): with _LeptonicaErrorTrap(): - pix = Pix(lept.pixConvertTo8(self._pix, 0)) - pixa_candidates = PixArray(lept.pixExtractBarcodes(pix._pix, 0)) + pix = Pix(lept.pixConvertTo8(self._cdata, 0)) + pixa_candidates = PixArray(lept.pixExtractBarcodes(pix._cdata, 0)) sarray = StringArray(lept.pixReadBarcodes(pixa_candidates._cdata, lept.L_BF_ANY, lept.L_USE_WIDTHS, @@ -553,13 +571,6 @@ class Pix: yield (decoded, (left, top, right, bottom)) - @staticmethod - def _destroy(pix): - p_pix = ffi.new('PIX **', pix) - lept.pixDestroy(p_pix) - # print('pix destroy ' + repr(pix)) - - class CompressedData(LeptonicaObject): """Wrapper for L_COMP_DATA - abstract compressed image data"""