about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2022-06-24 04:36:14 +0000
committergiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2022-06-24 04:36:14 +0000
commit5bcb369386d617631627a4fb086ff6ab80ffa157 (patch)
treeb2f6cae56a948d3897c1ed6452f870ffc8c832de
parent5991a720c260e171c145d035014609f18cb2eaf3 (diff)
downloadnetpbm-mirror-5bcb369386d617631627a4fb086ff6ab80ffa157.tar.gz
netpbm-mirror-5bcb369386d617631627a4fb086ff6ab80ffa157.tar.xz
netpbm-mirror-5bcb369386d617631627a4fb086ff6ab80ffa157.zip
Promote Development release to Advanced, version 10.99.00
git-svn-id: http://svn.code.sf.net/p/netpbm/code/advanced@4358 9d0c8265-081b-0410-96cb-a4ca84ce46f8
-rw-r--r--converter/other/Makefile7
-rw-r--r--converter/other/exif.c6
-rw-r--r--converter/other/pamtoqoi.c439
-rw-r--r--converter/other/qoi.h101
-rw-r--r--converter/other/qoitopam.c323
-rw-r--r--converter/other/winicontopam.c2
-rw-r--r--doc/HISTORY13
-rw-r--r--editor/Makefile9
-rw-r--r--editor/pamrestack.c472
-rw-r--r--editor/pamshuffle.c155
-rw-r--r--generator/pamseq.c319
-rw-r--r--lib/pm.h10
-rw-r--r--lib/pmfileio.c66
-rw-r--r--lib/util/nstring.h4
-rw-r--r--lib/util/shhopt.h35
-rw-r--r--test/Test-Order2
-rw-r--r--test/all-in-place.ok4
-rwxr-xr-xtest/all-in-place.test4
-rwxr-xr-xtest/channel-stack-roundtrip.test4
-rwxr-xr-xtest/gif-roundtrip.test64
-rwxr-xr-xtest/pamflip-pbm-roundtrip.test2
-rwxr-xr-xtest/pamhue.test4
-rwxr-xr-xtest/pamrecolor.test2
-rw-r--r--test/pamrestack.ok68
-rwxr-xr-xtest/pamrestack.test91
-rwxr-xr-xtest/pamscale-filters1.test2
-rwxr-xr-xtest/pamscale-filters2.test2
-rwxr-xr-xtest/pamscale-filters3.test2
-rwxr-xr-xtest/pamshuffle.ok19
-rwxr-xr-xtest/pamshuffle.test67
-rwxr-xr-xtest/pcx-roundtrip.test3
-rwxr-xr-xtest/pdb-roundtrip.test2
-rwxr-xr-xtest/pnmquantall.test30
-rwxr-xr-xtest/rgb3-roundtrip.test18
-rw-r--r--test/stdin-pam1.ok2
-rwxr-xr-xtest/stdin-pam1.test6
-rwxr-xr-xtest/stdin-pam3.test2
-rwxr-xr-xtest/stdin-pbm2.test4
-rwxr-xr-xtest/winicon-roundtrip.test1
-rwxr-xr-xtest/winicon-roundtrip2.test1
-rw-r--r--version.mk4
41 files changed, 2210 insertions, 161 deletions
diff --git a/converter/other/Makefile b/converter/other/Makefile
index 5dfc27ec..3b3b6aa0 100644
--- a/converter/other/Makefile
+++ b/converter/other/Makefile
@@ -134,14 +134,15 @@ PORTBINARIES =  avstopam bmptopnm fitstopnm \
 		gemtopnm giftopnm hdifftopam infotopam \
 		pamtoavs pamtodjvurle pamtofits pamtogif \
 		pamtohdiff pamtohtmltbl pamtompfont pamtooctaveimg \
-		pamtopam pamtopdbimg pamtopfm pamtopnm pamtosrf pamtouil \
+		pamtopam pamtopdbimg pamtopfm pamtopnm \
+                pamtoqoi pamtosrf pamtouil \
 		pamtowinicon pamtoxvmini \
 		pbmtopgm pdbimgtopam pfmtopam \
 	        pgmtopbm pgmtoppm ppmtopgm pnmtoddif \
 		pnmtopclxl pnmtorast \
-		pnmtosgi pnmtosir pamtotga pnmtoxwd \
+		pnmtosgi pnmtosir pamtotga pnmtoxwd qoitopam \
 		rasttopnm rlatopam sgitopnm sirtopnm srftopam sunicontopnm \
-		winicontopam xwdtopnm yuy2topam zeisstopnm
+		winicontopam xwdtopnm yuy2topam zeisstopnm \
 
 ifneq ($(DONT_HAVE_PROCESS_MGMT),Y)
   PORTBINARIES += pstopnm pnmtops
diff --git a/converter/other/exif.c b/converter/other/exif.c
index 1bfe4b2b..87236dc7 100644
--- a/converter/other/exif.c
+++ b/converter/other/exif.c
@@ -533,7 +533,7 @@ processDirEntry(const unsigned char *  const dirEntry,
         for (end = byteCount; end > 0 && value[end] == ' '; --end);
 
         /* Skip "ASCII" if it is there */
-        if (end >= 5 && MEMEQ(value, "ASCII", 5))
+        if (end >= 5 && memeq(value, "ASCII", 5))
             cursor = 5;
         else
             cursor = 0;
@@ -860,12 +860,12 @@ exif_parse(const unsigned char * const exifData,
     if (wantTagTrace)
         fprintf(stderr, "Exif header %d bytes long\n",length);
 
-    if (MEMEQ(exifData + 0, "II" , 2)) {
+    if (memeq(exifData + 0, "II" , 2)) {
         if (wantTagTrace) 
             fprintf(stderr, "Exif header in Intel order\n");
         byteOrder = NORMAL;
     } else {
-        if (MEMEQ(exifData + 0, "MM", 2)) {
+        if (memeq(exifData + 0, "MM", 2)) {
             if (wantTagTrace) 
                 fprintf(stderr, "Exif header in Motorola order\n");
             byteOrder = MOTOROLA;
diff --git a/converter/other/pamtoqoi.c b/converter/other/pamtoqoi.c
new file mode 100644
index 00000000..638efa3a
--- /dev/null
+++ b/converter/other/pamtoqoi.c
@@ -0,0 +1,439 @@
+/*
+  pamtoqoi -  Converts PAM to a QOI - The "Quite OK Image" format file
+
+  This program is part of Netpbm.
+
+  ---------------------------------------------------------------------
+
+  QOI - The "Quite OK Image" format for fast, lossless image compression
+
+  Encoder by Dominic Szablewski - https://phoboslab.org
+
+  -- LICENSE: The MIT License(MIT)
+
+  Copyright(c) 2021 Dominic Szablewski
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files(the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions :
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+
+  For more information on the format visit: https//qoiformat.org/ .
+
+  Modifications for Netpbm read routines by Akira F. Urushibata.
+
+*/
+
+#include <string.h>
+#include <assert.h>
+#include "pam.h"
+#include "nstring.h"
+#include "mallocvar.h"
+#include "shhopt.h"
+
+#include "qoi.h"
+
+
+
+struct CmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFileName;  /* '-' if stdin */
+};
+
+
+
+static void
+parseCommandLine(int                  argc,
+                 const char **        argv,
+                 struct CmdlineInfo * cmdlineP ) {
+/*----------------------------------------------------------------------------
+   Parse program command line described in Unix standard form by argc
+   and argv.  Return the information in the options as *cmdlineP.
+
+   If command line is internally inconsistent (invalid options, etc.),
+   issue error message to stderr and abort program.
+
+   Note that the strings we return are stored in the storage that
+   was passed to us as the argv array.  We also trash *argv.
+-----------------------------------------------------------------------------*/
+    optEntry * option_def;
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+
+    MALLOCARRAY(option_def, 100);
+
+    OPTENTINIT;
+
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
+
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFileName = argv[1];
+    else
+        pm_error("Program takes at most one argument: input file name.  "
+            "you specified %d", argc-1);
+}
+
+
+
+static void
+encodeQoiHeader(qoi_Desc const qoiDesc) {
+
+    assert (QOI_MAGIC_SIZE + 4 + 4 + 1 + 1 == QOI_HEADER_SIZE);
+
+    fwrite (qoi_magic, QOI_MAGIC_SIZE, 1, stdout);
+    pm_writebiglongu(stdout, qoiDesc.width);
+    pm_writebiglongu(stdout, qoiDesc.height);
+    putchar(qoiDesc.channelCt);
+    putchar(qoiDesc.colorspace);
+
+}
+
+enum Tupletype {BW, BWAlpha, GRAY, GRAYAlpha, RGB, RGBAlpha,
+                GRAY255, GRAY255Alpha, RGB255, RGB255Alpha};
+
+
+
+static void
+createSampleMap(sample    const oldMaxval,
+                sample ** const sampleMapP) {
+
+    unsigned int i;
+    sample * sampleMap;
+    sample   const newMaxval = 255;
+
+    MALLOCARRAY_NOFAIL(sampleMap, oldMaxval+1);
+
+    for (i = 0; i <= oldMaxval; ++i)
+        sampleMap[i] = ROUNDDIV(i * newMaxval, oldMaxval);
+
+    *sampleMapP = sampleMap;
+}
+
+
+
+static enum Tupletype
+tupleTypeFmPam(const char * const pamTupleType,
+               sample       const maxval) {
+
+    enum Tupletype retval;
+
+    if (streq(pamTupleType, PAM_PBM_TUPLETYPE)) {
+        if (maxval !=1)
+            pm_error("Invalid maxval (%lu) for tuple type '%s'.",
+                     maxval, pamTupleType);
+        else
+            retval =  BW;
+    } else if (streq(pamTupleType, PAM_PBM_ALPHA_TUPLETYPE)) {
+      if (maxval !=1)
+          pm_error("Invalid maxval (%lu) for tuple type '%s'.",
+                    maxval, pamTupleType);
+      else
+          retval =  BWAlpha;
+    } else if (maxval == 255) {
+        if (streq(pamTupleType, PAM_PPM_TUPLETYPE))
+            retval =  RGB255;
+        else if(streq(pamTupleType, PAM_PPM_ALPHA_TUPLETYPE))
+            retval =  RGB255Alpha;
+        else if(streq(pamTupleType, PAM_PGM_TUPLETYPE))
+            retval =  GRAY255;
+        else if(streq(pamTupleType, PAM_PGM_ALPHA_TUPLETYPE))
+            retval =  GRAY255Alpha;
+        else
+            pm_error("Don't know how to convert tuple type '%s'.",
+                     pamTupleType);
+    } else {
+        if (streq(pamTupleType, PAM_PPM_TUPLETYPE))
+            retval =  RGB;
+        else if(streq(pamTupleType, PAM_PPM_ALPHA_TUPLETYPE))
+            retval =  RGBAlpha;
+        else if(streq(pamTupleType, PAM_PGM_TUPLETYPE))
+            retval =  GRAY;
+        else if(streq(pamTupleType, PAM_PGM_ALPHA_TUPLETYPE))
+            retval =  GRAYAlpha;
+        else
+            pm_error("Don't know how to convert tuple type '%s'.",
+                     pamTupleType);
+    }
+
+    return retval;
+}
+
+
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wreturn-type"
+
+static unsigned int
+channelCtFmTupleType(enum Tupletype const tupleType) {
+
+    switch (tupleType) {
+        case RGB:
+          return 3;
+        case RGB255:
+          return 3;
+        case RGBAlpha:
+          return 4;
+        case RGB255Alpha:
+          return 4;
+        case BW:
+        case GRAY:
+          return 3;
+        case GRAY255:
+          return 3;
+        case BWAlpha:
+        case GRAYAlpha:
+          return 4;
+        case GRAY255Alpha:
+          return 4;
+    }
+}
+#pragma GCC diagnostic pop
+
+
+
+static qoi_Rgba
+pxFmTuple(tuple          const tuple0,
+          const sample * const sampleMap,
+          enum Tupletype const tupleType) {
+/*----------------------------------------------------------------------------
+   Convert PAM tuple to qoi rgba pixel struct
+-----------------------------------------------------------------------------*/
+    qoi_Rgba px;
+
+    switch (tupleType) {
+    case RGB:
+        px.rgba.r = sampleMap[tuple0[PAM_RED_PLANE]];
+        px.rgba.g = sampleMap[tuple0[PAM_GRN_PLANE]];
+        px.rgba.b = sampleMap[tuple0[PAM_BLU_PLANE]];
+        px.rgba.a = 255;
+        break;
+    case RGB255:
+        px.rgba.r = tuple0[PAM_RED_PLANE];
+        px.rgba.g = tuple0[PAM_GRN_PLANE];
+        px.rgba.b = tuple0[PAM_BLU_PLANE];
+        px.rgba.a = 255;
+        break;
+    case RGBAlpha:
+        px.rgba.r = sampleMap[tuple0[PAM_RED_PLANE]];
+        px.rgba.g = sampleMap[tuple0[PAM_GRN_PLANE]];
+        px.rgba.b = sampleMap[tuple0[PAM_BLU_PLANE]];
+        px.rgba.a = sampleMap[tuple0[PAM_TRN_PLANE]];
+        break;
+    case RGB255Alpha:
+        px.rgba.r = tuple0[PAM_RED_PLANE];
+        px.rgba.g = tuple0[PAM_GRN_PLANE];
+        px.rgba.b = tuple0[PAM_BLU_PLANE];
+        px.rgba.a = tuple0[PAM_TRN_PLANE];
+        break;
+    case BW:
+    case GRAY : {
+        unsigned char const qoiSample = sampleMap[tuple0[0]];
+        px.rgba.r = qoiSample;
+        px.rgba.g = qoiSample;
+        px.rgba.b = qoiSample;
+        px.rgba.a = 255;
+    } break;
+    case GRAY255: {
+        unsigned char const qoiSample = tuple0[0];
+        px.rgba.r = qoiSample;
+        px.rgba.g = qoiSample;
+        px.rgba.b = qoiSample;
+        px.rgba.a = 255;
+    } break;
+    case BWAlpha:
+    case GRAYAlpha: {
+        unsigned char const qoiSample = sampleMap[tuple0[0]];
+        px.rgba.r = qoiSample;
+        px.rgba.g = qoiSample;
+        px.rgba.b = qoiSample;
+        px.rgba.a = sampleMap[tuple0[PAM_GRAY_TRN_PLANE]];
+    } break;
+    case GRAY255Alpha: {
+        unsigned char const qoiSample = tuple0[0];
+        px.rgba.r = qoiSample;
+        px.rgba.g = qoiSample;
+        px.rgba.b = qoiSample;
+        px.rgba.a = tuple0[PAM_GRAY_TRN_PLANE];
+    } break;
+    }
+
+    return px;
+}
+
+
+
+static void
+encodeNewPixel(qoi_Rgba const px,
+               qoi_Rgba const pxPrev) {
+
+    if (px.rgba.a == pxPrev.rgba.a) {
+        signed char const vr = px.rgba.r - pxPrev.rgba.r;
+        signed char const vg = px.rgba.g - pxPrev.rgba.g;
+        signed char const vb = px.rgba.b - pxPrev.rgba.b;
+
+        signed char const vgR = vr - vg;
+        signed char const vgB = vb - vg;
+
+        if (
+            vr > -3 && vr < 2 &&
+            vg > -3 && vg < 2 &&
+            vb > -3 && vb < 2
+            ) {
+            putchar(QOI_OP_DIFF |
+                    (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2));
+        } else if (
+            vgR >  -9 && vgR <  8 &&
+            vg  > -33 && vg  < 32 &&
+            vgB >  -9 && vgB <  8
+            ) {
+            putchar(QOI_OP_LUMA    | (vg   + 32));
+            putchar((vgR + 8) << 4 | (vgB +  8));
+        } else {
+            putchar(QOI_OP_RGB);
+            putchar(px.rgba.r);
+            putchar(px.rgba.g);
+            putchar(px.rgba.b);
+        }
+    } else {
+        putchar(QOI_OP_RGBA);
+        putchar(px.rgba.r);
+        putchar(px.rgba.g);
+        putchar(px.rgba.b);
+        putchar(px.rgba.a);
+    }
+}
+
+
+
+static void
+qoiEncode(FILE           * const ifP,
+          struct pam     * const inpamP) {
+
+    tuple * tuplerow;
+    unsigned int row;
+
+    qoi_Rgba index[QOI_INDEX_SIZE];
+    qoi_Rgba pxPrev;
+    unsigned int run;
+    sample * sampleMap;
+    qoi_Desc qoiDesc;
+
+    enum Tupletype const tupleType =
+      tupleTypeFmPam(inpamP->tuple_type, inpamP->maxval);
+
+    if (inpamP->height > QOI_PIXELS_MAX / inpamP->width)
+        pm_error("Too many pixels for QOI: %u x %u (max is %u)",
+                 inpamP->height, inpamP->width, QOI_PIXELS_MAX);
+
+    qoiDesc.colorspace = QOI_SRGB;
+    qoiDesc.width      = inpamP->width;
+    qoiDesc.height     = inpamP->height;
+    qoiDesc.channelCt  = channelCtFmTupleType(tupleType);
+
+    encodeQoiHeader(qoiDesc);
+
+    tuplerow = pnm_allocpamrow(inpamP);
+
+    if (inpamP->maxval != 255)
+        createSampleMap(inpamP->maxval, &sampleMap);
+
+    qoi_clearQoiIndex(index);
+
+    pxPrev.rgba.r = 0;
+    pxPrev.rgba.g = 0;
+    pxPrev.rgba.b = 0;
+    pxPrev.rgba.a = 255;
+
+    /* Read and convert rows. */
+    for (row = 0, run = 0; row < inpamP->height; ++row) {
+        unsigned int col;
+
+        pnm_readpamrow(inpamP, tuplerow);
+
+        for (col = 0; col < inpamP->width; ++col) {
+            qoi_Rgba const px = pxFmTuple(tuplerow[col], sampleMap, tupleType);
+
+            if (px.v == pxPrev.v) {
+                ++run;
+                if (run == 62) {
+                    putchar(QOI_OP_RUN | (run - 1));
+                    run = 0;
+                }
+            } else {
+                unsigned int const indexPos = qoi_colorHash(px);
+
+                if (run > 0) {
+                    putchar(QOI_OP_RUN | (run - 1));
+                    run = 0;
+                }
+
+                if (index[indexPos].v == px.v) {
+                    putchar(QOI_OP_INDEX | indexPos);
+
+                } else {
+                    index[indexPos] = px;
+                    encodeNewPixel(px, pxPrev);
+                }
+            }
+            pxPrev = px;
+        }
+    }
+
+    if (run > 0)
+        putchar(QOI_OP_RUN | (run - 1));
+
+    fwrite(qoi_padding, sizeof(qoi_padding), 1, stdout);
+
+    if (inpamP->maxval != 255)
+        free(sampleMap);
+    pnm_freepamrow(tuplerow);
+}
+
+
+
+int
+main(int argc, const char **argv) {
+
+    struct CmdlineInfo cmdline;
+    struct pam inpam;
+    FILE * ifP;
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    qoiEncode(ifP, &inpam);
+
+    return 0;
+}
+
+
diff --git a/converter/other/qoi.h b/converter/other/qoi.h
new file mode 100644
index 00000000..52ee95e2
--- /dev/null
+++ b/converter/other/qoi.h
@@ -0,0 +1,101 @@
+#ifndef QOI_H_INCLUDED
+#define QOI_H_INCLUDED
+/*
+
+QOI - The "Quite OK Image" format for fast, lossless image compression
+
+Dominic Szablewski - https://phoboslab.org
+
+
+-- LICENSE: The MIT License(MIT)
+
+Copyright(c) 2021 Dominic Szablewski
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files(the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions :
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+*/
+
+
+typedef enum {
+    QOI_SRGB = 0,
+    QOI_LINEAR = 1
+} qoi_Colorspace;
+
+
+typedef struct {
+    unsigned int   width;
+    unsigned int   height;
+    unsigned int   channelCt;
+    qoi_Colorspace colorspace;
+} qoi_Desc;
+
+
+
+#define QOI_OP_INDEX  0x00 /* 00xxxxxx */
+#define QOI_OP_DIFF   0x40 /* 01xxxxxx */
+#define QOI_OP_LUMA   0x80 /* 10xxxxxx */
+#define QOI_OP_RUN    0xc0 /* 11xxxxxx */
+#define QOI_OP_RGB    0xfe /* 11111110 */
+#define QOI_OP_RGBA   0xff /* 11111111 */
+
+#define QOI_MASK_2    0xc0 /* 11000000 */
+
+#define QOI_HEADER_SIZE 14
+
+/* 2GB is the max file size that this implementation can safely handle. We
+   guard against anything larger than that, assuming the worst case with 5
+   bytes per pixel, rounded down to a nice clean value. 400 million pixels
+   ought to be enough for anybody.
+ */
+#define QOI_PIXELS_MAX 400000000
+
+static unsigned int const qoi_pixels_max = (unsigned int) QOI_PIXELS_MAX;
+
+#define QOI_MAXVAL 255
+
+#define QOI_INDEX_SIZE 64
+
+
+typedef union {
+    struct { unsigned char r, g, b, a; } rgba;
+    unsigned int v;
+} qoi_Rgba;
+
+static __inline__ unsigned int
+qoi_colorHash(qoi_Rgba const x) {
+
+    return
+        (x.rgba.r*3 + x.rgba.g*5 + x.rgba.b*7 + x.rgba.a*11) % QOI_INDEX_SIZE;
+}
+
+static __inline__ void
+qoi_clearQoiIndex(qoi_Rgba * index) {
+
+    memset(index, 0, QOI_INDEX_SIZE * sizeof(qoi_Rgba));
+
+}
+
+#define QOI_MAGIC_SIZE 4
+
+static char const qoi_magic[QOI_MAGIC_SIZE + 1] = {'q','o','i','f','\0'};
+
+#define QOI_PADDING_SIZE 8
+
+static unsigned char const qoi_padding[QOI_PADDING_SIZE] = {0,0,0,0,0,0,0,1};
+
+
+#endif
diff --git a/converter/other/qoitopam.c b/converter/other/qoitopam.c
new file mode 100644
index 00000000..af6817b7
--- /dev/null
+++ b/converter/other/qoitopam.c
@@ -0,0 +1,323 @@
+/*
+  qoitopam -  Converts from a QOI - The "Quite OK Image" format file to PAM
+
+  This program is part of Netpbm.
+
+  ---------------------------------------------------------------------
+
+
+  QOI - The "Quite OK Image" format for fast, lossless image compression
+
+  Decoder by Dominic Szablewski - https://phoboslab.org
+
+  -- LICENSE: The MIT License(MIT)
+
+  Copyright(c) 2021 Dominic Szablewski
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files(the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions :
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+
+  For more information on the format visit: https//qoiformat.org/ .
+
+  Modifications for Netpbm & PAM write routines by Akira F. Urushibata.
+*/
+
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+
+#include "pm.h"
+#include "pam.h"
+#include "mallocvar.h"
+#include "nstring.h"
+#include "shhopt.h"
+
+#include "qoi.h"
+
+
+
+struct CmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFileName;  /* '-' if stdin */
+};
+
+
+
+static void
+parseCommandLine(int                  argc,
+                 const char **        argv,
+                 struct CmdlineInfo * cmdlineP ) {
+/*----------------------------------------------------------------------------
+   Parse program command line described in Unix standard form by argc
+   and argv.  Return the information in the options as *cmdlineP.
+
+   If command line is internally inconsistent (invalid options, etc.),
+   issue error message to stderr and abort program.
+
+   Note that the strings we return are stored in the storage that
+   was passed to us as the argv array.  We also trash *argv.
+-----------------------------------------------------------------------------*/
+    optEntry * option_def;
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+
+    MALLOCARRAY(option_def, 100);
+
+    OPTENTINIT;
+
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
+
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFileName = argv[1];
+    else
+        pm_error("Program takes at most one argument: input file name.  "
+            "you specified %d", argc-1);
+}
+
+
+
+static void
+readAndValidateMagic(FILE * const ifP){
+
+    char magicBuff[QOI_MAGIC_SIZE];
+    size_t charsReadCt;
+
+    charsReadCt = fread(magicBuff, 1, QOI_MAGIC_SIZE, ifP);
+
+    if (charsReadCt == 0)
+        pm_error("Input file is empty.");
+    else if (charsReadCt < QOI_MAGIC_SIZE || !MEMSEQ(&magicBuff, &qoi_magic)) {
+        assert(QOI_MAGIC_SIZE == 4);
+        pm_error("Invalid QOI image: does not start with magic number "
+                 "'%c%c%c%c'",
+                 qoi_magic[0], qoi_magic[1], qoi_magic[2], qoi_magic[3]);
+    }
+}
+
+
+/* The following two functions are from lib/pmfileio.c */
+
+static void
+abortWithReadError(FILE * const ifP) {
+
+    if (feof(ifP))
+        pm_error("Unexpected end of input file");
+    else
+        pm_error("Error (not EOF) reading file.");
+}
+
+
+
+static unsigned char
+getcNofail(FILE * const ifP) {
+
+    int c;
+
+    c = getc(ifP);
+
+    if (c == EOF)
+        abortWithReadError(ifP);
+
+    return (unsigned char) c;
+}
+
+
+
+static void
+decodeQoiHeader(FILE *     const ifP,
+                qoi_Desc * const qoiDescP) {
+
+    unsigned long int width, height;
+
+    readAndValidateMagic(ifP);
+
+    pm_readbiglongu(ifP, &width);
+    if (width == 0)
+        pm_error("Invalid QOI image: width is zero");
+    else
+        qoiDescP->width = width;
+
+    pm_readbiglongu(ifP, &height);
+    if (height == 0)
+        pm_error("Invalid QOI image: height is zero");
+    else if (height > QOI_PIXELS_MAX / width)
+        pm_error ("Invalid QOI image: %u x %u is more than %u pixels",
+                  (unsigned int) width, (unsigned int) height, QOI_PIXELS_MAX);
+    else
+        qoiDescP->height = height;
+
+    qoiDescP->channelCt = getcNofail(ifP);
+    if (qoiDescP->channelCt != 3 && qoiDescP->channelCt != 4)
+        pm_error("Invalid QOI image: channel count is %u.  "
+                 "Only 3 and 4 are valid", qoiDescP->channelCt);
+
+    qoiDescP->colorspace = getcNofail(ifP);
+    if (qoiDescP->colorspace != QOI_SRGB && qoiDescP->colorspace != QOI_LINEAR)
+        pm_error("Invalid QOI image: colorspace code is %u.  "
+                 "Only %u (SRGB) and %u (LINEAR) are valid",
+                 qoiDescP->colorspace, QOI_SRGB, QOI_LINEAR);
+}
+
+
+
+static void
+qoiDecode(FILE *       const ifP,
+          qoi_Desc *   const qoiDescP,
+          struct pam * const outpamP) {
+
+    qoi_Rgba index[QOI_INDEX_SIZE];
+    unsigned int row;
+    qoi_Rgba px;
+    unsigned int run;
+    tuple * tuplerow;
+
+    assert(qoiDescP);
+    tuplerow = pnm_allocpamrow(outpamP);
+
+    qoi_clearQoiIndex(index);
+    px.rgba.r = px.rgba.g = px.rgba.b = 0;
+    px.rgba.a = 255;
+
+    for (row = 0, run = 0; row < outpamP->height; ++row) {
+        unsigned int col;
+
+        for (col = 0; col < outpamP->width; ++col) {
+            if (run > 0) {
+                 --run;
+            } else {
+                unsigned char const b1 = getcNofail(ifP);
+
+                if (b1 == QOI_OP_RGB) {
+                    px.rgba.r = getcNofail(ifP);
+                    px.rgba.g = getcNofail(ifP);
+                    px.rgba.b = getcNofail(ifP);
+                } else if (b1 == QOI_OP_RGBA) {
+                    px.rgba.r = getcNofail(ifP);
+                    px.rgba.g = getcNofail(ifP);
+                    px.rgba.b = getcNofail(ifP);
+                    px.rgba.a = getcNofail(ifP);
+                } else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX) {
+                  /* Official spec says 2 or more consecutive instances of
+                     QOI_OP_INDEX are not allowed, but we don't check */
+                    px = index[b1];
+                } else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF) {
+                    px.rgba.r += ((b1 >> 4) & 0x03) - 2;
+                    px.rgba.g += ((b1 >> 2) & 0x03) - 2;
+                    px.rgba.b += ( b1       & 0x03) - 2;
+                } else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA) {
+                    unsigned char const b2 = getcNofail(ifP);
+                    unsigned char const vg = (b1 & 0x3f) - 32;
+                    px.rgba.r += vg - 8 + ((b2 >> 4) & 0x0f);
+                    px.rgba.g += vg;
+                    px.rgba.b += vg - 8 +  (b2       & 0x0f);
+                } else if ((b1 & QOI_MASK_2) == QOI_OP_RUN) {
+                    run = (b1 & 0x3f);
+                }
+                /* register pixel in hash lookup array */
+                index[qoi_colorHash(px)] = px;
+            }
+            tuplerow[col][PAM_RED_PLANE] = px.rgba.r;
+            tuplerow[col][PAM_GRN_PLANE] = px.rgba.g;
+            tuplerow[col][PAM_BLU_PLANE] = px.rgba.b;
+            if (qoiDescP->channelCt == 4)
+                tuplerow[col][PAM_TRN_PLANE] = px.rgba.a;
+        }
+        pnm_writepamrow(outpamP, tuplerow);
+    }
+    if (run > 0)
+        pm_error("Invalid QOI image: %u (or more) extra pixels "
+                 "beyond end of image.", run);
+
+    pnm_freepamrow(tuplerow);
+}
+
+
+
+static void
+readAndValidatePadding(FILE * const ifP){
+
+    unsigned char padBuff[QOI_PADDING_SIZE];
+    size_t charsReadCt;
+
+    charsReadCt = fread(padBuff, 1, QOI_PADDING_SIZE, ifP);
+
+    if(charsReadCt < QOI_PADDING_SIZE) {
+        pm_error("Invalid QOI image.  Error reading final 8-byte padding.  "
+                 "Premature end of file.");
+    } else if (!MEMSEQ(&padBuff, &qoi_padding))
+        pm_error("Invalid QOI image.  Final 8-byte padding incorrect.");
+    else if (fgetc(ifP) != EOF)
+        pm_error("Invalid QOI image.  "
+                 "Extraneous bytes after final 8-byte padding.");
+}
+
+
+
+int
+main(int argc, const char **argv) {
+
+    struct CmdlineInfo cmdline;
+    qoi_Desc qoiDesc;
+    struct pam outpam;
+    FILE * ifP;
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    outpam.size        = sizeof(struct pam);
+    outpam.len         = PAM_STRUCT_SIZE(tuple_type);
+    outpam.maxval      = QOI_MAXVAL;
+    outpam.plainformat = 0;
+
+    decodeQoiHeader(ifP, &qoiDesc);
+
+    outpam.depth  = qoiDesc.channelCt == 3 ? 3 : 4;
+    outpam.width  = qoiDesc.width;
+    outpam.height = qoiDesc.height;
+    outpam.format = PAM_FORMAT;
+    outpam.file   = stdout;
+
+    if (qoiDesc.channelCt == 3)
+        strcpy(outpam.tuple_type, PAM_PPM_TUPLETYPE);
+    else
+        strcpy(outpam.tuple_type, PAM_PPM_ALPHA_TUPLETYPE);
+
+    pnm_writepaminit(&outpam);
+    qoiDecode(ifP, &qoiDesc, &outpam);
+
+    readAndValidatePadding(ifP);
+
+    return 0;
+}
+
+
diff --git a/converter/other/winicontopam.c b/converter/other/winicontopam.c
index f6f89e11..bb39bf60 100644
--- a/converter/other/winicontopam.c
+++ b/converter/other/winicontopam.c
@@ -1273,7 +1273,7 @@ convertImage(struct File *         const icoP,
 
     image = readImage(icoP, dirEntryP);
 
-    if (MEMEQ(image, pngSignature, sizeof (pngSignature)))
+    if (memeq(image, pngSignature, sizeof (pngSignature)))
         convertPng(image, ofP, dirEntryP);
     else
         convertBmp(image, ofP, dirEntryP, needHeaderDump, wantAndMaskPlane);
diff --git a/doc/HISTORY b/doc/HISTORY
index b962fdad..a457a207 100644
--- a/doc/HISTORY
+++ b/doc/HISTORY
@@ -4,15 +4,19 @@ Netpbm.
 CHANGE HISTORY 
 --------------
 
-22.04.24 BJH  Release 10.98.02
+22.06.24 BJH  Release 10.99.00
+
+              Add pamrestack.
+
+              Add pamshuffle.
+
+              Add pamtoqoi, qoitopam.
 
               palmtopnm: Fix failure with bogus claim of invalid input on
               architectures that do not use two's complement negative numbers.
               Always broken.  (Ability to convert PackBits input was new in
               Netpbm 10.27 (March 2005).
 
-22.04.10 BJH  Release 10.98.01
-
               pnmgamma -srgbtobt709, -bt709tosrgb: fix bug; incorrect output.
               Always broken (These options were new in Netpbm 10.32 (February
               2006)).  Thanks Alexander Shpilkin <ashpilkin@gmail.com>.
@@ -20,6 +24,9 @@ CHANGE HISTORY
               pamdice: Fix incorrect output file name with PAM input.  Always
               broken (pamdice was new in Netpbm 9.25 (March 2002).
 
+              libnetpbm: Stop bogus runtime error check failure in pmfileio.c
+              shifts.
+
 22.03.27 BJH  Release 10.98.00
 
               pamtopdbimg: Add -fixedtime.
diff --git a/editor/Makefile b/editor/Makefile
index 395deaf4..8798cf6e 100644
--- a/editor/Makefile
+++ b/editor/Makefile
@@ -17,13 +17,12 @@ SUBDIRS = pamflip specialty
 # build.
 
 PORTBINARIES = pamaddnoise pamaltsat pambackground pambrighten pamcomp pamcut \
-	       pamdice pamditherbw pamedge \
-	       pamenlarge \
+	       pamdice pamditherbw pamedge pamenlarge \
 	       pamfunc pamhomography pamhue pamlevels \
 	       pammasksharpen pammixmulti \
-	       pamperspective pamrecolor pamrubber \
-	       pamscale pamsistoaglyph pamstretch pamthreshold pamundice \
-	       pamwipeout \
+	       pamperspective pamrecolor pamrestack pamrubber \
+	       pamscale pamshuffle pamsistoaglyph pamstretch pamthreshold \
+	       pamundice pamwipeout \
 	       pbmclean pbmmask pbmpscale pbmreduce \
 	       pgmdeshadow pgmenhance \
 	       pgmmedian \
diff --git a/editor/pamrestack.c b/editor/pamrestack.c
new file mode 100644
index 00000000..46321774
--- /dev/null
+++ b/editor/pamrestack.c
@@ -0,0 +1,472 @@
+/*=============================================================================
+                               pamrestack
+===============================================================================
+  Part of the Netpbm package.
+
+  Rearrange pixels of a Netpbm image into different size rows.
+
+  E.g. if an image is 100 pixels wide and 50 pixels high, you can rearrange it
+  to 125 wide and 40 high.  In that case, 25 pixels from the 2nd row of the
+  input would be moved to the end of the 1st row of input, 50 pixels from the
+  3rd row would be moved to the 2nd row, etc.
+
+  If new width is less than the input image width, move the excess pixels
+  to the start (=left edge) of the next row.
+
+  If new width is larger, complete row by bringing pixels from the start
+  of the next row.
+
+  By Akira F. Urushibata
+
+  Contributed to the public domain by its author.
+=============================================================================*/
+
+#include <assert.h>
+#include <math.h>
+#include <limits.h>
+#include "pm_c_util.h"
+#include "mallocvar.h"
+#include "nstring.h"
+#include "pam.h"
+#include "shhopt.h"
+
+static unsigned int const maxSize = INT_MAX - 10;
+
+static void
+validateWidth(double       const width,
+              const char * const message) {
+/*----------------------------------------------------------------------------
+  Check width.  Ensure it is a value accepted by other Netpbm programs.
+-----------------------------------------------------------------------------*/
+    assert(maxSize < INT_MAX);
+
+    if (width > maxSize)
+        pm_error("%s %.0f is too large.", message, width);
+}
+
+
+
+static void
+validateHeight(double const height) {
+/*----------------------------------------------------------------------------
+  Fail if image height of 'height' is too great for the computations in
+  this program to work.
+-----------------------------------------------------------------------------*/
+    if (height > maxSize)
+        pm_error("Input image is large and -width value is small."
+                 "Calulated height %.0f is too large.", height);
+}
+
+
+
+enum TrimMode {TRIMMODE_NOP, TRIMMODE_FILL, TRIMMODE_CROP, TRIMMODE_ABORT};
+
+struct CmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char *  inputFileName;
+    unsigned int  width;
+    unsigned int  widthSpec;
+    enum TrimMode trim;
+    unsigned int  verbose;
+};
+
+static void
+parseCommandLine(int argc, const char ** const argv,
+                 struct CmdlineInfo * const cmdlineP) {
+/*----------------------------------------------------------------------------
+   Note that the file spec array we return is stored in the storage that
+   was passed to us as the argv array.
+-----------------------------------------------------------------------------*/
+    optEntry * option_def;
+        /* Instructions to OptParseOptions3 on how to parse our options. */
+    optStruct3 opt;
+
+    const char * trimOpt;
+    unsigned int trimSpec;
+
+    unsigned int option_def_index;
+
+    MALLOCARRAY(option_def, 100);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = false;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = true;  /* We have no parms that are negative numbers */
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "width",         OPT_UINT,    &cmdlineP->width,
+            &cmdlineP->widthSpec,     0);
+    OPTENT3(0, "trim",          OPT_STRING, &trimOpt,
+            &trimSpec,                0);
+    OPTENT3(0, "verbose",       OPT_FLAG,   NULL,
+            &cmdlineP->verbose,       0);
+
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+    /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    free(option_def);
+
+    if (cmdlineP->widthSpec) {
+        if (cmdlineP->width == 0)
+            pm_error("Width value must be positive.  You specified 0");
+        else
+            validateWidth((double) cmdlineP->width,
+                          "Specified -width value");
+    }
+
+    if (trimSpec) {
+        if (streq(trimOpt, "fill")) {
+            cmdlineP->trim = TRIMMODE_FILL;
+        } else if (streq(trimOpt, "crop")) {
+            cmdlineP->trim = TRIMMODE_CROP;
+        } else if (streq(trimOpt, "abort")) {
+            cmdlineP->trim = TRIMMODE_ABORT;
+        } else
+            /* NOP is not specified from the command line */
+            pm_error("Invalid value for -trim: '%s'", trimOpt);
+    } else
+        cmdlineP->trim = TRIMMODE_FILL;  /* default */
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else {
+        cmdlineP->inputFileName = argv[1];
+
+        if (argc-1 > 1)
+            pm_error("Too many arguments (%u). "
+                     "The only possible argument is the input file name.",
+                     argc-1);
+    }
+}
+
+
+
+static void
+adjustTrimMode(double          const inPixels,
+               double          const outWidth,
+               double          const outHeight,
+               bool            const verbose,
+               enum TrimMode   const originalMode,
+               enum TrimMode * const adjustedModeP) {
+/*----------------------------------------------------------------------------
+   Adjust trim mode, taking into account the number of pixels in the
+   input image and the width and height of the output image.
+
+   Check whether conditions are met for abort.
+   Set mode to NOP if all output rows will be full.
+-----------------------------------------------------------------------------*/
+    double const outPixels = outWidth * outHeight;
+
+    enum TrimMode adjustedMode;
+
+    if (inPixels == outPixels)
+        adjustedMode = TRIMMODE_NOP;
+    else {
+        if (originalMode == TRIMMODE_ABORT)
+            pm_error("Abort mode specified and input image has %.0f pixels "
+                     "which is %s specified width value %.0f",
+                     inPixels,
+                     inPixels < outWidth ? "less than" : "not a multiple of",
+                     outWidth);
+        else
+            adjustedMode = originalMode;
+    }
+
+    validateHeight(outHeight + (adjustedMode == TRIMMODE_FILL) ? 1 : 0);
+
+    switch (adjustedMode) {
+    case TRIMMODE_NOP:
+        if (verbose)
+            pm_message("Input image and output image have the same "
+                       "number of pixels.");
+        break;
+    case TRIMMODE_FILL:
+        if (verbose)
+            pm_message("Output image will have %.0f more pixels "
+                       "than input image.  Incomplete final row "
+                       "will be padded.", inPixels - outPixels);
+        break;
+    case TRIMMODE_CROP:
+        if (outHeight == 0)
+            pm_error("No row left after cropping incomplete row. "
+                     "Aborting.");
+        else if (verbose)
+            pm_message("Incomplete final row will be cropped.  %.0f "
+                       "pixels lost.", inPixels - outPixels);
+        break;
+    case TRIMMODE_ABORT:
+        pm_error("internal error");  /* Suppress compiler warning */
+        break;
+    }
+
+    *adjustedModeP = adjustedMode;
+}
+
+
+
+/*----------------------------------------------------------------------------
+  Width conversion using pointer arrays
+
+  This program reads input rows and converts to output rows of desired
+  width using a device which employs pointer arrays on both sides.
+  Conceptually similar, yet more simple, devices are used in pamcut,
+  pnmpad, pamflip and pnmcat.
+
+  inputPointers[] is an expanded version of incols[] seen in many pam
+  programs.  It reads multiple rows: as many rows as necessary to
+  complete at least one output row.
+
+  The read positions within inputPointers[] are fixed.  For example, if
+  the input row width is 100 and inputPointers has 400 elements, the read
+  positions will be: 0-99, 100-199, 200-299, 300-399.
+
+  The outPointers[] array is set up to allow selecting elements for
+  write from inputPointers[].  outPointers[] is at least as large as
+  inPointers[].  The write position migrates as necessary in a cycle.
+  If input width and output width are coprime and output has a
+  sufficient number of rows, all positions within outPointers[]
+  will be utilized.
+
+  Once set up, the conversion device is not altered until the input image
+  is completely read.
+
+  The following are special cases in which inPointers[] and outPointers[]
+  are set to the same size:
+
+  (1) Input width and output width are equal.
+  (2) Output width is an integer multiple of input width.
+  (3) Input width is an integer multiple of output width.
+
+  In cases (1) (2), the output position is fixed.
+  In case (3) the output position is mobile, but all of them will start
+  at integer multiples of output width.
+
+  Note that width, height and width * height variables are of type
+  "double" as a safeguard against overflows.
+-----------------------------------------------------------------------------*/
+
+
+
+static void
+setOutputDimensions(struct CmdlineInfo * const cmdlineP,
+                    double               const inPixelCt,
+                    int *                const outWidthP,
+                    int *                const outHeightP,
+                    enum TrimMode *      const trimModeP) {
+/*-----------------------------------------------------------------------------
+  Calculate the width and height of output from the number of pixels in
+  the input and command line arguments, most notably desired width.
+-----------------------------------------------------------------------------*/
+    double outWidth, outHeight;
+    enum TrimMode adjustedMode;
+
+    if (!cmdlineP->widthSpec) {
+        outWidth = inPixelCt;
+        outHeight = 1;
+        validateWidth(outWidth,
+                      "Input image is large and -width not specified. "
+                      "Output width");
+        adjustedMode = cmdlineP->trim;
+    } else {
+        double preAdjustedOutHeight;
+
+        outWidth  = cmdlineP->width;
+        preAdjustedOutHeight = floor(inPixelCt / outWidth);
+
+        adjustTrimMode(inPixelCt, outWidth, preAdjustedOutHeight,
+                       cmdlineP->verbose,
+                       cmdlineP->trim, &adjustedMode);
+
+        outHeight = adjustedMode == TRIMMODE_FILL ?
+            preAdjustedOutHeight + 1 : preAdjustedOutHeight;
+    }
+
+    *outWidthP  = (unsigned int)outWidth;
+    *outHeightP = (unsigned int)outHeight;
+    *trimModeP  = adjustedMode;
+}
+
+
+
+static void
+calculateInOutSize(unsigned int   const inWidth,
+                   unsigned int   const outWidth,
+                   unsigned int * const inputPointersWidthP,
+                   unsigned int * const outputPointersWidthP) {
+/*----------------------------------------------------------------------------
+  Calculate array size of inPointers[] and outPointers[] from
+  input width and output width.
+-----------------------------------------------------------------------------*/
+    double inputPointersWidth;
+    double outputPointersWidth;
+
+    if (outWidth > inWidth) {
+        if (outWidth % inWidth == 0)
+            inputPointersWidth = outputPointersWidth = outWidth;
+        else {
+            inputPointersWidth =
+              (outWidth / inWidth + 1) * inWidth * 2;
+            outputPointersWidth = inputPointersWidth + outWidth - 1;
+        }
+    }
+    else if (outWidth == inWidth)
+            inputPointersWidth = outputPointersWidth = outWidth;
+    else { /* outWidth < inWidth) */
+        if (inWidth % outWidth == 0)
+            inputPointersWidth = outputPointersWidth = inWidth;
+        else {
+            inputPointersWidth = inWidth * 2;
+            outputPointersWidth = inputPointersWidth + outWidth - 1;
+        }
+    }
+
+    if(inputPointersWidth > SIZE_MAX || outputPointersWidth > SIZE_MAX)
+        pm_error("Failed to set up conversion array.  Either input width, "
+                 "output width or their difference is too large.");
+
+    *inputPointersWidthP  = (unsigned int) inputPointersWidth;
+    *outputPointersWidthP = (unsigned int) outputPointersWidth;
+}
+
+
+
+static void
+restack(struct pam    * const inpamP,
+        struct pam    * const outpamP,
+        tuple         * const inputPointers,
+        tuple         * const outputPointers,
+        unsigned int    const inputPointersWidth,
+        unsigned int    const outputPointersWidth,
+        enum TrimMode   const trimMode) {
+/*----------------------------------------------------------------------------
+  Convert image, using inputPointers[] and outputPointers[]
+-----------------------------------------------------------------------------*/
+    unsigned int inoffset;
+    unsigned int outoffset;
+    unsigned int inPixelCt; /* Count of pixels read since last write */
+    unsigned int row;
+
+    /* Read all input and write all rows with the exception of the final
+       partial row */
+
+    for (row = 0, inoffset = 0, outoffset = 0, inPixelCt = 0;
+         row < inpamP->height; ++row) {
+
+        pnm_readpamrow(inpamP, &inputPointers[inoffset]);
+        inPixelCt += inpamP->width;
+
+        for ( ; inPixelCt >= outpamP->width; inPixelCt -= outpamP->width) {
+            pnm_writepamrow(outpamP, &outputPointers[outoffset]);
+            outoffset = (outoffset + outpamP->width ) % inputPointersWidth;
+        }
+        inoffset = (inoffset + inpamP->width) % inputPointersWidth;
+    }
+
+    /* Fill remainder of last row with black pixels and output */
+
+    if (inPixelCt > 0 && trimMode == TRIMMODE_FILL) {
+        tuple blackTuple;
+        unsigned int col;
+
+        pnm_createBlackTuple(outpamP, &blackTuple);
+
+        for (col = inPixelCt; col < outpamP->width; ++col) {
+            unsigned int const outoffset2 =
+                (outoffset + col) % outputPointersWidth;
+            outputPointers[outoffset2] = blackTuple;
+        }
+
+        /* output final row */
+        pnm_writepamrow(outpamP, &outputPointers[outoffset]);
+    }
+}
+
+
+
+static void
+restackSingleImage(FILE *               const ifP,
+                   struct CmdlineInfo * const cmdlineP) {
+
+    struct pam inpam;   /* Input PAM image */
+    struct pam mpam;    /* Adjusted PAM structure to read multiple rows */
+    struct pam outpam;  /* Output PAM image */
+
+    double        inPixelCt;
+    enum TrimMode trimMode;
+    tuple *       inputPointers;
+    tuple *       outputPointers;
+    unsigned int  inputPointersWidth;
+    unsigned int  outputPointersWidth;
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    inPixelCt = inpam.width * inpam.height;
+
+    outpam = inpam;
+
+    setOutputDimensions(cmdlineP, inPixelCt, &outpam.width, &outpam.height,
+                        &trimMode);
+
+    outpam.file = stdout;
+
+    pnm_writepaminit(&outpam);
+
+    calculateInOutSize(inpam.width, outpam.width,
+                       &inputPointersWidth, &outputPointersWidth);
+
+    mpam = inpam;
+    mpam.width = inputPointersWidth;
+
+    inputPointers = pnm_allocpamrow(&mpam);
+
+    if (outputPointersWidth > inputPointersWidth) {
+        unsigned int col;
+
+        MALLOCARRAY(outputPointers, outputPointersWidth);
+
+        if (!outputPointers) {
+            pm_error("Unable to allocate memory for %u output pointers",
+                     outputPointersWidth);
+        }
+
+        /* Copy pointers as far as inputPointers[] goes, then wrap around */
+        for (col = 0; col < outputPointersWidth; ++col)
+            outputPointers[col] = inputPointers[col % inputPointersWidth];
+
+    } else
+        outputPointers = inputPointers;
+
+    restack(&inpam, &outpam, inputPointers, outputPointers,
+            inputPointersWidth, outputPointersWidth, trimMode);
+
+    if (inputPointers != outputPointers)
+        free(outputPointers);
+
+    pnm_freepamrow(inputPointers);
+}
+
+
+
+int
+main(int argc, const char * argv[]) {
+
+    struct CmdlineInfo cmdline;
+    FILE * ifP;
+    int    eof;     /* no more images in input stream */
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    for (eof = false; !eof; ) {
+        restackSingleImage(ifP, &cmdline);
+        pnm_nextimage(ifP, &eof);
+    }
+
+    pm_close(ifP);
+
+    return 0;
+}
diff --git a/editor/pamshuffle.c b/editor/pamshuffle.c
new file mode 100644
index 00000000..bffb79c5
--- /dev/null
+++ b/editor/pamshuffle.c
@@ -0,0 +1,155 @@
+/*=============================================================================
+                               pamshuffle
+===============================================================================
+  Part of the Netpbm package.
+
+  Relocate pixels in row, randomly, using Fisher-Yates shuffling.
+
+  By Akira F. Urushibata
+
+  Contributed to the public domain by its author.
+=============================================================================*/
+
+#include <assert.h>
+#include "pm_c_util.h"
+#include "pam.h"
+#include "rand.h"
+#include "shhopt.h"
+
+struct CmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFileName;
+    unsigned int column;
+    unsigned int randomseedSpec;
+    unsigned int randomseed;
+};
+
+static void
+parseCommandLine(int argc, const char ** const argv,
+                 struct CmdlineInfo * const cmdlineP) {
+/*----------------------------------------------------------------------------
+   Note that the file spec array we return is stored in the storage that
+   was passed to us as the argv array.
+-----------------------------------------------------------------------------*/
+    optEntry * option_def;
+        /* Instructions to OptParseOptions3 on how to parse our options. */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+
+    MALLOCARRAY(option_def, 100);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = false;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = true;  /* We have no parms that are negative numbers */
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0,   "column",     OPT_FLAG,   NULL,
+                               &cmdlineP->column,                    0);
+    OPTENT3(0,   "randomseed", OPT_UINT,   &cmdlineP->randomseed,
+                               &cmdlineP->randomseedSpec,            0);
+
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    free(option_def);
+
+    if (argc-1 > 1)
+        pm_error("Too many arguments (%u). "
+                 "The only possible argument is the input file name.", argc-1);
+    else if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else
+        cmdlineP->inputFileName = argv[1];
+
+}
+
+
+
+static void
+shuffleRow(tuple *            const tuplerow,
+           unsigned int       const cols,
+           struct pm_randSt * const randStP) {
+
+    unsigned int col;
+
+    for (col = 0; col + 1 < cols; ++col) {
+        tuple        const temp    = tuplerow[col];
+        unsigned int const randcol = col + pm_rand(randStP) % (cols - col);
+
+        assert(randcol >= col );
+        assert(randcol < cols);
+
+        /* swap */
+        tuplerow[col]     = tuplerow[randcol];
+        tuplerow[randcol] = temp;
+    }
+}
+
+
+
+int
+main(int argc, const char * argv[]) {
+
+    FILE * ifP;
+    int    eof;     /* no more images in input stream */
+
+    struct CmdlineInfo cmdline;
+    struct pam inpam;   /* Input PAM image */
+    struct pam outpam;  /* Output PAM image */
+    struct pm_randSt randSt;
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    pm_randinit(&randSt);
+    pm_srand2(&randSt, cmdline.randomseedSpec, cmdline.randomseed);
+
+    for (eof = FALSE; !eof;) {
+        tuple * inrow;   /* Input row buffer */
+        tuple * outrow;  /* Pointers into the input row buffer to reorder it */
+        unsigned int row, col;
+
+        pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+        outpam = inpam;
+        outpam.file = stdout;
+
+        pnm_writepaminit(&outpam);
+
+        inrow = pnm_allocpamrow(&inpam);
+
+        MALLOCARRAY(outrow, inpam.width);
+
+        if (!outrow)
+            pm_error("Unable to allocate memory for %u-column output buffer",
+                     inpam.width);
+
+        for (col = 0; col < inpam.width; ++col)
+            outrow[col] = inrow[col];
+
+        for (row = 0; row < inpam.height; ++row) {
+            pnm_readpamrow(&inpam, inrow);
+
+            if (cmdline.column && row > 0) {
+                /* Use the same shuffle ('outrow') as the previous row */
+            } else
+                shuffleRow(outrow, inpam.width, &randSt);
+
+            pnm_writepamrow(&outpam, outrow);
+        }
+
+        pnm_freepamrow(inrow);
+        free(outrow);
+        pnm_nextimage(ifP, &eof);
+    }
+
+    pm_randterm(&randSt);
+
+    return 0;
+}
diff --git a/generator/pamseq.c b/generator/pamseq.c
index 1af5252a..4c00e2a5 100644
--- a/generator/pamseq.c
+++ b/generator/pamseq.c
@@ -1,3 +1,4 @@
+#include <assert.h>
 #include <string.h>
 #include <unistd.h>
 #include <stdlib.h>
@@ -6,49 +7,173 @@
 #include "pm_c_util.h"
 #include "pam.h"
 #include "shhopt.h"
+#include "mallocvar.h"
+#include "nstring.h"
 
 
 
-struct cmdlineInfo {
+struct CmdlineInfo {
     /* All the information the user supplied in the command line,
        in a form easy for the program to use.
     */
     unsigned int depth;
     sample maxval;
     const char * tupletype;
+    sample * min;   /* array of size 'depth' */
+    sample * max;   /* array of size 'depth' */
+    sample * step;  /* array of size 'depth' */
 };
 
 
 
 static void
-parseCommandLine(int argc, char ** argv,
-                 struct cmdlineInfo * const cmdlineP) {
+destroyCmdline(struct CmdlineInfo * const cmdlineP)  {
+
+    if (cmdlineP->min)
+        free(cmdlineP->min);
+    if (cmdlineP->max)
+        free(cmdlineP->max);
+    if (cmdlineP->step)
+        free(cmdlineP->step);
+}
+
+
+
+static unsigned int
+entryCt(char ** const stringList) {
+
+    unsigned int i;
+
+    for (i = 0; stringList[i]; ++i) {}
+
+    return i;
+}
+
+
+
+static void
+parseOptList(bool         const isSpec,
+             char **      const stringList,
+             unsigned int const depth,
+             sample       const maxval,
+             const char * const optNm,
+             sample **    const sampleListP) {
+
+    if (!isSpec)
+        *sampleListP = NULL;
+    else {
+        unsigned int i;
+        sample * sampleList;
+        const char * memberError;
+
+        if (entryCt(stringList) != depth) {
+            pm_error("Wrong number of values for -%s: %u.  Need %u",
+                     optNm, entryCt(stringList), depth);
+        }
+
+        MALLOCARRAY(sampleList, depth);
+
+        for (i = 0, memberError = NULL; i < depth && !memberError; ++i) {
+            char * endPtr;
+            long const n = strtol(stringList[i], &endPtr, 10);
+
+            if (strlen(stringList[i]) == 0)
+                pm_asprintf(&memberError, "is null string");
+            else if (*endPtr != '\0')
+                pm_asprintf(&memberError,
+                            "contains non-numeric character '%c'",
+                            *endPtr);
+            else if (n < 0)
+                pm_asprintf(&memberError, "is negative");
+            else if (n > maxval)
+                pm_asprintf(&memberError, "is greater than maxval %lu",
+                            maxval);
+            else
+                sampleList[i] = n;
+        }
+        if (memberError) {
+            free(sampleList);
+            pm_errormsg("Value in -%s %s", optNm, memberError);
+            pm_longjmp();
+        }
+        *sampleListP = sampleList;
+    }
+}
+
+
+
+static void
+validateMinIsAtMostMax(sample *     const min,
+                       sample *     const max,
+                       unsigned int const depth) {
+
+    unsigned int plane;
+
+    for (plane = 0; plane < depth; ++plane) {
+        if (min[plane] > max[plane])
+            pm_error("-min for plane %u (%lu) is greater than -max (%lu)",
+                     plane, min[plane], max[plane]);
+    }
+}
+
+
+
+static void
+validateStepIsPositive(sample *     const step,
+                       unsigned int const depth) {
+
+    unsigned int plane;
+
+    for (plane = 0; plane < depth; ++plane) {
+        if (step[plane] <= 0)
+            pm_error("-step for plane %u (%lu) is not positive",
+                     plane, step[plane]);
+    }
+}
+
+
+
+static void
+parseCommandLine(int argc, const char ** argv,
+                 struct CmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
-  Convert program invocation arguments (argc,argv) into a format the 
+  Convert program invocation arguments (argc,argv) into a format the
   program can use easily, struct cmdlineInfo.  Validate arguments along
   the way and exit program with message if invalid.
 
-  Note that some string information we return as *cmdlineP is in the storage 
+  Note that some string information we return as *cmdlineP is in the storage
   argv[] points to.
 -----------------------------------------------------------------------------*/
-    optEntry *option_def = malloc(100*sizeof(optEntry));
-        /* Instructions to OptParseOptions2 on how to parse our options.
-         */
+    optEntry *option_def;
     optStruct3 opt;
-
+        /* Instructions to pm_optParseOptions3 on how to parse our options. */
+
+    unsigned int maxSpec;
+    char ** max;
+    unsigned int minSpec;
+    char ** min;
+    unsigned int stepSpec;
+    char ** step;
     unsigned int tupletypeSpec;
     unsigned int option_def_index;
 
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
     option_def_index = 0;   /* incremented by OPTENT3 */
-    OPTENT3(0,   "tupletype",  OPT_STRING, &cmdlineP->tupletype, 
+    OPTENT3(0,   "tupletype",  OPT_STRING, &cmdlineP->tupletype,
             &tupletypeSpec,     0);
-
+    OPTENT3(0,   "min",         OPT_STRINGLIST, &min,
+            &minSpec,           0);
+    OPTENT3(0,   "max",         OPT_STRINGLIST, &max,
+            &maxSpec,           0);
+    OPTENT3(0,   "step",        OPT_STRINGLIST, &step,
+            &stepSpec,          0);
 
     opt.opt_table = option_def;
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (!tupletypeSpec)
@@ -57,9 +182,9 @@ parseCommandLine(int argc, char ** argv,
         struct pam pam;
         if (strlen(cmdlineP->tupletype)+1 > sizeof(pam.tuple_type))
             pm_error("The tuple type you specified is too long.  "
-                     "Maximum %u characters.", 
+                     "Maximum %u characters.",
                      (unsigned)sizeof(pam.tuple_type)-1);
-    }        
+    }
 
     if (argc-1 < 2)
         pm_error("Need two arguments: depth and maxval.");
@@ -77,73 +202,154 @@ parseCommandLine(int argc, char ** argv,
                      "specified '%s'", argv[2]);
         if (cmdlineP->maxval > PNM_OVERALLMAXVAL)
             pm_error("The maxval you specified (%u) is too big.  "
-                     "Maximum is %u", (unsigned int) cmdlineP->maxval, 
+                     "Maximum is %u", (unsigned int) cmdlineP->maxval,
                      PNM_OVERALLMAXVAL);
-        if (pm_maxvaltobits(cmdlineP->maxval) + 
+        if (pm_maxvaltobits(cmdlineP->maxval) +
             pm_maxvaltobits(cmdlineP->depth-1) > sizeof(unsigned int)*8)
             pm_error("The maxval (%u) and depth (%u) you specified result "
                      "in a larger number of tuples than this program can "
-                     "handle (roughly %u)", 
+                     "handle (roughly %u)",
                      (unsigned int) cmdlineP->maxval, cmdlineP->depth,
                      (unsigned int) -1);
     }
+    parseOptList(minSpec, min,  cmdlineP->depth, cmdlineP->maxval, "min",
+                 &cmdlineP->min);
+    parseOptList(maxSpec, max,  cmdlineP->depth, cmdlineP->maxval, "max",
+                 &cmdlineP->max);
+    parseOptList(stepSpec, step, cmdlineP->depth, cmdlineP->maxval, "step",
+                 &cmdlineP->step);
+
+    if (cmdlineP->min && cmdlineP->max)
+        validateMinIsAtMostMax(cmdlineP->min, cmdlineP->max, cmdlineP->depth);
+
+    if (cmdlineP->step)
+        validateStepIsPositive(cmdlineP->step, cmdlineP->depth);
+
+    if (minSpec)
+        free(min);
+    if (maxSpec)
+        free(max);
+    if (stepSpec)
+        free(step);
 }
 
 
 
-static unsigned int
-powint(unsigned int base, unsigned int exponent) {
+static void
+computeMinMaxStep(unsigned int   const depth,
+                  sample         const maxval,
+                  const sample * const min,
+                  const sample * const max,
+                  const sample * const step,
+                  sample **      const minP,
+                  sample **      const maxP,
+                  sample **      const stepP) {
+
+    unsigned int plane;
+
+    MALLOCARRAY(*minP,  depth);
+    MALLOCARRAY(*maxP,  depth);
+    MALLOCARRAY(*stepP, depth);
+
+    for (plane = 0; plane < depth; ++plane) {
+        (*minP)[plane]  = min  ? min[plane]  : 0;
+        (*maxP)[plane]  = max  ? max[plane]  : maxval;
+        (*stepP)[plane] = step ? step[plane] : 1;
+    }
+}
+
+
+
+static int
+imageWidth(unsigned int   const depth,
+           const sample * const min,
+           const sample * const max,
+           const sample * const step) {
 /*----------------------------------------------------------------------------
-   This is standard pow(), but for integers and optimized for small
-   exponents.
+   The width of the output image (i.e. the number of pixels in the image),
+   given that the minimum and maximum sample values in Plane P are min[P] and
+   max[P] and the samples step by step[P].
+
+   E.g. in the sample case of min 0, max 4, and step 1 everywhere, with
+   depth 2,  We return 5*5 = 25.
 -----------------------------------------------------------------------------*/
-    unsigned int result;
-    unsigned int i;
+    unsigned int product;
+    unsigned int plane;
+
+    for (plane = 0, product=1; plane < depth; ++plane) {
+        assert(max[plane] >= min[plane]);
+
+        unsigned int const valueCtThisPlane =
+            ROUNDUP((max[plane] - min[plane] + 1), step[plane])/step[plane];
 
-    result = 1;  /* initial value */
-    for (i = 0; i < exponent; ++i) 
-        result *= base;
+        if (INT_MAX / valueCtThisPlane < product)
+            pm_error("Uncomputably large number of pixels (greater than %u",
+                     INT_MAX);
 
-    return(result);
+        product *= valueCtThisPlane;
+    }
+    assert(product < INT_MAX);
+
+    return product;
 }
 
 
+
 static void
-permuteHigherPlanes(struct pam const pam, int const nextplane, 
-                    tuple * const tuplerow, int * const colP, 
-                    tuple const lowerPlanes) {
+permuteHigherPlanes(unsigned int   const depth,
+                    const sample * const min,
+                    const sample * const max,
+                    const sample * const step,
+                    unsigned int   const nextPlane,
+                    tuple *        const tuplerow,
+                    int *          const colP,
+                    tuple          const lowerPlanes) {
 /*----------------------------------------------------------------------------
    Create all the possible permutations of tuples whose lower-numbered planes
    contain the values from 'lowerPlanes'.  I.e. vary the higher-numbered
-   planes between zero and maxval.
+   planes according to min[], max[], and step[].
 
    Write them sequentially into *tuplerow, starting at *colP.  Adjust
    *colP to next the column after the ones we write.
 
-   lower-numbered means with plane numbers less than 'nextplane'.
+   lower-numbered means with plane numbers less than 'nextPlane'.
 
    We modify 'lowerPlanes' in the higher planes to undefined values.
 -----------------------------------------------------------------------------*/
-    if (nextplane == pam.depth - 1) {
+    if (nextPlane == depth - 1) {
+        /* lowerPlanes[] contains values for all the planes except the
+           highest, so we just vary the highest plane and combine that
+           with lowerPlanes[] and output that to tuplerow[].
+        */
         sample value;
-        for (value = 0; value <= pam.maxval; ++value) {
+
+        for (value = min[nextPlane];
+             value <= max[nextPlane];
+             value += step[nextPlane]) {
+
             unsigned int plane;
-            for (plane = 0; plane < nextplane; ++plane)
+
+            for (plane = 0; plane < nextPlane; ++plane)
                 tuplerow[*colP][plane] = lowerPlanes[plane];
-            tuplerow[*colP][nextplane] = value;
+
+            tuplerow[*colP][nextPlane] = value;
+
             ++(*colP);
         }
     } else {
         sample value;
 
-        for (value = 0; value <= pam.maxval; ++value) {
+        for (value = min[nextPlane];
+             value <= max[nextPlane];
+             value += step[nextPlane]) {
             /* We do something sleazy here and use Caller's lowerPlanes[]
                variable as a local variable, modifying it in the higher
                plane positions.  That's just for speed.
             */
-            lowerPlanes[nextplane] = value;
+            lowerPlanes[nextPlane] = value;
 
-            permuteHigherPlanes(pam, nextplane+1, tuplerow, colP, lowerPlanes);
+            permuteHigherPlanes(depth, min, max, step,
+                                nextPlane+1, tuplerow, colP, lowerPlanes);
         }
     }
 }
@@ -151,52 +357,65 @@ permuteHigherPlanes(struct pam const pam, int const nextplane,
 
 
 int
-main(int argc, char **argv) {
+main(int argc, const char **argv) {
 
-    struct cmdlineInfo cmdline;
+    struct CmdlineInfo cmdline;
     struct pam pam;
     int col;
     tuple lowerPlanes;
         /* This is working storage passed to permuteHigherPlanes(),
            which we call.  Note that because we always pass zero as the
-           "planes" argument to permuteHigherPlanes(), none of the 
-           "lower planes" value is defined as an input to 
+           "planes" argument to permuteHigherPlanes(), none of the
+           "lower planes" value is defined as an input to
            permuteHigherPlanes().
         */
     tuple * tuplerow;
-    
-    pnm_init(&argc, argv);
-   
+    sample * min;   /* malloc'ed array */
+    sample * max;   /* malloc'ed array */
+    sample * step;  /* malloc'ed array */
+
+    pm_proginit(&argc, argv);
+
     parseCommandLine(argc, argv, &cmdline);
 
+    computeMinMaxStep(cmdline.depth, cmdline.maxval,
+                      cmdline.min, cmdline.max, cmdline.step,
+                      &min, &max, &step);
+
     pam.size = sizeof(pam);
     pam.len = PAM_STRUCT_SIZE(tuple_type);
     pam.file = stdout;
     pam.format = PAM_FORMAT;
     pam.plainformat = 0;
-    pam.width = powint(cmdline.maxval+1, cmdline.depth);
+    pam.width = imageWidth(cmdline.depth, min, max, step);
     pam.height = 1;
     pam.depth = cmdline.depth;
     pam.maxval = cmdline.maxval;
     strcpy(pam.tuple_type, cmdline.tupletype);
 
     pnm_writepaminit(&pam);
-   
+
     tuplerow = pnm_allocpamrow(&pam);
 
     lowerPlanes = pnm_allocpamtuple(&pam);
 
     col = 0;
 
-    permuteHigherPlanes(pam, 0, tuplerow, &col, lowerPlanes);
+    permuteHigherPlanes(pam.depth, min, max, step,
+                        0, tuplerow, &col, lowerPlanes);
 
     if (col != pam.width)
         pm_error("INTERNAL ERROR: Wrote %d columns; should have written %d.",
                  col, pam.width);
 
     pnm_writepamrow(&pam, tuplerow);
-    
+
     pnm_freepamrow(tuplerow);
 
+    destroyCmdline(&cmdline);
+
     return 0;
 }
+
+
+
diff --git a/lib/pm.h b/lib/pm.h
index b3c3d202..8d5973eb 100644
--- a/lib/pm.h
+++ b/lib/pm.h
@@ -380,6 +380,16 @@ pm_getline(FILE *   const ifP,
            int *    const eofP,
            size_t * const lineLenP);
 
+void
+pm_readfile(FILE *                 const fileP,
+            const unsigned char ** const bytesP,
+            size_t *               const szP);
+
+void
+pm_writefile(FILE *                const fileP,
+             const unsigned char * const bytes,
+             size_t                const sz);
+
 short
 pm_bs_short(short const s);
 
diff --git a/lib/pmfileio.c b/lib/pmfileio.c
index 1ed71f18..5d6d9bc1 100644
--- a/lib/pmfileio.c
+++ b/lib/pmfileio.c
@@ -939,6 +939,72 @@ pm_getline(FILE *   const ifP,
 
 
 
+void
+pm_readfile(FILE *                 const fileP,
+            const unsigned char ** const bytesP,
+            size_t *               const szP) {
+
+    unsigned char * buf;
+    size_t allocatedSz;
+    size_t szSoFar;
+    size_t chunkSz;
+    bool eof;
+
+    for (szSoFar = 0, buf = NULL, allocatedSz = 0, chunkSz = 4096,
+             eof = false;
+         !eof; ) {
+
+        size_t bytesReadCt;
+
+        if (szSoFar + chunkSz > allocatedSz) {
+            allocatedSz = szSoFar + chunkSz;
+            REALLOCARRAY(buf, allocatedSz);
+
+            if (!buf) {
+                pm_error("Failed to get memory for %lu byte input buffer",
+                         allocatedSz);
+            }
+        }
+        bytesReadCt = fread(buf + szSoFar, 1, chunkSz, fileP);
+
+        if (ferror(fileP))
+            pm_error("Failed to read input from file");
+
+        szSoFar += bytesReadCt;
+
+        if (bytesReadCt < chunkSz)
+            eof = true;
+        else {
+            /* Double buffer and read size up to 1 MiB */
+            if (szSoFar <= 1024*1024)
+                chunkSz = szSoFar;
+        }
+    }
+
+    *bytesP = buf;
+    *szP    = szSoFar;
+}
+
+
+
+void
+pm_writefile(FILE *                const fileP,
+             const unsigned char * const bytes,
+             size_t                const sz) {
+
+    size_t bytesWrittenCt;
+
+    bytesWrittenCt = fwrite(bytes, 1, sz, fileP);
+
+    if (bytesWrittenCt != sz) {
+        pm_error("Failed to write %lu bytes to Standard Output.  "
+                 "%lu bytes successfully written",
+                 sz, bytesWrittenCt);
+    }
+}
+
+
+
 union cheat {
     uint32_t l;
     short s;
diff --git a/lib/util/nstring.h b/lib/util/nstring.h
index 8829617d..677e24cb 100644
--- a/lib/util/nstring.h
+++ b/lib/util/nstring.h
@@ -30,9 +30,7 @@ extern "C" {
 #define STRSEQ(A, B) \
 	(strneq((A), (B), sizeof(A)))
 
-#define MEMEQ(a,b,c) (memcmp(a, b, c) == 0)
-
-#define MEMSEQ(a,b) (memeq(a, b, sizeof(*(a))) == 0)
+#define MEMSEQ(a,b) (memeq(a, b, sizeof(*(a))))
 
 #define MEMSSET(a,b) (memset(a, b, sizeof(*(a))))
 
diff --git a/lib/util/shhopt.h b/lib/util/shhopt.h
index b6d4cdfd..9ba072cb 100644
--- a/lib/util/shhopt.h
+++ b/lib/util/shhopt.h
@@ -10,14 +10,12 @@ main ( int argc, char **argv ) {
     /* initial values here are just to demonstrate what gets set and
        what doesn't by the code below.
     */
-    int help_flag = 7;
-    unsigned int help_spec =7;
-    unsigned int height_spec =7;
-    unsigned int name_spec= 7;
+    unsigned int heightSpec =7;
+    unsigned int nameSpec= 7;
     char *name= "initial";
     int height=7;
-    int verbose_flag=7;
-    int debug_flag=7;
+    int verboseFlag=7;
+    int debugFlag=7;
     char ** methodlist;
     struct optNameValue * optlist;
 
@@ -26,12 +24,12 @@ main ( int argc, char **argv ) {
     optEntry * option_def;
     MALLOCARRAY(option_def, 100);
 
-    OPTENT3(0,   "height",   OPT_INT,        &height,       &height_spec, 0);
-    OPTENT3('n', "name",     OPT_STRING,     &name,         &name_spec,   0);
-    OPTENT3('v', "verbose",  OPT_FLAG,       &verbose_flag, NULL,         0);
-    OPTENT3('g', "debug",    OPT_FLAG,       &debug_flag,   NULL,         0);
-    OPTENT3(0,   "methods",  OPT_STRINGLIST, &methodlist,   NULL,         0);
-    OPTENT3(0,   "options",  OPT_NAMELIST,   &optlist,      NULL,         0);
+    OPTENT3(0,   "height",   OPT_INT,        &height,       &heightSpec,  0);
+    OPTENT3('n', "name",     OPT_STRING,     &name,         &nameSpec,    0);
+    OPTENT3('v', "verbose",  OPT_FLAG,       &verboseFlag,  NULL,         0);
+    OPTENT3('g', "debug",    OPT_FLAG,       &debugFlag,    NULL,         0);
+    OPTENT3(0,   "methods",  OPT_STRINGLIST, &methodlist,   &methodSpec,  0);
+    OPTENT3(0,   "options",  OPT_NAMELIST,   &optlist,      &optSpec,     0);
 
     opt.opt_table = option_def;
     opt.short_allowed = 1;
@@ -43,13 +41,13 @@ main ( int argc, char **argv ) {
 
     printf("argc=%d\n", argc);
     printf("height=%d\n", height);
-    printf("height_spec=%d\n", height_spec);
+    printf("height_spec=%d\n", heightSpec);
     printf("name='%s'\n", name);
-    printf("name_spec=%d\n", name_spec);
-    printf("verbose_flag=%d\n", verbose_flag);
-    printf("debug_flag=%d\n", verbose_flag);
+    printf("name_spec=%d\n", nameSpec);
+    printf("verbose_flag=%d\n", verboseFlag);
+    printf("debug_flag=%d\n", verboseFlag);
 
-    if (methodlist) {
+    if (methodSpec) {
         unsigned int i;
         printf("methods: ");
         while (methodlist[i]) {
@@ -60,7 +58,7 @@ main ( int argc, char **argv ) {
     } else
         printf("No -options\n");
 
-    if (optlist) {
+    if (optSpec) {
         unsigned int i;
         while (optlist[i].name) {
             printf("option '%s' = '%s'\n", optlist[i].name, optlist[i].value);
@@ -84,6 +82,7 @@ Now run this program with something like
   you need an OPTENTINIT call to establish the empty option table:
 
     optEntry * option_def;
+    unsigned int option_def_index;
     MALLOCARRAY(option_def, 1);
     OPTENTINIT;
 
diff --git a/test/Test-Order b/test/Test-Order
index dd89a91c..eafcb974 100644
--- a/test/Test-Order
+++ b/test/Test-Order
@@ -83,6 +83,8 @@ pambackground.test
 pnmpad-reportonly.test
 pnmpaste-pbm.test
 
+pamrestack.test
+pamshuffle.test
 ppmshift.test
 ppmspread.test
 
diff --git a/test/all-in-place.ok b/test/all-in-place.ok
index e50efc8c..0af78737 100644
--- a/test/all-in-place.ok
+++ b/test/all-in-place.ok
@@ -75,12 +75,14 @@ pamperspective: ok
 pampick: ok
 pampop9: ok
 pamrecolor: ok
+pamrestack: ok
 pamrubber: ok
 pamscale: ok
 pamseq: ok
 pamshadedrelief: ok
 pamsharpmap: ok
 pamsharpness: ok
+pamshuffle: ok
 pamsistoaglyph: ok
 pamslice: ok
 pamsplit: ok
@@ -107,6 +109,7 @@ pamtopdbimg: ok
 pamtopfm: ok
 pamtopng: ok
 pamtopnm: ok
+pamtoqoi: ok
 pamtosrf: ok
 pamtosvg: ok
 pamtotga: ok
@@ -300,6 +303,7 @@ ppmtv: ok
 ppmwheel: ok
 psidtopgm: ok
 pstopnm: ok
+qoitopam: ok
 qrttoppm: ok
 rasttopnm: ok
 rawtopgm: ok
diff --git a/test/all-in-place.test b/test/all-in-place.test
index d154dba4..c2b99328 100755
--- a/test/all-in-place.test
+++ b/test/all-in-place.test
@@ -117,12 +117,14 @@ ordinary_testprogs="\
   pampick \
   pampop9 \
   pamrecolor \
+  pamrestack \
   pamrubber \
   pamscale \
   pamseq \
   pamshadedrelief \
   pamsharpmap \
   pamsharpness \
+  pamshuffle \
   pamsistoaglyph \
   pamslice \
   pamsplit \
@@ -149,6 +151,7 @@ ordinary_testprogs="\
   pamtopfm \
   pamtopng \
   pamtopnm \
+  pamtoqoi \
   pamtosrf \
   pamtosvg \
   pamtotga \
@@ -342,6 +345,7 @@ ordinary_testprogs="\
   ppmwheel \
   psidtopgm \
   pstopnm \
+  qoitopam \
   qrttoppm \
   rasttopnm \
   rawtopgm \
diff --git a/test/channel-stack-roundtrip.test b/test/channel-stack-roundtrip.test
index a640b98d..14ac0d2e 100755
--- a/test/channel-stack-roundtrip.test
+++ b/test/channel-stack-roundtrip.test
@@ -1,6 +1,6 @@
 #! /bin/sh
-# This script tests: pamchanel pamstack
-# Also requires: pamtopam pamstack pamtopnm
+# This script tests: pamchannel pamstack
+# Also requires: pamtopam pamtopnm
 
 tmpdir=${tmpdir:-/tmp}
 r_pam=${tmpdir}/testimg_r.pam
diff --git a/test/gif-roundtrip.test b/test/gif-roundtrip.test
index 2bc3a98d..a30be1aa 100755
--- a/test/gif-roundtrip.test
+++ b/test/gif-roundtrip.test
@@ -10,40 +10,40 @@ tmpdir=${tmpdir:-/tmp}
 
 echo "Test 1. Should print 1926073387 101484"
 
-test_ppm=${tmpdir}/testimg.ppm
+rose_ppm=${tmpdir}/rose.ppm
 
-cp testimg.ppm ${tmpdir} &&
-ppmtorgb3 ${test_ppm}
+cp testimg.ppm ${rose_ppm} &&
+ppmtorgb3 ${rose_ppm}
 
-test_red=${tmpdir}/testimg.red
-test_grn=${tmpdir}/testimg.grn
-test_blu=${tmpdir}/testimg.blu
+rose_red=${tmpdir}/rose.red
+rose_grn=${tmpdir}/rose.grn
+rose_blu=${tmpdir}/rose.blu
 out_red=${tmpdir}/out.red
 out_grn=${tmpdir}/out.grn
 #out_blu=${tmpdir}/out.blu
 
-pamtogif ${test_red} | giftopnm > ${out_red} &&
-pamtogif ${test_grn} | giftopnm > ${out_grn} &&
-pamtogif ${test_blu} | giftopnm | \
+pamtogif ${rose_red} | giftopnm > ${out_red} &&
+pamtogif ${rose_grn} | giftopnm > ${out_grn} &&
+pamtogif ${rose_blu} | giftopnm | \
   rgb3toppm ${out_red} ${out_grn} - | \
   cksum
 
-rm ${test_ppm} ${test_grn} ${test_blu} ${out_red} ${out_grn}
+rm ${rose_ppm} ${rose_grn} ${rose_blu} ${out_red} ${out_grn}
 
 echo "Test 2. Should produce 1571496937 33838 six times"
 
-test_gif=${tmpdir}/testimg.gif
+rose_gif=${tmpdir}/rose.gif
 
-cat ${test_red} | cksum
-pamtogif ${test_red} | giftopnm | cksum
-pamtogif -interlace ${test_red} | giftopnm | cksum
-pamtogif -noclear ${test_red} | giftopnm | cksum
-pamtogif -sort ${test_red} | tee ${test_gif} | \
+cat ${rose_red} | cksum
+pamtogif ${rose_red} | giftopnm | cksum
+pamtogif -interlace ${rose_red} | giftopnm | cksum
+pamtogif -noclear ${rose_red} | giftopnm | cksum
+pamtogif -sort ${rose_red} | tee ${rose_gif} | \
   giftopnm | cksum
-echo "junk" >> ${test_gif} && \
-  giftopnm -image=1 -quitearly ${test_gif} | cksum
+echo "junk" >> ${rose_gif} && \
+  giftopnm -image=1 -quitearly ${rose_gif} | cksum
 
-rm  ${test_gif} ${test_red}
+rm  ${rose_gif} ${rose_red}
 
 echo "Test 3. Should produce 281226646 481 six times"
 # maze.pbm is too small for -noclear to take effect 
@@ -78,7 +78,7 @@ echo ""
 echo "Test 5. Should produce: N : 0 0 0 0 : 0 , N : 0 0 0 0 : 0"
 echo "(N=238, 239, 240, 241, 255, 256, 257, 4030, 4031, 4097)"
 
-test_pgm=${tmpdir}/testimg.pgm
+rose_pgm=${tmpdir}/rose.pgm
 
 # The following awk scripts produce a PGM file with no repeated
 # sequences.  Obviously this cannot be compressed at all; the codes
@@ -114,14 +114,14 @@ awk -v maxval=${maxval} 'BEGIN \
 
 for size in 238 239 240 241 255 256 257
   do
-  pamcut -height=${size} ${test257_pgm} > ${test_pgm} &&
-  pamtogif -verbose ${test_pgm} | giftopnm | pamdepth ${maxval} | \
-    cmp - ${test_pgm}
+  pamcut -height=${size} ${test257_pgm} > ${rose_pgm} &&
+  pamtogif -verbose ${rose_pgm} | giftopnm | pamdepth ${maxval} | \
+    cmp - ${rose_pgm}
   echo -n ${size} ":" ${PIPESTATUS[@]} ":" $? ", "
-  pamtogif -nolzw -verbose ${test_pgm} | giftopnm | pamdepth ${maxval} | \
-    cmp - ${test_pgm}
+  pamtogif -nolzw -verbose ${rose_pgm} | giftopnm | pamdepth ${maxval} | \
+    cmp - ${rose_pgm}
   echo ${size} ":" ${PIPESTATUS[@]} ":" $?
-  rm ${test_pgm}
+  rm ${rose_pgm}
   done 
 
 rm ${test257_pgm}
@@ -144,16 +144,16 @@ awk -v maxval=${maxval} 'BEGIN \
 
 for size in 4030 4031 4097
   do
-  pamcut -height ${size} ${test4097_pgm} > ${test_pgm} &&
-  pamtogif -verbose ${test_pgm} | giftopnm | pamdepth ${maxval} | \
-    cmp - ${test_pgm}
+  pamcut -height ${size} ${test4097_pgm} > ${rose_pgm} &&
+  pamtogif -verbose ${rose_pgm} | giftopnm | pamdepth ${maxval} | \
+    cmp - ${rose_pgm}
   # pamdepth ${maxval} is necessary because
   # giftopnm output is maxval 255
   echo -n ${size} ":" ${PIPESTATUS[@]} ":" $? ", "
-  pamtogif -nolzw ${test_pgm} | giftopnm | pamdepth ${maxval} | \
-    cmp - ${test_pgm}
+  pamtogif -nolzw ${rose_pgm} | giftopnm | pamdepth ${maxval} | \
+    cmp - ${rose_pgm}
   echo ${size} ":" ${PIPESTATUS[@]} ":" $?
-  rm ${test_pgm}
+  rm ${rose_pgm}
   done 
 
 rm ${test4097_pgm}
diff --git a/test/pamflip-pbm-roundtrip.test b/test/pamflip-pbm-roundtrip.test
index 71f2e926..02a342cb 100755
--- a/test/pamflip-pbm-roundtrip.test
+++ b/test/pamflip-pbm-roundtrip.test
@@ -1,6 +1,6 @@
 #! /bin/sh
 # This script tests: pamflip
-# Also requires: ppmpat pamseq pamtopnm
+# Also requires: pbmmake pbmnoise
 
 tmpdir=${tmpdir:-/tmp}
 dot_pbm=${tmpdir}/dot.pbm
diff --git a/test/pamhue.test b/test/pamhue.test
index 622fdf2f..cd5430a8 100755
--- a/test/pamhue.test
+++ b/test/pamhue.test
@@ -16,7 +16,7 @@ pamseq -tupletype=RGB 3 1 | pamdepth 255 | pamhue -huechange=60 | \
 
 echo "Test 3"
 # pamhue has no effect on monotone images
-# Should print 281226646 481 twice
+# Should print 0 0 : 0 twice
 
 pamhue -huechange=45  maze.pbm | cmp -s - maze.pbm
   echo ${PIPESTATUS[@]} ":" $?
@@ -25,7 +25,7 @@ pamhue -huechange=180 maze.pbm | cmp -s - maze.pbm
 
 echo "Test 4"
 # spinning the color wheel by multiples of 360 leaves the image unchanged
-# Should print 1926073387 101484 twice
+# Should print 0 0 : 0 twice
 
 pamhue -huechange=0 testimg.ppm   | cmp -s - testimg.ppm
   echo ${PIPESTATUS[@]} ":" $?
diff --git a/test/pamrecolor.test b/test/pamrecolor.test
index 6f323169..0ba35b66 100755
--- a/test/pamrecolor.test
+++ b/test/pamrecolor.test
@@ -1,6 +1,6 @@
 #! /bin/sh
 # This script tests: pamrecolor
-# Also requires: ppmtopgm pgmmake
+# Also requires: pgmmake
 
 tmpdir=${tmpdir:-/tmp}
 base_pgm=${tmpdir}/base.pgm
diff --git a/test/pamrestack.ok b/test/pamrestack.ok
new file mode 100644
index 00000000..07552913
--- /dev/null
+++ b/test/pamrestack.ok
@@ -0,0 +1,68 @@
+Test 1.
+P2
+24 1
+7
+0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 
+P2
+10 3
+7
+0 1 2 3 4 5 6 7 0 1 
+2 3 4 5 6 7 0 1 2 3 
+4 5 6 7 0 0 0 0 0 0 
+P2
+10 2
+7
+0 1 2 3 4 5 6 7 0 1 
+2 3 4 5 6 7 0 1 2 3 
+P2
+4 6
+7
+0 1 2 3 
+4 5 6 7 
+0 1 2 3 
+4 5 6 7 
+0 1 2 3 
+4 5 6 7 
+P2
+12 2
+7
+0 1 2 3 4 5 6 7 0 1 2 3 
+4 5 6 7 0 1 2 3 4 5 6 7 
+Test 2.  Should print 0 twelve times
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+Test 3.  Should produce 3141273448 431 fifteen times
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+3141273448 431
+Test 4. Should produce 1768948962 101484 twice
+1768948962 101484
+1768948962 101484
+Test Invalid.
+Expected failure 1 1
+Expected failure 2 1
+Expected failure 3 1
+Expected failure 4 1
+Expected failure 5 1
diff --git a/test/pamrestack.test b/test/pamrestack.test
new file mode 100755
index 00000000..17e42e30
--- /dev/null
+++ b/test/pamrestack.test
@@ -0,0 +1,91 @@
+#! /bin/sh
+# This script tests: pamrestack
+# Also requires: pamfile pamflip pgmramp pnmcrop pnminvert
+
+tmpdir=${tmpdir:-/tmp}
+ramp_pgm=${tmpdir}/ramp.pgm
+ramp2_pgm=${tmpdir}/ramp2.pgm
+maze_singlerow_pbm=${tmpdir}/maze_singlerow.pbm
+maze_inverted_pbm=${tmpdir}/maze_inverted.pbm
+
+echo "Test 1."
+
+pgmramp -lr -maxval=7 8 3 | tee ${ramp_pgm} | pamrestack -plain
+pamrestack -width=10 -trim=fill -plain ${ramp_pgm}
+pamrestack -width=10 -trim=crop -plain ${ramp_pgm}
+pamrestack -width=4 -trim=fill -plain ${ramp_pgm}
+pamrestack -width=12 -trim=fill -plain ${ramp_pgm}
+
+echo "Test 2.  Should print 0 twelve times"
+
+for width in 2 4 5 8 12 24
+do
+    pamrestack -width=${width} -trim=crop ${ramp_pgm} > ${ramp2_pgm}
+    for flag in "-trim=crop"  "-trim=fill"
+    do
+    pamrestack -width=${width} ${flag} ${ramp2_pgm} | cmp -s - ${ramp2_pgm}
+    echo $?
+    done
+done
+
+rm ${ramp_pgm} ${ramp2_pgm}
+
+echo "Test 3.  Should produce 3141273448 431 fifteen times"
+
+# Invert maze.pbm because the lower right corner is black
+
+pixels=`pamfile -size maze.pbm | awk '{print $1 * $2}'`
+
+pnminvert maze.pbm | tee ${maze_inverted_pbm} | \
+  pamrestack | tee ${maze_singlerow_pbm} | pnmcrop -right -black | cksum
+for width in 1 2 3 100 1000 ${pixels} $((pixels -1))
+do
+pamrestack -width=${width} ${maze_inverted_pbm} | pamrestack | \
+  pnmcrop -right -black | cksum
+pamrestack -width=${width} ${maze_singlerow_pbm} | \
+  pamrestack | pnmcrop -right -black | cksum
+done
+
+rm ${maze_inverted_pbm} ${maze_singlerow_pbm}
+
+echo "Test 4. Should produce 1768948962 101484 twice"
+
+pamrestack -width=1 testimg.ppm | pamflip -ccw | cksum
+pamrestack \
+  -width=`pamfile -size testimg.ppm | cut -d " " -f2` testimg.ppm | \
+  pamrestack | cksum
+
+test_out=${tmpdir}/test_out
+echo "Test Invalid."
+
+echo 1>&2
+echo "Invalid command-line argument combinations." 1>&2
+echo "Error messages should appear below the line." 1>&2
+echo "-----------------------------------------------------------" 1>&2
+
+pamrestack testgrid.pbm maze.pbm > ${test_out} || \
+  echo -n "Expected failure 1"
+  test -s ${test_out}; echo " "$?
+  rm -f ${test_out}
+
+pamrestack -abort \
+  -width=$((pixels * 2 + 1 )) maze.pbm > ${test_out} || \
+  echo -n "Expected failure 2"
+  test -s ${test_out}; echo " "$?
+  rm -f ${test_out}
+
+pamrestack -crop \
+  -width=$((pixels * 2 + 1)) maze.pbm > ${test_out} || \
+  echo -n "Expected failure 3"
+  test -s ${test_out}; echo " "$?
+  rm -f ${test_out}
+
+pamrestack -width=0 maze.pbm > ${test_out} || \
+  echo -n "Expected failure 4"
+  test -s ${test_out}; echo " "$?
+  rm -f ${test_out}
+
+pamrestack -width maze.pbm > ${test_out} || \
+  echo -n "Expected failure 5"
+  test -s ${test_out}; echo " "$?
+  rm -f ${test_out}
diff --git a/test/pamscale-filters1.test b/test/pamscale-filters1.test
index 575355c6..c81dcf9a 100755
--- a/test/pamscale-filters1.test
+++ b/test/pamscale-filters1.test
@@ -1,6 +1,6 @@
 #! /bin/bash
 # This script tests: pamscale pamenlarge
-# Also requires: pamvalidate pnmpsnr
+# Also requires: pamfile pamvalidate pnmpsnr
 
 tmpdir=${tmpdir:-/tmp}
 enlarge_ppm=${tmpdir}/enlarge.ppm
diff --git a/test/pamscale-filters2.test b/test/pamscale-filters2.test
index 764488c5..d3cb8d6c 100755
--- a/test/pamscale-filters2.test
+++ b/test/pamscale-filters2.test
@@ -1,6 +1,6 @@
 #! /bin/bash
 # This script tests: pamscale pamstretch pamstretch-gen
-# Also requires: pamvalidate pnmpsnr
+# Also requires: pamfile pamvalidate pnmpsnr
 
 tmpdir=${tmpdir:-/tmp}
 stretch_ppm=${tmpdir}/stretch.ppm
diff --git a/test/pamscale-filters3.test b/test/pamscale-filters3.test
index 23e79373..8d20e690 100755
--- a/test/pamscale-filters3.test
+++ b/test/pamscale-filters3.test
@@ -1,6 +1,6 @@
 #! /bin/bash
 # This script tests: pamscale pamstretch pamstretch-gen 
-# Also requires: pamvalidate pnmpsnr
+# Also requires: pamfile pamvalidate pnmpsnr
 
 tmpdir=${tmpdir:-/tmp}
 stretch_ppm=${tmpdir}/stretch.ppm
diff --git a/test/pamshuffle.ok b/test/pamshuffle.ok
new file mode 100755
index 00000000..4f829744
--- /dev/null
+++ b/test/pamshuffle.ok
@@ -0,0 +1,19 @@
+Test 1.  Should print 1081361896 1432 five times
+1081361896 1432
+1081361896 1432
+1081361896 1432
+1081361896 1432
+1081361896 1432
+Test 2.  Should print 1936883899 143517 four times
+1936883899 143517
+1936883899 143517
+1936883899 143517
+1936883899 143517
+Test 3.  Should print nomatch three times
+nomatch
+nomatch
+nomatch
+Test Invalid.
+Expected failure 1 1
+Expected failure 2 1
+Expected failure 3 1
diff --git a/test/pamshuffle.test b/test/pamshuffle.test
new file mode 100755
index 00000000..5582ce3b
--- /dev/null
+++ b/test/pamshuffle.test
@@ -0,0 +1,67 @@
+#! /bin/sh
+# This script tests: pamshuffle
+# Also requires: pamseq pamrestack pgmhist ppmhist pnmpsnr
+
+tmpdir=${tmpdir:-/tmp}
+seq_pam=${tmpdir}/seq.pam
+seq16_pam=${tmpdir}/seq16.pam
+
+out1_pam=${tmpdir}/out1.pam
+
+echo "Test 1.  Should print 1081361896 1432 five times"
+
+pgmhist -machine maze.pbm | cksum
+pamshuffle -randomseed=1 maze.pbm | pgmhist -machine | cksum
+pamshuffle -randomseed=2 maze.pbm | pgmhist -machine | cksum
+pamshuffle -column -randomseed=3 maze.pbm | pgmhist -machine | cksum
+pamrestack maze.pbm | pamshuffle -randomseed=3 | pgmhist -machine | cksum
+
+echo "Test 2.  Should print 1936883899 143517 four times"
+
+pamseq -tupletype="RGB" 3 15 > ${seq_pam}
+
+ppmhist -map ${seq_pam} | cksum
+pamshuffle -randomseed=2 ${seq_pam} | ppmhist -map | cksum
+pamrestack -width=16 -trim=abort ${seq_pam} | tee ${seq16_pam} | \
+  ppmhist -map | cksum
+pamshuffle -column -randomseed=3 ${seq16_pam} | ppmhist -map | cksum
+
+echo "Test 3.  Should print nomatch three times"
+
+pamshuffle -randomseed=$((100 +i)) ${seq16_pam} > ${out1_pam}
+pamshuffle -randomseed=${i} ${seq16_pam} | \
+pnmpsnr -target=14.0 ${out1_pam} -
+rm ${seq_pam} ${seq16_pam} ${out1_pam}
+
+pamshuffle -randomseed=$((100 +i)) testimg.ppm > ${out1_pam}
+pamshuffle -randomseed=${i} testimg.ppm | \
+  pnmpsnr -target=14.0 ${out1_pam} -
+rm ${out1_pam}
+
+pamshuffle -randomseed=$((100 +i)) -column testimg.ppm > ${out1_pam}
+pamshuffle -randomseed=${i} -column testimg.ppm | \
+  pnmpsnr -target=14.0 ${out1_pam} -
+rm ${out1_pam}
+
+test_out=${tmpdir}/test_out
+echo "Test Invalid."
+
+echo 1>&2
+echo "Invalid command-line argument combinations." 1>&2
+echo "Error messages should appear below the line." 1>&2
+echo "-----------------------------------------------------------" 1>&2
+
+pamshuffle testimg.ppm testgrid.pbm > ${test_out} || \
+  echo -n "Expected failure 1"
+  test -s ${test_out}; echo " "$?
+  rm -f ${test_out}
+
+pamshuffle -randomseed -column testgrid.pbm > ${test_out} || \
+  echo -n "Expected failure 2"
+  test -s ${test_out}; echo " "$?
+  rm -f ${test_out}
+
+pamshuffle -randomseed=null testgrid.pbm > ${test_out} || \
+  echo -n "Expected failure 3"
+  test -s ${test_out}; echo " "$?
+  rm -f ${test_out}
diff --git a/test/pcx-roundtrip.test b/test/pcx-roundtrip.test
index 3a78faba..194af131 100755
--- a/test/pcx-roundtrip.test
+++ b/test/pcx-roundtrip.test
@@ -1,7 +1,6 @@
 #! /bin/sh
 # This script tests: ppmtopcx pcxtoppm
-# Also requires: pnmremap
-
+# Also requires: pgmtoppm pnmremap
 
 tmpdir=${tmpdir:-/tmp}
 pcxstd_ppm=${tmpdir}/pcxstd_ppm
diff --git a/test/pdb-roundtrip.test b/test/pdb-roundtrip.test
index c31a63e1..f56be1bf 100755
--- a/test/pdb-roundtrip.test
+++ b/test/pdb-roundtrip.test
@@ -1,6 +1,6 @@
 #! /bin/bash
 # This script tests: pamtopdbimg pdbimgtopam
-# Also requires: pnmtile pgmramp pamtopnm pamdepth
+# Also requires: pbmnoise pgmramp pamtopnm pamdepth
 
 tmpdir=${tmpdir:-/tmp}
 noise_pbm=${tmpdir}/noise.pbm
diff --git a/test/pnmquantall.test b/test/pnmquantall.test
index fb3beebb..f3594c0f 100755
--- a/test/pnmquantall.test
+++ b/test/pnmquantall.test
@@ -3,45 +3,45 @@
 # Also requires: ppmtorgb3 pgmhist pnmcat
 
 tmpdir=${tmpdir:-/tmp}
-test_ppm=${tmpdir}/testimg.ppm
+rose_ppm=${tmpdir}/rose.ppm
 
-cp testimg.ppm ${tmpdir} &&
-ppmtorgb3 ${test_ppm}
+cp testimg.ppm ${rose_ppm} &&
+ppmtorgb3 ${rose_ppm}
 
-test_red=${tmpdir}/testimg.red
-test_grn=${tmpdir}/testimg.grn
-test_blu=${tmpdir}/testimg.blu
+rose_red=${tmpdir}/rose.red
+rose_grn=${tmpdir}/rose.grn
+rose_blu=${tmpdir}/rose.blu
 
-pnmquantall 20 ${test_red} ${test_grn} ${test_blu}
+pnmquantall 20 ${rose_red} ${rose_grn} ${rose_blu}
 
-for i in ${test_red} ${test_grn} ${test_blu}
+for i in ${rose_red} ${rose_grn} ${rose_blu}
 do
 cat $i | cksum
 done
 
 # Should print 1
 
-pnmcat ${test_red} ${test_grn} ${test_blu} -tb | \
+pnmcat ${rose_red} ${rose_grn} ${rose_blu} -tb | \
     pgmhist -m | awk '$2>0 {s++}; END { print (s<=20) }'
 
 
 tmpdir=${tmpdir:-/tmp}
-test_out=${tmpdir}/test_out
+rose_out=${tmpdir}/rose_out
 
 echo 1>&2
 echo "Invalid command-line argument combinations." 1>&2
 echo "Error messages should appear below the line." 1>&2
 echo "-----------------------------------------------------------" 1>&2
 
-pnmquantall -ext xx 0 ${test_red} ${test_grn} ${test_blu} || \
+pnmquantall -ext xx 0 ${rose_red} ${rose_grn} ${rose_blu} || \
    echo "Expected failure 1"
-rm ${test_red}xx ${test_grn}xx ${test_blu}xx || \
+rm ${rose_red}xx ${rose_grn}xx ${rose_blu}xx || \
    echo "Expected failure 1.rm"
-pnmquantall -ext xx 1 ${test_red} ${test_grn} ${test_blu} || \
+pnmquantall -ext xx 1 ${rose_red} ${rose_grn} ${rose_blu} || \
    echo "Expected failure 2"
-rm ${test_red}xx ${test_grn}xx ${test_blu}xx || \
+rm ${rose_red}xx ${rose_grn}xx ${rose_blu}xx || \
    echo "Expected failure 2.rm"
 pnmquantall -ext xx 2 || \
    echo "Expected failure 3"
 
-rm ${test_red} ${test_grn} ${test_blu} ${test_ppm}
\ No newline at end of file
+rm ${rose_red} ${rose_grn} ${rose_blu} ${rose_ppm}
\ No newline at end of file
diff --git a/test/rgb3-roundtrip.test b/test/rgb3-roundtrip.test
index 5edb1f19..76bd90f1 100755
--- a/test/rgb3-roundtrip.test
+++ b/test/rgb3-roundtrip.test
@@ -11,20 +11,20 @@ tmpdir=${tmpdir:-/tmp}
 
 # Test 1.  PPM (color) input
 echo "Test 1.  Should print 1926073387 101484"
-testimg_ppm=${tmpdir}/testimg.ppm
-testimg_red=${tmpdir}/testimg.red
-testimg_grn=${tmpdir}/testimg.grn
-testimg_blu=${tmpdir}/testimg.blu
+rose_ppm=${tmpdir}/rose.ppm
+rose_red=${tmpdir}/rose.red
+rose_grn=${tmpdir}/rose.grn
+rose_blu=${tmpdir}/rose.blu
 
-cp testimg.ppm ${tmpdir} &&
-ppmtorgb3 ${testimg_ppm} &&
-rgb3toppm ${testimg_red} ${testimg_grn} ${testimg_blu} | cksum
+cp testimg.ppm ${rose_ppm} &&
+ppmtorgb3 ${rose_ppm} &&
+rgb3toppm ${rose_red} ${rose_grn} ${rose_blu} | cksum
 
 # Simple cat of three planes
 echo "Test 2.  Should print 3744829044 101514"
-cat ${testimg_red} ${testimg_grn} ${testimg_blu} | cksum
+cat ${rose_red} ${rose_grn} ${rose_blu} | cksum
 
-rm ${testimg_ppm} ${testimg_red} ${testimg_grn} ${testimg_blu}
+rm ${rose_ppm} ${rose_red} ${rose_grn} ${rose_blu}
 
 # Test 3.  PBM (monochrome) input
 echo "Test 3.  Should print 281226646 481 twice"
diff --git a/test/stdin-pam1.ok b/test/stdin-pam1.ok
index 31f24b3d..e66da036 100644
--- a/test/stdin-pam1.ok
+++ b/test/stdin-pam1.ok
@@ -22,10 +22,12 @@ pammosaicknit: 0 0 0 0
 pamoil: 0 0 0 0
 pamperspective 0 0 0 1 1 0 1 1: 0 0 0 0
 pamrecolor: 0 0 0 0
+pamrestack: 0 0 0 0
 pamrubber -quad 1 1 2 2: 0 0 0 0
 pamscale 2: 0 0 0 0
 pamshadedrelief: 0 0 0 0
 pamsharpness: 0 0 0 0
+pamshuffle -randomseed=1: 0 0 0 0
 pamsistoaglyph: 0 0 0 0
 pamslice -row=1: 0 0 0 0
 pamstack: 0 0 0 0
diff --git a/test/stdin-pam1.test b/test/stdin-pam1.test
index 68d0d34c..1331abd0 100755
--- a/test/stdin-pam1.test
+++ b/test/stdin-pam1.test
@@ -4,8 +4,8 @@
 # This script tests: pamedge pamexec pamfile pamfind pamfix pamflip
 # This script tests: pamfunc pamhomography pamhue pamlevels
 # This script tests: pammixinterlace pammosaicknit pamoil
-# This script tests: pamperspective pamrecolor pamrubber pamscale
-# This script tests: pamshadedrelief pamsharpness pamsistoaglyph
+# This script tests: pamperspective pamrecolor pamrestack pamrubber pamscale
+# This script tests: pamshadedrelief pamsharpness pamshuffle pamsistoaglyph
 # This script tests: pamslice pamstack pamstereogram pamstretch
 # This script tests: pamstretch-gen pamsumm pamsummcol pamtable pamthreshold
 # This script tests: pamtilt pamtopnm pamwipeout
@@ -43,10 +43,12 @@ for testprog in \
     pamoil \
     "pamperspective 0 0 0 1 1 0 1 1" \
     pamrecolor \
+    pamrestack \
     "pamrubber -quad 1 1 2 2" \
     "pamscale 2" \
     pamshadedrelief \
     pamsharpness \
+    "pamshuffle -randomseed=1" \
     pamsistoaglyph \
     "pamslice -row=1" \
     pamstack \
diff --git a/test/stdin-pam3.test b/test/stdin-pam3.test
index 169bb5cd..e8c31781 100755
--- a/test/stdin-pam3.test
+++ b/test/stdin-pam3.test
@@ -16,7 +16,7 @@
 # Also requires: pgmmake
 
 tmpdir=${tmpdir:-/tmp}
-test_pgm=${tmpdir}/test.ppm
+test_pgm=${tmpdir}/test.pgm
 out1=${tmpdir}/out1
 out2=${tmpdir}/out2
 out3=${tmpdir}/out3
diff --git a/test/stdin-pbm2.test b/test/stdin-pbm2.test
index 724a9934..ba585516 100755
--- a/test/stdin-pbm2.test
+++ b/test/stdin-pbm2.test
@@ -4,13 +4,13 @@
 # This script tests: cmuwmtopbm pbmtocmuwm
 # This script tests: escp2topbm pbmtoescp2
 # This script tests: g3topbm pbmtog3
-# This script tests: gemtopbm pbmtogem
+# This script tests: gemtopnm pbmtogem
 # This script tests: macptopbm pbmtomacp
 # This script tests: mdatopbm pbmtomda
 # This script tests: mgrtopbm pbmtomgr
 # This script tests: mrftopbm pbmtomrf
 # This script tests: pi3topbm pbmtopi3
-# This script tests: sunicontopbm pbmtosunicon
+# This script tests: sunicontopnm pbmtosunicon
 # This script tests: wbmptopbm pbmtowbmp
 # This script tests: ybmtopbm pbmtoybm
 # Also requires:
diff --git a/test/winicon-roundtrip.test b/test/winicon-roundtrip.test
index 0acadd53..31fb89aa 100755
--- a/test/winicon-roundtrip.test
+++ b/test/winicon-roundtrip.test
@@ -1,6 +1,7 @@
 #! /bin/sh
 # This script tests: pamtowinicon ppmtowinicon winicontopam
 # Also requires: pamchannel pamcut pamdepth pamtopam
+# Also requires: pngtopam pnmtopng
 
 tmpdir=${tmpdir:-/tmp}
 test_pam=${tmpdir}/testimg.pam
diff --git a/test/winicon-roundtrip2.test b/test/winicon-roundtrip2.test
index eae4ce90..de40446e 100755
--- a/test/winicon-roundtrip2.test
+++ b/test/winicon-roundtrip2.test
@@ -1,6 +1,7 @@
 #! /bin/sh
 # This script tests: pamtowinicon winicontopam
 # Also requires: pamchannel pamdepth pamstack pamtopam pbmmake ppmpat
+# Also requires: pngtopam pnmtopng
 
 tmpdir=${tmpdir:-/tmp}
 test_pam=${tmpdir}/testimg.pam
diff --git a/version.mk b/version.mk
index 36054e93..de759fa2 100644
--- a/version.mk
+++ b/version.mk
@@ -1,3 +1,3 @@
 NETPBM_MAJOR_RELEASE = 10
-NETPBM_MINOR_RELEASE = 98
-NETPBM_POINT_RELEASE = 2
+NETPBM_MINOR_RELEASE = 99
+NETPBM_POINT_RELEASE = 0