about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--analyzer/Makefile1
-rw-r--r--analyzer/pamfile.c248
-rw-r--r--analyzer/pamfind.c222
-rw-r--r--analyzer/pamgetcolor.c52
-rw-r--r--analyzer/pamtable.c9
-rwxr-xr-xbuildtools/installnetpbm.pl209
-rw-r--r--common.mk2
-rw-r--r--converter/other/pamtopng.c395
-rw-r--r--converter/other/pngx.c64
-rw-r--r--converter/other/pngx.h13
-rw-r--r--converter/other/pnmtopng.c383
-rw-r--r--doc/HISTORY62
-rw-r--r--doc/TESTS19
-rw-r--r--editor/Makefile4
-rw-r--r--editor/pambrighten.c271
-rw-r--r--editor/pamcomp.c3
-rw-r--r--editor/pamenlarge.c717
-rw-r--r--editor/pamhue.c209
-rw-r--r--editor/pamscale.c78
-rwxr-xr-xeditor/pamstretch-gen167
-rw-r--r--editor/pamstretch.c364
-rw-r--r--editor/pnmcrop.c715
-rw-r--r--editor/pnmhisteq.c2
-rw-r--r--editor/pnmnorm.c116
-rw-r--r--editor/ppmbrighten.c259
-rw-r--r--generator/pamgradient.c28
-rw-r--r--generator/pamtris/input.c12
-rw-r--r--generator/pgmkernel.c4
-rw-r--r--generator/pgmmake.c2
-rw-r--r--generator/ppmwheel.c309
-rw-r--r--lib/Makefile15
-rw-r--r--lib/colorname.c6
-rw-r--r--lib/libpamcolor.c4
-rw-r--r--lib/libpamn.c17
-rw-r--r--lib/libpgm.h6
-rw-r--r--lib/libpnm3.c293
-rw-r--r--lib/libppmcolor.c6
-rw-r--r--lib/pam.h7
-rw-r--r--lib/pgm.h38
-rw-r--r--lib/pnm.h53
-rw-r--r--lib/ppm.h3
-rw-r--r--lib/util/pm_c_util.h7
-rw-r--r--lib/util/shhopt.h34
-rw-r--r--test/Test-Order13
-rw-r--r--test/all-in-place.ok4
-rwxr-xr-xtest/all-in-place.test4
-rw-r--r--test/pamenlarge-pamscale-point.ok9
-rwxr-xr-xtest/pamenlarge-pamscale-point.test36
-rw-r--r--test/pamenlarge-pbm.ok86
-rwxr-xr-xtest/pamenlarge-pbm.test61
-rw-r--r--test/pamfile.ok7
-rwxr-xr-xtest/pamfile.test7
-rw-r--r--test/pamfind.ok83
-rwxr-xr-xtest/pamfind.test32
-rw-r--r--test/pamscale-filters1.ok15
-rwxr-xr-xtest/pamscale-filters1.test64
-rw-r--r--test/pamscale-filters2.ok24
-rwxr-xr-xtest/pamscale-filters2.test97
-rw-r--r--test/pamscale-filters3.ok7
-rwxr-xr-xtest/pamscale-filters3.test32
-rw-r--r--test/pamscale-reportonly.ok18
-rwxr-xr-xtest/pamscale-reportonly.test35
-rw-r--r--test/pamstretch.ok20
-rwxr-xr-xtest/pamstretch.test48
-rw-r--r--test/ppmbrighten.ok6
-rwxr-xr-xtest/ppmbrighten.test4
-rw-r--r--version.mk4
67 files changed, 4589 insertions, 1555 deletions
diff --git a/analyzer/Makefile b/analyzer/Makefile
index dc7a0cb2..11466662 100644
--- a/analyzer/Makefile
+++ b/analyzer/Makefile
@@ -15,6 +15,7 @@ include $(BUILDDIR)/config.mk
 # build.
 
 PORTBINARIES = \
+  pamfind \
   pamgetcolor \
   pamfile \
   pammosaicknit \
diff --git a/analyzer/pamfile.c b/analyzer/pamfile.c
index a7c3c5ac..7b61f6fd 100644
--- a/analyzer/pamfile.c
+++ b/analyzer/pamfile.c
@@ -10,21 +10,24 @@
 ** implied warranty.
 */
 
+#include <stdbool.h>
 #include "pm_c_util.h"
 #include "mallocvar.h"
 #include "nstring.h"
 #include "shhopt.h"
 #include "pam.h"
 
+enum ReportFormat {RF_HUMAN, RF_COUNT, RF_MACHINE, RF_SIZE};
+
 struct CmdlineInfo {
     /* All the information the user supplied in the command line,
        in a form easy for the program to use.
     */
     int inputFileCount;  /* Number of input files */
     const char ** inputFilespec;  /* Filespecs of input files */
-    unsigned int allimages;  /* -allimages or -count */
-    unsigned int count;      /* -count */
-    unsigned int comments;   /* -comments */
+    unsigned int allimages;
+    unsigned int comments;
+    enum ReportFormat reportFormat;
 };
 
 
@@ -42,35 +45,49 @@ parseCommandLine(int argc, const char ** argv,
     optStruct3 opt;
 
     unsigned int option_def_index;
+    unsigned int countSpec, machineSpec, sizeSpec;
 
     MALLOCARRAY_NOFAIL(option_def, 100);
-    
+
     option_def_index = 0;   /* incremented by OPTENT3 */
     OPTENT3(0,   "allimages", OPT_FLAG,  NULL, &cmdlineP->allimages,   0);
-    OPTENT3(0,   "count",     OPT_FLAG,  NULL, &cmdlineP->count,       0);
+    OPTENT3(0,   "count",     OPT_FLAG,  NULL, &countSpec,             0);
     OPTENT3(0,   "comments",  OPT_FLAG,  NULL, &cmdlineP->comments,    0);
+    OPTENT3(0,   "machine",   OPT_FLAG,  NULL, &machineSpec,           0);
+    OPTENT3(0,   "size",      OPT_FLAG,  NULL, &sizeSpec,              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 */
-    
+    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 */
 
     cmdlineP->inputFilespec = (const char **)&argv[1];
     cmdlineP->inputFileCount = argc - 1;
 
+    if (machineSpec + sizeSpec + countSpec > 1)
+        pm_error("You can specify only one of -machine, -size, and -count");
+    else if (machineSpec)
+        cmdlineP->reportFormat = RF_MACHINE;
+    else if (sizeSpec)
+        cmdlineP->reportFormat = RF_SIZE;
+    else if (countSpec)
+        cmdlineP->reportFormat = RF_COUNT;
+    else
+        cmdlineP->reportFormat = RF_HUMAN;
+
     free(option_def);
 }
 
 
 
 static void
-dumpHeader(struct pam const pam) {
+dumpHeaderHuman(struct pam const pam) {
 
     switch (pam.format) {
     case PAM_FORMAT:
-        printf("PAM, %d by %d by %d maxval %ld\n", 
+        printf("PAM, %d by %d by %d maxval %ld\n",
                pam.width, pam.height, pam.depth, pam.maxval);
         printf("    Tuple type: %s\n", pam.tuple_type);
         break;
@@ -84,22 +101,22 @@ dumpHeader(struct pam const pam) {
         break;
 
 	case PGM_FORMAT:
-        printf("PGM plain, %d by %d  maxval %ld\n", 
+        printf("PGM plain, %d by %d  maxval %ld\n",
                pam.width, pam.height, pam.maxval);
         break;
 
 	case RPGM_FORMAT:
-        printf("PGM raw, %d by %d  maxval %ld\n", 
+        printf("PGM raw, %d by %d  maxval %ld\n",
                pam.width, pam.height, pam.maxval);
         break;
 
 	case PPM_FORMAT:
-        printf("PPM plain, %d by %d  maxval %ld\n", 
+        printf("PPM plain, %d by %d  maxval %ld\n",
                pam.width, pam.height, pam.maxval);
         break;
 
 	case RPPM_FORMAT:
-        printf("PPM raw, %d by %d  maxval %ld\n", 
+        printf("PPM raw, %d by %d  maxval %ld\n",
                pam.width, pam.height, pam.maxval);
         break;
     }
@@ -108,23 +125,79 @@ dumpHeader(struct pam const pam) {
 
 
 static void
+dumpHeaderMachine(struct pam const pam) {
+
+    const char * formatString;
+    bool plain;
+
+    switch (pam.format) {
+    case PAM_FORMAT:
+        formatString = "PAM";
+        plain = false;
+        break;
+
+	case PBM_FORMAT:
+        formatString = "PBM";
+        plain = TRUE;
+        break;
+
+	case RPBM_FORMAT:
+        formatString = "PBM";
+        plain = false;
+        break;
+
+	case PGM_FORMAT:
+        formatString = "PGM";
+        plain = TRUE;
+        break;
+
+	case RPGM_FORMAT:
+        formatString = "PGM";
+        plain = false;
+        break;
+
+	case PPM_FORMAT:
+        formatString = "PPM";
+        plain = TRUE;
+        break;
+
+	case RPPM_FORMAT:
+        formatString = "PPM";
+        plain = false;        break;
+    }
+
+    printf("%s %s %d %d %d %ld %s\n", formatString,
+           plain ? "PLAIN" : "RAW", pam.width, pam.height,
+           pam.depth, pam.maxval, pam.tuple_type);
+
+}
+
+
+static void
+dumpHeaderSize(struct pam const pam) {
+
+    printf("%d %d\n", pam.width, pam.height);
+}
+
+
+static void
 dumpComments(const char * const comments) {
 
     const char * p;
     bool startOfLine;
-    
+
     printf("Comments:\n");
 
     for (p = &comments[0], startOfLine = TRUE; *p; ++p) {
         if (startOfLine)
             printf("  #");
-        
+
         fputc(*p, stdout);
-        
+
         if (*p == '\n')
             startOfLine = TRUE;
         else
-            startOfLine = FALSE;
+            startOfLine = false;
     }
     if (!startOfLine)
         fputc('\n', stdout);
@@ -133,14 +206,36 @@ dumpComments(const char * const comments) {
 
 
 static void
-doOneImage(const char * const name,
-           unsigned int const imageDoneCount,
-           FILE *       const fileP,
-           bool         const allimages,
-           bool         const justCount,
-           bool         const wantComments,
-           bool *       const eofP) {
-                    
+readToNextImage(const struct pam * const pamP,
+                bool *             const eofP) {
+
+    tuple * tuplerow;
+    unsigned int row;
+
+    tuplerow = pnm_allocpamrow(pamP);
+
+    for (row = 0; row < pamP->height; ++row)
+        pnm_readpamrow(pamP, tuplerow);
+
+    pnm_freepamrow(tuplerow);
+
+    {
+        int eof;
+        pnm_nextimage(pamP->file, &eof);
+        *eofP = eof;
+    }
+}
+
+
+static void
+doOneImage(const char *      const name,
+           unsigned int      const imageDoneCt,
+           FILE *            const fileP,
+           enum ReportFormat const reportFormat,
+           bool              const allimages,
+           bool              const wantComments,
+           bool *            const eofP) {
+
     struct pam pam;
     const char * comments;
     enum pm_check_code checkRetval;
@@ -148,76 +243,71 @@ doOneImage(const char * const name,
     pam.comment_p = &comments;
 
     pnm_readpaminit(fileP, &pam, PAM_STRUCT_SIZE(comment_p));
-        
-    if (!justCount) {
+
+    switch (reportFormat) {
+    case RF_COUNT:
+        break;
+    case RF_SIZE:
+        dumpHeaderSize(pam);
+        break;
+    case RF_MACHINE:
+        printf("%s: ", name);
+        dumpHeaderMachine(pam);
+        break;
+    case RF_HUMAN:
         if (allimages)
-            printf("%s:\tImage %d:\t", name, imageDoneCount);
-        else 
+            printf("%s:\tImage %d:\t", name, imageDoneCt);
+        else
             printf("%s:\t", name);
-            
-        dumpHeader(pam);
+
+        dumpHeaderHuman(pam);
+
         if (wantComments)
             dumpComments(comments);
     }
     pm_strfree(comments);
 
     pnm_checkpam(&pam, PM_CHECK_BASIC, &checkRetval);
-    if (allimages) {
-        tuple * tuplerow;
-        unsigned int row;
-        
-        tuplerow = pnm_allocpamrow(&pam);
-        
-        for (row = 0; row < pam.height; ++row) 
-            pnm_readpamrow(&pam, tuplerow);
-        
-        pnm_freepamrow(tuplerow);
-        
-        {
-            int eof;
-            pnm_nextimage(fileP, &eof);
-            *eofP = eof;
-        }
-    }
+
+    if (allimages)
+        readToNextImage(&pam, eofP);
 }
 
 
 
 static void
-describeOneFile(const char * const name,
-                FILE *       const fileP,
-                bool         const allimages,
-                bool         const justCount,
-                bool         const wantComments) {
+describeOneFile(const char *      const name,
+                FILE *            const fileP,
+                enum ReportFormat const reportFormat,
+                bool              const allimages,
+                bool              const wantComments) {
 /*----------------------------------------------------------------------------
    Describe one image stream (file).  Its name, for purposes of display,
    is 'name'.  The file is open as *fileP and positioned to the beginning.
 
+   'reportFormat' tells which of the various sets of information we provide.
+
    'allimages' means report on every image in the stream and read all of
    every image from it, as opposed to reading just the header of the first
    image and reporting just on that.
 
-   'justCount' means don't tell anything about the stream except how
-   many images are in it.  Pretty useless without 'allimages'.
-
    'wantComments' means to show the comments from the image header.
-   Meaningless with 'justCount'.
+   Meaningful only when 'reportFormat' is RF_HUMAN.
 -----------------------------------------------------------------------------*/
-    unsigned int imageDoneCount;
+    unsigned int imageDoneCt;
         /* Number of images we've processed so far */
     bool eof;
-    
-    eof = FALSE;
-    imageDoneCount = 0;
 
-    while (!eof && (imageDoneCount < 1 || allimages)) {
-        doOneImage(name, imageDoneCount, fileP,
-                   allimages, justCount, wantComments,
+    for (eof = false, imageDoneCt = 0;
+         !eof && (imageDoneCt < 1 || allimages);
+        ++imageDoneCt
+        ) {
+        doOneImage(name, imageDoneCt, fileP,
+                   reportFormat, allimages, wantComments,
                    &eof);
-        ++imageDoneCount;
     }
-    if (justCount)
-        printf("%s:\t%u images\n", name, imageDoneCount);
+    if (reportFormat == RF_COUNT)
+        printf("%s:\t%u images\n", name, imageDoneCt);
 }
 
 
@@ -232,19 +322,29 @@ main(int argc, const char *argv[]) {
     parseCommandLine(argc, argv, &cmdline);
 
     if (cmdline.inputFileCount == 0)
-        describeOneFile("stdin", stdin, cmdline.allimages || cmdline.count,
-                        cmdline.count, cmdline.comments);
+        describeOneFile("stdin", stdin, cmdline.reportFormat,
+                        cmdline.allimages ||
+                            cmdline.reportFormat == RF_COUNT,
+                        cmdline.comments);
     else {
         unsigned int i;
         for (i = 0; i < cmdline.inputFileCount; ++i) {
             FILE * ifP;
+
             ifP = pm_openr(cmdline.inputFilespec[i]);
-            describeOneFile(cmdline.inputFilespec[i], ifP, 
-                            cmdline.allimages || cmdline.count,
-                            cmdline.count, cmdline.comments);
+
+            describeOneFile(cmdline.inputFilespec[i], ifP,
+                            cmdline.reportFormat,
+                            cmdline.allimages ||
+                                cmdline.reportFormat == RF_COUNT,
+                            cmdline.comments);
+
             pm_close(ifP);
 	    }
 	}
-    
+
     return 0;
 }
+
+
+
diff --git a/analyzer/pamfind.c b/analyzer/pamfind.c
new file mode 100644
index 00000000..860c01ff
--- /dev/null
+++ b/analyzer/pamfind.c
@@ -0,0 +1,222 @@
+#include <nstring.h>
+#include <pam.h>
+
+#include "pm_c_util.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+
+typedef struct {
+    unsigned int * target;
+    unsigned int   targetDepth;
+    const char *   color;  /* NULL means not specified */
+    const char *   inputFileName;
+} CmdLineInfo;
+
+
+static CmdLineInfo
+parsedCommandLine(int                 argc,
+                  const char ** const argv) {
+
+    optEntry * option_def;
+        /* Instructions to OptParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+
+    CmdLineInfo cmdLine;
+
+    unsigned int targetSpec, colorSpec;
+    const char ** target;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0,   "target",  OPT_STRINGLIST, &target,         &targetSpec, 0);
+    OPTENT3(0,   "color",   OPT_STRING,     &cmdLine.color,  &colorSpec,  0);
+    OPTENT3(0,  0,          OPT_END,        NULL,            NULL,        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, (char **)argv, opt, sizeof(opt), 0);
+
+    if (targetSpec) {
+        if (colorSpec)
+            pm_error("You cannot specify both -target and -color");
+        else {
+            unsigned int i;
+
+            cmdLine.color = NULL;
+
+            cmdLine.target = NULL;  /* initial value */
+
+            for (i = 0, cmdLine.targetDepth = 0; target[i]; ++i) {
+                unsigned int sampleVal;
+                const char * error;
+
+                pm_string_to_uint(target[i], &sampleVal, &error);
+                if (error) {
+                    pm_error("Invalid sample value in -target option: '%s'.  "
+                             "%s", target[i], error);
+                }
+
+                REALLOCARRAY(cmdLine.target, i+1);
+
+                cmdLine.target[cmdLine.targetDepth++] = sampleVal;
+            }
+
+            free(target);
+        }
+    } else if (!colorSpec)
+        pm_error("You must specify either -target or -color");
+
+    if (argc-1 < 1)
+        cmdLine.inputFileName = "-";
+    else {
+        cmdLine.inputFileName = argv[1];
+
+        if (argc-1 > 1)
+            pm_error("Too many arguments: %u.  "
+                     "The only possible argument is the input file name",
+                     argc-1);
+    }
+    free(option_def);
+
+    return cmdLine;
+}
+
+
+
+static void
+freeCmdLine(CmdLineInfo const cmdLine) {
+
+    if (!cmdLine.color)
+        pnm_freepamtuple(cmdLine.target);
+}
+
+
+
+static tuple
+targetValue(CmdLineInfo  const cmdLine,
+            struct pam * const inpamP) {
+/*----------------------------------------------------------------------------
+   The tuple value user wants us to find in the image, per 'cmdLine'.
+
+   The return value is to be interpreted per *inpamP.
+-----------------------------------------------------------------------------*/
+    tuple retval;
+
+    if (cmdLine.color) {
+        if (inpamP->depth != 3)
+            pm_error("You specified -color, but the input image has "
+                     "depth %u, not 3", inpamP->depth);
+        else
+            retval = pnm_parsecolor(cmdLine.color, inpamP->maxval);
+    } else {
+        if (cmdLine.targetDepth != inpamP->depth)
+            pm_error("You specified a %u-tuple for -target, "
+                     "but the input image of of depth %u",
+                     cmdLine.targetDepth, inpamP->depth);
+        else {
+            unsigned int i;
+
+            retval = pnm_allocpamtuple(inpamP);
+
+            for (i = 0; i < inpamP->depth; ++i)
+                retval[i] = cmdLine.target[i];
+        }
+    }
+
+    return retval;
+}
+
+
+
+static void
+printHeader(FILE *       const ofP,
+            struct pam * const inpamP,
+            tuple        const target) {
+
+    unsigned int plane;
+
+    fprintf(ofP, "Locations containing tuple ");
+
+    fprintf(ofP, "(");
+
+    for (plane = 0; plane < inpamP->depth; ++plane) {
+        fprintf(ofP, "%u", (unsigned)target[plane]);
+        if (plane + 1 < inpamP->depth)
+            fprintf(ofP, "/");
+    }
+
+    fprintf(ofP, ")");
+
+    fprintf(ofP, "/%u", (unsigned)inpamP->maxval);
+
+    fprintf(ofP, ":\n");
+}
+
+
+
+static void
+pamfind(FILE *       const ifP,
+        struct pam * const inpamP,
+        CmdLineInfo  const cmdLine,
+        FILE *       const ofP) {
+
+    pnm_readpaminit(ifP, inpamP, PAM_STRUCT_SIZE(tuple_type));
+
+    {
+        tuple * const inputRow = pnm_allocpamrow(inpamP);
+        tuple   const target   = targetValue(cmdLine, inpamP);
+
+        unsigned int row;
+
+        printHeader(ofP, inpamP, target);
+
+        for (row = 0; row < inpamP->height; ++row) {
+            unsigned int col;
+
+            pnm_readpamrow(inpamP, inputRow);
+
+            for (col = 0; col < inpamP->width; ++col) {
+
+                if (pnm_tupleequal(inpamP, target, inputRow[col])) {
+                    fprintf(ofP, "(%u, %u)\n", row, col);
+                }
+            }
+        }
+        pnm_freepamtuple(target);
+        pnm_freepamrow(inputRow);
+    }
+}
+
+
+
+int
+main(int argc, const char *argv[]) {
+
+    FILE * ifP;
+    CmdLineInfo cmdLine;
+    struct pam  inpam;
+
+    pm_proginit(&argc, argv);
+
+    cmdLine = parsedCommandLine(argc, argv);
+
+    ifP = pm_openr(cmdLine.inputFileName);
+
+    pamfind(ifP, &inpam, cmdLine, stdout);
+
+    freeCmdLine(cmdLine);
+
+    pm_close(inpam.file);
+
+    return 0;
+}
+
+
+
diff --git a/analyzer/pamgetcolor.c b/analyzer/pamgetcolor.c
index cd9d2028..430f3b07 100644
--- a/analyzer/pamgetcolor.c
+++ b/analyzer/pamgetcolor.c
@@ -23,7 +23,7 @@ typedef struct {
   Represents a single color measurement over a "region"
 -----------------------------------------------------------------------------*/
     uint         area;     /* area in pixels over which to average the color */
-    /* cumulative normalised intensity-proportiunal value of the region:     */
+    /* cumulative normalized intensity-proportiunal value of the region:     */
     double       color[3];
 } RegData;
 
@@ -136,7 +136,7 @@ parsedRegSpec(const char * const s) {
   's' is of the format x,y[:label].
 -----------------------------------------------------------------------------*/
     char * end;
-    char *start;
+    char * start;
     RegSpec res;
 
     start = (char *)s;
@@ -184,25 +184,26 @@ parseColorFmt(const char * const formatStr,
 
   A format specification string is of format format[:arg].
 -----------------------------------------------------------------------------*/
-    int           const FmtNotFound = -1;
     const char *  const errSpec = "Wrong color format specification: ";
 
     const char *  colonLoc; /* location of the colon in the specification */
     uint          n, f;
     const ColorFormat * formatP;
     uint formatId;
+    bool found;
 
     colonLoc  = strchr(formatStr, ':');
     if (colonLoc != NULL) n = colonLoc - formatStr;
     else                  n = strlen(formatStr);
 
-    for (f = 0, formatId = FmtNotFound;
-         f < ARRAY_SIZE(formats) && formatId == FmtNotFound; ++f) {
-        if (strncmp(formatStr, formats[f].id, n) == 0)
+    for (f = 0, found = false; f < ARRAY_SIZE(formats) && !found; ++f) {
+        if (strncmp(formatStr, formats[f].id, n) == 0) {
+            found = true;
             formatId = f;
+        }
     }
-    if (formatId == FmtNotFound)
-        pm_error("Color format not recognised.");
+    if (!found)
+        pm_error("Color format not recognized.");
 
     *formatIdP = formatId;
 
@@ -326,20 +327,22 @@ freeCommandLine(CmdLineInfo const cmdLine) {
 
 
 
-static RegData * allocRegSamples(uint n) {
+static RegData *
+allocRegSamples(uint n) {
 /*----------------------------------------------------------------------------
-  Allocate an array of <n> initialised region samles.  The array should be
+  Allocate an array of <n> initialized region samples.  The array should be
   freed after use.
 -----------------------------------------------------------------------------*/
     uint r;
     RegData * regSamples;
     regSamples = calloc(n, sizeof(RegData));
-    for (r = 0; r < n; r++) {
+
+    for (r = 0; r < n; ++r) {
         uint l;
 
         regSamples[r].area = 0;
 
-        for (l = 0; l < 3; l++)
+        for (l = 0; l < 3; ++l)
             regSamples[r].color[l] = 0.0;
     }
     return regSamples;
@@ -347,14 +350,16 @@ static RegData * allocRegSamples(uint n) {
 
 
 
-static uint getYmax(struct pam * const pamP,
-                    CmdLineInfo  const cmdLine) {
+static uint
+getYmax(struct pam * const pamP,
+        CmdLineInfo  const cmdLine) {
 /*----------------------------------------------------------------------------
   Find the maximum row in the image that contains a pixel from a region.
 -----------------------------------------------------------------------------*/
     uint ymax, r, ycmax;
     ycmax = 0;
-    for (r = 0; r < cmdLine.regN; r++) {
+
+    for (r = 0; r < cmdLine.regN; ++r) {
         RegSpec spec = cmdLine.regSpecs[r];
         if (spec.y >= pamP->height || spec.x >= pamP->width)
             pm_error("Region at %i,%i is outside the image boundaries.",
@@ -363,9 +368,8 @@ static uint getYmax(struct pam * const pamP,
         if (spec.y > ycmax)
             ycmax = spec.y;
     }
-    ymax = ycmax + cmdLine.radius;
-    if (ymax > pamP->height - 1)
-        ymax = pamP->height - 1;
+    ymax = MIN(pamP->height - 1, ycmax + cmdLine.radius);
+
     return ymax;
 }
 
@@ -405,11 +409,11 @@ readChord(RegData *    const dataP,
 
 
 static void
-processRow(tuple *      const   row,
-           uint         const   y,
-           struct pam * const   pamP,
-           CmdLineInfo  const * cmdLineP,
-           RegData *    const   regSamples) {
+processRow(tuple *             const row,
+           uint                const y,
+           struct pam *        const pamP,
+           const CmdLineInfo * const cmdLineP,
+           RegData *           const regSamples) {
 /*----------------------------------------------------------------------------
   Read a row from image described by *pamP into 'row', and update region
   samples regSamples[] from it.  'y' is the position of the row.
@@ -512,7 +516,7 @@ printColors(struct pam *    const pamP,
             const RegData * const regSamples) {
 /*----------------------------------------------------------------------------
   Print the colors regSamples[] to *ofP in the format
-  requested by 'cmdLine'. 
+  requested by 'cmdLine'.
 
   *pamP tells how to interpret regSamples[]
 -----------------------------------------------------------------------------*/
diff --git a/analyzer/pamtable.c b/analyzer/pamtable.c
index 8ed245fd..2835469a 100644
--- a/analyzer/pamtable.c
+++ b/analyzer/pamtable.c
@@ -55,7 +55,7 @@ parseCommandLine(int argc, const char ** const argv,
 
     if (argc-1 < 1)
         cmdlineP->inputFileName = "-";
-    else 
+    else
         cmdlineP->inputFileName = argv[1];
 
     free(option_def);
@@ -90,7 +90,7 @@ sampleFormat(const struct pam * const pamP) {
     unsigned int const decimalWidth = ROUNDU(ceil(log10(pamP->maxval + 1)));
 
     const char * retval;
-    
+
     pm_asprintf(&retval, "%%%uu", decimalWidth);
 
     return retval;
@@ -192,6 +192,9 @@ main(int argc, const char *argv[]) {
     printRaster(ifP, &inpam, stdout);
 
     pm_close(inpam.file);
-    
+
     return 0;
 }
+
+
+
diff --git a/buildtools/installnetpbm.pl b/buildtools/installnetpbm.pl
index e9bcd231..d29fda1a 100755
--- a/buildtools/installnetpbm.pl
+++ b/buildtools/installnetpbm.pl
@@ -8,6 +8,7 @@ use strict;
 use English;
 use Fcntl;
 use File::Basename;
+use Cwd qw(getcwd);
 
 my ($TRUE, $FALSE) = (1,0);
 
@@ -621,9 +622,194 @@ sub installSharedLib($$$) {
 
 
 
-sub getLinkDir($) {
+sub getSharedLinkDir($) {
 #-----------------------------------------------------------------------------
-#  Find out from the user where he wants the link-edit libraries installed and
+#  Find out from the user where he wants the shared library stubs installed
+#  and return that.
+#-----------------------------------------------------------------------------
+    my ($prefix) = @_;
+
+    print("Where do you want the shared library stub (used to link-edit\n" .
+          "programs to use the shared lirary) installed?\n");
+    print("\n");
+
+    my $linkDir;
+
+    while (!$linkDir) {
+        my $default = "$prefix/lib";
+
+        my $response = fsObjPrompt("shared library stub directory", $default);
+        
+        if (-d($response)) {
+            $linkDir = $response;
+        } else {
+            my $succeeded = mkdir($response, 0777);
+            
+            if (!$succeeded) {
+                print("Unable to create directory '$response'.  " .
+                      "Error=$ERRNO\n");
+            } else {
+                $linkDir = $response;
+            }
+        }
+    }
+    print("\n");
+
+    return $linkDir;
+}
+
+
+
+sub removeDotDirs($) {
+
+    my ($readDirResultR) = @_;
+
+    my @dirContents;
+
+    foreach (@{$readDirResultR}) {
+        if ($_ ne '.' && $_ ne '..') {
+            push(@dirContents, $_);
+        }
+    }
+
+    return \@dirContents;
+}
+
+
+
+sub readDirContents($$$) {
+    my ($dirName, $contentsRR, $errorR) = @_;
+#-----------------------------------------------------------------------------
+#  Return the contents of the directory named $dirName, excluding the
+#  fake . and .. entries.
+#-----------------------------------------------------------------------------
+    my $dirContentsR;
+    my $error;
+
+    my $success = opendir(DIR, $dirName);
+
+    if (!$success) {
+        $error = "Unable to open directory '$dirName' with opendir()";
+    } else {
+        my @readDirResult = readdir(DIR);
+
+        $dirContentsR = removeDotDirs(\@readDirResult);
+
+        closedir(DIR);
+    }
+
+    $$contentsRR = $dirContentsR;
+
+    if ($errorR) {
+        $$errorR = $error;
+    }
+}
+
+
+
+sub dirContents($) {
+    my ($dirName) = @_;
+#-----------------------------------------------------------------------------
+#  Return the contents of the directory named $dirName, excluding the
+#  fake . and .. entries.
+#-----------------------------------------------------------------------------
+
+    readDirContents($dirName, \my $contentsR, \my $error);
+
+    if ($error) {
+        die($error);
+    }
+    return @{$contentsR};
+}
+
+
+
+sub fixSharedStubSymlink($$) {
+#-----------------------------------------------------------------------------
+#  This is a hack to install a shared library link on a GNU system.
+#
+# On systems that use the GNU dynamic linker, the shared library stub (the
+# file one uses at link-edit time to tell the linker what it needs to know
+# about the shared library that the code will use at run time) is just a
+# symbolic link to a copy of the actual shared library.  In the Netpbm
+# package, this is a relative symbolic link to the shared library in the
+# package.
+
+# Assuming Caller just copied the contents of the 'sharedlink' directory
+# straight from the package to the install target system, that symbolic link
+# isn't necessarily correct, and even if it is, it's probably messy.  (In the
+# normal case, the link value is ../lib/libnetpbm.so.<MAJ>).
+
+# So what we do is just detect and patch up that case.  If the stub is a
+# symbolic link to something in the shared library directory of the package,
+# we replace it with a symbolic link to the same thing in the shared library
+# directory of the install target system.
+# -----------------------------------------------------------------------------
+    my ($linkDir, $shlibDir) = @_;
+
+    my $oldCwd = getcwd();
+    chdir($linkDir);
+
+    foreach my $fsObjNm (dirContents('.')) {
+        if (-l("$fsObjNm")) {
+            if (readlink($fsObjNm) =~ m{^\.\./lib/(.*)$}) {
+                my $shlibNm = $1;
+
+                unlink($fsObjNm) or
+                    die("Failed to delete symlink copied from package " .
+                        "in order to replace it with a proper symlink " .
+                        "for this installation");
+
+                if ($linkDir eq $shlibDir) {
+                    symlink($shlibNm, $fsObjNm) or
+                        die("Failed to create symlink as shared library stub");
+                } else {
+                    symlink("$shlibDir/$shlibNm", $fsObjNm) or
+                        die("Failed to create symlink as shared library stub");
+                }
+                    
+                print("Linked $shlibDir/$shlibNm from $linkDir/$fsObjNm");
+            }
+        }
+    }
+    chdir($oldCwd);
+}
+
+
+
+sub installSharedStub($$$$) {
+
+    my ($pkgdir, $prefix, $shlibDir, $linkdirR) = @_;
+
+    if (-d("$pkgdir/sharedlink")) {
+        my $linkDir = getSharedLinkDir($prefix);
+
+        print("Installing shared library stubs.\n");
+
+        my $rc = system("$cpCommand $pkgdir/sharedlink/* $linkDir/");
+
+        if ($rc != 0) {
+            print("Copy of files from $pkgdir/sharedlink " .
+                  "to $linkDir failed.\n");
+            print("cp return code is $rc\n");
+        } else {
+            fixSharedStubSymlink($linkDir, $shlibDir);
+
+            print("done.\n");
+        }
+        $$linkdirR = $linkDir;
+    } else {
+        print("You did not build a shared library, so I will not " .
+              "install a stub \n");
+        $$linkdirR = undef;
+    }
+}
+
+
+
+sub getStaticLinkDir($) {
+#-----------------------------------------------------------------------------
+#  Find out from the user where he wants the static  libraries installed and
 #  return that.
 #-----------------------------------------------------------------------------
     my ($prefix) = @_;
@@ -662,15 +848,16 @@ sub installStaticLib($$$) {
 
     my ($pkgdir, $prefix, $linkdirR) = @_;
 
-    if (-d("$pkgdir/link")) {
-        my $linkDir = getLinkDir($prefix);
+    if (-d("$pkgdir/staticlink")) {
+        my $linkDir = getStaticLinkDir($prefix);
 
-        print("Installing link libraries.\n");
+        print("Installing static link libraries.\n");
 
-        my $rc = system("$cpCommand $pkgdir/link/* $linkDir/");
+        my $rc = system("$cpCommand $pkgdir/staticlink/* $linkDir/");
 
         if ($rc != 0) {
-            print("Copy of files from $pkgdir/link to $linkDir failed.\n");
+            print("Copy of files from $pkgdir/staticlink " .
+                  "to $linkDir failed.\n");
             print("cp return code is $rc\n");
         } else {
             print("done.\n");
@@ -679,6 +866,7 @@ sub installStaticLib($$$) {
     } else {
         print("You did not build a static library, so I will not " .
               "install one \n");
+        $$linkdirR = undef;
     }
 }
 
@@ -1037,7 +1225,10 @@ print("\n");
 installSharedLib($pkgdir, $prefix, \my $libdir);
 print("\n");
 
-installStaticLib($pkgdir, $prefix, \my $linkdir);
+installSharedStub($pkgdir, $prefix, $libdir, \my $sharedlinkdir);
+print("\n");
+
+installStaticLib($pkgdir, $prefix, \my $staticlinkdir);
 print("\n");
 
 installDataFile($pkgdir, $prefix, \my $datadir);
@@ -1046,6 +1237,8 @@ print("\n");
 installHeader($pkgdir, $prefix, \my $includedir);
 print("\n");
 
+my $linkdir = defined($sharedlinkdir) ? $sharedlinkdir : $staticlinkdir;
+
 my $templateSubsR =
     {VERSION    => netpbmVersion($pkgdir),
      BINDIR     => $bindir,
diff --git a/common.mk b/common.mk
index 4bfdb6a1..749488c2 100644
--- a/common.mk
+++ b/common.mk
@@ -482,7 +482,7 @@ endif
 
 PKGMANSUBDIRS = man1 man3 man5 web
 
-PKGSUBDIRS = bin include include/netpbm lib link misc \
+PKGSUBDIRS = bin include include/netpbm lib sharedlink staticlink misc \
   $(PKGMANSUBDIRS:%=$(PKGMANDIR)/%)
 
 $(PKGSUBDIRS:%=$(PKGDIR)/%):
diff --git a/converter/other/pamtopng.c b/converter/other/pamtopng.c
index a3c60f24..a323844f 100644
--- a/converter/other/pamtopng.c
+++ b/converter/other/pamtopng.c
@@ -1,21 +1,8 @@
-/*
-** read a PNM/PAM image and produce a Portable Network Graphics (PNG) file
-**
-** derived from pnmtorast.c by Jef Poskanzer and pamrgbatopng.c by Bryan
-** Henderson <bryanh@giraffe-data.com> and probably some other sources
-**
-** Copyright (C) 1995-1998 by Alexander Lehmann <alex@hal.rhein-main.de>
-**                        and Willem van Schaik <willem@schaik.com>
-** Copyright (C) 1999,2001 by Greg Roelofs <newt@pobox.com>
-** Copyright (C) 2015 by Willem van Schaik <willem@schaik.com>
-**
-** Permission to use, copy, modify, and distribute this software and its
-** documentation for any purpose and without fee is hereby granted, provided
-** that the above copyright notice appear in all copies and that both that
-** copyright notice and this permission notice appear in supporting
-** documentation.  This software is provided "as is" without express or
-** implied warranty.
-*/
+/*=============================================================================
+                                  pamtopng
+===============================================================================
+  Read a Netpbm image and produce a PNG (Portable Network Graphics) image.
+=============================================================================*/
 
 /*
   This Netpbm program pamtopng was derived in 2015 from the Netpbm program
@@ -29,16 +16,16 @@
     had become rather complex.  This program is roughly 1/3 the size of
     pnmtopng.c that it replaces.
 
-  - In 1995 bandwith was limited and therefore filesize had to be kept
+  - In 1995, bandwith was limited and therefore filesize had to be kept
     small. The original program tried to optimize for that by applying
-    many "clever tricks". Today that isn't an issue anymore, so gone 
+    many "clever tricks". Today that isn't an issue anymore, so gone
     are filters, palettes, etc. Also, image conversions were removed,
     because those should be done with other NetPBM tools.
 
   - Add ability to create iTXt (international language) chunks.
 */
 
-
+#include <assert.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <time.h>
@@ -62,6 +49,7 @@ static bool verbose;
 struct CmdlineInfo {
     const char * inputFileName;
     unsigned int verbose;
+    unsigned int interlace;
     unsigned int transparencySpec;
     const char * transparency;
     unsigned int chromaSpec;
@@ -89,7 +77,7 @@ parseChromaOpt(const char *         const chromaOpt,
                struct pngx_chroma * const chromaP) {
 
     int count;
-    
+
     count = sscanf(chromaOpt, "%f %f %f %f %f %f %f %f",
                    &chromaP->wx, &chromaP->wy,
                    &chromaP->rx, &chromaP->ry,
@@ -108,7 +96,7 @@ parseChromaOpt(const char *         const chromaOpt,
 static void
 parseSrgbintentOpt(const char *      const srgbintentOpt,
                    pngx_srgbIntent * const srgbintentP) {
-    
+
     if (streq(srgbintentOpt, "perceptual"))
         *srgbintentP = PNGX_PERCEPTUAL;
     else if (streq(srgbintentOpt, "relativecolorimetric"))
@@ -146,7 +134,7 @@ parseTimeOpt(const char * const timeOpt,
     if (count != 6)
         pm_error("Invalid value for -time '%s'.   It should have "
                  "the form [yy]yy-mm-dd hh:mm:ss.", timeOpt);
-    
+
     if (year < 0)
         pm_error("Year is negative in -time value '%s'", timeOpt);
     if (year > 9999)
@@ -193,7 +181,7 @@ static void
 parseCommandLine (int                  argc,
                   const char **        argv,
                   struct CmdlineInfo * const cmdlineP) {
-    
+
     optEntry * option_def;
     optStruct3 opt;
     unsigned int option_def_index = 0;  /* incremented by OPTENT3 */
@@ -206,6 +194,8 @@ parseCommandLine (int                  argc,
 
     OPTENT3(0,  "verbose",      OPT_FLAG,       NULL,
             &cmdlineP->verbose,        0);
+    OPTENT3(0,  "interlace",    OPT_FLAG,       NULL,
+            &cmdlineP->interlace,      0);
     OPTENT3(0,  "transparency", OPT_STRING,     &cmdlineP->transparency,
             &cmdlineP->transparencySpec, 0);
     OPTENT3(0,  "chroma",       OPT_STRING,     &chroma,
@@ -240,7 +230,7 @@ parseCommandLine (int                  argc,
 
     if (cmdlineP->timeSpec)
         parseTimeOpt(time, &cmdlineP->time);
-    
+
     /* get the input-file or stdin pipe */
     if (argc-1 < 1)
         cmdlineP->inputFileName = "-";
@@ -257,7 +247,7 @@ parseCommandLine (int                  argc,
 static png_byte
 colorTypeFromInputType(const struct pam * const pamP) {
 /*----------------------------------------------------------------------------
-  Analyse the Netpbm image for color-type and bit-depth
+  Analyze the Netpbm image for color-type and bit-depth
 -----------------------------------------------------------------------------*/
     png_byte retval;
 
@@ -287,7 +277,7 @@ colorTypeFromInputType(const struct pam * const pamP) {
         if (pamP->depth == 2)
             retval = PNG_COLOR_TYPE_GRAY_ALPHA;
         else
-            pm_error("Input tupel type is GRAYSCALE_ALPHA, "
+            pm_error("Input tuple type is GRAYSCALE_ALPHA, "
                      "but number of planes is %u instread of 2",
                      pamP->depth);
     } else if (strneq(pamP->tuple_type, "GRAYSCALE", 9)) {
@@ -386,6 +376,8 @@ sigBitsFmImgType(unsigned int const pnmBitDepth,
             retval.gray  = pnmBitDepth;
             retval.alpha = pnmBitDepth;
             break;
+        default:
+            assert(false);
         }
     } else {
         /* PNG can (so presumably will) use original bit depth */
@@ -405,7 +397,7 @@ doTrnsChunk(const struct pam * const pamP,
             struct pngx *      const pngxP,
             const char *       const trans) {
 
-    if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_GRAY_ALPHA || 
+    if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_GRAY_ALPHA ||
         pngx_colorType(pngxP) == PNG_COLOR_TYPE_RGB_ALPHA)
         pm_error("Both alpha channel and transparency chunk not allowed.");
     else {
@@ -435,13 +427,13 @@ doTrnsChunk(const struct pam * const pamP,
 static void
 doChrmChunk(struct pngx *      const pngxP,
             struct pngx_chroma const chroma) {
-    
+
     pngx_setChrm(pngxP, chroma);
 
     if (verbose) {
         pm_message("writing cHRM chunk { wx, wy, rx, ry, gx, gy, bx, by } = "
                    "{ %4.2f, %4.2f, %4.2f, %4.2f, "
-                   "%4.2f, %4.2f, %4.2f, %4.2f }", 
+                   "%4.2f, %4.2f, %4.2f, %4.2f }",
                    chroma.wx, chroma.wy,
                    chroma.rx, chroma.ry,
                    chroma.gx, chroma.gy,
@@ -451,7 +443,7 @@ doChrmChunk(struct pngx *      const pngxP,
 
 
 
-static void 
+static void
 doGamaChunk(struct pngx *  const pngxP,
             float          const gamma) {
 
@@ -501,9 +493,9 @@ doTextChunkSet(struct pngx * const pngxP,
     FILE * tfP;
 
     tfP = pm_openr(textFileName);
-    
+
     pngtxt_addChunk(pngxP, tfP, ztxt, itxt, verbose);
-    
+
     pm_close(tfP);
 }
 
@@ -519,9 +511,9 @@ doZtxtChunkSet(struct pngx * const pngxP,
     FILE * tfP;
 
     tfP = pm_openr(textFileName);
-    
+
     pngtxt_addChunk(pngxP, tfP, ztxt, itxt, verbose);
-    
+
     pm_close(tfP);
 }
 
@@ -557,11 +549,11 @@ doBkgdChunk (const struct pam * const pamP,
     pngx_setBkgdRgb(pngxP, pngColor);
 
     if (verbose) {
-        if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_GRAY || 
+        if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_GRAY ||
             pngx_colorType(pngxP) == PNG_COLOR_TYPE_GRAY_ALPHA) {
             pm_message("writing bKGD chunk with gray level = %u",
                        pngColor.gray);
-        } else if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_RGB || 
+        } else if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_RGB ||
                    pngx_colorType(pngxP) == PNG_COLOR_TYPE_RGB_ALPHA) {
             pm_message("writing bKGD chunk with color {red, green, blue} = "
                        "{%u, %u, %u}",
@@ -592,24 +584,10 @@ doTimeChunk(struct pngx * const pngxP,
 
 
 static void
-setShift(struct pngx * const pngxP,
-         png_color_8   const sigBits) {
-
-    if (sigBits.red + sigBits.green + sigBits.blue +
-        sigBits.gray + sigBits.alpha > 0) {
-
-        /* Move the 1, 2, 4 bits to most significant bits */
-        pngx_setShift(pngxP, sigBits);
-    }
-}
-
-
-
-static void
-convertRaster(const struct pam * const pamP,
-              const tuple *      const tuplerow,
-              png_byte *         const pngRow,
-              unsigned int       const bitDepth) {
+convertRow(const struct pam * const pamP,
+           const tuple *      const tuplerow,
+           png_byte *         const pngRow,
+           unsigned int       const bitDepth) {
 
     unsigned int col;
 
@@ -638,9 +616,12 @@ convertRaster(const struct pam * const pamP,
 
 
 static void
-writeRaster(const struct pam * const pamP,
-            struct pngx *      const pngxP,
-            int                const bitDepth) {
+writeRasterRowByRow(const struct pam * const pamP,
+                    struct pngx *      const pngxP,
+                    int                const bitDepth) {
+
+    unsigned int const rowSz =
+        pamP->width * pamP->depth * (MAX(1, bitDepth/8));
 
     tuple * tupleRow;
     png_byte * pngRow;
@@ -650,8 +631,7 @@ writeRaster(const struct pam * const pamP,
 
     tupleRow = pnm_allocpamrow(pamP);
 
-    MALLOCARRAY(pngRow, pamP->width * 8);
-        /* sufficient to store a 16-bit RGB+A row */
+    MALLOCARRAY(pngRow, rowSz);
 
     if (pngRow == NULL)
         pm_error("Unable to allocate space for PNG pixel row for "
@@ -660,9 +640,9 @@ writeRaster(const struct pam * const pamP,
         for (row = 0; row < pamP->height; ++row) {
             pnm_readpamrow(pamP, tupleRow);
 
-            convertRaster(pamP, tupleRow, pngRow, bitDepth);
+            convertRow(pamP, tupleRow, pngRow, bitDepth);
 
-            png_write_row(pngxP->png_ptr, pngRow);
+            pngx_writeRow(pngxP, pngRow);
         }
         free(pngRow);
     }
@@ -671,40 +651,160 @@ writeRaster(const struct pam * const pamP,
 
 
 
+static png_bytep
+mallocPngImage(unsigned int const rowSize,
+               unsigned int const height) {
+
+    png_bytep pngImage;
+
+    if (UINT_MAX / rowSize < height)
+        pm_error("Image is uncomputably large at %u rows of %u bytes",
+                 height, rowSize);
+
+    MALLOCARRAY(pngImage, height * rowSize);
+
+    if (!pngImage)
+        pm_error("could not allocate %u bytes for a PNG image buffer",
+                 height * rowSize);
+
+    return pngImage;
+}
+
+
+
+static unsigned int
+pngLineSize(struct pngx * const pngxP) {
+
+    unsigned int const bytesPerSample = pngx_bitDepth(pngxP) == 16 ? 2 : 1;
+
+    unsigned int samplesPerPixel;
+
+    switch (pngx_colorType(pngxP)) {
+        case PNG_COLOR_TYPE_GRAY:
+            samplesPerPixel = 1;
+            break;
+        case PNG_COLOR_TYPE_GRAY_ALPHA:
+            samplesPerPixel = 2;
+            break;
+        case PNG_COLOR_TYPE_RGB:
+            samplesPerPixel = 3;
+            break;
+        case PNG_COLOR_TYPE_RGB_ALPHA:
+            samplesPerPixel = 4;
+            break;
+        default:
+            assert(false);
+    }
+
+    if (UINT_MAX / bytesPerSample / samplesPerPixel < pngx_imageWidth(pngxP)) {
+        pm_error("pngcopy: width %u of PNG is uncomputably large\n",
+                  pngx_imageWidth(pngxP));
+    }
+
+    return pngx_imageWidth(pngxP) * bytesPerSample * samplesPerPixel;
+}
+
+
+
 static void
-writePng(const struct pam * const pamP,
-         FILE *             const ofP,
-         struct CmdlineInfo const cmdline) {
+writeRasterWholeImg(struct pam *  const pamP,
+                    struct pngx * const pngxP,
+                    unsigned int  const bitDepth) {
 
-    unsigned int const pnmBitDepth = pm_maxvaltobits(pamP->maxval);
-    int const pngColorType = colorTypeFromInputType(pamP);
+    unsigned int const pngRowSize = pngLineSize(pngxP);
 
-    struct pngx * pngxP;
-    unsigned int pngBitDepth;
-    png_color_8 sBit;
+    tuple * tupleRow;
+    png_bytep pngImage;
+        /* A one-dimensional malloc'ed array of all pixels in image */
+    png_bytep * pngRowP;
+        /* A malloc'ed array of row pointers into pngImage[] */
+    unsigned int row;
 
-    pngx_create(&pngxP, PNGX_WRITE, NULL);
+    tupleRow = pnm_allocpamrow(pamP);
+
+    pngImage = mallocPngImage(pngRowSize, pamP->height);
+
+    MALLOCARRAY(pngRowP, pamP->height);
+
+    if (!pngRowP)
+        pm_error("Failed to allocate an array for %u PNG row pointers",
+                 pamP->height);
 
+    for (row = 0; row < pamP->height; ++row) {
+        png_bytep const thisPngRowP = &pngImage[row * pngRowSize];
 
+        pnm_readpamrow(pamP, tupleRow);
+
+        convertRow(pamP, tupleRow, thisPngRowP, bitDepth);
+
+        pngRowP[row] = thisPngRowP;
+    }
+
+    pngx_writeImage(pngxP, pngRowP);
+
+    free(pngRowP);
+    free(pngImage);
+    pnm_freepamrow(tupleRow);
+}
+
+
+
+static void
+reportInputFormat(const struct pam * const pamP) {
+
+    const char * formatDesc;
+
+    if (pamP->format == PBM_FORMAT || pamP->format == RPBM_FORMAT)
+        formatDesc = "PBM";
+    else if (pamP->format == PGM_FORMAT || pamP->format == RPGM_FORMAT)
+        formatDesc = "PGM";
+    else if (pamP->format == PPM_FORMAT || pamP->format == RPPM_FORMAT)
+        formatDesc = "PPM";
+    else if (pamP->format == PAM_FORMAT)
+        formatDesc = "PAM";
+    else
+        formatDesc = NULL;
+
+    if (formatDesc)
+        pm_message("Input format = %s", formatDesc);
+    else
+        pm_message("Unrecognized input format, format code = 0x%x",
+                   pamP->format);
+
+    pm_message("Input tuple type = '%s'", pamP->tuple_type);
+    pm_message("Input depth = %u", pamP->depth);
+    pm_message("Input maxval = %u", (unsigned int) pamP->maxval);
+}
+
+
+
+static unsigned int
+pngBitDepth(unsigned int const pnmBitDepth,
+            int          const pngColorType) {
+
+    unsigned int retval;
 
     if ((pngColorType == PNG_COLOR_TYPE_RGB ||
          pngColorType == PNG_COLOR_TYPE_RGB_ALPHA) &&
         pnmBitDepth < 8) {
 
-        pngBitDepth = 8;
+        retval = 8;
     } else
-        pngBitDepth = pnmBitDepth;
+        retval = pnmBitDepth;
 
-    png_init_io(pngxP->png_ptr, ofP);
+    return retval;
+}
 
-    pngx_setIhdr(pngxP, pamP->width, pamP->height,
-                 pngBitDepth, pngColorType,
-                 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE,
-                 PNG_FILTER_TYPE_BASE);
 
-    sBit = sigBitsFmImgType(pnmBitDepth, pngColorType);
 
-    /* Where requested, add ancillary chunks */
+static void
+addAncillaryChunks(struct pam *       const pamP,
+                   struct pngx *      const pngxP,
+                   struct CmdlineInfo const cmdline,
+                   png_color_8        const sigBits) {
+/*----------------------------------------------------------------------------
+  Where requested, add ancillary chunks.
+-----------------------------------------------------------------------------*/
     if (cmdline.transparencySpec)
         doTrnsChunk(pamP, pngxP,cmdline.transparency);
 
@@ -716,7 +816,7 @@ writePng(const struct pam * const pamP,
 
     /* no iccp */
 
-    doSbitChunk(pamP, pngxP, sBit);
+    doSbitChunk(pamP, pngxP, sigBits);
 
     if (cmdline.srgbintentSpec)
         doSrgbChunk(pngxP, cmdline.srgbintent);
@@ -742,54 +842,96 @@ writePng(const struct pam * const pamP,
     if (cmdline.timeSpec)
         doTimeChunk(pngxP, cmdline.time);
 
-    setShift(pngxP, sBit);
-
-    /* Write the ancillary chunks to PNG file */
+    /* Write the ancillary chunks to PNG image */
     pngx_writeInfo(pngxP);
+}
+
+
+
+static void
+setShift(struct pngx * const pngxP,
+         png_color_8   const sigBits) {
+
+    if (sigBits.red + sigBits.green + sigBits.blue +
+        sigBits.gray + sigBits.alpha > 0) {
 
-    if (pngColorType != PNG_COLOR_TYPE_GRAY && pnmBitDepth < 8) {
         /* Move the 1, 2, 4 bits to most significant bits */
-        pngx_setShift(pngxP, sBit);
-    }
-    if ((pngColorType == PNG_COLOR_TYPE_GRAY) && (pnmBitDepth < 8)) {
-        /* Pack multiple pixels in a byte */
-        pngx_setPacking(pngxP);
+        pngx_setShift(pngxP, sigBits);
     }
+}
 
-    writeRaster(pamP, pngxP, pnmBitDepth);
 
-    pngx_writeEnd(pngxP);
-    pngx_destroy(pngxP);
-}
 
+static void
+doIhdrChunk(struct pngx * const pngxP,
+            unsigned int  const width,
+            unsigned int  const height,
+            unsigned int  const pnmBitDepth,
+            int           const pngColorType,
+            bool          const interlace) {
+
+    int const interlaceMethod =
+        interlace ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE;
+
+    pngx_setIhdr(pngxP, width, height,
+                 pngBitDepth(pnmBitDepth, pngColorType), pngColorType,
+                 interlaceMethod, PNG_COMPRESSION_TYPE_BASE,
+                 PNG_FILTER_TYPE_BASE);
+}
 
 
 
 static void
-reportInputFormat(const struct pam * const pamP) {
+pamtopng(FILE *             const ifP,
+         FILE *             const ofP,
+         struct CmdlineInfo const cmdline) {
 
-    const char * formatDesc;
+    unsigned int  pnmBitDepth;
+    int           pngColorType;
+    struct pngx * pngxP;
+    png_color_8   sigBits;
+    struct pam    pam;
 
-    if (pamP->format == PBM_FORMAT || pamP->format == RPBM_FORMAT)
-        formatDesc = "PBM";
-    else if (pamP->format == PGM_FORMAT || pamP->format == RPGM_FORMAT)
-        formatDesc = "PGM";
-    else if (pamP->format == PPM_FORMAT || pamP->format == RPPM_FORMAT)
-        formatDesc = "PPM";
-    else if (pamP->format == PAM_FORMAT)
-        formatDesc = "PAM";
-    else
-        formatDesc = NULL;
+    pnm_readpaminit(ifP, &pam, PAM_STRUCT_SIZE(tuple_type));
 
-    if (formatDesc)
-        pm_message("Input format = %s", formatDesc);
-    else
-        pm_message("Unrecognized input format, format code = 0x%x",
-                   pamP->format);
+    if (verbose)
+        reportInputFormat(&pam);
 
-    pm_message("Input tuple type = '%s'", pamP->tuple_type);
-    pm_message("Input depth = %u", pamP->depth);
-    pm_message("Input maxval = %u", (unsigned int) pamP->maxval);
+    pnmBitDepth = pm_maxvaltobits(pam.maxval);
+
+    pngColorType = colorTypeFromInputType(&pam);
+
+    pngx_create(&pngxP, PNGX_WRITE, NULL);
+
+    png_init_io(pngxP->png_ptr, ofP);
+
+    doIhdrChunk(pngxP, pam.width, pam.height,
+                pnmBitDepth, pngColorType, cmdline.interlace > 0);
+
+    sigBits = sigBitsFmImgType(pnmBitDepth, pngColorType);
+
+    addAncillaryChunks(&pam, pngxP, cmdline, sigBits);
+
+    setShift(pngxP, sigBits);
+
+    if ((pngColorType == PNG_COLOR_TYPE_GRAY) && (pnmBitDepth < 8)) {
+        /* Pack multiple pixels in a byte */
+        pngx_setPacking(pngxP);
+    }
+
+    if (cmdline.interlace) {
+        /* Libpng will expect us to provide pixels in interlaced sequence
+           if we write row-by-row, and that is much to difficult, so we
+           do whole-image-at-once and let Libpng do the work.
+        */
+        writeRasterWholeImg(&pam, pngxP, pnmBitDepth);
+    } else {
+        /* We save memory by going row-by-row */
+        writeRasterRowByRow(&pam, pngxP, pnmBitDepth);
+    }
+
+    pngx_writeEnd(pngxP);
+    pngx_destroy(pngxP);
 }
 
 
@@ -800,7 +942,6 @@ main(int           argc,
 
     FILE * ifP;
     struct CmdlineInfo cmdline;
-    struct pam pam;
 
     pm_proginit(&argc, argv);
 
@@ -810,12 +951,7 @@ main(int           argc,
 
     ifP = pm_openr(cmdline.inputFileName);
 
-    pnm_readpaminit(ifP, &pam, PAM_STRUCT_SIZE(tuple_type));
-
-    if (verbose)
-        reportInputFormat(&pam);
-
-    writePng(&pam, stdout, cmdline);
+    pamtopng(ifP, stdout, cmdline);
 
     pm_close(ifP);
 
@@ -824,3 +960,18 @@ main(int           argc,
 
 
 
+/* Derived from pnmtorast.c by Jef Poskanzer and pamrgbatopng.c by Bryan
+** Henderson <bryanh@giraffe-data.com> and probably some other sources
+**
+** Copyright (C) 1995-1998 by Alexander Lehmann <alex@hal.rhein-main.de>
+**                        and Willem van Schaik <willem@schaik.com>
+** Copyright (C) 1999,2001 by Greg Roelofs <newt@pobox.com>
+** Copyright (C) 2015 by Willem van Schaik <willem@schaik.com>
+**
+** Permission to use, copy, modify, and distribute this software and its
+** documentation for any purpose and without fee is hereby granted, provided
+** that the above copyright notice appear in all copies and that both that
+** copyright notice and this permission notice appear in supporting
+** documentation.  This software is provided "as is" without express or
+** implied warranty.
+*/
diff --git a/converter/other/pngx.c b/converter/other/pngx.c
index dfc74485..4bb09421 100644
--- a/converter/other/pngx.c
+++ b/converter/other/pngx.c
@@ -1,3 +1,4 @@
+#include <stdbool.h>
 #include <assert.h>
 #include <png.h>
 #include "pm_c_util.h"
@@ -59,6 +60,7 @@ pngx_create(struct pngx ** const pngxPP,
     if (!pngxP)
         pm_error("Failed to allocate memory for PNG object");
     else {
+        pngxP->infoPrepared = false;
         pngxP->numPassesRequired = 1;
 
         switch(rw) {
@@ -466,6 +468,37 @@ pngx_setIhdr(struct pngx * const pngxP,
 
 void
 pngx_setInterlaceHandling(struct pngx * const pngxP) {
+    /* The documentation is vague and contradictory on what this does, but
+       what it appears from reasoning and experimentation to do is the
+       following.
+
+       It applies to reading and writing by rows (png_write_row, png_read_row)
+       as opposed to whole image (png_write_image, png_read_image).  It has
+       no effect on whole image read and write.
+
+       This is not what makes an image interlaced or tells the decompressor
+       that it is interlaced.  All it does is control how you you read and
+       write the raster when the image is interlaced.  It has no effect if the
+       image is not interlaced.  (You make an image interlaced by setting the
+       IHDR; the decompressor finds out from the IHDR that it is interlaced).
+
+       In the write case, it controls whether you construct the subimages
+       yourself and feed them to libpng in sequence or you feed libpng the
+       entire image multiple times and libpng picks out the pixels appropriate
+       for each subimage in each pass.
+
+       In the read case, it controls whether you get the raw subimages and you
+       assemble them into the full image or you read the whole image multiple
+       times into the same buffer, with the pixels that belong to each
+       subimage being filled in on each pass.
+
+       Note that the only kind of interlacing that exists today is ADAM7 and
+       consequently, the number of passes is always 1 (for no interlacing) or
+       7 (for interlacing).
+    */
+    if (!pngxP->infoPrepared)
+        pm_error("pngx_setInterlaceHandling must not be called before "
+                 "pngx_writeInfo or pngx_readInfo");
 
     pngxP->numPassesRequired = png_set_interlace_handling(pngxP->png_ptr);
 }
@@ -475,6 +508,10 @@ pngx_setInterlaceHandling(struct pngx * const pngxP) {
 void
 pngx_setPacking(struct pngx * const pngxP) {
 
+    if (!pngxP->infoPrepared)
+        pm_error("pngx_setPacking must not be called before "
+                 "pngx_writeInfo or pngx_readInfo");
+
     png_set_packing(pngxP->png_ptr);
 }
 
@@ -516,7 +553,19 @@ pngx_setSbit(struct pngx * const pngxP,
 void
 pngx_setShift(struct pngx * const pngxP,
               png_color_8   const sigBitArg) {
-
+/*----------------------------------------------------------------------------
+   Tell the number of significant bits in the row buffers that will be given
+   to the compressor.  Those bits are the least significant of the 8 bits of
+   space in the row buffer for each sample.  For example, if red sample values
+   are in the range 0-7, only the lower 3 bits of the 8-bit byte for each
+   red sample will be used, so one would call this with sigBitArg.red == 3.
+
+   The name alludes to the fact that to normalize the sample to 8 bits, one
+   shifts it left, and this function tells how much shift has to happen.  In
+   the example above, each red sample has to be shifted left 5 bits (so that
+   the upper 3 bits are significant and the lower 5 bits are always zero) to
+   create an 8 bit sample out of the 3 bit samples.
+-----------------------------------------------------------------------------*/
     png_color_8 sigBit;
 
     sigBit = sigBitArg;
@@ -651,6 +700,8 @@ void
 pngx_readInfo(struct pngx * const pngxP) {
 
     png_read_info(pngxP->png_ptr, pngxP->info_ptr);
+
+    pngxP->infoPrepared = true;
 }
 
 
@@ -659,6 +710,8 @@ void
 pngx_writeInfo(struct pngx * const pngxP) {
 
     png_write_info(pngxP->png_ptr, pngxP->info_ptr);
+
+    pngxP->infoPrepared = true;
 }
 
 
@@ -733,6 +786,15 @@ pngx_writeRow(struct pngx *    const pngxP,
 
 
 void
+pngx_writeImage(struct pngx * const pngxP,
+                png_byte **   const raster) {
+
+    png_write_image(pngxP->png_ptr, (png_byte **)raster);
+}
+
+
+
+void
 pngx_readEnd(struct pngx * const pngxP) {
 
     /* Note that some of info_ptr is not defined until png_read_end()
diff --git a/converter/other/pngx.h b/converter/other/pngx.h
index be9b9558..f4701bb2 100644
--- a/converter/other/pngx.h
+++ b/converter/other/pngx.h
@@ -1,6 +1,7 @@
 #ifndef PNGX_H_INCLUDED
 #define PNGX_H_INCLUDED
 
+#include <stdbool.h>
 #include <png.h>
     /* This includes the Zlib interface header file zlib.h because libpng uses
        libz and some of the Zlib interface, e.g. the Z_DEFLATED constant,
@@ -53,10 +54,16 @@ struct pngx {
     pngx_rw      rw;
     png_uint_16  maxval;
     unsigned int numPassesRequired;
-        /* The number of times we have write the complete image to the
+        /* The number of times we have to write the complete image to the
            compressor.  This is more than one when the compressor is set
            up to do an interlaced format.
         */
+    bool         infoPrepared;
+        /* png_write_info or png_read_info has been called, so libpng is in a
+           state in which things such as png_set_interlace_handling will work.
+           These functions use information in *png_ptr that is set by
+           png_XXX_info.
+        */
 };
 
 void
@@ -270,6 +277,10 @@ pngx_writeRow(struct pngx *    const pngxP,
               const png_byte * const line);
 
 void
+pngx_writeImage(struct pngx * const pngxP,
+                png_byte **   const raster);
+
+void
 pngx_readEnd(struct pngx * const pngxP);
 
 void
diff --git a/converter/other/pnmtopng.c b/converter/other/pnmtopng.c
index 08263f29..80f74adb 100644
--- a/converter/other/pnmtopng.c
+++ b/converter/other/pnmtopng.c
@@ -26,7 +26,7 @@
    it's easy to see that an ordinary fax could deplete your virtual
    memory and even if it didn't, it might deplete your real memory and
    iterating through the array would cause thrashing.  This program
-   iterates through the image multiple times.  
+   iterates through the image multiple times.
 
    So instead, we read the image into memory one row at a time, into a
    single row buffer.  We use Netpbm's pm_openr_seekable() facility to
@@ -38,19 +38,19 @@
    in the system's I/O cache (remember that the file is a lot smaller than
    the xel array you'd get by doing a pnm_readpnm() of it).
 
-   However, it does introduce some delay because of all the system calls 
+   However, it does introduce some delay because of all the system calls
    required to read the file.  A future enhancement might read the entire
-   file into an xel array in some cases, and read one row at a time in 
+   file into an xel array in some cases, and read one row at a time in
    others, depending on the needs of the particular use.
 
    We do still read the entire alpha mask (if there is one) into a
-   'gray' array, rather than access it one row at a time.  
+   'gray' array, rather than access it one row at a time.
 
    Before May 2001, we did in fact read the whole image into an xel array,
    and we got complaints.  Before April 2000, it wasn't as big a problem
    because xels were only 24 bits.  Now they're 96.
 */
-   
+
 #ifndef PNMTOPNG_WARNING_LEVEL
 #  define PNMTOPNG_WARNING_LEVEL 0   /* use 0 for backward compatibility, */
 #endif                               /*  2 for warnings (1 == error) */
@@ -62,7 +62,7 @@
 /* Because of a design error in png.h, you must not #include <setjmp.h> before
    <png.h>.  If you do, png.h won't compile.
 */
-#include <setjmp.h> 
+#include <setjmp.h>
 #include <zlib.h>
 
 #include "pm_c_util.h"
@@ -169,7 +169,7 @@ parseSizeOpt(const char *       const sizeOpt,
              struct pngx_phys * const sizeP) {
 
     int count;
-    
+
     count = sscanf(sizeOpt, "%d %d %d", &sizeP->x, &sizeP->y, &sizeP->unit);
 
     if (count != 3)
@@ -184,7 +184,7 @@ parseRgbOpt(const char *         const rgbOpt,
             struct pngx_chroma * const rgbP) {
 
     int count;
-    
+
     count = sscanf(rgbOpt, "%f %f %f %f %f %f %f %f",
                    &rgbP->wx, &rgbP->wy,
                    &rgbP->rx, &rgbP->ry,
@@ -203,7 +203,7 @@ parseRgbOpt(const char *         const rgbOpt,
 static void
 parseSrgbintentOpt(const char *      const srgbintentOpt,
                    pngx_srgbIntent * const srgbintentP) {
-    
+
     if (streq(srgbintentOpt, "perceptual"))
         *srgbintentP = PNGX_PERCEPTUAL;
     else if (streq(srgbintentOpt, "relativecolorimetric"))
@@ -241,7 +241,7 @@ parseModtimeOpt(const char * const modtimeOpt,
     if (count != 6)
         pm_error("Invalid value for -modtime '%s'.   It should have "
                  "the form [yy]yy-mm-dd hh:mm:ss.", modtimeOpt);
-    
+
     if (year < 0)
         pm_error("Year is negative in -modtime value '%s'", modtimeOpt);
     if (year > 9999)
@@ -289,7 +289,7 @@ parseCommandLine(int argc, const char ** argv,
                  struct cmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
    parse program command line described in Unix standard form by argc
-   and argv.  Return the information in the options as *cmdlineP.  
+   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.
@@ -421,11 +421,11 @@ parseCommandLine(int argc, const char ** argv,
         cmdlineP->ztxt = NULL;
     if (!paletteSpec)
         cmdlineP->palette = NULL;
-    
+
     if (filterSpec + nofilter + sub + up + avg + paeth > 1)
         pm_error("You may specify at most one of "
                  "-nofilter, -sub, -up, -avg, -paeth, and -filter");
-    
+
     if (filterSpec) {
         if (filter < 0 || filter > 4)
             pm_error("-filter is obsolete.  Use -nofilter, -sub, -up, -avg, "
@@ -452,13 +452,13 @@ parseCommandLine(int argc, const char ** argv,
         else
             cmdlineP->filterSet = PNG_FILTER_NONE;
     }
-    
+
     if (cmdlineP->sizeSpec)
         parseSizeOpt(size, &cmdlineP->size);
 
     if (cmdlineP->rgbSpec)
         parseRgbOpt(rgb, &cmdlineP->rgb);
-    
+
     if (cmdlineP->srgbintentSpec)
         parseSrgbintentOpt(srgbintent, &cmdlineP->srgbintent);
 
@@ -530,14 +530,14 @@ reportInputType(int    const format,
 
 
 static png_color_16
-xelToPngColor_16(xel    const input, 
-                 xelval const maxval, 
+xelToPngColor_16(xel    const input,
+                 xelval const maxval,
                  xelval const pngMaxval) {
 
     png_color_16 retval;
 
     xel scaled;
-    
+
     PPM_DEPTH(scaled, input, maxval, pngMaxval);
 
     retval.red   = PPM_GETR(scaled);
@@ -551,12 +551,12 @@ xelToPngColor_16(xel    const input,
 
 
 static void
-closestColorInPalette(pixel          const targetColor, 
+closestColorInPalette(pixel          const targetColor,
                       pixel                palette_pnm[],
                       unsigned int   const paletteSize,
                       unsigned int * const bestIndexP,
                       unsigned int * const bestMatchP) {
-    
+
     unsigned int paletteIndex;
     unsigned int bestIndex;
     unsigned int bestMatch;
@@ -565,7 +565,7 @@ closestColorInPalette(pixel          const targetColor,
 
     bestMatch = UINT_MAX;
     for (paletteIndex = 0; paletteIndex < paletteSize; ++paletteIndex) {
-        unsigned int const dist = 
+        unsigned int const dist =
             PPM_DISTANCE(palette_pnm[paletteIndex], targetColor);
 
         if (dist < bestMatch) {
@@ -670,13 +670,13 @@ static colorhist_vector getChv_chv;
 
 
 static void
-getChv(FILE *             const ifP, 
+getChv(FILE *             const ifP,
        pm_filepos         const rasterPos,
-       int                const cols, 
-       int                const rows, 
+       int                const cols,
+       int                const rows,
        xelval             const maxval,
-       int                const format, 
-       int                const maxColors, 
+       int                const format,
+       int                const maxColors,
        colorhist_vector * const chvP,
        unsigned int *     const colorsP) {
 /*----------------------------------------------------------------------------
@@ -685,7 +685,7 @@ getChv(FILE *             const ifP,
    raster starts at position 'rasterPos' of the file.  The image's properties
    are 'cols', 'rows', 'maxval', and 'format'.
 
-   Return the number of colors as *colorsP.  Return the details of the 
+   Return the number of colors as *colorsP.  Return the details of the
    colors in newly malloc'ed storage, and its address as *chvP.  If
    there are more than 'maxColors' colors, though, just return NULL as
    *chvP and leave *colorsP undefined.
@@ -700,13 +700,13 @@ getChv(FILE *             const ifP,
 
     if (!getChv_computed) {
         int colorCount;
-        if (verbose) 
+        if (verbose)
             pm_message ("Finding colors in input image...");
 
         pm_seek2(ifP, &rasterPos, sizeof(rasterPos));
-        getChv_chv = ppm_computecolorhist2(ifP, cols, rows, maxval, format, 
+        getChv_chv = ppm_computecolorhist2(ifP, cols, rows, maxval, format,
                                            maxColors, &colorCount);
-        
+
         getChv_colors = colorCount;
 
         if (verbose) {
@@ -737,7 +737,7 @@ static void freeChv(void) {
 static bool
 pgmBitsAreRepeated(unsigned int const repeatedSize,
                    FILE *       const ifP,
-                   pm_filepos   const rasterPos, 
+                   pm_filepos   const rasterPos,
                    int          const cols,
                    int          const rows,
                    xelval       const maxval,
@@ -764,7 +764,7 @@ pgmBitsAreRepeated(unsigned int const repeatedSize,
     xel * xelrow;
 
     xelrow = pnm_allocrow(cols);
-    
+
     pm_seek2(ifP, &rasterPos, sizeof(rasterPos));
 
     mayscale = TRUE;  /* initial assumption */
@@ -789,8 +789,8 @@ pgmBitsAreRepeated(unsigned int const repeatedSize,
 
 
 static void
-meaningful_bits_pgm(FILE *         const ifP, 
-                    pm_filepos     const rasterPos, 
+meaningful_bits_pgm(FILE *         const ifP,
+                    pm_filepos     const rasterPos,
                     int            const cols,
                     int            const rows,
                     xelval         const maxval,
@@ -841,8 +841,8 @@ meaningful_bits_pgm(FILE *         const ifP,
 
 
 static void
-meaningful_bits_ppm(FILE *         const ifp, 
-                    pm_filepos     const rasterPos, 
+meaningful_bits_ppm(FILE *         const ifp,
+                    pm_filepos     const rasterPos,
                     int            const cols,
                     int            const rows,
                     xelval         const maxval,
@@ -895,12 +895,12 @@ meaningful_bits_ppm(FILE *         const ifp,
 
 
 static void
-tryTransparentColor(FILE *     const ifp, 
-                    pm_filepos const rasterPos, 
-                    int        const cols, 
-                    int        const rows, 
+tryTransparentColor(FILE *     const ifp,
+                    pm_filepos const rasterPos,
+                    int        const cols,
+                    int        const rows,
                     xelval     const maxval,
-                    int        const format, 
+                    int        const format,
                     gray **    const alphaMask,
                     gray       const alphaMaxval,
                     pixel      const transcolor,
@@ -918,7 +918,7 @@ tryTransparentColor(FILE *     const ifp,
     pm_seek2(ifp, &rasterPos, sizeof(rasterPos));
 
     singleColorIsTrans = TRUE;  /* initial assumption */
-        
+
     for (row = 0; row < rows && singleColorIsTrans; ++row) {
         int col;
         pnm_readpnmrow(ifp, xelrow, cols, maxval, format);
@@ -953,7 +953,7 @@ tryTransparentColor(FILE *     const ifp,
                 }
             }
         }
-    }  
+    }
     pnm_freerow(xelrow);
 }
 
@@ -961,15 +961,15 @@ tryTransparentColor(FILE *     const ifp,
 
 static void
 analyzeAlpha(FILE *       const ifP,
-             pm_filepos   const rasterPos, 
-             unsigned int const cols, 
-             unsigned int const rows, 
+             pm_filepos   const rasterPos,
+             unsigned int const cols,
+             unsigned int const rows,
              xelval       const maxval,
-             int          const format, 
+             int          const format,
              gray **      const alphaMask,
              gray         const alphaMaxval,
              bool *       const allOpaqueP,
-             bool *       const singleColorIsTransP, 
+             bool *       const singleColorIsTransP,
              pixel *      const alphaTranscolorP) {
 /*----------------------------------------------------------------------------
   Get information about the alpha mask, in combination with the masked
@@ -995,7 +995,7 @@ analyzeAlpha(FILE *       const ifP,
         /* We found a pixel in the image where the alpha mask says it is
            not fully opaque.
         */
-    
+
     xelrow = pnm_allocrow(cols);
 
     {
@@ -1075,7 +1075,7 @@ determineTransparency(struct cmdlineInfo const cmdline,
    or identify a transparent color.
 
    We return as *transparentP:
-   
+
      -1 PNG is not to have single-color transparency
       1 PNG is to have single-color transparency as directed by user
       2 PNG is to have single-color transparency that effects an alpha
@@ -1099,7 +1099,7 @@ determineTransparency(struct cmdlineInfo const cmdline,
         if (alphaCols != cols || alphaRows != rows) {
             pm_error("dimensions for image and alpha mask do not agree");
         }
-        analyzeAlpha(ifP, rasterPos, cols, rows, maxval, format, 
+        analyzeAlpha(ifP, rasterPos, cols, rows, maxval, format,
                      alphaMask, alphaMaxval, &allOpaque,
                      &alphaCanBeTransparencyIndex, &alphaTranscolor);
 
@@ -1129,7 +1129,7 @@ determineTransparency(struct cmdlineInfo const cmdline,
         *alphaMaxvalP = 255;
 
         if (cmdline.transparent) {
-            const char * transstring2;  
+            const char * transstring2;
             /* The -transparent value, but with possible leading '=' removed */
             if (cmdline.transparent[0] == '=') {
                 *transExactP = TRUE;
@@ -1137,12 +1137,12 @@ determineTransparency(struct cmdlineInfo const cmdline,
             } else {
                 *transExactP = FALSE;
                 transstring2 = cmdline.transparent;
-            }  
+            }
             /* We do this funny PPM_DEPTH thing instead of just passing 'maxval'
                to ppm_parsecolor() because ppm_parsecolor() does a cheap maxval
                scaling, and this is more precise.
             */
-            PPM_DEPTH(*transColorP, 
+            PPM_DEPTH(*transColorP,
                       ppm_parsecolor(transstring2, PNM_OVERALLMAXVAL),
                       PNM_OVERALLMAXVAL, maxval);
 
@@ -1159,9 +1159,9 @@ determineBackground(struct cmdlineInfo const cmdline,
                     xelval             const maxval,
                     xel *              const backColorP) {
 
-  if (cmdline.background) 
+  if (cmdline.background)
       PPM_DEPTH(*backColorP,
-                ppm_parsecolor(cmdline.background, PNM_OVERALLMAXVAL), 
+                ppm_parsecolor(cmdline.background, PNM_OVERALLMAXVAL),
                 PNM_OVERALLMAXVAL, maxval);;
 }
 
@@ -1213,8 +1213,8 @@ hasColor(FILE *       const ifP,
 
 
 static void
-findRedundantBits(FILE *         const ifp, 
-                  int            const rasterPos, 
+findRedundantBits(FILE *         const ifp,
+                  int            const rasterPos,
                   int            const cols,
                   int            const rows,
                   xelval         const maxval,
@@ -1231,13 +1231,13 @@ findRedundantBits(FILE *         const ifp,
    of bits, starting from the least significant end, that contain
    original information.
 -----------------------------------------------------------------------------*/
-  if (!alpha && PNM_FORMAT_TYPE(format) == PGM_TYPE && !force) 
+  if (!alpha && PNM_FORMAT_TYPE(format) == PGM_TYPE && !force)
       meaningful_bits_pgm(ifp, rasterPos, cols, rows, maxval, format,
                           meaningfulBitsP);
   else if (PNM_FORMAT_TYPE(format) == PPM_TYPE && !force)
       meaningful_bits_ppm(ifp, rasterPos, cols, rows, maxval, format,
                           meaningfulBitsP);
-  else 
+  else
       *meaningfulBitsP = pm_maxvaltobits(maxval);
 
   if (verbose && *meaningfulBitsP != pm_maxvaltobits(maxval))
@@ -1249,24 +1249,24 @@ findRedundantBits(FILE *         const ifp,
 
 static void
 readOrderedPalette(FILE *         const pfp,
-                   xel                  ordered_palette[], 
+                   xel                  ordered_palette[],
                    unsigned int * const ordered_palette_size_p) {
 
     xel ** xels;
     int cols, rows;
     xelval maxval;
     int format;
-    
+
     if (verbose)
         pm_message("reading ordered palette (colormap)...");
 
     xels = pnm_readpnm(pfp, &cols, &rows, &maxval, &format);
-    
-    if (PNM_FORMAT_TYPE(format) != PPM_TYPE) 
+
+    if (PNM_FORMAT_TYPE(format) != PPM_TYPE)
         pm_error("ordered palette must be a PPM file, not type %d", format);
 
     *ordered_palette_size_p = rows * cols;
-    if (*ordered_palette_size_p > MAXCOLORS) 
+    if (*ordered_palette_size_p > MAXCOLORS)
         pm_error("ordered-palette image contains %d pixels.  Maximum is %d",
                  *ordered_palette_size_p, MAXCOLORS);
     if (verbose)
@@ -1278,12 +1278,12 @@ readOrderedPalette(FILE *         const pfp,
         j = 0;  /* initial value */
         for (row = 0; row < rows; ++row) {
             int col;
-            for (col = 0; col < cols; ++col) 
+            for (col = 0; col < cols; ++col)
                 ordered_palette[j++] = xels[row][col];
         }
     }
     pnm_freearray(xels, rows);
-}        
+}
 
 
 
@@ -1307,31 +1307,31 @@ compute_nonalpha_palette(colorhist_vector const chv,
    wants the colors in a particular order in the palette.
 -----------------------------------------------------------------------------*/
     unsigned int colorIndex;
-    
+
     xel ordered_palette[MAXCOLORS];
     unsigned int ordered_palette_size;
 
     if (pfp) {
         readOrderedPalette(pfp, ordered_palette, &ordered_palette_size);
 
-        if (colors != ordered_palette_size) 
+        if (colors != ordered_palette_size)
             pm_error("sizes of ordered palette (%d) "
                      "and existing palette (%d) differ",
                      ordered_palette_size, colors);
-        
+
         /* Make sure the ordered palette contains all the colors in
-           the image 
+           the image
         */
         for (colorIndex = 0; colorIndex < colors; colorIndex++) {
             int j;
             bool found;
-            
+
             found = FALSE;
             for (j = 0; j < ordered_palette_size && !found; ++j) {
-                if (PNM_EQUAL(ordered_palette[j], chv[colorIndex].color)) 
+                if (PNM_EQUAL(ordered_palette[j], chv[colorIndex].color))
                     found = TRUE;
             }
-            if (!found) 
+            if (!found)
                 pm_error("failed to find color (%d, %d, %d), which is in the "
                          "input image, in the ordered palette",
                          PPM_GETR(chv[colorIndex].color),
@@ -1344,7 +1344,7 @@ compute_nonalpha_palette(colorhist_vector const chv,
         for (colorIndex = 0; colorIndex < colors; ++colorIndex)
             palette_pnm[colorIndex] = ordered_palette[colorIndex];
     } else {
-        for (colorIndex = 0; colorIndex < colors; ++colorIndex) 
+        for (colorIndex = 0; colorIndex < colors; ++colorIndex)
             palette_pnm[colorIndex] = chv[colorIndex].color;
     }
     *paletteSizeP = colors;
@@ -1434,7 +1434,7 @@ computeUnsortedAlphaPalette(FILE *           const ifP,
    described by 'cols', 'rows', 'maxval', and 'format'.
 
    Using the alpha mask 'alpha_mask' and color map 'chv' (of size 'colors')
-   for the image, construct a palette of (color index, alpha) ordered pairs 
+   for the image, construct a palette of (color index, alpha) ordered pairs
    for the image, as follows.
 
    The alpha/color palette is the set of all ordered pairs of
@@ -1468,7 +1468,7 @@ computeUnsortedAlphaPalette(FILE *           const ifP,
         alphasOfColor[colorIndex] = NULL;
         alphasOfColorCnt[colorIndex] = 0;
     }
- 
+
     pm_seek2(ifP, &rasterPos, sizeof(rasterPos));
 
     xelrow = pnm_allocrow(cols);
@@ -1554,22 +1554,22 @@ sortAlphaPalette(gray *         const alphasOfColor[],
         unsigned int bot_idx;
         unsigned int top_idx;
         unsigned int colorIndex;
-    
+
         /* We start one index at the bottom of the palette index range
            and another at the top.  We run through the unsorted palette,
            and when we see an opaque entry, we map it to the current top
-           cursor and bump it down.  When we see a non-opaque entry, we map 
+           cursor and bump it down.  When we see a non-opaque entry, we map
            it to the current bottom cursor and bump it up.  Because the input
            and output palettes are the same size, the two cursors should meet
            right when we process the last entry of the unsorted palette.
-        */    
+        */
         bot_idx = 0;
         top_idx = alphasFirstIndex[colors-1] + alphasOfColorCnt[colors-1] - 1;
-    
+
         for (colorIndex = 0;  colorIndex < colors;  ++colorIndex) {
             unsigned int j;
             for (j = 0; j < alphasOfColorCnt[colorIndex]; ++j) {
-                unsigned int const paletteIndex = 
+                unsigned int const paletteIndex =
                     alphasFirstIndex[colorIndex] + j;
                 if (alphasOfColor[colorIndex][j] == alphaMaxval)
                     mapping[paletteIndex] = top_idx--;
@@ -1590,7 +1590,7 @@ sortAlphaPalette(gray *         const alphasOfColor[],
 
 
 static void
-compute_alpha_palette(FILE *         const ifP, 
+compute_alpha_palette(FILE *         const ifP,
                       int            const cols,
                       int            const rows,
                       xelval         const maxval,
@@ -1618,10 +1618,10 @@ compute_alpha_palette(FILE *         const ifP,
    The palette is sorted so that the opaque entries are last, and we return
    *transSizeP as the number of non-opaque entries.
 
-   palette[] and trans[] are allocated by the caller to at least 
+   palette[] and trans[] are allocated by the caller to at least
    MAXPALETTEENTRIES elements.
 
-   If there are more than MAXPALETTEENTRIES color/alpha pairs in the image, 
+   If there are more than MAXPALETTEENTRIES color/alpha pairs in the image,
    don't return any palette information -- just return *tooBigP == TRUE.
 -----------------------------------------------------------------------------*/
     colorhist_vector chv;
@@ -1630,8 +1630,8 @@ compute_alpha_palette(FILE *         const ifP,
     gray * alphas_of_color[MAXPALETTEENTRIES];
     unsigned int alphas_first_index[MAXPALETTEENTRIES];
     unsigned int alphas_of_color_cnt[MAXPALETTEENTRIES];
- 
-    getChv(ifP, rasterPos, cols, rows, maxval, format, MAXCOLORS, 
+
+    getChv(ifP, rasterPos, cols, rows, maxval, format, MAXCOLORS,
            &chv, &colors);
 
     assert(colors <= ARRAY_SIZE(alphas_of_color));
@@ -1650,7 +1650,7 @@ compute_alpha_palette(FILE *         const ifP,
                index into the sorted PNG palette of the alpha/color
                pair whose index is x in the unsorted PNG palette.
                This mapping sorts the palette so that opaque entries
-               are last.  
+               are last.
             */
 
         *paletteSizeP = colors == 0 ?
@@ -1669,22 +1669,22 @@ compute_alpha_palette(FILE *         const ifP,
             for (colorIndex = 0; colorIndex < colors; ++colorIndex) {
                 unsigned int j;
                 for (j = 0; j < alphas_of_color_cnt[colorIndex]; ++j) {
-                    unsigned int const paletteIndex = 
+                    unsigned int const paletteIndex =
                         alphas_first_index[colorIndex] + j;
                     palette_pnm[mapping[paletteIndex]] = chv[colorIndex].color;
-                    trans_pnm[mapping[paletteIndex]] = 
+                    trans_pnm[mapping[paletteIndex]] =
                     alphas_of_color[colorIndex][j];
                 }
             }
         }
         freeAlphasOfColor(alphas_of_color, colors);
     }
-} 
+}
 
 
 
 static void
-makeOneColorTransparentInPalette(xel            const transColor, 
+makeOneColorTransparentInPalette(xel            const transColor,
                                  bool           const exact,
                                  pixel                palette_pnm[],
                                  unsigned int   const paletteSize,
@@ -1692,12 +1692,12 @@ makeOneColorTransparentInPalette(xel            const transColor,
                                  unsigned int * const transSizeP) {
 /*----------------------------------------------------------------------------
    Find the color 'transColor' in the color/alpha palette defined by
-   palette_pnm[], paletteSize, trans_pnm[] and *transSizeP.  
+   palette_pnm[], paletteSize, trans_pnm[] and *transSizeP.
 
    Make that entry fully transparent.
 
    Rearrange the palette so that that entry is first.  (The PNG compressor
-   can do a better job when the opaque entries are all last in the 
+   can do a better job when the opaque entries are all last in the
    color/alpha palette).
 
    If the specified color is not there and exact == TRUE, return
@@ -1712,26 +1712,26 @@ makeOneColorTransparentInPalette(xel            const transColor,
     unsigned int distance;
 
     assert(paletteSize > 0);
-    
+
     if (*transSizeP != 0)
         pm_error("Internal error: trying to make a color in the palette "
                  "transparent where there already is one.");
 
-    closestColorInPalette(transColor, palette_pnm, paletteSize, 
+    closestColorInPalette(transColor, palette_pnm, paletteSize,
                           &transparentIndex, &distance);
 
     if (distance != 0 && exact) {
         pm_message("specified transparent color not present in palette; "
                    "ignoring -transparent");
         errorlevel = PNMTOPNG_WARNING_LEVEL;
-    } else {        
+    } else {
         /* Swap this with the first entry in the palette */
         pixel tmp;
-    
+
         tmp = palette_pnm[transparentIndex];
         palette_pnm[transparentIndex] = palette_pnm[0];
         palette_pnm[0] = tmp;
-        
+
         /* Make it transparent */
         trans_pnm[0] = PGM_TRANSPARENT;
         *transSizeP = 1;
@@ -1747,8 +1747,8 @@ makeOneColorTransparentInPalette(xel            const transColor,
 
 
 static void
-findOrAddBackgroundInPalette(pixel          const backColor, 
-                             pixel                palette_pnm[], 
+findOrAddBackgroundInPalette(pixel          const backColor,
+                             pixel                palette_pnm[],
                              unsigned int * const paletteSizeP,
                              unsigned int * const backgroundIndexP) {
 /*----------------------------------------------------------------------------
@@ -1757,11 +1757,11 @@ findOrAddBackgroundInPalette(pixel          const backColor,
   add it, choose a background color that's already in the palette,
   as close to 'backColor' as possible.
 
-  If we add an entry to the palette, make it opaque.  But in searching the 
+  If we add an entry to the palette, make it opaque.  But in searching the
   existing palette, ignore transparency.
 
   Note that PNG specs say that transparency of the background is meaningless;
-  i.e. a viewer must ignore the transparency of the palette entry when 
+  i.e. a viewer must ignore the transparency of the palette entry when
   using the background color.
 
   Return the palette index of the background color as *backgroundIndexP.
@@ -1770,9 +1770,9 @@ findOrAddBackgroundInPalette(pixel          const backColor,
     unsigned int paletteIndex;
 
     backgroundIndex = -1;
-    for (paletteIndex = 0; 
-         paletteIndex < *paletteSizeP; 
-         ++paletteIndex) 
+    for (paletteIndex = 0;
+         paletteIndex < *paletteSizeP;
+         ++paletteIndex)
         if (PPM_EQUAL(palette_pnm[paletteIndex], backColor))
             backgroundIndex = paletteIndex;
 
@@ -1815,8 +1815,8 @@ findOrAddBackgroundInPalette(pixel          const backColor,
 
 
 
-static void 
-buildColorLookup(pixel                   palette_pnm[], 
+static void
+buildColorLookup(pixel                   palette_pnm[],
                  unsigned int      const paletteSize,
                  colorhash_table * const chtP) {
 /*----------------------------------------------------------------------------
@@ -1841,14 +1841,14 @@ buildColorLookup(pixel                   palette_pnm[],
 
 
 
-static void 
-buildColorAlphaLookup(pixel              palettePnm[], 
+static void
+buildColorAlphaLookup(pixel              palettePnm[],
                       unsigned int const paletteSize,
-                      gray               transPnm[], 
+                      gray               transPnm[],
                       unsigned int const transSize,
                       gray         const alphaMaxval,
                       coloralphahash_table * const cahtP) {
-    
+
     coloralphahash_table const caht = alloccoloralphahash();
 
     unsigned int paletteIndex;
@@ -1897,18 +1897,18 @@ tryAlphaPalette(FILE *         const ifP,
                  "a PNG with transparency when you specify "
                  "the palette with -palette.");
 
-    compute_alpha_palette(ifP, cols, rows, maxval, format, 
+    compute_alpha_palette(ifP, cols, rows, maxval, format,
                           rasterPos,  alpha_mask, alphaMaxval,
-                          palette_pnm, trans_pnm, 
+                          palette_pnm, trans_pnm,
                           paletteSizeP, transSizeP, &tooBig);
     if (tooBig) {
         pm_asprintf(impossibleReasonP,
                     "too many color/transparency pairs "
-                    "(more than the PNG maximum of %u", 
+                    "(more than the PNG maximum of %u",
                     MAXPALETTEENTRIES);
     } else
         *impossibleReasonP = NULL;
-} 
+}
 
 
 
@@ -1922,15 +1922,15 @@ computePixelWidth(bool           const colorPng,
     unsigned int bitsPerSample, bitsPerPixel;
 
     if (colorPng || alpha) {
-        /* PNG allows only depths of 8 and 16 for a truecolor image 
+        /* PNG allows only depths of 8 and 16 for a truecolor image
            and for a grayscale image with an alpha channel.
           */
         if (pnmMeaningfulBitCt > 8)
             bitsPerSample = 16;
-        else 
+        else
             bitsPerSample = 8;
     } else {
-        /* A grayscale, non-colormapped, no-alpha PNG may have any 
+        /* A grayscale, non-colormapped, no-alpha PNG may have any
              bit depth from 1 to 16
           */
         if (pnmMeaningfulBitCt > 8)
@@ -1969,7 +1969,7 @@ paletteIndexBits(unsigned int const nColors) {
   Return the number of bits that a palette index in the PNG will
   occupy given that the palette has 'nColors' colors in it.  It is 1,
   2, 4, or 8 bits.
-  
+
   If 'nColors' is not a valid PNG palette size, return 0.
 -----------------------------------------------------------------------------*/
     unsigned int retval;
@@ -2048,7 +2048,7 @@ computeColorMap(FILE *         const ifP,
         pm_asprintf(noColormapReasonP, "You requested no color map");
     else if (maxval > PALETTEMAXVAL)
         pm_asprintf(noColormapReasonP, "The maxval of the input image (%u) "
-                    "exceeds the PNG palette maxval (%u)", 
+                    "exceeds the PNG palette maxval (%u)",
                     maxval, PALETTEMAXVAL);
     else {
         unsigned int bitsPerPixel;
@@ -2068,20 +2068,20 @@ computeColorMap(FILE *         const ifP,
             */
             colorhist_vector chv;
             unsigned int colors;
-            
-            getChv(ifP, rasterPos, cols, rows, maxval, format, MAXCOLORS, 
+
+            getChv(ifP, rasterPos, cols, rows, maxval, format, MAXCOLORS,
                    &chv, &colors);
 
             if (chv == NULL) {
-                pm_asprintf(noColormapReasonP, 
+                pm_asprintf(noColormapReasonP,
                             "More than %u colors found -- too many for a "
                             "colormapped PNG", MAXCOLORS);
             } else {
                 /* There are few enough colors that a palette is possible */
                 if (bitsPerPixel <= paletteIndexBits(colors) && !pfP)
-                    pm_asprintf(noColormapReasonP, 
+                    pm_asprintf(noColormapReasonP,
                                 "palette index for %u colors would be "
-                                "no smaller than the indexed value (%u bits)", 
+                                "no smaller than the indexed value (%u bits)",
                                 colors, bitsPerPixel);
                 else {
                     unsigned int paletteSize;
@@ -2090,7 +2090,7 @@ computeColorMap(FILE *         const ifP,
                         tryAlphaPalette(ifP, cols, rows, maxval, format,
                                         rasterPos, alpha_mask, alphaMaxval,
                                         pfP,
-                                        palette_pnm, &paletteSize, 
+                                        palette_pnm, &paletteSize,
                                         trans_pnm, &transSize,
                                         noColormapReasonP);
 
@@ -2098,13 +2098,13 @@ computeColorMap(FILE *         const ifP,
                         *noColormapReasonP = NULL;
 
                         compute_nonalpha_palette(chv, colors, maxval, pfP,
-                                                 palette_pnm, &paletteSize, 
+                                                 palette_pnm, &paletteSize,
                                                  trans_pnm, &transSize);
-    
+
                         if (transparent)
                             makeOneColorTransparentInPalette(
-                                transcolor, transexact, 
-                                palette_pnm, paletteSize, trans_pnm, 
+                                transcolor, transexact,
+                                palette_pnm, paletteSize, trans_pnm,
                                 &transSize);
                     }
                     if (!*noColormapReasonP) {
@@ -2137,7 +2137,7 @@ static void computeColorMapLookupTable(
 /*----------------------------------------------------------------------------
    Compute applicable lookup tables for the palette index.  If there's no
    alpha mask, this is just a standard Netpbm colorhash_table.  If there's
-   an alpha mask, it is the slower Pnmtopng-specific 
+   an alpha mask, it is the slower Pnmtopng-specific
    coloralphahash_table.
 
    If a lookup table is not applicable to the image, return NULL as
@@ -2145,10 +2145,10 @@ static void computeColorMapLookupTable(
 -----------------------------------------------------------------------------*/
     if (colorMapped) {
         if (alpha) {
-            buildColorAlphaLookup(palette_pnm, palette_size, 
+            buildColorAlphaLookup(palette_pnm, palette_size,
                                   trans_pnm, trans_size, alpha_maxval, cahtP);
             *chtP = NULL;
-        } else { 
+        } else {
             buildColorLookup(palette_pnm, palette_size, chtP);
             *cahtP = NULL;
         }
@@ -2195,15 +2195,15 @@ computeRasterWidth(bool           const colorMapped,
                           bitsPerSampleP, bitsPerPixelP);
 
         if (verbose)
-            pm_message("Writing %u bits per component per pixel", 
+            pm_message("Writing %u bits per component per pixel",
                        *bitsPerSampleP);
     }
 }
 
 
 static void
-createPngPalette(pixel              palette_pnm[], 
-                 unsigned int const paletteSize, 
+createPngPalette(pixel              palette_pnm[],
+                 unsigned int const paletteSize,
                  pixval       const maxval,
                  gray               trans_pnm[],
                  unsigned int const transSize,
@@ -2261,7 +2261,7 @@ setZlibCompression(struct pngx *          const pngxP,
         pngx_setCompressionSize(pngxP, zlibCompression.buffer_size);
     }
 }
-                  
+
 
 
 static void
@@ -2276,7 +2276,7 @@ makePngLine(png_byte *           const line,
             struct pngx *        const pngxP,
             xelval               const png_maxval,
             unsigned int         const depth) {
-            
+
     unsigned int col;
     png_byte *pp;
 
@@ -2310,7 +2310,7 @@ makePngLine(png_byte *           const line,
             *pp++ = PPM_GETB(p_png) & 0xff;
         } else
             pm_error("INTERNAL ERROR: undefined color_type");
-                
+
         if (pngx_colorType(pngxP) & PNG_COLOR_MASK_ALPHA) {
             int const png_alphaval = (int)
                 alpha_mask[col] * (float) png_maxval / maxval + 0.5;
@@ -2364,7 +2364,7 @@ writeRaster(struct pngx *        const pngxP,
             pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
             pnm_promoteformatrow(xelrow, cols, maxval, format, maxval,
                                  PPM_TYPE);
-            
+
             makePngLine(line, xelrow, cols, maxval,
                         alpha, alpha ? alpha_mask[row] : NULL,
                         cht, caht, pngxP, png_maxval, depth);
@@ -2393,15 +2393,15 @@ doHistChunk(struct pngx * const pngxP,
         colorhist_vector chv;
         unsigned int colorCt;
         colorhash_table cht;
-        
-        getChv(ifP, rasterPos, cols, rows, maxval, format, MAXCOLORS, 
+
+        getChv(ifP, rasterPos, cols, rows, maxval, format, MAXCOLORS,
                &chv, &colorCt);
 
         cht = ppm_colorhisttocolorhash(chv, colorCt);
-                
-        { 
+
+        {
             png_uint_16 * histogram;  /* malloc'ed */
-        
+
             MALLOCARRAY(histogram, MAXCOLORS);
 
             if (!histogram)
@@ -2416,7 +2416,7 @@ doHistChunk(struct pngx * const pngxP,
                     else
                         histogram[i] = chv[chvIndex].value;
                 }
-            
+
                 pngx_setHist(pngxP, histogram);
 
                 if (verbose)
@@ -2463,7 +2463,7 @@ doIhdrChunk(struct pngx * const pngxP,
 static void
 doGamaChunk(struct cmdlineInfo const cmdline,
             struct pngx *      const pngxP) {
-            
+
     if (cmdline.gammaSpec)
         pngx_setGama(pngxP, cmdline.gamma);
 }
@@ -2507,7 +2507,7 @@ reportTrans(struct pngx * const pngxP) {
         struct pngx_trns const transInfo = pngx_trns(pngxP);
 
         pm_message("%u transparency values", transInfo.numTrans);
-        
+
         pm_message("Transparent color {gray, red, green, blue} = "
                    "{%d, %d, %d, %d}",
                    transInfo.transColor.gray,
@@ -2560,21 +2560,21 @@ doBkgdChunk(struct pngx * const pngxP,
             xelval        const maxval,
             xelval        const pngMaxval,
             bool          const verbose) {
-    
+
     if (bkgdRequested) {
         if (pngx_colorType(pngxP) == PNG_COLOR_TYPE_PALETTE)
             pngx_setBkgdPalette(pngxP, backgroundIndex);
         else {
-            png_color_16 const pngBackground = 
+            png_color_16 const pngBackground =
                 xelToPngColor_16(backColor, maxval, pngMaxval);
             pngx_setBkgdRgb(pngxP, pngBackground);
             if (verbose)
                 pm_message("Writing bKGD chunk with background color "
                            " {gray, red, green, blue} = {%d, %d, %d, %d}",
-                           pngBackground.gray, 
-                           pngBackground.red, 
-                           pngBackground.green, 
-                           pngBackground.blue ); 
+                           pngBackground.gray,
+                           pngBackground.red,
+                           pngBackground.green,
+                           pngBackground.blue );
         }
     }
 }
@@ -2591,20 +2591,19 @@ doSbitChunk(struct pngx * const pngxP,
     if (pngx_colorType(pngxP) != PNG_COLOR_TYPE_PALETTE &&
         (pngMaxval > maxval || (alpha && pngMaxval > alphaMaxval))) {
 
-        /* We're writing in a bit depth that doesn't match the maxval
-           of the input image and the alpha mask.  So we write an sBIT
-           chunk to tell what the original image's maxval was.  The
-           sBit chunk doesn't let us specify any maxval -- only powers
-           of two minus one.  So we pick the power of two minus one
-           which is greater than or equal to the actual input maxval.
-           
-           PNG also doesn't let an sBIT chunk indicate a maxval
-           _greater_ than the the PNG maxval.  The designers probably
-           did not conceive of the case where that would happen.  The
-           case is this: We detected redundancy in the bits so were
-           able to store fewer bits than the user provided.  But since
-           PNG doesn't allow it, we don't attempt to create such an
-           sBIT chunk.
+        /* We're writing in a bit depth that doesn't match the maxval of the
+           input image and the alpha mask.  So we write an sBIT chunk to tell
+           what the original image's maxval was.  The sBit chunk doesn't let
+           us specify any maxval -- only powers of two minus one.  So we pick
+           the power of two minus one which is greater than or equal to the
+           actual input maxval.
+
+           PNG also doesn't let an sBIT chunk indicate a maxval _greater_ than
+           the PNG maxval.  The designers probably did not conceive of the
+           case where that would happen.  The case is this: We detected
+           redundancy in the bits so were able to store fewer bits than the
+           user provided.  But since PNG doesn't allow it, we don't attempt to
+           create such an sBIT chunk.
         */
 
         {
@@ -2618,7 +2617,7 @@ doSbitChunk(struct pngx * const pngxP,
                 sbit.blue  = sbitval;
             } else
                 sbit.gray  = sbitval;
-            
+
             if (verbose)
                 pm_message("Writing sBIT chunk with bits = %d", sbitval);
 
@@ -2649,7 +2648,7 @@ addSrgbChunk(struct pngx *   const pngxP,
 
 
 
-static void 
+static void
 convertpnm(struct cmdlineInfo const cmdline,
            FILE *             const ifP,
            FILE *             const ofP,
@@ -2668,14 +2667,14 @@ convertpnm(struct cmdlineInfo const cmdline,
     xelval maxval;
     /* The maxval of the input image */
     xelval pngMaxval;
-        /* The maxval of the samples in the PNG output 
+        /* The maxval of the samples in the PNG output
            (must be 1, 3, 7, 15, 255, or 65535)
         */
     pixel transcolor;
         /* The color that is to be transparent, with maxval equal to that
            of the input image.
         */
-    bool transExact;  
+    bool transExact;
         /* boolean: the user wants only the exact color he specified to be
            transparent; not just something close to it.
         */
@@ -2694,14 +2693,14 @@ convertpnm(struct cmdlineInfo const cmdline,
     pixel palettePnm[MAXCOLORS];
     png_color palette[MAXCOLORS];
         /* The color part of the color/alpha palette passed to the PNG
-           compressor 
+           compressor
         */
     unsigned int paletteSize;
 
     gray transPnm[MAXCOLORS];
     png_byte  trans[MAXCOLORS];
         /* The alpha part of the color/alpha palette passed to the PNG
-           compressor 
+           compressor
         */
     unsigned int transSize;
 
@@ -2717,7 +2716,7 @@ convertpnm(struct cmdlineInfo const cmdline,
            we should.  malloc'ed null-terminated string.
         */
     unsigned int depth;
-        /* The number of bits per sample in the (uncompressed) png 
+        /* The number of bits per sample in the (uncompressed) png
            raster -- if the raster contains palette indices, this is the
            number of bits in the index.
         */
@@ -2725,7 +2724,7 @@ convertpnm(struct cmdlineInfo const cmdline,
         /* The total number of bits per pixel in the (uncompressed) png
            raster, including all channels.
         */
-    pm_filepos rasterPos;  
+    pm_filepos rasterPos;
         /* file position in input image file of start of image (i.e. after
            the header)
         */
@@ -2766,7 +2765,7 @@ convertpnm(struct cmdlineInfo const cmdline,
         colorPng = (PNM_FORMAT_TYPE(format) == PPM_TYPE);
     else {
         if (PNM_FORMAT_TYPE(format) == PPM_TYPE) {
-            colorPng = hasColor(ifP, cols, rows, maxval, format, rasterPos); 
+            colorPng = hasColor(ifP, cols, rows, maxval, format, rasterPos);
         } else
             colorPng = false;
     }
@@ -2781,10 +2780,10 @@ convertpnm(struct cmdlineInfo const cmdline,
 
     findRedundantBits(ifP, rasterPos, cols, rows, maxval, format, alpha,
                       cmdline.force, &pnmMeaningfulBitCt);
-  
+
     computeColorMap(ifP, rasterPos, cols, rows, maxval, colorPng, format,
                     cmdline.force, pfP,
-                    alpha, transparent >= 0, transcolor, transExact, 
+                    alpha, transparent >= 0, transcolor, transExact,
                     !!cmdline.background, backColor,
                     alpha_mask, alphaMaxval, pnmMeaningfulBitCt,
                     palettePnm, &paletteSize, transPnm, &transSize,
@@ -2801,7 +2800,7 @@ convertpnm(struct cmdlineInfo const cmdline,
         colorMapped = FALSE;
     } else
         colorMapped = TRUE;
-  
+
     computeColorMapLookupTable(colorMapped, palettePnm, paletteSize,
                                transPnm, transSize, alpha, alphaMaxval,
                                &cht, &caht);
@@ -2812,7 +2811,7 @@ convertpnm(struct cmdlineInfo const cmdline,
     if (verbose)
         pm_message ("writing a%s %d-bit %s%s file%s",
                     fulldepth == 8 ? "n" : "", fulldepth,
-                    colorMapped ? "palette": 
+                    colorMapped ? "palette":
                     colorPng ? "RGB" : "gray",
                     alpha ? (colorMapped ? "+transparency" : "+alpha") : "",
                     cmdline.interlace ? " (interlaced)" : "");
@@ -2839,7 +2838,7 @@ convertpnm(struct cmdlineInfo const cmdline,
         /* creating PNG palette (Not counting the transparency palette) */
 
         createPngPalette(palettePnm, paletteSize, maxval,
-                         transPnm, transSize, alphaMaxval, 
+                         transPnm, transSize, alphaMaxval,
                          palette, trans);
         pngx_setPlte(pngxP, palette, paletteSize);
 
@@ -2940,7 +2939,7 @@ displayVersion() {
 
 
 
-int 
+int
 main(int argc, const char * argv[]) {
 
     struct cmdlineInfo cmdline;
@@ -2950,29 +2949,29 @@ main(int argc, const char * argv[]) {
     FILE * tfP;
 
     int errorlevel;
-    
+
     pm_proginit(&argc, argv);
-    
+
     parseCommandLine(argc, argv, &cmdline);
-    
+
     if (cmdline.libversion) {
         displayVersion();
         return 0;
     }
     verbose = cmdline.verbose;
-    
+
     ifP = pm_openr_seekable(cmdline.inputFileName);
-    
+
     if (cmdline.alpha)
         afP = pm_openr(cmdline.alpha);
     else
         afP = NULL;
-    
+
     if (cmdline.palette)
         pfP = pm_openr(cmdline.palette);
     else
         pfP = NULL;
-    
+
     if (cmdline.text)
         tfP = pm_openr(cmdline.text);
     else if (cmdline.ztxt)
@@ -2981,7 +2980,7 @@ main(int argc, const char * argv[]) {
         tfP = NULL;
 
     convertpnm(cmdline, ifP, stdout, afP, pfP, tfP, &errorlevel);
-    
+
     if (afP)
         pm_close(afP);
     if (pfP)
diff --git a/doc/HISTORY b/doc/HISTORY
index 46e179d3..e936536b 100644
--- a/doc/HISTORY
+++ b/doc/HISTORY
@@ -3,32 +3,58 @@ Netpbm.
 
 CHANGE HISTORY 
 --------------
-19.03.23 BJH  Release 10.85.05
 
-              Build: update 'mkdeb' so it works on Debian 9 (in addition to
-              Debian 8).  Thanks Neil R Ormos <ormos@ormos.org>.
+19.03.30 BJH  Release 10.86.00
 
-19.03.08 BJH  Release 10.85.04
+              Add pamfind.
 
-              pamtopng: Fix sBit chunk, bit shift value for 1-, 2-, and 4-bit-
-              per-sample images.  Always broken (Pamtopng was new in Netpbm
-              10.71 (June 2015)).
+              Add pambrighten.
 
-              pamtopng: Fix buffer overrun.  Always broken (Pamtopng was new
-              in Netpbm 10.71 (June 2015)).
+              Add pamhue.
+
+              pnmcrop: Add -bg-color, bg-corner, -reportfull, -reportsize,
+              -blank-image .
+
+              pamtopng: Add -interlace .
+
+              pamenlarge: Add -scale, -xscale, -yscale .
+
+              pamenlarge: Much faster for PBM with xscale factor above 10;
+              slightly faster for xscale factor 2-10.
+
+              pamfile: Add -machine and -size .
+
+              pamscale: Add -reportonly
+
+              pamstretch-gen: Add -quiet, -plain.
+
+              pamstretch-gen: Use -dropedge on the 'pamstretch' piece for
+              better looking output.
 
-19.03.01 BJH  Release 10.85.03
+              pamstretch: Accept scale factor of 1 as a parameter (already
+              was accepted with -xscale and -yscale options).
+
+              pamstretch: Don't mess with edge when scale factor is 1,
+              regardless of -dropedge and -blackedge.
+
+              ppmwheel: Add -huevalue, -huesaturation, -maxval .
+
+              various: Fix unnormalizing code so a value exactly between two
+              sample values rounds consistently up.  Affects many programs.
 
               pstopnm: Fix bug: -textalphabits has no effect.  Always broken.
               (-textalphabits was new in Netpbm 10.53 (December 2010)).
 
-19.02.10 BJH  Release 10.85.02
+              pamtopng: Fix sBit chunk, bit shift value for 1-, 2-, and 4-bit-
+              per-sample images.  Always broken (Pamtopng was new in Netpbm
+              10.71 (June 2015)).
+
+              pamtopng: Fix buffer overrun.  Always broken (Pamtopng was new
+              in Netpbm 10.71 (June 2015)).
 
               pnmtopng: fix bug: -interlace ignored.  Broken in 10.55
               (June 2011).
 
-19.01.07 BJH  Release 10.85.01
-
               pamstretch: Reject very large scale factors instead of producing
               incorrect output.
 
@@ -39,6 +65,16 @@ CHANGE HISTORY
               ppmdraw: Fix bug: 'setlinetype nodiag' says invalid type.
               Always broken.  (Ppmdraw was new in Netpbm 10.29 (August 2005)).
 
+              Build: split link/ directory in package tree into staticlink/
+              and sharedlink/ .  Make 'installnetpbm' do separate prompts to
+              install static libraries and shared library stubs.  Make
+              'installnetpbm' install clean symlink in the usual case that the
+              shared library stub and shared library are in the same
+              directory.
+
+              Build: update 'mkdeb' so it works on Debian 9 (in addition to
+              Debian 8).  Thanks Neil R Ormos <ormos@ormos.org>.
+
 18.12.29 BJH  Release 10.85.00
 
               pnmpaste: Add -nand, -nor, and -nxor.
diff --git a/doc/TESTS b/doc/TESTS
index ca4930a2..9434f580 100644
--- a/doc/TESTS
+++ b/doc/TESTS
@@ -12,7 +12,7 @@ Contents
   1.6 Testing package in designated directory
   1.7 Pre-packaging check
   1.8 Post-install check
-  1.9 Skipping test items
+  1.9 Testing less than everything
   1.10 Valgrind
   
 2. Troubleshooting
@@ -166,18 +166,23 @@ a non-standard location.  This must be an absolute path.
 
 
 
-1.9 Skipping test items
-=======================
+1.9 Testing less than everything
+================================
 
 The file test/Test-Order is a list of tests which are run.  If you want to
-skip any test, remove the line or comment it out with a "#".
+skip a test, remove the line or comment it out with a "#".
 
-The variable "target", a comma-separated list of Netpbm programs
-provides another way to run only select tests.  For example to run
-only the test scripts which examine giftopnm and pamtogif, do:
+If you want to run just a few of the tests, use the make variable "target", a
+comma-separated list of Netpbm programs.
 
   make check target=giftopnm,pamtogif
 
+    This runs only the tests of 'giftopnm' and 'pamtogif', in the package
+    (staging) directory.
+
+  make check-tree target=ppmbrighten
+
+    This runs only the tests of 'ppmbrighten', from the build tree.
 
 
 1.10 Valgrind
diff --git a/editor/Makefile b/editor/Makefile
index de5ae666..5b12e4ca 100644
--- a/editor/Makefile
+++ b/editor/Makefile
@@ -16,10 +16,10 @@ SUBDIRS = pamflip specialty
 # This package is so big, it's useful even when some parts won't 
 # build.
 
-PORTBINARIES = pamaddnoise pamaltsat pambackground pamcomp pamcut \
+PORTBINARIES = pamaddnoise pamaltsat pambackground pambrighten pamcomp pamcut \
 	       pamdice pamditherbw pamedge \
 	       pamenlarge \
-	       pamfunc pamlevels pammasksharpen pammixmulti \
+	       pamfunc pamhue pamlevels pammasksharpen pammixmulti \
 	       pamperspective pamrecolor pamrubber \
 	       pamscale pamsistoaglyph pamstretch pamthreshold pamundice \
 	       pamwipeout \
diff --git a/editor/pambrighten.c b/editor/pambrighten.c
new file mode 100644
index 00000000..51bd0d23
--- /dev/null
+++ b/editor/pambrighten.c
@@ -0,0 +1,271 @@
+/*=============================================================================
+                                  pambrighten
+===============================================================================
+  Change Value and Saturation of Netpbm image.
+=============================================================================*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <math.h>
+#include "pm_c_util.h"
+#include "mallocvar.h"
+#include "shhopt.h"
+#include "pam.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 */
+    float        valchange;
+    float        satchange;
+};
+
+
+
+static void
+parseCommandLine(int                        argc,
+                 const char **              argv,
+                 struct CmdlineInfo * const 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;
+
+    unsigned int valueSpec;
+    int          valueOpt;
+    unsigned int saturationSpec;
+    int          saturationOpt;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "value",       OPT_INT,    &valueOpt,
+            &valueSpec,           0 );
+    OPTENT3(0, "saturation",  OPT_INT,    &saturationOpt,
+            &saturationSpec,      0 );
+
+    opt.opt_table = option_def;
+    opt.short_allowed = false;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = false;    /* No negative arguments */
+
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (valueSpec) {
+        if (valueOpt < -100)
+            pm_error("Value reduction cannot be more than 100%%.  "
+                     "You specified %d", valueOpt);
+        else
+            cmdlineP->valchange = 1.0 + (float)valueOpt / 100;
+    } else
+        cmdlineP->valchange = 1.0;
+
+    if (saturationSpec) {
+        if (saturationOpt < -100)
+            pm_error("Saturation reduction cannot be more than 100%%.  "
+                     "You specified %d", saturationOpt);
+        else
+            cmdlineP->satchange = 1.0 + (float)saturationOpt / 100;
+    } else
+        cmdlineP->satchange = 1.0;
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFileName = argv[1];
+    else
+        pm_error("Program takes at most one argument:  file specification");
+}
+
+
+
+static void
+changeColorPix(tuple              const tupleval,
+               float              const valchange,
+               float              const satchange,
+               const struct pam * const pamP) {
+
+    pixel oldRgb, newRgb;
+    struct hsv oldHsv, newHsv;
+
+    PPM_PUTR(oldRgb, tupleval[PAM_RED_PLANE]);
+    PPM_PUTG(oldRgb, tupleval[PAM_GRN_PLANE]);
+    PPM_PUTB(oldRgb, tupleval[PAM_BLU_PLANE]);
+    oldHsv = ppm_hsv_from_color(oldRgb, pamP->maxval);
+
+    newHsv.h = oldHsv.h;
+
+    newHsv.s = MIN(1.0, MAX(0.0, oldHsv.s * satchange));
+
+    newHsv.v = MIN(1.0, MAX(0.0, oldHsv.v * valchange));
+
+    newRgb = ppm_color_from_hsv(newHsv, pamP->maxval);
+
+    tupleval[PAM_RED_PLANE] = PPM_GETR(newRgb);
+    tupleval[PAM_GRN_PLANE] = PPM_GETG(newRgb);
+    tupleval[PAM_BLU_PLANE] = PPM_GETB(newRgb);
+}
+
+
+
+static void
+changeGrayPix(tuple        const tupleval,
+              float        const valchange,
+              struct pam * const pamP) {
+
+    samplen const oldGray = pnm_normalized_sample(pamP, tupleval[0]);
+    samplen const newGray = MIN(1.0, MAX(0.0, oldGray * valchange));
+
+    tupleval[0] = pnm_unnormalized_sample(pamP, newGray);
+}
+
+
+
+typedef enum {COLORTYPE_COLOR, COLORTYPE_GRAY, COLORTYPE_BW} ColorType;
+
+
+
+static ColorType
+colorTypeOfImage(struct pam * const pamP) {
+/*----------------------------------------------------------------------------
+   The basic type of color represented in the image described by *pamP: full
+   color, grayscale, or black and white
+
+   Note that we're talking about the format of the image, not the reality of
+   the pixels.  A color image is still a color image even if all the colors in
+   it happen to be gray.
+
+   For a PAM image, as is customary in Netpbm, we do not consider the tuple
+   type, but rather infer the color type from the depth and maxval.  This
+   gives us more flexibility for future tuple types.
+-----------------------------------------------------------------------------*/
+    ColorType retval;
+
+    if (pamP->format == PPM_FORMAT ||
+        pamP->format == RPPM_FORMAT ||
+        (pamP->format == PAM_FORMAT && pamP->depth >= 3)) {
+
+        retval = COLORTYPE_COLOR;
+
+    } else if (pamP->format == PGM_FORMAT ||
+               pamP->format == RPGM_FORMAT ||
+               (pamP->format == PAM_FORMAT &&
+                pamP->depth >= 1 &&
+                pamP->maxval > 1)) {
+
+        retval = COLORTYPE_GRAY;
+
+    } else {
+
+        retval = COLORTYPE_BW;
+
+    }
+    return retval;
+}
+
+
+
+static void
+pambrighten(struct CmdlineInfo const cmdline,
+            FILE *             const ifP) {
+
+    struct pam inpam, outpam;
+    tuple * tuplerow;
+    ColorType colorType;
+    unsigned int row;
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    colorType = colorTypeOfImage(&inpam);
+
+    outpam = inpam;
+    outpam.file = stdout;
+
+    pnm_writepaminit(&outpam);
+
+    tuplerow = pnm_allocpamrow(&inpam);
+
+    for (row = 0; row < inpam.height; ++row) {
+        unsigned int col;
+
+        pnm_readpamrow(&inpam, tuplerow);
+
+        for (col = 0; col < inpam.width; ++col)  {
+            switch (colorType) {
+            case COLORTYPE_COLOR:
+                changeColorPix(tuplerow[col],
+                               cmdline.valchange, cmdline.satchange,
+                               &inpam);
+                break;
+            case COLORTYPE_GRAY:
+                changeGrayPix(tuplerow[col],
+                              cmdline.valchange,
+                              &inpam);
+                break;
+            case COLORTYPE_BW:
+                /* Nothing to change. */
+                break;
+            }
+        }
+        pnm_writepamrow(&outpam, tuplerow);
+    }
+    pnm_freepamrow(tuplerow);
+}
+
+
+
+int
+main(int argc, const char *argv[]) {
+
+    struct CmdlineInfo cmdline;
+    FILE * ifP;
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    pambrighten(cmdline, ifP);
+
+    pm_close(ifP);
+    pm_close(stdout);
+
+    return 0;
+}
+
+
+
+/*
+   This was derived from ppmbrighten code written by Jef Poskanzer and
+   Brian Moffet. Updated by Willem van Schaik to support PAM.
+
+   Copyright (C) 1989 by Jef Poskanzer.
+   Copyright (C) 1990 by Brian Moffet.
+   Copyright (C) 2019 by Willem van Schaik (willem@schaik.com)
+
+   Permission to use, copy, modify, and distribute this software and its
+   documentation for any purpose and without fee is hereby granted, provided
+   that the above copyright notice appear in all copies and that both that
+   copyright notice and this permission notice appear in supporting
+   documentation.  This software is provided "as is" without express or
+   implied warranty.
+
+   Bryan Henderson contributes his work to the public domain.
+*/
diff --git a/editor/pamcomp.c b/editor/pamcomp.c
index 3e27731f..6e1e7f7d 100644
--- a/editor/pamcomp.c
+++ b/editor/pamcomp.c
@@ -532,7 +532,8 @@ composeComponents(sample           const compA,
             float const mix =
                 compALinear * distrib + compBLinearAdj * (1.0 - distrib)
                 * composedFactor;
-            sample const sampleValue = ROUNDU(pm_gamma709(mix) * maxval);
+            sample const sampleValue =
+                pnm_unnormalize(pm_gamma709(mix), maxval);
             retval = MIN(maxval, MAX(0, sampleValue));
         }
     }
diff --git a/editor/pamenlarge.c b/editor/pamenlarge.c
index 187bfb6e..56a8c6f7 100644
--- a/editor/pamenlarge.c
+++ b/editor/pamenlarge.c
@@ -3,46 +3,132 @@
 ===============================================================================
   By Bryan Henderson 2004.09.26.  Contributed to the public domain by its
   author.
+
+  The design and code for the fast processing of PBMs is by Akira Urushibata
+  in March 2010 and substantially improved in February 2019.
 =============================================================================*/
 
+#include <stdbool.h>
+#include <assert.h>
+
 #include "netpbm/mallocvar.h"
 #include "netpbm/pm_c_util.h"
 #include "netpbm/pam.h"
 #include "netpbm/pbm.h"
+#include "netpbm/shhopt.h"
+#include "netpbm/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.
     */
-    const char *inputFilespec;  
-    unsigned int scaleFactor;
+    const char * inputFilespec;
+    unsigned int xScaleFactor;
+    unsigned int yScaleFactor;
 };
 
 
 
 static void
-parseCommandLine(int                  const argc,
-                 const char **        const argv,
-                 struct cmdlineInfo * const cmdlineP) {
+parseCommandLine(int                  argc,
+                 const char        ** argv,
+                 struct CmdlineInfo * cmdlineP) {
 /*----------------------------------------------------------------------------
    Note that the file spec array we return is stored in the storage that
    was passed to us as the argv array.
 -----------------------------------------------------------------------------*/
-    if (argc-1 < 1)
-        pm_error("You must specify at least one argument:  The scale factor");
-    else {
-        cmdlineP->scaleFactor = atoi(argv[1]);
-        
-        if (cmdlineP->scaleFactor < 1)
-            pm_error("Scale factor must be an integer at least 1.  "
-                     "You specified '%s'", argv[1]);
+    optStruct3 opt;  /* set by OPTENT3 */
+    optEntry * option_def;
+    unsigned int option_def_index;
+
+    unsigned int scale;
+    unsigned int xscaleSpec;
+    unsigned int yscaleSpec;
+    unsigned int scaleSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    OPTENT3(0, "xscale", OPT_UINT, &cmdlineP->xScaleFactor,  &xscaleSpec, 0);
+    OPTENT3(0, "yscale", OPT_UINT, &cmdlineP->yScaleFactor,  &yscaleSpec, 0);
+    OPTENT3(0, "scale",  OPT_UINT, &scale,                   &scaleSpec, 0);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = false; /* We have some 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 (scaleSpec && scale == 0)
+        pm_error("-scale must be positive.  You specified zero");
+
+    if (xscaleSpec && cmdlineP->xScaleFactor == 0)
+        pm_error("-xscale must be positive.  You specified zero");
+
+    if (yscaleSpec && cmdlineP->yScaleFactor == 0)
+        pm_error("-yscale must be positive.  You specified zero");
+
+    if (scaleSpec && xscaleSpec)
+        pm_error("You cannot specify both -scale and -xscale");
+
+    if (scaleSpec && yscaleSpec)
+        pm_error("You cannot specify both -scale and -yscale");
+
+    if (scaleSpec) {
+        cmdlineP->xScaleFactor = scale;
+        cmdlineP->yScaleFactor = scale;
+    }
+
+    if (xscaleSpec && !yscaleSpec)
+        cmdlineP->yScaleFactor = 1;
+
+    if (yscaleSpec && !xscaleSpec)
+        cmdlineP->xScaleFactor = 1;
+
+    if (scaleSpec || xscaleSpec || yscaleSpec) {
+        /* Scale options specified.  Naked scale argument not allowed */
 
-        if (argc-1 >= 2)
+        if ((argc-1) > 1)
+            pm_error("Too many arguments (%u).  With a scale option, "
+                     "the only argument is the "
+                     "optional file specification", argc-1);
+
+        if (argc-1 > 0)
+            cmdlineP->inputFilespec = argv[1];
+        else
+            cmdlineP->inputFilespec = "-";
+    } else {
+        /* scale must be specified in an argument */
+        if ((argc-1) != 1 && (argc-1) != 2)
+            pm_error("Wrong number of arguments (%d).  Without scale options, "
+                     "you must supply 1 or 2 arguments:  scale and "
+                     "optional file specification", argc-1);
+
+        {
+            const char * error;   /* error message of pm_string_to_uint */
+            unsigned int scale;
+
+            pm_string_to_uint(argv[1], &scale, &error);
+
+            if (error == NULL) {
+                if (scale == 0)
+                    pm_error("Scale argument must be positive.  "
+                             "You specified zero");
+                else
+                    cmdlineP->xScaleFactor = cmdlineP->yScaleFactor = scale;
+            } else
+                pm_error("Invalid scale factor: %s", error);
+
+        }
+        if (argc-1 > 1)
             cmdlineP->inputFilespec = argv[2];
         else
             cmdlineP->inputFilespec = "-";
     }
-}        
+    free(option_def);
+}
 
 
 
@@ -75,253 +161,571 @@ makeOutputRowMap(tuple **     const outTupleRowP,
 static void
 validateComputableDimensions(unsigned int const width,
                              unsigned int const height,
-                             unsigned int const scaleFactor) {
+                             unsigned int const xScaleFactor,
+                             unsigned int const yScaleFactor) {
 /*----------------------------------------------------------------------------
    Make sure that multiplication for output image width and height do not
    overflow.
-   See validateComputetableSize() in libpam.c
-   and pbm_readpbminitrest() in libpbm2.c
+
+   See validateComputetableSize() in libpam.c and pbm_readpbminitrest() in
+   libpbm2.c
 -----------------------------------------------------------------------------*/
     unsigned int const maxWidthHeight = INT_MAX - 2;
     unsigned int const maxScaleFactor = maxWidthHeight / MAX(height, width);
+    unsigned int const greaterScaleFactor = MAX(xScaleFactor, yScaleFactor);
 
-    if (scaleFactor > maxScaleFactor)
-       pm_error("Scale factor '%u' too large.  "
-                "The maximum for this %u x %u input image is %u.",
-                scaleFactor, width, height, maxScaleFactor);
+    if (greaterScaleFactor > maxScaleFactor)
+        pm_error("Scale factor '%u' too large.  "
+                 "The maximum for this %u x %u input image is %u.",
+                 greaterScaleFactor, width, height, maxScaleFactor);
 }
 
 
+static unsigned char const pair[7][4] = {
+    { 0x00 , 0x7F , 0x80 , 0xFF},
+    { 0x00 , 0x3F , 0xC0 , 0xFF},
+    { 0x00 , 0x1F , 0xE0 , 0xFF},
+    { 0x00 , 0x0F , 0xF0 , 0xFF},
+    { 0x00 , 0x07 , 0xF8 , 0xFF},
+    { 0x00 , 0x03 , 0xFC , 0xFF},
+    { 0x00 , 0x01 , 0xFE , 0xFF} };
+
+
 
 static void
-enlargePbmRowHorizontally(struct pam *          const inpamP,
-                          const unsigned char * const inrow,
-                          unsigned int          const inColChars,
-                          unsigned int          const outColChars,
-                          unsigned int          const scaleFactor,
-                          unsigned char *       const outrow) {
-
-    static unsigned char const dbl[16] = { 
-        0x00, 0x03, 0x0C, 0x0F, 0x30, 0x33, 0x3C, 0x3F, 
+enlargePbmRowHorizontallySmall(const unsigned char * const inrow,
+                               unsigned int          const inColChars,
+                               unsigned int          const xScaleFactor,
+                               unsigned char *       const outrow) {
+/*----------------------------------------------------------------------------
+   Fast routines for scale factors 1-13.
+
+   Using a temp value "inrowChar" makes a difference.  We know that inrow
+   and outrow don't overlap, but the compiler does not and emits code
+   which reads inrow[colChar] each time fearing that a write to outrow[x]
+   may have altered the value.  (The first "const" for inrow in the above
+   argument list is not enough for the compiler.)
+-----------------------------------------------------------------------------*/
+
+    static unsigned char const dbl[16] = {
+        0x00, 0x03, 0x0C, 0x0F, 0x30, 0x33, 0x3C, 0x3F,
         0xC0, 0xC3, 0xCC, 0xCF, 0xF0, 0xF3, 0xFC, 0xFF };
 
-    static unsigned char const trp1[8] = { 
+    static unsigned char const trp1[8] = {
         0x00, 0x03, 0x1C, 0x1F, 0xE0, 0xE3, 0xFC, 0xFF };
-        
-    static unsigned char const trp2[16] = { 
+
+    static unsigned char const trp2[16] = {
         0x00, 0x01, 0x0E, 0x0F, 0x70, 0x71, 0x7E, 0x7F,
         0x80, 0x81, 0x8E, 0x8F, 0xF0, 0xF1, 0xFE, 0xFF };
 
-    static unsigned char const trp3[8] = { 
+    static unsigned char const trp3[8] = {
         0x00, 0x07, 0x38, 0x3F, 0xC0, 0xC7, 0xF8, 0xFF };
 
-    static unsigned char const quad[4] = { 0x00, 0x0F, 0xF0, 0xFF };
-
     static unsigned char const quin2[8] = {
         0x00, 0x01, 0x3E, 0x3F, 0xC0, 0xC1, 0xFE, 0xFF };
 
     static unsigned char const quin4[8] = {
         0x00, 0x03, 0x7C, 0x7F, 0x80, 0x83, 0xFC, 0xFF };
 
-    static unsigned int const pair[4] = { 0x0000, 0x00FF, 0xFF00, 0xFFFF };
+    static unsigned char const * quad = pair[3];
 
     unsigned int colChar;
 
-    switch (scaleFactor) {
+    switch (xScaleFactor) {
     case 1:  break; /* outrow set to inrow */
-    case 2:  /* Make outrow using prefabricated parts (same for 3, 5). */ 
-        for (colChar = 0; colChar < inColChars; ++colChar) { 
-            outrow[colChar*2]   = dbl[(inrow[colChar] & 0xF0) >> 4];
-            outrow[colChar*2+1] = dbl[(inrow[colChar] & 0x0F) >> 0];
+
+    case 2:  /* Make outrow using prefabricated parts (same for 3, 5). */
+        for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
+            outrow[colChar*2]   = dbl[ inrowChar >> 4];
+            outrow[colChar*2+1] = dbl[(inrowChar & 0x0F) >> 0];
             /* Possible outrow overrun by one byte. */
         }
         break;
 
     case 3:
-        for (colChar = 0; colChar < inColChars; ++colChar) { 
-            outrow[colChar*3]   = trp1[(inrow[colChar] & 0xF0) >> 5];
-            outrow[colChar*3+1] = trp2[(inrow[colChar] >> 2) & 0x0F];
-            outrow[colChar*3+2] = trp3[(inrow[colChar] >> 0) & 0x07];
+        for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
+            outrow[colChar*3]   = trp1[ inrowChar >> 5];
+            outrow[colChar*3+1] = trp2[(inrowChar >> 2) & 0x0F];
+            outrow[colChar*3+2] = trp3[(inrowChar >> 0) & 0x07];
         }
-        break;  
+        break;
 
     case 4:
         for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
             unsigned int i;
-            for (i = 0; i < 4; ++i) 
-                outrow[colChar*4+i]=
-                    quad[(inrow[colChar] >> (6 - 2 * i)) & 0x03]; 
+            for (i = 0; i < 4; ++i)
+                outrow[colChar*4+i] =
+                    quad[(inrowChar >> (6 - 2 * i)) & 0x03];
         }
         break;
 
     case 5:
-        for (colChar = 0; colChar < inColChars; ++colChar) { 
-            outrow[colChar*5]   = pair [(inrow[colChar] >> 6) & 0x03] >> 5; 
-            outrow[colChar*5+1] = quin2[(inrow[colChar] >> 4) & 0x07] >> 0; 
-            outrow[colChar*5+2] = quad [(inrow[colChar] >> 3) & 0x03] >> 0; 
-            outrow[colChar*5+3] = quin4[(inrow[colChar] >> 1) & 0x07] >> 0;
-            outrow[colChar*5+4] = pair [(inrow[colChar] >> 0) & 0x03] >> 3; 
+        for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
+            outrow[colChar*5]   = pair [4][(inrowChar >> 6) & 0x03];
+            outrow[colChar*5+1] = quin2[(inrowChar >> 4) & 0x07] >> 0;
+            outrow[colChar*5+2] = quad [(inrowChar >> 3) & 0x03] >> 0;
+            outrow[colChar*5+3] = quin4[(inrowChar >> 1) & 0x07] >> 0;
+            outrow[colChar*5+4] = pair [2][(inrowChar >> 0) & 0x03];
         }
         break;
 
-    case 6:  /* Compound of 2 and 3 */ 
-        for (colChar = 0; colChar < inColChars; ++colChar) { 
-            unsigned char const hi = dbl[(inrow[colChar] & 0xF0) >> 4];
-            unsigned char const lo = dbl[(inrow[colChar] & 0x0F) >> 0];
+    case 6:  /* Compound of 2 and 3 */
+        for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
+            unsigned char const hi = dbl[(inrowChar & 0xF0) >> 4];
+            unsigned char const lo = dbl[(inrowChar & 0x0F) >> 0];
 
-            outrow[colChar*6]   = trp1[(hi & 0xF0) >> 5];
+            outrow[colChar*6]   = trp1[hi >> 5];
             outrow[colChar*6+1] = trp2[(hi >> 2) & 0x0F];
             outrow[colChar*6+2] = trp3[hi & 0x07];
 
-            outrow[colChar*6+3] = trp1[(lo & 0xF0) >> 5];
+            outrow[colChar*6+3] = trp1[lo >> 5];
             outrow[colChar*6+4] = trp2[(lo >> 2) & 0x0F];
             outrow[colChar*6+5] = trp3[lo & 0x07];
         }
-        break;  
+        break;
 
     case 7:
-        for (colChar = 0; colChar < inColChars; ++colChar) { 
+        /* This approach can be used for other scale values.
+           Good for architectures which provide wide registers
+           such as SSE.
+        */
+        for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
             uint32_t hi, lo;
 
-            hi = inrow[colChar] >> 4;
+            hi = inrowChar >> 4;
             hi = ((((hi>>1) * 0x00082080) | (0x01 & hi)) & 0x00204081 ) * 0x7F;
             hi >>= 4;
             outrow[colChar*7]   =  (unsigned char) ( hi >> 16);
             outrow[colChar*7+1] =  (unsigned char) ((hi >>  8) & 0xFF);
             outrow[colChar*7+2] =  (unsigned char) ((hi >>  0) & 0xFF);
 
-            lo = inrow[colChar] & 0x001F;
+            lo = inrowChar & 0x001F;
             lo = ((((lo>>1) * 0x02082080) | (0x01 & lo)) & 0x10204081 ) * 0x7F;
             outrow[colChar*7+3] =  (unsigned char) ((lo >> 24) & 0xFF);
-            outrow[colChar*7+4] =  (unsigned char) ((lo >> 16) & 0xFF); 
+            outrow[colChar*7+4] =  (unsigned char) ((lo >> 16) & 0xFF);
             outrow[colChar*7+5] =  (unsigned char) ((lo >>  8) & 0xFF);
             outrow[colChar*7+6] =  (unsigned char) ((lo >>  0) & 0xFF);
         }
         break;
 
     case 8:
-        for (colChar = 0; colChar < inColChars; ++colChar) { 
+        for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
             unsigned int i;
             for (i = 0; i < 8; ++i) {
                 outrow[colChar*8+i] =
-                    ((inrow[colChar] >> (7-i)) & 0x01) *0xFF;
+                    ((inrowChar >> (7-i)) & 0x01) *0xFF;
             }
         }
         break;
 
     case 9:
-        for (colChar = 0; colChar < inColChars; ++colChar) { 
-            outrow[colChar*9]   =  ((inrow[colChar] >> 7) & 0x01) * 0xFF;
-            outrow[colChar*9+1] =  pair[(inrow[colChar] >> 6) & 0x03] >> 1; 
-            outrow[colChar*9+2] =  pair[(inrow[colChar] >> 5) & 0x03] >> 2; 
-            outrow[colChar*9+3] =  pair[(inrow[colChar] >> 4) & 0x03] >> 3; 
-            outrow[colChar*9+4] =  pair[(inrow[colChar] >> 3) & 0x03] >> 4; 
-            outrow[colChar*9+5] =  pair[(inrow[colChar] >> 2) & 0x03] >> 5; 
-            outrow[colChar*9+6] =  pair[(inrow[colChar] >> 1) & 0x03] >> 6; 
-            outrow[colChar*9+7] =  pair[(inrow[colChar] >> 0) & 0x03] >> 7; 
-            outrow[colChar*9+8] =  (inrow[colChar] & 0x01) * 0xFF;
+        for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
+            outrow[colChar*9]   =  ((inrowChar >> 7) & 0x01) * 0xFF;
+            outrow[colChar*9+1] =  pair[0][(inrowChar >> 6) & 0x03];
+            outrow[colChar*9+2] =  pair[1][(inrowChar >> 5) & 0x03];
+            outrow[colChar*9+3] =  pair[2][(inrowChar >> 4) & 0x03];
+            outrow[colChar*9+4] =  pair[3][(inrowChar >> 3) & 0x03];
+            outrow[colChar*9+5] =  pair[4][(inrowChar >> 2) & 0x03];
+            outrow[colChar*9+6] =  pair[5][(inrowChar >> 1) & 0x03];
+            outrow[colChar*9+7] =  pair[6][(inrowChar >> 0) & 0x03];
+            outrow[colChar*9+8] =  (inrowChar & 0x01) * 0xFF;
         }
         break;
 
     case 10:
-        for (colChar = 0; colChar < inColChars; ++colChar) { 
-            outrow[colChar*10]   = ((inrow[colChar] >> 7) & 0x01 ) * 0xFF;
-            outrow[colChar*10+1] = pair[(inrow[colChar] >> 6) & 0x03] >> 2; 
-            outrow[colChar*10+2] = pair[(inrow[colChar] >> 5) & 0x03] >> 4; 
-            outrow[colChar*10+3] = pair[(inrow[colChar] >> 4) & 0x03] >> 6;
-            outrow[colChar*10+4] = ((inrow[colChar] >> 4) & 0x01) * 0xFF;
-            outrow[colChar*10+5] = ((inrow[colChar] >> 3) & 0x01) * 0xFF; 
-            outrow[colChar*10+6] = pair[(inrow[colChar] >> 2) & 0x03] >> 2; 
-            outrow[colChar*10+7] = pair[(inrow[colChar] >> 1) & 0x03] >> 4; 
-            outrow[colChar*10+8] = pair[(inrow[colChar] >> 0) & 0x03] >> 6; 
-            outrow[colChar*10+9] = ((inrow[colChar] >> 0) & 0x01) * 0xFF;
+        for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
+            outrow[colChar*10]   = ((inrowChar >> 7) & 0x01 ) * 0xFF;
+            outrow[colChar*10+1] = pair[1][(inrowChar >> 6) & 0x03];
+            outrow[colChar*10+2] = quad[(inrowChar >> 5) & 0x03];
+            outrow[colChar*10+3] = pair[5][(inrowChar >> 4) & 0x03];
+            outrow[colChar*10+4] = ((inrowChar >> 4) & 0x01) * 0xFF;
+            outrow[colChar*10+5] = ((inrowChar >> 3) & 0x01) * 0xFF;
+            outrow[colChar*10+6] = pair[1][(inrowChar >> 2) & 0x03];
+            outrow[colChar*10+7] = quad[(inrowChar >> 1) & 0x03];
+            outrow[colChar*10+8] = pair[5][(inrowChar >> 0) & 0x03];
+            outrow[colChar*10+9] = ((inrowChar >> 0) & 0x01) * 0xFF;
+        }
+        break;
+
+    case 11:
+        for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
+            outrow[colChar*11]   = ((inrowChar >> 7) & 0x01 ) * 0xFF;
+            outrow[colChar*11+1] = pair[2][(inrowChar >> 6) & 0x03];
+            outrow[colChar*11+2] = pair[5][(inrowChar >> 5) & 0x03];
+            outrow[colChar*11+3] = ((inrowChar >> 5) & 0x01) * 0xFF;
+            outrow[colChar*11+4] = pair[0][(inrowChar >> 4) & 0x03];
+            outrow[colChar*11+5] = quad[(inrowChar >> 3) & 0x03];
+            outrow[colChar*11+6] = pair[6][(inrowChar >> 2) & 0x03];
+            outrow[colChar*11+7] = ((inrowChar >> 2) & 0x01) * 0xFF;
+            outrow[colChar*11+8] = pair[1][(inrowChar >> 1) & 0x03];
+            outrow[colChar*11+9] = pair[4][(inrowChar >> 0) & 0x03];
+            outrow[colChar*11+10] = ((inrowChar >> 0) & 0x01) * 0xFF;
+        }
+        break;
+
+    case 12:
+        for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
+            outrow[colChar*12+ 0] = ((inrowChar >> 7) & 0x01) * 0xFF;
+            outrow[colChar*12+ 1] = quad[(inrowChar >> 6) & 0x03];
+            outrow[colChar*12+ 2] = ((inrowChar >> 6) & 0x01) * 0xFF;
+            outrow[colChar*12+ 3] = ((inrowChar >> 5) & 0x01) * 0xFF;
+            outrow[colChar*12+ 4] = quad[(inrowChar >> 4) & 0x03];
+            outrow[colChar*12+ 5] = ((inrowChar >> 4) & 0x01) * 0xFF;
+            outrow[colChar*12+ 6] = ((inrowChar >> 3) & 0x01) * 0xFF;
+            outrow[colChar*12+ 7] = quad[(inrowChar >> 2) & 0x03];
+            outrow[colChar*12+ 8] = ((inrowChar >> 2) & 0x01) * 0xFF;
+            outrow[colChar*12+ 9] = ((inrowChar >> 1) & 0x01) * 0xFF;
+            outrow[colChar*12+10] = quad[(inrowChar >> 0) & 0x03];
+            outrow[colChar*12+11] = ((inrowChar >> 0) & 0x01) * 0xFF;
+        }
+        break;
+
+    case 13:
+      /* Math quiz: 13 is the last entry here.
+         Is this an arbitrary choice?
+         Or is there something which makes 13 necessary?
+
+         If you like working on questions like this you may like
+         number/group theory.  However don't expect a straightforward
+         answer from a college math textbook.  - afu
+      */
+         for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned char const inrowChar = inrow[colChar];
+            outrow[colChar*13+ 0] = ((inrowChar >> 7) & 0x01) * 0xFF;
+            outrow[colChar*13+ 1] = pair[4][(inrowChar >> 6) & 0x03];
+            outrow[colChar*13+ 2] = ((inrowChar >> 6) & 0x01) * 0xFF;
+            outrow[colChar*13+ 3] = pair[1][(inrowChar >> 5) & 0x03];
+            outrow[colChar*13+ 4] = pair[6][(inrowChar >> 4) & 0x03];
+            outrow[colChar*13+ 5] = ((inrowChar >> 4) & 0x01) * 0xFF;
+            outrow[colChar*13+ 6] = quad[(inrowChar >> 3) & 0x03];
+            outrow[colChar*13+ 7] = ((inrowChar >> 3) & 0x01) * 0xFF;
+            outrow[colChar*13+ 8] = pair[0][(inrowChar >> 2) & 0x03];
+            outrow[colChar*13+ 9] = pair[5][(inrowChar >> 1) & 0x03];
+            outrow[colChar*13+10] = ((inrowChar >> 1) & 0x01) * 0xFF;
+            outrow[colChar*13+11] = pair[2][(inrowChar >> 0) & 0x03];
+            outrow[colChar*13+12] = ((inrowChar >> 0) & 0x01) * 0xFF;
         }
         break;
- 
 
     default:
-        /*  Unlike the above cases, we iterate through outrow.  To compute the
-            color composition of each outrow byte, we consult a single bit or
-            two consecutive bits in inrow.
+        pm_error("Internal error.  Invalid scale factor for "
+                 "enlargePbmRowHorizontallySmall: %u", xScaleFactor);
+    }
+}
+
+
+/*
+  General method for scale values 14 and above
+
+  First notice that all output characters are either entirely 0, entirely 1
+  or a combination with the change from 0->1 or 1->0 happening only once.
+  (Sequences like 00111000 never appear when scale value is above 8).
+
+  Let us call the chars which are entirely 0 or 1 "solid" and those which
+  may potentially contain both "transitional".   For scale values 6 - 14
+  each input character expands to output characters aligned as follows:
+
+  6 : TTTTTT
+  7 : TTTTTTT
+  8 : SSSSSSSS
+  9 : STTTTTTTS
+  10: STSTSSTSTS
+  11: STTSTTTSTTS
+  12: STSSTSSTSSTS
+  13: STSTTSTSTTSTS
+  14: STSTSTSSTSTSTS
+
+  Above 15 we insert strings of solid chars as necessary:
+
+  22: SsTSsTSsTSsSsTSsTSsTSs
+  30: SssTSssTSssTSssSssTSssTSssTSss
+  38: SsssTSsssTSsssTSsssSsssTSsssTSsssTSsss
+*/
 
-            Color changes never happen twice in a single outrow byte.
 
-            This is a generalization of above routines for scale factors
-            9 and 10.
+struct OffsetInit {
+  unsigned int scale;
+  const char * alignment;
+};
+
+
+static struct OffsetInit const offsetInit[8] = {
+  /* 0: single char copied from output of enlargePbmRowHorizontallySmall()
+     1: stretch of solid chars
+
+     Each entry is symmetrical left-right and has exactly eight '2's
+   */
+
+  {  8, "22222222" },               /* 8n+0 */
+  {  9, "21121212121212112" },      /* 8n+1 */
+  { 10, "211212112211212112" },     /* 8n+2 */
+  { 11, "2112121121211212112" },    /* 8n+3 */
+  {  4, "212212212212" },           /* 8n+4 */
+  { 13, "211211211212112112112" },  /* 8n+5 */
+  {  6, "21212122121212" },         /* 8n+6 */
+  {  7, "212121212121212" }         /* 8n+7 */
+};
+
+  /*   Relationship between 'S' 'T' in previous comment and '1' '2' here
+
+     11: STTSTTTSTTS
+     19: sSTsTsSTsTsTSsTsTSs
+         2112121121211212112           # table entry for 8n+3
+     27: ssSTssTssSTssTssTSssTssTSss
+         2*112*12*112*12*112*12*112*
+     35: sssSTsssTsssSTsssTsssTSsssTsssTSsss
+         2**112**12**112**12**112**12**112**
+     42: ssssSTssssTssssSTssssTssssTSssssTssssTSssss
+         2***112***12***112***12***112***12***112***
+  */
+
+
+struct OffsetTable {
+    unsigned int offsetSolid[8];
+    unsigned int offsetTrans[13];
+    unsigned int scale;
+    unsigned int solidChars;
+};
+
 
-            Logic works for scale factors 4, 6, 7, 8, and above (but not 5).
+
+static void
+setupOffsetTable(unsigned int         const xScaleFactor,
+                 struct OffsetTable * const offsetTableP) {
+
+    unsigned int i, j0, j1, dest;
+    struct OffsetInit const classEntry = offsetInit[xScaleFactor % 8];
+    unsigned int const scale = classEntry.scale;
+    unsigned int const solidChars = xScaleFactor / 8 - (scale > 8 ? 1 : 0);
+
+    for (i = j0 = j1 = dest = 0; classEntry.alignment[i] != '\0'; ++i) {
+      switch (classEntry.alignment[i]) {
+        case '1': offsetTableP->offsetTrans[j0++] = dest++;
+                  break;
+
+        case '2': offsetTableP->offsetSolid[j1++] = dest;
+                  dest += solidChars;
+                  break;
+
+        default:  pm_error("Internal error. Abnormal alignment value");
+        }
+    }
+
+    offsetTableP->scale = scale;
+    offsetTableP->solidChars = solidChars;
+}
+
+
+
+static void
+enlargePbmRowFractional(unsigned char         const inrowChar,
+                        unsigned int          const outColChars,
+                        unsigned int          const xScaleFactor,
+                        unsigned char       * const outrow,
+                        struct OffsetTable  * const tableP) {
+
+/*----------------------------------------------------------------------------
+  Routine called from enlargePbmRowHorizontallyGen() to process the final
+  fractional inrow char.
+
+  outrow : write position for this function (not left edge of entire row)
+----------------------------------------------------------------------------*/
+
+    unsigned int i;
+
+    /* Deploy (most) solid chars */
+
+    for (i = 0; i < 7; ++i) {
+        unsigned int j;
+        unsigned char const bit8 = (inrowChar >> (7 - i) & 0x01) * 0xFF;
+
+        if (tableP->offsetSolid[i] >= outColChars)
+            break;
+        else
+            for (j = 0; j < tableP->solidChars; ++j) {
+                outrow[j + tableP->offsetSolid[i]] = bit8;
+            }
+     }
+    /* If scale factor is a multiple of 8 we are done. */
+
+    if (tableP->scale != 8) {
+        unsigned char outrowTemp[16];
+
+        enlargePbmRowHorizontallySmall(&inrowChar, 1,
+                                       tableP->scale, outrowTemp);
+
+        for (i = 0 ; i < tableP->scale; ++i) {
+            unsigned int const offset = tableP->offsetTrans[i];
+            if (offset >= outColChars)
+                break;
+            else
+                outrow[offset] = outrowTemp[i];
+            }
+
+    }
+
+}
+
+
+
+static void
+enlargePbmRowHorizontallyGen(const unsigned char * const inrow,
+                             unsigned int          const inColChars,
+                             unsigned int          const outColChars,
+                             unsigned int          const xScaleFactor,
+                             unsigned char       * const outrow,
+                             struct OffsetTable  * const tableP) {
+
+/*----------------------------------------------------------------------------
+  We iterate through inrow.
+  Output chars are deployed according to offsetTable.
+
+  All transitional chars and some solid chars are determined by calling
+  one the fast routines in enlargePbmRowHorizontallySmall().
+----------------------------------------------------------------------------*/
+    unsigned int colChar;
+    unsigned int const fullColChars =
+        inColChars - ((inColChars * xScaleFactor == outColChars) ? 0 : 1);
+
+    for (colChar = 0; colChar < fullColChars; ++colChar) {
+        unsigned char const inrowChar = inrow[colChar];
+        char bit8[8];
+        unsigned int i;
+
+        /* Deploy most solid chars
+           Some scale factors yield uneven string lengths: in such
+           cases we don't handle the odd solid chars at this point
         */
 
-        for (colChar = 0; colChar < outColChars; ++colChar) {
-            unsigned int const mult = scaleFactor;
-            unsigned int const mod = colChar % mult;
-            unsigned int const bit = (mod*8)/mult;
-            /* source bit position, leftmost=0 */
-            unsigned int const offset = mult - (mod*8)%mult;
-            /* number of outrow bits derived from the same
-               "source" inrow bit, starting at and to the right
-               of leftmost bit of outrow byte, inclusive
-            */
-
-            if (offset >= 8)  /* Bits in outrow byte are all 1 or 0 */
-                outrow[colChar] =
-                    (inrow[colChar/mult] >> (7-bit) & 0x01) * 0xFF;
-            else           /* Two inrow bits influence this outrow byte */ 
-                outrow[colChar] = (unsigned char)
-                    (pair[inrow[colChar/mult] >> (6-bit) & 0x03] >> offset)
-                    & 0xFF;
+        for (i = 0; i < 8; ++i)
+            bit8[i] = (inrowChar >> (7 - i) & 0x01) * 0xFF;
+
+        for (i = 0; i < tableP->solidChars; ++i) {
+            unsigned int base = colChar * xScaleFactor + i;
+            outrow[base]              = bit8[0];
+            outrow[base + tableP->offsetSolid[1]] = bit8[1];
+            outrow[base + tableP->offsetSolid[2]] = bit8[2];
+            outrow[base + tableP->offsetSolid[3]] = bit8[3];
+            outrow[base + tableP->offsetSolid[4]] = bit8[4];
+            outrow[base + tableP->offsetSolid[5]] = bit8[5];
+            outrow[base + tableP->offsetSolid[6]] = bit8[6];
+            outrow[base + tableP->offsetSolid[7]] = bit8[7];
+        }
+
+        /* If scale factor is a multiple of 8 we are done */
+
+        if (tableP->scale != 8) {
+            /* Deploy transitional chars and any remaining solid chars */
+            unsigned char outrowTemp[16];
+            unsigned int base = colChar * xScaleFactor;
+
+            enlargePbmRowHorizontallySmall(&inrowChar, 1,
+                                           tableP->scale, outrowTemp);
+
+            /* There are at least 4 valid entries in offsetTrans[] */
+
+            outrow[base + tableP->offsetTrans[0]] = outrowTemp[0];
+            outrow[base + tableP->offsetTrans[1]] = outrowTemp[1];
+            outrow[base + tableP->offsetTrans[2]] = outrowTemp[2];
+            outrow[base + tableP->offsetTrans[3]] = outrowTemp[3];
+
+            for (i = 4; i < tableP->scale; ++i)
+                outrow[base + tableP->offsetTrans[i]] = outrowTemp[i];
         }
     }
+
+    /* Process the fractional final inrow byte */
+
+     if (fullColChars < inColChars) {
+        unsigned int  const start = fullColChars * xScaleFactor;
+        unsigned char const inrowLast = inrow[inColChars -1];
+
+        enlargePbmRowFractional(inrowLast, outColChars - start,
+                                xScaleFactor, &outrow[start], tableP);
+        }
+
 }
 
 
 
 static void
 enlargePbm(struct pam * const inpamP,
-           unsigned int const scaleFactor,
+           unsigned int const xScaleFactor,
+           unsigned int const yScaleFactor,
            FILE *       const ofP) {
 
-    unsigned char * inrow;
-    unsigned char * outrow;
+    enum ScaleMethod {METHOD_USEINPUT, METHOD_SMALL, METHOD_GENERAL};
+    enum ScaleMethod const scaleMethod =
+        xScaleFactor == 1  ? METHOD_USEINPUT :
+        xScaleFactor <= 13 ? METHOD_SMALL : METHOD_GENERAL;
 
-    unsigned int row;
-
-    unsigned int const outcols = inpamP->width * scaleFactor;
-    unsigned int const outrows = inpamP->height * scaleFactor;
+    unsigned int const outcols = inpamP->width * xScaleFactor;
+    unsigned int const outrows = inpamP->height * yScaleFactor;
     unsigned int const inColChars  = pbm_packed_bytes(inpamP->width);
     unsigned int const outColChars = pbm_packed_bytes(outcols);
 
+    unsigned char * inrow;
+    unsigned char * outrow;
+    unsigned int row;
+    struct OffsetTable offsetTable;
+
     inrow  = pbm_allocrow_packed(inpamP->width);
-    
-    if (scaleFactor == 1)
+
+    if (scaleMethod == METHOD_USEINPUT)
         outrow = inrow;
-    else  {
-        /* Allow writes beyond outrow data end when scaleFactor is
-           one of the special fast cases: 2, 3, 4, 5, 6, 7, 8, 9, 10.
+    else {
+        /* Allow writes beyond outrow data end when using the table method.
         */
-        unsigned int const rightPadding = 
-            scaleFactor > 10 ? 0 : (scaleFactor - 1) * 8;  
+        unsigned int const rightPadding =
+            scaleMethod == METHOD_GENERAL ? 0 : (xScaleFactor - 1) * 8;
+
         outrow = pbm_allocrow_packed(outcols + rightPadding);
+
+        if (scaleMethod == METHOD_GENERAL)
+            setupOffsetTable(xScaleFactor, &offsetTable);
     }
 
     pbm_writepbminit(ofP, outcols, outrows, 0);
-    
+
     for (row = 0; row < inpamP->height; ++row) {
         unsigned int i;
 
         pbm_readpbmrow_packed(inpamP->file, inrow, inpamP->width,
                               inpamP->format);
 
-        if (outcols % 8 > 0)           /* clean final partial byte */ 
+        if (outcols % 8 > 0)           /* clean final partial byte */
             pbm_cleanrowend_packed(inrow, inpamP->width);
 
-        enlargePbmRowHorizontally(inpamP, inrow, inColChars, outColChars,
-                                  scaleFactor, outrow);
+        switch (scaleMethod) {
+        case METHOD_USEINPUT:
+            /* Nothing to do */
+            break;
+        case METHOD_SMALL:
+            enlargePbmRowHorizontallySmall(inrow, inColChars,
+                                           xScaleFactor, outrow);
+            break;
+        case METHOD_GENERAL:
+            enlargePbmRowHorizontallyGen(inrow, inColChars, outColChars,
+                                         xScaleFactor, outrow,
+                                         &offsetTable);
+            break;
+        }
 
-        for (i = 0; i < scaleFactor; ++i)  
+        for (i = 0; i < yScaleFactor; ++i)
             pbm_writepbmrow_packed(ofP, outrow, outcols, 0);
     }
-    
+
     if (outrow != inrow)
         pbm_freerow(outrow);
 
@@ -332,7 +736,8 @@ enlargePbm(struct pam * const inpamP,
 
 static void
 enlargeGeneral(struct pam * const inpamP,
-               unsigned int const scaleFactor,
+               unsigned int const xScaleFactor,
+               unsigned int const yScaleFactor,
                FILE *       const ofP) {
 /*----------------------------------------------------------------------------
    Enlarge the input image described by *pamP.
@@ -342,15 +747,15 @@ enlargeGeneral(struct pam * const inpamP,
    This works on all kinds of images, but is slower than enlargePbm on
    PBM.
 -----------------------------------------------------------------------------*/
-    struct pam outpam; 
+    struct pam outpam;
     tuple * tuplerow;
     tuple * newtuplerow;
     unsigned int row;
 
-    outpam = *inpamP; 
+    outpam = *inpamP;
     outpam.file   = ofP;
-    outpam.width  = inpamP->width  * scaleFactor;
-    outpam.height = inpamP->height * scaleFactor; 
+    outpam.width  = inpamP->width  * xScaleFactor;
+    outpam.height = inpamP->height * yScaleFactor;
 
     pnm_writepaminit(&outpam);
 
@@ -360,7 +765,7 @@ enlargeGeneral(struct pam * const inpamP,
 
     for (row = 0; row < inpamP->height; ++row) {
         pnm_readpamrow(inpamP, tuplerow);
-        pnm_writepamrowmult(&outpam, newtuplerow, scaleFactor);
+        pnm_writepamrowmult(&outpam, newtuplerow, yScaleFactor);
     }
 
     free(newtuplerow);
@@ -371,10 +776,10 @@ enlargeGeneral(struct pam * const inpamP,
 
 
 int
-main(int           argc, 
-     const char ** argv) {
+main(int           argc,
+     const char ** const argv) {
 
-    struct cmdlineInfo cmdline;
+    struct CmdlineInfo cmdline;
     FILE * ifP;
     struct pam inpam;
 
@@ -383,16 +788,21 @@ main(int           argc,
     parseCommandLine(argc, argv, &cmdline);
 
     ifP = pm_openr(cmdline.inputFilespec);
- 
+
     pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
-    
+
+    assert(cmdline.xScaleFactor > 0);
+    assert(cmdline.yScaleFactor > 0);
+
     validateComputableDimensions(inpam.width, inpam.height,
-                                 cmdline.scaleFactor); 
-    
+                                 cmdline.xScaleFactor, cmdline.yScaleFactor);
+
     if (PNM_FORMAT_TYPE(inpam.format) == PBM_TYPE)
-        enlargePbm(&inpam, cmdline.scaleFactor, stdout);
+        enlargePbm(&inpam, cmdline.xScaleFactor, cmdline.yScaleFactor,
+                   stdout);
     else
-        enlargeGeneral(&inpam, cmdline.scaleFactor, stdout);
+        enlargeGeneral(&inpam, cmdline.xScaleFactor, cmdline.yScaleFactor,
+                       stdout);
 
     pm_close(ifP);
     pm_close(stdout);
@@ -400,3 +810,4 @@ main(int           argc,
     return 0;
 }
 
+
diff --git a/editor/pamhue.c b/editor/pamhue.c
new file mode 100644
index 00000000..7dddedd8
--- /dev/null
+++ b/editor/pamhue.c
@@ -0,0 +1,209 @@
+/*=============================================================================
+                                  pamhue
+===============================================================================
+  Change the hue, the Hue-Saturation-Value model, every pixel in an image
+  by a specified angle.
+=============================================================================*/
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <math.h>
+#include "mallocvar.h"
+#include "shhopt.h"
+#include "pam.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 */
+    float        huechange;
+};
+
+
+
+static void
+parseCommandLine(int                        argc,
+                 const char **              argv,
+                 struct CmdlineInfo * const 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;
+
+    unsigned int huechangeSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0,  "huechange",          OPT_FLOAT,
+            &cmdlineP->huechange,           &huechangeSpec,             0);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = false;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = false;    /* No negative arguments */
+
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (!huechangeSpec)
+        pm_error("You must specify -huechange");
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFileName = argv[1];
+    else
+        pm_error("Program takes at most one argument:  file specification");
+}
+
+
+
+static float
+positiveMod(float const arg,
+            float const modulus) {
+/*----------------------------------------------------------------------------
+   'arg' mod 'modulus', but positive (i.e. in the range 0.0 - 'modulus').
+-----------------------------------------------------------------------------*/
+    float const mod = fmodf(arg, modulus);
+
+    return mod >= 0.0 ? mod : 360 + mod;
+}
+
+
+
+static void
+changeHue(tuple   const tupleval,
+          float   const huechange,
+          sample  const maxval) {
+
+    pixel oldRgb, newRgb;
+    struct hsv oldHsv, newHsv;
+
+    PPM_PUTR(oldRgb, tupleval[PAM_RED_PLANE]);
+    PPM_PUTG(oldRgb, tupleval[PAM_GRN_PLANE]);
+    PPM_PUTB(oldRgb, tupleval[PAM_BLU_PLANE]);
+
+    oldHsv = ppm_hsv_from_color(oldRgb, maxval);
+
+    newHsv.h = positiveMod(oldHsv.h + huechange, 360.0);
+    newHsv.s = oldHsv.s;
+    newHsv.v = oldHsv.v;
+
+    newRgb = ppm_color_from_hsv(newHsv, maxval);
+
+    tupleval[PAM_RED_PLANE] = PPM_GETR(newRgb);
+    tupleval[PAM_GRN_PLANE] = PPM_GETG(newRgb);
+    tupleval[PAM_BLU_PLANE] = PPM_GETB(newRgb);
+}
+
+
+
+static void
+convertRow(tuple *            const tuplerow,
+           float              const huechange,
+           const struct pam * const pamP) {
+
+    unsigned int col;
+
+    for (col = 0; col < pamP->width; ++col)  {
+        if ((pamP->format == PPM_FORMAT) || (pamP->format == RPPM_FORMAT) ||
+                 ((pamP->format == PAM_FORMAT) && (pamP->depth >= 3))) {
+            /* It's a color image, so there is a hue to change */
+
+            changeHue(tuplerow[col], huechange, pamP->maxval);
+        } else {
+            /* It's black and white or grayscale, which means fully
+               desaturated, so hue is meaningless.  Nothing to change.
+            */
+        }
+    }
+}
+
+
+
+static void
+pamhue(struct CmdlineInfo const cmdline,
+       FILE *             const ifP,
+       FILE *             const ofP) {
+
+    struct pam inpam, outpam;
+    tuple * tuplerow;
+    unsigned int row;
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    outpam = inpam;
+    outpam.file = ofP;
+
+    pnm_writepaminit(&outpam);
+
+    tuplerow = pnm_allocpamrow(&inpam);
+
+    for (row = 0; row < inpam.height; ++row) {
+        pnm_readpamrow(&inpam, tuplerow);
+
+        convertRow(tuplerow, cmdline.huechange, &inpam);
+
+        pnm_writepamrow(&outpam, tuplerow);
+    }
+
+    pnm_freepamrow(tuplerow);
+}
+
+
+
+int
+main(int argc, const char *argv[]) {
+
+    struct CmdlineInfo cmdline;
+    FILE * ifP;
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    pamhue(cmdline, ifP, stdout);
+
+    pm_close(ifP);
+    pm_close(stdout);
+
+    return 0;
+}
+
+
+
+/* This was derived by Bryan Henderson from code by Willem van Schaik
+   (willem@schaik.com), which was derived from Ppmbrighten code written by Jef
+   Poskanzer and Brian Moffet.
+
+   Copyright (C) 1989 by Jef Poskanzer.
+   Copyright (C) 1990 by Brian Moffet.
+   Copyright (C) 2019 by Willem van Schaik (willem@schaik.com)
+
+   Permission to use, copy, modify, and distribute this software and its
+   documentation for any purpose and without fee is hereby granted, provided
+   that the above copyright notice appear in all copies and that both that
+   copyright notice and this permission notice appear in supporting
+   documentation.  This software is provided "as is" without express or
+   implied warranty.
+
+   Bryan contributes his work to the public domain.
+*/
diff --git a/editor/pamscale.c b/editor/pamscale.c
index 27902dbf..d8436689 100644
--- a/editor/pamscale.c
+++ b/editor/pamscale.c
@@ -47,7 +47,7 @@
 ** (sinc, bessel) are IIR (infinite impulse respone).
 ** They should be windowed with hanning, hamming, blackman or
 ** kaiser window.
-** For sinc and bessel the blackman window will be used per default.
+** For sinc and bessel the blackman window will be used by default.
 */
 
 #define EPSILON 1e-7
@@ -70,7 +70,7 @@ pow3(double const x) {
 
 
 /* box, pulse, Fourier window, */
-/* box function also know as rectangle function */
+/* box function also known as rectangle function */
 /* 1st order (constant) b-spline */
 
 #define radius_point (0.0)
@@ -417,6 +417,7 @@ struct CmdlineInfo {
      * in a form easy for the program to use.
      */
     const char * inputFileName;  /* Filespec of input file */
+    unsigned int reportonly;
     unsigned int nomix;
     basicFunction_t filterFunction; /* NULL if not using resample method */
     basicFunction_t windowFunction;
@@ -667,6 +668,8 @@ parseCommandLine(int argc,
     OPTENT3(0, "window",    OPT_STRING,  &window,    &windowSpec,          0);
     OPTENT3(0, "nomix",     OPT_FLAG,    NULL,       &cmdlineP->nomix,     0);
     OPTENT3(0, "linear",    OPT_FLAG,    NULL,       &cmdlineP->linear,    0);
+    OPTENT3(0, "reportonly",        OPT_FLAG,    NULL,
+            &cmdlineP->reportonly,    0);
 
     opt.opt_table = option_def;
     opt.short_allowed = false;  /* We have no short (old-fashioned) options */
@@ -2137,6 +2140,52 @@ scaleWithoutMixing(const struct pam * const inpamP,
 }
 
 
+static void
+skipImage(struct pam * const pamP) {
+
+        tuple * tuplerow;
+        unsigned int row;
+
+        tuplerow = pnm_allocpamrow(pamP);
+
+        for (row = 0; row < pamP->height; ++row)
+            pnm_readpamrow(pamP, tuplerow);
+
+        pnm_freepamrow(tuplerow);
+}
+
+
+
+static void
+scale(FILE *             const ifP,
+      struct pam *       const inpamP,
+      struct pam *       const outpamP,
+      float              const xscale,
+      float              const yscale,
+      struct CmdlineInfo const cmdline) {
+
+    pnm_writepaminit(outpamP);
+
+    if (cmdline.nomix) {
+        if (cmdline.verbose)
+            pm_message("Using nomix method");
+        scaleWithoutMixing(inpamP, outpamP, xscale, yscale);
+    } else if (!cmdline.filterFunction) {
+        if (cmdline.verbose)
+            pm_message("Using simple pixel mixing rescaling method");
+        scaleWithMixing(inpamP, outpamP, xscale, yscale,
+                        cmdline.linear, cmdline.verbose);
+    } else {
+        if (cmdline.verbose)
+            pm_message("Using general filter method");
+        resample(inpamP, outpamP,
+                 cmdline.filterFunction, cmdline.filterRadius,
+                 cmdline.windowFunction, cmdline.verbose,
+                 cmdline.linear);
+    }
+}
+
+
 
 static void
 pamscale(FILE *             const ifP,
@@ -2179,25 +2228,12 @@ pamscale(FILE *             const ifP,
                  "do the specified scaling.  Use a smaller input image "
                  "or a slightly different scale factor.");
 
-    pnm_writepaminit(&outpam);
-
-    if (cmdline.nomix) {
-        if (cmdline.verbose)
-            pm_message("Using nomix method");
-        scaleWithoutMixing(&inpam, &outpam, xscale, yscale);
-    } else if (!cmdline.filterFunction) {
-        if (cmdline.verbose)
-            pm_message("Using simple pixel mixing rescaling method");
-        scaleWithMixing(&inpam, &outpam, xscale, yscale,
-                        cmdline.linear, cmdline.verbose);
-    } else {
-        if (cmdline.verbose)
-            pm_message("Using general filter method");
-        resample(&inpam, &outpam,
-                 cmdline.filterFunction, cmdline.filterRadius,
-                 cmdline.windowFunction, cmdline.verbose,
-                 cmdline.linear);
-    }
+    if (cmdline.reportonly) {
+        printf("%d %d %f %f %d %d\n", inpam.width, inpam.height,
+               xscale, yscale, outpam.width, outpam.height);
+        skipImage(&inpam);
+    } else
+        scale(ifP, &inpam, &outpam, xscale, yscale, cmdline);
 }
 
 
diff --git a/editor/pamstretch-gen b/editor/pamstretch-gen
index dac3da7f..fec4469c 100755
--- a/editor/pamstretch-gen
+++ b/editor/pamstretch-gen
@@ -1,82 +1,129 @@
 #!/bin/sh
+###############################################################################
+#                              pamstretch-gen
+###############################################################################
+# A generalized version of pamstretch that can do non-integer scale factors.
 #
-# pamstretch-gen - a shell script which acts a little like a general
-# form of pamstretch, by scaling up with pamstretch then scaling
-# down with pamscale.
+# It works by scaling up with pamstretch then scaling down with pamscale.
 #
 # it also copes with N<1, but then it just uses pamscale. :-)
 #
 # Formerly named 'pnminterp-gen' and 'pnmstretch-gen'.
 #
-# Copyright (C) 1998,2000 Russell Marks.
-# 
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or (at
-# your option) any later version.
-# 
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-# 
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-# 
+###############################################################################
 
-if [ "$1" = "--version" -o "$1" = "-version" ]; then
-        pamstretch --version; exit $?;
-fi
+# Scan command line arguments
 
-if [ "$1" = "" ]; then
-  echo 'usage: pamstretch-gen N [pnmfile]'
+while true ; do
+    case "$1" in
+        -version|--version )
+        pamstretch --version; exit $?;
+        ;;
+        -p|-pl|-pla|-plai|-plain|--p|--pl|--pla|--plai|--plain )
+        plainopt="-plain"
+        shift
+        ;;
+        -q|-qu|-qui|-quie|-quiet|--q|--qu|--qui|--quie|--quiet )
+        quietopt="-plain"
+        shift
+        ;;
+        -q|-qu|-qui|-quie|-quiet|--q|--qu|--qui|--quie|--quiet )
+        quietopt="-quiet"
+        shift
+        ;;
+        -verb|-verbo|-verbos|-verbose|--verb|--verbo|--verbos|--verbose )
+        verboseopt="-verbose"
+        shift
+        ;;
+        -* )
+        echo 'usage: pamstretch-gen N [pnmfile]' 1>&2
+        exit 1
+        ;;
+        * )
+        break
+        ;;
+    esac
+done
+ 
+tempfile=$(mktemp "${TMPDIR:-/tmp}/netpbm.XXXXXXXX")
+if [ $? -ne 0 -o ! -e $tempfile ]; then
+  echo "Could not create temporary file. Exiting." 1>&2
   exit 1
 fi
+trap 'rm -rf $tempfile' 0 1 3 15
 
-tempdir=$(mktemp -d "${TMPDIR:-/tmp}/netpbm.XXXXXXXX") ||
-    ( echo "Could not create temporary file. Exiting." 1>&2; exit 1; ) 
-trap 'rm -rf $tempdir' 0 1 3 15
+case "$#" in
+    0)
+    echo "pamstretch-gen: too few arguments" 1>&2
+    exit 1
+    ;;   
+    1 )
+    if ! cat > $tempfile; then
+    echo "pamstretch-gen: error reading input" 1>&2
+    exit 1
+    fi
+    ;;
+    2 )
+    if ! cat $2 > $tempfile; then
+    echo "pamstretch-gen: error reading file "$2 1>&2
+    exit 1
+    fi
+    ;;
+    * )
+    echo "pamstretch-gen: misaligned arguments or too many arguments" 1>&2
+    exit 1
+    ;;
+esac
 
-tempfile=$tempdir/pnmig
+# Calculate pamstretch scale factor (="iscale") and output width and
+# height.  Usually "int(scale) + 1" is sufficient for iscale but
+# in some exceptional cases adjustment is necessary because of
+# "-dropedge".
 
-if ! cat $2 >$tempfile 2>/dev/null; then
-  echo 'pamstretch-gen: error reading file' 1>&2
+report=$(pamscale -reportonly $1 $tempfile)
+if [ $? -ne 0 ]; then
+  echo "pamstretch-gen: pamscale -reportonly $1 (file) failed" 1>&2
   exit 1
 fi
 
-if ! pnmfile $tempfile 1>/dev/null 2>/dev/null; then
-  echo 'Not valid pnm input'
-  exit 1
-fi
+iscale_width_height=$(echo $report |\
+  awk 'NF!=6 || $1<=0 || $2<=0 || $3<=0 || $5<=0 || $6<=0  { exit 1 }
+           { if ($3 > 1.0)  { iscale = int($3) + 1;
+                              if (iscale * ($1-1) < $5 ||
+                                  iscale * ($2-1) < $6 )
+                                     ++iscale;            }
+             else { iscale = 1 }  # $3 <= 1.0
+       }
+       { print iscale, "-width="$5, "-height="$6}' )
 
-# we use the width as indication of how much to scale; width and
-# height are being scaled equally, so this should be ok.
-width=`pnmfile $tempfile 2>/dev/null|cut -d " " -f 3`
+# Note that $1, $2, ..., $6 here are fields of the input line fed to awk,
+# not shell positional parameters.
 
-if [ "$width" = "" ]; then
-  echo 'pamstretch-gen: not a PNM file' 1>&2
-  exit 1
-fi
+iscale=${iscale_width_height% -width=* -height=*}
+width_height=${iscale_width_height#* }
+if [ -n "$verboseopt" ]; then
+    echo "pamstretch-gen: rounded scale factor=$iscale $width_height" 1>&2
+fi 
 
-# should really use dc for maths, but awk is less painful :-)
-target_width=`awk 'BEGIN{printf("%d",'0.5+"$width"*"$1"')}'`
+pamstretch -dropedge $quietopt $iscale $tempfile |\
+  pamscale $verboseopt $quietopt $plainopt $width_height
 
-# work out how far we have to scale it up with pamstretch so that the
-# new width is >= the target width.
-int_scale=`awk '
-BEGIN {
-int_scale=1;int_width='"$width"'
-while(int_width<'"$target_width"')
-  {
-  int_scale++
-  int_width+='"$width"'
-  }
-print int_scale
-}'`
 
-if [ "$int_scale" -eq 1 ]; then
-  pamscale "$1" $tempfile
-else
-  pamstretch "$int_scale" $tempfile | pnmscale -xsi "$target_width"
-fi
+# Copyright (C) 1998,2000 Russell Marks.
+# Modifications for "pamscale -reportonly" "pamstretch -dropedge" by
+# Akira Urushibata (Jan. 2019)
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or (at
+# your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
diff --git a/editor/pamstretch.c b/editor/pamstretch.c
index 04883c35..5d24e437 100644
--- a/editor/pamstretch.c
+++ b/editor/pamstretch.c
@@ -1,5 +1,5 @@
 /* pamstretch - scale up portable anymap by interpolating between pixels.
- * 
+ *
  * This program is based on 'pnminterp' by Russell Marks, rename
  * pnmstretch for inclusion in Netpbm, then rewritten and renamed to
  * pamstretch by Bryan Henderson in December 2001.
@@ -10,12 +10,12 @@
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation; either version 2 of the License, or (at
  * your option) any later version.
- * 
+ *
  * This program is distributed in the hope that it will be useful, but
  * WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  * General Public License for more details.
- * 
+ *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
  * Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */
@@ -27,33 +27,35 @@
 #include <limits.h>
 
 #include "pm_c_util.h"
-#include "pam.h"
+#include "mallocvar.h"
+#include "nstring.h"
 #include "shhopt.h"
+#include "pam.h"
 
-enum an_edge_mode {
+enum EdgeMode {
     EDGE_DROP,
         /* drop one (source) pixel at right/bottom edges. */
     EDGE_INTERP_TO_BLACK,
         /* interpolate right/bottom edge pixels to black. */
     EDGE_NON_INTERP
-        /* don't interpolate right/bottom edge pixels 
+        /* don't interpolate right/bottom edge pixels
            (default, and what zgv does). */
 };
 
 
-struct cmdline_info {
+struct CmdlineInfo {
     /* All the information the user supplied in the command line,
        in a form easy for the program to use.
     */
-    const char *input_filespec;  /* Filespecs of input files */
-    enum an_edge_mode edge_mode;
+    const char * inputFileName;  /* Filespecs of input files */
+    enum EdgeMode edgeMode;
     unsigned int xscale;
     unsigned int yscale;
 };
 
 
 
-tuple blackTuple;  
+tuple blackTuple;
    /* A "black" tuple.  Unless our input image is PBM, PGM, or PPM, we
       don't really know what "black" means, so this is just something
       arbitrary in that case.
@@ -61,116 +63,125 @@ tuple blackTuple;
 
 
 static void
-parse_command_line(int argc, char ** argv,
-                   struct cmdline_info *cmdline_p) {
+parseCommandLine(int argc, const char ** argv,
+                 struct CmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
-   Note that the file spec array we return is stored in the storage that
+   Note that the file name array we return is stored in the storage that
    was passed to us as the argv array.
 -----------------------------------------------------------------------------*/
     optStruct3 opt;  /* set by OPTENT3 */
-    optEntry *option_def = malloc(100*sizeof(optEntry));
+    optEntry * option_def;
     unsigned int option_def_index;
 
     unsigned int blackedge;
     unsigned int dropedge;
-    unsigned int xscale_spec;
-    unsigned int yscale_spec;
+    unsigned int xscaleSpec;
+    unsigned int yscaleSpec;
+
+    MALLOCARRAY(option_def, 100);
 
     option_def_index = 0;   /* incremented by OPTENTRY */
     OPTENT3('b', "blackedge",    OPT_FLAG, NULL, &blackedge,            0);
     OPTENT3('d', "dropedge",     OPT_FLAG, NULL, &dropedge,             0);
-    OPTENT3(0,   "xscale",       OPT_UINT, 
-            &cmdline_p->xscale, &xscale_spec, 0);
-    OPTENT3(0,   "yscale",       OPT_UINT, 
-            &cmdline_p->yscale, &yscale_spec, 0);
+    OPTENT3(0,   "xscale",       OPT_UINT,
+            &cmdlineP->xscale, &xscaleSpec, 0);
+    OPTENT3(0,   "yscale",       OPT_UINT,
+            &cmdlineP->yscale, &yscaleSpec, 0);
 
     opt.opt_table = option_def;
     opt.short_allowed = FALSE; /* We have some short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
-        /* Uses and sets argc, argv, and some of *cmdline_p and others. */
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
-    if (blackedge && dropedge) 
+    if (blackedge && dropedge)
         pm_error("Can't specify both -blackedge and -dropedge options.");
     else if (blackedge)
-        cmdline_p->edge_mode = EDGE_INTERP_TO_BLACK;
+        cmdlineP->edgeMode = EDGE_INTERP_TO_BLACK;
     else if (dropedge)
-        cmdline_p->edge_mode = EDGE_DROP;
+        cmdlineP->edgeMode = EDGE_DROP;
     else
-        cmdline_p->edge_mode = EDGE_NON_INTERP;
+        cmdlineP->edgeMode = EDGE_NON_INTERP;
 
-    if (xscale_spec && cmdline_p->xscale == 0)
+    if (xscaleSpec && cmdlineP->xscale == 0)
         pm_error("You specified zero for the X scale factor.");
-    if (yscale_spec && cmdline_p->yscale == 0)
+    if (yscaleSpec && cmdlineP->yscale == 0)
         pm_error("You specified zero for the Y scale factor.");
 
-    if (xscale_spec && !yscale_spec)
-        cmdline_p->yscale = 1;
-    if (yscale_spec && !xscale_spec)
-        cmdline_p->xscale = 1;
+    if (xscaleSpec && !yscaleSpec)
+        cmdlineP->yscale = 1;
+    if (yscaleSpec && !xscaleSpec)
+        cmdlineP->xscale = 1;
 
-    if (!(xscale_spec || yscale_spec)) {
+    if (!(xscaleSpec || yscaleSpec)) {
         /* scale must be specified in an argument */
-        if ((argc-1) != 1 && (argc-1) != 2)
+        if (argc-1 != 1 && argc-1 != 2)
             pm_error("Wrong number of arguments (%d).  Without scale options, "
                      "you must supply 1 or 2 arguments:  scale and "
                      "optional file specification", argc-1);
-        
+
         {
-            char *endptr;   /* ptr to 1st invalid character in scale arg */
+            const char * error;   /* error message of pm_string_to_uint */
             unsigned int scale;
-            
-            scale = strtol(argv[1], &endptr, 10);
-            if (*argv[1] == '\0') 
-                pm_error("Scale argument is a null string.  "
-                         "Must be a number.");
-            else if (*endptr != '\0')
-                pm_error("Scale argument contains non-numeric character '%c'.",
-                         *endptr);
-            else if (scale < 2)
-                pm_error("Scale argument must be at least 2.  "
-                         "You specified %d", scale);
-            cmdline_p->xscale = scale;
-            cmdline_p->yscale = scale;
+
+            pm_string_to_uint(argv[1], &scale, &error);
+
+            if (error)
+                pm_error("Invalid scale factor: %s", error);
+            else {
+                if (scale < 1)
+                    pm_error("Scale argument must be at least 1.  "
+                             "You specified %d", scale);
+                cmdlineP->xscale = scale;
+                cmdlineP->yscale = scale;
+            }
         }
-        if (argc-1 > 1) 
-            cmdline_p->input_filespec = argv[2];
+
+        if (argc-1 > 1)
+            cmdlineP->inputFileName = argv[2];
         else
-            cmdline_p->input_filespec = "-";
+            cmdlineP->inputFileName = "-";
     } else {
         /* No scale argument allowed */
-        if ((argc-1) > 1)
+        if (argc-1 > 1)
             pm_error("Too many arguments (%d).  With a scale option, "
                      "the only argument is the "
                      "optional file specification", argc-1);
-        if (argc-1 > 0) 
-            cmdline_p->input_filespec = argv[1];
-        else
-            cmdline_p->input_filespec = "-";
+        else {
+            if (argc-1 > 0)
+                cmdlineP->inputFileName = argv[1];
+            else
+                cmdlineP->inputFileName = "-";
+        }
     }
 }
 
 
 
 static void
-stretch_line(struct pam * const inpamP, 
-             const tuple * const line, const tuple * const line_stretched, 
-             unsigned int const scale, enum an_edge_mode const edge_mode) {
+stretchLine(struct pam *  const inpamP,
+            const tuple * const line,
+            const tuple * const lineStretched,
+            unsigned int  const scale,
+            enum EdgeMode const edgeMode) {
 /*----------------------------------------------------------------------------
    Stretch the line of tuples 'line' into the output buffer 'line_stretched',
    by factor 'scale'.
 -----------------------------------------------------------------------------*/
+    enum EdgeMode const horizontalEdgeMode =
+        (scale == 1) ? EDGE_NON_INTERP : edgeMode;
+
     int scaleincr;
-    int sisize;   
+    int sisize;
         /* normalizing factor to make fractions representable as integers.
            E.g. if sisize = 100, one half is represented as 50.
         */
     unsigned int col;
     unsigned int outcol;
-    
+
     sisize=0;
-    while (sisize<256) 
+    while (sisize<256)
         sisize += scale;
     scaleincr = sisize/scale;  /* (1/scale, normalized) */
 
@@ -185,7 +196,7 @@ stretch_line(struct pam * const inpamP,
             /* We're at the edge.  There is no column to the right with which
                to interpolate.
             */
-            switch(edge_mode) {
+            switch(horizontalEdgeMode) {
             case EDGE_DROP:
                 /* No output column needed for this input column */
                 break;
@@ -194,32 +205,30 @@ stretch_line(struct pam * const inpamP,
                 for (pos = 0; pos < sisize; pos += scaleincr) {
                     unsigned int plane;
                     for (plane = 0; plane < inpamP->depth; ++plane)
-                        line_stretched[outcol][plane] = 
+                        lineStretched[outcol][plane] =
                             (line[col][plane] * (sisize-pos)) / sisize;
                     ++outcol;
                 }
-            }
-            break;
+            } break;
             case EDGE_NON_INTERP: {
                 unsigned int pos;
                 for (pos = 0; pos < sisize; pos += scaleincr) {
                     unsigned int plane;
                     for (plane = 0; plane < inpamP->depth; ++plane)
-                        line_stretched[outcol][plane] = line[col][plane];
+                        lineStretched[outcol][plane] = line[col][plane];
                     ++outcol;
                 }
-            }
-            break;
-            default: 
-                pm_error("INTERNAL ERROR: invalid value for edge_mode");
+            } break;
+            default:
+                pm_error("INTERNAL ERROR: invalid value for edgeMode");
             }
         } else {
             /* Interpolate with the next input column to the right */
             for (pos = 0; pos < sisize; pos += scaleincr) {
                 unsigned int plane;
                 for (plane = 0; plane < inpamP->depth; ++plane)
-                    line_stretched[outcol][plane] = 
-                        (line[col][plane] * (sisize-pos) 
+                    lineStretched[outcol][plane] =
+                        (line[col][plane] * (sisize-pos)
                          +  line[col+1][plane] * pos) / sisize;
                 ++outcol;
             }
@@ -229,31 +238,33 @@ stretch_line(struct pam * const inpamP,
 
 
 
-static void 
-write_interp_rows(struct pam *      const outpamP,
-                  const tuple *     const curline,
-                  const tuple *     const nextline, 
-                  tuple *           const outbuf,
-                  int               const scale) {
+static void
+writeInterpRows(struct pam *      const outpamP,
+                const tuple *     const curline,
+                const tuple *     const nextline,
+                tuple *           const outbuf,
+                int               const scale) {
 /*----------------------------------------------------------------------------
-   Write out 'scale' rows, being 'curline' followed by rows that are 
+   Write out 'scale' rows, being 'curline' followed by rows that are
    interpolated between 'curline' and 'nextline'.
 -----------------------------------------------------------------------------*/
-    unsigned int scaleincr;
-    unsigned int sisize;
+    unsigned int scaleIncr;
+    unsigned int siSize;
     unsigned int pos;
 
-    sisize=0;
-    while(sisize<256) sisize+=scale;
-    scaleincr=sisize/scale;
+    for (siSize = 0; siSize < 256; siSize += scale);
+
+    scaleIncr = siSize / scale;
 
-    for (pos = 0; pos < sisize; pos += scaleincr) {
+    for (pos = 0; pos < siSize; pos += scaleIncr) {
         unsigned int col;
+
         for (col = 0; col < outpamP->width; ++col) {
             unsigned int plane;
-            for (plane = 0; plane < outpamP->depth; ++plane) 
-                outbuf[col][plane] = (curline[col][plane] * (sisize-pos)
-                    + nextline[col][plane] * pos) / sisize;
+
+            for (plane = 0; plane < outpamP->depth; ++plane)
+                outbuf[col][plane] = (curline[col][plane] * (siSize - pos)
+                    + nextline[col][plane] * pos) / siSize;
         }
         pnm_writepamrow(outpamP, outbuf);
     }
@@ -262,10 +273,11 @@ write_interp_rows(struct pam *      const outpamP,
 
 
 static void
-swap_buffers(tuple ** const buffer1P, tuple ** const buffer2P) {
-    /* Advance "next" line to "current" line by switching
-       line buffers 
-    */
+swapBuffers(tuple ** const buffer1P,
+            tuple ** const buffer2P) {
+/*----------------------------------------------------------------------------
+  Advance "next" line to "current" line by switching line buffers.
+-----------------------------------------------------------------------------*/
     tuple *tmp;
 
     tmp = *buffer1P;
@@ -274,110 +286,142 @@ swap_buffers(tuple ** const buffer1P, tuple ** const buffer2P) {
 }
 
 
-static void 
-stretch(struct pam * const inpamP, struct pam * const outpamP,
-        int const xscale, int const yscale,
-        enum an_edge_mode const edge_mode) {
+
+static void
+stretch(struct pam *  const inpamP,
+        struct pam *  const outpamP,
+        unsigned int  const xscale,
+        unsigned int  const yscale,
+        enum EdgeMode const edgeMode) {
+
+    enum EdgeMode const verticalEdgeMode =
+        (yscale == 1) ? EDGE_NON_INTERP : edgeMode;
 
     tuple *linebuf1, *linebuf2;  /* Input buffers for two rows at a time */
     tuple *curline, *nextline;   /* Pointers to one of the two above buffers */
     /* And the stretched versions: */
-    tuple *stretched_linebuf1, *stretched_linebuf2;
-    tuple *curline_stretched, *nextline_stretched;
+    tuple *stretchedLinebuf1, *stretchedLinebuf2;
+    tuple *curlineStretched, *nextlineStretched;
 
     tuple *outbuf;   /* One-row output buffer */
     unsigned int row;
-    unsigned int rowsToStretch;
-    
-    linebuf1 =           pnm_allocpamrow(inpamP);
-    linebuf2 =           pnm_allocpamrow(inpamP);
-    stretched_linebuf1 = pnm_allocpamrow(outpamP);
-    stretched_linebuf2 = pnm_allocpamrow(outpamP);
-    outbuf =             pnm_allocpamrow(outpamP);
+    unsigned int nRowsToStretch;
+
+    linebuf1          = pnm_allocpamrow(inpamP);
+    linebuf2          = pnm_allocpamrow(inpamP);
+    stretchedLinebuf1 = pnm_allocpamrow(outpamP);
+    stretchedLinebuf2 = pnm_allocpamrow(outpamP);
+    outbuf            = pnm_allocpamrow(outpamP);
 
     curline = linebuf1;
-    curline_stretched = stretched_linebuf1;
+    curlineStretched = stretchedLinebuf1;
     nextline = linebuf2;
-    nextline_stretched = stretched_linebuf2;
+    nextlineStretched = stretchedLinebuf2;
 
     pnm_readpamrow(inpamP, curline);
-    stretch_line(inpamP, curline, curline_stretched, xscale, edge_mode);
+    stretchLine(inpamP, curline, curlineStretched, xscale, edgeMode);
 
-    if (edge_mode == EDGE_DROP) 
-        rowsToStretch = inpamP->height - 1;
+    if (verticalEdgeMode == EDGE_DROP)
+        nRowsToStretch = inpamP->height - 1;
     else
-        rowsToStretch = inpamP->height;
-    
-    for (row = 0; row < rowsToStretch; row++) {
-        if (row == inpamP->height-1) {
+        nRowsToStretch = inpamP->height;
+
+    for (row = 0; row < nRowsToStretch; ++row) {
+        if (row == inpamP->height - 1) {
             /* last line is about to be output. there is no further
              * `next line'.  if EDGE_DROP, we stop here, with output
              * of rows-1 rows.  if EDGE_INTERP_TO_BLACK we make next
              * line black.  if EDGE_NON_INTERP (default) we make it a
-             * copy of the current line.  
+             * copy of the current line.
              */
-            switch (edge_mode) {
+            switch (verticalEdgeMode) {
             case EDGE_INTERP_TO_BLACK: {
-                int col;
-                for (col = 0; col < outpamP->width; col++)
-                    nextline_stretched[col] = blackTuple;
-            } 
+                unsigned int col;
+                for (col = 0; col < outpamP->width; ++col)
+                    nextlineStretched[col] = blackTuple;
+            }
             break;
             case EDGE_NON_INTERP: {
                 /* EDGE_NON_INTERP */
-                int col;
-                for (col = 0; col < outpamP->width; col++)
-                    nextline_stretched[col] = curline_stretched[col];
+                unsigned int col;
+                for (col = 0; col < outpamP->width; ++col)
+                    nextlineStretched[col] = curlineStretched[col];
             }
             break;
-            case EDGE_DROP: 
+            case EDGE_DROP:
                 pm_error("INTERNAL ERROR: processing last row, but "
-                         "edge_mode is EDGE_DROP.");
+                         "edgeMode is EDGE_DROP.");
             }
         } else {
             pnm_readpamrow(inpamP, nextline);
-            stretch_line(inpamP, nextline, nextline_stretched, xscale,
-                         edge_mode);
+            stretchLine(inpamP, nextline, nextlineStretched, xscale, edgeMode);
         }
-        
+
         /* interpolate curline towards nextline into outbuf */
-        write_interp_rows(outpamP, curline_stretched, nextline_stretched,
-                          outbuf, yscale);
+        writeInterpRows(outpamP, curlineStretched, nextlineStretched,
+                        outbuf, yscale);
 
-        swap_buffers(&curline, &nextline);
-        swap_buffers(&curline_stretched, &nextline_stretched);
+        swapBuffers(&curline, &nextline);
+        swapBuffers(&curlineStretched, &nextlineStretched);
     }
     pnm_freerow(outbuf);
-    pnm_freerow(stretched_linebuf2);
-    pnm_freerow(stretched_linebuf1);
+    pnm_freerow(stretchedLinebuf2);
+    pnm_freerow(stretchedLinebuf1);
     pnm_freerow(linebuf2);
     pnm_freerow(linebuf1);
 }
 
 
+static void
+computeOutputWidthHeight(int           const inWidth,
+                         int           const inHeight,
+                         unsigned int  const xScale,
+                         unsigned int  const yScale,
+                         enum EdgeMode const edgeMode,
+                         int *         const outWidthP,
+                         int *         const outHeightP) {
+
+    unsigned int const xDropped =
+        (edgeMode == EDGE_DROP && xScale != 1) ? 1 : 0;
+    unsigned int const yDropped =
+        (edgeMode == EDGE_DROP && yScale != 1) ? 1 : 0;
+    double const width  = (inWidth  - xDropped) * xScale;
+    double const height = (inHeight - yDropped) * yScale;
+
+    if (width > INT_MAX - 2)
+        pm_error("output image width (%f) too large for computations",
+                 width);
+    if (height > INT_MAX - 2)
+        pm_error("output image height (%f) too large for computation",
+                 height);
+
+    *outWidthP  = (unsigned int)width;
+    *outHeightP = (unsigned int)height;
+}
+
+
 
-int 
-main(int argc,char *argv[]) {
+int
+main(int argc, const char ** argv) {
 
-    FILE *ifp;
+    FILE * ifP;
 
-    struct cmdline_info cmdline; 
+    struct CmdlineInfo cmdline;
     struct pam inpam, outpam;
-    
-    pnm_init(&argc, argv);
 
-    parse_command_line(argc, argv, &cmdline);
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
 
-    ifp = pm_openr(cmdline.input_filespec);
+    ifP = pm_openr(cmdline.inputFileName);
 
-    pnm_readpaminit(ifp, &inpam, PAM_STRUCT_SIZE(tuple_type));
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
 
     if (inpam.width < 2)
         pm_error("Image is too narrow.  Must be at least 2 columns.");
     if (inpam.height < 2)
         pm_error("Image is too short.  Must be at least 2 lines.");
 
-
     outpam = inpam;  /* initial value */
     outpam.file = stdout;
 
@@ -388,30 +432,18 @@ main(int argc,char *argv[]) {
     } else {
         outpam.format = inpam.format;
     }
-    {
-        unsigned int const dropped = cmdline.edge_mode == EDGE_DROP ? 1 : 0;
-        double const width  = (inpam.width  - dropped) * cmdline.xscale;
-        double const height = (inpam.height - dropped) * cmdline.yscale;
-
-        if (width > INT_MAX - 2)
-            pm_error("output image width (%f) too large for computations",
-                     width);
-        if (height > INT_MAX - 2)
-            pm_error("output image height (%f) too large for computation",
-                     height);
- 
-        outpam.width  = width;
-        outpam.height = height;
-
-        pnm_writepaminit(&outpam);
-    }
+    computeOutputWidthHeight(inpam.width, inpam.height,
+                             cmdline.xscale, cmdline.yscale, cmdline.edgeMode,
+                             &outpam.width, &outpam.height);
+
+    pnm_writepaminit(&outpam);
 
     pnm_createBlackTuple(&outpam, &blackTuple);
 
-    stretch(&inpam, &outpam, 
-            cmdline.xscale, cmdline.yscale, cmdline.edge_mode);
+    stretch(&inpam, &outpam,
+            cmdline.xscale, cmdline.yscale, cmdline.edgeMode);
 
-    pm_close(ifp);
+    pm_close(ifP);
 
     exit(0);
 }
diff --git a/editor/pnmcrop.c b/editor/pnmcrop.c
index 17b2b6c8..bea7a1bf 100644
--- a/editor/pnmcrop.c
+++ b/editor/pnmcrop.c
@@ -29,14 +29,24 @@
 #include "pnm.h"
 #include "shhopt.h"
 #include "mallocvar.h"
+#include "nstring.h"
 
 static double const sqrt3 = 1.73205080756887729352;
     /* The square root of 3 */
 static double const EPSILON = 1.0e-5;
 
-enum bg_choice {BG_BLACK, BG_WHITE, BG_DEFAULT, BG_SIDES};
+enum BgChoice {BG_BLACK, BG_WHITE, BG_DEFAULT, BG_SIDES, BG_CORNER, BG_COLOR};
 
-typedef enum { LEFT = 0, RIGHT = 1, TOP = 2, BOTTOM = 3} edgeLocation;
+enum BaseOp {OP_CROP, OP_REPORT_FULL, OP_REPORT_SIZE};
+
+enum BlankMode {BLANK_ABORT, BLANK_PASS, BLANK_MINIMIZE, BLANK_MAXCROP};
+
+typedef enum {LEFT = 0, RIGHT = 1, TOP = 2, BOTTOM = 3} EdgeLocation;
+
+typedef struct {
+    EdgeLocation v;
+    EdgeLocation h;
+} CornerLocation;
 
 static const char * const edgeName[] = {
     "left",
@@ -62,13 +72,20 @@ struct CmdlineInfo {
        in a form easy for the program to use.
     */
     const char * inputFilespec;
-    enum bg_choice background;
+    enum BaseOp baseOperation;
+    enum BgChoice background;
     bool wantCrop[4];
         /* User wants crop of left, right, top, bottom, resp. */
-    unsigned int verbose;
     unsigned int margin;
     const char * borderfile;  /* NULL if none */
     float closeness;
+    CornerLocation bgCorner;     /* valid if background == BG_CORNER */
+    const char * bgColor;  /* valid if background == BG_COLOR */
+        /* Note that we can have only the name of the color, not the color
+           itself, because we don't know the maxval at option parsing time.
+        */
+    enum BlankMode blankMode;
+    unsigned int verbose;
 };
 
 
@@ -88,26 +105,40 @@ parseCommandLine(int argc, const char ** argv,
     unsigned int blackOpt, whiteOpt, sidesOpt;
     unsigned int marginSpec, borderfileSpec, closenessSpec;
     unsigned int leftOpt, rightOpt, topOpt, bottomOpt;
-    
+    unsigned int bgCornerSpec, bgColorSpec;
+    unsigned int blankModeSpec;
+    unsigned int reportFullOpt, reportSizeOpt;
+
     unsigned int option_def_index;
 
+    char * bgCornerOpt;
+    char * blankModeOpOpt;
+
     MALLOCARRAY_NOFAIL(option_def, 100);
 
     option_def_index = 0;   /* incremented by OPTENT3 */
-    OPTENT3(0, "black",      OPT_FLAG, NULL, &blackOpt,            0);
-    OPTENT3(0, "white",      OPT_FLAG, NULL, &whiteOpt,            0);
-    OPTENT3(0, "sides",      OPT_FLAG, NULL, &sidesOpt,            0);
-    OPTENT3(0, "left",       OPT_FLAG, NULL, &leftOpt,             0);
-    OPTENT3(0, "right",      OPT_FLAG, NULL, &rightOpt,            0);
-    OPTENT3(0, "top",        OPT_FLAG, NULL, &topOpt,              0);
-    OPTENT3(0, "bottom",     OPT_FLAG, NULL, &bottomOpt,           0);
-    OPTENT3(0, "verbose",    OPT_FLAG, NULL, &cmdlineP->verbose,   0);
-    OPTENT3(0, "margin",     OPT_UINT,   &cmdlineP->margin,    
+    OPTENT3(0, "black",       OPT_FLAG,   NULL, &blackOpt,            0);
+    OPTENT3(0, "white",       OPT_FLAG,   NULL, &whiteOpt,            0);
+    OPTENT3(0, "sides",       OPT_FLAG,   NULL, &sidesOpt,            0);
+    OPTENT3(0, "bg-color",    OPT_STRING, &cmdlineP->bgColor,
+            &bgColorSpec,   0);
+    OPTENT3(0, "bg-corner",   OPT_STRING, &bgCornerOpt,
+            &bgCornerSpec,  0);
+    OPTENT3(0, "left",        OPT_FLAG,   NULL, &leftOpt,             0);
+    OPTENT3(0, "right",       OPT_FLAG,   NULL, &rightOpt,            0);
+    OPTENT3(0, "top",         OPT_FLAG,   NULL, &topOpt,              0);
+    OPTENT3(0, "bottom",      OPT_FLAG,   NULL, &bottomOpt,           0);
+    OPTENT3(0, "margin",      OPT_UINT,   &cmdlineP->margin,
             &marginSpec,     0);
-    OPTENT3(0, "borderfile", OPT_STRING, &cmdlineP->borderfile,
+    OPTENT3(0, "borderfile",  OPT_STRING, &cmdlineP->borderfile,
             &borderfileSpec, 0);
-    OPTENT3(0, "closeness",  OPT_FLOAT,  &cmdlineP->closeness,
+    OPTENT3(0, "closeness",   OPT_FLOAT,  &cmdlineP->closeness,
             &closenessSpec,  0);
+    OPTENT3(0, "blank-image", OPT_STRING, &blankModeOpOpt,
+            &blankModeSpec,  0);
+    OPTENT3(0, "reportfull",  OPT_FLAG,   NULL, &reportFullOpt,       0);
+    OPTENT3(0, "reportsize",  OPT_FLAG,   NULL, &reportSizeOpt,       0);
+    OPTENT3(0, "verbose",     OPT_FLAG,   NULL, &cmdlineP->verbose,   0);
 
     opt.opt_table = option_def;
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
@@ -115,30 +146,69 @@ parseCommandLine(int argc, const char ** argv,
 
     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 == 0)
         cmdlineP->inputFilespec = "-";  /* stdin */
     else if (argc-1 == 1)
         cmdlineP->inputFilespec = argv[1];
-    else 
+    else
         pm_error("Too many arguments (%d).  "
                  "Only need one: the input filespec", argc-1);
 
-    if (blackOpt && whiteOpt)
-        pm_error("You cannot specify both -black and -white");
-    else if (sidesOpt &&( blackOpt || whiteOpt ))
-        pm_error("You cannot specify both -sides and either -black or -white");
+    /* Base operation */
+
+    if (reportFullOpt && reportSizeOpt)
+        pm_error("You cannot specify both -reportfull and -reportsize");
+
+    if (reportFullOpt)
+        cmdlineP->baseOperation = OP_REPORT_FULL;
+    else if (reportSizeOpt)
+        cmdlineP->baseOperation = OP_REPORT_SIZE;
+    else
+        cmdlineP->baseOperation = OP_CROP;
+
+    /* Background color */
+
+    if (blackOpt + whiteOpt + sidesOpt + bgColorSpec + bgCornerSpec > 1)
+        pm_error("You cannot specify more than one of "
+                 "-black, -white, -sides, -bg-color, -bg-corner");
     else if (blackOpt)
         cmdlineP->background = BG_BLACK;
     else if (whiteOpt)
         cmdlineP->background = BG_WHITE;
     else if (sidesOpt)
         cmdlineP->background = BG_SIDES;
+    else if (bgColorSpec)
+        cmdlineP->background = BG_COLOR;
+    else if (bgCornerSpec)
+        cmdlineP->background = BG_CORNER;
     else
         cmdlineP->background = BG_DEFAULT;
 
+    if (bgCornerSpec) {
+        if (false) {
+        } else if (streq(bgCornerOpt, "topleft")) {
+            cmdlineP->bgCorner.v = TOP;
+            cmdlineP->bgCorner.h = LEFT;
+        } else if (streq(bgCornerOpt, "topright")) {
+            cmdlineP->bgCorner.v = TOP;
+            cmdlineP->bgCorner.h = RIGHT;
+        } else if (streq(bgCornerOpt, "bottomleft")) {
+            cmdlineP->bgCorner.v = BOTTOM;
+            cmdlineP->bgCorner.h = LEFT;
+        } else if (streq(bgCornerOpt, "bottomright")) {
+            cmdlineP->bgCorner.v = BOTTOM;
+            cmdlineP->bgCorner.h = RIGHT;
+        } else
+            pm_error("Invalid value for -bg-corner."
+                     "Must be one of "
+                     "'topleft', 'topright', 'bottomleft', 'bottomright'");
+    }
+
+    /* Border specification */
+
     if (!leftOpt && !rightOpt && !topOpt && !bottomOpt) {
         unsigned int i;
         for (i = 0; i < 4; ++i)
@@ -149,6 +219,30 @@ parseCommandLine(int argc, const char ** argv,
         cmdlineP->wantCrop[TOP]    = !!topOpt;
         cmdlineP->wantCrop[BOTTOM] = !!bottomOpt;
     }
+
+    /* Blank image handling */
+
+    if (blankModeSpec) {
+        if (false) {
+        } else if (streq(blankModeOpOpt, "abort"))
+            cmdlineP->blankMode = BLANK_ABORT;
+        else if (streq(blankModeOpOpt,   "pass"))
+            cmdlineP->blankMode = BLANK_PASS;
+        else if (streq(blankModeOpOpt,   "minimize"))
+            cmdlineP->blankMode = BLANK_MINIMIZE;
+        else if (streq(blankModeOpOpt,   "maxcrop")) {
+            if (cmdlineP->baseOperation == OP_CROP)
+                pm_error("Option -blank-image=maxcrop requires "
+                         "-reportfull or -reportsize");
+            else
+                cmdlineP->blankMode = BLANK_MAXCROP;
+        } else
+            pm_error ("Invalid value for -blank-image");
+    } else
+        cmdlineP->blankMode = BLANK_ABORT; /* the default */
+
+    /* Other options */
+
     if (!marginSpec)
         cmdlineP->margin = 0;
 
@@ -180,21 +274,21 @@ typedef struct {
         /* Size in pixels of the border to remove */
     unsigned int padSize;
         /* Size in pixels of the border to add */
-} cropOp;
+} CropOp;
 
 
 typedef struct {
-    cropOp op[4];
-} cropSet;
+    CropOp op[4];
+} CropSet;
 
 
 
 static xel
-background3Corners(FILE * const ifP,
-                   int    const rows,
-                   int    const cols,
-                   pixval const maxval,
-                   int    const format) {
+background3Corners(FILE *       const ifP,
+                   unsigned int const rows,
+                   unsigned int const cols,
+                   pixval       const maxval,
+                   int          const format) {
 /*----------------------------------------------------------------------------
   Read in the whole image, and check all the corners to determine the
   background color.  This is a quite reliable way to determine the
@@ -203,14 +297,14 @@ background3Corners(FILE * const ifP,
   Expect the file to be positioned to the start of the raster, and leave
   it positioned arbitrarily.
 ----------------------------------------------------------------------------*/
-    int row;
+    unsigned int row;
     xel ** xels;
     xel background;   /* our return value */
 
     xels = pnm_allocarray(cols, rows);
 
     for (row = 0; row < rows; ++row)
-        pnm_readpnmrow( ifP, xels[row], cols, maxval, format );
+        pnm_readpnmrow(ifP, xels[row], cols, maxval, format);
 
     background = pnm_backgroundxel(xels, cols, rows, maxval, format);
 
@@ -222,10 +316,10 @@ background3Corners(FILE * const ifP,
 
 
 static xel
-background2Corners(FILE * const ifP,
-                   int    const cols,
-                   pixval const maxval,
-                   int    const format) {
+background2Corners(FILE *       const ifP,
+                   unsigned int const cols,
+                   pixval       const maxval,
+                   int          const format) {
 /*----------------------------------------------------------------------------
   Look at just the top row of pixels and determine the background
   color from the top corners; often this is enough to accurately
@@ -236,7 +330,7 @@ background2Corners(FILE * const ifP,
 ----------------------------------------------------------------------------*/
     xel * xelrow;
     xel background;   /* our return value */
-    
+
     xelrow = pnm_allocrow(cols);
 
     pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
@@ -251,24 +345,116 @@ background2Corners(FILE * const ifP,
 
 
 static xel
-computeBackground(FILE *         const ifP,
-                  int            const cols,
-                  int            const rows,
-                  xelval         const maxval,
+background1Corner(FILE *         const ifP,
+                  unsigned int   const rows,
+                  unsigned int   const cols,
+                  pixval         const maxval,
                   int            const format,
-                  enum bg_choice const backgroundChoice) {
+                  CornerLocation const corner) {
+/*----------------------------------------------------------------------------
+  Let the pixel in corner 'corner' be the background.
+
+  Expect the file to be positioned to the start of the raster, and leave
+  it positioned arbitrarily.
+----------------------------------------------------------------------------*/
+    xel * xelrow;
+    xel background;   /* our return value */
+
+    xelrow = pnm_allocrow(cols);
+
+    if (corner.v == BOTTOM) {
+        /* read and discard all but bottom row */
+        unsigned int row;
+
+        for (row = 0; row < rows - 1; ++row)
+            pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
+    }
+    pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
+
+    background = corner.h == LEFT ? xelrow[0] : xelrow[cols - 1];
+
+    pnm_freerow(xelrow);
+
+    return background;
+}
+
+
+
+static xel
+backgroundColorFmName(const char * const colorName,
+                      xelval       const maxval,
+                      int          const format) {
+/*----------------------------------------------------------------------------
+   The color indicated by 'colorName'.
+
+   If the image is PGM we allow only shades of gray.  If it is PBM, we allow
+   only pure black and pure white.
+
+   Development note: It would be logical to relax the above restriction when
+   -closeness is specified.  Implementation is harder than it seems because of
+   the -margin option.  It is unlikely that there is demand for this feature.
+   If really necessary, the user can convert the input image to PPM.
+
+   Adjust xel for maxval and image type (PPM, PGM, PBM) of the image to
+   examine.  For PGM and PBM, only the blue plane is given a value.  So set
+   the other two to zero.  (This is necessary for making comparisons).
+-----------------------------------------------------------------------------*/
+    pixel const backgroundColor    =
+        ppm_parsecolor(colorName, maxval);
+
+    pixel const backgroundColorMax =
+        ppm_parsecolor(colorName, PNM_MAXMAXVAL);
+
+    bool const hasColor =
+        !(backgroundColorMax.r == backgroundColorMax.g &&
+          backgroundColorMax.r == backgroundColorMax.b);
+
+    bool const hasGray  =
+        !hasColor &&
+        (backgroundColorMax.r != PNM_MAXMAXVAL &&
+         backgroundColorMax.r !=  0 );
+
+    xel backgroundXel;
+
+    backgroundXel = pnm_pixeltoxel(backgroundColor);  /* initial value */
+
+    /* Adjust backgroundXel to match input image format, if necessary */
+
+    if (PBM_FORMAT_TYPE(format) == PBM_TYPE && hasGray)
+        pm_error("Invalid color specified: '%s'.  "
+                 "Image has no intermediate levels of gray.",
+                 colorName);
+
+    if (PPM_FORMAT_TYPE(format) != PPM_TYPE && hasColor)
+        pm_error("Invalid color specified: '%s'.  "
+                 "Image does not have color.", colorName);
+
+    return backgroundXel;
+}
+
+
+
+static xel
+backgroundColor(FILE *         const ifP,
+                int            const cols,
+                int            const rows,
+                xelval         const maxval,
+                int            const format,
+                enum BgChoice  const backgroundChoice,
+                CornerLocation const corner,
+                const char   * const colorName) {
 /*----------------------------------------------------------------------------
    Determine what color is the background color of the image in file
    *ifP, which is described by 'cols', 'rows', 'maxval', and 'format'.
 
    'backgroundChoice' is the method we are to use in determining the
    background color.
-   
+
    Expect the file to be positioned to the start of the raster, and leave
    it positioned arbitrarily.
 -----------------------------------------------------------------------------*/
     xel background;  /* Our return value */
-    
+
     switch (backgroundChoice) {
     case BG_WHITE:
         background = pnm_whitexel(maxval, format);
@@ -276,14 +462,24 @@ computeBackground(FILE *         const ifP,
     case BG_BLACK:
         background = pnm_blackxel(maxval, format);
         break;
-    case BG_SIDES: 
-        background = 
+    case BG_COLOR:
+        backgroundColorFmName(colorName, maxval, format);
+        break;
+    case BG_SIDES:
+        background =
             background3Corners(ifP, rows, cols, maxval, format);
         break;
-    case BG_DEFAULT: 
-        background = 
+    case BG_DEFAULT:
+        background =
             background2Corners(ifP, cols, maxval, format);
         break;
+    case BG_CORNER:
+        background =
+            background1Corner(ifP, rows, cols, maxval, format, corner);
+        break;
+
+    default:
+        pm_error("internal error");
     }
 
     return background;
@@ -292,8 +488,8 @@ computeBackground(FILE *         const ifP,
 
 
 static bool
-colorMatches(pixel        const comparand, 
-             pixel        const comparator, 
+colorMatches(pixel        const comparand,
+             pixel        const comparator,
              unsigned int const allowableDiff) {
 /*----------------------------------------------------------------------------
    The colors 'comparand' and 'comparator' are within 'allowableDiff'
@@ -321,7 +517,7 @@ findBordersInImage(FILE *         const ifP,
 /*----------------------------------------------------------------------------
    Find the left, right, top, and bottom borders in the image 'ifP'.
    Return their sizes in pixels as borderSize[n].
-   
+
    Iff the image is all background, *hasBordersP == FALSE.
 
    Expect the input file to be positioned to the beginning of the
@@ -336,7 +532,7 @@ findBordersInImage(FILE *         const ifP,
         /* leftmost, etc. nonbackground pixel found so far; -1 for none */
 
     xelrow = pnm_allocrow(cols);
-    
+
     left   = cols;  /* initial value */
     right  = -1;    /* initial value */
     top    = rows;  /* initial value */
@@ -348,7 +544,7 @@ findBordersInImage(FILE *         const ifP,
         int thisRowRight;
 
         pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
-        
+
         col = 0;
         while (col < cols &&
                colorMatches(xelrow[col], backgroundColor, allowableDiff))
@@ -363,7 +559,7 @@ findBordersInImage(FILE *         const ifP,
 
         if (thisRowLeft < cols) {
             /* This row is not entirely background */
-            
+
             left  = MIN(thisRowLeft,  left);
             right = MAX(thisRowRight, right);
 
@@ -374,7 +570,7 @@ findBordersInImage(FILE *         const ifP,
             bottom = row + 1;   /* New candidate */
         }
     }
-    
+
     free(xelrow);
 
     if (right == -1)
@@ -397,8 +593,10 @@ analyzeImage(FILE *         const ifP,
              unsigned int   const rows,
              xelval         const maxval,
              int            const format,
-             enum bg_choice const backgroundReq,
+             enum BgChoice  const backgroundReq,
              double         const closeness,
+             CornerLocation const corner,
+             const char   * const colorName,
              imageFilePos   const newFilePos,
              xel *          const backgroundColorP,
              bool *         const hasBordersP,
@@ -422,12 +620,12 @@ analyzeImage(FILE *         const ifP,
 
     pm_tell2(ifP, &rasterpos, sizeof(rasterpos));
 
-    background = computeBackground(ifP, cols, rows, maxval, format,
-                                   backgroundReq);
+    background = backgroundColor(ifP, cols, rows, maxval, format,
+                                 backgroundReq, corner, colorName);
 
     pm_seek2(ifP, &rasterpos, sizeof(rasterpos));
 
-    findBordersInImage(ifP, cols, rows, maxval, format, 
+    findBordersInImage(ifP, cols, rows, maxval, format,
                        background, closeness, hasBordersP, borderSizeP);
 
     if (newFilePos == FILEPOS_BEG)
@@ -447,7 +645,7 @@ ending(unsigned int const n) {
 
 
 static void
-reportCroppingParameters(cropSet const crop) {
+reportCroppingParameters(CropSet const crop) {
 
     unsigned int i;
 
@@ -470,6 +668,85 @@ reportCroppingParameters(cropSet const crop) {
 
 
 
+static void
+reportDimensions(CropSet      const crop,
+                 unsigned int const cols,
+                 unsigned int const rows) {
+
+    unsigned int i;
+
+    for (i = 0; i < ARRAY_SIZE(crop.op); ++i) {
+        if (crop.op[i].removeSize > 0 && crop.op[i].padSize > 0)
+            pm_error("Attempt to add %u and crop %u on %s edge.  "
+                     "Simultaneous pad and crop is not allowed",
+                     crop.op[i].padSize, crop.op[i].removeSize, edgeName[i]);
+        else if (crop.op[i].removeSize > 0)   /* crop */
+            printf ("-%u ", crop.op[i].removeSize);
+        else if (crop.op[i].removeSize == 0) {
+            if (crop.op[i].padSize == 0)      /* no operation */
+                printf ("0 ");
+            else                              /* pad */
+                printf ("+%u ", crop.op[i].padSize);
+        }
+    }
+
+    {
+        unsigned int outputCols, outputRows;
+
+        if (crop.op[LEFT ].removeSize == cols ||
+            crop.op[RIGHT].removeSize == cols)
+            outputCols = cols;
+        else {
+            outputCols =
+                cols - crop.op[LEFT].removeSize - crop.op[RIGHT].removeSize +
+                crop.op[LEFT].padSize + crop.op[RIGHT].padSize;
+        }
+
+        if (crop.op[TOP   ].removeSize == rows ||
+            crop.op[BOTTOM].removeSize == rows)
+            outputRows = rows;
+        else
+            outputRows =
+                rows - crop.op[TOP].removeSize - crop.op[BOTTOM].removeSize +
+                crop.op[TOP].padSize + crop.op[BOTTOM].padSize;
+
+        printf("%u %u", outputCols, outputRows);
+    }
+}
+
+
+static void
+reportSize(CropSet      const crop,
+           unsigned int const cols,
+           unsigned int const rows) {
+
+    reportDimensions(crop, cols, rows);
+
+    putchar('\n');
+
+}
+
+
+
+static void
+reportFull(CropSet      const crop,
+           unsigned int const cols,
+           unsigned int const rows,
+           int          const format,
+           xelval       const maxval,
+           xel          const bgColor,
+           float        const closeness) {
+
+    pixel const backgroundPixel = pnm_xeltopixel(bgColor, format);
+
+    reportDimensions(crop, cols, rows);
+
+    printf(" rgb-%u:%u/%u/%u %f\n", maxval,
+           backgroundPixel.r, backgroundPixel.g, backgroundPixel.b,
+           closeness);
+}
+
+
 
 static void
 fillRow(xel *        const xelrow,
@@ -524,7 +801,7 @@ outputNewBorderNonPbm(unsigned int const height,
 
     for (i = 0; i < height; ++i)
         pnm_writepnmrow(ofP, xelrow, width, maxval, format, 0);
-    
+
     pnm_freerow(xelrow);
 }
 
@@ -536,7 +813,7 @@ writeCroppedNonPbm(FILE *       const ifP,
                    unsigned int const rows,
                    xelval       const maxval,
                    int          const format,
-                   cropSet      const crop,
+                   CropSet      const crop,
                    xel          const backgroundColor,
                    FILE *       const ofP) {
 
@@ -568,7 +845,7 @@ writeCroppedNonPbm(FILE *       const ifP,
        the buffer that lines up the first foreground pixel at
        'foregroundLeft'.
 
-       When we output the row, we pick a starting location in the 
+       When we output the row, we pick a starting location in the
        buffer that includes the proper number of left border pixels
        before 'foregroundLeft'.
 
@@ -581,7 +858,7 @@ writeCroppedNonPbm(FILE *       const ifP,
 
     unsigned int const foregroundCols =
         cols - crop.op[LEFT].removeSize - crop.op[RIGHT].removeSize;
-    unsigned int const outputCols     = 
+    unsigned int const outputCols     =
         foregroundCols + crop.op[LEFT].padSize + crop.op[RIGHT].padSize;
     unsigned int const foregroundRows =
         rows - crop.op[TOP].removeSize - crop.op[BOTTOM].removeSize;
@@ -620,19 +897,19 @@ writeCroppedNonPbm(FILE *       const ifP,
 
     /* Read and output foreground rows */
     for (i = 0; i < foregroundRows; ++i) {
- 
+
         /* Read foreground pixels */
         pnm_readpnmrow(ifP,
                        &(xelrow[foregroundLeft - crop.op[LEFT].removeSize]),
                        cols, maxval, format);
-        
+
         pnm_writepnmrow(ofP,
                         &(xelrow[foregroundLeft - crop.op[LEFT].padSize]),
                         outputCols, maxval, format, 0);
     }
 
     readOffBorderNonPbm(crop.op[BOTTOM].removeSize, ifP, cols, maxval, format);
-    
+
     outputNewBorderNonPbm(crop.op[BOTTOM].padSize, outputCols,
                           backgroundColor,
                           ofP, maxval, format);
@@ -655,10 +932,10 @@ fillRowPBM(unsigned char * const bitrow,
     unsigned int i;
 
     assert(blackWhite == 0 || blackWhite == 1);
-    
+
     for (i = 0; i < colChars; ++i)
         bitrow[i] = blackWhite * 0xff;
-        
+
     if (cols % 8 > 0)
         bitrow[colChars-1] <<= 8 - cols % 8;
 }
@@ -703,7 +980,7 @@ outputNewBorderPbm(unsigned int const height,
 
     for (i = 0; i < height; ++i)
         pbm_writepbmrow_packed(ofP, bitrow, width, 0);
-    
+
     pbm_freerow_packed(bitrow);
 }
 
@@ -714,17 +991,17 @@ writeCroppedPBM(FILE *       const ifP,
                 unsigned int const cols,
                 unsigned int const rows,
                 int          const format,
-                cropSet      const crop,
+                CropSet      const crop,
                 xel          const backgroundColor,
                 FILE *       const ofP) {
-    
-    /* See comments for writeCroppedNonPBM(), which uses identical logic flow. 
+
+    /* See comments for writeCroppedNonPBM(), which uses identical logic flow.
        Uses pbm functions instead of general pnm functions.
     */
 
     unsigned int const foregroundCols =
         cols - crop.op[LEFT].removeSize - crop.op[RIGHT].removeSize;
-    unsigned int const outputCols     = 
+    unsigned int const outputCols     =
         foregroundCols + crop.op[LEFT].padSize + crop.op[RIGHT].padSize;
     unsigned int const foregroundRows =
         rows - crop.op[TOP].removeSize - crop.op[BOTTOM].removeSize;
@@ -736,7 +1013,7 @@ writeCroppedPBM(FILE *       const ifP,
     unsigned int const foregroundRight = foregroundLeft + foregroundCols;
 
     unsigned int const allocCols =
-        foregroundRight + 
+        foregroundRight +
         MAX(crop.op[RIGHT].removeSize, crop.op[RIGHT].padSize);
 
     unsigned int const backgroundBlackWhite =
@@ -748,7 +1025,7 @@ writeCroppedPBM(FILE *       const ifP,
     unsigned int const lastWriteChar = writeOffset/8 + (outputCols-1)/8;
     unsigned char * bitrow;
     unsigned int i;
-    
+
     pbm_writepbminit(ofP, outputCols, outputRows, 0);
 
     bitrow = pbm_allocrow_packed(allocCols);
@@ -765,15 +1042,15 @@ writeCroppedPBM(FILE *       const ifP,
     for (i = 0; i < foregroundRows; ++i) {
         /* Read foreground pixels */
         pbm_readpbmrow_bitoffset(ifP, bitrow, cols, format, readOffset);
-  
+
         pbm_writepbmrow_bitoffset(ofP,
                                   bitrow, outputCols, format, writeOffset);
-                              
+
         /* If there is right-side padding, repair the write buffer
-           distorted by pbm_writepbmrow_bitoffset() 
+           distorted by pbm_writepbmrow_bitoffset()
            (No need to mend any left-side padding)
         */
-        if (crop.op[RIGHT].padSize > 0)    
+        if (crop.op[RIGHT].padSize > 0)
             bitrow[lastWriteChar] = backgroundBlackWhite * 0xff;
     }
 
@@ -788,29 +1065,168 @@ writeCroppedPBM(FILE *       const ifP,
 
 
 
-static void
-determineCrops(struct CmdlineInfo const cmdline,
-               borderSet *        const oldBorderSizeP,
-               cropSet *          const cropP) {
+static CropSet
+crops(struct CmdlineInfo const cmdline,
+      borderSet          const oldBorderSize) {
 
-    edgeLocation i;
+    CropSet retval;
 
-    for (i = 0; i < 4; ++i) {
+    EdgeLocation i;
+
+    for (i = 0; i < ARRAY_SIZE(retval.op); ++i) {
         if (cmdline.wantCrop[i]) {
-            if (oldBorderSizeP->size[i] > cmdline.margin) {
-                cropP->op[i].removeSize =
-                    oldBorderSizeP->size[i] - cmdline.margin;
-                cropP->op[i].padSize    = 0;
+            if (oldBorderSize.size[i] > cmdline.margin) {
+                retval.op[i].removeSize =
+                    oldBorderSize.size[i] - cmdline.margin;
+                retval.op[i].padSize    = 0;
             } else {
-                cropP->op[i].removeSize = 0;
-                cropP->op[i].padSize    =
-                    cmdline.margin - oldBorderSizeP->size[i];
+                retval.op[i].removeSize = 0;
+                retval.op[i].padSize    =
+                    cmdline.margin - oldBorderSize.size[i];
             }
         } else {
-            cropP->op[i].removeSize = 0;
-            cropP->op[i].padSize    = 0;
+            retval.op[i].removeSize = 0;
+            retval.op[i].padSize    = 0;
         }
     }
+    return retval;
+}
+
+
+
+static CropSet
+noCrops(struct CmdlineInfo const cmdline) {
+
+    CropSet retval;
+
+    EdgeLocation i;
+
+    if (cmdline.verbose)
+        pm_message("The image is entirely background; "
+                   "there is nothing to crop.  Copying to output.");
+
+    if (cmdline.margin > 0)
+        pm_message ("-margin value %u ignored", cmdline.margin);
+
+    for (i = 0; i < 4; ++i) {
+        retval.op[i].removeSize = 0;
+        retval.op[i].padSize    = 0;
+    }
+    return retval;
+}
+
+
+
+static CropSet
+extremeCrops(struct CmdlineInfo const cmdline,
+             unsigned int       const cols,
+             unsigned int       const rows) {
+/*----------------------------------------------------------------------------
+   Crops that crop as much as possible, reducing output to a single pixel.
+-----------------------------------------------------------------------------*/
+    CropSet retval;
+
+    if (cmdline.verbose)
+        pm_message("Input image has no distinction between "
+                   "border and content");
+
+    /* We can't just pick a representive pixel, say top-left corner.
+       If -top and/or -bottom was specified but not -left and -right,
+       the output should be one row, not a single pixel.
+
+       The "entirely background" image may have several colors: this
+       happens when -closeness was specified.
+    */
+
+    if (cmdline.wantCrop[LEFT] && cmdline.wantCrop[RIGHT]) {
+        retval.op[LEFT ].removeSize = cols / 2;
+        retval.op[RIGHT].removeSize = cols - retval.op[LEFT].removeSize -1;
+    } else if (cmdline.wantCrop[LEFT]) {
+        retval.op[LEFT ].removeSize = cols - 1;
+        retval.op[RIGHT].removeSize = 0;
+    } else if (cmdline.wantCrop[RIGHT]) {
+        retval.op[LEFT ].removeSize = 0;
+        retval.op[RIGHT].removeSize = cols - 1;
+    } else {
+        retval.op[LEFT ].removeSize = 0;
+        retval.op[RIGHT].removeSize = 0;
+    }
+
+    if (cmdline.wantCrop[TOP] && cmdline.wantCrop[BOTTOM]) {
+        retval.op[ TOP  ].removeSize = rows / 2;
+        retval.op[BOTTOM].removeSize = rows - retval.op[TOP].removeSize -1;
+    } else if (cmdline.wantCrop[TOP]) {
+        retval.op[ TOP  ].removeSize = rows - 1;
+        retval.op[BOTTOM].removeSize = 0;
+    } else if (cmdline.wantCrop[BOTTOM]) {
+        retval.op[ TOP  ].removeSize = 0;
+        retval.op[BOTTOM].removeSize = rows - 1;
+    } else {
+        retval.op[ TOP  ].removeSize = 0;
+        retval.op[BOTTOM].removeSize = 0;
+    }
+
+    if (cmdline.margin > 0)
+        pm_message ("-margin value %u ignored", cmdline.margin);
+
+    {
+        EdgeLocation i;
+        for (i = 0; i < ARRAY_SIZE(retval.op); ++i)
+            retval.op[i].padSize = 0;
+    }
+    return retval;
+}
+
+
+
+static CropSet
+maxcropReport(struct CmdlineInfo const cmdline,
+              unsigned int       const cols,
+              unsigned int       const rows) {
+/*----------------------------------------------------------------------------
+   Report maximum possible crop extents.
+-----------------------------------------------------------------------------*/
+    CropSet retval;
+
+    if (cmdline.wantCrop[LEFT] && cmdline.wantCrop[RIGHT]) {
+        retval.op[LEFT ].removeSize = cols;
+        retval.op[RIGHT].removeSize = cols;
+    } else if (cmdline.wantCrop[LEFT]) {
+        retval.op[LEFT ].removeSize = cols;
+        retval.op[RIGHT].removeSize = 0;
+    } else if (cmdline.wantCrop[RIGHT]) {
+        retval.op[LEFT ].removeSize = 0;
+        retval.op[RIGHT].removeSize = cols;
+    } else {
+        retval.op[LEFT ].removeSize = 0;
+        retval.op[RIGHT].removeSize = 0;
+    }
+
+    if (cmdline.wantCrop[TOP] && cmdline.wantCrop[BOTTOM]) {
+        retval.op[ TOP  ].removeSize = rows;
+        retval.op[BOTTOM].removeSize = rows;
+    } else if (cmdline.wantCrop[TOP]) {
+        retval.op[ TOP  ].removeSize = rows;
+        retval.op[BOTTOM].removeSize = 0;
+    } else if (cmdline.wantCrop[BOTTOM]) {
+        retval.op[ TOP  ].removeSize = 0;
+        retval.op[BOTTOM].removeSize = rows;
+    } else {
+        retval.op[ TOP  ].removeSize = 0;
+        retval.op[BOTTOM].removeSize = 0;
+    }
+
+
+    if (cmdline.margin > 0)
+        pm_message("-margin value %u ignored", cmdline.margin);
+
+    {
+        EdgeLocation i;
+
+        for (i = 0; i < ARRAY_SIZE(retval.op); ++i)
+            retval.op[i].padSize = 0;
+    }
+    return retval;
 }
 
 
@@ -818,7 +1234,7 @@ determineCrops(struct CmdlineInfo const cmdline,
 static void
 validateComputableSize(unsigned int const cols,
                        unsigned int const rows,
-                       cropSet      const crop) {
+                       CropSet      const crop) {
 
     double const newcols =
         (double)cols +
@@ -850,51 +1266,95 @@ cropOneImage(struct CmdlineInfo const cmdline,
 
    Both files are seekable.
 -----------------------------------------------------------------------------*/
-    xelval maxval, bmaxval;
-    int format, bformat;
-    int rows, cols, brows, bcols;
+    int rows, cols, format;
+    xelval maxval;      /* The input file image */
+
+    int brows, bcols, bformat;
+    xelval bmaxval;     /* The separate border file, if specified */
+
+    FILE * afP;
+    int arows, acols, aformat;
+    xelval amaxval;
+    /* The file we use for analysis, either the input file or border file */
+
     bool hasBorders;
     borderSet oldBorder;
         /* The sizes of the borders in the input image */
-    cropSet crop;
+    CropSet crop;
         /* The crops we have to do on each side */
     xel background;
 
     pnm_readpnminit(ifP, &cols, &rows, &maxval, &format);
 
-    if (bdfP)
+    if (bdfP) {
         pnm_readpnminit(bdfP, &bcols, &brows, &bmaxval, &bformat);
 
-    if (bdfP)
-        analyzeImage(bdfP, bcols, brows, bmaxval, bformat, cmdline.background,
-                     cmdline.closeness, FILEPOS_END,
-                     &background, &hasBorders, &oldBorder);
-    else
-        analyzeImage(ifP, cols, rows, maxval, format, cmdline.background,
-                     cmdline.closeness, FILEPOS_BEG,
-                     &background, &hasBorders, &oldBorder);
+        if (cols != bcols || rows != brows)
+            pm_error("Input file image [%u x %u] and border file image "
+                     "[%u x %u] differ in size", cols, rows, bcols, brows);
+        else {
+            afP = bdfP;
+            acols = bcols; arows = brows;
+            amaxval = maxval;
+            aformat = bformat;
+        }
+    } else {
+        afP = ifP;
+        acols = cols; arows = rows;
+        amaxval = maxval;
+        aformat = format;
+    }
+
+    analyzeImage(afP, acols, arows, amaxval, aformat,
+                 cmdline.background, cmdline.closeness,
+                 cmdline.bgCorner, cmdline.bgColor,
+                 (bdfP || cmdline.baseOperation != OP_CROP) ?
+                     FILEPOS_END : FILEPOS_BEG,
+                 &background, &hasBorders, &oldBorder);
 
     if (cmdline.verbose) {
         pixel const backgroundPixel = pnm_xeltopixel(background, format);
-        pm_message("Background color is %s", 
+        pm_message("Background color is %s",
                    ppm_colorname(&backgroundPixel, maxval, TRUE /*hexok*/));
     }
-    if (!hasBorders)
-        pm_error("The image is entirely background; "
-                 "there is nothing to crop.");
-
-    determineCrops(cmdline, &oldBorder, &crop);
+    if (!hasBorders) {
+        switch (cmdline.blankMode) {
+        case BLANK_ABORT:
+            pm_error("The image is entirely background; "
+                     "there is nothing to crop.");
+            break;
+        case BLANK_PASS:
+            crop = noCrops(cmdline);                   break;
+        case BLANK_MINIMIZE:
+            crop = extremeCrops(cmdline, cols, rows);  break;
+        case BLANK_MAXCROP:
+            crop = maxcropReport(cmdline, cols, rows); break;
+        }
+    } else {
+        crop = crops(cmdline, oldBorder);
 
-    validateComputableSize(cols, rows, crop);
+        validateComputableSize(cols, rows, crop);
 
-    if (cmdline.verbose) 
-        reportCroppingParameters(crop);
+        if (cmdline.verbose)
+            reportCroppingParameters(crop);
+    }
 
-    if (PNM_FORMAT_TYPE(format) == PBM_TYPE)
-        writeCroppedPBM(ifP, cols, rows, format, crop, background, ofP);
-    else
-        writeCroppedNonPbm(ifP, cols, rows, maxval, format, crop,
-                           background, ofP);
+    switch (cmdline.baseOperation) {
+    case OP_CROP:
+        if (PNM_FORMAT_TYPE(format) == PBM_TYPE)
+            writeCroppedPBM(ifP, cols, rows, format, crop, background, ofP);
+        else
+            writeCroppedNonPbm(ifP, cols, rows, maxval, format, crop,
+                               background, ofP);
+        break;
+    case OP_REPORT_FULL:
+        reportFull(crop, cols, rows,
+                   aformat, amaxval, background, cmdline.closeness);
+        break;
+    case OP_REPORT_SIZE:
+        reportSize(crop, cols, rows);
+        break;
+    }
 }
 
 
@@ -903,7 +1363,7 @@ int
 main(int argc, const char *argv[]) {
 
     struct CmdlineInfo cmdline;
-    FILE * ifP;   
+    FILE * ifP;
         /* The program's regular input file.  Could be a seekable copy of
            it in a temporary file.
         */
@@ -923,18 +1383,17 @@ main(int argc, const char *argv[]) {
     else
         bdfP = NULL;
 
-    eof = beof = FALSE;
-    while (!eof) {
+    for (eof = beof = FALSE; !eof; ) {
         cropOneImage(cmdline, ifP, bdfP, stdout);
 
         pnm_nextimage(ifP, &eof);
 
         if (bdfP) {
             pnm_nextimage(bdfP, &beof);
-            
+
             if (eof != beof) {
                 if (!eof)
-                    pm_error("Input file has more images than border file."); 
+                    pm_error("Input file has more images than border file.");
                 else
                     pm_error("Border file has more images than image file.");
             }
diff --git a/editor/pnmhisteq.c b/editor/pnmhisteq.c
index 76fd6d2a..a339f73f 100644
--- a/editor/pnmhisteq.c
+++ b/editor/pnmhisteq.c
@@ -426,7 +426,7 @@ remapRgbValue(xel          const thisXel,
     struct hsv const hsv =
         ppm_hsv_from_color(thisXel, maxval);
     xelval const oldValue =
-        MIN(maxval, ROUNDU(hsv.v * maxval));
+        MIN(maxval, pnm_unnormalize(hsv.v, maxval));
     xelval const newValue =
         lumamap[oldValue];
 
diff --git a/editor/pnmnorm.c b/editor/pnmnorm.c
index 131b39d0..2f9a6b20 100644
--- a/editor/pnmnorm.c
+++ b/editor/pnmnorm.c
@@ -9,20 +9,20 @@
 
   Ppmnorm is by Wilson H. Bent, Jr. (whb@usc.edu)
   Extensively hacked from pgmnorm.c, which carries the following note:
-  
+
   Copyright (C) 1989, 1991 by Jef Poskanzer.
-  
+
   Permission to use, copy, modify, and distribute this software and its
   documentation for any purpose and without fee is hereby granted, provided
   that the above copyright notice appear in all copies and that both that
   copyright notice and this permission notice appear in supporting
   documentation.  This software is provided "as is" without express or
   implied warranty.
-  
+
   (End of note from pgmnorm.c)
 
   Pgmnorm's man page also said:
-  
+
   Partially based on the fbnorm filter in Michael Mauldin's "Fuzzy Pixmap"
   package.
 *****************************************************************************/
@@ -73,7 +73,7 @@ parseCommandLine(int argc, const char ** argv,
                  struct cmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
    parse program command line described in Unix standard form by argc
-   and argv.  Return the information in the options as *cmdlineP.  
+   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.
@@ -89,41 +89,41 @@ parseCommandLine(int argc, const char ** argv,
     unsigned int luminosity, colorvalue, saturation;
     unsigned int middleSpec, maxexpandSpec;
     float maxexpand;
-    
+
     unsigned int option_def_index;
 
     MALLOCARRAY_NOFAIL(option_def, 100);
 
     option_def_index = 0;   /* incremented by OPTENT3 */
-    OPTENT3(0,   "bpercent",      OPT_FLOAT,   
+    OPTENT3(0,   "bpercent",      OPT_FLOAT,
             &cmdlineP->bpercent,   &cmdlineP->bpercentSpec, 0);
-    OPTENT3(0,   "wpercent",      OPT_FLOAT,   
+    OPTENT3(0,   "wpercent",      OPT_FLOAT,
             &cmdlineP->wpercent,   &cmdlineP->wpercentSpec, 0);
-    OPTENT3(0,   "bvalue",        OPT_UINT,   
+    OPTENT3(0,   "bvalue",        OPT_UINT,
             &cmdlineP->bvalue,     &cmdlineP->bvalueSpec, 0);
-    OPTENT3(0,   "wvalue",        OPT_UINT,   
+    OPTENT3(0,   "wvalue",        OPT_UINT,
             &cmdlineP->wvalue,     &cmdlineP->wvalueSpec, 0);
-    OPTENT3(0,   "bsingle",       OPT_FLAG,   
+    OPTENT3(0,   "bsingle",       OPT_FLAG,
             NULL,                 &cmdlineP->bsingle, 0);
-    OPTENT3(0,   "wsingle",       OPT_FLAG,   
+    OPTENT3(0,   "wsingle",       OPT_FLAG,
             NULL,                 &cmdlineP->wsingle, 0);
-    OPTENT3(0,   "middle",        OPT_FLOAT,   
+    OPTENT3(0,   "middle",        OPT_FLOAT,
             &cmdlineP->middle,     &middleSpec, 0);
-    OPTENT3(0,   "midvalue",      OPT_UINT,   
+    OPTENT3(0,   "midvalue",      OPT_UINT,
             &cmdlineP->midvalue,   &cmdlineP->midvalueSpec, 0);
-    OPTENT3(0,   "maxexpand",     OPT_FLOAT,   
+    OPTENT3(0,   "maxexpand",     OPT_FLOAT,
             &maxexpand,            &maxexpandSpec, 0);
-    OPTENT3(0,   "keephues",      OPT_FLAG,   
+    OPTENT3(0,   "keephues",      OPT_FLAG,
             NULL,                  &cmdlineP->keephues, 0);
-    OPTENT3(0,   "luminosity",    OPT_FLAG,   
+    OPTENT3(0,   "luminosity",    OPT_FLAG,
             NULL,                  &luminosity, 0);
-    OPTENT3(0,   "colorvalue",    OPT_FLAG,   
+    OPTENT3(0,   "colorvalue",    OPT_FLAG,
             NULL,                  &colorvalue, 0);
-    OPTENT3(0,   "saturation",    OPT_FLAG,   
+    OPTENT3(0,   "saturation",    OPT_FLAG,
             NULL,                  &saturation, 0);
-    OPTENT3(0,   "brightmax",     OPT_FLAG,   
+    OPTENT3(0,   "brightmax",     OPT_FLAG,
             NULL,                  &colorvalue, 0);
-    OPTENT3(0,   "verbose",       OPT_FLAG,   
+    OPTENT3(0,   "verbose",       OPT_FLAG,
             NULL,                  &cmdlineP->verbose, 0);
 
     /* Note: -brightmax was documented and accepted long before it was
@@ -203,7 +203,7 @@ parseCommandLine(int argc, const char ** argv,
 
 
 static void
-buildHistogram(FILE *   const ifp, 
+buildHistogram(FILE *   const ifp,
                int      const cols,
                int      const rows,
                xelval   const maxval,
@@ -226,7 +226,7 @@ buildHistogram(FILE *   const ifp,
 -----------------------------------------------------------------------------*/
     int row;
     xel * xelrow;
-    
+
     xelrow = pnm_allocrow(cols);
 
     {
@@ -307,10 +307,10 @@ maximumValue(const unsigned int * const hist,
 
 
 static void
-computeBottomPercentile(unsigned int         hist[], 
+computeBottomPercentile(unsigned int         hist[],
                         unsigned int   const highest,
                         unsigned int   const total,
-                        float          const percent, 
+                        float          const percent,
                         unsigned int * const percentileP) {
 /*----------------------------------------------------------------------------
    Compute the lowest index of hist[] such that the sum of the hist[]
@@ -332,17 +332,17 @@ computeBottomPercentile(unsigned int         hist[],
                      "values");
         ++percentile;
         count += hist[percentile];
-    }        
+    }
     *percentileP = percentile;
 }
 
 
 
 static void
-computeTopPercentile(unsigned int         hist[], 
-                     unsigned int   const highest, 
+computeTopPercentile(unsigned int         hist[],
+                     unsigned int   const highest,
                      unsigned int   const total,
-                     float          const percent, 
+                     float          const percent,
                      unsigned int * const percentileP) {
 /*----------------------------------------------------------------------------
    Compute the highest index of hist[] such that the sum of the hist[]
@@ -400,7 +400,7 @@ computeAdjustmentForExpansionLimit(xelval   const maxval,
            to 0 .. maxval, if we used the unlimited bvalue and wvalue
         */
     float const unlExpansion = (float)newRange/oldRange;
-    
+
     if (unlExpansion <= maxExpansion) {
         /* No capping is necessary.  Unlimited values are already within
            range.
@@ -527,7 +527,7 @@ resolvePercentParams(FILE *             const ifP,
             *bvalueP = cmdline.bvalue;
         } else {
             xelval percentBvalue;
-            computeBottomPercentile(hist, maxval, cols*rows, cmdline.bpercent, 
+            computeBottomPercentile(hist, maxval, cols*rows, cmdline.bpercent,
                                     &percentBvalue);
             if (cmdline.bvalueSpec)
                 *bvalueP = MIN(percentBvalue, cmdline.bvalue);
@@ -541,7 +541,7 @@ resolvePercentParams(FILE *             const ifP,
             *wvalueP = cmdline.wvalue;
         } else {
             xelval percentWvalue;
-            computeTopPercentile(hist, maxval, cols*rows, cmdline.wpercent, 
+            computeTopPercentile(hist, maxval, cols*rows, cmdline.wpercent,
                                  &percentWvalue);
             if (cmdline.wvalueSpec)
                 *wvalueP = MAX(percentWvalue, cmdline.wvalue);
@@ -630,8 +630,8 @@ computeLinearTransfer(xelval   const bvalue,
        newBrightness[i] = (i-bvalue)*maxval/range);
        (with proper rounding)
     */
-    for (i = bvalue, val = range/2; 
-         i <= wvalue; 
+    for (i = bvalue, val = range/2;
+         i <= wvalue;
          ++i, val += maxval)
         newBrightness[i] = MIN(val / range, maxval);
 
@@ -674,11 +674,11 @@ computeQuadraticFunction(xelval   const bvalue,
     a[0][0] = SQR(bvalue);   a[0][1] = bvalue;   a[0][2] = 1.0;
     a[1][0] = SQR(midvalue); a[1][1] = midvalue; a[1][2] = 1.0;
     a[2][0] = SQR(wvalue);   a[2][1] = wvalue;   a[2][2] = 1.0;
-        
+
     c[0] = 0.0;
     c[1] = middle;
     c[2] = maxval;
-    
+
     pm_solvelineareq(a, x, c, 3, &error);
 
     if (error) {
@@ -714,11 +714,11 @@ computeQuadraticTransfer(xelval   const bvalue,
 
    Set this mapping in newBrightness[].
 -----------------------------------------------------------------------------*/
-    xelval const middle = ROUNDU(middleNorm * maxval);
+    xelval const middle = pnm_unnormalize(middleNorm, maxval);
 
     /* Computing this function is just the task of finding a parabola that
        passes through 3 given points:
-        
+
            (bvalue, 0)
            (midvalue, middle)
            (wvalue, maxval)
@@ -772,8 +772,8 @@ computeQuadraticTransfer(xelval   const bvalue,
 
 static void
 computeTransferFunction(bool      const quadratic,
-                        xelval    const bvalue, 
-                        xelval    const midvalue, 
+                        xelval    const bvalue,
+                        xelval    const midvalue,
                         xelval    const wvalue,
                         float     const middle,
                         xelval    const maxval,
@@ -809,12 +809,12 @@ computeTransferFunction(bool      const quadratic,
         pm_error("Unable to allocate memory for transfer function.");
 
     /* Clip the lowest brightnesses to zero */
-    if (bvalue > 0) 
+    if (bvalue > 0)
         for (i = 0; i < bvalue; ++i)
             newBrightness[i] = 0;
 
     /* Map the middle brightnesses onto 0..maxval */
-    
+
     if (quadratic)
         computeQuadraticTransfer(bvalue, midvalue, wvalue, middle, maxval,
                                  verbose, newBrightness);
@@ -827,7 +827,7 @@ computeTransferFunction(bool      const quadratic,
 
     *newBrightnessP = newBrightness;
 }
-            
+
 
 
 static float
@@ -846,7 +846,7 @@ brightScaler(xel               const p,
 -----------------------------------------------------------------------------*/
     xelval oldBrightness;
     float scaler;
-             
+
     switch (brightMethod) {
     case BRIGHT_LUMINOSITY:
         oldBrightness = ppm_luminosity(p);
@@ -867,7 +867,7 @@ brightScaler(xel               const p,
 
     return scaler;
 }
-            
+
 
 
 static void
@@ -880,13 +880,13 @@ writeRowNormalized(xel *             const xelrow,
                    xelval            const newBrightness[],
                    xel *             const rowbuf) {
 /*----------------------------------------------------------------------------
-   Write to Standard Output a normalized version of the xel row 
+   Write to Standard Output a normalized version of the xel row
    'xelrow'.  Normalize it via the transfer function newBrightness[].
 
    Use 'rowbuf' as a work buffer.  It is at least 'cols' columns wide.
 -----------------------------------------------------------------------------*/
     xel * const outrow = rowbuf;
-                
+
     unsigned int col;
     for (col = 0; col < cols; ++col) {
         xel const p = xelrow[col];
@@ -900,12 +900,12 @@ writeRowNormalized(xel *             const xelrow,
                 xelval const g = MIN(ROUNDU(PPM_GETG(p)*scaler), maxval);
                 xelval const b = MIN(ROUNDU(PPM_GETB(p)*scaler), maxval);
                 PNM_ASSIGN(outrow[col], r, g, b);
-            } else 
-                PNM_ASSIGN(outrow[col], 
-                           newBrightness[PPM_GETR(p)], 
-                           newBrightness[PPM_GETG(p)], 
+            } else
+                PNM_ASSIGN(outrow[col],
+                           newBrightness[PPM_GETR(p)],
+                           newBrightness[PPM_GETG(p)],
                            newBrightness[PPM_GETB(p)]);
-        } else 
+        } else
             PNM_ASSIGN1(outrow[col], newBrightness[PNM_GET1(p)]);
     }
     pnm_writepnmrow(stdout, outrow, cols, maxval, format, 0);
@@ -924,7 +924,7 @@ reportTransferParm(bool   const quadratic,
     if (quadratic)
         pm_message("remapping %u..%u..%u to %u..%u..%u",
                    bvalue, midvalue, wvalue,
-                   0, ROUNDU(maxval*middle), maxval);
+                   0, pnm_unnormalize(middle, maxval), maxval);
     else
         pm_message("remapping %u..%u to %u..%u",
                    bvalue, wvalue, 0, maxval);
@@ -942,7 +942,7 @@ main(int argc, const char *argv[]) {
     int rows, cols, format;
     bool quadratic;
     xelval bvalue, midvalue, wvalue;
-    
+
     pm_proginit(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
@@ -953,14 +953,14 @@ main(int argc, const char *argv[]) {
     pnm_readpnminit(ifP, &cols, &rows, &maxval, &format);
     pm_tell2(ifP, &imagePos, sizeof(imagePos));
 
-    computeEndValues(ifP, cols, rows, maxval, format, cmdline, 
+    computeEndValues(ifP, cols, rows, maxval, format, cmdline,
                      &bvalue, &wvalue, &quadratic, &midvalue);
     {
         xelval * newBrightness;
         int row;
         xel * xelrow;
         xel * rowbuf;
-        
+
         assert(wvalue > bvalue);
 
         xelrow = pnm_allocrow(cols);
@@ -968,7 +968,7 @@ main(int argc, const char *argv[]) {
         reportTransferParm(quadratic, bvalue, midvalue, wvalue, maxval,
                            cmdline.middle);
 
-        
+
         computeTransferFunction(quadratic, bvalue, midvalue, wvalue,
                                 cmdline.middle, maxval, cmdline.verbose,
                                 &newBrightness);
@@ -987,7 +987,7 @@ main(int argc, const char *argv[]) {
         free(newBrightness);
         pnm_freerow(rowbuf);
         pnm_freerow(xelrow);
-    } 
+    }
     pm_close(ifP);
     return 0;
 }
diff --git a/editor/ppmbrighten.c b/editor/ppmbrighten.c
index 96bca478..0446bb75 100644
--- a/editor/ppmbrighten.c
+++ b/editor/ppmbrighten.c
@@ -1,28 +1,19 @@
-/* ppmbrighten.c - allow user control over Value and Saturation of PPM file
-**
-** Copyright (C) 1989 by Jef Poskanzer.
-** Copyright (C) 1990 by Brian Moffet.
-**
-** Permission to use, copy, modify, and distribute this software and its
-** documentation for any purpose and without fee is hereby granted, provided
-** that the above copyright notice appear in all copies and that both that
-** copyright notice and this permission notice appear in supporting
-** documentation.  This software is provided "as is" without express or
-** implied warranty.
-*/
+/*=============================================================================
+                              ppmbrighten
+===============================================================================
+  Change Value and Saturation of PPM image.
+=============================================================================*/
 
 #include "pm_c_util.h"
 #include "ppm.h"
 #include "shhopt.h"
 #include "mallocvar.h"
 
-#define MULTI   1000
-
-struct cmdlineInfo {
+struct CmdlineInfo {
     /* All the information the user supplied in the command line,
        in a form easy for the program to use.
     */
-    const char * inputFilespec;  /* '-' if stdin */
+    const char * inputFileName;  /* '-' if stdin */
     float saturation;
     float value;
     unsigned int normalize;
@@ -32,7 +23,7 @@ struct cmdlineInfo {
 
 static void
 parseCommandLine(int argc, const char ** argv,
-                 struct cmdlineInfo * const cmdlineP) {
+                 struct CmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
    parse program command line described in Unix standard form by argc
    and argv.  Return the information in the options as *cmdlineP.
@@ -63,12 +54,11 @@ parseCommandLine(int argc, const char ** argv,
     OPTENT3(0, "normalize",   OPT_FLAG,   NULL,
             &cmdlineP->normalize, 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, (char **)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 (saturationSpec) {
@@ -90,172 +80,41 @@ parseCommandLine(int argc, const char ** argv,
         cmdlineP->value = 1.0;
 
     if (argc-1 < 1)
-        cmdlineP->inputFilespec = "-";
+        cmdlineP->inputFileName = "-";
     else if (argc-1 == 1)
-        cmdlineP->inputFilespec = argv[1];
+        cmdlineP->inputFileName = argv[1];
     else
         pm_error("Program takes at most one argument:  file specification");
 }
 
 
 
-static __inline__ unsigned int
-mod(int const dividend, unsigned int const divisor) {
-
-    int remainder = dividend % (int)divisor;
-
-    if (remainder < 0)
-        return divisor + remainder;
-    else
-        return (unsigned int) remainder;
-}
-
-
-
-static void
-RGBtoHSV(pixel          const color,
-         pixval         const maxval,
-         unsigned int * const hP,
-         unsigned int * const sP,
-         unsigned int * const vP) {
-
-    unsigned int const R = (MULTI * PPM_GETR(color) + maxval - 1) / maxval;
-    unsigned int const G = (MULTI * PPM_GETG(color) + maxval - 1) / maxval;
-    unsigned int const B = (MULTI * PPM_GETB(color) + maxval - 1) / maxval;
-
-    unsigned int s, v;
-    unsigned int t;
-    unsigned int sector;
-
-    v = MAX(R, MAX(G, B));
-
-    t = MIN(R, MIN(G, B));
-
-    if (v == 0)
-        s = 0;
-    else
-        s = ((v - t)*MULTI)/v;
-
-    if (s == 0)
-        sector = 0;
-    else {
-        unsigned int const cr = (MULTI * (v - R))/(v - t);
-        unsigned int const cg = (MULTI * (v - G))/(v - t);
-        unsigned int const cb = (MULTI * (v - B))/(v - t);
-
-        if (R == v)
-            sector = mod((int)(cb - cg), 6*MULTI);
-        else if (G == v)
-            sector = mod((int)((2*MULTI) + cr - cb), 6*MULTI);
-        else if (B == v)
-            sector = mod((int)((4*MULTI) + cg - cr), 6*MULTI);
-        else
-            pm_error("Internal error: neither r, g, nor b is maximum");
-    }
-
-    *hP = sector * 60;
-    *sP = s;
-    *vP = v;
-}
-
-
-
 static void
-HSVtoRGB(unsigned int   const h,
-         unsigned int   const s,
-         unsigned int   const v,
-         pixval         const maxval,
-         pixel *        const colorP) {
-
-    unsigned int R, G, B;
-
-    if (s == 0) {
-        R = v;
-        G = v;
-        B = v;
-    } else {
-        unsigned int const sectorSize = 60 * MULTI;
-            /* Color wheel is divided into six 60 degree sectors. */
-        unsigned int const sector = (h/sectorSize);
-            /* The sector in which our color resides.  Value is in 0..5 */
-        unsigned int const f = (h - sector*sectorSize)/60;
-            /* The fraction of the way the color is from one side of
-               our sector to the other side, going clockwise.  Value is
-               in [0, MULTI).
-            */
-        unsigned int const m = (v * (MULTI - s)) / MULTI;
-        unsigned int const n = (v * (MULTI - (s * f)/MULTI)) / MULTI;
-        unsigned int const k = (v * (MULTI - (s * (MULTI - f))/MULTI)) / MULTI;
-
-        switch (sector) {
-        case 0:
-            R = v;
-            G = k;
-            B = m;
-            break;
-        case 1:
-            R = n;
-            G = v;
-            B = m;
-            break;
-        case 2:
-            R = m;
-            G = v;
-            B = k;
-            break;
-        case 3:
-            R = m;
-            G = n;
-            B = v;
-            break;
-        case 4:
-            R = k;
-            G = m;
-            B = v;
-            break;
-        case 5:
-            R = v;
-            G = m;
-            B = n;
-            break;
-        default:
-            pm_error("Invalid H value passed to HSVtoRGB: %u/%u", h, MULTI);
-        }
-    }
-    PPM_ASSIGN(*colorP,
-               (R * maxval) / MULTI,
-               (G * maxval) / MULTI,
-               (B * maxval) / MULTI);
-}
-
-
-
-static void
-getMinMax(FILE *         const ifP,
-          int            const cols,
-          int            const rows,
-          pixval         const maxval,
-          int            const format,
-          unsigned int * const minValueP,
-          unsigned int * const maxValueP) {
+getMinMax(FILE *       const ifP,
+          unsigned int const cols,
+          unsigned int const rows,
+          pixval       const maxval,
+          int          const format,
+          double *     const minValueP,
+          double *     const maxValueP) {
 
     pixel * pixelrow;
-    unsigned int minValue, maxValue;
-    int row;
+    double minValue, maxValue;
+    unsigned int row;
 
     pixelrow = ppm_allocrow(cols);
 
-    maxValue = 0;
-    minValue = MULTI;
-    for (row = 0; row < rows; ++row) {
+    for (row = 0, minValue = 65536.0, maxValue = 0.0; row < rows; ++row) {
         unsigned int col;
+
         ppm_readppmrow(ifP, pixelrow, cols, maxval, format);
+
         for (col = 0; col < cols; ++col) {
-            unsigned int H, S, V;
+            struct hsv const pixhsv =
+                ppm_hsv_from_color(pixelrow[col], maxval);
 
-            RGBtoHSV(pixelrow[col], maxval, &H, &S, &V);
-            maxValue = MAX(maxValue, V);
-            minValue = MIN(minValue, V);
+            maxValue = MAX(maxValue, pixhsv.v);
+            minValue = MIN(minValue, pixhsv.v);
         }
     }
     ppm_freerow(pixelrow);
@@ -269,21 +128,22 @@ getMinMax(FILE *         const ifP,
 int
 main(int argc, const char ** argv) {
 
-    struct cmdlineInfo cmdline;
+    double const EPSILON = 1.0e-5;
+    struct CmdlineInfo cmdline;
     FILE * ifP;
-    pixval minValue, maxValue;
     pixel * pixelrow;
     pixval maxval;
     int rows, cols, format, row;
+    double minValue, maxValue;
 
     pm_proginit(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
 
     if (cmdline.normalize)
-        ifP = pm_openr_seekable(cmdline.inputFilespec);
+        ifP = pm_openr_seekable(cmdline.inputFileName);
     else
-        ifP = pm_openr(cmdline.inputFilespec);
+        ifP = pm_openr(cmdline.inputFileName);
 
     ppm_readppminit(ifP, &cols, &rows, &maxval, &format);
 
@@ -292,17 +152,17 @@ main(int argc, const char ** argv) {
         pm_tell2(ifP, &rasterPos, sizeof(rasterPos));
         getMinMax(ifP, cols, rows, maxval, format, &minValue, &maxValue);
         pm_seek2(ifP, &rasterPos, sizeof(rasterPos));
-        if (maxValue > minValue) {
-            pm_message("Minimum value %u%% of full intensity "
+        if (maxValue - minValue > EPSILON) {
+            pm_message("Minimum value %.0f%% of full intensity "
                        "being remapped to zero.",
-                       (minValue*100+MULTI/2)/MULTI);
-            pm_message("Maximum value %u%% of full intensity "
+                       (minValue * 100.0));
+            pm_message("Maximum value %.0f%% of full intensity "
                        "being remapped to full.",
-                       (maxValue*100+MULTI/2)/MULTI);
+                       (maxValue * 100.0));
         } else
-            pm_message("Sole intensity value %u%% of full intensity "
+            pm_message("Sole value of %.0f%% of full intensity "
                        "not being remapped",
-                       (minValue*100+MULTI/2)/MULTI);
+                       (maxValue * 100.0));
     }
 
     pixelrow = ppm_allocrow(cols);
@@ -311,37 +171,48 @@ main(int argc, const char ** argv) {
 
     for (row = 0; row < rows; ++row) {
         unsigned int col;
+
         ppm_readppmrow(ifP, pixelrow, cols, maxval, format);
+
         for (col = 0; col < cols; ++col) {
-            unsigned int H, S, V;
+            struct hsv pixhsv;
 
-            RGBtoHSV(pixelrow[col], maxval, &H, &S, &V);
+            pixhsv = ppm_hsv_from_color(pixelrow[col], maxval);
+                /* initial value */
 
             if (cmdline.normalize) {
-                if (maxValue > minValue) {
-                    V -= minValue;
-                    V = (V * MULTI) /
-                        (MULTI - (minValue+MULTI-maxValue));
-                }
+                if (maxValue - minValue > EPSILON)
+                    pixhsv.v = (pixhsv.v - minValue) / (maxValue - minValue);
             }
-
-            S = MIN(MULTI, (unsigned int) (S * cmdline.saturation + 0.5));
-            V = MIN(MULTI, (unsigned int) (V * cmdline.value + 0.5));
-
-            HSVtoRGB(H, S, V, maxval, &pixelrow[col]);
+            pixhsv.s = pixhsv.s * cmdline.saturation;
+            pixhsv.s = MAX(0.0, MIN(1.0, pixhsv.s));
+            pixhsv.v = pixhsv.v * cmdline.value;
+            pixhsv.v = MAX(0.0, MIN(1.0, pixhsv.v));
+            pixelrow[col] = ppm_color_from_hsv(pixhsv, maxval);
         }
-
         ppm_writeppmrow(stdout, pixelrow, cols, maxval, 0);
     }
     ppm_freerow(pixelrow);
 
     pm_close(ifP);
 
-    /* If the program failed, it previously aborted with nonzero completion
-       code, via various function calls.
+    /* If the program failed, it previously aborted with nonzero exit status
+       via various function calls.
     */
     return 0;
 }
 
 
 
+/**
+** Copyright (C) 1989 by Jef Poskanzer.
+** Copyright (C) 1990 by Brian Moffet.
+**
+** Permission to use, copy, modify, and distribute this software and its
+** documentation for any purpose and without fee is hereby granted, provided
+** that the above copyright notice appear in all copies and that both that
+** copyright notice and this permission notice appear in supporting
+** documentation.  This software is provided "as is" without express or
+** implied warranty.
+*/
+
diff --git a/generator/pamgradient.c b/generator/pamgradient.c
index 95e4d8c4..526efdae 100644
--- a/generator/pamgradient.c
+++ b/generator/pamgradient.c
@@ -7,7 +7,7 @@
 
 
 
-struct cmdlineInfo {
+struct CmdlineInfo {
     tuple colorTopLeft;
     tuple colorTopRight;
     tuple colorBottomLeft;
@@ -19,13 +19,13 @@ struct cmdlineInfo {
 
 static void
 parseCommandLine(int argc, const char **argv,
-                 struct cmdlineInfo * const cmdlineP) {
+                 struct CmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
-  Convert program invocation arguments (argc,argv) into a format the 
-  program can use easily, struct cmdlineInfo.  Validate arguments along
+  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;
@@ -55,14 +55,14 @@ parseCommandLine(int argc, const char **argv,
             pm_error("The value you specified for -maxval (%u) is too big.  "
                      "Max allowed is %u", cmdlineP->maxval,
                      PAM_OVERALL_MAXVAL);
-        
+
         if (cmdlineP->maxval < 1)
             pm_error("You cannot specify 0 for -maxval");
-    }    
+    }
 
     if (argc-1 != 6) {
         pm_error("Need 6 arguments: colorTopLeft, colorTopRight, "
-                 "colorBottomLeft, colorBottomRight, width, height"); 
+                 "colorBottomLeft, colorBottomRight, width, height");
     } else {
         cmdlineP->colorTopLeft     = pnm_parsecolor(argv[1], cmdlineP->maxval);
         cmdlineP->colorTopRight    = pnm_parsecolor(argv[2], cmdlineP->maxval);
@@ -83,7 +83,7 @@ parseCommandLine(int argc, const char **argv,
 
 
 static void
-freeCmdline(struct cmdlineInfo const cmdline) {
+freeCmdline(struct CmdlineInfo const cmdline) {
 
     pnm_freepamtuple(cmdline.colorTopLeft);
     pnm_freepamtuple(cmdline.colorTopRight);
@@ -100,7 +100,7 @@ interpolate(struct pam * const pamP,
             tuple        const last) {
 
     unsigned int plane;
-    
+
     for (plane = 0; plane < pamP->depth; ++plane) {
         int const spread = last[plane] - first[plane];
 
@@ -156,13 +156,13 @@ createEdge(const struct pam * const pamP,
 int
 main(int argc, const char *argv[]) {
 
-    struct cmdlineInfo cmdline;
+    struct CmdlineInfo cmdline;
     struct pam pam;
     tuple * tupleRow;
     tuple * leftEdge;
     tuple * rightEdge;
     unsigned int row;
-    
+
     pm_proginit(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
@@ -188,7 +188,7 @@ main(int argc, const char *argv[]) {
     }
 
     pnm_writepaminit(&pam);
-    
+
     tupleRow = pnm_allocpamrow(&pam);
 
     leftEdge  = createEdge(&pam,
@@ -199,7 +199,7 @@ main(int argc, const char *argv[]) {
     /* interpolate each row between the left edge and the right edge */
     for (row = 0; row < pam.height; ++row) {
         interpolate(&pam, tupleRow, leftEdge[row], rightEdge[row]);
-        pnm_writepamrow(&pam, tupleRow); 
+        pnm_writepamrow(&pam, tupleRow);
     }
 
     pm_close(stdout);
diff --git a/generator/pamtris/input.c b/generator/pamtris/input.c
index 2ea35734..ef79be07 100644
--- a/generator/pamtris/input.c
+++ b/generator/pamtris/input.c
@@ -37,8 +37,6 @@
 #define ARG_IMAGE           "image"
 #define ARG_DEPTH           "depth"
 
-#define WARNING_EXCESS_ARGS "warning: ignoring excess arguments: line %lu."
-#define SYNTAX_ERROR        "syntax error: line %lu."
 
 typedef struct {
     Xy v_xy;
@@ -676,15 +674,17 @@ input_process_next_command(Input *                const inputP,
         char const next = *nextToken(nt.end).begin;
 
         if (unrecognizedCmd) {
-            pm_errormsg("error: unrecognized command: line %lu.",
-                        inputP->number);
+            pm_errormsg("error: unrecognized command: line %u.",
+                        (unsigned)inputP->number);
         } else {
             if (error) {
-                pm_errormsg("Error in line %lu: %s", inputP->number, error);
+                pm_errormsg("Error in line %u: %s",
+                            (unsigned)inputP->number, error);
                 pm_strfree(error);
             } else {
                 if (next != '\0')
-                    pm_message(WARNING_EXCESS_ARGS, inputP->number);
+                    pm_message("warning: ignoring excess arguments: line %u",
+                               (unsigned)inputP->number);
             }
         }
     }
diff --git a/generator/pgmkernel.c b/generator/pgmkernel.c
index ec634c16..37072c38 100644
--- a/generator/pgmkernel.c
+++ b/generator/pgmkernel.c
@@ -223,11 +223,13 @@ main(int argc, const char * argv[]) {
 
         unsigned int col;
         for (col = 0; col < (cmdline.cols +1) / 2; ++col) {
+            double const epsilon = 1e-15;
             double const dx2 = SQR(col - xcenter);
 
             double const normalized = t(dx2, dy2, cmdline.weight) / 2 / tMax;
 
-            gray const grayval = ROUNDU(cmdline.maxval * (0.5 + normalized));
+            gray const grayval =
+                ROUNDU(cmdline.maxval * (0.5 + normalized + epsilon));
 
             halfKernel[arow][col                   ] = grayval;
             halfKernel[arow][cmdline.cols - col - 1] = grayval;
diff --git a/generator/pgmmake.c b/generator/pgmmake.c
index 3843e316..ae706639 100644
--- a/generator/pgmmake.c
+++ b/generator/pgmmake.c
@@ -117,7 +117,7 @@ main(int argc, const char ** const argv) {
 
     parseCommandLine(argc, argv, &cmdline);
 
-    grayLevel = ROUNDU(cmdline.grayLevel * cmdline.maxval);
+    grayLevel = pgm_unnormalize(cmdline.grayLevel, cmdline.maxval);
 
     pgm_writepgminit(stdout, cmdline.cols, cmdline.rows, cmdline.maxval, 0);
 
diff --git a/generator/ppmwheel.c b/generator/ppmwheel.c
index ef5021f9..29e4730c 100644
--- a/generator/ppmwheel.c
+++ b/generator/ppmwheel.c
@@ -16,142 +16,245 @@
 #include <string.h>
 #include <math.h>
 
+#include "pm_c_util.h"
+#include "mallocvar.h"
+#include "shhopt.h"
 #include "ppm.h"
 
 #ifndef PI
 #define PI  3.14159265358979323846
 #endif
 
-#ifndef ABS
-#define ABS(a) ((a) < 0 ? -(a) : (a))
-#endif
 
-static void 
-hsv_rgb(double const in_h, double const in_s, double const in_v, 
-        double * const r, double * const g, double * const b) {
+
+typedef enum {WT_HUE_VAL, WT_HUE_SAT, WT_PPMCIRC} WheelType;
+
+
+struct CmdlineInfo {
+    unsigned int diameter;
+    WheelType    wheelType;
+    pixval       maxval;
+};
+
+
+
+static void
+parseCommandLine(int argc, const char **argv,
+                 struct CmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
-   This is a stripped down hsv->rgb converter that works only for
-   Saturation of zero.
+  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
+  argv[] points to.
 -----------------------------------------------------------------------------*/
-    double h, s, v;
-
-    h = in_h < 0.0 ? 0.0 : in_h > 360.0 ? 360.0 : in_h;
-
-    v = in_v < 0.0 ? 0.0 : in_v > 1.0 ? 1.0 : in_v;
-
-    s = in_s < 0.0 ? 0.0 : in_s > 1.0 ? 1.0 : in_s;
-
-    if (s != 0.0)
-        pm_error("Internal error: non-zero saturation");
-
-    if (h <= 60.0) {          /* from red to yellow */
-        *r = 1.0;
-        *g = h / 60.0;
-        *b = 0.0;
-    } else if ( h <= 120.0 ) {   /* from yellow to green */
-        *r = 1.0 - (h - 60.0) / 60.0;
-        *g = 1.0;
-        *b = 0.0;
-    } else if ( h <= 180.0 ) {   /* from green to cyan */
-        *r = 0.0;
-        *g = 1.0;
-        *b = (h - 120.0) / 60.0;
-    } else if ( h <= 240.0 ) {    /* from cyan to blue */
-        *r = 0.0;
-        *g = 1.0 - (h - 180.0) / 60.0;
-        *b = 1.0;
-    } else if ( h <= 300.0) {    /* from blue to magenta */
-        *r = (h - 240.0) / 60.0;
-        *g = 0.0;
-        *b = 1.0;
-    } else {                      /* from magenta to red */
-        *r = 1.0;
-        *g = 0.0;
-        *b = 1.0 - (h - 300.0) / 60.0;
+    optEntry * option_def;
+        /* Instructions to OptParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int maxvalSpec, huevalueSpec, huesaturationSpec;
+    unsigned int option_def_index;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;
+    OPTENT3(0, "maxval",         OPT_UINT,
+            &cmdlineP->maxval, &maxvalSpec,        0);
+    OPTENT3(0, "huevalue",       OPT_FLAG,
+            NULL,              &huevalueSpec,      0);
+    OPTENT3(0, "huesaturation",  OPT_FLAG,
+            NULL,              &huesaturationSpec, 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, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (!maxvalSpec)
+        cmdlineP->maxval = PPM_MAXMAXVAL;
+    else {
+        if (cmdlineP->maxval > PPM_OVERALLMAXVAL)
+            pm_error("The value you specified for -maxval (%u) is too big.  "
+                     "Max allowed is %u", cmdlineP->maxval,
+                     PPM_OVERALLMAXVAL);
+
+        if (cmdlineP->maxval < 1)
+            pm_error("You cannot specify 0 for -maxval");
     }
 
-    if ( v >= 0.5) {
-        v = 2.0 - 2.0 * v;
-        v = sqrt (v);
-        *r = 1.0 + v * (*r - 1.0);
-        *g = 1.0 + v * (*g - 1.0);
-        *b = 1.0 + v * (*b - 1.0);
+    if (huevalueSpec + huesaturationSpec > 1)
+        pm_error("You may specify at most one of "
+                 "-huevalue and -huesaturation");
+
+    cmdlineP->wheelType =
+        huevalueSpec      ? WT_HUE_VAL :
+        huesaturationSpec ? WT_HUE_SAT :
+        WT_PPMCIRC;
+
+    if (argc-1 != 1) {
+        pm_error("Need 1 argument diameter of the wheel in pixels");
     } else {
-        v *= 2.0;
-        v = sqrt (sqrt ( sqrt (v)));
-        *r *= v;
-        *g *= v;
-        *b *= v;
+        const char * const diameterArg = argv[1];
+
+        if (strlen(diameterArg) == 0)
+            pm_error("Diameter argument is a null string");
+        else {
+            long argNumber;
+            char * tailptr;
+            argNumber = strtol(diameterArg, &tailptr, 10);
+
+            if (*tailptr != '\0')
+                pm_error("You specified an invalid number as diameter: '%s'",
+                         diameterArg);
+            if (argNumber <= 0)
+                pm_error("Diameter must be positive.  You specified %ld.",
+                         argNumber);
+            if (argNumber < 4)
+                pm_error("Diameter must be at least 4.  You specified %ld",
+                         argNumber);
+
+            cmdlineP->diameter = argNumber;
+        }
     }
+    free(option_def);
 }
 
 
-int
-main(int argc, char *argv[]) {
-    pixel *orow;
-    int rows, cols;
-    pixval maxval;
-    unsigned int row;
-    unsigned int xcenter, ycenter, radius;
-    long diameter;
-    char * tailptr;
 
-    ppm_init( &argc, argv );
 
-    if (argc-1 != 1)
-        pm_error("Program takes one argument:  diameter of color wheel");
 
-    diameter = strtol(argv[1], &tailptr, 10);
-    if (strlen(argv[1]) == 0 || *tailptr != '\0')
-        pm_error("You specified an invalid diameter: '%s'", argv[1]);
-    if (diameter <= 0)
-        pm_error("Diameter must be positive.  You specified %ld.", diameter);
-    if (diameter < 4)
-        pm_error("Diameter must be at least 4.  You specified %ld", diameter);
 
-    cols = rows = diameter;
-    
-    orow = ppm_allocrow(cols);
 
-    maxval = PPM_MAXMAXVAL;
-    ppm_writeppminit(stdout, cols, rows, maxval, 0);
+static pixel
+ppmcircColor(pixel  const normalColor,
+             pixval const maxval,
+             double const d) {
+/*----------------------------------------------------------------------------
+   The color that Ppmcirc (by Peter Kirchgessner, not part of Netpbm) puts at
+   'd' units from the center where the normal color in a hue-value color wheel
+   is 'normalColor'.
+
+   We have no idea what the point of this is.
+-----------------------------------------------------------------------------*/
+    pixel retval;
+
+    if (d >= 0.5) {
+        double const scale = sqrt(2.0 - 2.0 * d);
+
+        PPM_ASSIGN(retval,
+                   maxval - scale * (maxval - normalColor.r/d),
+                   maxval - scale * (maxval - normalColor.g/d),
+                   maxval - scale * (maxval - normalColor.b/d));
+    } else if (d == 0.0) {
+        PPM_ASSIGN(retval, 0, 0, 0);
+    } else {
+        double const scale = sqrt(sqrt(sqrt(2.0 * d)))/d;
+        PPM_ASSIGN(retval,
+                   normalColor.r * scale,
+                   normalColor.g * scale,
+                   normalColor.b * scale);
+    }
+    return retval;
+}
+
 
-    radius = diameter/2 - 1;
 
-    xcenter = cols / 2;
-    ycenter = rows / 2;
+static pixel
+wheelColor(WheelType const wheelType,
+           double    const dx,
+           double    const dy,
+           double    const radius,
+           pixval    const maxval) {
+
+    double const dist = sqrt(SQR(dx) + SQR(dy));
+
+    pixel retval;
+
+    if (dist > radius) {
+        retval = ppm_whitepixel(maxval);
+    } else {
+        double const hue90 = atan2(dx, dy) / PI * 180.0;
+        struct hsv hsv;
+
+        hsv.h = hue90 < 0.0 ? 360.0 + hue90 : hue90;
+
+        switch (wheelType) {
+        case WT_HUE_SAT:
+            hsv.v = 1.0;
+            hsv.s = dist / radius;
+            retval = ppm_color_from_hsv(hsv, maxval);
+            break;
+        case WT_HUE_VAL:
+            hsv.s = 1.0;
+            hsv.v = dist / radius;
+            retval = ppm_color_from_hsv(hsv, maxval);
+            break;
+        case WT_PPMCIRC:
+            hsv.s = 1.0;
+            hsv.v = dist / radius;
+            {
+                pixel const hvColor = ppm_color_from_hsv(hsv, maxval);
+                retval = ppmcircColor(hvColor, maxval, dist/radius);
+            }
+            break;
+        }
+    }
+    return retval;
+}
+
+
+
+static void
+ppmwheel(WheelType    const wheelType,
+         unsigned int const diameter,
+         pixval       const maxval,
+         FILE *       const ofP) {
+
+    unsigned int const cols   = diameter;
+    unsigned int const rows   = diameter;
+    unsigned int const radius = diameter/2 - 1;
+    unsigned int const xcenter = cols / 2;
+    unsigned int const ycenter = rows / 2;
+
+    unsigned int row;
+    pixel * orow;
+
+    orow = ppm_allocrow(cols);
+
+    ppm_writeppminit(ofP, cols, rows, maxval, 0);
 
     for (row = 0; row < rows; ++row) {
         unsigned int col;
         for (col = 0; col < cols; ++col) {
             double const dx = (int)col - (int)xcenter;
             double const dy = (int)row - (int)ycenter;
-            double const dist = sqrt(dx*dx + dy*dy);
 
-            pixval r, g, b;
+            orow[col] = wheelColor(wheelType, dx, dy, radius, maxval);
+        }
+        ppm_writeppmrow(ofP, orow, cols, maxval, 0);
+    }
+    ppm_freerow(orow);
+}
 
-            if (dist > radius) {
-                r = g = b = maxval;
-            } else {
-                double hue, sat, val;
-                double dr, dg, db;
 
-                hue = atan2(dx, dy) / PI * 180.0;
-                if (hue < 0.0) 
-                    hue = 360.0 + hue;
-                sat = 0.0;
-                val = dist / radius;
 
-                hsv_rgb(hue, sat, val, &dr, &dg, &db);
+int
+main(int argc, const char ** argv) {
+
+    struct CmdlineInfo cmdline;
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ppmwheel(cmdline.wheelType, cmdline.diameter, cmdline.maxval, stdout);
 
-                r = (pixval)(maxval * dr);
-                g = (pixval)(maxval * dg);
-                b = (pixval)(maxval * db);
-            }
-            PPM_ASSIGN (orow[col], r, g, b );
-        }
-        ppm_writeppmrow(stdout, orow, cols, maxval, 0);
-    }
     pm_close(stdout);
-    exit(0);
+    return 0;
 }
+
+
diff --git a/lib/Makefile b/lib/Makefile
index 65177758..bc758df4 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -221,7 +221,7 @@ libpm.o: compile.h
 .PHONY: install.lib
 ifeq ($(NETPBMLIBTYPE),unixshared)
 # install a Unix-style shared library
-install.lib: $(PKGDIR)/lib $(PKGDIR)/link
+install.lib: $(PKGDIR)/lib $(PKGDIR)/sharedlink
 	cd $(PKGDIR)/lib ; rm -f libnetpbm.$(NETPBMLIBSUFFIX).$(MAJ).*
 	$(INSTALL) -c -m $(INSTALL_PERM_LIBD) \
 	  libnetpbm.$(NETPBMLIBSUFFIX).$(MAJ).$(MIN)  $(PKGDIR)/lib/
@@ -263,26 +263,27 @@ $(INTERFACE_HEADERS:%=%_installhdr): $(PKGDIR)/include/netpbm
 	  $(SRCDIR)/lib/$(@:%_installhdr=%) $(PKGDIR)/include/netpbm/
 
 .PHONY: install.staticlib
-install.staticlib: $(PKGDIR)/link
+install.staticlib: $(PKGDIR)/staticlink
 	$(INSTALL) -c -m $(INSTALL_PERM_LIBS) libnetpbm.$(STATICLIBSUFFIX) \
-	  $(PKGDIR)/link
+	  $(PKGDIR)/staticlink
 
 # Install a shared library stub -- the ".so" file used at link time to
 # prepare a program for dynamically linking a library at run time 
 .PHONY: install.sharedlibstub
-install.sharedlibstub: $(PKGDIR)/link
+install.sharedlibstub: $(PKGDIR)/sharedlink
 ifeq ($(NETPBMLIBTYPE),unixshared)
 # install the link-time (.so) links to the runtime libraries
-	cd $(PKGDIR)/link ; \
+	cd $(PKGDIR)/sharedlink ; \
           rm -f libnetpbm.$(NETPBMLIBSUFFIX); \
           $(SYMLINK) ../lib/libnetpbm.$(NETPBMLIBSUFFIX).$(MAJ) \
             libnetpbm.$(NETPBMLIBSUFFIX)
 endif
 ifeq ($(NETPBMLIBTYPE),dll)
-	$(INSTALL) -c -m $(INSTALL_PERM_LIBS) libnetpbm.dll.a $(PKGDIR)/link
+	$(INSTALL) -c -m $(INSTALL_PERM_LIBS) libnetpbm.dll.a \
+	  $(PKGDIR)/sharedlink
 endif
 ifeq ($(NETPBMLIBTYPE),dylib)
-	cd $(PKGDIR)/link/ ; \
+	cd $(PKGDIR)/sharedlink/ ; \
           rm -f libnetpbm.dylib; \
 	$(SYMLINK) ../lib/libnetpbm.$(MAJ).$(MIN).dylib libnetpbm.dylib
 endif
diff --git a/lib/colorname.c b/lib/colorname.c
index 9400adf7..fe580cb9 100644
--- a/lib/colorname.c
+++ b/lib/colorname.c
@@ -247,9 +247,9 @@ pm_parse_dictionary_name(char    const colorname[],
 
     pm_parse_dictionary_namen(colorname, color);
 
-    r = ROUNDU(color[PAM_RED_PLANE] * maxval);
-    g = ROUNDU(color[PAM_GRN_PLANE] * maxval);
-    b = ROUNDU(color[PAM_BLU_PLANE] * maxval);
+    r = ppm_unnormalize(color[PAM_RED_PLANE], maxval);
+    g = ppm_unnormalize(color[PAM_GRN_PLANE], maxval);
+    b = ppm_unnormalize(color[PAM_BLU_PLANE], maxval);
 
     if (!closeOk) {
         if (maxval != PAM_COLORFILE_MAXVAL) {
diff --git a/lib/libpamcolor.c b/lib/libpamcolor.c
index f9add1a1..e1a24c66 100644
--- a/lib/libpamcolor.c
+++ b/lib/libpamcolor.c
@@ -366,9 +366,7 @@ pnm_parsecolor2(const char * const colorname,
 
     color = pnm_parsecolorn(colorname);
 
-    retval[PAM_RED_PLANE] = ROUNDU(color[PAM_RED_PLANE] * maxval);
-    retval[PAM_GRN_PLANE] = ROUNDU(color[PAM_GRN_PLANE] * maxval);
-    retval[PAM_BLU_PLANE] = ROUNDU(color[PAM_BLU_PLANE] * maxval);
+    pnm_unnormalizetuple(&pam, color, retval);
 
     if (!closeOk) {
         warnIfNotExact(colorname, retval, color, maxval, PAM_RED_PLANE);
diff --git a/lib/libpamn.c b/lib/libpamn.c
index 8ae57037..ae28283a 100644
--- a/lib/libpamn.c
+++ b/lib/libpamn.c
@@ -414,6 +414,19 @@ pnm_writepamn(struct pam * const pamP,
 
 
 
+samplen
+pnm_normalized_sample(struct pam * const pamP,
+                      sample       const sample) {
+    return (samplen)sample/pamP->maxval;
+}
+
+sample
+pnm_unnormalized_sample(struct pam * const pamP,
+                        samplen      const sampleVal) {
+    double const epsilon = 1e-6;
+    return (sample)((sampleVal + epsilon) * pamP->maxval + 0.5);
+}
+
 void
 pnm_normalizetuple(struct pam * const pamP,
                    tuple        const tuple,
@@ -422,7 +435,7 @@ pnm_normalizetuple(struct pam * const pamP,
     unsigned int plane;
 
     for (plane = 0; plane < pamP->depth; ++plane)
-        tuplen[plane] = (samplen)tuple[plane] / pamP->maxval;
+        tuplen[plane] = pnm_normalized_sample(pamP, tuple[plane]);
 }
 
 
@@ -435,7 +448,7 @@ pnm_unnormalizetuple(struct pam * const pamP,
     unsigned int plane;
 
     for (plane = 0; plane < pamP->depth; ++plane)
-        tuple[plane] = tuplen[plane] * pamP->maxval + 0.5;
+        tuple[plane] = pnm_unnormalized_sample(pamP, tuplen[plane]);
 }
 
 
diff --git a/lib/libpgm.h b/lib/libpgm.h
index 7523faaf..eb292c80 100644
--- a/lib/libpgm.h
+++ b/lib/libpgm.h
@@ -7,9 +7,9 @@
 #include "pgm.h"
 
 void
-pgm_readpgminitrest(FILE * const file, 
-                    int *  const colsP, 
-                    int *  const rowsP, 
+pgm_readpgminitrest(FILE * const file,
+                    int *  const colsP,
+                    int *  const rowsP,
                     gray * const maxvalP);
 
 #endif
diff --git a/lib/libpnm3.c b/lib/libpnm3.c
index 0426ebcb..3970c734 100644
--- a/lib/libpnm3.c
+++ b/lib/libpnm3.c
@@ -10,6 +10,9 @@
 ** implied warranty.
 */
 
+#include <stdbool.h>
+#include <assert.h>
+
 #include "pnm.h"
 #include "ppm.h"
 #include "pgm.h"
@@ -199,22 +202,22 @@ pnm_blackxel(xelval const maxval,
     default:
         pm_error("Invalid format %d passed to pnm_blackxel()", format);
     }
-    
+
     return retval;
 }
 
 
 
 void
-pnm_invertxel(xel*   const xP, 
-              xelval const maxval, 
+pnm_invertxel(xel*   const xP,
+              xelval const maxval,
               int    const format) {
 
     switch (PNM_FORMAT_TYPE(format)) {
     case PPM_TYPE:
-        PPM_ASSIGN(*xP, 
+        PPM_ASSIGN(*xP,
                    maxval - PPM_GETR(*xP),
-                   maxval - PPM_GETG(*xP), 
+                   maxval - PPM_GETG(*xP),
                    maxval - PPM_GETB(*xP));
         break;
 
@@ -234,145 +237,199 @@ pnm_invertxel(xel*   const xP,
 
 
 void
-pnm_promoteformat( xel** xels, int cols, int rows, xelval maxval, int format, xelval newmaxval, int newformat )
-    {
-    int row;
+pnm_promoteformat(xel ** const xels,
+                  int    const cols,
+                  int    const rows,
+                  xelval const maxval,
+                  int    const format,
+                  xelval const newmaxval,
+                  int    const newformat) {
 
-    for ( row = 0; row < rows; ++row )
-    pnm_promoteformatrow(
-        xels[row], cols, maxval, format, newmaxval, newformat );
-    }
+    unsigned int row;
 
-void
-pnm_promoteformatrow( xel* xelrow, int cols, xelval maxval, int format, xelval newmaxval, int newformat )
-    {
-    register int col;
-    register xel* xP;
-
-    if ( ( PNM_FORMAT_TYPE(format) == PPM_TYPE &&
-       ( PNM_FORMAT_TYPE(newformat) == PGM_TYPE ||
-         PNM_FORMAT_TYPE(newformat) == PBM_TYPE ) ) ||
-     ( PNM_FORMAT_TYPE(format) == PGM_TYPE &&
-       PNM_FORMAT_TYPE(newformat) == PBM_TYPE ) )
-    pm_error( "pnm_promoteformatrow: can't promote downwards!" );
-
-    /* Are we promoting to the same type? */
-    if ( PNM_FORMAT_TYPE(format) == PNM_FORMAT_TYPE(newformat) )
-    {
-    if ( PNM_FORMAT_TYPE(format) == PBM_TYPE )
-        return;
-    if ( newmaxval < maxval )
-        pm_error(
-       "pnm_promoteformatrow: can't decrease maxval - try using pnmdepth" );
-    if ( newmaxval == maxval )
-        return;
-    /* Increase maxval. */
-    switch ( PNM_FORMAT_TYPE(format) )
-        {
-        case PGM_TYPE:
-        for ( col = 0, xP = xelrow; col < cols; ++col, ++xP )
-        PNM_ASSIGN1(
-            *xP, (int) PNM_GET1(*xP) * newmaxval / maxval );
-        break;
+    for (row = 0; row < rows; ++row)
+        pnm_promoteformatrow(
+            xels[row], cols, maxval, format, newmaxval, newformat);
+}
 
-        case PPM_TYPE:
-        for ( col = 0, xP = xelrow; col < cols; ++col, ++xP )
-        PPM_DEPTH( *xP, *xP, maxval, newmaxval );
-        break;
 
-        default:
-        pm_error( "Invalid old format passed to pnm_promoteformatrow()" );
-        }
-    return;
-    }
 
-    /* We must be promoting to a higher type. */
-    switch ( PNM_FORMAT_TYPE(format) )
-    {
-    case PBM_TYPE:
-    switch ( PNM_FORMAT_TYPE(newformat) )
-        {
-        case PGM_TYPE:
-        for ( col = 0, xP = xelrow; col < cols; ++col, ++xP )
-        if ( PNM_GET1(*xP) == 0 )
-            PNM_ASSIGN1( *xP, 0 );
-        else
-            PNM_ASSIGN1( *xP, newmaxval );
-        break;
-
-        case PPM_TYPE:
-        for ( col = 0, xP = xelrow; col < cols; ++col, ++xP )
-        if ( PNM_GET1(*xP) == 0 )
-            PPM_ASSIGN( *xP, 0, 0, 0 );
-        else
-            PPM_ASSIGN( *xP, newmaxval, newmaxval, newmaxval );
-        break;
-
-        default:
-        pm_error( "Invalid new format passed to pnm_promoteformatrow()" );
+void
+pnm_promoteformatrow(xel *  const xelrow,
+                     int    const cols,
+                     xelval const maxval,
+                     int    const format,
+                     xelval const newmaxval,
+                     int    const newformat) {
+
+    if ((PNM_FORMAT_TYPE(format) == PPM_TYPE &&
+         (PNM_FORMAT_TYPE(newformat) == PGM_TYPE ||
+          PNM_FORMAT_TYPE(newformat) == PBM_TYPE)) ||
+        (PNM_FORMAT_TYPE(format) == PGM_TYPE &&
+         PNM_FORMAT_TYPE(newformat) == PBM_TYPE)) {
+
+        pm_error( "pnm_promoteformatrow: can't promote downwards!" );
+    } else if (PNM_FORMAT_TYPE(format) == PNM_FORMAT_TYPE(newformat)) {
+        /* We're promoting to the same type - but not necessarily maxval */
+        if (PNM_FORMAT_TYPE(format) == PBM_TYPE) {
+            /* PBM doesn't have maxval, so this is idempotent */
+        } else if (newmaxval < maxval)
+            pm_error("pnm_promoteformatrow: can't decrease maxval - "
+                     "try using pamdepth");
+        else if (newmaxval == maxval) {
+            /* Same type, same maxval => idempotent function */
+        } else {
+            /* Increase maxval. */
+            switch (PNM_FORMAT_TYPE(format)) {
+            case PGM_TYPE: {
+                unsigned int col;
+                for (col = 0; col < cols; ++col)
+                    PNM_ASSIGN1(xelrow[col],
+                                PNM_GET1(xelrow[col]) * newmaxval / maxval);
+            } break;
+
+            case PPM_TYPE: {
+                unsigned int col;
+                for (col = 0; col < cols; ++col)
+                    PPM_DEPTH(xelrow[col], xelrow[col], maxval, newmaxval);
+            } break;
+
+            default:
+                pm_error("Invalid old format passed to "
+                         "pnm_promoteformatrow()");
+            }
         }
-    break;
+    } else {
+        /* Promote to a higher type. */
+        switch (PNM_FORMAT_TYPE(format)) {
+        case PBM_TYPE:
+            switch (PNM_FORMAT_TYPE(newformat)) {
+            case PGM_TYPE: {
+                unsigned int col;
+                for (col = 0; col < cols; ++col) {
+                    if (PNM_GET1(xelrow[col]) == 0)
+                        PNM_ASSIGN1(xelrow[col], 0);
+                    else
+                        PNM_ASSIGN1(xelrow[col], newmaxval);
+                }
+            } break;
+
+            case PPM_TYPE: {
+                unsigned int col;
+                for (col = 0; col < cols; ++col) {
+                    if (PNM_GET1(xelrow[col]) == 0)
+                        PPM_ASSIGN(xelrow[col], 0, 0, 0);
+                    else
+                        PPM_ASSIGN(xelrow[col],
+                                   newmaxval, newmaxval, newmaxval );
+                }
+            } break;
+
+            default:
+                pm_error("Invalid new format passed to "
+                         "pnm_promoteformatrow()");
+            }
+            break;
 
-    case PGM_TYPE:
-    switch ( PNM_FORMAT_TYPE(newformat) )
-        {
-        case PPM_TYPE:
-        if ( newmaxval < maxval )
-        pm_error(
-       "pnm_promoteformatrow: can't decrease maxval - try using pnmdepth" );
-        if ( newmaxval == maxval )
-        {
-        for ( col = 0, xP = xelrow; col < cols; ++col, ++xP )
-            PPM_ASSIGN(
-            *xP, PNM_GET1(*xP), PNM_GET1(*xP), PNM_GET1(*xP) );
-        }
-        else
-        { /* Increase maxval. */
-        for ( col = 0, xP = xelrow; col < cols; ++col, ++xP )
-            PPM_ASSIGN(
-            *xP, (int) PNM_GET1(*xP) * newmaxval / maxval,
-            (int) PNM_GET1(*xP) * newmaxval / maxval,
-            (int) PNM_GET1(*xP) * newmaxval / maxval );
-        }
-        break;
+        case PGM_TYPE:
+            switch (PNM_FORMAT_TYPE(newformat)) {
+            case PPM_TYPE:
+                if (newmaxval < maxval)
+                    pm_error("pnm_promoteformatrow: can't decrease maxval - "
+                             "try using pamdepth");
+                else if (newmaxval == maxval) {
+                    unsigned int col;
+                    for (col = 0; col < cols; ++col) {
+                        PPM_ASSIGN(xelrow[col],
+                                   PNM_GET1(xelrow[col]),
+                                   PNM_GET1(xelrow[col]),
+                                   PNM_GET1(xelrow[col]));
+                    }
+                } else {
+                    /* Increase maxval. */
+                    unsigned int col;
+                    for (col = 0; col < cols; ++col) {
+                        PPM_ASSIGN(xelrow[col],
+                                   PNM_GET1(xelrow[col]) * newmaxval / maxval,
+                                   PNM_GET1(xelrow[col]) * newmaxval / maxval,
+                                   PNM_GET1(xelrow[col]) * newmaxval / maxval);
+                    }
+                }
+                break;
+
+            default:
+                pm_error("Invalid new format passed to "
+                         "pnm_promoteformatrow()");
+            }
+            break;
 
         default:
-        pm_error( "Invalid new format passed to pnm_promoteformatrow()" );
+            pm_error("Invalid old format passed to pnm_promoteformatrow()");
         }
-    break;
-
-    default:
-        pm_error( "Invalid old format passed to pnm_promoteformatrow()" );
-    }
     }
+}
+
 
 
 pixel
-pnm_xeltopixel(xel const inputxel,
+pnm_xeltopixel(xel const inputXel,
                int const format) {
-    
-    pixel outputpixel;
+
+    pixel outputPixel;
 
     switch (PNM_FORMAT_TYPE(format)) {
     case PPM_TYPE:
-        PPM_ASSIGN(outputpixel,
-                   PPM_GETR(inputxel),
-                   PPM_GETG(inputxel),
-                   PPM_GETB(inputxel));
+        PPM_ASSIGN(outputPixel,
+                   PNM_GETR(inputXel),
+                   PNM_GETG(inputXel),
+                   PNM_GETB(inputXel));
         break;
     case PGM_TYPE:
     case PBM_TYPE:
-        PPM_ASSIGN(outputpixel,
-                   PNM_GET1(inputxel),
-                   PNM_GET1(inputxel),
-                   PNM_GET1(inputxel));
+        PPM_ASSIGN(outputPixel,
+                   PNM_GET1(inputXel),
+                   PNM_GET1(inputXel),
+                   PNM_GET1(inputXel));
         break;
     default:
         pm_error("Invalid format code %d passed to pnm_xeltopixel()",
                  format);
     }
 
-    return outputpixel;
+    return outputPixel;
+}
+
+
+
+xel
+pnm_pixeltoxel(pixel const inputPixel) {
+
+    return inputPixel;
+}
+
+
+
+xel
+pnm_graytoxel(gray const inputGray) {
+
+    xel outputXel;
+
+    PNM_ASSIGN1(outputXel, inputGray);
+
+    return outputXel;
+}
+
+
+xel
+pnm_bittoxel(bit    const inputBit,
+             xelval const maxval) {
+
+    switch (inputBit) {
+    case PBM_BLACK: return pnm_blackxel(maxval, PBM_TYPE); break;
+    case PBM_WHITE: return pnm_whitexel(maxval, PBM_TYPE); break;
+    default:
+        assert(false);
+    }
 }
 
 
@@ -413,6 +470,6 @@ pnm_parsecolorxel(const char * const colorName,
         pm_error("Invalid format code %d passed to pnm_parsecolorxel()",
                  format);
     }
-    
+
     return retval;
 }
diff --git a/lib/libppmcolor.c b/lib/libppmcolor.c
index 7079482c..c0a88dc2 100644
--- a/lib/libppmcolor.c
+++ b/lib/libppmcolor.c
@@ -553,9 +553,9 @@ ppm_color_from_hsv(struct hsv const hsv,
         }
     }
     PPM_ASSIGN(retval,
-               ROUNDU(R * maxval),
-               ROUNDU(G * maxval),
-               ROUNDU(B * maxval));
+               ppm_unnormalize(R, maxval),
+               ppm_unnormalize(G, maxval),
+               ppm_unnormalize(B, maxval));
 
     return retval;
 }
diff --git a/lib/pam.h b/lib/pam.h
index 74b20f46..0055858b 100644
--- a/lib/pam.h
+++ b/lib/pam.h
@@ -438,6 +438,13 @@ void
 pnm_writepamn(struct pam * const pamP,
               tuplen **    const tuplenarray);
 
+samplen
+pnm_normalized_sample(struct pam * const pamP,
+                      sample       const sample);
+
+sample
+pnm_unnormalized_sample(struct pam * const pamP,
+                        samplen      const sampleVal);
 
 void
 pnm_normalizetuple(struct pam * const pamP,
diff --git a/lib/pgm.h b/lib/pgm.h
index 2de8d531..d4655239 100644
--- a/lib/pgm.h
+++ b/lib/pgm.h
@@ -24,7 +24,7 @@ typedef unsigned int gray;
    (because then old Netpbm programs can process them, and they're
    only half as big).
 
-   So we keep PGM_MAXMAXVAL = 255, even though it's kind of a misnomer.  
+   So we keep PGM_MAXMAXVAL = 255, even though it's kind of a misnomer.
 
    Note that one could always write a file with maxval > PGM_MAXMAXVAL and
    it would just go into plain (text) format instead of raw (binary) format.
@@ -42,6 +42,8 @@ typedef unsigned int gray;
 #define PGM_OVERALLMAXVAL 65535
 #define PGM_MAXMAXVAL 255
 
+#define pgm_unnormalize(value, maxval) \
+  ((gray)((value + 1e-6) * (maxval) + 0.5))
 
 /* Magic constants. */
 
@@ -62,7 +64,7 @@ typedef unsigned int gray;
 
 /* Declarations of routines. */
 
-void 
+void
 pgm_init(int *   const argcP,
          char ** const argv);
 
@@ -78,35 +80,35 @@ pgm_allocrow(unsigned int const cols);
 gray **
 pgm_readpgm(FILE * const file,
             int *  const colsP,
-            int *  const rowsP, 
+            int *  const rowsP,
             gray * const maxvalP);
 
 void
 pgm_readpgminit(FILE * const file,
-                int *  const colsP, 
+                int *  const colsP,
                 int *  const rowsP,
                 gray * const maxvalP,
                 int *  const formatP);
 
 void
 pgm_readpgmrow(FILE * const file,
-               gray * const grayrow, 
+               gray * const grayrow,
                int    const cols,
                gray   const maxval,
                int    const format);
 
 void
-pgm_writepgminit(FILE * const fileP, 
-                 int    const cols, 
-                 int    const rows, 
-                 gray   const maxval, 
+pgm_writepgminit(FILE * const fileP,
+                 int    const cols,
+                 int    const rows,
+                 gray   const maxval,
                  int    const forceplain);
 
 void
-pgm_writepgmrow(FILE *       const fileP, 
-                const gray * const grayrow, 
-                int          const cols, 
-                gray         const maxval, 
+pgm_writepgmrow(FILE *       const fileP,
+                const gray * const grayrow,
+                int          const cols,
+                gray         const maxval,
                 int          const forceplain);
 
 void
@@ -121,11 +123,11 @@ void
 pgm_nextimage(FILE * const file, int * const eofP);
 
 void
-pgm_check(FILE *               const file, 
-          enum pm_check_type   const check_type, 
-          int                  const format, 
-          int                  const cols, 
-          int                  const rows, 
+pgm_check(FILE *               const file,
+          enum pm_check_type   const check_type,
+          int                  const format,
+          int                  const cols,
+          int                  const rows,
           gray                 const maxval,
           enum pm_check_code * const retval_p);
 
diff --git a/lib/pnm.h b/lib/pnm.h
index 3b490552..0625cb5c 100644
--- a/lib/pnm.h
+++ b/lib/pnm.h
@@ -5,6 +5,8 @@
 #define _PNM_H_
 
 #include <netpbm/pm.h>
+#include <netpbm/pbm.h>
+#include <netpbm/pgm.h>
 #include <netpbm/ppm.h>
 
 #ifdef __cplusplus
@@ -19,6 +21,7 @@ typedef pixel xel;
 typedef pixval xelval;
 #define PNM_OVERALLMAXVAL PPM_OVERALLMAXVAL
 #define PNM_MAXMAXVAL PPM_MAXMAXVAL
+#define pnm_unnormalize ppm_unnormalize
 #define PNM_GET1(x) PPM_GETB(x)
 #define PNM_GETR(x) PPM_GETR(x)
 #define PNM_GETG(x) PPM_GETG(x)
@@ -72,7 +75,7 @@ pnm_readpnm(FILE *   const fileP,
 
 void
 pnm_check(FILE *               const fileP,
-          enum pm_check_type   const check_type, 
+          enum pm_check_type   const check_type,
           int                  const format,
           int                  const cols,
           int                  const rows,
@@ -81,19 +84,19 @@ pnm_check(FILE *               const fileP,
 
 
 void
-pnm_writepnminit(FILE * const fileP, 
-                 int    const cols, 
-                 int    const rows, 
-                 xelval const maxval, 
-                 int    const format, 
+pnm_writepnminit(FILE * const fileP,
+                 int    const cols,
+                 int    const rows,
+                 xelval const maxval,
+                 int    const format,
                  int    const forceplain);
 
 void
-pnm_writepnmrow(FILE *      const fileP, 
-                const xel * const xelrow, 
-                int         const cols, 
-                xelval      const maxval, 
-                int         const format, 
+pnm_writepnmrow(FILE *      const fileP,
+                const xel * const xelrow,
+                int         const cols,
+                xelval      const maxval,
+                int         const format,
                 int         const forceplain);
 
 void
@@ -105,28 +108,28 @@ pnm_writepnm(FILE * const fileP,
              int    const format,
              int    const forceplain);
 
-xel 
+xel
 pnm_backgroundxel(xel** xels, int cols, int rows, xelval maxval, int format);
 
-xel 
+xel
 pnm_backgroundxelrow(xel* xelrow, int cols, xelval maxval, int format);
 
-xel 
+xel
 pnm_whitexel(xelval maxval, int format);
 
-xel 
+xel
 pnm_blackxel(xelval maxval, int format);
 
-void 
+void
 pnm_invertxel(xel *  const x,
               xelval const maxval,
               int    const format);
 
-void 
-pnm_promoteformat(xel** xels, int cols, int rows, xelval maxval, int format, 
+void
+pnm_promoteformat(xel** xels, int cols, int rows, xelval maxval, int format,
                   xelval newmaxval, int newformat);
-void 
-pnm_promoteformatrow(xel* xelrow, int cols, xelval maxval, int format, 
+void
+pnm_promoteformatrow(xel* xelrow, int cols, xelval maxval, int format,
                      xelval newmaxval, int newformat);
 
 pixel
@@ -134,6 +137,16 @@ pnm_xeltopixel(xel const inputxel,
                int const format);
 
 xel
+pnm_pixeltoxel(pixel const inputPixel);
+
+xel
+pnm_graytoxel(gray const inputGray);
+
+xel
+pnm_bittoxel(bit    const inputBit,
+             xelval const maxval);
+
+xel
 pnm_parsecolorxel(const char * const colorName,
                   xelval       const maxval,
                   int          const format);
diff --git a/lib/ppm.h b/lib/ppm.h
index 7c483b59..9fc90bb3 100644
--- a/lib/ppm.h
+++ b/lib/ppm.h
@@ -23,6 +23,9 @@ typedef gray pixval;
 
 #define PPM_OVERALLMAXVAL PGM_OVERALLMAXVAL
 #define PPM_MAXMAXVAL PGM_MAXMAXVAL
+
+#define ppm_unnormalize pgm_unnormalize
+
 typedef struct {
     pixval r, g, b;
 } pixel;
diff --git a/lib/util/pm_c_util.h b/lib/util/pm_c_util.h
index 4890da05..a093adb6 100644
--- a/lib/util/pm_c_util.h
+++ b/lib/util/pm_c_util.h
@@ -20,6 +20,13 @@
 #define ROUND(X) (((X) >= 0) ? (int)((X)+0.5) : (int)((X)-0.5))
 #undef ROUNDU
 #define ROUNDU(X) ((unsigned int)((X)+0.5))
+    /* Note that imprecision in floating point arithmetic can make an exact
+       half fractional part round down instead of up.  What should be
+       1000.5 might actually be 1000.49999999999.
+
+       Use 'pnm_unnormalized_sample' instead of ROUNDU(samplen*maxval) to get
+       consistent rounding up when you are unnormalizing sample values.
+    */
 
 /* ROUNDUP rounds up to a specified multiple.  E.g. ROUNDUP(22, 8) == 24 */
 
diff --git a/lib/util/shhopt.h b/lib/util/shhopt.h
index 5effa5fd..d9304f9f 100644
--- a/lib/util/shhopt.h
+++ b/lib/util/shhopt.h
@@ -4,7 +4,7 @@ HERE IS AN EXAMPLE OF THE USE OF SHHOPT:
 
 
 #include <shhopt.h>
-int 
+int
 main ( int argc, char **argv ) {
 
     /* initial values here are just to demonstrate what gets set and
@@ -20,7 +20,7 @@ main ( int argc, char **argv ) {
     int debug_flag=7;
     char ** methodlist;
     struct optNameValue * optlist;
-    
+
     optStruct3 opt;
     unsigned int option_def_index = 0;
     optEntry * option_def;
@@ -40,7 +40,7 @@ main ( int argc, char **argv ) {
 
 
     pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
-    
+
 
     printf("argc=%d\n", argc);
     printf("help_flag=%d\n", help_flag);
@@ -129,7 +129,7 @@ typedef struct {
                             * or pointer to function if type == OPT_FUNC. */
     int        flags;      /* modifier flags. */
 } optStruct;
-    
+
 typedef struct {
     /* This structure describes a single program option in a form for
      use by the pm_optParseOptions3() function.
@@ -137,10 +137,10 @@ typedef struct {
     char       shortName;  /* short option name. */
     const char *longName;  /* long option name, not including '--' or '-' */
     optArgType type;       /* option type. */
-    void       *arg;       
+    void       *arg;
         /* pointer to variable in which to return option's argument (or TRUE
-           if it's a flag option), or pointer to function if 
-           type == OPT_FUNC.  If the option is specified multiple times, only 
+           if it's a flag option), or pointer to function if
+           type == OPT_FUNC.  If the option is specified multiple times, only
            the rightmost one affects this return value.
         */
     unsigned int *specified;
@@ -149,7 +149,7 @@ typedef struct {
         */
     int        flags;      /* modifier flags. */
 } optEntry;
-    
+
 
 typedef struct {
     /* This structure describes the options of a program in a form for
@@ -159,20 +159,20 @@ typedef struct {
         /* The syntax may include short (i.e. one-character) options.
            These options may be stacked within a single token (e.g.
            -abc = -a -b -c).  If this value is not true, the short option
-           member of the option table entry is meaningless and long 
+           member of the option table entry is meaningless and long
            options may have either one or two dashes.
            */
     unsigned char allowNegNum;  /* boolean */
         /* Anything that starts with - and then a digit is a numeric
-           parameter, not an option 
+           parameter, not an option
            */
     optStruct *opt_table;
 } optStruct2;
 
 typedef struct {
     /* Same as optStruct2, but for pm_optParseOptions3() */
-    unsigned char short_allowed;  
-    unsigned char allowNegNum;    
+    unsigned char short_allowed;
+    unsigned char allowNegNum;
     optEntry *opt_table;
 } optStruct3;
 
@@ -186,9 +186,9 @@ typedef struct {
        optStruct *option_def = malloc(100*sizeof(optStruct));
        OPTENTRY('h', "help",     OPT_FLAG, &help_flag, 0);
        OPTENTRY(0,   "alphaout", OPT_STRING, &alpha_filename, 0);
-*/   
+*/
 
-/* If you name your variables option_def and option_def_index like in the 
+/* If you name your variables option_def and option_def_index like in the
    example above, everything's easy.  If you want to use OPTENTRY with other
    variables, define macros OPTION_DEF and OPTION_DEF_INDEX before calling
    OPTENTRY.
@@ -242,7 +242,7 @@ struct optNameValue {
 };
 
 
-        
+
 void
 pm_optSetFatalFunc(void (*f)(const char *, ...));
 
@@ -250,10 +250,10 @@ void
 pm_optParseOptions(int *argc, char *argv[],
                    optStruct opt[], int allowNegNum);
 void
-pm_optParseOptions2(int * const argc_p, char *argv[], const optStruct2 opt, 
+pm_optParseOptions2(int * const argc_p, char *argv[], const optStruct2 opt,
                  const unsigned long flags);
 void
-pm_optParseOptions3(int * const argc_p, char *argv[], const optStruct3 opt, 
+pm_optParseOptions3(int * const argc_p, char *argv[], const optStruct3 opt,
                  const unsigned int optStructSize, const unsigned long flags);
 
 void
diff --git a/test/Test-Order b/test/Test-Order
index 8bda73a4..49eaeff2 100644
--- a/test/Test-Order
+++ b/test/Test-Order
@@ -33,6 +33,7 @@ ppmrough.test
 # Analyzer tests
 
 pamfile.test
+pamfind.test
 pamtable.test
 pgmhist.test
 ppmhist.test
@@ -58,7 +59,6 @@ pamcut.test
 pnmcat.test
 pamflip1.test
 pamflip2.test
-pamenlarge.test
 pnminvert.test
 pamchannel.test
 ppmchange.test
@@ -78,6 +78,17 @@ ppmdim.test
 pnmshear.test
 pgmbentley.test
 
+pamenlarge.test
+pamenlarge-pbm.test
+
+pamscale-reportonly.test
+pamscale-filters1.test
+pamscale-filters2.test
+pamscale-filters3.test
+pamenlarge-pamscale-point.test
+
+pamstretch.test
+
 ppmmix.test
 pammixmulti-identity.test
 
diff --git a/test/all-in-place.ok b/test/all-in-place.ok
index 81eaa320..2df9f392 100644
--- a/test/all-in-place.ok
+++ b/test/all-in-place.ok
@@ -40,6 +40,7 @@ pamaltsat: ok
 pamarith: ok
 pambackground: ok
 pambayer: ok
+pambrighten: ok
 pamchannel: ok
 pamcomp: ok
 pamcrater: ok
@@ -53,16 +54,19 @@ pamendian: ok
 pamenlarge: ok
 pamexec: ok
 pamfile: ok
+pamfind: ok
 pamfix: ok
 pamflip: ok
 pamfunc: ok
 pamgauss: ok
 pamgetcolor: ok
 pamgradient: ok
+pamhue: ok
 pamlevels: ok
 pamlookup: ok
 pammasksharpen: ok
 pammixinterlace: ok
+pammixmulti: ok
 pammosaicknit: ok
 pamoil: ok
 pampaintspill: ok
diff --git a/test/all-in-place.test b/test/all-in-place.test
index cf402b6f..6cf677ef 100755
--- a/test/all-in-place.test
+++ b/test/all-in-place.test
@@ -82,6 +82,7 @@ ordinary_testprogs="\
   pamarith \
   pambackground \
   pambayer \
+  pambrighten \
   pamchannel \
   pamcomp \
   pamcrater \
@@ -95,16 +96,19 @@ ordinary_testprogs="\
   pamenlarge \
   pamexec \
   pamfile \
+  pamfind \
   pamfix \
   pamflip \
   pamfunc \
   pamgauss \
   pamgetcolor \
   pamgradient \
+  pamhue \
   pamlevels \
   pamlookup \
   pammasksharpen \
   pammixinterlace \
+  pammixmulti \
   pammosaicknit \
   pamoil \
   pampaintspill \
diff --git a/test/pamenlarge-pamscale-point.ok b/test/pamenlarge-pamscale-point.ok
new file mode 100644
index 00000000..dea0a3a9
--- /dev/null
+++ b/test/pamenlarge-pamscale-point.ok
@@ -0,0 +1,9 @@
+1 0 0 : 0
+2 -filter=point 0 0 : 0
+3 -linear 0 0 : 0
+4 -nomix 0 0 : 0
+5 0 0 : 0
+7 0 0 : 0
+6 -nomix 0 0 : 0
+15 -nomix -linear 0 0 : 0
+24 -nomix 0 0 : 0
diff --git a/test/pamenlarge-pamscale-point.test b/test/pamenlarge-pamscale-point.test
new file mode 100755
index 00000000..08b34bb7
--- /dev/null
+++ b/test/pamenlarge-pamscale-point.test
@@ -0,0 +1,36 @@
+#! /bin/bash
+# This script tests: pamenlarge pamscale
+# Also requires:
+
+tmpdir=${tmpdir:-/tmp}
+enlarge_ppm=${tmpdir}/enlarge.ppm
+
+# When scale factor is an integer and the pamscale filter is point (default)
+# pamenlarge and pamscale should produce identical output
+
+for option in 1 "2 -filter=point" "3 -linear" "4 -nomix" 5
+  do
+  scale=${option% *}
+  pamenlarge $scale testimg.ppm > ${enlarge_ppm}
+  pamscale   $option testimg.ppm | cmp -s - ${enlarge_ppm}  
+  echo $option ${PIPESTATUS[@]} ":" $?
+  rm  ${enlarge_ppm}  
+  done
+
+
+pamenlarge -xscale=7 -yscale=7 testimg.ppm > ${enlarge_ppm}
+pamscale   -xscale=7 -yscale=7 testimg.ppm | cmp -s - ${enlarge_ppm}  
+echo 7 ${PIPESTATUS[@]} ":" $?
+rm  ${enlarge_ppm}
+
+
+enlarge_pbm=${tmpdir}/enlarge.pbm
+
+for option in "6 -nomix" "15 -nomix -linear" "24 -nomix"
+  do
+  scale=${option%% *}
+  pamenlarge $scale testgrid.pbm > ${enlarge_pbm}
+  pamscale   $option testgrid.pbm | cmp -s - ${enlarge_pbm}  
+  echo $option ${PIPESTATUS[@]} ":" $?
+  rm  ${enlarge_pbm}
+  done
diff --git a/test/pamenlarge-pbm.ok b/test/pamenlarge-pbm.ok
new file mode 100644
index 00000000..dcc2da97
--- /dev/null
+++ b/test/pamenlarge-pbm.ok
@@ -0,0 +1,86 @@
+test 1
+1777627284 265
+2806322261 522
+1951888658 778
+3502911227 1034
+3041987260 1290
+2012524746 1546
+4279863183 1802
+3672763467 2058
+3994368242 2314
+4119105059 2570
+1760702195 2826
+2751077869 3082
+262508159 3339
+743430575 3595
+94950162 3851
+2113456870 4107
+1143977004 4363
+2167141643 4619
+1816864555 4875
+2848735506 5131
+600474914 5387
+3831078134 5643
+1848221383 5899
+test 2
+2525154083 7435
+2525154083 7435
+2525154083 7435
+1983944984 14347
+1983944984 14347
+1983944984 14347
+2319780757 20747
+2319780757 20747
+2319780757 20747
+141252239 26635
+141252239 26635
+141252239 26635
+2074950535 32012
+2074950535 32012
+2074950535 32012
+2906360922 36876
+2906360922 36876
+2906360922 36876
+1320535490 41228
+1320535490 41228
+1320535490 41228
+1972046289 45068
+1972046289 45068
+1972046289 45068
+2203980644 48396
+2203980644 48396
+2203980644 48396
+2060433297 51212
+2060433297 51212
+2060433297 51212
+912377848 53516
+912377848 53516
+912377848 53516
+3346570289 55308
+3346570289 55308
+3346570289 55308
+1851051473 56588
+1851051473 56588
+1851051473 56588
+1940412039 57356
+1940412039 57356
+1940412039 57356
+2001859943 57612
+2001859943 57612
+test 3
+1  2764166846 191
+2  971905193 244
+3  6339889 299
+4  3973389476 342
+5  530215023 403
+6  2124428491 451
+7  2999665126 507
+8  4073302787 539
+9  3877636246 612
+10  1762263341 663
+11  3034611851 717
+12  3339857159 760
+13  119666911 821
+14  4197425209 869
+15  1169924963 924
+16  1845126791 954
diff --git a/test/pamenlarge-pbm.test b/test/pamenlarge-pbm.test
new file mode 100755
index 00000000..10c00ba0
--- /dev/null
+++ b/test/pamenlarge-pbm.test
@@ -0,0 +1,61 @@
+#! /bin/bash
+# This script tests: pamenlarge
+# Also requires: pbmmake pnmpad
+
+tmpdir=${tmpdir:-/tmp}
+complete256_pbm=${tmpdir}/complete256.pbm
+
+# works with gawk and mawk
+# produce all possible 8-bit patterns
+
+LC_ALL=C awk 'BEGIN { print "P4";         # header
+                      print "8 256";
+                      for (i=0;i<256;++i) # raster
+                           printf("%c",i) }' > ${complete256_pbm}
+
+# xscale factor should be tested to at least 21 (=13+8)
+
+# Test 1.
+echo "test 1"
+ 
+for xs in `seq 23`
+  do
+  pamenlarge -xscale=$xs ${complete256_pbm} | cksum
+  done
+
+# Test 2.
+echo "test 2"
+
+for xs1 in `seq 15`
+  do
+  xs2=$((30-$xs1))
+  pamenlarge -xscale=$xs1 ${complete256_pbm} | pamenlarge -xscale=$xs2 | cksum
+  if [ $xs1 != $xs2 ]; then
+  pamenlarge -xscale=$xs2 ${complete256_pbm} | pamenlarge -xscale=$xs1 | cksum
+  fi
+  pamenlarge -xscale=$(($xs1 * $xs2)) ${complete256_pbm} | cksum
+  done
+
+rm ${complete256_pbm}
+
+# Test 3.
+echo "test 3"
+
+test3_pbm=${tmpdir}/test3.pbm
+
+for width in `seq 16`
+  do
+  pbmmake -g ${width} 1 | pnmpad -top=1 -white | \
+      pnmpad -bottom=1 -black > ${test3_pbm}.${width}
+  done
+ 
+  for xscale in `seq 16`
+    do echo -n ${xscale} " "
+    for width in `seq 16`
+      do pamenlarge -xscale=${xscale} ${test3_pbm}.${width} ; done | cksum
+      #
+      # unlike most other tests we take the cksum of a composite PBM file
+      #
+    done
+
+rm ${test3_pbm}.[1-9]  ${test3_pbm}.1[1-6]
\ No newline at end of file
diff --git a/test/pamfile.ok b/test/pamfile.ok
index 57cc8cfd..c0d80c28 100644
--- a/test/pamfile.ok
+++ b/test/pamfile.ok
@@ -3,3 +3,10 @@ testgrid.pbm:	PBM raw, 14 by 16
 stdin:	PGM raw, 227 by 149  maxval 255
 stdin:	PAM, 227 by 149 by 1 maxval 255
     Tuple type: GRAYSCALE
+stdin:	3 images
+stdin:	Image 0:	PBM raw, 14 by 16
+stdin:	Image 1:	PBM raw, 14 by 16
+stdin:	Image 2:	PBM raw, 14 by 16
+227 149
+testimg.ppm: PPM RAW 227 149 3 255 RGB
+stdin: PBM RAW 14 16 1 1 BLACKANDWHITE
diff --git a/test/pamfile.test b/test/pamfile.test
index ac927172..545a2289 100755
--- a/test/pamfile.test
+++ b/test/pamfile.test
@@ -7,3 +7,10 @@ pamfile testimg.ppm
 pamfile testgrid.pbm
 pamchannel -infile=testimg.ppm -tupletype="GRAYSCALE" 0 | pamtopnm | pamfile
 pamchannel -tupletype="GRAYSCALE" -infile=testimg.ppm 0 | pamfile
+
+cat testgrid.pbm testgrid.pbm testgrid.pbm | pamfile -count
+cat testgrid.pbm testgrid.pbm testgrid.pbm | pamfile -allimages
+
+pamfile -size testimg.ppm
+pamfile -machine testimg.ppm
+cat testgrid.pbm testimg.ppm testgrid.pbm | pamfile -machine
diff --git a/test/pamfind.ok b/test/pamfind.ok
new file mode 100644
index 00000000..dc0f95ed
--- /dev/null
+++ b/test/pamfind.ok
@@ -0,0 +1,83 @@
+Test 1
+Locations containing tuple (43/43/43)/255:
+(6, 0)
+(6, 1)
+(7, 0)
+(7, 1)
+(19, 184)
+Locations containing tuple (210/57/41)/255:
+(44, 109)
+(96, 74)
+(96, 75)
+(100, 75)
+(100, 76)
+(100, 77)
+(100, 78)
+(101, 73)
+(101, 74)
+(101, 75)
+(101, 76)
+(101, 77)
+(101, 78)
+(102, 72)
+Test 2
+Locations containing tuple (1)/1:
+(0, 1)
+(0, 3)
+(0, 5)
+(0, 7)
+(0, 9)
+(0, 11)
+(0, 13)
+(2, 1)
+(2, 3)
+(2, 5)
+(2, 7)
+(2, 9)
+(2, 11)
+(2, 13)
+(4, 1)
+(4, 3)
+(4, 5)
+(4, 7)
+(4, 9)
+(4, 11)
+(4, 13)
+(6, 1)
+(6, 3)
+(6, 5)
+(6, 7)
+(6, 9)
+(6, 11)
+(6, 13)
+(8, 1)
+(8, 3)
+(8, 5)
+(8, 7)
+(8, 9)
+(8, 11)
+(8, 13)
+(10, 1)
+(10, 3)
+(10, 5)
+(10, 7)
+(10, 9)
+(10, 11)
+(10, 13)
+(12, 1)
+(12, 3)
+(12, 5)
+(12, 7)
+(12, 9)
+(12, 11)
+(12, 13)
+(14, 1)
+(14, 3)
+(14, 5)
+(14, 7)
+(14, 9)
+(14, 11)
+(14, 13)
+Test 3
+okay
+okay
diff --git a/test/pamfind.test b/test/pamfind.test
new file mode 100755
index 00000000..39cb1437
--- /dev/null
+++ b/test/pamfind.test
@@ -0,0 +1,32 @@
+#! /bin/bash
+# This script tests: pamfind
+# Also requires:
+
+tmpdir=${tmpdir:-/tmp}
+sorted0_res=${tmpdir}/pamfind_sorted0.res
+sorted1_res=${tmpdir}/pamfind_sorted1.res
+
+# Test 1
+echo Test 1
+pamfind -color=grey17     testimg.ppm 
+pamfind -target=210,57,41 testimg.ppm
+
+# Test 2
+echo Test 2
+pamfind -target=1 testgrid.pbm
+
+# Test 3
+# The two outputs should be disjoint
+echo Test 3
+pamfind -target=0 testgrid.pbm | sort > ${sorted0_res}
+pamfind -target=1 testgrid.pbm | sort > ${sorted1_res}
+comm -3 ${sorted0_res}  ${sorted1_res}  |
+  awk 'END {if (NR==226) print  "okay";
+            else printf("failure (line count=%d)\n", NR)}'
+comm -12 ${sorted0_res}  ${sorted1_res} | 
+  awk '{print}; END { if(NR == 0) print  "okay";
+            else printf("failure (line count=%d)\n", NR)}'
+
+rm ${sorted0_res} ${sorted1_res}
+
+
diff --git a/test/pamscale-filters1.ok b/test/pamscale-filters1.ok
new file mode 100644
index 00000000..083505ee
--- /dev/null
+++ b/test/pamscale-filters1.ok
@@ -0,0 +1,15 @@
+4 box:
+match
+match
+9 triangle:
+match
+match
+4 quadratic:
+match
+match
+10 cubic:
+match
+match
+4 catrom:
+match
+match
diff --git a/test/pamscale-filters1.test b/test/pamscale-filters1.test
new file mode 100755
index 00000000..63e0d012
--- /dev/null
+++ b/test/pamscale-filters1.test
@@ -0,0 +1,64 @@
+#! /bin/bash
+# This script tests: pamscale pamenlarge
+# Also requires: pamvalidate pnmpsnr
+
+tmpdir=${tmpdir:-/tmp}
+enlarge_ppm=${tmpdir}/enlarge.ppm
+
+width_height=`pamfile -size testimg.ppm | \
+                awk '{print "-width="$1, "-height="$2}'`
+
+# The target values here were determined by running the test on a
+# 32-bit GNU/Linux system and subtracting 2.5 (dB) from observed
+# values.
+
+pamenlarge  4 testimg.ppm | pamvalidate > ${enlarge_ppm}
+echo  4 box:  
+pamscale  4 -filter=box testimg.ppm | \
+  pnmpsnr -target1=35.67 -target2=49.25 -target3=43.28 - ${enlarge_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+ 
+pamscale ${width_height} -filter=point ${enlarge_ppm} | \
+  pnmpsnr -target1=1000 -target2=1000 -target3=1000 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${enlarge_ppm}
+
+pamenlarge  9 testimg.ppm | pamvalidate > ${enlarge_ppm}
+echo  9 triangle:  
+pamscale  9 -filter=triangle testimg.ppm | \
+  pnmpsnr -target1=35.27 -target2=49.06 -target3=43.11 - ${enlarge_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+pamscale ${width_height} -filter=triangle ${enlarge_ppm} | \
+  pnmpsnr -target1=41.52 -target2=56.96 -target3=52.68 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${enlarge_ppm}
+
+pamenlarge  4 testimg.ppm | pamvalidate > ${enlarge_ppm}
+echo  4 quadratic:  
+pamscale  4 -filter=quadratic testimg.ppm | \
+  pnmpsnr -target1=35.39 -target2=49.36 -target3=43.46 - ${enlarge_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+pamscale ${width_height} -filter=quadratic ${enlarge_ppm} | \
+  pnmpsnr -target1=39.34 -target2=55.24 -target3=50.65 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${enlarge_ppm}
+
+pamenlarge 10 testimg.ppm | pamvalidate > ${enlarge_ppm}
+echo 10 cubic:  
+pamscale 10 -filter=cubic testimg.ppm | \
+  pnmpsnr -target1=34.39 -target2=48.45 -target3=42.52 - ${enlarge_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+pamscale ${width_height} -filter=cubic ${enlarge_ppm} | \
+  pnmpsnr -target1=37.7 -target2=53.84 -target3=48.96 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${enlarge_ppm}
+
+pamenlarge  4 testimg.ppm | pamvalidate > ${enlarge_ppm}
+echo  4 catrom:  
+pamscale  4 -filter=catrom testimg.ppm | \
+  pnmpsnr -target1=36.05 -target2=49.51 -target3=43.49 - ${enlarge_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+pamscale ${width_height} -filter=catrom ${enlarge_ppm} | \
+  pnmpsnr -target1=46.5 -target2=60.68 -target3=57.8 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${enlarge_ppm}
diff --git a/test/pamscale-filters2.ok b/test/pamscale-filters2.ok
new file mode 100644
index 00000000..b8d51d37
--- /dev/null
+++ b/test/pamscale-filters2.ok
@@ -0,0 +1,24 @@
+-xscale=4 -yscale=3 mitchell:
+match
+match
+-xscale=3 -yscale=4 gauss:
+match
+match
+-xscale=2 -yscale=2 sinc:
+match
+match
+-xscale=2 -yscale=4 bessel:
+match
+match
+-xscale=3 -yscale=3 hanning:
+match
+match
+-xscale=5 -yscale=5 hamming:
+match
+match
+5.85 blackman:
+match
+match
+5.10 kaiser:
+match
+match
diff --git a/test/pamscale-filters2.test b/test/pamscale-filters2.test
new file mode 100755
index 00000000..f7370020
--- /dev/null
+++ b/test/pamscale-filters2.test
@@ -0,0 +1,97 @@
+#! /bin/bash
+# This script tests: pamscale pamstretch pamstretch-gen
+# Also requires: pamvalidate pnmpsnr
+
+tmpdir=${tmpdir:-/tmp}
+stretch_ppm=${tmpdir}/stretch.ppm
+
+width_height=`pamfile -size testimg.ppm | \
+                awk '{print "-width="$1, "-height="$2}'`
+
+pamstretch -xscale=4 -yscale=3 testimg.ppm | pamvalidate > ${stretch_ppm}
+echo -xscale=4 -yscale=3 mitchell:  
+pamscale -xscale=4 -yscale=3 -filter=mitchell testimg.ppm | \
+  pnmpsnr -target1=34.15 -target2=46.95 -target3=41.02 - ${stretch_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+
+pamscale ${width_height} -filter=mitchell ${stretch_ppm} | \
+  pnmpsnr -target1=33.36 -target2=46.74 -target3=40.79 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${stretch_ppm}
+
+pamstretch -xscale=3 -yscale=4 testimg.ppm | pamvalidate > ${stretch_ppm}
+echo -xscale=3 -yscale=4 gauss:  
+pamscale -xscale=3 -yscale=4 -filter=gauss testimg.ppm | \
+  pnmpsnr -target1=34.4 -target2=46.98 -target3=41.07 - ${stretch_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+
+pamscale ${width_height} -filter=gauss ${stretch_ppm} | \
+  pnmpsnr -target1=33.1 -target2=46.61 -target3=40.64 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${stretch_ppm}
+
+pamstretch -xscale=2 -yscale=2 testimg.ppm | pamvalidate > ${stretch_ppm}
+echo -xscale=2 -yscale=2 sinc:  
+pamscale -xscale=2 -yscale=2 -filter=sinc testimg.ppm | \
+  pnmpsnr -target1=36.27 -target2=49.39 -target3=43.62 - ${stretch_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+
+pamscale ${width_height} -filter=sinc ${stretch_ppm} | \
+  pnmpsnr -target1=36.23 -target2=49.47 -target3=43.69 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${stretch_ppm}
+
+pamstretch -xscale=2 -yscale=4 testimg.ppm | pamvalidate > ${stretch_ppm}
+echo -xscale=2 -yscale=4 bessel:  
+pamscale -xscale=2 -yscale=4 -filter=bessel testimg.ppm | \
+  pnmpsnr -target1=35.09 -target2=47.77 -target3=41.88 - ${stretch_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+
+pamscale ${width_height} -filter=bessel ${stretch_ppm} | \
+  pnmpsnr -target1=33.99 -target2=47.47 -target3=41.55 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${stretch_ppm}
+
+pamstretch -xscale=3 -yscale=3 testimg.ppm | pamvalidate > ${stretch_ppm}
+echo -xscale=3 -yscale=3 hanning:  
+pamscale -xscale=3 -yscale=3 -filter=hanning testimg.ppm | \
+  pnmpsnr -target1=34.73 -target2=47.42 -target3=41.54 - ${stretch_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+
+pamscale ${width_height} -filter=hanning ${stretch_ppm} | \
+  pnmpsnr -target1=33.86 -target2=47.24 -target3=41.29 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${stretch_ppm}
+
+pamstretch -xscale=5 -yscale=5 testimg.ppm | pamvalidate > ${stretch_ppm}
+echo -xscale=5 -yscale=5 hamming:  
+pamscale -xscale=5 -yscale=5 -filter=hamming testimg.ppm | \
+  pnmpsnr -target1=33.4 -target2=46.02 -target3=40.07 - ${stretch_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+
+pamscale ${width_height} -filter=hamming ${stretch_ppm} | \
+  pnmpsnr -target1=32.49 -target2=45.81 -target3=39.8 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${stretch_ppm}
+
+pamstretch-gen 5.85 testimg.ppm | pamvalidate > ${stretch_ppm}
+echo 5.85 blackman:  
+pamscale 5.85 -filter=blackman testimg.ppm | \
+  pnmpsnr -target1=34.29 -target2=48.78 -target3=42.75 - ${stretch_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+
+pamscale ${width_height} -filter=blackman ${stretch_ppm} | \
+  pnmpsnr -target1=33.69 -target2=48.83 -target3=42.72 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${stretch_ppm}
+
+pamstretch-gen 5.10 testimg.ppm | pamvalidate > ${stretch_ppm}
+echo 5.10 kaiser:  
+pamscale 5.10 -filter=kaiser testimg.ppm | \
+  pnmpsnr -target1=34.58 -target2=49.03 -target3=43.01 - ${stretch_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+
+pamscale ${width_height} -filter=blackman ${stretch_ppm} | \
+  pnmpsnr -target1=33.69 -target2=48.83 -target3=42.72 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${stretch_ppm}
diff --git a/test/pamscale-filters3.ok b/test/pamscale-filters3.ok
new file mode 100644
index 00000000..94d4ae96
--- /dev/null
+++ b/test/pamscale-filters3.ok
@@ -0,0 +1,7 @@
+failure : 0
+3.96 hermite:
+match
+match
+2.75 lanczos:
+match
+match
diff --git a/test/pamscale-filters3.test b/test/pamscale-filters3.test
new file mode 100755
index 00000000..83198e2b
--- /dev/null
+++ b/test/pamscale-filters3.test
@@ -0,0 +1,32 @@
+#! /bin/bash
+# This script tests: pamscale pamstretch pamstretch-gen 
+ # Also requires: pamvalidate pnmpsnr || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+
+tmpdir=${tmpdir:-/tmp}
+stretch_ppm=${tmpdir}/stretch.ppm
+
+width_height=`pamfile -size testimg.ppm | \
+                awk '{print "-width="$1, "-height="$2}'`
+
+pamstretch-gen 3.96 testimg.ppm | pamvalidate > ${stretch_ppm}
+echo 3.96 hermite:  
+pamscale 3.96 -filter=hermite testimg.ppm | \
+  pnmpsnr -target1=34.13 -target2=48.95 -target3=42.73 - ${stretch_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+
+pamscale ${width_height} -filter=hermite ${stretch_ppm} | \
+  pnmpsnr -target1=33.12 -target2=48.59 -target3=42.3 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${stretch_ppm}
+
+pamstretch-gen 2.75 testimg.ppm | pamvalidate > ${stretch_ppm}
+echo 2.75 lanczos:  
+pamscale 2.75 -filter=lanczos testimg.ppm | \
+  pnmpsnr -target1=32.93 -target2=48.28 -target3=41.89 - ${stretch_ppm} || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+
+pamscale ${width_height} -filter=lanczos ${stretch_ppm} | \
+  pnmpsnr -target1=32.81 -target2=48.43 -target3=41.94 testimg.ppm - || \
+  echo failure ${PIPESTATUS[@]} ":" $?
+rm ${stretch_ppm}
diff --git a/test/pamscale-reportonly.ok b/test/pamscale-reportonly.ok
new file mode 100644
index 00000000..00152411
--- /dev/null
+++ b/test/pamscale-reportonly.ok
@@ -0,0 +1,18 @@
+227 149 3.000000 3.000000 681 447
+227 149 5.000000 5.000000 1135 745
+227 149 2.682819 2.684564 609 400
+227 149 2.819383 2.818792 640 420
+227 149 0.440529 0.442953 100 66
+227 149 0.167401 0.167785 38 25
+227 149 0.101322 0.100671 23 15
+227 149 2.819383 2.684564 640 400
+227 149 2.819383 2.684564 640 400
+227 149 2.819383 2.000000 640 298
+227 149 1.400881 2.684564 318 400
+227 149 1.000000 1.000000 227 149
+expected error
+expected error
+expected error
+expected error
+expected error
+expected error
diff --git a/test/pamscale-reportonly.test b/test/pamscale-reportonly.test
new file mode 100755
index 00000000..7205be2f
--- /dev/null
+++ b/test/pamscale-reportonly.test
@@ -0,0 +1,35 @@
+#! /bin/bash
+# This script tests: pamscale
+# Also requires:
+
+pamscale -reportonly 3 testimg.ppm
+pamscale -reportonly 5 testimg.ppm
+pamscale -reportonly -xysize 640 400 testimg.ppm
+pamscale -reportonly -xyfill 640 400 testimg.ppm
+pamscale -reportonly -xyfit  100 100 testimg.ppm
+pamscale -reportonly -reduce 6 testimg.ppm
+pamscale -reportonly -reduce 10 testimg.ppm
+pamscale -reportonly -xsize=640 -ysize=400 testimg.ppm
+pamscale -reportonly -width=640 -height=400 testimg.ppm
+pamscale -reportonly -width=640 -yscale=2 testimg.ppm
+pamscale -reportonly -xscale=1.4 -height=400 testimg.ppm
+pamscale -reportonly -pixels=45000 testimg.ppm
+
+# expected error cases
+echo 1>&2
+echo "Invalid command-line argument combinations." 1>&2
+echo "Error messages should appear below the line." 1>&2
+echo "-----------------------------------------------------------" 1>&2
+pamscale -reportonly -xsize=640 -ysize=400 -xscale=2 testimg.ppm || \
+  echo expected error
+pamscale -reportonly -xsize=640 -xscale=2 -yscale=3 testimg.ppm || \
+  echo expected error
+pamscale -reportonly -xsize=640 -ysize=400 -pixels=200000 testimg.ppm || \
+  echo expected error
+pamscale -reportonly -xsize=640 -ysize=400 -xysize 640 400 testimg.ppm || \
+  echo expected error
+pamscale -reportonly -xsize=640 -ysize=400 -xyfit  640 400 testimg.ppm || \
+  echo expected error
+pamscale -reportonly -xsize=640 -ysize=400 -xyfill 640 400 testimg.ppm || \
+  echo expected error
+
diff --git a/test/pamstretch.ok b/test/pamstretch.ok
new file mode 100644
index 00000000..3253cfd6
--- /dev/null
+++ b/test/pamstretch.ok
@@ -0,0 +1,20 @@
+test 1
+211995824 12277766
+test 2
+1361899 202953
+test 3
+2735552884 302379
+test 4
+3681010585 802767
+test 5
+1926073387 101484
+1926073387 101484
+1926073387 101484
+1926073387 101484
+test 6
+3638965616 913236
+598302847 913236
+220708621 903111
+3948746482 903111
+3948746482 903111
+220708621 903111
diff --git a/test/pamstretch.test b/test/pamstretch.test
new file mode 100755
index 00000000..55dd4e9c
--- /dev/null
+++ b/test/pamstretch.test
@@ -0,0 +1,48 @@
+#! /bin/bash
+# This script tests: pamstretch
+# Also requires: pamcut pamfile
+
+# Test 1.  Should print 211995824 12277766
+echo test 1
+pamstretch 11 testimg.ppm | cksum
+
+# Test 2.  Should print 1361899 202953
+echo test 2
+
+pamstretch -xscale=2 -blackedge testimg.ppm | cksum
+
+# Test 3.  Should print 3427416462 301047
+echo test 3
+dropedge1_ppm=${tmpdir}/drop1.ppm
+pamstretch -yscale=3 -dropedge testimg.ppm | tee ${dropedge1_ppm} | cksum
+
+# Test 4. Should print 3681010585 802767
+echo test 4
+pamstretch -xscale=2 -yscale=4 -dropedge testimg.ppm | cksum
+
+# Test 5.  Should print 1926073387 101484 four times
+echo test 5
+pamstretch 1 testimg.ppm | cksum
+pamstretch -xscale=1 -yscale=1 testimg.ppm | cksum
+pamstretch 1 -dropedge testimg.ppm | cksum
+pamstretch 1 -blackedge testimg.ppm | cksum
+
+# Test 6.
+# Should print 3638965616 913236 , 598302847 913236 
+# followed by 3948746482 903111 four times and finally
+# -width=678 -height=444"
+
+echo test 6
+stretch_ppm=${tmpdir}/stretch.ppm
+dropedge_ppm=${tmpdir}/drop.ppm
+blackedge_ppm=${tmpdir}/black.ppm
+pamstretch 3 testimg.ppm | tee ${stretch_ppm} | cksum
+pamstretch 3 -blackedge testimg.ppm | tee ${blackedge_ppm} | cksum
+pamstretch -xscale=3 -dropedge ${dropedge1_ppm} | tee ${dropedge_ppm} | cksum
+width_height=`pamfile -size ${dropedge_ppm} | \
+              awk '{print "-width="$1, "-height="$2}'`
+pamcut -left=0 -top=0 ${width_height} ${stretch_ppm} | cksum
+pamcut -left=0 -top=0 ${width_height} ${blackedge_ppm} | cksum
+pamcut -left=0 -top=0 ${width_height} ${dropedge_ppm} | cksum
+
+rm ${stretch_ppm} ${dropedge_ppm} ${dropedge1_ppm} ${blackedge_ppm}
\ No newline at end of file
diff --git a/test/ppmbrighten.ok b/test/ppmbrighten.ok
index 0ce9f722..376e71d3 100644
--- a/test/ppmbrighten.ok
+++ b/test/ppmbrighten.ok
@@ -1,3 +1,3 @@
-2634278866 101484
-2791274519 101484
-2464564658 101484
+1969633344 101484
+3688219243 101484
+295150171 101484
diff --git a/test/ppmbrighten.test b/test/ppmbrighten.test
index fa7702d8..46c5cab5 100755
--- a/test/ppmbrighten.test
+++ b/test/ppmbrighten.test
@@ -2,6 +2,10 @@
 # This script tests: ppmbrighten
 # Also requires:
 
+# Failure message
+## Ppmbrighten is sensitive to subtle differences in floating point math.
+## If this test fails, please run the program and visually examine
+## the output.
 
 ppmbrighten -v 100 testimg.ppm | cksum
 ppmbrighten -v 100 -normalize testimg.ppm | cksum
diff --git a/version.mk b/version.mk
index ca69fbb0..dc0d192a 100644
--- a/version.mk
+++ b/version.mk
@@ -1,3 +1,3 @@
 NETPBM_MAJOR_RELEASE = 10
-NETPBM_MINOR_RELEASE = 85
-NETPBM_POINT_RELEASE = 5
+NETPBM_MINOR_RELEASE = 86
+NETPBM_POINT_RELEASE = 0