diff mupdf-source/thirdparty/leptonica/src/edge.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/edge.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,635 @@
+/*====================================================================*
+ -  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 edge.c
+ * <pre>
+ *
+ *      Sobel edge detecting filter
+ *          PIX      *pixSobelEdgeFilter()
+ *
+ *      Two-sided edge gradient filter
+ *          PIX      *pixTwoSidedEdgeFilter()
+ *
+ *      Measurement of edge smoothness
+ *          l_int32   pixMeasureEdgeSmoothness()
+ *          NUMA     *pixGetEdgeProfile()
+ *          l_int32   pixGetLastOffPixelInRun()
+ *          l_int32   pixGetLastOnPixelInRun()
+ *
+ *
+ *  The Sobel edge detector uses these two simple gradient filters.
+ *
+ *       1    2    1             1    0   -1
+ *       0    0    0             2    0   -2
+ *      -1   -2   -1             1    0   -1
+ *
+ *      (horizontal)             (vertical)
+ *
+ *  To use both the vertical and horizontal filters, set the orientation
+ *  flag to L_ALL_EDGES; this sums the abs. value of their outputs,
+ *  clipped to 255.
+ *
+ *  See comments below for displaying the resulting image with
+ *  the edges dark, both for 8 bpp and 1 bpp.
+ * </pre>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config_auto.h>
+#endif  /* HAVE_CONFIG_H */
+
+#include "allheaders.h"
+
+/*----------------------------------------------------------------------*
+ *                    Sobel edge detecting filter                       *
+ *----------------------------------------------------------------------*/
+/*!
+ * \brief   pixSobelEdgeFilter()
+ *
+ * \param[in]    pixs         8 bpp; no colormap
+ * \param[in]    orientflag   L_HORIZONTAL_EDGES, L_VERTICAL_EDGES, L_ALL_EDGES
+ * \return  pixd   8 bpp, edges are brighter, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Invert pixd to see larger gradients as darker (grayscale).
+ *      (2) To generate a binary image of the edges, threshold
+ *          the result using pixThresholdToBinary().  If the high
+ *          edge values are to be fg (1), invert after running
+ *          pixThresholdToBinary().
+ *      (3) Label the pixels as follows:
+ *              1    4    7
+ *              2    5    8
+ *              3    6    9
+ *          Read the data incrementally across the image and unroll
+ *          the loop.
+ *      (4) This runs at about 45 Mpix/sec on a 3 GHz processor.
+ * </pre>
+ */
+PIX *
+pixSobelEdgeFilter(PIX     *pixs,
+                   l_int32  orientflag)
+{
+l_int32    w, h, d, i, j, wplt, wpld, gx, gy, vald;
+l_int32    val1, val2, val3, val4, val5, val6, val7, val8, val9;
+l_uint32  *datat, *linet, *datad, *lined;
+PIX       *pixt, *pixd;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (d != 8)
+        return (PIX *)ERROR_PTR("pixs not 8 bpp", __func__, NULL);
+    if (orientflag != L_HORIZONTAL_EDGES && orientflag != L_VERTICAL_EDGES &&
+        orientflag != L_ALL_EDGES)
+        return (PIX *)ERROR_PTR("invalid orientflag", __func__, NULL);
+
+        /* Add 1 pixel (mirrored) to each side of the image. */
+    if ((pixt = pixAddMirroredBorder(pixs, 1, 1, 1, 1)) == NULL)
+        return (PIX *)ERROR_PTR("pixt not made", __func__, NULL);
+
+        /* Compute filter output at each location. */
+    pixd = pixCreateTemplate(pixs);
+    datat = pixGetData(pixt);
+    wplt = pixGetWpl(pixt);
+    datad = pixGetData(pixd);
+    wpld = pixGetWpl(pixd);
+    for (i = 0; i < h; i++) {
+        linet = datat + i * wplt;
+        lined = datad + i * wpld;
+        for (j = 0; j < w; j++) {
+            if (j == 0) {  /* start a new row */
+                val1 = GET_DATA_BYTE(linet, j);
+                val2 = GET_DATA_BYTE(linet + wplt, j);
+                val3 = GET_DATA_BYTE(linet + 2 * wplt, j);
+                val4 = GET_DATA_BYTE(linet, j + 1);
+                val5 = GET_DATA_BYTE(linet + wplt, j + 1);
+                val6 = GET_DATA_BYTE(linet + 2 * wplt, j + 1);
+                val7 = GET_DATA_BYTE(linet, j + 2);
+                val8 = GET_DATA_BYTE(linet + wplt, j + 2);
+                val9 = GET_DATA_BYTE(linet + 2 * wplt, j + 2);
+            } else {  /* shift right by 1 pixel; update incrementally */
+                val1 = val4;
+                val2 = val5;
+                val3 = val6;
+                val4 = val7;
+                val5 = val8;
+                val6 = val9;
+                val7 = GET_DATA_BYTE(linet, j + 2);
+                val8 = GET_DATA_BYTE(linet + wplt, j + 2);
+                val9 = GET_DATA_BYTE(linet + 2 * wplt, j + 2);
+            }
+            if (orientflag == L_HORIZONTAL_EDGES)
+                vald = L_ABS(val1 + 2 * val4 + val7
+                             - val3 - 2 * val6 - val9) >> 3;
+            else if (orientflag == L_VERTICAL_EDGES)
+                vald = L_ABS(val1 + 2 * val2 + val3 - val7
+                             - 2 * val8 - val9) >> 3;
+            else {  /* L_ALL_EDGES */
+                gx = L_ABS(val1 + 2 * val2 + val3 - val7
+                           - 2 * val8 - val9) >> 3;
+                gy = L_ABS(val1 + 2 * val4 + val7
+                             - val3 - 2 * val6 - val9) >> 3;
+                vald = L_MIN(255, gx + gy);
+            }
+            SET_DATA_BYTE(lined, j, vald);
+        }
+    }
+
+    pixDestroy(&pixt);
+    return pixd;
+}
+
+
+/*----------------------------------------------------------------------*
+ *                   Two-sided edge gradient filter                     *
+ *----------------------------------------------------------------------*/
+/*!
+ * \brief   pixTwoSidedEdgeFilter()
+ *
+ * \param[in]    pixs         8 bpp; no colormap
+ * \param[in]    orientflag   L_HORIZONTAL_EDGES, L_VERTICAL_EDGES
+ * \return  pixd    8 bpp, edges are brighter, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) For detecting vertical edges, this considers the
+ *          difference of the central pixel from those on the left
+ *          and right.  For situations where the gradient is the same
+ *          sign on both sides, this computes and stores the minimum
+ *          (absolute value of the) difference.  The reason for
+ *          checking the sign is that we are looking for pixels within
+ *          a transition.  By contrast, for single pixel noise, the pixel
+ *          value is either larger than or smaller than its neighbors,
+ *          so the gradient would change direction on each side.  Horizontal
+ *          edges are handled similarly, looking for vertical gradients.
+ *      (2) To generate a binary image of the edges, threshold
+ *          the result using pixThresholdToBinary().  If the high
+ *          edge values are to be fg (1), invert after running
+ *          pixThresholdToBinary().
+ *      (3) This runs at about 60 Mpix/sec on a 3 GHz processor.
+ *          It is about 30% faster than Sobel, and the results are
+ *          similar.
+ * </pre>
+ */
+PIX *
+pixTwoSidedEdgeFilter(PIX     *pixs,
+                      l_int32  orientflag)
+{
+l_int32    w, h, d, i, j, wpls, wpld;
+l_int32    cval, rval, bval, val, lgrad, rgrad, tgrad, bgrad;
+l_uint32  *datas, *lines, *datad, *lined;
+PIX       *pixd;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (d != 8)
+        return (PIX *)ERROR_PTR("pixs not 8 bpp", __func__, NULL);
+    if (orientflag != L_HORIZONTAL_EDGES && orientflag != L_VERTICAL_EDGES)
+        return (PIX *)ERROR_PTR("invalid orientflag", __func__, NULL);
+
+    pixd = pixCreateTemplate(pixs);
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+    datad = pixGetData(pixd);
+    wpld = pixGetWpl(pixd);
+    if (orientflag == L_VERTICAL_EDGES) {
+        for (i = 0; i < h; i++) {
+            lines = datas + i * wpls;
+            lined = datad + i * wpld;
+            cval = GET_DATA_BYTE(lines, 1);
+            lgrad = cval - GET_DATA_BYTE(lines, 0);
+            for (j = 1; j < w - 1; j++) {
+                rval = GET_DATA_BYTE(lines, j + 1);
+                rgrad = rval - cval;
+                if (lgrad * rgrad > 0) {
+                    if (lgrad < 0)
+                        val = -L_MAX(lgrad, rgrad);
+                    else
+                        val = L_MIN(lgrad, rgrad);
+                    SET_DATA_BYTE(lined, j, val);
+                }
+                lgrad = rgrad;
+                cval = rval;
+            }
+        }
+    }
+    else {  /* L_HORIZONTAL_EDGES) */
+        for (j = 0; j < w; j++) {
+            lines = datas + wpls;
+            cval = GET_DATA_BYTE(lines, j);  /* for line 1 */
+            tgrad = cval - GET_DATA_BYTE(datas, j);
+            for (i = 1; i < h - 1; i++) {
+                lines += wpls;  /* for line i + 1 */
+                lined = datad + i * wpld;
+                bval = GET_DATA_BYTE(lines, j);
+                bgrad = bval - cval;
+                if (tgrad * bgrad > 0) {
+                    if (tgrad < 0)
+                        val = -L_MAX(tgrad, bgrad);
+                    else
+                        val = L_MIN(tgrad, bgrad);
+                    SET_DATA_BYTE(lined, j, val);
+                }
+                tgrad = bgrad;
+                cval = bval;
+            }
+        }
+    }
+
+    return pixd;
+}
+
+
+/*----------------------------------------------------------------------*
+ *                   Measurement of edge smoothness                     *
+ *----------------------------------------------------------------------*/
+/*!
+ * \brief   pixMeasureEdgeSmoothness()
+ *
+ * \param[in]    pixs          1 bpp
+ * \param[in]    side          L_FROM_LEFT, L_FROM_RIGHT, L_FROM_TOP, L_FROM_BOT
+ * \param[in]    minjump       minimum jump to be counted; >= 1
+ * \param[in]    minreversal   minimum reversal size for new peak or valley
+ * \param[out]   pjpl          [optional] jumps/length: number of jumps,
+ *                             normalized to length of component side
+ * \param[out]   pjspl         [optional] jumpsum/length: sum of all
+ *                             sufficiently large jumps, normalized to length
+ *                             of component side
+ * \param[out]   prpl          [optional] reversals/length: number of
+ *                             peak-to-valley or valley-to-peak reversals,
+ *                             normalized to length of component side
+ * \param[in]    debugfile     [optional] displays constructed edge; use NULL
+ *                             for no output
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This computes three measures of smoothness of the edge of a
+ *          connected component:
+ *            * jumps/length: (jpl) the number of jumps of size >= %minjump,
+ *              normalized to the length of the side
+ *            * jump sum/length: (jspl) the sum of all jump lengths of
+ *              size >= %minjump, normalized to the length of the side
+ *            * reversals/length: (rpl) the number of peak <--> valley
+ *              reversals, using %minreverse as a minimum deviation of
+ *              the peak or valley from its preceding extremum,
+ *              normalized to the length of the side
+ *      (2) The input pix should be a single connected component, but
+ *          this is not required.
+ * </pre>
+ */
+l_ok
+pixMeasureEdgeSmoothness(PIX         *pixs,
+                         l_int32      side,
+                         l_int32      minjump,
+                         l_int32      minreversal,
+                         l_float32   *pjpl,
+                         l_float32   *pjspl,
+                         l_float32   *prpl,
+                         const char  *debugfile)
+{
+l_int32  i, n, val, nval, diff, njumps, jumpsum, nreversal;
+NUMA    *na, *nae;
+
+    if (pjpl) *pjpl = 0.0;
+    if (pjspl) *pjspl = 0.0;
+    if (prpl) *prpl = 0.0;
+    if (!pjpl && !pjspl && !prpl && !debugfile)
+        return ERROR_INT("no output requested", __func__, 1);
+    if (!pixs || pixGetDepth(pixs) != 1)
+        return ERROR_INT("pixs not defined or not 1 bpp", __func__, 1);
+    if (side != L_FROM_LEFT && side != L_FROM_RIGHT &&
+        side != L_FROM_TOP && side != L_FROM_BOT)
+        return ERROR_INT("invalid side", __func__, 1);
+    if (minjump < 1)
+        return ERROR_INT("invalid minjump; must be >= 1", __func__, 1);
+    if (minreversal < 1)
+        return ERROR_INT("invalid minreversal; must be >= 1", __func__, 1);
+
+    if ((na = pixGetEdgeProfile(pixs, side, debugfile)) == NULL)
+        return ERROR_INT("edge profile not made", __func__, 1);
+    if ((n = numaGetCount(na)) < 2) {
+        numaDestroy(&na);
+        return 0;
+    }
+
+    if (pjpl || pjspl) {
+        jumpsum = 0;
+        njumps = 0;
+        numaGetIValue(na, 0, &val);
+        for (i = 1; i < n; i++) {
+            numaGetIValue(na, i, &nval);
+            diff = L_ABS(nval - val);
+            if (diff >= minjump) {
+                njumps++;
+                jumpsum += diff;
+            }
+            val = nval;
+        }
+        if (pjpl)
+            *pjpl = (l_float32)njumps / (l_float32)(n - 1);
+        if (pjspl)
+            *pjspl = (l_float32)jumpsum / (l_float32)(n - 1);
+    }
+
+    if (prpl) {
+        nae = numaFindExtrema(na, minreversal, NULL);
+        nreversal = numaGetCount(nae) - 1;
+        *prpl = (l_float32)nreversal / (l_float32)(n - 1);
+        numaDestroy(&nae);
+    }
+
+    numaDestroy(&na);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetEdgeProfile()
+ *
+ * \param[in]    pixs        1 bpp
+ * \param[in]    side        L_FROM_LEFT, L_FROM_RIGHT, L_FROM_TOP, L_FROM_BOT
+ * \param[in]    debugfile   [optional] displays constructed edge; use NULL
+ *                           for no output
+ * \return  na   of fg edge pixel locations, or NULL on error
+ */
+NUMA *
+pixGetEdgeProfile(PIX         *pixs,
+                  l_int32      side,
+                  const char  *debugfile)
+{
+l_int32   x, y, w, h, loc, index, ival;
+l_uint32  val;
+NUMA     *na;
+PIX      *pixt;
+PIXCMAP  *cmap;
+
+    if (!pixs || pixGetDepth(pixs) != 1)
+        return (NUMA *)ERROR_PTR("pixs undefined or not 1 bpp", __func__, NULL);
+    if (side != L_FROM_LEFT && side != L_FROM_RIGHT &&
+        side != L_FROM_TOP && side != L_FROM_BOT)
+        return (NUMA *)ERROR_PTR("invalid side", __func__, NULL);
+
+    pixGetDimensions(pixs, &w, &h, NULL);
+    if (side == L_FROM_LEFT || side == L_FROM_RIGHT)
+        na = numaCreate(h);
+    else
+        na = numaCreate(w);
+    if (side == L_FROM_LEFT) {
+        pixGetLastOffPixelInRun(pixs, 0, 0, L_FROM_LEFT, &loc);
+        loc = (loc == w - 1) ? 0 : loc + 1;  /* back to the left edge */
+        numaAddNumber(na, loc);
+        for (y = 1; y < h; y++) {
+            pixGetPixel(pixs, loc, y, &val);
+            if (val == 1) {
+                pixGetLastOnPixelInRun(pixs, loc, y, L_FROM_RIGHT, &loc);
+            } else {
+                pixGetLastOffPixelInRun(pixs, loc, y, L_FROM_LEFT, &loc);
+                loc = (loc == w - 1) ? 0 : loc + 1;
+            }
+            numaAddNumber(na, loc);
+        }
+    }
+    else if (side == L_FROM_RIGHT) {
+        pixGetLastOffPixelInRun(pixs, w - 1, 0, L_FROM_RIGHT, &loc);
+        loc = (loc == 0) ? w - 1 : loc - 1;  /* back to the right edge */
+        numaAddNumber(na, loc);
+        for (y = 1; y < h; y++) {
+            pixGetPixel(pixs, loc, y, &val);
+            if (val == 1) {
+                pixGetLastOnPixelInRun(pixs, loc, y, L_FROM_LEFT, &loc);
+            } else {
+                pixGetLastOffPixelInRun(pixs, loc, y, L_FROM_RIGHT, &loc);
+                loc = (loc == 0) ? w - 1 : loc - 1;
+            }
+            numaAddNumber(na, loc);
+        }
+    }
+    else if (side == L_FROM_TOP) {
+        pixGetLastOffPixelInRun(pixs, 0, 0, L_FROM_TOP, &loc);
+        loc = (loc == h - 1) ? 0 : loc + 1;  /* back to the top edge */
+        numaAddNumber(na, loc);
+        for (x = 1; x < w; x++) {
+            pixGetPixel(pixs, x, loc, &val);
+            if (val == 1) {
+                pixGetLastOnPixelInRun(pixs, x, loc, L_FROM_BOT, &loc);
+            } else {
+                pixGetLastOffPixelInRun(pixs, x, loc, L_FROM_TOP, &loc);
+                loc = (loc == h - 1) ? 0 : loc + 1;
+            }
+            numaAddNumber(na, loc);
+        }
+    }
+    else {  /* side == L_FROM_BOT */
+        pixGetLastOffPixelInRun(pixs, 0, h - 1, L_FROM_BOT, &loc);
+        loc = (loc == 0) ? h - 1 : loc - 1;  /* back to the bottom edge */
+        numaAddNumber(na, loc);
+        for (x = 1; x < w; x++) {
+            pixGetPixel(pixs, x, loc, &val);
+            if (val == 1) {
+                pixGetLastOnPixelInRun(pixs, x, loc, L_FROM_TOP, &loc);
+            } else {
+                pixGetLastOffPixelInRun(pixs, x, loc, L_FROM_BOT, &loc);
+                loc = (loc == 0) ? h - 1 : loc - 1;
+            }
+            numaAddNumber(na, loc);
+        }
+    }
+
+    if (debugfile) {
+        pixt = pixConvertTo8(pixs, TRUE);
+        cmap = pixGetColormap(pixt);
+        pixcmapAddColor(cmap, 255, 0, 0);
+        index = pixcmapGetCount(cmap) - 1;
+        if (side == L_FROM_LEFT || side == L_FROM_RIGHT) {
+            for (y = 0; y < h; y++) {
+                numaGetIValue(na, y, &ival);
+                pixSetPixel(pixt, ival, y, index);
+            }
+        } else {  /* L_FROM_TOP or L_FROM_BOT */
+            for (x = 0; x < w; x++) {
+                numaGetIValue(na, x, &ival);
+                pixSetPixel(pixt, x, ival, index);
+            }
+        }
+        pixWrite(debugfile, pixt, IFF_PNG);
+        pixDestroy(&pixt);
+    }
+
+    return na;
+}
+
+
+/*
+ * \brief   pixGetLastOffPixelInRun()
+ *
+ * \param[in]    pixs        1 bpp
+ * \param[in]    x, y        starting location
+ * \param[in]    direction   L_FROM_LEFT, L_FROM_RIGHT, L_FROM_TOP, L_FROM_BOT
+ * \param[out]   ploc        location in scan direction coordinate
+ *                           of last OFF pixel found
+ * \return   0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Search starts from the pixel at (x, y), which is OFF.
+ *      (2) It returns the location in the scan direction of the last
+ *          pixel in the current run that is OFF.
+ *      (3) The interface for these pixel run functions is cleaner when
+ *          you ask for the last pixel in the current run, rather than the
+ *          first pixel of opposite polarity that is found, because the
+ *          current run may go to the edge of the image, in which case
+ *          no pixel of opposite polarity is found.
+ * </pre>
+ */
+l_ok
+pixGetLastOffPixelInRun(PIX      *pixs,
+                        l_int32   x,
+                        l_int32   y,
+                        l_int32   direction,
+                        l_int32  *ploc)
+{
+l_int32   loc, w, h;
+l_uint32  val;
+
+    if (!ploc)
+        return ERROR_INT("&loc not defined", __func__, 1);
+    *ploc = 0;
+    if (!pixs || pixGetDepth(pixs) != 1)
+        return ERROR_INT("pixs undefined or not 1 bpp", __func__, 1);
+    if (direction != L_FROM_LEFT && direction != L_FROM_RIGHT &&
+        direction != L_FROM_TOP && direction != L_FROM_BOT)
+        return ERROR_INT("invalid side", __func__, 1);
+
+    pixGetDimensions(pixs, &w, &h, NULL);
+    if (direction == L_FROM_LEFT) {
+        for (loc = x; loc < w; loc++) {
+            pixGetPixel(pixs, loc, y, &val);
+            if (val == 1)
+                break;
+        }
+        *ploc = loc - 1;
+    } else if (direction == L_FROM_RIGHT) {
+        for (loc = x; loc >= 0; loc--) {
+            pixGetPixel(pixs, loc, y, &val);
+            if (val == 1)
+                break;
+        }
+        *ploc = loc + 1;
+    }
+    else if (direction == L_FROM_TOP) {
+        for (loc = y; loc < h; loc++) {
+            pixGetPixel(pixs, x, loc, &val);
+            if (val == 1)
+                break;
+        }
+        *ploc = loc - 1;
+    }
+    else if (direction == L_FROM_BOT) {
+        for (loc = y; loc >= 0; loc--) {
+            pixGetPixel(pixs, x, loc, &val);
+            if (val == 1)
+                break;
+        }
+        *ploc = loc + 1;
+    }
+    return 0;
+}
+
+
+/*
+ * \brief   pixGetLastOnPixelInRun()
+ *
+ * \param[in]    pixs        1 bpp
+ * \param[in]    x, y        starting location
+ * \param[in]    direction   L_FROM_LEFT, L_FROM_RIGHT, L_FROM_TOP, L_FROM_BOT
+ * \param[out]   ploc        location in scan direction coordinate
+ *                           of first ON pixel found
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Search starts from the pixel at (x, y), which is ON.
+ *      (2) It returns the location in the scan direction of the last
+ *          pixel in the current run that is ON.
+ * </pre>
+ */
+l_int32
+pixGetLastOnPixelInRun(PIX      *pixs,
+                       l_int32   x,
+                       l_int32   y,
+                       l_int32   direction,
+                       l_int32  *ploc)
+{
+l_int32   loc, w, h;
+l_uint32  val;
+
+    if (!ploc)
+        return ERROR_INT("&loc not defined", __func__, 1);
+    *ploc = 0;
+    if (!pixs || pixGetDepth(pixs) != 1)
+        return ERROR_INT("pixs undefined or not 1 bpp", __func__, 1);
+    if (direction != L_FROM_LEFT && direction != L_FROM_RIGHT &&
+        direction != L_FROM_TOP && direction != L_FROM_BOT)
+        return ERROR_INT("invalid side", __func__, 1);
+
+    pixGetDimensions(pixs, &w, &h, NULL);
+    if (direction == L_FROM_LEFT) {
+        for (loc = x; loc < w; loc++) {
+            pixGetPixel(pixs, loc, y, &val);
+            if (val == 0)
+                break;
+        }
+        *ploc = loc - 1;
+    } else if (direction == L_FROM_RIGHT) {
+        for (loc = x; loc >= 0; loc--) {
+            pixGetPixel(pixs, loc, y, &val);
+            if (val == 0)
+                break;
+        }
+        *ploc = loc + 1;
+    }
+    else if (direction == L_FROM_TOP) {
+        for (loc = y; loc < h; loc++) {
+            pixGetPixel(pixs, x, loc, &val);
+            if (val == 0)
+                break;
+        }
+        *ploc = loc - 1;
+    }
+    else if (direction == L_FROM_BOT) {
+        for (loc = y; loc >= 0; loc--) {
+            pixGetPixel(pixs, x, loc, &val);
+            if (val == 0)
+                break;
+        }
+        *ploc = loc + 1;
+    }
+    return 0;
+}