diff mupdf-source/thirdparty/leptonica/src/pnmio.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/pnmio.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1483 @@
+/*====================================================================*
+ -  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 pnmio.c
+ * <pre>
+ *
+ *      Stream interface
+ *          PIX             *pixReadStreamPnm()
+ *          l_int32          readHeaderPnm()
+ *          l_int32          freadHeaderPnm()
+ *          l_int32          pixWriteStreamPnm()
+ *          l_int32          pixWriteStreamAsciiPnm()
+ *          l_int32          pixWriteStreamPam()
+ *
+ *      Read/write to memory
+ *          PIX             *pixReadMemPnm()
+ *          l_int32          readHeaderMemPnm()
+ *          l_int32          pixWriteMemPnm()
+ *          l_int32          pixWriteMemPam()
+ *
+ *      Local helpers
+ *          static l_int32   pnmReadNextAsciiValue();
+ *          static l_int32   pnmReadNextNumber();
+ *          static l_int32   pnmReadNextString();
+ *          static l_int32   pnmSkipCommentLines();
+ *
+ *      These are here by popular demand, with the help of Mattias
+ *      Kregert (mattias@kregert.se), who provided the first implementation.
+ *
+ *      The pnm formats are exceedingly simple, because they have
+ *      no compression and no colormaps.  They support images that
+ *      are 1 bpp; 2, 4, 8 and 16 bpp grayscale; and rgb.
+ *
+ *      The original pnm formats ("ASCII") are included for completeness,
+ *      but their use is deprecated for all but tiny iconic images.
+ *      They are extremely wasteful of memory; for example, the P1 binary
+ *      ASCII format is 16 times as big as the packed uncompressed
+ *      format, because 2 characters are used to represent every bit
+ *      (pixel) in the image.  Reading is slow because we check for extra
+ *      white space and EOL at every sample value.
+ *
+ *      The packed pnm formats ("raw") give file sizes similar to
+ *      bmp files, which are uncompressed packed.  However, bmp
+ *      are more flexible, because they can support colormaps.
+ *
+ *      We don't differentiate between the different types ("pbm",
+ *      "pgm", "ppm") at the interface level, because this is really a
+ *      "distinction without a difference."  You read a file, you get
+ *      the appropriate Pix.  You write a file from a Pix, you get the
+ *      appropriate type of file.  If there is a colormap on the Pix,
+ *      and the Pix is more than 1 bpp, you get either an 8 bpp pgm
+ *      or a 24 bpp RGB pnm, depending on whether the colormap colors
+ *      are gray or rgb, respectively.
+ *
+ *      This follows the general policy that the I/O routines don't
+ *      make decisions about the content of the image -- you do that
+ *      with image processing before you write it out to file.
+ *      The I/O routines just try to make the closest connection
+ *      possible between the file and the Pix in memory.
+ *
+ *      On systems like Windows without fmemopen() and open_memstream(),
+ *      we write data to a temp file and read it back for operations
+ *      between pix and compressed-data, such as pixReadMemPnm() and
+ *      pixWriteMemPnm().
+ *
+ *      The P7 format is new. It introduced a header with multiple
+ *      lines containing distinct tags for the various fields.
+ *      See: http://netpbm.sourceforge.net/doc/pam.html
+ *
+ *        WIDTH <int>         ; mandatory, exactly once
+ *        HEIGHT <int>        ; mandatory, exactly once
+ *        DEPTH <int>         ; mandatory, exactly once,
+ *                            ; its meaning is equivalent to spp
+ *        MAXVAL <int>        ; mandatory, one of 1, 3, 15, 255 or 65535
+ *        TUPLTYPE <string>   ; optional; BLACKANDWHITE, GRAYSCALE, RGB
+ *                            ; and optional suffix _ALPHA, e.g. RGB_ALPHA
+ *        ENDHDR              ; mandatory, last header line
+ *
+ *      Reading BLACKANDWHITE_ALPHA and GRAYSCALE_ALPHA, which have a DEPTH
+ *      value of 2, is supported. The original image is converted to a Pix
+ *      with 32-bpp and alpha channel (spp == 4).
+ *
+ *      Writing P7 format is currently selected for 32-bpp with alpha
+ *      channel, i.e. for Pix which have spp == 4, using pixWriteStreamPam().
+ *
+ *      Jürgen Buchmüller provided the implementation for the P7 (pam) format.
+ *
+ *      Giulio Lunati made an elegant reimplementation of the static helper
+ *      functions using fscanf() instead of fseek(), so that it works with
+ *      pnm data from stdin.
+ * </pre>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config_auto.h>
+#endif  /* HAVE_CONFIG_H */
+
+#include <string.h>
+#include <ctype.h>
+#include "allheaders.h"
+
+/* --------------------------------------------*/
+#if  USE_PNMIO   /* defined in environ.h */
+/* --------------------------------------------*/
+
+static l_int32 pnmReadNextAsciiValue(FILE  *fp, l_int32 *pval);
+static l_int32 pnmReadNextNumber(FILE *fp, l_int32 *pval);
+static l_int32 pnmReadNextString(FILE *fp, char *buff, l_int32 size);
+static l_int32 pnmSkipCommentLines(FILE  *fp);
+
+    /* a sanity check on the size read from file */
+static const l_int32  MAX_PNM_WIDTH = 100000;
+static const l_int32  MAX_PNM_HEIGHT = 100000;
+
+
+/*--------------------------------------------------------------------*
+ *                          Stream interface                          *
+ *--------------------------------------------------------------------*/
+/*!
+ * \brief   pixReadStreamPnm()
+ *
+ * \param[in]    fp   file stream opened for read
+ * \return  pix, or NULL on error
+ */
+PIX *
+pixReadStreamPnm(FILE  *fp)
+{
+l_uint8    val8, rval8, gval8, bval8, aval8, mask8;
+l_uint16   val16, rval16, gval16, bval16, aval16;
+l_int32    w, h, d, bps, spp, bpl, wpl, i, j, type;
+l_int32    val, rval, gval, bval;
+l_uint32   rgbval;
+l_uint32  *line, *data;
+PIX       *pix;
+
+    if (!fp)
+        return (PIX *)ERROR_PTR("fp not defined", __func__, NULL);
+
+    if (freadHeaderPnm(fp, &w, &h, &d, &type, &bps, &spp))
+        return (PIX *)ERROR_PTR("header read failed", __func__, NULL);
+    if (bps < 1 || bps > 16)
+        return (PIX *)ERROR_PTR("invalid bps", __func__, NULL);
+    if (spp < 1 || spp > 4)
+        return (PIX *)ERROR_PTR("invalid spp", __func__, NULL);
+    if ((pix = pixCreate(w, h, d)) == NULL)
+        return (PIX *)ERROR_PTR("pix not made", __func__, NULL);
+    pixSetInputFormat(pix, IFF_PNM);
+    data = pixGetData(pix);
+    wpl = pixGetWpl(pix);
+
+        /* If type == 6 and bps == 16, we use the code in type 7
+         * to read 6 bytes/pixel from the input file. */
+    if (type == 6 && bps == 16)
+        type = 7;
+
+    switch (type) {
+    case 1:
+    case 2:
+        /* Old "ASCII" binary or gray format */
+        for (i = 0; i < h; i++) {
+            for (j = 0; j < w; j++) {
+                if (pnmReadNextAsciiValue(fp, &val)) {
+                    pixDestroy(&pix);
+                    return (PIX *)ERROR_PTR("read abend", __func__, NULL);
+                }
+                pixSetPixel(pix, j, i, val);
+            }
+        }
+        break;
+
+    case 3:
+        /* Old "ASCII" rgb format */
+        for (i = 0; i < h; i++) {
+            for (j = 0; j < w; j++) {
+                if (pnmReadNextAsciiValue(fp, &rval)) {
+                    pixDestroy(&pix);
+                    return (PIX *)ERROR_PTR("read abend", __func__, NULL);
+                }
+                if (pnmReadNextAsciiValue(fp, &gval)) {
+                    pixDestroy(&pix);
+                    return (PIX *)ERROR_PTR("read abend", __func__, NULL);
+                }
+                if (pnmReadNextAsciiValue(fp, &bval)) {
+                    pixDestroy(&pix);
+                    return (PIX *)ERROR_PTR("read abend", __func__, NULL);
+                }
+                composeRGBPixel(rval, gval, bval, &rgbval);
+                pixSetPixel(pix, j, i, rgbval);
+            }
+        }
+        break;
+
+    case 4:
+        /* "raw" format for 1 bpp */
+        bpl = (d * w + 7) / 8;
+        for (i = 0; i < h; i++) {
+            line = data + i * wpl;
+            for (j = 0; j < bpl; j++) {
+                if (fread(&val8, 1, 1, fp) != 1) {
+                    pixDestroy(&pix);
+                    return (PIX *)ERROR_PTR("read error in 4", __func__, NULL);
+                }
+                SET_DATA_BYTE(line, j, val8);
+            }
+        }
+        break;
+
+    case 5:
+        /* "raw" format for grayscale */
+        for (i = 0; i < h; i++) {
+            line = data + i * wpl;
+            if (d != 16) {
+                for (j = 0; j < w; j++) {
+                    if (fread(&val8, 1, 1, fp) != 1) {
+                        pixDestroy(&pix);
+                        return (PIX *)ERROR_PTR("error in 5", __func__, NULL);
+                    }
+                    if (d == 2)
+                        SET_DATA_DIBIT(line, j, val8);
+                    else if (d == 4)
+                        SET_DATA_QBIT(line, j, val8);
+                    else  /* d == 8 */
+                        SET_DATA_BYTE(line, j, val8);
+                }
+            } else {  /* d == 16 */
+                for (j = 0; j < w; j++) {
+                    if (fread(&val16, 2, 1, fp) != 1) {
+                        pixDestroy(&pix);
+                        return (PIX *)ERROR_PTR("16 bpp error", __func__, NULL);
+                    }
+                    SET_DATA_TWO_BYTES(line, j, val16);
+                }
+            }
+        }
+        break;
+
+    case 6:
+        /* "raw" format, type == 6; 8 bps, rgb */
+        for (i = 0; i < h; i++) {
+            line = data + i * wpl;
+            for (j = 0; j < wpl; j++) {
+                if (fread(&rval8, 1, 1, fp) != 1) {
+                    pixDestroy(&pix);
+                    return (PIX *)ERROR_PTR("read error type 6",
+                                            __func__, NULL);
+                }
+                if (fread(&gval8, 1, 1, fp) != 1) {
+                    pixDestroy(&pix);
+                    return (PIX *)ERROR_PTR("read error type 6",
+                                            __func__, NULL);
+                }
+                if (fread(&bval8, 1, 1, fp) != 1) {
+                    pixDestroy(&pix);
+                    return (PIX *)ERROR_PTR("read error type 6",
+                                            __func__, NULL);
+                }
+                composeRGBPixel(rval8, gval8, bval8, &rgbval);
+                line[j] = rgbval;
+            }
+        }
+        break;
+
+    case 7:
+        /* "arbitrary" format; type == 7; */
+        if (bps != 16) {
+            mask8 = (1 << bps) - 1;
+            switch (spp) {
+            case 1: /* 1, 2, 4, 8 bpp grayscale */
+                for (i = 0; i < h; i++) {
+                    for (j = 0; j < w; j++) {
+                        if (fread(&val8, 1, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        val8 = val8 & mask8;
+                        if (bps == 1) val8 ^= 1;  /* white-is-1 photometry */
+                        pixSetPixel(pix, j, i, val8);
+                    }
+                }
+                break;
+
+            case 2: /* 1, 2, 4, 8 bpp grayscale + alpha */
+                for (i = 0; i < h; i++) {
+                    for (j = 0; j < w; j++) {
+                        if (fread(&val8, 1, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&aval8, 1, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        val8 = val8 & mask8;
+                        aval8 = aval8 & mask8;
+                        composeRGBAPixel(val8, val8, val8, aval8, &rgbval);
+                        pixSetPixel(pix, j, i, rgbval);
+                    }
+                }
+                pixSetSpp(pix, 4);
+                break;
+
+            case 3: /* rgb */
+                for (i = 0; i < h; i++) {
+                    line = data + i * wpl;
+                    for (j = 0; j < wpl; j++) {
+                        if (fread(&rval8, 1, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&gval8, 1, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&bval8, 1, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        rval8 = rval8 & mask8;
+                        gval8 = gval8 & mask8;
+                        bval8 = bval8 & mask8;
+                        composeRGBPixel(rval8, gval8, bval8, &rgbval);
+                        line[j] = rgbval;
+                    }
+                }
+                break;
+
+            case 4: /* rgba */
+                for (i = 0; i < h; i++) {
+                    line = data + i * wpl;
+                    for (j = 0; j < wpl; j++) {
+                        if (fread(&rval8, 1, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&gval8, 1, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&bval8, 1, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&aval8, 1, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        rval8 = rval8 & mask8;
+                        gval8 = gval8 & mask8;
+                        bval8 = bval8 & mask8;
+                        aval8 = aval8 & mask8;
+                        composeRGBAPixel(rval8, gval8, bval8, aval8, &rgbval);
+                        line[j] = rgbval;
+                    }
+                }
+                pixSetSpp(pix, 4);
+                break;
+            }
+        } else {  /* bps == 16 */
+                /* I have only seen one example that is type 6, 16 bps.
+                 * It was 3 spp (rgb), and the 8 bps of real data was stored
+                 * in the second byte.  In the following, I make the wild
+                 * assumption that for all 16 bpp pnm/pam files, we can
+                 * take the second byte. */
+            switch (spp) {
+            case 1: /* 16 bps grayscale */
+                for (i = 0; i < h; i++) {
+                    for (j = 0; j < w; j++) {
+                        if (fread(&val16, 2, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        val8 = val16 & 0xff;
+                        pixSetPixel(pix, j, i, val8);
+                    }
+                }
+                break;
+
+            case 2: /* 16 bps grayscale + alpha */
+                for (i = 0; i < h; i++) {
+                    for (j = 0; j < w; j++) {
+                        if (fread(&val16, 2, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&aval16, 2, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        val8 = val16 & 0xff;
+                        aval8 = aval16 & 0xff;
+                        composeRGBAPixel(val8, val8, val8, aval8, &rgbval);
+                        pixSetPixel(pix, j, i, rgbval);
+                    }
+                }
+                pixSetSpp(pix, 4);
+                break;
+
+            case 3: /* 16bps rgb */
+                for (i = 0; i < h; i++) {
+                    line = data + i * wpl;
+                    for (j = 0; j < wpl; j++) {
+                        if (fread(&rval16, 2, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&gval16, 2, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&bval16, 2, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        rval8 = rval16 & 0xff;
+                        gval8 = gval16 & 0xff;
+                        bval8 = bval16 & 0xff;
+                        composeRGBPixel(rval8, gval8, bval8, &rgbval);
+                        line[j] = rgbval;
+                    }
+                }
+                break;
+
+            case 4: /* 16bps rgba */
+                for (i = 0; i < h; i++) {
+                    line = data + i * wpl;
+                    for (j = 0; j < wpl; j++) {
+                        if (fread(&rval16, 2, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&gval16, 2, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&bval16, 2, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        if (fread(&aval16, 2, 1, fp) != 1) {
+                            pixDestroy(&pix);
+                            return (PIX *)ERROR_PTR("read error type 7",
+                                                    __func__, NULL);
+                        }
+                        rval8 = rval16 & 0xff;
+                        gval8 = gval16 & 0xff;
+                        bval8 = bval16 & 0xff;
+                        aval8 = aval16 & 0xff;
+                        composeRGBAPixel(rval8, gval8, bval8, aval8, &rgbval);
+                        line[j] = rgbval;
+                    }
+                }
+                pixSetSpp(pix, 4);
+                break;
+            }
+        }
+        break;
+    }
+    return pix;
+}
+
+
+/*!
+ * \brief   readHeaderPnm()
+ *
+ * \param[in]    filename
+ * \param[out]   pw       [optional]
+ * \param[out]   ph       [optional]
+ * \param[out]   pd       [optional]
+ * \param[out]   ptype    [optional] pnm type
+ * \param[out]   pbps     [optional] bits/sample
+ * \param[out]   pspp     [optional] samples/pixel
+ * \return  0 if OK, 1 on error
+ */
+l_ok
+readHeaderPnm(const char *filename,
+              l_int32    *pw,
+              l_int32    *ph,
+              l_int32    *pd,
+              l_int32    *ptype,
+              l_int32    *pbps,
+              l_int32    *pspp)
+{
+l_int32  ret;
+FILE    *fp;
+
+    if (pw) *pw = 0;
+    if (ph) *ph = 0;
+    if (pd) *pd = 0;
+    if (ptype) *ptype = 0;
+    if (pbps) *pbps = 0;
+    if (pspp) *pspp = 0;
+    if (!filename)
+        return ERROR_INT("filename not defined", __func__, 1);
+
+    if ((fp = fopenReadStream(filename)) == NULL)
+        return ERROR_INT_1("image file not found", filename, __func__, 1);
+    ret = freadHeaderPnm(fp, pw, ph, pd, ptype, pbps, pspp);
+    fclose(fp);
+    return ret;
+}
+
+
+/*!
+ * \brief   freadHeaderPnm()
+ *
+ * \param[in]    fp     file stream opened for read
+ * \param[out]   pw     [optional]
+ * \param[out]   ph     [optional]
+ * \param[out]   pd     [optional]
+ * \param[out]   ptype  [optional] pnm type
+ * \param[out]   pbps   [optional]  bits/sample
+ * \param[out]   pspp   [optional]  samples/pixel
+ * \return  0 if OK, 1 on error
+ */
+l_ok
+freadHeaderPnm(FILE     *fp,
+               l_int32  *pw,
+               l_int32  *ph,
+               l_int32  *pd,
+               l_int32  *ptype,
+               l_int32  *pbps,
+               l_int32  *pspp)
+{
+char     tag[16], tupltype[32];
+l_int32  i, w, h, d, bps, spp, type;
+l_int32  maxval;
+l_int32  ch;
+
+    if (pw) *pw = 0;
+    if (ph) *ph = 0;
+    if (pd) *pd = 0;
+    if (ptype) *ptype = 0;
+    if (pbps) *pbps = 0;
+    if (pspp) *pspp = 0;
+    if (!fp)
+        return ERROR_INT("fp not defined", __func__, 1);
+
+    if (fscanf(fp, "P%d\n", &type) != 1)
+        return ERROR_INT("invalid read for type", __func__, 1);
+    if (type < 1 || type > 7)
+        return ERROR_INT("invalid pnm file", __func__, 1);
+
+    if (pnmSkipCommentLines(fp))
+        return ERROR_INT("no data in file", __func__, 1);
+
+    if (type == 7) {
+        w = h = d = bps = spp = maxval = 0;
+        for (i = 0; i < 10; i++) {   /* limit to 10 lines of this header */
+            if (pnmReadNextString(fp, tag, sizeof(tag)))
+                return ERROR_INT("found no next tag", __func__, 1);
+            if (!strcmp(tag, "WIDTH")) {
+                if (pnmReadNextNumber(fp, &w))
+                    return ERROR_INT("failed reading width", __func__, 1);
+                continue;
+            }
+            if (!strcmp(tag, "HEIGHT")) {
+                if (pnmReadNextNumber(fp, &h))
+                    return ERROR_INT("failed reading height", __func__, 1);
+                continue;
+            }
+            if (!strcmp(tag, "DEPTH")) {
+                if (pnmReadNextNumber(fp, &spp))
+                    return ERROR_INT("failed reading depth", __func__, 1);
+                continue;
+            }
+            if (!strcmp(tag, "MAXVAL")) {
+                if (pnmReadNextNumber(fp, &maxval))
+                    return ERROR_INT("failed reading maxval", __func__, 1);
+                continue;
+            }
+            if (!strcmp(tag, "TUPLTYPE")) {
+                if (pnmReadNextString(fp, tupltype, sizeof(tupltype)))
+                    return ERROR_INT("failed reading tuple type", __func__, 1);
+                continue;
+            }
+            if (!strcmp(tag, "ENDHDR")) {
+                if ('\n' != (ch = fgetc(fp)))
+                    return ERROR_INT("missing LF after ENDHDR", __func__, 1);
+                break;
+            }
+        }
+        if (w <= 0 || h <= 0 || w > MAX_PNM_WIDTH || h > MAX_PNM_HEIGHT) {
+            L_INFO("invalid size: w = %d, h = %d\n", __func__, w, h);
+            return 1;
+        }
+        if (maxval == 1) {
+            d = bps = 1;
+        } else if (maxval == 3) {
+            d = bps = 2;
+        } else if (maxval == 15) {
+            d = bps = 4;
+        } else if (maxval == 255) {
+            d = bps = 8;
+        } else if (maxval == 0xffff) {
+            d = bps = 16;
+        } else {
+            L_INFO("invalid maxval = %d\n", __func__, maxval);
+            return 1;
+        }
+        switch (spp) {
+        case 1:
+            /* d and bps are already set */
+            break;
+        case 2:
+        case 3:
+        case 4:
+            /* create a 32 bpp Pix */
+            d = 32;
+            break;
+        default:
+            L_INFO("invalid depth = %d\n", __func__, spp);
+            return 1;
+        }
+    } else {
+
+        if (fscanf(fp, "%d %d\n", &w, &h) != 2)
+            return ERROR_INT("invalid read for w,h", __func__, 1);
+        if (w <= 0 || h <= 0 || w > MAX_PNM_WIDTH || h > MAX_PNM_HEIGHT) {
+            L_INFO("invalid size: w = %d, h = %d\n", __func__, w, h);
+            return 1;
+        }
+
+       /* Get depth of pix.  For types 2 and 5, we use the maxval.
+        * Important implementation note:
+        *   - You can't use fscanf(), which throws away whitespace,
+        *     and will discard binary data if it starts with whitespace(s).
+        *   - You can't use fgets(), which stops at newlines, but this
+        *     dumb format doesn't require a newline after the maxval
+        *     number -- it just requires one whitespace character.
+        *   - Which leaves repeated calls to fgetc, including swallowing
+        *     the single whitespace character. */
+        if (type == 1 || type == 4) {
+            d = 1;
+            spp = 1;
+            bps = 1;
+        } else if (type == 2 || type == 5) {
+            if (pnmReadNextNumber(fp, &maxval))
+                return ERROR_INT("invalid read for maxval (2,5)", __func__, 1);
+            if (maxval == 3) {
+                d = 2;
+            } else if (maxval == 15) {
+                d = 4;
+            } else if (maxval == 255) {
+                d = 8;
+            } else if (maxval == 0xffff) {
+                d = 16;
+            } else {
+                lept_stderr("maxval = %d\n", maxval);
+                return ERROR_INT("invalid maxval", __func__, 1);
+            }
+            bps = d;
+            spp = 1;
+        } else {  /* type == 3 || type == 6; this is rgb  */
+            if (pnmReadNextNumber(fp, &maxval))
+                return ERROR_INT("invalid read for maxval (3,6)", __func__, 1);
+            if (maxval != 255 && maxval != 0xffff) {
+                L_ERROR("unexpected maxval = %d\n", __func__, maxval);
+                return 1;
+            }
+            bps = (maxval == 255) ? 8 : 16;
+            d = 32;
+            spp = 3;
+        }
+    }
+    if (pw) *pw = w;
+    if (ph) *ph = h;
+    if (pd) *pd = d;
+    if (ptype) *ptype = type;
+    if (pbps) *pbps = bps;
+    if (pspp) *pspp = spp;
+    return 0;
+}
+
+
+/*!
+ * \brief   pixWriteStreamPnm()
+ *
+ * \param[in]   fp    file stream opened for write
+ * \param[in]   pix
+ * \return  0 if OK; 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This writes "raw" packed format only:
+ *          1 bpp --> pbm (P4)
+ *          2, 4, 8, 16 bpp, no colormap or grayscale colormap --> pgm (P5)
+ *          2, 4, 8 bpp with color-valued colormap, or rgb --> rgb ppm (P6)
+ *      (2) 24 bpp rgb are not supported in leptonica, but this will
+ *          write them out as a packed array of bytes (3 to a pixel).
+ * </pre>
+ */
+l_ok
+pixWriteStreamPnm(FILE  *fp,
+                  PIX   *pix)
+{
+l_uint8    val8;
+l_uint8    pel[4];
+l_uint16   val16;
+l_int32    h, w, d, ds, i, j, wpls, bpl, filebpl, writeerror, maxval;
+l_uint32  *pword, *datas, *lines;
+PIX       *pixs;
+
+    if (!fp)
+        return ERROR_INT("fp not defined", __func__, 1);
+    if (!pix)
+        return ERROR_INT("pix not defined", __func__, 1);
+
+    pixGetDimensions(pix, &w, &h, &d);
+    if (d != 1 && d != 2 && d != 4 && d != 8 && d != 16 && d != 24 && d != 32)
+        return ERROR_INT("d not in {1,2,4,8,16,24,32}", __func__, 1);
+    if (d == 32 && pixGetSpp(pix) == 4)
+        return pixWriteStreamPam(fp, pix);
+
+        /* If a colormap exists, remove and convert to grayscale or rgb */
+    if (pixGetColormap(pix) != NULL)
+        pixs = pixRemoveColormap(pix, REMOVE_CMAP_BASED_ON_SRC);
+    else
+        pixs = pixClone(pix);
+    ds =  pixGetDepth(pixs);
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+
+    writeerror = 0;
+
+    if (ds == 1) {  /* binary */
+        fprintf(fp, "P4\n# Raw PBM file written by leptonica "
+                    "(www.leptonica.com)\n%d %d\n", w, h);
+
+        bpl = (w + 7) / 8;
+        for (i = 0; i < h; i++) {
+            lines = datas + i * wpls;
+            for (j = 0; j < bpl; j++) {
+                val8 = GET_DATA_BYTE(lines, j);
+                fwrite(&val8, 1, 1, fp);
+            }
+        }
+    } else if (ds == 2 || ds == 4 || ds == 8 || ds == 16) {  /* grayscale */
+        maxval = (1 << ds) - 1;
+        fprintf(fp, "P5\n# Raw PGM file written by leptonica "
+                    "(www.leptonica.com)\n%d %d\n%d\n", w, h, maxval);
+
+        if (ds != 16) {
+            for (i = 0; i < h; i++) {
+                lines = datas + i * wpls;
+                for (j = 0; j < w; j++) {
+                    if (ds == 2)
+                        val8 = GET_DATA_DIBIT(lines, j);
+                    else if (ds == 4)
+                        val8 = GET_DATA_QBIT(lines, j);
+                    else  /* ds == 8 */
+                        val8 = GET_DATA_BYTE(lines, j);
+                    fwrite(&val8, 1, 1, fp);
+                }
+            }
+        } else {  /* ds == 16 */
+            for (i = 0; i < h; i++) {
+                lines = datas + i * wpls;
+                for (j = 0; j < w; j++) {
+                    val16 = GET_DATA_TWO_BYTES(lines, j);
+                    fwrite(&val16, 2, 1, fp);
+                }
+            }
+        }
+    } else {  /* rgb color */
+        fprintf(fp, "P6\n# Raw PPM file written by leptonica "
+                    "(www.leptonica.com)\n%d %d\n255\n", w, h);
+
+        if (d == 24) {   /* packed, 3 bytes to a pixel */
+            filebpl = 3 * w;
+            for (i = 0; i < h; i++) {  /* write out each raster line */
+                lines = datas + i * wpls;
+                if (fwrite(lines, 1, filebpl, fp) != filebpl)
+                    writeerror = 1;
+            }
+        } else {  /* 32 bpp rgb */
+            for (i = 0; i < h; i++) {
+                lines = datas + i * wpls;
+                for (j = 0; j < wpls; j++) {
+                    pword = lines + j;
+                    pel[0] = GET_DATA_BYTE(pword, COLOR_RED);
+                    pel[1] = GET_DATA_BYTE(pword, COLOR_GREEN);
+                    pel[2] = GET_DATA_BYTE(pword, COLOR_BLUE);
+                    if (fwrite(pel, 1, 3, fp) != 3)
+                        writeerror = 1;
+                }
+            }
+        }
+    }
+
+    pixDestroy(&pixs);
+    if (writeerror)
+        return ERROR_INT("image write fail", __func__, 1);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixWriteStreamAsciiPnm()
+ *
+ * \param[in]   fp    file stream opened for write
+ * \param[in]   pix
+ * \return  0 if OK; 1 on error
+ *
+ *  Writes "ASCII" format only:
+ *      1 bpp --> pbm P1
+ *      2, 4, 8, 16 bpp, no colormap or grayscale colormap --> pgm P2
+ *      2, 4, 8 bpp with color-valued colormap, or rgb --> rgb ppm P3
+ */
+l_ok
+pixWriteStreamAsciiPnm(FILE  *fp,
+                       PIX   *pix)
+{
+char       buffer[256];
+l_uint8    cval[3];
+l_int32    h, w, d, ds, i, j, k, maxval, count;
+l_uint32   val;
+PIX       *pixs;
+
+    if (!fp)
+        return ERROR_INT("fp not defined", __func__, 1);
+    if (!pix)
+        return ERROR_INT("pix not defined", __func__, 1);
+
+    pixGetDimensions(pix, &w, &h, &d);
+    if (d != 1 && d != 2 && d != 4 && d != 8 && d != 16 && d != 32)
+        return ERROR_INT("d not in {1,2,4,8,16,32}", __func__, 1);
+
+        /* If a colormap exists, remove and convert to grayscale or rgb */
+    if (pixGetColormap(pix) != NULL)
+        pixs = pixRemoveColormap(pix, REMOVE_CMAP_BASED_ON_SRC);
+    else
+        pixs = pixClone(pix);
+    ds =  pixGetDepth(pixs);
+
+    if (ds == 1) {  /* binary */
+        fprintf(fp, "P1\n# Ascii PBM file written by leptonica "
+                    "(www.leptonica.com)\n%d %d\n", w, h);
+
+        count = 0;
+        for (i = 0; i < h; i++) {
+            for (j = 0; j < w; j++) {
+                pixGetPixel(pixs, j, i, &val);
+                if (val == 0)
+                    fputc('0', fp);
+                else  /* val == 1 */
+                    fputc('1', fp);
+                fputc(' ', fp);
+                count += 2;
+                if (count >= 70) {
+                    fputc('\n', fp);
+                    count = 0;
+                }
+            }
+        }
+    } else if (ds == 2 || ds == 4 || ds == 8 || ds == 16) {  /* grayscale */
+        maxval = (1 << ds) - 1;
+        fprintf(fp, "P2\n# Ascii PGM file written by leptonica "
+                    "(www.leptonica.com)\n%d %d\n%d\n", w, h, maxval);
+
+        count = 0;
+        for (i = 0; i < h; i++) {
+            for (j = 0; j < w; j++) {
+                pixGetPixel(pixs, j, i, &val);
+                if (ds == 2) {
+                    snprintf(buffer, sizeof(buffer), "%1d ", val);
+                    fwrite(buffer, 1, 2, fp);
+                    count += 2;
+                } else if (ds == 4) {
+                    snprintf(buffer, sizeof(buffer), "%2d ", val);
+                    fwrite(buffer, 1, 3, fp);
+                    count += 3;
+                } else if (ds == 8) {
+                    snprintf(buffer, sizeof(buffer), "%3d ", val);
+                    fwrite(buffer, 1, 4, fp);
+                    count += 4;
+                } else {  /* ds == 16 */
+                    snprintf(buffer, sizeof(buffer), "%5d ", val);
+                    fwrite(buffer, 1, 6, fp);
+                    count += 6;
+                }
+                if (count >= 60) {
+                    fputc('\n', fp);
+                    count = 0;
+                }
+            }
+        }
+    } else {  /* rgb color */
+        fprintf(fp, "P3\n# Ascii PPM file written by leptonica "
+                    "(www.leptonica.com)\n%d %d\n255\n", w, h);
+        count = 0;
+        for (i = 0; i < h; i++) {
+            for (j = 0; j < w; j++) {
+                pixGetPixel(pixs, j, i, &val);
+                cval[0] = GET_DATA_BYTE(&val, COLOR_RED);
+                cval[1] = GET_DATA_BYTE(&val, COLOR_GREEN);
+                cval[2] = GET_DATA_BYTE(&val, COLOR_BLUE);
+                for (k = 0; k < 3; k++) {
+                    snprintf(buffer, sizeof(buffer), "%3d ", cval[k]);
+                    fwrite(buffer, 1, 4, fp);
+                    count += 4;
+                    if (count >= 60) {
+                        fputc('\n', fp);
+                        count = 0;
+                    }
+                }
+            }
+        }
+    }
+
+    pixDestroy(&pixs);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixWriteStreamPam()
+ *
+ * \param[in]   fp    file stream opened for write
+ * \param[in]   pix
+ * \return  0 if OK; 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This writes arbitrary PAM (P7) packed format.
+ *      (2) 24 bpp rgb are not supported in leptonica, but this will
+ *          write them out as a packed array of bytes (3 to a pixel).
+ * </pre>
+ */
+l_ok
+pixWriteStreamPam(FILE  *fp,
+                  PIX   *pix)
+{
+l_uint8    val8;
+l_uint8    pel[8];
+l_uint16   val16;
+l_int32    h, w, d, ds, i, j;
+l_int32    wpls, spps, filebpl, writeerror, maxval;
+l_uint32  *pword, *datas, *lines;
+PIX       *pixs;
+
+    if (!fp)
+        return ERROR_INT("fp not defined", __func__, 1);
+    if (!pix)
+        return ERROR_INT("pix not defined", __func__, 1);
+
+    pixGetDimensions(pix, &w, &h, &d);
+    if (d != 1 && d != 2 && d != 4 && d != 8 && d != 16 && d != 24 && d != 32)
+        return ERROR_INT("d not in {1,2,4,8,16,24,32}", __func__, 1);
+
+        /* If a colormap exists, remove and convert to grayscale or rgb */
+    if (pixGetColormap(pix) != NULL)
+        pixs = pixRemoveColormap(pix, REMOVE_CMAP_BASED_ON_SRC);
+    else
+        pixs = pixClone(pix);
+    ds = pixGetDepth(pixs);
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+    spps = pixGetSpp(pixs);
+    if (ds < 24)
+        maxval = (1 << ds) - 1;
+    else
+        maxval = 255;
+
+    writeerror = 0;
+    fprintf(fp, "P7\n# Arbitrary PAM file written by leptonica "
+                "(www.leptonica.com)\n");
+    fprintf(fp, "WIDTH %d\n", w);
+    fprintf(fp, "HEIGHT %d\n", h);
+    fprintf(fp, "DEPTH %d\n", spps);
+    fprintf(fp, "MAXVAL %d\n", maxval);
+    if (spps == 1 && ds == 1)
+        fprintf(fp, "TUPLTYPE BLACKANDWHITE\n");
+    else if (spps == 1)
+        fprintf(fp, "TUPLTYPE GRAYSCALE\n");
+    else if (spps == 3)
+        fprintf(fp, "TUPLTYPE RGB\n");
+    else if (spps == 4)
+        fprintf(fp, "TUPLTYPE RGB_ALPHA\n");
+    fprintf(fp, "ENDHDR\n");
+
+    switch (d) {
+    case 1:
+        for (i = 0; i < h; i++) {
+            lines = datas + i * wpls;
+            for (j = 0; j < w; j++) {
+                val8 = GET_DATA_BIT(lines, j);
+                val8 ^= 1;  /* pam apparently uses white-is-1 photometry */
+                if (fwrite(&val8, 1, 1, fp) != 1)
+                    writeerror = 1;
+            }
+        }
+        break;
+
+    case 2:
+        for (i = 0; i < h; i++) {
+            lines = datas + i * wpls;
+            for (j = 0; j < w; j++) {
+                val8 = GET_DATA_DIBIT(lines, j);
+                if (fwrite(&val8, 1, 1, fp) != 1)
+                    writeerror = 1;
+            }
+        }
+        break;
+
+    case 4:
+        for (i = 0; i < h; i++) {
+            lines = datas + i * wpls;
+            for (j = 0; j < w; j++) {
+                val8 = GET_DATA_QBIT(lines, j);
+                if (fwrite(&val8, 1, 1, fp) != 1)
+                    writeerror = 1;
+            }
+        }
+        break;
+
+    case 8:
+        for (i = 0; i < h; i++) {
+            lines = datas + i * wpls;
+            for (j = 0; j < w; j++) {
+                val8 = GET_DATA_BYTE(lines, j);
+                if (fwrite(&val8, 1, 1, fp) != 1)
+                    writeerror = 1;
+            }
+        }
+        break;
+
+    case 16:
+        for (i = 0; i < h; i++) {
+            lines = datas + i * wpls;
+            for (j = 0; j < w; j++) {
+                val16 = GET_DATA_TWO_BYTES(lines, j);
+                if (fwrite(&val16, 2, 1, fp) != 1)
+                    writeerror = 1;
+            }
+        }
+        break;
+
+    case 24:
+        filebpl = 3 * w;
+        for (i = 0; i < h; i++) {
+            lines = datas + i * wpls;
+            if (fwrite(lines, 1, filebpl, fp) != filebpl)
+                writeerror = 1;
+        }
+        break;
+
+    case 32:
+        switch (spps) {
+        case 3:
+            for (i = 0; i < h; i++) {
+                lines = datas + i * wpls;
+                for (j = 0; j < wpls; j++) {
+                    pword = lines + j;
+                    pel[0] = GET_DATA_BYTE(pword, COLOR_RED);
+                    pel[1] = GET_DATA_BYTE(pword, COLOR_GREEN);
+                    pel[2] = GET_DATA_BYTE(pword, COLOR_BLUE);
+                    if (fwrite(pel, 1, 3, fp) != 3)
+                        writeerror = 1;
+                }
+            }
+            break;
+        case 4:
+            for (i = 0; i < h; i++) {
+                lines = datas + i * wpls;
+                for (j = 0; j < wpls; j++) {
+                    pword = lines + j;
+                    pel[0] = GET_DATA_BYTE(pword, COLOR_RED);
+                    pel[1] = GET_DATA_BYTE(pword, COLOR_GREEN);
+                    pel[2] = GET_DATA_BYTE(pword, COLOR_BLUE);
+                    pel[3] = GET_DATA_BYTE(pword, L_ALPHA_CHANNEL);
+                    if (fwrite(pel, 1, 4, fp) != 4)
+                        writeerror = 1;
+                }
+            }
+            break;
+        }
+        break;
+    }
+
+    pixDestroy(&pixs);
+    if (writeerror)
+        return ERROR_INT("image write fail", __func__, 1);
+    return 0;
+}
+
+
+/*---------------------------------------------------------------------*
+ *                         Read/write to memory                        *
+ *---------------------------------------------------------------------*/
+
+/*!
+ * \brief   pixReadMemPnm()
+ *
+ * \param[in]   data   const; pnm-encoded
+ * \param[in]   size   of data
+ * \return  pix, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) The %size byte of %data must be a null character.
+ * </pre>
+ */
+PIX *
+pixReadMemPnm(const l_uint8  *data,
+              size_t          size)
+{
+FILE  *fp;
+PIX   *pix;
+
+    if (!data)
+        return (PIX *)ERROR_PTR("data not defined", __func__, NULL);
+    if ((fp = fopenReadFromMemory(data, size)) == NULL)
+        return (PIX *)ERROR_PTR("stream not opened", __func__, NULL);
+    pix = pixReadStreamPnm(fp);
+    fclose(fp);
+    if (!pix) L_ERROR("pix not read\n", __func__);
+    return pix;
+}
+
+
+/*!
+ * \brief   readHeaderMemPnm()
+ *
+ * \param[in]    data    const; pnm-encoded
+ * \param[in]    size    of data
+ * \param[out]   pw      [optional]
+ * \param[out]   ph      [optional]
+ * \param[out]   pd      [optional]
+ * \param[out]   ptype   [optional] pnm type
+ * \param[out]   pbps    [optional] bits/sample
+ * \param[out]   pspp    [optional] samples/pixel
+ * \return  0 if OK, 1 on error
+ */
+l_ok
+readHeaderMemPnm(const l_uint8  *data,
+                 size_t          size,
+                 l_int32        *pw,
+                 l_int32        *ph,
+                 l_int32        *pd,
+                 l_int32        *ptype,
+                 l_int32        *pbps,
+                 l_int32        *pspp)
+{
+l_int32  ret;
+FILE    *fp;
+
+    if (!data)
+        return ERROR_INT("data not defined", __func__, 1);
+
+    if ((fp = fopenReadFromMemory(data, size)) == NULL)
+        return ERROR_INT("stream not opened", __func__, 1);
+    ret = freadHeaderPnm(fp, pw, ph, pd, ptype, pbps, pspp);
+    fclose(fp);
+    if (ret)
+        return ERROR_INT("header data read failed", __func__, 1);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixWriteMemPnm()
+ *
+ * \param[out]   pdata   data of PNM image
+ * \param[out]   psize   size of returned data
+ * \param[in]    pix
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) See pixWriteStreamPnm() for usage.  This version writes to
+ *          memory instead of to a file stream.
+ * </pre>
+ */
+l_ok
+pixWriteMemPnm(l_uint8  **pdata,
+               size_t    *psize,
+               PIX       *pix)
+{
+l_int32  ret;
+FILE    *fp;
+
+    if (pdata) *pdata = NULL;
+    if (psize) *psize = 0;
+    if (!pdata)
+        return ERROR_INT("&data not defined", __func__, 1 );
+    if (!psize)
+        return ERROR_INT("&size not defined", __func__, 1 );
+    if (!pix)
+        return ERROR_INT("&pix not defined", __func__, 1 );
+
+#if HAVE_FMEMOPEN
+    if ((fp = open_memstream((char **)pdata, psize)) == NULL)
+        return ERROR_INT("stream not opened", __func__, 1);
+    ret = pixWriteStreamPnm(fp, pix);
+    fputc('\0', fp);
+    fclose(fp);
+    if (*psize > 0) *psize = *psize - 1;
+#else
+    L_INFO("no fmemopen API --> work-around: write to temp file\n", __func__);
+  #ifdef _WIN32
+    if ((fp = fopenWriteWinTempfile()) == NULL)
+        return ERROR_INT("tmpfile stream not opened", __func__, 1);
+  #else
+    if ((fp = tmpfile()) == NULL)
+        return ERROR_INT("tmpfile stream not opened", __func__, 1);
+  #endif  /* _WIN32 */
+    ret = pixWriteStreamPnm(fp, pix);
+    rewind(fp);
+    *pdata = l_binaryReadStream(fp, psize);
+    fclose(fp);
+#endif  /* HAVE_FMEMOPEN */
+    return ret;
+}
+
+
+/*!
+ * \brief   pixWriteMemPam()
+ *
+ * \param[out]   pdata   data of PAM image
+ * \param[out]   psize   size of returned data
+ * \param[in]    pix
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) See pixWriteStreamPnm() for usage.  This version writes to
+ *          memory instead of to a file stream.
+ * </pre>
+ */
+l_ok
+pixWriteMemPam(l_uint8  **pdata,
+               size_t    *psize,
+               PIX       *pix)
+{
+l_int32  ret;
+FILE    *fp;
+
+    if (pdata) *pdata = NULL;
+    if (psize) *psize = 0;
+    if (!pdata)
+        return ERROR_INT("&data not defined", __func__, 1 );
+    if (!psize)
+        return ERROR_INT("&size not defined", __func__, 1 );
+    if (!pix)
+        return ERROR_INT("&pix not defined", __func__, 1 );
+
+#if HAVE_FMEMOPEN
+    if ((fp = open_memstream((char **)pdata, psize)) == NULL)
+        return ERROR_INT("stream not opened", __func__, 1);
+    ret = pixWriteStreamPam(fp, pix);
+    fputc('\0', fp);
+    fclose(fp);
+    if (*psize > 0) *psize = *psize - 1;
+#else
+    L_INFO("no fmemopen API --> work-around: write to temp file\n", __func__);
+  #ifdef _WIN32
+    if ((fp = fopenWriteWinTempfile()) == NULL)
+        return ERROR_INT("tmpfile stream not opened", __func__, 1);
+  #else
+    if ((fp = tmpfile()) == NULL)
+        return ERROR_INT("tmpfile stream not opened", __func__, 1);
+  #endif  /* _WIN32 */
+    ret = pixWriteStreamPam(fp, pix);
+    rewind(fp);
+    *pdata = l_binaryReadStream(fp, psize);
+    fclose(fp);
+#endif  /* HAVE_FMEMOPEN */
+    return ret;
+}
+
+
+/*--------------------------------------------------------------------*
+ *                          Static helpers                            *
+ *--------------------------------------------------------------------*/
+/*!
+ * \brief   pnmReadNextAsciiValue()
+ *
+ *      Return: 0 if OK, 1 on error or EOF.
+ *
+ *  Notes:
+ *      (1) This reads the next sample value in ASCII from the file.
+ */
+static l_int32
+pnmReadNextAsciiValue(FILE     *fp,
+                      l_int32  *pval)
+{
+l_int32  ignore;
+
+    if (!pval)
+        return ERROR_INT("&val not defined", __func__, 1);
+    *pval = 0;
+    if (!fp)
+        return ERROR_INT("stream not open", __func__, 1);
+
+    if (EOF == fscanf(fp, " "))
+        return 1;
+    if (1 != fscanf(fp, "%d", pval))
+        return 1;
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pnmReadNextNumber()
+ *
+ * \param[in]    fp    file stream
+ * \param[out]   pval  value as an integer
+ * \return  0 if OK, 1 on error or EOF.
+ *
+ * <pre>
+ * Notes:
+ *      (1) This reads the next set of numeric chars, returning
+ *          the value and swallowing initial whitespaces and ONE
+ *          trailing whitespace character.  This is needed to read
+ *          the maxval in the header, which precedes the binary data.
+ * </pre>
+ */
+static l_int32
+pnmReadNextNumber(FILE     *fp,
+                  l_int32  *pval)
+{
+char      buf[8];
+l_int32   i, c, foundws;
+
+    if (!pval)
+        return ERROR_INT("&val not defined", __func__, 1);
+    *pval = 0;
+    if (!fp)
+        return ERROR_INT("stream not open", __func__, 1);
+
+        /* Swallow whitespace */
+    if (fscanf(fp, " ") == EOF)
+        return ERROR_INT("end of file reached", __func__, 1);
+
+        /* The ASCII characters for the number are followed by exactly
+         * one whitespace character. */
+    foundws = FALSE;
+    for (i = 0; i < 8; i++)
+        buf[i] = '\0';
+    for (i = 0; i < 8; i++) {
+        if ((c = fgetc(fp)) == EOF)
+            return ERROR_INT("end of file reached", __func__, 1);
+        if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
+            foundws = TRUE;
+            buf[i] = '\n';
+            break;
+        }
+        if (!isdigit(c))
+            return ERROR_INT("char read is not a digit", __func__, 1);
+        buf[i] = c;
+    }
+    if (!foundws)
+        return ERROR_INT("no whitespace found", __func__, 1);
+    if (sscanf(buf, "%d", pval) != 1)
+        return ERROR_INT("invalid read", __func__, 1);
+    return 0;
+}
+
+/*!
+ * \brief   pnmReadNextString()
+ *
+ * \param[in]    fp    file stream
+ * \param[out]   buff  pointer to the string buffer
+ * \param[in]    size  max. number of characters in buffer
+ * \return  0 if OK, 1 on error or EOF.
+ *
+ * <pre>
+ * Notes:
+ *      (1) This reads the next set of alphanumeric chars, returning the string.
+ *          This is needed to read header lines, which precede the P7
+ *          format binary data.
+ * </pre>
+ */
+static l_int32
+pnmReadNextString(FILE    *fp,
+                  char    *buff,
+                  l_int32  size)
+{
+char  fmtString[7];  /* must contain "%9999s" [*] */
+
+    if (!buff)
+        return ERROR_INT("buff not defined", __func__, 1);
+    *buff = '\0';
+    if (size > 10000)  /* size - 1 has > 4 digits [*]  */
+        return ERROR_INT("size is too big", __func__, 1);
+    if (size <= 0)
+        return ERROR_INT("size is too small", __func__, 1);
+    if (!fp)
+        return ERROR_INT("stream not open", __func__, 1);
+
+        /* Skip whitespace */
+    if (fscanf(fp, " ") == EOF)
+        return 1;
+
+        /* Comment lines are allowed to appear anywhere in the header lines */
+    if (pnmSkipCommentLines(fp))
+        return ERROR_INT("end of file reached", __func__, 1);
+
+    snprintf(fmtString, 7, "%%%ds", size - 1);
+    if (fscanf(fp, fmtString, buff) == EOF)
+        return 1;
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pnmSkipCommentLines()
+ *
+ *      Return: 0 if OK, 1 on error or EOF
+ *
+ *  Notes:
+ *      (1) Comment lines begin with '#'
+ *      (2) Usage: caller should check return value for EOF
+ *      (3) The previous implementation used fseek(fp, -1L, SEEK_CUR)
+ *          to back up one character, which doesn't work with stdin.
+ */
+static l_int32
+pnmSkipCommentLines(FILE  *fp)
+{
+l_int32  i;
+char     c;
+
+    if (!fp)
+        return ERROR_INT("stream not open", __func__, 1);
+    while ((i = fscanf(fp, "#%c", &c))) {
+        if (i == EOF) return 1;
+        while (c != '\n') {
+            if (fscanf(fp, "%c", &c) == EOF)
+                return 1;
+        }
+    }
+    return 0;
+}
+
+
+/* --------------------------------------------*/
+#endif  /* USE_PNMIO */
+/* --------------------------------------------*/