diff mupdf-source/thirdparty/leptonica/src/runlength.c @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/thirdparty/leptonica/src/runlength.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,796 @@
+/*====================================================================*
+ -  Copyright (C) 2001 Leptonica.  All rights reserved.
+ -
+ -  Redistribution and use in source and binary forms, with or without
+ -  modification, are permitted provided that the following conditions
+ -  are met:
+ -  1. Redistributions of source code must retain the above copyright
+ -     notice, this list of conditions and the following disclaimer.
+ -  2. 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.
+ -
+ -  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 ANY
+ -  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.
+ *====================================================================*/
+
+/*!
+ * \file runlength.c
+ * <pre>
+ *
+ *     Label pixels by membership in runs
+ *           PIX         *pixStrokeWidthTransform()
+ *           static PIX  *pixFindMinRunsOrthogonal()
+ *           PIX         *pixRunlengthTransform()
+ *
+ *     Find runs along horizontal and vertical lines
+ *           l_int32      pixFindHorizontalRuns()
+ *           l_int32      pixFindVerticalRuns()
+ *
+ *     Find max runs along horizontal and vertical lines
+ *           l_int32      pixFindMaxRuns()
+ *           l_int32      pixFindMaxHorizontalRunOnLine()
+ *           l_int32      pixFindMaxVerticalRunOnLine()
+ *
+ *     Compute runlength-to-membership transform on a line
+ *           l_int32      runlengthMembershipOnLine()
+ *
+ *     Make byte position LUT
+ *           l_int32      makeMSBitLocTab()
+ *
+ *  Here we're handling runs of either black or white pixels on 1 bpp
+ *  images.  The directions of the runs in the stroke width transform
+ *  are selectable from given sets of angles.  Most of the other runs
+ *  are oriented either horizontally along the raster lines or
+ *  vertically along pixel columns.
+ * </pre>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config_auto.h>
+#endif  /* HAVE_CONFIG_H */
+
+#include <string.h>
+#include <math.h>
+#include "allheaders.h"
+
+static PIX *pixFindMinRunsOrthogonal(PIX *pixs, l_float32 angle, l_int32 depth);
+
+/*-----------------------------------------------------------------------*
+ *                   Label pixels by membership in runs                  *
+ *-----------------------------------------------------------------------*/
+/*!
+ * \brief   pixStrokeWidthTransform()
+ *
+ * \param[in]     pixs      1 bpp
+ * \param[in]     color     0 for white runs, 1 for black runs
+ * \param[in]     depth     of pixd: 8 or 16 bpp
+ * \param[in]     nangles   2, 4, 6 or 8
+ * \return   pixd   8 or 16 bpp, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) The dest Pix is 8 or 16 bpp, with the pixel values
+ *          equal to the stroke width in which it is a member.
+ *          The values are clipped to the max pixel value if necessary.
+ *      (2) %color determines if we're labelling white or black strokes.
+ *      (3) A pixel that is not a member of the chosen color gets
+ *          value 0; it belongs to a width of length 0 of the
+ *          chosen color.
+ *      (4) This chooses, for each dest pixel, the minimum of sets
+ *          of runlengths through each pixel.  Here are the sets:
+ *            nangles    increment          set
+ *            -------    ---------    --------------------------------
+ *               2          90       {0, 90}
+ *               4          45       {0, 45, 90, 135}
+ *               6          30       {0, 30, 60, 90, 120, 150}
+ *               8          22.5     {0, 22.5, 45, 67.5, 90, 112.5, 135, 157.5}
+ *      (5) Runtime scales linearly with (%nangles - 2).
+ * </pre>
+ */
+PIX *
+pixStrokeWidthTransform(PIX     *pixs,
+                        l_int32  color,
+                        l_int32  depth,
+                        l_int32  nangles)
+{
+l_float32  angle, pi;
+PIX       *pixh, *pixv, *pixt, *pixg1, *pixg2, *pixg3, *pixg4;
+
+    if (!pixs || pixGetDepth(pixs) != 1)
+        return (PIX *)ERROR_PTR("pixs undefined or not 1 bpp", __func__, NULL);
+    if (depth != 8 && depth != 16)
+        return (PIX *)ERROR_PTR("depth must be 8 or 16 bpp", __func__, NULL);
+    if (nangles != 2 && nangles != 4 && nangles != 6 && nangles != 8)
+        return (PIX *)ERROR_PTR("nangles not in {2,4,6,8}", __func__, NULL);
+
+        /* Use fg runs for evaluation */
+    if (color == 0)
+        pixt = pixInvert(NULL, pixs);
+    else
+        pixt = pixClone(pixs);
+
+        /* Find min length at 0 and 90 degrees */
+    pixh = pixRunlengthTransform(pixt, 1, L_HORIZONTAL_RUNS, depth);
+    pixv = pixRunlengthTransform(pixt, 1, L_VERTICAL_RUNS, depth);
+    pixg1 = pixMinOrMax(NULL, pixh, pixv, L_CHOOSE_MIN);
+    pixDestroy(&pixh);
+    pixDestroy(&pixv);
+
+    pixg2 = pixg3 = pixg4 = NULL;
+    pi = 3.1415926535f;
+    if (nangles == 4 || nangles == 8) {
+            /* Find min length at +45 and -45 degrees */
+        angle = pi / 4.0f;
+        pixg2 = pixFindMinRunsOrthogonal(pixt, angle, depth);
+    }
+
+    if (nangles == 6) {
+            /* Find min length at +30 and -60 degrees */
+        angle = pi / 6.0f;
+        pixg2 = pixFindMinRunsOrthogonal(pixt, angle, depth);
+
+            /* Find min length at +60 and -30 degrees */
+        angle = pi / 3.0f;
+        pixg3 = pixFindMinRunsOrthogonal(pixt, angle, depth);
+    }
+
+    if (nangles == 8) {
+            /* Find min length at +22.5 and -67.5 degrees */
+        angle = pi / 8.0f;
+        pixg3 = pixFindMinRunsOrthogonal(pixt, angle, depth);
+
+            /* Find min length at +67.5 and -22.5 degrees */
+        angle = 3.0 * pi / 8.0f;
+        pixg4 = pixFindMinRunsOrthogonal(pixt, angle, depth);
+    }
+    pixDestroy(&pixt);
+
+    if (nangles > 2)
+        pixMinOrMax(pixg1, pixg1, pixg2, L_CHOOSE_MIN);
+    if (nangles > 4)
+        pixMinOrMax(pixg1, pixg1, pixg3, L_CHOOSE_MIN);
+    if (nangles > 6)
+        pixMinOrMax(pixg1, pixg1, pixg4, L_CHOOSE_MIN);
+    pixDestroy(&pixg2);
+    pixDestroy(&pixg3);
+    pixDestroy(&pixg4);
+    return pixg1;
+}
+
+
+/*!
+ * \brief   pixFindMinRunsOrthogonal()
+ *
+ * \param[in]     pixs     1 bpp
+ * \param[in]     angle    in radians
+ * \param[in]     depth    of pixd: 8 or 16 bpp
+ * \return   pixd 8 or 16 bpp, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This computes, for each fg pixel in pixs, the minimum of
+ *          the runlengths going through that pixel in two orthogonal
+ *          directions: at %angle and at (90 + %angle).
+ *      (2) We use rotation by shear because the forward and backward
+ *          rotations by the same angle are exact inverse operations.
+ *          As a result, the nonzero pixels in pixd correspond exactly
+ *          to the fg pixels in pixs.  This is not the case with
+ *          sampled rotation, due to spatial quantization.  Nevertheless,
+ *          the result suffers from lack of exact correspondence
+ *          between original and rotated pixels, also due to spatial
+ *          quantization, causing some boundary pixels to be
+ *          shifted from bg to fg or v.v.
+ * </pre>
+ */
+static PIX *
+pixFindMinRunsOrthogonal(PIX       *pixs,
+                         l_float32  angle,
+                         l_int32    depth)
+{
+l_int32  w, h, diag, xoff, yoff;
+PIX     *pixb, *pixr, *pixh, *pixv, *pixg1, *pixg2, *pixd;
+BOX     *box;
+
+    if (!pixs || pixGetDepth(pixs) != 1)
+        return (PIX *)ERROR_PTR("pixs undefined or not 1 bpp", __func__, NULL);
+
+        /* Rasterop into the center of a sufficiently large image
+         * so we don't lose pixels for any rotation angle. */
+    pixGetDimensions(pixs, &w, &h, NULL);
+    diag = (l_int32)(sqrt((l_float64)(w * w + h * h)) + 2.5);
+    xoff = (diag - w) / 2;
+    yoff = (diag - h) / 2;
+    pixb = pixCreate(diag, diag, 1);
+    pixRasterop(pixb, xoff, yoff, w, h, PIX_SRC, pixs, 0, 0);
+
+        /* Rotate about the 'center', get the min of orthogonal transforms,
+         * rotate back, and crop the part corresponding to pixs.  */
+    pixr = pixRotateShear(pixb, diag / 2, diag / 2, angle, L_BRING_IN_WHITE);
+    pixh = pixRunlengthTransform(pixr, 1, L_HORIZONTAL_RUNS, depth);
+    pixv = pixRunlengthTransform(pixr, 1, L_VERTICAL_RUNS, depth);
+    pixg1 = pixMinOrMax(NULL, pixh, pixv, L_CHOOSE_MIN);
+    pixg2 = pixRotateShear(pixg1, diag / 2, diag / 2, -angle, L_BRING_IN_WHITE);
+    box = boxCreate(xoff, yoff, w, h);
+    pixd = pixClipRectangle(pixg2, box, NULL);
+
+    pixDestroy(&pixb);
+    pixDestroy(&pixr);
+    pixDestroy(&pixh);
+    pixDestroy(&pixv);
+    pixDestroy(&pixg1);
+    pixDestroy(&pixg2);
+    boxDestroy(&box);
+    return pixd;
+}
+
+
+/*!
+ * \brief   pixRunlengthTransform()
+ *
+ * \param[in]     pixs        1 bpp
+ * \param[in]     color       0 for white runs, 1 for black runs
+ * \param[in]     direction   L_HORIZONTAL_RUNS, L_VERTICAL_RUNS
+ * \param[in]     depth       8 or 16 bpp
+ * \return   pixd   8 or 16 bpp, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) The dest Pix is 8 or 16 bpp, with the pixel values
+ *          equal to the runlength in which it is a member.
+ *          The length is clipped to the max pixel value if necessary.
+ *      (2) %color determines if we're labelling white or black runs.
+ *      (3) A pixel that is not a member of the chosen color gets
+ *          value 0; it belongs to a run of length 0 of the
+ *          chosen color.
+ *      (4) To convert for maximum dynamic range, either linear or
+ *          log, use pixMaxDynamicRange().
+ * </pre>
+ */
+PIX *
+pixRunlengthTransform(PIX     *pixs,
+                      l_int32  color,
+                      l_int32  direction,
+                      l_int32  depth)
+{
+l_int32    i, j, w, h, wpld, bufsize, maxsize, n;
+l_int32   *start, *end, *buffer;
+l_uint32  *datad, *lined;
+PIX       *pixt, *pixd;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (pixGetDepth(pixs) != 1)
+        return (PIX *)ERROR_PTR("pixs not 1 bpp", __func__, NULL);
+    if (depth != 8 && depth != 16)
+        return (PIX *)ERROR_PTR("depth must be 8 or 16 bpp", __func__, NULL);
+
+    pixGetDimensions(pixs, &w, &h, NULL);
+    if (direction == L_HORIZONTAL_RUNS)
+        maxsize = 1 + w / 2;
+    else if (direction == L_VERTICAL_RUNS)
+        maxsize = 1 + h / 2;
+    else
+        return (PIX *)ERROR_PTR("invalid direction", __func__, NULL);
+    bufsize = L_MAX(w, h);
+    if (bufsize > 1000000) {
+        L_ERROR("largest image dimension = %d; too big\n", __func__, bufsize);
+        return NULL;
+    }
+
+    if ((pixd = pixCreate(w, h, depth)) == NULL)
+        return (PIX *)ERROR_PTR("pixd not made", __func__, NULL);
+    datad = pixGetData(pixd);
+    wpld = pixGetWpl(pixd);
+
+    start = (l_int32 *)LEPT_CALLOC(maxsize, sizeof(l_int32));
+    end = (l_int32 *)LEPT_CALLOC(maxsize, sizeof(l_int32));
+    buffer = (l_int32 *)LEPT_CALLOC(bufsize, sizeof(l_int32));
+
+        /* Use fg runs for evaluation */
+    if (color == 0)
+        pixt = pixInvert(NULL, pixs);
+    else
+        pixt = pixClone(pixs);
+
+    if (direction == L_HORIZONTAL_RUNS) {
+        for (i = 0; i < h; i++) {
+            pixFindHorizontalRuns(pixt, i, start, end, &n);
+            runlengthMembershipOnLine(buffer, w, depth, start, end, n);
+            lined = datad + i * wpld;
+            if (depth == 8) {
+                for (j = 0; j < w; j++)
+                    SET_DATA_BYTE(lined, j, buffer[j]);
+            } else {  /* depth == 16 */
+                for (j = 0; j < w; j++)
+                    SET_DATA_TWO_BYTES(lined, j, buffer[j]);
+            }
+        }
+    } else {  /* L_VERTICAL_RUNS */
+        for (j = 0; j < w; j++) {
+            pixFindVerticalRuns(pixt, j, start, end, &n);
+            runlengthMembershipOnLine(buffer, h, depth, start, end, n);
+            if (depth == 8) {
+                for (i = 0; i < h; i++) {
+                    lined = datad + i * wpld;
+                    SET_DATA_BYTE(lined, j, buffer[i]);
+                }
+            } else {  /* depth == 16 */
+                for (i = 0; i < h; i++) {
+                    lined = datad + i * wpld;
+                    SET_DATA_TWO_BYTES(lined, j, buffer[i]);
+                }
+            }
+        }
+    }
+
+    pixDestroy(&pixt);
+    LEPT_FREE(start);
+    LEPT_FREE(end);
+    LEPT_FREE(buffer);
+    return pixd;
+}
+
+
+/*-----------------------------------------------------------------------*
+ *               Find runs along horizontal and vertical lines           *
+ *-----------------------------------------------------------------------*/
+/*!
+ * \brief   pixFindHorizontalRuns()
+ *
+ * \param[in]    pix      1 bpp
+ * \param[in]    y        line to traverse
+ * \param[in]    xstart   returns array of start positions for fg runs
+ * \param[in]    xend     returns array of end positions for fg runs
+ * \param[out]   pn       the number of runs found
+ * \return  0 if OK; 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This finds foreground horizontal runs on a single scanline.
+ *      (2) To find background runs, use pixInvert() before applying
+ *          this function.
+ *      (3) %xstart and %xend arrays are input.  They should be
+ *          of size w/2 + 1 to insure that they can hold
+ *          the maximum number of runs in the raster line.
+ * </pre>
+ */
+l_ok
+pixFindHorizontalRuns(PIX      *pix,
+                      l_int32   y,
+                      l_int32  *xstart,
+                      l_int32  *xend,
+                      l_int32  *pn)
+{
+l_int32    inrun;  /* boolean */
+l_int32    index, w, h, d, j, wpl, val;
+l_uint32  *line;
+
+    if (!pn)
+        return ERROR_INT("&n not defined", __func__, 1);
+    *pn = 0;
+    if (!pix)
+        return ERROR_INT("pix not defined", __func__, 1);
+    pixGetDimensions(pix, &w, &h, &d);
+    if (d != 1)
+        return ERROR_INT("pix not 1 bpp", __func__, 1);
+    if (y < 0 || y >= h)
+        return ERROR_INT("y not in [0 ... h - 1]", __func__, 1);
+    if (!xstart)
+        return ERROR_INT("xstart not defined", __func__, 1);
+    if (!xend)
+        return ERROR_INT("xend not defined", __func__, 1);
+
+    wpl = pixGetWpl(pix);
+    line = pixGetData(pix) + y * wpl;
+
+    inrun = FALSE;
+    index = 0;
+    for (j = 0; j < w; j++) {
+        val = GET_DATA_BIT(line, j);
+        if (!inrun) {
+            if (val) {
+                xstart[index] = j;
+                inrun = TRUE;
+            }
+        } else {
+            if (!val) {
+                xend[index++] = j - 1;
+                inrun = FALSE;
+            }
+        }
+    }
+
+        /* Finish last run if necessary */
+    if (inrun)
+        xend[index++] = w - 1;
+
+    *pn = index;
+    return 0;
+}
+
+
+/*!
+ * \brief   pixFindVerticalRuns()
+ *
+ * \param[in]    pix      1 bpp
+ * \param[in]    x        line to traverse
+ * \param[in]    ystart   returns array of start positions for fg runs
+ * \param[in]    yend     returns array of end positions for fg runs
+ * \param[out]   pn       the number of runs found
+ * \return  0 if OK; 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This finds foreground vertical runs on a single scanline.
+ *      (2) To find background runs, use pixInvert() before applying
+ *          this function.
+ *      (3) %ystart and %yend arrays are input.  They should be
+ *          of size h/2 + 1 to insure that they can hold
+ *          the maximum number of runs in the raster line.
+ * </pre>
+ */
+l_ok
+pixFindVerticalRuns(PIX      *pix,
+                    l_int32   x,
+                    l_int32  *ystart,
+                    l_int32  *yend,
+                    l_int32  *pn)
+{
+l_int32    inrun;  /* boolean */
+l_int32    index, w, h, d, i, wpl, val;
+l_uint32  *data, *line;
+
+    if (!pn)
+        return ERROR_INT("&n not defined", __func__, 1);
+    *pn = 0;
+    if (!pix)
+        return ERROR_INT("pix not defined", __func__, 1);
+    pixGetDimensions(pix, &w, &h, &d);
+    if (d != 1)
+        return ERROR_INT("pix not 1 bpp", __func__, 1);
+    if (x < 0 || x >= w)
+        return ERROR_INT("x not in [0 ... w - 1]", __func__, 1);
+    if (!ystart)
+        return ERROR_INT("ystart not defined", __func__, 1);
+    if (!yend)
+        return ERROR_INT("yend not defined", __func__, 1);
+
+    wpl = pixGetWpl(pix);
+    data = pixGetData(pix);
+
+    inrun = FALSE;
+    index = 0;
+    for (i = 0; i < h; i++) {
+        line = data + i * wpl;
+        val = GET_DATA_BIT(line, x);
+        if (!inrun) {
+            if (val) {
+                ystart[index] = i;
+                inrun = TRUE;
+            }
+        } else {
+            if (!val) {
+                yend[index++] = i - 1;
+                inrun = FALSE;
+            }
+        }
+    }
+
+        /* Finish last run if necessary */
+    if (inrun)
+        yend[index++] = h - 1;
+
+    *pn = index;
+    return 0;
+}
+
+
+/*-----------------------------------------------------------------------*
+ *            Find max runs along horizontal and vertical lines          *
+ *-----------------------------------------------------------------------*/
+/*!
+ * \brief   pixFindMaxRuns()
+ *
+ * \param[in]    pix         1 bpp
+ * \param[in]    direction   L_HORIZONTAL_RUNS or L_VERTICAL_RUNS
+ * \param[out]   pnastart    [optional] start locations of longest runs
+ * \return  na of lengths of runs, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This finds the longest foreground runs by row or column
+ *      (2) To find background runs, use pixInvert() before applying
+ *          this function.
+ * </pre>
+ */
+NUMA *
+pixFindMaxRuns(PIX     *pix,
+               l_int32  direction,
+               NUMA   **pnastart)
+{
+l_int32  w, h, i, start, size;
+NUMA    *nasize;
+
+    if (pnastart) *pnastart = NULL;
+    if (direction != L_HORIZONTAL_RUNS && direction != L_VERTICAL_RUNS)
+        return (NUMA *)ERROR_PTR("direction invalid", __func__, NULL);
+    if (!pix || pixGetDepth(pix) != 1)
+        return (NUMA *)ERROR_PTR("pix undefined or not 1 bpp", __func__, NULL);
+
+    pixGetDimensions(pix, &w, &h, NULL);
+    nasize = numaCreate(w);
+    if (pnastart) *pnastart = numaCreate(w);
+    if (direction == L_HORIZONTAL_RUNS) {
+        for (i = 0; i < h; i++) {
+            pixFindMaxHorizontalRunOnLine(pix, i, &start, &size);
+            numaAddNumber(nasize, size);
+            if (pnastart) numaAddNumber(*pnastart, start);
+        }
+    } else {  /* vertical scans */
+        for (i = 0; i < w; i++) {
+            pixFindMaxVerticalRunOnLine(pix, i, &start, &size);
+            numaAddNumber(nasize, size);
+            if (pnastart) numaAddNumber(*pnastart, start);
+        }
+    }
+
+    return nasize;
+}
+
+
+/*!
+ * \brief   pixFindMaxHorizontalRunOnLine()
+ *
+ * \param[in]    pix       1 bpp
+ * \param[in]    y         line to traverse
+ * \param[out]   pxstart   [optional] start position
+ * \param[out]   psize     the size of the run
+ * \return  0 if OK; 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This finds the longest foreground horizontal run on a scanline.
+ *      (2) To find background runs, use pixInvert() before applying
+ *          this function.
+ * </pre>
+ */
+l_ok
+pixFindMaxHorizontalRunOnLine(PIX      *pix,
+                              l_int32   y,
+                              l_int32  *pxstart,
+                              l_int32  *psize)
+{
+l_int32    inrun;  /* boolean */
+l_int32    w, h, j, wpl, val, maxstart, maxsize, length, start;
+l_uint32  *line;
+
+    if (pxstart) *pxstart = 0;
+    if (!psize)
+        return ERROR_INT("&size not defined", __func__, 1);
+    *psize = 0;
+    if (!pix || pixGetDepth(pix) != 1)
+        return ERROR_INT("pix not defined or not 1 bpp", __func__, 1);
+    pixGetDimensions(pix, &w, &h, NULL);
+    if (y < 0 || y >= h)
+        return ERROR_INT("y not in [0 ... h - 1]", __func__, 1);
+
+    wpl = pixGetWpl(pix);
+    line = pixGetData(pix) + y * wpl;
+    inrun = FALSE;
+    start = 0;
+    maxstart = 0;
+    maxsize = 0;
+    for (j = 0; j < w; j++) {
+        val = GET_DATA_BIT(line, j);
+        if (!inrun) {
+            if (val) {
+                start = j;
+                inrun = TRUE;
+            }
+        } else if (!val) {  /* run just ended */
+            length = j - start;
+            if (length > maxsize) {
+                maxsize = length;
+                maxstart = start;
+            }
+            inrun = FALSE;
+        }
+    }
+
+    if (inrun) {  /* a run has continued to the end of the row */
+        length = j - start;
+        if (length > maxsize) {
+            maxsize = length;
+            maxstart = start;
+        }
+    }
+    if (pxstart) *pxstart = maxstart;
+    *psize = maxsize;
+    return 0;
+}
+
+
+/*!
+ * \brief   pixFindMaxVerticalRunOnLine()
+ *
+ * \param[in]    pix       1 bpp
+ * \param[in]    x         column to traverse
+ * \param[out]   pystart   [optional] start position
+ * \param[out]   psize     the size of the run
+ * \return  0 if OK; 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This finds the longest foreground vertical run on a scanline.
+ *      (2) To find background runs, use pixInvert() before applying
+ *          this function.
+ * </pre>
+ */
+l_ok
+pixFindMaxVerticalRunOnLine(PIX      *pix,
+                            l_int32   x,
+                            l_int32  *pystart,
+                            l_int32  *psize)
+{
+l_int32    inrun;  /* boolean */
+l_int32    w, h, i, wpl, val, maxstart, maxsize, length, start;
+l_uint32  *data, *line;
+
+    if (pystart) *pystart = 0;
+    if (!psize)
+        return ERROR_INT("&size not defined", __func__, 1);
+    *psize = 0;
+    if (!pix || pixGetDepth(pix) != 1)
+        return ERROR_INT("pix not defined or not 1 bpp", __func__, 1);
+    pixGetDimensions(pix, &w, &h, NULL);
+    if (x < 0 || x >= w)
+        return ERROR_INT("x not in [0 ... w - 1]", __func__, 1);
+
+    wpl = pixGetWpl(pix);
+    data = pixGetData(pix);
+    inrun = FALSE;
+    start = 0;
+    maxstart = 0;
+    maxsize = 0;
+    for (i = 0; i < h; i++) {
+        line = data + i * wpl;
+        val = GET_DATA_BIT(line, x);
+        if (!inrun) {
+            if (val) {
+                start = i;
+                inrun = TRUE;
+            }
+        } else if (!val) {  /* run just ended */
+            length = i - start;
+            if (length > maxsize) {
+                maxsize = length;
+                maxstart = start;
+            }
+            inrun = FALSE;
+        }
+    }
+
+    if (inrun) {  /* a run has continued to the end of the column */
+        length = i - start;
+        if (length > maxsize) {
+            maxsize = length;
+            maxstart = start;
+        }
+    }
+    if (pystart) *pystart = maxstart;
+    *psize = maxsize;
+    return 0;
+}
+
+
+/*-----------------------------------------------------------------------*
+ *            Compute runlength-to-membership transform on a line        *
+ *-----------------------------------------------------------------------*/
+/*!
+ * \brief   runlengthMembershipOnLine()
+ *
+ * \param[in]     buffer   into which full line of data is placed
+ * \param[in]     size     full size of line; w or h
+ * \param[in]     depth    8 or 16 bpp
+ * \param[in]     start    array of start positions for fg runs
+ * \param[in]     end      array of end positions for fg runs
+ * \param[in]     n        the number of runs
+ * \return   0 if OK; 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Converts a set of runlengths into a buffer of
+ *          runlength membership values.
+ *      (2) Initialization of the array gives pixels that are
+ *          not within a run the value 0.
+ * </pre>
+ */
+l_ok
+runlengthMembershipOnLine(l_int32  *buffer,
+                          l_int32   size,
+                          l_int32   depth,
+                          l_int32  *start,
+                          l_int32  *end,
+                          l_int32   n)
+{
+l_int32  i, j, first, last, diff, max;
+
+    if (!buffer)
+        return ERROR_INT("buffer not defined", __func__, 1);
+    if (!start)
+        return ERROR_INT("start not defined", __func__, 1);
+    if (!end)
+        return ERROR_INT("end not defined", __func__, 1);
+
+    if (depth == 8)
+        max = 0xff;
+    else  /* depth == 16 */
+        max = 0xffff;
+
+    memset(buffer, 0, 4 * size);
+    for (i = 0; i < n; i++) {
+        first = start[i];
+        last = end[i];
+        diff = last - first + 1;
+        diff = L_MIN(diff, max);
+        for (j = first; j <= last; j++)
+            buffer[j] = diff;
+    }
+
+    return 0;
+}
+
+
+/*-----------------------------------------------------------------------*
+ *                       Make byte position LUT                          *
+ *-----------------------------------------------------------------------*/
+/*!
+ * \brief   makeMSBitLocTab()
+ *
+ * \param[in]    bitval   either 0 or 1
+ * \return  table:  for an input byte, the MS bit location, starting at 0
+ *                  with the MSBit in the byte, or NULL on error.
+ *
+ * <pre>
+ * Notes:
+ *      (1) If %bitval == 1, it finds the leftmost ON pixel in a byte;
+ *          otherwise if %bitval == 0, it finds the leftmost OFF pixel.
+ *      (2) If there are no pixels of the indicated color in the byte,
+ *          this returns 8.
+ * </pre>
+ */
+l_int32 *
+makeMSBitLocTab(l_int32  bitval)
+{
+l_int32   i, j;
+l_int32  *tab;
+l_uint8   byte, mask;
+
+    tab = (l_int32 *)LEPT_CALLOC(256, sizeof(l_int32));
+    for (i = 0; i < 256; i++) {
+        byte = (l_uint8)i;
+        if (bitval == 0)
+            byte = ~byte;
+        tab[i] = 8;
+        mask = 0x80;
+        for (j = 0; j < 8; j++) {
+            if (byte & mask) {
+                tab[i] = j;
+                break;
+            }
+            mask >>= 1;
+        }
+    }
+    return tab;
+}