about summary refs log tree commit diff
path: root/editor
diff options
context:
space:
mode:
authorgiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2019-03-30 15:18:58 +0000
committergiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2019-03-30 15:18:58 +0000
commitaad47e743f21763ac13b73eb927ad7d001254572 (patch)
tree2be395fe14e199ac00f002dc8b0ded2f0304c557 /editor
parentb10a418cefe3413a727b89300848dc84e8a6195d (diff)
downloadnetpbm-mirror-aad47e743f21763ac13b73eb927ad7d001254572.tar.gz
netpbm-mirror-aad47e743f21763ac13b73eb927ad7d001254572.tar.xz
netpbm-mirror-aad47e743f21763ac13b73eb927ad7d001254572.zip
Promote Development to Advanced
git-svn-id: http://svn.code.sf.net/p/netpbm/code/advanced@3587 9d0c8265-081b-0410-96cb-a4ca84ce46f8
Diffstat (limited to 'editor')
-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
12 files changed, 2121 insertions, 784 deletions
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.
+*/
+