about summary refs log tree commit diff
path: root/editor
diff options
context:
space:
mode:
authorgiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2016-03-27 01:46:26 +0000
committergiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2016-03-27 01:46:26 +0000
commitdff6b9fdfeb78fe21a66aa459ddc1d5f7e362dfa (patch)
treeb147568ccffc4cdba9e2a98de1452450ba8e55c3 /editor
parent4ce684c4978610d1ea42be1b00f7332f3f5f337a (diff)
downloadnetpbm-mirror-dff6b9fdfeb78fe21a66aa459ddc1d5f7e362dfa.tar.gz
netpbm-mirror-dff6b9fdfeb78fe21a66aa459ddc1d5f7e362dfa.tar.xz
netpbm-mirror-dff6b9fdfeb78fe21a66aa459ddc1d5f7e362dfa.zip
Promote Advanced (10.73) to Stable
git-svn-id: http://svn.code.sf.net/p/netpbm/code/stable@2692 9d0c8265-081b-0410-96cb-a4ca84ce46f8
Diffstat (limited to 'editor')
-rw-r--r--editor/Makefile22
-rw-r--r--editor/pambackground.c4
-rw-r--r--editor/pamcomp.c505
-rw-r--r--editor/pamcut.c32
-rw-r--r--editor/pamcut.test10
-rw-r--r--editor/pamdice.c16
-rw-r--r--editor/pamdither.c320
-rw-r--r--editor/pamditherbw.c13
-rw-r--r--editor/pamedge.c5
-rw-r--r--editor/pamenlarge.c146
-rw-r--r--editor/pamenlarge.test8
-rw-r--r--editor/pamflip.test12
-rw-r--r--editor/pamflip/Makefile36
-rw-r--r--editor/pamflip/config.h7
-rw-r--r--editor/pamflip/flip.h16
-rw-r--r--editor/pamflip/pamflip.c (renamed from editor/pamflip.c)217
-rw-r--r--editor/pamflip/pamflip_sse.c429
-rw-r--r--editor/pamflip/pamflip_sse.h12
-rw-r--r--editor/pamfunc.c128
-rw-r--r--editor/pammasksharpen.c10
-rw-r--r--editor/pamperspective.c5
-rw-r--r--editor/pamrecolor.c528
-rw-r--r--editor/pamrubber.c1475
-rw-r--r--editor/pamscale.c552
-rw-r--r--editor/pamsistoaglyph.c19
-rw-r--r--editor/pamstretch.c2
-rw-r--r--editor/pamthreshold.c20
-rw-r--r--editor/pamundice.c35
-rw-r--r--editor/pamwipeout.c229
-rw-r--r--editor/pbmclean.c835
-rw-r--r--editor/pbmpscale.c550
-rw-r--r--editor/pgmdeshadow.c6
-rw-r--r--editor/pgmmedian.c4
-rw-r--r--editor/pnmcat.c20
-rw-r--r--editor/pnmcomp.c460
-rw-r--r--editor/pnmconvol.c3694
-rw-r--r--editor/pnmcrop.c6
-rwxr-xr-xeditor/pnmflip25
-rw-r--r--editor/pnmgamma.c14
-rw-r--r--editor/pnmhisteq.c253
-rw-r--r--editor/pnminvert.c21
-rw-r--r--editor/pnminvert.test15
-rwxr-xr-xeditor/pnmmargin54
-rw-r--r--editor/pnmmontage.c434
-rw-r--r--editor/pnmnlfilt.c2
-rw-r--r--editor/pnmnorm.c389
-rw-r--r--editor/pnmpad.c266
-rw-r--r--editor/pnmpaste.c58
-rwxr-xr-xeditor/pnmquant64
-rwxr-xr-xeditor/pnmquantall208
-rw-r--r--editor/pnmremap.c77
-rw-r--r--editor/pnmrotate.c2
-rw-r--r--editor/pnmscalefixed.c39
-rw-r--r--editor/pnmshear.c37
-rw-r--r--editor/pnmsmooth.c162
-rw-r--r--editor/pnmstitch.c20
-rw-r--r--editor/pnmtile.c2
-rw-r--r--editor/ppmbrighten.c4
-rw-r--r--editor/ppmchange.c2
-rw-r--r--editor/ppmcolormask.c5
-rw-r--r--editor/ppmdist.c29
-rw-r--r--editor/ppmdither.c704
-rw-r--r--editor/ppmdraw.c73
-rwxr-xr-xeditor/ppmfade32
-rw-r--r--editor/ppmmix.c179
-rwxr-xr-xeditor/ppmquant28
-rwxr-xr-xeditor/ppmquantall96
-rw-r--r--editor/ppmquantall.csh57
-rwxr-xr-xeditor/ppmshadow35
-rw-r--r--editor/specialty/Makefile2
-rw-r--r--editor/specialty/pamdeinterlace.c2
-rw-r--r--editor/specialty/pammixinterlace.c2
-rw-r--r--editor/specialty/pampaintspill.c500
-rw-r--r--editor/specialty/pgmabel.c6
-rw-r--r--editor/specialty/pgmmorphconv.c520
-rw-r--r--editor/specialty/pnmindex.c81
-rw-r--r--editor/specialty/pnmmercator.c430
-rw-r--r--editor/specialty/ppm3d.c4
-rw-r--r--editor/specialty/ppmglobe.c4
-rw-r--r--editor/specialty/ppmntsc.c311
-rw-r--r--editor/specialty/ppmrelief.c112
81 files changed, 10871 insertions, 4877 deletions
diff --git a/editor/Makefile b/editor/Makefile
index d5d7633f..c163f220 100644
--- a/editor/Makefile
+++ b/editor/Makefile
@@ -7,7 +7,7 @@ VPATH=.:$(SRCDIR)/$(SUBDIR)
 
 include $(BUILDDIR)/config.mk
 
-SUBDIRS = specialty
+SUBDIRS = pamflip specialty
 
 # We tend to separate out the build targets so that we don't have
 # any more dependencies for a given target than it really needs.
@@ -19,13 +19,14 @@ SUBDIRS = specialty
 PORTBINARIES = pamaddnoise pambackground pamcomp pamcut \
 	       pamdice pamditherbw pamedge \
 	       pamenlarge \
-	       pamflip pamfunc pammasksharpen \
-	       pamperspective \
+	       pamfunc pammasksharpen \
+	       pamperspective pamrecolor pamrubber \
 	       pamscale pamsistoaglyph pamstretch pamthreshold pamundice \
+	       pamwipeout \
 	       pbmclean pbmmask pbmpscale pbmreduce \
 	       pgmdeshadow pgmenhance \
 	       pgmmedian \
-	       pnmalias pnmcat pnmcomp pnmconvol pnmcrop \
+	       pnmalias pnmcat pnmconvol pnmcrop \
 	       pnmgamma \
 	       pnmhisteq pnminvert pnmmontage \
 	       pnmnlfilt pnmnorm pnmpad pnmpaste \
@@ -42,10 +43,9 @@ PORTBINARIES = pamaddnoise pambackground pamcomp pamcut \
 NOMERGEBINARIES = 
 MERGEBINARIES = $(PORTBINARIES)
 
-
 BINARIES = $(MERGEBINARIES) $(NOMERGEBINARIES)
-SCRIPTS = pnmflip ppmfade ppmquant ppmquantall ppmshadow \
-	  pamstretch-gen pnmmargin pnmquant 
+SCRIPTS = pnmflip ppmfade ppmquant ppmshadow \
+	  pamstretch-gen pnmmargin pnmquant pnmquantall 
 
 OBJECTS = $(BINARIES:%=%.o)
 
@@ -90,3 +90,11 @@ install.bin.local: $(PKGDIR)/bin
 	cd $(PKGDIR)/bin ; \
 	rm -f pnmscale$(EXE) ; \
 	$(SYMLINK) pamscale$(EXE) pnmscale$(EXE)
+# In March 2012, pnmquantall replaced ppmquantall
+	cd $(PKGDIR)/bin ; \
+	rm -f ppmquantall$(EXE) ; \
+	$(SYMLINK) pnmquantall ppmquantall
+# In August 2014, pamcomp replaced pnmcomp
+	cd $(PKGDIR)/bin ; \
+	rm -f pnmcomp$(EXE) ; \
+	$(SYMLINK) pamcomp$(EXE) pnmcomp$(EXE)
diff --git a/editor/pambackground.c b/editor/pambackground.c
index 1aa53177..b4941042 100644
--- a/editor/pambackground.c
+++ b/editor/pambackground.c
@@ -37,7 +37,7 @@ parseCommandLine(int argc, char ** const argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (argc-1 < 1)
@@ -182,7 +182,7 @@ determineBackgroundColor(struct pam * const pamP,
             pnm_colorname(pamP, *bgColorP, hexokTrue);
         pm_message("Background color is %s", colorname);
 
-        strfree(colorname);
+        pm_strfree(colorname);
     }
 
     pnm_freepamtuple(lr);
diff --git a/editor/pamcomp.c b/editor/pamcomp.c
index e89509cc..9855e173 100644
--- a/editor/pamcomp.c
+++ b/editor/pamcomp.c
@@ -39,13 +39,30 @@ enum vertPos {ABOVE, TOP, MIDDLE, BOTTOM, BELOW};
 
 
 enum sampleScale {INTENSITY_SAMPLE, GAMMA_SAMPLE};
-/* This indicates a scale for a PAM sample value.  INTENSITY_SAMPLE means
-   the value is proportional to light intensity; GAMMA_SAMPLE means the 
-   value is gamma-adjusted as defined in the PGM/PPM spec.  In both
-   scales, the values are continuous and normalized to the range 0..1.
+    /* This indicates a scale for a PAM sample value.  INTENSITY_SAMPLE means
+       the value is proportional to light intensity; GAMMA_SAMPLE means the 
+       value is gamma-adjusted as defined in the PGM/PPM spec.  In both
+       scales, the values are continuous and normalized to the range 0..1.
+       
+       This scale has no meaning if the PAM is not a visual image.  
+    */
 
-   This scale has no meaning if the PAM is not a visual image.  
-*/
+enum alphaMix {AM_KEEPUNDER, AM_OVERLAY};
+    /* This is a way to combine the alpha channels (transparency/opacity)
+       of the underlay and overlay images to form the alpha channel of the
+       composed result.
+
+       AM_KEEPUNDER means the alpha for the composed result is identical
+       to that of the underlay image.  I.e. the overlay merely modifies the
+       color.
+
+       AM_OVERLAY means the result is as if the underlay and overlay images
+       are plastic slides and they are taped together to form a composed slide.
+       So for one thing, the transparency of the composed image is the product
+       of the transparencies of the component images.  But the color is also
+       different, because the transparency of the underlaying image affects
+       its contribution to the composition.
+    */
 
 struct cmdlineInfo {
     /* All the information the user supplied in the command line,
@@ -61,6 +78,7 @@ struct cmdlineInfo {
     enum horizPos align;
     enum vertPos valign;
     unsigned int linear;
+    unsigned int mixtransparency;
 };
 
 
@@ -80,7 +98,7 @@ parseCommandLine(int                        argc,
    was passed to us as the argv array.  We also trash *argv.
 -----------------------------------------------------------------------------*/
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -93,28 +111,30 @@ parseCommandLine(int                        argc,
     MALLOCARRAY_NOFAIL(option_def, 100);
 
     option_def_index = 0;   /* incremented by OPTENT3 */
-    OPTENT3(0, "invert",     OPT_FLAG,   NULL,                  
+    OPTENT3(0, "invert",             OPT_FLAG,   NULL,                  
             &cmdlineP->alphaInvert,       0);
-    OPTENT3(0, "xoff",       OPT_INT,    &cmdlineP->xoff,       
+    OPTENT3(0, "xoff",               OPT_INT,    &cmdlineP->xoff,       
             &xoffSpec,                    0);
-    OPTENT3(0, "yoff",       OPT_INT,    &cmdlineP->yoff,       
+    OPTENT3(0, "yoff",               OPT_INT,    &cmdlineP->yoff,       
             &yoffSpec,                    0);
-    OPTENT3(0, "opacity",    OPT_FLOAT, &cmdlineP->opacity,
+    OPTENT3(0, "opacity",            OPT_FLOAT,  &cmdlineP->opacity,
             &opacitySpec,                 0);
-    OPTENT3(0, "alpha",      OPT_STRING, &cmdlineP->alphaFilespec,
+    OPTENT3(0, "alpha",              OPT_STRING, &cmdlineP->alphaFilespec,
             &alphaSpec,                   0);
-    OPTENT3(0, "align",      OPT_STRING, &align,
+    OPTENT3(0, "align",              OPT_STRING, &align,
             &alignSpec,                   0);
-    OPTENT3(0, "valign",     OPT_STRING, &valign,
+    OPTENT3(0, "valign",             OPT_STRING, &valign,
             &valignSpec,                  0);
-    OPTENT3(0, "linear",     OPT_FLAG,    NULL,       
+    OPTENT3(0, "linear",             OPT_FLAG,   NULL,       
             &cmdlineP->linear,            0);
+    OPTENT3(0, "mixtransparency",    OPT_FLAG,   NULL,       
+            &cmdlineP->mixtransparency,   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 */
 
-    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. */
 
 
@@ -217,32 +237,65 @@ commonFormat(int const formatA,
 
 
 
-static void
+typedef enum { TT_BLACKANDWHITE, TT_GRAYSCALE, TT_RGB } BaseTupletype;
+
+
+
+static BaseTupletype
 commonTupletype(const char * const tupletypeA, 
-                const char * const tupletypeB, 
-                char *       const tupletypeOut,
-                unsigned int const size) {
-
-    if (strncmp(tupletypeA, "RGB", 3) == 0 ||
-        strncmp(tupletypeB, "RGB", 3) == 0)
-        strncpy(tupletypeOut, "RGB", size);
-    else if (strncmp(tupletypeA, "GRAYSCALE", 9) == 0 ||
-        strncmp(tupletypeB, "GRAYSCALE", 9) == 0)
-        strncpy(tupletypeOut, "GRAYSCALE", size);
-    else if (strncmp(tupletypeA, "BLACKANDWHITE", 13) == 0 ||
-        strncmp(tupletypeB, "BLACKANDWHITE", 13) == 0)
-        strncpy(tupletypeOut, "BLACKANDWHITE", size);
+                const char * const tupletypeB) {
+
+    if (strneq(tupletypeA, "RGB", 3) ||
+        strneq(tupletypeB, "RGB", 3))
+        return TT_RGB;
+    else if (strneq(tupletypeA, "GRAYSCALE", 9) ||
+             strneq(tupletypeB, "GRAYSCALE", 9))
+        return TT_GRAYSCALE;
+    else if (strneq(tupletypeA, "BLACKANDWHITE", 13) ||
+             strneq(tupletypeB, "BLACKANDWHITE", 13))
+        return TT_BLACKANDWHITE;
     else
         /* Results are undefined for this case, so we do a hail Mary. */
-        strncpy(tupletypeOut, tupletypeA, size);
+        return TT_RGB;
+}
+
+
+
+static void
+determineOutputTupleType(BaseTupletype const baseTupletype,
+                         bool          const underlayHaveOpacity,
+                         char *        const tupleType,
+                         size_t        const size) {
+
+    char buffer[80];
+
+    switch (baseTupletype) {
+    case TT_BLACKANDWHITE:
+        STRSCPY(buffer, "RGB");
+        break;
+    case TT_GRAYSCALE:
+        STRSCPY(buffer, "GRAYSCALE");
+        break;
+    case TT_RGB:
+        STRSCPY(buffer, "RGB");
+        break;
+    }
+
+    if (underlayHaveOpacity)
+        STRSCAT(buffer, "_ALPHA");
+
+    strncpy(tupleType, buffer, size);
 }
 
 
 
 static void
-determineOutputType(struct pam * const composedPamP,
-                    struct pam * const underlayPamP,
-                    struct pam * const overlayPamP) {
+determineOutputType(const struct pam * const underlayPamP,
+                    const struct pam * const overlayPamP,
+                    struct pam *       const composedPamP) {
+
+    BaseTupletype const baseTupletype =
+        commonTupletype(underlayPamP->tuple_type, overlayPamP->tuple_type);
 
     composedPamP->height = underlayPamP->height;
     composedPamP->width  = underlayPamP->width;
@@ -250,22 +303,22 @@ determineOutputType(struct pam * const composedPamP,
     composedPamP->format = commonFormat(underlayPamP->format, 
                                         overlayPamP->format);
     composedPamP->plainformat = FALSE;
-    commonTupletype(underlayPamP->tuple_type, overlayPamP->tuple_type,
-                    composedPamP->tuple_type, 
-                    sizeof(composedPamP->tuple_type));
 
     composedPamP->maxval = pm_lcm(underlayPamP->maxval, overlayPamP->maxval, 
                                   1, PNM_OVERALLMAXVAL);
 
-    if (strcmp(composedPamP->tuple_type, "RGB") == 0)
-        composedPamP->depth = 3;
-    else if (strcmp(composedPamP->tuple_type, "GRAYSCALE") == 0)
-        composedPamP->depth = 1;
-    else if (strcmp(composedPamP->tuple_type, "BLACKANDWHITE") == 0)
-        composedPamP->depth = 1;
-    else
-        /* Results are undefined for this case, so we just do something safe */
-        composedPamP->depth = MIN(underlayPamP->depth, overlayPamP->depth);
+    composedPamP->visual = true;
+    composedPamP->color_depth = (baseTupletype == TT_RGB ? 3 : 1);
+    composedPamP->have_opacity = underlayPamP->have_opacity;
+    composedPamP->opacity_plane = (baseTupletype == TT_RGB ? 3 : 1);
+
+    composedPamP->depth =
+        (baseTupletype == TT_RGB ? 3 : 1) +
+        (underlayPamP->have_opacity ? 1 : 0);
+
+    determineOutputTupleType(baseTupletype, underlayPamP->have_opacity,
+                             composedPamP->tuple_type,
+                             sizeof(composedPamP->tuple_type));
 }
 
 
@@ -374,16 +427,78 @@ computeOverlayPosition(int                const underCols,
 
 
 
+/*----------------------------------------------------------------------------
+   COMPOSING TRANSPARENT PIXELS - AM_OVERLAY
+
+   There are various interpretations of composing transparent pixels, which
+   you can see in the definition of enum alphaMix.
+
+   For AM_OVERLAY, which means the result is as if the underlaying and
+   overlaying images are plastic slides and we taped them together to make a
+   composed plastic slide, the calculations go as follows.
+
+   Opacity and transparency are fractions in [0,1] and are complements:
+
+     X_T = 1 - X_O
+
+   We consider a further composed image, where in the future someone projects
+   another image (the background image) through our composed slide.  The
+   opacity and color of our composed image must be such that the light
+   emerging (the result image) is the same as if it travelled serially through
+   the underlaying and overlaying slide.
+
+   The transparency of each slide is the fraction of light that gets
+   through that slide, so the transparency of the composed slide is the
+   product of the underlay and overlay transparencies.
+
+       C_T = U_T * O_T
+
+   The composed transparency determines the distribution of background and
+   composed image in the result.  We have to choose the composed color such
+   that the result of combining it thus with the background image is the same
+   as the result of combining the background image with the underlay image
+   (distributed according to the underlay transparency), then combining that
+   with the overlay image (distributed according to the overlay transparency).
+
+       C = (O_T * U_O * U + O_O * O) / C_O   if C_O != 0
+
+   But this is meaningless if both components are fully transparent, which
+   means composed image opacity (C_O) is zero.  In that case, the composed
+   color is irrelevant to the result image and we can choose whatever color
+   is most likely to be convenient in applications that don't use the
+   transparency of the composed image.  We choose the pure underlay color.
+
+       C = U   if C_O = 0
+
+   function composeComponents() computes the above formula.
+
+-----------------------------------------------------------------------------*/
+
+
+
 static sample
 composeComponents(sample           const compA, 
                   sample           const compB,
                   float            const distrib,
+                  float            const aFactor,
+                  float            const composedFactor,
                   sample           const maxval,
                   enum sampleScale const sampleScale) {
 /*----------------------------------------------------------------------------
-  Compose a single component of each of two pixels, with 'distrib' being
+  Compose a single color component of each of two pixels, with 'distrib' being
   the fraction of 'compA' in the result, 1-distrib the fraction of 'compB'.
-  
+  Note that this does not apply to an opacity component.
+
+  'sampleScale' tells in what domain the 'distrib' fraction applies:
+  brightness or light intensity (gamma-adjusted or not).
+
+  'aFactor' is a factor in [0,1] to apply to 'compA' first.
+
+  'composedFactor' is a factor to apply to the result.
+
+  See above for explanation of why 'aFactor' and 'composedFactor' are
+  useful.
+
   The inputs and result are based on a maxval of 'maxval'.
   
   Note that while 'distrib' in the straightforward case is always in
@@ -392,21 +507,23 @@ composeComponents(sample           const compA,
 -----------------------------------------------------------------------------*/
     sample retval;
 
-    if (fabs(1.0-distrib) < .001)
+    if (fabs(distrib) > .999 && aFactor > .999 && composedFactor > .999)
         /* Fast path for common case */
         retval = compA;
     else {
         if (sampleScale == INTENSITY_SAMPLE) {
             sample const mix = 
-                ROUNDU(compA * distrib + compB * (1.0 - distrib));
+                ROUNDU(compA * aFactor * distrib + compB * (1.0 - distrib));
             retval = MIN(maxval, MAX(0, mix));
         } else {
             float const compANormalized = (float)compA/maxval;
             float const compBNormalized = (float)compB/maxval;
             float const compALinear = pm_ungamma709(compANormalized);
             float const compBLinear = pm_ungamma709(compBNormalized);
+            float const compALinearAdj = compALinear * aFactor;
             float const mix = 
-                compALinear * distrib + compBLinear * (1.0 - distrib);
+                compALinearAdj * distrib + compBLinear * (1.0 - distrib)
+                * composedFactor;
             sample const sampleValue = ROUNDU(pm_gamma709(mix) * maxval);
             retval = MIN(maxval, MAX(0, sampleValue));
         }
@@ -416,6 +533,40 @@ composeComponents(sample           const compA,
 
 
 
+static sample
+composedOpacity(tuple         const uTuple,
+                struct pam *  const uPamP,
+                tuple         const oTuple,
+                struct pam *  const oPamP,
+                struct pam *  const cPamP,
+                enum alphaMix const alphaMix) {
+
+    sample retval;
+
+    assert(uPamP->have_opacity);
+
+    switch (alphaMix) {
+    case AM_OVERLAY: {
+        /* output transparency is product of two input transparencies */
+        float const underlayTrans =
+            1.0 - ((float)uTuple[uPamP->opacity_plane]/uPamP->maxval);
+        float const overlayTrans =
+            1.0 - ((float)oTuple[oPamP->opacity_plane]/oPamP->maxval);
+        float const composedTrans = underlayTrans * overlayTrans;
+        sample const sampleValue =  (1.0 - composedTrans) * cPamP->maxval;
+        retval = MIN(cPamP->maxval, MAX(0, sampleValue));
+        pm_message("underlay = %lu/%f, overlay = %f, composed = %f",
+                   uTuple[uPamP->opacity_plane],underlayTrans, overlayTrans, composedTrans);
+    } break;
+    case AM_KEEPUNDER:
+        retval = uTuple[uPamP->opacity_plane];
+        break;
+    }
+    return retval;
+}
+
+
+
 static void
 overlayPixel(tuple            const overlayTuple,
              struct pam *     const overlayPamP,
@@ -423,20 +574,54 @@ overlayPixel(tuple            const overlayTuple,
              struct pam *     const underlayPamP,
              tuplen           const alphaTuplen,
              bool             const invertAlpha,
-             bool             const overlayHasOpacity,
-             unsigned int     const opacityPlane,
              tuple            const composedTuple,
              struct pam *     const composedPamP,
              float            const masterOpacity,
-             enum sampleScale const sampleScale) {
+             enum sampleScale const sampleScale,
+             enum alphaMix    const alphaMix) {
+/*----------------------------------------------------------------------------
+   Generate the result of overlaying one pixel with another, taking opacity
+   into account, viz overlaying 'underlayTuple' with 'overlayTuple'.
+   'overlayPamP' and 'underlayPamP', respectively, describe those tuples.
+
+   We always assume the underlay pixel is opaque.
+
+   We use the following declarations of the opacity of the overlay pixel in
+   deciding how much of the underlay pixel should show through.  The product
+   of all the indicated opacity factors is the overall opacity factor, where
+   an opacity factor is a real number from 0 to 1 and 1 means none of the
+   underlay pixel shows through and 0 means the overlay pixel is invisible and
+   the underlay pixel shows through in full force.
+
+     if *overlayPamP may indicate that 'overlayTuple' has an opacity component.
 
+     'alphaTuplen' is a normalized tuple whose first sample is the opacity
+     factor, except that iff 'invertAlpha' is true, it is a transparency
+     factor instead (opacity = 1 - transparency).
+
+     'masterOpacity' is a direct opacity factor
+
+   'sampleScale' tells whether the samples in the tuples are proportional
+   to brightness or light intensity (gamma-adjusted or not).  Opacity
+   factors apply to brightness (.5 means half the brightness of the result
+   comes from the underlay pixel, half comes from the overlay).
+
+   'alphaMix' tells how to determine the opacity of the result pixel.
+
+   Return the result as 'composedTuple', which has the form described by
+   'composedPamP'.
+-----------------------------------------------------------------------------*/
     float overlayWeight;
+    float underlayWeight;
+        /* Part of formula for AM_OVERLAY -- see explanation above */
+    float composedWeight;
+        /* Part of formula for AM_OVERLAY -- see explanation above */
 
     overlayWeight = masterOpacity;  /* initial value */
     
-    if (overlayHasOpacity)
+    if (overlayPamP->have_opacity)
         overlayWeight *= (float)
-            overlayTuple[opacityPlane] / overlayPamP->maxval;
+            overlayTuple[overlayPamP->opacity_plane] / overlayPamP->maxval;
     
     if (alphaTuplen) {
         float const alphaval = 
@@ -444,34 +629,73 @@ overlayPixel(tuple            const overlayTuple,
         overlayWeight *= alphaval;
     }
 
+    if (underlayPamP->have_opacity && alphaMix == AM_OVERLAY) {
+        struct pam * const uPamP = underlayPamP;
+        struct pam * const oPamP = overlayPamP;
+        sample const uOpacity = underlayTuple[uPamP->opacity_plane];
+        sample const oOpacity = overlayTuple[oPamP->opacity_plane];
+        sample const uMaxval = uPamP->maxval;
+        sample const oMaxval = oPamP->maxval;
+        float  const uOpacityN = uOpacity / uMaxval;
+        float  const oOpacityN = oOpacity / oMaxval;
+        float  const composedTrans = (1.0 - uOpacityN) * (1.0 * oOpacityN);
+        
+        if (composedTrans > .999) {
+            underlayWeight = 1.0;
+            composedWeight = 1.0;
+        } else {
+            underlayWeight = uOpacityN;
+            composedWeight = 1.0 / (1.0 - composedTrans);
+        }
+    } else {
+        underlayWeight = 1.0;
+        composedWeight = 1.0;
+    }
     {
         unsigned int plane;
         
-        for (plane = 0; plane < composedPamP->depth; ++plane)
+        for (plane = 0; plane < composedPamP->color_depth; ++plane)
             composedTuple[plane] = 
                 composeComponents(overlayTuple[plane], underlayTuple[plane], 
-                                  overlayWeight,
+                                  overlayWeight, underlayWeight,
+                                  composedWeight,
                                   composedPamP->maxval,
                                   sampleScale);
+
+        if (composedPamP->have_opacity)
+            composedTuple[composedPamP->opacity_plane] =
+                composedOpacity(underlayTuple, underlayPamP,
+                                overlayTuple, overlayPamP,
+                                composedPamP, alphaMix);
     }
 }
 
 
 
 static void
-adaptRowToOutputFormat(struct pam * const inpamP,
-                       tuple *      const tuplerow,
-                       struct pam * const outpamP) {
+adaptRowFormat(struct pam * const inpamP,
+               struct pam * const outpamP,
+               tuple *      const tuplerow) {
 /*----------------------------------------------------------------------------
    Convert the row in 'tuplerow', which is in a format described by
    *inpamP, to the format described by *outpamP.
 
    'tuplerow' must have enough allocated depth to do this.
 -----------------------------------------------------------------------------*/
+    assert(outpamP->visual);
+    assert(inpamP->visual);
+
     pnm_scaletuplerow(inpamP, tuplerow, tuplerow, outpamP->maxval);
 
-    if (strncmp(outpamP->tuple_type, "RGB", 3) == 0)
-        pnm_makerowrgb(inpamP, tuplerow);
+    if (outpamP->color_depth == 3) {
+        if (outpamP->have_opacity)
+            pnm_makerowrgba(inpamP, tuplerow);
+        else
+            pnm_makerowrgb(inpamP, tuplerow);
+    } else {
+        if (outpamP->have_opacity)
+            pnm_addopacityrow(inpamP, tuplerow);
+    }
 }
 
 
@@ -482,16 +706,22 @@ composeRow(int              const originleft,
            struct pam *     const overlayPamP,
            bool             const invertAlpha,
            float            const masterOpacity,
-           bool             const overlayHasOpacity,
-           unsigned int     const opacityPlane,
            struct pam *     const composedPamP,
            enum sampleScale const sampleScale,
+           enum alphaMix    const alphaMix,
            const tuple *    const underlayTuplerow,
            const tuple *    const overlayTuplerow,
            const tuplen *   const alphaTuplerown,
            tuple *          const composedTuplerow) {
+/*----------------------------------------------------------------------------
+   Create a row of tuples ('composedTupleRow') which is the composition of
+   row 'overlayTupleRow' laid over row 'underlayTupleRow', starting at
+   column 'originLeft'.
 
+   *underlayPamP and *overlayPamP describe the respective tuple rows.
+-----------------------------------------------------------------------------*/
     unsigned int col;
+
     for (col = 0; col < composedPamP->width; ++col) {
         int const ovlcol = col - originleft;
 
@@ -502,9 +732,8 @@ composeRow(int              const originleft,
             overlayPixel(overlayTuplerow[ovlcol], overlayPamP,
                          underlayTuplerow[col], underlayPamP,
                          alphaTuplen, invertAlpha,
-                         overlayHasOpacity, opacityPlane,
                          composedTuplerow[col], composedPamP,
-                         masterOpacity, sampleScale);
+                         masterOpacity, sampleScale, alphaMix);
         } else
             /* Overlay image does not touch this column. */
             pnm_assigntuple(composedPamP, composedTuplerow[col],
@@ -515,6 +744,55 @@ composeRow(int              const originleft,
 
 
 static void
+determineInputAdaptations(const struct pam * const underlayPamP,
+                          const struct pam * const overlayPamP,
+                          const struct pam * const composedPamP,
+                          struct pam *       const adaptUnderlayPamP,
+                          struct pam *       const adaptOverlayPamP) {
+/*----------------------------------------------------------------------------
+   For easy of computation, this program reads a tuple row from one of the
+   input files, then transforms it something similar to the format of the
+   eventual output tuple row.  E.g. if the input is grayscale and the
+   output color, it converts the depth 1 row read from the file to a depth
+   3 row for use in computations.
+
+   This function determines what the result of that transformation should be.
+   It's not as simple as it sounds because of opacity.  The overlay may have
+   an opacity plane that has to be kept for the computations, while the output
+   has no opacity plane.
+
+   Our output PAMs are meaningless except in the fields that pertain to a
+   row of tuples.  E.g. the file descriptor and image height members are
+   meaningless.
+-----------------------------------------------------------------------------*/
+    /* We make the underlay row identical to the composed (output) row,
+       except for its width.
+    */
+
+    *adaptUnderlayPamP = *composedPamP;
+    adaptUnderlayPamP->width = underlayPamP->width;
+
+    /* Same for the overlay row, except that it retains is original
+       opacity.
+    */
+
+    adaptOverlayPamP->width = overlayPamP->width;
+    adaptOverlayPamP->tuple_type[0] = '\0';  /* a hack; this doesn't matter */
+    adaptOverlayPamP->visual = true;
+    adaptOverlayPamP->color_depth = composedPamP->color_depth;
+    adaptOverlayPamP->have_opacity = overlayPamP->have_opacity;
+    adaptOverlayPamP->opacity_plane = composedPamP->color_depth;
+    adaptOverlayPamP->depth =
+        composedPamP->color_depth +
+        (overlayPamP->have_opacity ? 1 : 0);
+    adaptOverlayPamP->maxval = composedPamP->maxval;
+    adaptOverlayPamP->bytes_per_sample = composedPamP->bytes_per_sample;
+    adaptOverlayPamP->allocation_depth = overlayPamP->allocation_depth;
+}
+
+
+
+static void
 composite(int          const originleft, 
           int          const origintop, 
           struct pam * const underlayPamP,
@@ -523,7 +801,8 @@ composite(int          const originleft,
           bool         const invertAlpha,
           float        const masterOpacity,
           struct pam * const composedPamP,
-          bool         const assumeLinear) {
+          bool         const assumeLinear,
+          bool         const mixTransparency) {
 /*----------------------------------------------------------------------------
    Overlay the overlay image in the array 'overlayImage', described by
    *overlayPamP, onto the underlying image from the input image file
@@ -546,17 +825,17 @@ composite(int          const originleft,
 -----------------------------------------------------------------------------*/
     enum sampleScale const sampleScale = 
         assumeLinear ? INTENSITY_SAMPLE : GAMMA_SAMPLE;
+    enum alphaMix const alphaMix =
+        mixTransparency ? AM_OVERLAY : AM_KEEPUNDER;
 
     int underlayRow;  /* NB may be negative */
     int overlayRow;   /* NB may be negative */
     tuple * composedTuplerow;
     tuple * underlayTuplerow;
+    struct pam adaptUnderlayPam;
     tuple * overlayTuplerow;
+    struct pam adaptOverlayPam;
     tuplen * alphaTuplerown;
-    bool overlayHasOpacity;
-    unsigned int opacityPlane;
-
-    pnm_getopacity(overlayPamP, &overlayHasOpacity, &opacityPlane);
 
     composedTuplerow = pnm_allocpamrow(composedPamP);
     underlayTuplerow = pnm_allocpamrow(underlayPamP);
@@ -566,6 +845,9 @@ composite(int          const originleft,
     else
         alphaTuplerown = NULL;
 
+    determineInputAdaptations(underlayPamP, overlayPamP, composedPamP,
+                              &adaptUnderlayPam, &adaptOverlayPam);
+
     pnm_writepaminit(composedPamP);
 
     assert(INT_MAX - overlayPamP->height > origintop); /* arg constraint */
@@ -577,15 +859,13 @@ composite(int          const originleft,
 
         if (overlayRow >= 0 && overlayRow < overlayPamP->height) {
             pnm_readpamrow(overlayPamP, overlayTuplerow);
-            adaptRowToOutputFormat(overlayPamP, overlayTuplerow, composedPamP);
+            adaptRowFormat(overlayPamP, &adaptOverlayPam, overlayTuplerow);
             if (alphaPamP)
                 pnm_readpamrown(alphaPamP, alphaTuplerown);
         }
         if (underlayRow >= 0 && underlayRow < underlayPamP->height) {
             pnm_readpamrow(underlayPamP, underlayTuplerow);
-            adaptRowToOutputFormat(underlayPamP, underlayTuplerow, 
-                                   composedPamP);
-
+            adaptRowFormat(underlayPamP, &adaptUnderlayPam, underlayTuplerow);
             if (underlayRow < origintop || 
                 underlayRow >= origintop + overlayPamP->height) {
             
@@ -593,9 +873,9 @@ composite(int          const originleft,
 
                 pnm_writepamrow(composedPamP, underlayTuplerow);
             } else {
-                composeRow(originleft, underlayPamP, overlayPamP,
-                           invertAlpha, masterOpacity, overlayHasOpacity,
-                           opacityPlane, composedPamP, sampleScale,
+                composeRow(originleft, &adaptUnderlayPam, &adaptOverlayPam,
+                           invertAlpha, masterOpacity, 
+                           composedPamP, sampleScale, alphaMix,
                            underlayTuplerow, overlayTuplerow, alphaTuplerown,
                            composedTuplerow);
                 
@@ -612,6 +892,30 @@ composite(int          const originleft,
 
 
 
+static void
+initAlphaFile(struct cmdlineInfo const cmdline,
+              struct pam *       const overlayPamP,
+              FILE **            const filePP,
+              struct pam *       const pamP) {
+
+    FILE * fileP;
+    
+    if (cmdline.alphaFilespec) {
+        fileP = pm_openr(cmdline.alphaFilespec);
+        pamP->comment_p = NULL;
+        pnm_readpaminit(fileP, pamP, PAM_STRUCT_SIZE(opacity_plane));
+
+        if (overlayPamP->width != pamP->width || 
+            overlayPamP->height != pamP->height)
+            pm_error("Opacity map and overlay image are not the same size");
+    } else
+        fileP = NULL;
+
+    *filePP = fileP;
+}
+
+
+
 int
 main(int argc, const char *argv[]) {
 
@@ -630,34 +934,46 @@ main(int argc, const char *argv[]) {
     parseCommandLine(argc, argv, &cmdline);
 
     overlayFileP = pm_openr(cmdline.overlayFilespec);
+
+    overlayPam.comment_p = NULL;
     pnm_readpaminit(overlayFileP, &overlayPam, 
-                    PAM_STRUCT_SIZE(allocation_depth));
-    if (cmdline.alphaFilespec) {
-        alphaFileP = pm_openr(cmdline.alphaFilespec);
-        pnm_readpaminit(alphaFileP, &alphaPam, 
-                        PAM_STRUCT_SIZE(allocation_depth));
+                    PAM_STRUCT_SIZE(opacity_plane));
 
-        if (overlayPam.width != alphaPam.width || 
-            overlayPam.height != alphaPam.height)
-            pm_error("Opacity map and overlay image are not the same size");
-    } else
-        alphaFileP = NULL;
+    if (overlayPam.len < PAM_STRUCT_SIZE(opacity_plane))
+        pm_error("Libnetpbm is too old.  This program requires libnetpbm from "
+                 "Netpbm 10.56 (September 2011) or newer");
+
+    if (!overlayPam.visual)
+        pm_error("Overlay image has tuple type '%s', which is not a "
+                 "standard visual type.  We don't know how to compose.",
+                 overlayPam.tuple_type);
+
+    initAlphaFile(cmdline, &overlayPam, &alphaFileP, &alphaPam);
 
     underlayFileP = pm_openr(cmdline.underlyingFilespec);
 
+    underlayPam.comment_p = NULL;
     pnm_readpaminit(underlayFileP, &underlayPam, 
-                    PAM_STRUCT_SIZE(allocation_depth));
+                    PAM_STRUCT_SIZE(opacity_plane));
 
+    assert(underlayPam.len >= PAM_STRUCT_SIZE(opacity_plane));
+
+    if (!overlayPam.visual)
+        pm_error("Overlay image has tuple type '%s', which is not a "
+                 "standard visual type.  We don't know how to compose.",
+                 overlayPam.tuple_type);
+    
     computeOverlayPosition(underlayPam.width, underlayPam.height, 
                            overlayPam.width,  overlayPam.height,
                            cmdline, &originLeft, &originTop);
 
-    composedPam.size             = sizeof(composedPam);
+    composedPam.size             = PAM_STRUCT_SIZE(opacity_plane);
     composedPam.len              = PAM_STRUCT_SIZE(allocation_depth);
     composedPam.allocation_depth = 0;
     composedPam.file             = pm_openw(cmdline.outputFilespec);
+    composedPam.comment_p        = NULL;
 
-    determineOutputType(&composedPam, &underlayPam, &overlayPam);
+    determineOutputType(&underlayPam, &overlayPam, &composedPam);
 
     pnm_setminallocationdepth(&underlayPam, composedPam.depth);
     pnm_setminallocationdepth(&overlayPam,  composedPam.depth);
@@ -665,7 +981,7 @@ main(int argc, const char *argv[]) {
     composite(originLeft, originTop,
               &underlayPam, &overlayPam, alphaFileP ? &alphaPam : NULL,
               cmdline.alphaInvert, cmdline.opacity,
-              &composedPam, cmdline.linear);
+              &composedPam, cmdline.linear, cmdline.mixtransparency);
 
     if (alphaFileP)
         pm_close(alphaFileP);
@@ -678,3 +994,6 @@ main(int argc, const char *argv[]) {
     */
     return 0;
 }
+
+
+
diff --git a/editor/pamcut.c b/editor/pamcut.c
index 8d4c2240..7c41af38 100644
--- a/editor/pamcut.c
+++ b/editor/pamcut.c
@@ -24,11 +24,11 @@
        but we hope not.
        */
 
-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;  /* Filespec of input file */
+    const char * inputFileName;  /* File name of input file */
 
     /* The following describe the rectangle the user wants to cut out. 
        the value UNSPEC for any of them indicates that value was not
@@ -51,8 +51,8 @@ struct cmdlineInfo {
 
 
 static void
-parseCommandLine(int argc, char ** const argv,
-                 struct cmdlineInfo * const cmdlineP) {
+parseCommandLine(int argc, const char ** const argv,
+                 struct CmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
    Note that the file spec array we return is stored in the storage that
    was passed to us as the argv array.
@@ -87,7 +87,7 @@ parseCommandLine(int argc, char ** const argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = TRUE;  /* We may have parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (cmdlineP->width < 0)
@@ -103,10 +103,10 @@ parseCommandLine(int argc, char ** const argv,
 
     switch (argc-1) {
     case 0:
-        cmdlineP->inputFilespec = "-";
+        cmdlineP->inputFileName = "-";
         break;
     case 1:
-        cmdlineP->inputFilespec = argv[1];
+        cmdlineP->inputFileName = argv[1];
         break;
     case 4:
     case 5: {
@@ -137,9 +137,9 @@ parseCommandLine(int argc, char ** const argv,
         }
 
         if (argc-1 == 4)
-            cmdlineP->inputFilespec = "-";
+            cmdlineP->inputFileName = "-";
         else
-            cmdlineP->inputFilespec = argv[5];
+            cmdlineP->inputFileName = argv[5];
         break;
     }
     }
@@ -628,7 +628,7 @@ extractRowsPBM(const struct pam * const inpamP,
 
 static void
 cutOneImage(FILE *             const ifP,
-            struct cmdlineInfo const cmdline,
+            struct CmdlineInfo const cmdline,
             FILE *             const ofP) {
 
     int leftcol, rightcol, toprow, bottomrow;
@@ -669,19 +669,19 @@ cutOneImage(FILE *             const ifP,
 
 
 int
-main(int argc, char *argv[]) {
+main(int argc, const char *argv[]) {
 
     FILE * const ofP = stdout;
 
-    struct cmdlineInfo cmdline;
-    FILE* ifP;
-    bool eof;
+    struct CmdlineInfo cmdline;
+    FILE * ifP;
+    int eof;
 
-    pnm_init(&argc, argv);
+    pm_proginit(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
 
-    ifP = pm_openr(cmdline.inputFilespec);
+    ifP = pm_openr(cmdline.inputFileName);
 
     eof = FALSE;
     while (!eof) {
diff --git a/editor/pamcut.test b/editor/pamcut.test
deleted file mode 100644
index be70f1fd..00000000
--- a/editor/pamcut.test
+++ /dev/null
@@ -1,10 +0,0 @@
-echo Test 1.  Should print 2958909756 124815
-./pamcut -top 0 -left 0 -width 260 -height 160 -pad ../testimg.ppm | cksum
-echo Test 2.  Should print 1550940962 10933
-./pamcut -top 200 -left 120 -width 40 -height 40 -pad ../testimg.ppm | cksum
-echo Test 3.  Should print 708474423 14
-./pamcut -top 5 -left 5 -bottom 5 -right 5 ../testimg.ppm | cksum
-echo Test 3.  Should print 3412257956 129
-pbmmake -g 50 50 | ./pamcut 5 5 30 30 | cksum
-
-
diff --git a/editor/pamdice.c b/editor/pamdice.c
index f72420ab..2c53a110 100644
--- a/editor/pamdice.c
+++ b/editor/pamdice.c
@@ -53,7 +53,7 @@ parseCommandLine(int argc, char ** argv,
    was passed to us as the argv array.  We also trash *argv.
 -----------------------------------------------------------------------------*/
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
     
@@ -80,7 +80,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0 );
+    pm_optParseOptions3( &argc, argv, opt, sizeof(opt), 0 );
         /* Uses and sets argc, argv, and some of *cmdline_p and others. */
 
     if (cmdlineP->sliceVertically) {
@@ -223,9 +223,9 @@ computeOutputFilenameFormat(int           const format,
     default:       filenameSuffix = "";    break;
     }
     
-    asprintfN(filenameFormatP, "%s_%%0%uu_%%0%uu.%s",
-              outstem, ndigits(nHorizSlice), ndigits(nVertSlice),
-              filenameSuffix);
+    pm_asprintf(filenameFormatP, "%s_%%0%uu_%%0%uu.%s",
+                outstem, ndigits(nHorizSlice), ndigits(nVertSlice),
+                filenameSuffix);
 
     if (*filenameFormatP == NULL)
         pm_error("Unable to allocate memory for filename format string");
@@ -258,7 +258,7 @@ openOutStreams(struct pam   const inpam,
     for (vertSlice = 0; vertSlice < nVertSlice; ++vertSlice) {
         const char * filename;
 
-        asprintfN(&filename, filenameFormat, horizSlice, vertSlice);
+        pm_asprintf(&filename, filenameFormat, horizSlice, vertSlice);
 
         if (filename == NULL)
             pm_error("Unable to allocate memory for output filename");
@@ -273,10 +273,10 @@ openOutStreams(struct pam   const inpam,
             
             pnm_writepaminit(&outpam[vertSlice]);
 
-            strfree(filename);
+            pm_strfree(filename);
         }
     }        
-    strfree(filenameFormat);
+    pm_strfree(filenameFormat);
 }
 
 
diff --git a/editor/pamdither.c b/editor/pamdither.c
deleted file mode 100644
index e084892c..00000000
--- a/editor/pamdither.c
+++ /dev/null
@@ -1,320 +0,0 @@
-/*=============================================================================
-                                 pamdither
-===============================================================================
-  By Bryan Henderson, July 2006.
-
-  Contributed to the public domain.
-
-  This is meant to replace Ppmdither by Christos Zoulas, 1991.
-=============================================================================*/
-
-#include "pm_c_util.h"
-#include "mallocvar.h"
-#include "shhopt.h"
-#include "pam.h"
-
-/* Besides having to have enough memory available, the limiting factor
-   in the dithering matrix power is the size of the dithering value.
-   We need 2*dith_power bits in an unsigned int.  We also reserve
-   one bit to give headroom to do calculations with these numbers.
-*/
-#define MAX_DITH_POWER ((sizeof(unsigned int)*8 - 1) / 2)
-
-
-/* COLOR():
- *	returns the index in the colormap for the
- *      r, g, b values specified.
- */
-#define COLOR(r,g,b) (((r) * dith_ng + (g)) * dith_nb + (b))
-
-struct cmdlineInfo {
-    /* All the information the user supplied in the command line,
-       in a form easy for the program to use.
-    */
-    const char * inputFileName;  /* File name of input file */
-    const char * mapFileName;    /* File name of colormap file */
-    unsigned int dim;
-    unsigned int verbose;
-};
-
-
-
-static void
-parseCommandLine (int argc, char ** argv,
-                  struct cmdlineInfo *cmdlineP) {
-/*----------------------------------------------------------------------------
-   parse program command line described in Unix standard form by argc
-   and argv.  Return the information in the options as *cmdlineP.  
-
-   If command line is internally inconsistent (invalid options, etc.),
-   issue error message to stderr and abort program.
-
-   Note that the strings we return are stored in the storage that
-   was passed to us as the argv array.  We also trash *argv.
------------------------------------------------------------------------------*/
-    optEntry * option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
-         */
-    optStruct3 opt;
-
-    unsigned int option_def_index;
-
-    unsigned int dimSpec, mapfileSpec;
-
-    MALLOCARRAY_NOFAIL(option_def, 100);
-    
-    option_def_index = 0;   /* incremented by OPTENT3 */
-    OPTENT3(0,   "dim",      OPT_UINT, 
-            &cmdlineP->dim,    &dimSpec, 0);
-    OPTENT3(0,   "mapfile",      OPT_STRING, 
-            &cmdlineP->mapFilespec,    &mapfileSpec, 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 */
-    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
-
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0 );
-        /* Uses and sets argc, argv, and some of *cmdline_p and others. */
-
-    if (!dimSpec)
-        cmdlineP->dim = 4;
-
-    if (cmdlineP->dim > MAX_DITH_POWER)
-        pm_error("Dithering matrix power %u (-dim) is too large.  "
-                 "Must be <= %d",
-                 dithPower, MAX_DITH_POWER);
-        
-    if (!mapfileSpec)
-        pm_error("You must specify the -mapfile option.");
-
-    if (argc-1 > 1)
-        pm_error("Program takes at most one argument: the input file "
-                 "specification.  "
-                 "You specified %d arguments.", argc-1);
-    if (argc-1 < 1)
-        cmdlineP->inputFilespec = "-";
-    else
-        cmdlineP->inputFilespec = argv[1];
-}
-
-
-
-static unsigned int
-dither(sample       const p,
-       sample       const maxval,
-       unsigned int const d,
-       unsigned int const ditheredMaxval,
-       unsigned int const ditherMatrixArea) {
-/*----------------------------------------------------------------------------
-  Return the dithered brightness for a component of a pixel whose real 
-  brightness for that component is 'p' based on a maxval of 'maxval'.
-  The returned brightness is based on a maxval of ditheredMaxval.
-
-  'd' is the entry in the dithering matrix for the position of this pixel
-  within the dithered square.
-
-  'ditherMatrixArea' is the area (number of pixels in) the dithered square.
------------------------------------------------------------------------------*/
-    unsigned int const ditherSquareMaxval = ditheredMaxval * ditherMatrixArea;
-        /* This is the maxval for an intensity that an entire dithered
-           square can represent.
-        */
-    pixval const pScaled = ditherSquareMaxval * p / maxval;
-        /* This is the input intensity P expressed with a maxval of
-           'ditherSquareMaxval'
-        */
-    
-    /* Now we scale the intensity back down to the 'ditheredMaxval', and
-       as that will involve rounding, we round up or down based on the position
-       in the dithered square, as determined by 'd'
-    */
-
-    return (pScaled + d) / ditherMatrixArea;
-}
-
-
-
-static unsigned int
-dithValue(unsigned int const y,
-          unsigned int const x,
-          unsigned int const dithPower) { 
-/*----------------------------------------------------------------------------
-  Return the value of a dither matrix which is 2 ** dithPower elements
-  square at Row x, Column y.
-  [graphics gems, p. 714]
------------------------------------------------------------------------------*/
-    unsigned int d;
-        /*
-          Think of d as the density. At every iteration, d is shifted
-          left one and a new bit is put in the low bit based on x and y.
-          If x is odd and y is even, or visa versa, then a bit is shifted in.
-          This generates the checkerboard pattern seen in dithering.
-          This quantity is shifted again and the low bit of y is added in.
-          This whole thing interleaves a checkerboard pattern and y's bits
-          which is what you want.
-        */
-    unsigned int i;
-
-    for (i = 0, d = 0; i < dithPower; i++, x >>= 1, y >>= 1)
-        d = (d << 2) | (((x & 1) ^ (y & 1)) << 1) | (y & 1);
-
-    return(d);
-}
-
-
-
-static unsigned int **
-dithMatrix(unsigned int const dithPower) {
-/*----------------------------------------------------------------------------
-   Create the dithering matrix for dimension 'dithDim'.
-
-   Return it in newly malloc'ed storage.
-
-   Note that we assume 'dith_dim' is small enough that the dith_mat_sz
-   computed within fits in an int.  Otherwise, results are undefined.
------------------------------------------------------------------------------*/
-    unsigned int const dithDim = 1 << dithPower;
-
-    unsigned int ** dithMat;
-
-    assert(dithPower < sizeof(unsigned int) * 8);
-
-    {
-        unsigned int const dithMatSize = 
-            (dithDim * sizeof(*dithMat)) + /* pointers */
-            (dithDim * dithDim * sizeof(**dithMat)); /* data */
-        
-        dithMat = malloc(dithMatSize);
-        
-        if (dithMat == NULL) 
-            pm_error("Out of memory.  "
-                     "Cannot allocate %d bytes for dithering matrix.",
-                     dithMatSize);
-    }
-    {
-        unsigned int * const rowStorage = (unsigned int *)&dithMat[dithDim];
-        unsigned int y;
-        for (y = 0; y < dithDim; ++y)
-            dithMat[y] = &rowStorage[y * dithDim];
-    }
-    {
-        unsigned int y;
-        for (y = 0; y < dithDim; ++y) {
-            unsigned int x;
-            for (x = 0; x < dithDim; ++x)
-                dithMat[y][x] = dithValue(y, x, dithPower);
-        }
-    }
-    return dithMat;
-}
-
-    
-
-static void
-ditherImage(struct pam   const inpam,
-            tuple *      const colormap, 
-            unsigned int const dithPower,
-            struct pam   const outpam;
-            tuple **     const inTuples,
-            tuple ***    const outTuplesP) {
-
-    unsigned int const dithDim = 1 << dithPower;
-    unsigned int const ditherMatrixArea = SQR(dithDim);
-
-    unsigned int const modMask = (dithDim - 1);
-       /* And this into N to compute N % dithDim cheaply, since we
-          know (though the compiler doesn't) that dithDim is a power of 2
-       */
-    unsigned int ** const ditherMatrix = dithMatrix(dithPower);
-
-    tuple ** ouputTuples;
-    unsigned int row; 
-
-    assert(dithPower < sizeof(unsigned int) * 8);
-    assert(UINT_MAX / dithDim >= dithDim);
-
-    outTuples = ppm_allocpamarray(outpam);
-
-    for (row = 0; row < inpam.height; ++row) {
-        unsigned int col;
-        for (col = 0; col < inpam.width; ++col) {
-            unsigned int const d =
-                ditherMatrix[row & modMask][(width-col-1) & modMask];
-            tuple const inputTuple = inTuples[row][col];
-            unsigned int dithered[3];
-
-            unsigned int plane;
-
-            assert(inpam.depth >= 3);
-
-            for (plane = 0; plane < 3; ++plane)
-                dithered[plane] =
-                    dither(inputTuple[plane], inpam.maxval, d, outpam.maxval,
-                           ditherMatrixArea);
-
-            outTuples[row][col] = 
-                colormap[COLOR(dithered[RED_PLANE],
-                               dithered[GRN_PLANE],
-                               dithered[BLU_PLANE])];
-        }
-    }
-    free(ditherMatrix);
-    *outTuplesP = outTuples;
-}
-
-
-
-static void
-getColormap(const char * const mapFileName,
-            tuple **     const colormapP) {
-
-    TODO("write this");
-
-}
-
-
-
-int
-main(int argc,
-     char ** argv) {
-
-    struct cmdlineInfo cmdline;
-    FILE * ifP;
-    tuple ** inTuples;        /* Input image */
-    tuple ** outTuples;        /* Output image */
-    tuple * colormap;
-    int cols, rows;
-    pixval maxval;  /* Maxval of the input image */
-
-    struct pam outpamCommon;
-        /* Describes the output images.  Width and height fields are
-           not meaningful, because different output images might have
-           different dimensions.  The rest of the information is common
-           across all output images.
-        */
-
-    pnm_init(&argc, argv);
-
-    parseCommandLine(&argc, &argv);
-
-    pm_openr(cmdline.inputFileName);
-
-    inTuples = pnm_readpam(ifP, &inpam, PAM_STRUCT_SIZE(allocation_depth));
-    pm_close(ifP);
-
-    getColormap(cmdline.mapFileName, &colormap);
-
-    ditherImage(inpam, colormap, dithPower, inTuples, &outTuples);
-
-    ppm_writeppm(stdout, opixels, cols, rows, outputMaxval, 0);
-    pm_close(stdout);
-
-    free(colormap);
-
-    pnm_freepamarray(inTuples, &inpam);
-    pnm_freepamarray(outTuples, &outpam);
-
-    return 0;
-}
diff --git a/editor/pamditherbw.c b/editor/pamditherbw.c
index 90e2abd5..36eb7d9e 100644
--- a/editor/pamditherbw.c
+++ b/editor/pamditherbw.c
@@ -41,6 +41,8 @@ struct cmdlineInfo {
     unsigned int  clusterRadius;  
         /* Defined only for halftone == QT_CLUSTER */
     float         threshval;
+    unsigned int  randomseed;
+    unsigned int  randomseedSpec;
 };
 
 
@@ -54,7 +56,7 @@ parseCommandLine(int argc, char ** argv,
    was passed to us as the argv array.
 -----------------------------------------------------------------------------*/
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -83,12 +85,14 @@ parseCommandLine(int argc, char ** argv,
             &valueSpec, 0);
     OPTENT3(0, "clump",     OPT_UINT,  &cmdlineP->clumpSize, 
             &clumpSpec, 0);
+    OPTENT3(0,   "randomseed",   OPT_UINT,    &cmdlineP->randomseed,
+            &cmdlineP->randomseedSpec,      0);
 
     opt.opt_table = option_def;
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We may have parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (floydOpt + atkinsonOpt + thresholdOpt + hilbertOpt + dither8Opt + 
@@ -518,7 +522,6 @@ createFsConverter(struct pam * const graypamP,
     /* Initialize Floyd-Steinberg error vectors. */
     MALLOCARRAY_NOFAIL(stateP->thiserr, graypamP->width + 2);
     MALLOCARRAY_NOFAIL(stateP->nexterr, graypamP->width + 2);
-    srand(pm_randseed());
 
     {
         /* (random errors in [-1/8 .. 1/8]) */
@@ -661,8 +664,6 @@ createAtkinsonConverter(struct pam * const graypamP,
     for (relRow = 0; relRow < 3; ++relRow)
         MALLOCARRAY_NOFAIL(stateP->error[relRow], graypamP->width + 2);
 
-    srand(pm_randseed());
-
     {
         /* (random errors in [-1/8 .. 1/8]) */
         unsigned int col;
@@ -857,6 +858,8 @@ main(int argc, char *argv[]) {
 
     parseCommandLine(argc, argv, &cmdline);
 
+    srand(cmdline.randomseedSpec ? cmdline.randomseed : pm_randseed());
+
     ifP = pm_openr(cmdline.inputFilespec);
 
     if (cmdline.halftone == QT_HILBERT)
diff --git a/editor/pamedge.c b/editor/pamedge.c
index ac94e2ae..f4384535 100644
--- a/editor/pamedge.c
+++ b/editor/pamedge.c
@@ -156,11 +156,12 @@ writeMiddleRows(struct pam * const inpamP,
 
 
 int
-main(int argc, char *argv[]) {
+main(int argc, const char ** argv) {
+
     FILE *ifP;
     struct pam inpam, outpam;
 
-    pnm_init( &argc, argv );
+    pm_proginit(&argc, argv);
 
     if (argc-1 == 1) 
         ifP = pm_openr(argv[1]);
diff --git a/editor/pamenlarge.c b/editor/pamenlarge.c
index b3039424..187bfb6e 100644
--- a/editor/pamenlarge.c
+++ b/editor/pamenlarge.c
@@ -115,6 +115,8 @@ enlargePbmRowHorizontally(struct pam *          const inpamP,
     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 };
 
@@ -129,33 +131,127 @@ enlargePbmRowHorizontally(struct pam *          const inpamP,
     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];
+            outrow[colChar*2]   = dbl[(inrow[colChar] & 0xF0) >> 4];
+            outrow[colChar*2+1] = dbl[(inrow[colChar] & 0x0F) >> 0];
             /* Possible outrow overrun by one byte. */
         }
-        break;   
+        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] & 0x07];
+            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];
         }
         break;  
+
+    case 4:
+        for (colChar = 0; colChar < inColChars; ++colChar) {
+            unsigned int i;
+            for (i = 0; i < 4; ++i) 
+                outrow[colChar*4+i]=
+                    quad[(inrow[colChar] >> (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 ]; 
-            outrow[colChar*5+2]= pair[ (inrow[colChar] >>3) & 0x03 ] >> 4; 
-            outrow[colChar*5+3]= quin4[(inrow[colChar] >>1) & 0x07 ];
-            outrow[colChar*5+4]= pair[ inrow[colChar] & 0x03 ] >>3; 
+            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; 
+        }
+        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];
+
+            outrow[colChar*6]   = trp1[(hi & 0xF0) >> 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+4] = trp2[(lo >> 2) & 0x0F];
+            outrow[colChar*6+5] = trp3[lo & 0x07];
+        }
+        break;  
+
+    case 7:
+        for (colChar = 0; colChar < inColChars; ++colChar) { 
+            uint32_t hi, lo;
+
+            hi = inrow[colChar] >> 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 = ((((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+5] =  (unsigned char) ((lo >>  8) & 0xFF);
+            outrow[colChar*7+6] =  (unsigned char) ((lo >>  0) & 0xFF);
         }
         break;
-    case 4: default:
-        /*  Unlike the above cases, we iterate through outrow.  The color
-            composition of each outrow byte is computed by consulting
-            a single bit or two consecutive bits in inrow. 
+
+    case 8:
+        for (colChar = 0; colChar < inColChars; ++colChar) { 
+            unsigned int i;
+            for (i = 0; i < 8; ++i) {
+                outrow[colChar*8+i] =
+                    ((inrow[colChar] >> (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;
+        }
+        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;
+        }
+        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.
+
             Color changes never happen twice in a single outrow byte.
+
+            This is a generalization of above routines for scale factors
+            9 and 10.
+
+            Logic works for scale factors 4, 6, 7, 8, and above (but not 5).
         */
+
         for (colChar = 0; colChar < outColChars; ++colChar) {
             unsigned int const mult = scaleFactor;
             unsigned int const mod = colChar % mult;
@@ -199,12 +295,14 @@ enlargePbm(struct pam * const inpamP,
     
     if (scaleFactor == 1)
         outrow = inrow;
-    else  
-        outrow = pbm_allocrow_packed(outcols + 32);
-            /* The 32 (=4 bytes) is to allow writes beyond outrow data
-               end when scaleFactor is 2, 3, 5.  (max 4 bytes when
-               scaleFactor is 5)
-            */
+    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.
+        */
+        unsigned int const rightPadding = 
+            scaleFactor > 10 ? 0 : (scaleFactor - 1) * 8;  
+        outrow = pbm_allocrow_packed(outcols + rightPadding);
+    }
 
     pbm_writepbminit(ofP, outcols, outrows, 0);
     
@@ -214,10 +312,8 @@ enlargePbm(struct pam * const inpamP,
         pbm_readpbmrow_packed(inpamP->file, inrow, inpamP->width,
                               inpamP->format);
 
-        if (inpamP->width % 8 > 0) {  /* clean final partial byte */ 
-            inrow[inColChars-1] >>= 8 - inpamP->width % 8;
-            inrow[inColChars-1] <<= 8 - inpamP->width % 8;
-        }
+        if (outcols % 8 > 0)           /* clean final partial byte */ 
+            pbm_cleanrowend_packed(inrow, inpamP->width);
 
         enlargePbmRowHorizontally(inpamP, inrow, inColChars, outColChars,
                                   scaleFactor, outrow);
diff --git a/editor/pamenlarge.test b/editor/pamenlarge.test
deleted file mode 100644
index 7584af01..00000000
--- a/editor/pamenlarge.test
+++ /dev/null
@@ -1,8 +0,0 @@
-echo Test 1.  Should print 3424505894 913236
-./pamenlarge 3 ../testimg.ppm | cksum
-echo Test 2.  Should print 4152147096 304422
-ppmtopgm ../testimg.ppm | ./pamenlarge 3 | cksum
-echo Test 3.  Should print 3342398172 297
-./pamenlarge 3 ../testgrid.pbm | cksum
-echo Test 4.  Should print 237488670 3133413
-./pamenlarge 3 -plain ../testimg.ppm | cksum
diff --git a/editor/pamflip.test b/editor/pamflip.test
deleted file mode 100644
index 96e889ea..00000000
--- a/editor/pamflip.test
+++ /dev/null
@@ -1,12 +0,0 @@
-echo Test 1.  Should print 2116496681 101484
-./pamflip -lr ../testimg.ppm | cksum 
-echo Test 2.  Should print 217037000 101484
-./pamflip -cw ../testimg.ppm | cksum
-echo Test 3.  Should print 2052917888 101484
-./pamflip -tb ../testimg.ppm | cksum
-echo Test 4.  Should print 3375384165 41
-./pamflip -lr ../testgrid.pbm | cksum
-echo Test 5.  Should print 604323149 41
-./pamflip -tb ../testgrid.pbm | cksum
-echo Test 6.  Should print 490797850 37
-./pamflip -cw ../testgrid.pbm | cksum
diff --git a/editor/pamflip/Makefile b/editor/pamflip/Makefile
new file mode 100644
index 00000000..ce3345f1
--- /dev/null
+++ b/editor/pamflip/Makefile
@@ -0,0 +1,36 @@
+ifeq ($(SRCDIR)x,x)
+  SRCDIR = $(CURDIR)/../..
+  BUILDDIR = $(SRCDIR)
+endif
+SUBDIR = editor/pamflip
+VPATH=.:$(SRCDIR)/$(SUBDIR)
+
+default: all
+
+include $(BUILDDIR)/config.mk
+
+SUBDIRS =
+
+MERGEBINARIES = pamflip
+
+PORTBINARIES = pamflip
+
+BINARIES = $(PORTBINARIES)
+
+SCRIPTS =
+
+ADDL_OBJECTS = pamflip_sse.o
+
+OBJECTS = pamflip.o $(ADDL_OBJECTS)
+
+MERGE_OBJECTS = $(OBJECTS:%.o=%.o2)
+
+include $(SRCDIR)/common.mk
+
+.PHONY: all
+all: $(BINARIES) $(SUBDIRS:%=%/all)
+
+pamflip_sse.o pamflip_sse.o2: CFLAGS_TARGET = $(CFLAGS_SSE)
+pamflip.o pamflip.o2: CFLAGS_TARGET = $(CFLAGS_SSE)
+
+pamflip:%:%.o $(ADDL_OBJECTS)
diff --git a/editor/pamflip/config.h b/editor/pamflip/config.h
new file mode 100644
index 00000000..42aefb6e
--- /dev/null
+++ b/editor/pamflip/config.h
@@ -0,0 +1,7 @@
+#ifndef SSE_PBM_XY_FLIP
+  #if WANT_SSE && HAVE_WORKING_SSE2
+    #define SSE_PBM_XY_FLIP 1
+  #else
+    #define SSE_PBM_XY_FLIP 0
+  #endif
+#endif
diff --git a/editor/pamflip/flip.h b/editor/pamflip/flip.h
new file mode 100644
index 00000000..612a7f84
--- /dev/null
+++ b/editor/pamflip/flip.h
@@ -0,0 +1,16 @@
+#ifndef FLIP_H_INCLUDED
+#define FLIP_H_INCLUDED
+
+struct xformCore {
+    /* a b
+       c d
+    */
+    int a;  /* -1, 0, or 1 */
+    int b;  /* -1, 0, or 1 */
+    int c;  /* -1, 0, or 1 */
+    int d;  /* -1, 0, or 1 */
+};
+
+
+
+#endif
diff --git a/editor/pamflip.c b/editor/pamflip/pamflip.c
index 1c479e54..149ab310 100644
--- a/editor/pamflip.c
+++ b/editor/pamflip/pamflip.c
@@ -12,7 +12,7 @@
 
 /*
    transformNonPbmChunk() is the general transformation function.
-   It can tranform anything, albeit slowly and expensively.
+   It can transform anything, albeit slowly and expensively.
    
    The following are enhancements for specific cases:
    
@@ -26,6 +26,10 @@
      transformRowsBottomTopNonPbm()
        non-PBM image with bottom-top transformation
        (also works for PBM, but we don't use it)
+     transformRowsToColumnsPbmSse()
+       PBM image with column-for-row transformation
+       requires Intel/AMD x86 SSE2
+       (can only do 90 degree/xy flips)
      transformPbm()
        PBM image with column-for-row transformation
        (also works for all other transformations, but we don't use it)
@@ -68,6 +72,10 @@
 #include "nstring.h"
 #include "bitreverse.h"
 
+#include "config.h"  /* Defines SSE_PBM_XY_FLIP */
+#include "flip.h"
+#include "pamflip_sse.h"
+
 enum xformType {LEFTRIGHT, TOPBOTTOM, TRANSPOSE};
 
 static void
@@ -92,7 +100,7 @@ parseXformOpt(const char *     const xformOpt,
     xformCount = 0; /* initial value */
     while (!eol && xformCount < 10) {
         const char * token;
-        token = strsepN(&cursor, ",");
+        token = pm_strsep(&cursor, ",");
         if (token) {
             if (streq(token, "leftright"))
                 xformList[xformCount++] = LEFTRIGHT;
@@ -115,25 +123,13 @@ parseXformOpt(const char *     const xformOpt,
 
 
 
-/* See transformPoint() for an explanation of the transform matrix types.
-   The difference between the two types is that 'xformCore' is particular
-   to the source image dimensions and can be used to do the transformation,
-   while 'xformCore' is independent of the source image and just
-   tells what kind of transformation.
+/* See transformPoint() for an explanation of the transform matrix types.  The
+   difference between xformCore and xformMatrix is that 'xformCore' is
+   particular to the source image dimensions and can be used to do the
+   transformation, while 'xformCore' is independent of the source image and
+   just tells what kind of transformation.
 */
 
-struct xformCore {
-    /* a b
-       c d
-    */
-    int a;  /* -1, 0, or 1 */
-    int b;  /* -1, 0, or 1 */
-    int c;  /* -1, 0, or 1 */
-    int d;  /* -1, 0, or 1 */
-};
-
-
-
 struct xformMatrix {
     /* a b 0
        c d 0
@@ -276,7 +272,7 @@ interpretMemorySize(unsigned int const memsizeSpec,
         if (memsizeOpt > sizeMax / Meg)
             pm_error("-memsize value too large: %u MiB.  Maximum this program "
                      "can handle is %u MiB", 
-                     memsizeOpt, sizeMax / Meg);
+                     memsizeOpt, (unsigned)sizeMax / Meg);
         *availableMemoryP = memsizeOpt * Meg;
     } else
         *availableMemoryP = sizeMax;
@@ -337,7 +333,7 @@ parseCommandLine(int argc, char ** const argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We don't parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (lr + tb + xy + r90 + r180 + r270 + null > 1)
@@ -387,6 +383,8 @@ parseCommandLine(int argc, char ** const argv,
                  "specified %d", argc-1);
     else
         cmdlineP->inputFilespec = argv[1];
+
+    free(option_def);
 }
 
 
@@ -397,6 +395,9 @@ bitOrderReverse(unsigned char * const bitrow,
 /*----------------------------------------------------------------------------
   Reverse the bits in a packed pbm row (1 bit per pixel).  I.e. the leftmost
   bit becomes the rightmost, etc.
+
+  Exchange pixels in units of eight.  If both are zero, skip instead of
+  exchanging zeros.
 -----------------------------------------------------------------------------*/
     unsigned int const lastfullByteIdx = cols/8 - 1;
 
@@ -407,11 +408,14 @@ bitOrderReverse(unsigned char * const bitrow,
         bitrow[0] = bitreverse[bitrow[0]] << (8-cols);
     else if (cols % 8 == 0) {
         unsigned int i, j;
-        for (i = 0, j = lastfullByteIdx; i <= j; ++i, --j) {
-            unsigned char const t = bitreverse[bitrow[j]]; 
-            bitrow[j] = bitreverse[bitrow[i]];
-            bitrow[i] = t;
-        }
+        for (i = 0, j = lastfullByteIdx; i <= j; ++i, --j)
+            if ((bitrow[j] | bitrow[i]) == 0) {
+                /* Both are 0x00 - skip */
+            } else {
+                unsigned char const t = bitreverse[bitrow[j]]; 
+                bitrow[j] = bitreverse[bitrow[i]];
+                bitrow[i] = t;
+            }
     } else {
         unsigned int const m = cols % 8; 
 
@@ -422,18 +426,23 @@ bitOrderReverse(unsigned char * const bitrow,
         unsigned char th, tl;  /* 16 bit temp ( th << 8 | tl ) */
         tl = 0;
         for (i = 0, j = lastfullByteIdx+1; i <= lastfullByteIdx/2; ++i, --j) {
-            th = bitreverse[bitrow[i]];
-            bitrow[i] =
-                bitreverse[0xff & ((bitrow[j-1] << 8 | bitrow[j]) >> (8-m))];
-            bitrow[j] = 0xff & ((th << 8 | tl) >> m);
-            tl = th;
+            if( (tl | bitrow[i] | bitrow[j] | bitrow[j-1]) != 0) {
+                /* Skip if both are 0x00 */
+                th = bitreverse[bitrow[i]];
+                bitrow[i] =
+                    bitreverse[0xff & ((bitrow[j-1] << 8 
+                                        | bitrow[j]) >> (8-m))];
+                bitrow[j] = 0xff & ((th << 8 | tl) >> m);
+                tl = th;
+            }
         }
-        if (i == j) 
+        if (i == j && (bitrow[i] | tl) != 0) {
             /* bitrow[] has an odd number of bytes (an even number of
                full bytes; lastfullByteIdx is odd), so we did all but
                the center byte above.  We do the center byte now.
             */
             bitrow[j] = 0xff & ((bitreverse[bitrow[i]] << 8 | tl) >> m);
+        }
     }
 }
 
@@ -636,6 +645,10 @@ writeRaster(struct pam *    const pamP,
 
 
 
+
+
+
+
 static void
 transformPbmGen(struct pam *     const inpamP,
                 struct pam *     const outpamP,
@@ -656,28 +669,44 @@ transformPbmGen(struct pam *     const inpamP,
             
     computeXformMatrix(&xform, inpamP->width, inpamP->height, xformCore);
     
-    bitrow = pbm_allocrow(inpamP->width);
-    newbits = pbm_allocarray(pbm_packed_bytes(outpamP->width), 
-                             outpamP->height);
+    bitrow = pbm_allocrow_packed(inpamP->width);
+    newbits = pbm_allocarray_packed( outpamP->width, outpamP->height );
             
     /* Initialize entire array to zeroes.  One bits will be or'ed in later */
     for (row = 0; row < outpamP->height; ++row) {
         unsigned int col;
         for (col = 0; col < pbm_packed_bytes(outpamP->width); ++col) 
-             newbits[row][col] = 0; 
+            newbits[row][col] = 0; 
     }
     
     for (row = 0; row < inpamP->height; ++row) {
         unsigned int col;
-        pbm_readpbmrow(inpamP->file, bitrow, inpamP->width, inpamP->format);
-        for (col = 0; col < inpamP->width; ++col) {
-            unsigned int newcol, newrow;
-            transformPoint(col, row, xform, &newcol, &newrow);
-            newbits[newrow][newcol/8] |= bitrow[col] << (7 - newcol % 8);
-                /* Use of "|=" patterned after pbm_readpbmrow_packed. */
-         }
+
+        pbm_readpbmrow_packed(inpamP->file, bitrow,
+                              inpamP->width, inpamP->format);
+        for (col = 0; col < inpamP->width; ) {
+            if (bitrow[col/8] == 0x00) 
+                col += 8;  /* Blank.   Skip to next byte. */
+            else {      /* Examine each pixel. */
+                unsigned int const colLimit = MIN(col+8, inpamP->width);
+                unsigned int i;
+
+                for (i = 0; col < colLimit; ++i, ++col) {
+                    bool const bitIsOne = (bitrow[col/8] >> (7-i)) & 0x01;
+                    if (bitIsOne) {
+                        /* Write in only the one bits. */  
+                        unsigned int newcol, newrow;
+                        transformPoint(col, row, xform, &newcol, &newrow);
+                        newbits[newrow][newcol/8] |= 0x01 << (7 - newcol % 8);
+                            /* Use of "|=" patterned after
+                               pbm_readpbmrow_packed().
+                            */
+                    }
+                }
+            }
+        }
     }
-    
+
     for (row = 0; row < outpamP->height; ++row)
         pbm_writepbmrow_packed(outpamP->file, newbits[row], outpamP->width, 0);
     
@@ -745,7 +774,7 @@ typedef struct {
 /*----------------------------------------------------------------------------
    A description of the quilt of cells that make up the output image.
 -----------------------------------------------------------------------------*/
-    unsigned int ranks, files;
+    unsigned int rankCt, fileCt;
         /* Dimensions of the output image in cells */
     struct pam ** pam;
         /* pam[y][x] is the pam structure for the cell at rank y, file x
@@ -769,8 +798,12 @@ initOutCell(struct pam *     const outCellPamP,
             unsigned int     const inCellHeight,
             struct pam *     const inpamP,
             struct xformCore const xformCore) {
-
-    unsigned int outCellFiles, outCellRanks;
+/*----------------------------------------------------------------------------
+   Set up an output cell.  Create and open a temporary file to hold its
+   raster.  Figure out the dimensions of the cell.  Return a PAM structure
+   that describes the cell (including identifying that temporary file).
+-----------------------------------------------------------------------------*/
+    unsigned int outCellFileCt, outCellRankCt;
 
     *outCellPamP = *inpamP;  /* initial value */
 
@@ -779,10 +812,10 @@ initOutCell(struct pam *     const outCellPamP,
     outCellPamP->file = pm_tmpfile();
 
     xformDimensions(xformCore, inCellWidth, inCellHeight,
-                    &outCellFiles, &outCellRanks);
+                    &outCellFileCt, &outCellRankCt);
 
-    outCellPamP->width = outCellFiles;
-    outCellPamP->height = outCellRanks;
+    outCellPamP->width = outCellFileCt;
+    outCellPamP->height = outCellRankCt;
 }
 
 
@@ -792,9 +825,26 @@ createOutputMap(struct pam *       const inpamP,
                 unsigned int       const maxRows,
                 struct xformMatrix const cellXform,
                 struct xformCore   const xformCore) {
-
-    unsigned int const inCellFiles = 1;
-    unsigned int const inCellRanks = (inpamP->height + maxRows - 1) / maxRows;
+/*----------------------------------------------------------------------------
+   Create and return the output map.  That's a map of all the output cells
+   (from which the output image can be assembled, once those cells are filled
+   in).
+
+   The map contains the dimensions of each output cell as well as file
+   stream handles for the temporary files containing the pixels of each
+   cell.  We create and open those files.
+
+   Note that a complexity of determining cell dimensions (which we handle)
+   is that the input image isn't necessarily a whole multiple of the input
+   cell size, so the last cell may be short.
+
+   The map does not contain the mapping from input cells to output cells
+   (e.g. in a top-bottom transformation of a 10-cell image, input cell
+   0 maps to output cell 9); caller can compute that when needed from
+   'cellXform'.
+-----------------------------------------------------------------------------*/
+    unsigned int const inCellFileCt = 1;
+    unsigned int const inCellRankCt = (inpamP->height + maxRows - 1) / maxRows;
 
     outputMap * mapP;
     unsigned int outCellRank;
@@ -802,27 +852,27 @@ createOutputMap(struct pam *       const inpamP,
 
     MALLOCVAR_NOFAIL(mapP);
 
-    xformDimensions(xformCore, inCellFiles, inCellRanks,
-                    &mapP->files, &mapP->ranks);
+    xformDimensions(xformCore, inCellFileCt, inCellRankCt,
+                    &mapP->fileCt, &mapP->rankCt);
 
-    MALLOCARRAY(mapP->pam, mapP->ranks);
+    MALLOCARRAY(mapP->pam, mapP->rankCt);
     if (mapP->pam == NULL)
         pm_error("Could not allocate a cell array for %u ranks of cells.",
-                 mapP->ranks);
+                 mapP->rankCt);
 
-    for (outCellRank = 0; outCellRank < mapP->ranks; ++outCellRank) {
+    for (outCellRank = 0; outCellRank < mapP->rankCt; ++outCellRank) {
 
-        MALLOCARRAY(mapP->pam[outCellRank], mapP->files);
+        MALLOCARRAY(mapP->pam[outCellRank], mapP->fileCt);
 
         if (mapP->pam[outCellRank] == NULL)
             pm_error("Failed to allocate rank %u of the cell array, "
-                     "%u cells wide", outCellRank, mapP->files);
+                     "%u cells wide", outCellRank, mapP->fileCt);
     }
 
-    for (inCellRank = 0; inCellRank < inCellRanks; ++inCellRank) {
+    for (inCellRank = 0; inCellRank < inCellRankCt; ++inCellRank) {
         unsigned int const inCellFile = 0;
         unsigned int const inCellStartRow = inCellRank * maxRows;
-        unsigned int const inCellRows =
+        unsigned int const inCellRowCt =
             MIN(inpamP->height - inCellStartRow, maxRows);
 
         unsigned int outCellFile, outCellRank;
@@ -830,7 +880,7 @@ createOutputMap(struct pam *       const inpamP,
                        &outCellFile, &outCellRank);
     
         initOutCell(&mapP->pam[outCellRank][outCellFile],
-                    inpamP->width, inCellRows,
+                    inpamP->width, inCellRowCt,
                     inpamP, xformCore);
     }
     return mapP;
@@ -843,7 +893,7 @@ destroyOutputMap(outputMap * const mapP) {
 
     unsigned int outCellRank;
 
-    for (outCellRank = 0; outCellRank < mapP->ranks; ++outCellRank)
+    for (outCellRank = 0; outCellRank < mapP->rankCt; ++outCellRank)
         free(mapP->pam[outCellRank]);
 
     free(mapP->pam);
@@ -858,9 +908,9 @@ rewindCellFiles(outputMap * const outputMapP) {
 
     unsigned int outCellRank;
 
-    for (outCellRank = 0; outCellRank < outputMapP->ranks; ++outCellRank) {
+    for (outCellRank = 0; outCellRank < outputMapP->rankCt; ++outCellRank) {
         unsigned int outCellFile;
-        for (outCellFile = 0; outCellFile < outputMapP->files; ++outCellFile)
+        for (outCellFile = 0; outCellFile < outputMapP->fileCt; ++outCellFile)
             pm_seek(outputMapP->pam[outCellRank][outCellFile].file, 0);
     }
 }
@@ -872,9 +922,9 @@ closeCellFiles(outputMap * const outputMapP) {
 
     unsigned int outCellRank;
 
-    for (outCellRank = 0; outCellRank < outputMapP->ranks; ++outCellRank) {
+    for (outCellRank = 0; outCellRank < outputMapP->rankCt; ++outCellRank) {
         unsigned int outCellFile;
-        for (outCellFile = 0; outCellFile < outputMapP->files; ++outCellFile)
+        for (outCellFile = 0; outCellFile < outputMapP->fileCt; ++outCellFile)
             pm_close(outputMapP->pam[outCellRank][outCellFile].file);
     }
 }
@@ -932,7 +982,7 @@ stitchCellsToOutput(outputMap *  const outputMapP,
 
     tupleRow = pnm_allocpamrow(outpamP);
 
-    for (outRank = 0; outRank < outputMapP->ranks; ++outRank) {
+    for (outRank = 0; outRank < outputMapP->rankCt; ++outRank) {
         unsigned int const cellRows = outputMapP->pam[outRank][0].height;
             /* Number of rows in every cell in this rank */
 
@@ -944,7 +994,7 @@ stitchCellsToOutput(outputMap *  const outputMapP,
 
             outCol = 0;
 
-            for (outFile = 0; outFile < outputMapP->files; ++outFile) {
+            for (outFile = 0; outFile < outputMapP->fileCt; ++outFile) {
                 struct pam * const outCellPamP = 
                     &outputMapP->pam[outRank][outFile];
 
@@ -973,7 +1023,7 @@ transformNonPbmChunk(struct pam *     const inpamP,
                      unsigned int     const maxRows,
                      bool             const verbose) {
 /*----------------------------------------------------------------------------
-  Same as transformNonPbmChunk(), except we read 'maxRows' rows of the
+  Same as transformNonPbmWhole(), except we read 'maxRows' rows of the
   input into memory at a time, storing intermediate results in temporary
   files, to limit our use of virtual and real memory.
 
@@ -981,28 +1031,33 @@ transformNonPbmChunk(struct pam *     const inpamP,
   header).
 
   We call the strip of 'maxRows' rows that we read a source cell.  We
-  transform that cell according to 'xformCore' to create to create a
+  transform that cell according to 'xformCore' to create a
   target cell.  We store all the target cells in temporary files.
   We consider the target cells to be arranged in a column matrix the
   same as the source cells within the source image; we transform that
   matrix according to 'xformCore'.  The resulting cell matrix is the
   target image.
 -----------------------------------------------------------------------------*/
-    unsigned int const inCellFiles = 1;
-    unsigned int const inCellRanks = (inpamP->height + maxRows - 1) / maxRows;
+    /* The cells of the source image ("inCell") are in a 1-column matrix.
+       "rank" is the vertical position of a cell in that matrix; "file" is
+       the horizontal position (always 0, of course).
+    */
+    unsigned int const inCellFileCt = 1;
+    unsigned int const inCellRankCt = (inpamP->height + maxRows - 1) / maxRows;
 
     struct xformMatrix cellXform;
     unsigned int inCellRank;
     outputMap * outputMapP;
 
     if (verbose)
-        pm_message("Transforming in %u chunks, using temp files", inCellRanks);
+        pm_message("Transforming in %u chunks, using temp files",
+                   inCellRankCt);
 
-    computeXformMatrix(&cellXform, inCellFiles, inCellRanks, xformCore);
+    computeXformMatrix(&cellXform, inCellFileCt, inCellRankCt, xformCore);
 
     outputMapP = createOutputMap(inpamP, maxRows, cellXform, xformCore);
 
-    for (inCellRank = 0; inCellRank < inCellRanks; ++inCellRank) {
+    for (inCellRank = 0; inCellRank < inCellRankCt; ++inCellRank) {
         unsigned int const inCellFile = 0;
         unsigned int const inCellStartRow = inCellRank * maxRows;
         unsigned int const inCellRows =
@@ -1083,11 +1138,15 @@ transformPbm(struct pam *     const inpamP,
                through them only twice, so there is no page thrashing concern.
             */
             transformRowsBottomTopPbm(inpamP, outpamP, xform.a == -1);
-    } else
+    } else {
         /* This is a column-for-row type of transformation, which requires
            complex traversal of an in-memory image.
         */
-        transformPbmGen(inpamP, outpamP, xform);
+        if (SSE_PBM_XY_FLIP)
+            pamflip_transformRowsToColumnsPbmSse(inpamP, outpamP, xform);
+        else
+            transformPbmGen(inpamP, outpamP, xform);
+    }
 }
 
 
diff --git a/editor/pamflip/pamflip_sse.c b/editor/pamflip/pamflip_sse.c
new file mode 100644
index 00000000..e0929f65
--- /dev/null
+++ b/editor/pamflip/pamflip_sse.c
@@ -0,0 +1,429 @@
+/*=============================================================================
+                                   pamflip_sse.c
+===============================================================================
+  This is part of the Pamflip program.  It contains code that exploits
+  the SSE facility of some CPUs.
+
+  This code was originally written by Akira Urushibata ("Douso") in 2010 and is
+  contributed to the public domain by all authors.
+
+  The author makes the following request (which is not a reservation of legal
+  rights): Please study the code and make adjustments to meet specific needs.
+  This part is critical to performance.  I have seen code copied from
+  elsewhere poorly implemented.  A lot of work goes into the development of
+  free software.  It is sad to see derivative work which fails to reach its
+  potential.  Please put a comment in the code so people will know where it
+  came from.
+
+=============================================================================*/
+
+#include <assert.h>
+
+#include "pm_config.h"
+#include "pm_c_util.h"
+#include "mallocvar.h"
+#include "pam.h"
+
+#include "config.h"  /* Defines SSE_PBM_XY_FLIP */
+#include "flip.h"
+
+#include "pamflip_sse.h"
+
+/* Note that WANT_SSE implies the user expects SSE to be available
+   (i.e. <emmintrin.h> exists).
+*/
+
+#if SSE_PBM_XY_FLIP
+
+/*----------------------------------------------------------------------------
+   This is a specialized routine for row-for-column PBM transformations.
+   (-cw, -ccw, -xy).  It requires GCC (>= v. 4.2.0) and SSE2. 
+
+   In each cycle, we read sixteen rows from the input.  We process this band
+   left to right in blocks 8 pixels wide.  We use the SSE2 instruction
+   pmovmskb128, which reports the MSB of each byte in a 16 byte array, for
+   fast processing.  We place the 8x16 block into a 16 byte array, and
+   pmovmskb128 reports the 16 pixels on the left edge in one instruction
+   execution.  pslldi128 shifts the array contents leftward.
+
+   The following routines can write both in both directions (left and right)
+   into the output rows.  They do this by controlling the vertical stacking
+   order when they make the 8x16 blocks.
+
+   We do all transposition in 8x16 block units, adding padding to
+   the 8 row input buffer and the output plane raster as necessary.
+   doPartialBlockTop() or doPartialBlockBottom() handles the partial
+   input band.  This part can come from either the top or bottom of the
+   vertical input column, but always goes to the right end of the output
+   rows.
+
+   As an enhancement, we clear the output raster to zero (=white) in the
+   beginning and flip only the 8x16 blocks that contain non-zero bits (=any
+   amount of black pixels).  When we add padding to the edges, we initialize
+   it all to zero to prevent unnecessary transpositions.  Because most
+   real-world documents are largely white, this saves much execution time.  If
+   you are porting this code to an environment in which non-zero bits are the
+   majority, for example, BMP where zero means black, you should seriously
+   consider modifying this.
+
+   All instructions unique to GCC/SSE are in transpose16Bitrows().
+   It is possible to write a non-SSE version by providing a generic
+   version of transpose16Bitrows() or one tuned for a specific
+   architecture.  Use 8x8 blocks to avoid endian issues.
+ 
+   Further enhancement should be possible by employing wider bands,
+   larger blocks as wider SIMD registers become available.  Clearing
+   the white parts after instead of before transposition is also a
+   possibility.
+-----------------------------------------------------------------------------*/
+
+#include <emmintrin.h>
+
+typedef char v16qi __attribute__ ((vector_size (16)));
+typedef int  v4di  __attribute__ ((vector_size (16)));
+
+/* Beware when making modifications to code which involve SSE.
+   This is a sensitive part of GCC.  Different compiler versions
+   respond differently to trivial matters such as the difference
+   between above v16qi, v4di and a union defined for handling both.
+   What can be placed into a register is another issue.  Some
+   compilers issue warnings, others abort with error.
+
+   A char array cannot be loaded into v16qi by casting.  A vector
+   variable must be vector from the beginning.
+
+   Changes for your local system are okay, but if you intend to
+   publish them, please specify the compiler version you used.
+
+   This code has been tested on gcc versions 4.2.0, 4.2.4, 4.3.2,
+   4.4.3, 4.4.4, 4.5.0, 4.5.2, 4.6.0 and 4.6.1 clang versions
+   3.0, 3.2, 3.3.
+
+   We use SSE instructions in "_mm_" form in favor of "__builtin_".
+   In GCC the "__builtin_" form is documented but "_mm_" is not.
+   Former versions of this source file used "__builtin_".  This was
+   changed to make possible compilation with clang.
+
+   _mm_slli_epi32 : __builtin_ia32_pslldi128
+   _mm_cmpeq_epi8 : __builtin_ia32_pcmpeqb128
+   _mm_movemask_epi8 : __builtin_ia32_pmovmskb128
+
+   The conversion requires <emmintrin.h> .
+
+*/
+
+
+
+static void
+transpose16Bitrows(unsigned int const cols,
+                   unsigned int const rows,
+                   const bit *  const block[16],
+                   uint16_t **  const outplane,
+                   unsigned int const outcol16) {
+/*--------------------------------------------------------------------------
+  Convert input rows to output columns.  Works in units of 8x16.
+
+  Uses pre-calculated pointers ( block[i][col8] ) instead of
+  (xdir > 0) ? (i & 0x08) + 7 - (i & 0x07) : (24 - rows % 16 +i) % 16
+  for efficiency.
+
+  We load the block directly into a register.  (using a union like:
+
+       union V16 {
+          v16qi v;
+          unsigned char i[16];
+       };
+  )
+
+  gcc (v. 4.2, 4.4) sees the suffix [x] of v16.i[x] and apparently decides
+  that the variable has to be addressable and therefore needs to be placed
+  into memory.)
+---------------------------------------------------------------------------*/
+    unsigned int col;
+    register v16qi zero128;   /* 16 bytes of zero, in a SSE register */
+
+    zero128 = zero128 ^ zero128;
+
+    for (col = 0; col < cols; col += 8) {
+        unsigned int const col8 = col / 8;
+
+        register v16qi vReg = {
+            block[0][col8],  block[1][col8],
+            block[2][col8],  block[3][col8],  
+            block[4][col8],  block[5][col8],
+            block[6][col8],  block[7][col8],
+            block[8][col8],  block[9][col8],
+            block[10][col8], block[11][col8],
+            block[12][col8], block[13][col8],
+            block[14][col8], block[15][col8] };
+
+        register __m128i const compare =
+            _mm_cmpeq_epi8((__m128i)vReg, (__m128i)zero128);
+
+        if (_mm_movemask_epi8(compare) != 0xffff) {
+
+            /* There is some black content in this block; write to outplane */
+            
+            unsigned int outrow;
+            unsigned int i;
+
+            outrow = col;  /* initial value */
+
+            for (i = 0; i < 7; ++i) {
+                /* GCC (>=4.2) automatically unrolls this loop */  
+                outplane[outrow++][outcol16] =
+                    _mm_movemask_epi8((__m128i)vReg);
+                vReg = (v16qi)_mm_slli_epi32((__m128i)vReg, 1);
+            }
+            outplane[outrow][outcol16] = _mm_movemask_epi8((__m128i)vReg);
+        } else {
+            /* The block is completely white; skip. */
+        }
+    }
+}
+
+
+
+static void
+analyzeBlock(const struct pam * const inpamP,
+             bit **             const inrow,
+             int                const xdir,
+             const bit **       const block,
+             const bit **       const blockPartial,
+             unsigned int *     const topOfFullBlockP,
+             unsigned int *     const outcol16P) {
+/*--------------------------------------------------------------------------
+  Set up block[] pointer array.  Provide for both directions and the two
+  "twists" brought about by Intel byte ordering which occur when:
+    (1) 16 bytes are loaded to a SSE register
+    (2) 16 bits are written to memory.
+ 
+  If 'rows' is not a multiple of 8, a partial input band appears at one edge.
+  Set *topOfFullBlockP accordingly.  blockPartial[] is an adjusted "block" for
+  this partial band, brought up to a size of 8 rows.  The extra pointers point
+  to a single row which doPartialBlockTop() and doPartialBlockBottom() clear
+  to white.
+---------------------------------------------------------------------------*/
+    if (xdir > 0){
+        /* Write output columns left to right */
+        unsigned int i;
+        for (i = 0; i < 16; ++i){
+            unsigned int const index = (i & 0x8) + 7 - (i & 0x7);
+            /* Absorb little-endian "twists" */
+            block[i] = inrow[index];
+            blockPartial[i] = index < inpamP->height%16 ? block[i] : inrow[15];
+        }
+        *topOfFullBlockP = 0;
+        *outcol16P = 0;
+    } else {
+        /* Write output columns right to left */
+        unsigned int i;
+        for (i = 0; i < 16; ++i){
+            unsigned int const index = ((i & 0x8) ^ 0x8) + (i & 0x7);
+            /* Absorb little-endian "twists" */
+            block[i]= inrow[index];
+            blockPartial[i] = index < (16-inpamP->height%16)
+                ? inrow[0]
+                : block[i];
+        }
+        *topOfFullBlockP = inpamP->height % 16;
+    
+        if (inpamP->height >= 16) {
+            *outcol16P = inpamP->height/16 - 1;
+        } else
+            *outcol16P = 0;
+    }
+}
+
+
+
+static void
+doPartialBlockTop(const struct pam * const inpamP,
+                  bit **             const inrow,
+                  const bit *        const blockPartial[16],
+                  unsigned int       const topOfFullBlock,
+                  uint16_t **        const outplane) {
+    
+    if (topOfFullBlock > 0) {
+        unsigned int colChar, row;
+        unsigned int pad = 16 - topOfFullBlock;
+
+        for (colChar=0; colChar < pbm_packed_bytes(inpamP->width); ++colChar)
+            inrow[0][colChar] = 0x00;
+
+        for (row = 0; row < topOfFullBlock; ++row){
+            pbm_readpbmrow_packed(inpamP->file, inrow[row+pad],
+                                  inpamP->width, inpamP->format);
+            if (inpamP->width % 8 > 0){
+                /* Clear partial byte at end of input row */
+                int const lastByte = pbm_packed_bytes(inpamP->width) -1;
+
+                inrow[row+pad][lastByte] >>= (8 - inpamP->width % 8);
+                inrow[row+pad][lastByte] <<= (8 - inpamP->width % 8);
+            }
+        }
+
+        transpose16Bitrows(inpamP->width, inpamP->height, blockPartial,
+                           outplane, inpamP->height /16);
+            /* Transpose partial rows on top of input.  Place on right edge of
+               output.
+            */ 
+    }
+}
+
+
+
+static void
+doFullBlocks(const struct pam * const inpamP,
+             bit **             const inrow,
+             int                const xdir,
+             const bit *        const block[16],
+             unsigned int       const topOfFullBlock,
+             unsigned int       const initOutcol16,
+             uint16_t **        const outplane) {
+
+    unsigned int row;
+    unsigned int outcol16;
+    unsigned int modrow;
+    /* Number of current row within buffer */
+
+    for (row = topOfFullBlock, outcol16 = initOutcol16, modrow = 0;
+         row < inpamP->height;
+         ++row) {
+
+        pbm_readpbmrow_packed(inpamP->file, inrow[modrow],
+                              inpamP->width, inpamP->format);
+        if (inpamP->width % 8 > 0) {
+            /* Clear partial byte at end of input row */
+            int const lastByte = pbm_packed_bytes(inpamP->width) - 1;
+            inrow[modrow][lastByte] >>= (8 - inpamP->width % 8);
+            inrow[modrow][lastByte] <<= (8 - inpamP->width % 8);
+        }
+
+        ++modrow;
+        if (modrow == 16) {
+            /* 16 row buffer is full.  Transpose. */
+            modrow = 0; 
+
+            transpose16Bitrows(inpamP->width, inpamP->height,
+                               block, outplane, outcol16);
+            outcol16 += xdir;
+        }
+    }
+}
+
+
+
+static void
+doPartialBlockBottom(const struct pam * const inpamP,
+                     bit **             const inrow,
+                     int                const xdir,
+                     const bit *        const blockPartial[16],
+                     uint16_t **        const outplane) {
+    
+    if (xdir > 0 && inpamP->height % 16 > 0) {
+        unsigned int colChar;
+
+        for (colChar=0; colChar < pbm_packed_bytes(inpamP->width); ++colChar)
+            inrow[15][colChar] = 0x00;
+
+        transpose16Bitrows(inpamP->width, inpamP->height, blockPartial,
+                           outplane, inpamP->height/16);
+            /* Transpose partial rows on bottom of input.  Place on right edge
+               of output.
+            */ 
+    }
+}
+
+
+
+static void
+writeOut(const struct pam * const outpamP,
+         uint16_t **        const outplane,
+         int                const ydir) {
+             
+    unsigned int row;
+
+    for (row = 0; row < outpamP->height; ++row) {
+        unsigned int const outrow = (ydir > 0) ?
+            row :
+            outpamP->height - row - 1;  /* reverse order */
+
+        pbm_writepbmrow_packed(stdout, (bit *)outplane[outrow],
+                               outpamP->width, 0);
+    }
+}
+
+
+static void
+clearOutplane(const struct pam * const outpamP,
+              uint16_t **        const outplane) { 
+    
+    unsigned int row;
+    
+    for (row = 0; row < outpamP->height; ++row) {
+        unsigned int col16;  /* column divided by 16 */
+        for (col16 = 0; col16 < (outpamP->width + 15)/16; ++col16)
+            outplane[row][col16] = 0x0000;
+    }
+} 
+
+
+
+void
+pamflip_transformRowsToColumnsPbmSse(const struct pam * const inpamP,
+                                     const struct pam * const outpamP,
+                                     struct xformCore const xformCore) { 
+/*----------------------------------------------------------------------------
+  This is a specialized routine for row-for-column PBM transformations.
+  (-cw, -ccw, -xy).
+-----------------------------------------------------------------------------*/
+    int const xdir = xformCore.c;
+        /* Input top  => output left (+1)/ right (-1)  */
+    int const ydir = xformCore.b;
+        /* Input left => output top  (+1)/ bottom (-1) */
+    int const blocksPerRow = ((unsigned int) outpamP->width + 15) /16;
+
+    bit ** inrow;
+    uint16_t ** outplane;
+    const bit * block[16];
+    const bit * blockPartial[16];
+    unsigned int topOfFullBlock;
+    unsigned int outcol16;
+
+    inrow = pbm_allocarray_packed( inpamP->width, 16);
+    MALLOCARRAY2(outplane, outpamP->height + 7, blocksPerRow);
+    if (outplane == NULL)
+        pm_error("Could not allocate %u x %u array of 16 bit units",
+                 blocksPerRow, outpamP->height + 7);
+
+    /* We write to the output array in 16 bit units.  Add margin. */  
+
+    clearOutplane(outpamP, outplane);
+
+    analyzeBlock(inpamP, inrow, xdir, block, blockPartial, 
+                 &topOfFullBlock, &outcol16);
+
+    doPartialBlockTop(inpamP, inrow, blockPartial, topOfFullBlock, outplane);
+
+    doFullBlocks(inpamP, inrow, xdir, block,
+                 topOfFullBlock, outcol16, outplane);
+
+    doPartialBlockBottom(inpamP, inrow, xdir, blockPartial, outplane);
+
+    writeOut(outpamP, outplane, ydir);
+
+    pbm_freearray(outplane, outpamP->height + 7);
+    pbm_freearray(inrow, 16);
+}
+#else  /* WANT_SSE */
+
+void
+pamflip_transformRowsToColumnsPbmSse(const struct pam * const inpamP,
+                                     const struct pam * const outpamP,
+                                     struct xformCore   const xformCore) { 
+
+    /* Nobody is supposed to call this */
+    assert(false);
+}
+#endif 
diff --git a/editor/pamflip/pamflip_sse.h b/editor/pamflip/pamflip_sse.h
new file mode 100644
index 00000000..59e7c026
--- /dev/null
+++ b/editor/pamflip/pamflip_sse.h
@@ -0,0 +1,12 @@
+#ifndef PAMFLIP_SSE_H_INCLUDED
+#define PAMFLIP_SSE_H_INCLUDED
+
+struct pam;
+#include "flip.h"
+
+void
+pamflip_transformRowsToColumnsPbmSse(const struct pam *     const inpamP,
+                                     const struct pam *     const outpamP,
+                                     struct xformCore       const xformCore);
+
+#endif
diff --git a/editor/pamfunc.c b/editor/pamfunc.c
index b6e56e17..5945b82d 100644
--- a/editor/pamfunc.c
+++ b/editor/pamfunc.c
@@ -13,8 +13,8 @@
   multiply/divide where possible.  Especially when multiplying by an 
   integer.
 
-  2) For multiply/divide, give option of simply changing the maxval and
-  leaving the raster alone.
+  2) speed up by not transforming the raster in the idempotent cases
+  (e.g. multiply by one).
 
 ******************************************************************************/
 
@@ -23,7 +23,7 @@
 #include "shhopt.h"
 #include "pam.h"
 
-enum function {
+enum Function {
     FN_MULTIPLY,
     FN_DIVIDE,
     FN_ADD,
@@ -42,22 +42,23 @@ enum function {
    a "max" function.
 */
 
-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;  /* Filespec of input file */
-    enum function function;
+    const char * inputFileName;
+    enum Function function;
     union {
-        float multiplier;
-        float divisor;
-        int adder;
-        int subtractor;
+        float        multiplier;
+        float        divisor;
+        int          adder;
+        int          subtractor;
         unsigned int max;
         unsigned int min;
         unsigned int mask;
         unsigned int shiftCount;
     } u;
+    unsigned int changemaxval;
     unsigned int verbose;
 };
 
@@ -80,8 +81,8 @@ parseHex(const char * const hexString) {
          
 
 static void
-parseCommandLine(int argc, char ** const argv,
-                 struct cmdlineInfo * const cmdlineP) {
+parseCommandLine(int argc, const char ** const argv,
+                 struct CmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
    Note that the file spec array we return is stored in the storage that
    was passed to us as the argv array.
@@ -126,13 +127,16 @@ parseCommandLine(int argc, char ** const argv,
             &shiftleftSpec,  0);
     OPTENT3(0,   "shiftright", OPT_UINT,   &cmdlineP->u.shiftCount,
             &shiftrightSpec, 0);
-    OPTENT3(0,   "verbose",    OPT_FLAG,   NULL, &cmdlineP->verbose,       0);
+    OPTENT3(0,   "verbose",      OPT_FLAG,   NULL, &cmdlineP->verbose,
+            0);
+    OPTENT3(0,   "changemaxval", OPT_FLAG,   NULL, &cmdlineP->changemaxval,
+            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 */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (multiplierSpec + divisorSpec + adderSpec + subtractorSpec +
@@ -186,16 +190,17 @@ parseCommandLine(int argc, char ** const argv,
                  argc-1);
 
     if (argc-1 < 1)
-        cmdlineP->inputFilespec = "-";
+        cmdlineP->inputFileName = "-";
     else 
-        cmdlineP->inputFilespec = argv[1];
+        cmdlineP->inputFileName = argv[1];
     
+    free(option_def);
 }
 
 
 
 static bool
-isDyadicMaskFunction(enum function const fn) {
+isDyadicMaskFunction(enum Function const fn) {
 
     return (fn == FN_AND || fn == FN_OR || fn == FN_XOR);
 }
@@ -203,7 +208,7 @@ isDyadicMaskFunction(enum function const fn) {
 
 
 static bool
-isMaskFunction(enum function const fn) {
+isMaskFunction(enum Function const fn) {
 
     return (isDyadicMaskFunction(fn) || fn == FN_NOT);
 }
@@ -211,7 +216,7 @@ isMaskFunction(enum function const fn) {
 
 
 static bool
-isShiftFunction(enum function const fn) {
+isShiftFunction(enum Function const fn) {
 
     return (fn == FN_SHIFTLEFT || fn == FN_SHIFTRIGHT);
 }
@@ -219,7 +224,7 @@ isShiftFunction(enum function const fn) {
 
 
 static bool
-isBitstringFunction(enum function const fn) {
+isBitstringFunction(enum Function const fn) {
 
     return isMaskFunction(fn) || isShiftFunction(fn);
 }
@@ -227,7 +232,7 @@ isBitstringFunction(enum function const fn) {
 
 
 static void
-validateFunction(struct cmdlineInfo const cmdline,
+validateFunction(struct CmdlineInfo const cmdline,
                  const struct pam * const pamP) {
 
     if (isBitstringFunction(cmdline.function)) {
@@ -259,7 +264,58 @@ validateFunction(struct cmdlineInfo const cmdline,
 
 
 static void
-applyFunction(struct cmdlineInfo const cmdline,
+planTransform(struct CmdlineInfo const cmdline,
+              sample             const inputMaxval,
+              sample *           const outputMaxvalP,
+              bool *             const mustChangeRasterP) {
+/*----------------------------------------------------------------------------
+   Plan the transform described by 'cmdline', given the maxval of the input
+   image is 'inputMaxval.
+
+   The plan just consists of whether to change the maxval or the raster.
+   Some multiplications and divisions can be achieved just by changing the
+   maxval and leaving the samples in the raster alone.
+-----------------------------------------------------------------------------*/
+    if (cmdline.changemaxval) {
+        /* User allows us to change the maxval, if that makes it easier */
+        if (cmdline.function == FN_MULTIPLY || cmdline.function == FN_DIVIDE) {
+            float const multiplier =
+                cmdline.function == FN_MULTIPLY ? cmdline.u.multiplier :
+                (1/cmdline.u.divisor);
+
+            float const neededMaxval = inputMaxval / multiplier;
+
+            if (neededMaxval + 0.5 < inputMaxval) {
+                /* Lowering the maxval might make some of the sample values
+                   higher than the maxval, so we'd have to modify the raster
+                   to clip them.
+                */
+                *outputMaxvalP     = inputMaxval;
+                *mustChangeRasterP = true;
+            } else if (neededMaxval > PAM_OVERALL_MAXVAL) {
+                *outputMaxvalP     = inputMaxval;
+                *mustChangeRasterP = true;
+            } else {
+                *outputMaxvalP     = ROUNDU(neededMaxval);
+                *mustChangeRasterP = false;
+            }
+        } else {
+            *outputMaxvalP     = inputMaxval;
+            *mustChangeRasterP = true;
+        }
+    } else {
+        *outputMaxvalP     = inputMaxval;
+        *mustChangeRasterP = true;
+    }
+    if (*outputMaxvalP != inputMaxval)
+        pm_message("Changing maxval to %u because of -changemaxval",
+                   (unsigned)*outputMaxvalP);
+}
+
+
+
+static void
+applyFunction(struct CmdlineInfo const cmdline,
               struct pam         const inpam,
               struct pam         const outpam,
               tuple *            const inputRow,
@@ -275,10 +331,10 @@ applyFunction(struct cmdlineInfo const cmdline,
            divide, both cmdline.u.divisor and oneOverDivisor are
            meaningless.  
         */
-    int col;
+    unsigned int col;
 
     for (col = 0; col < inpam.width; ++col) {
-        int plane;
+        unsigned int plane;
         for (plane = 0; plane < inpam.depth; ++plane) {
             sample const inSample = inputRow[col][plane];
             sample outSample;  /* Could be > maxval  */
@@ -330,21 +386,22 @@ applyFunction(struct cmdlineInfo const cmdline,
 
 
 int
-main(int argc, char *argv[]) {
+main(int argc, const char *argv[]) {
 
     FILE * ifP;
     tuple * inputRow;   /* Row from input image */
     tuple * outputRow;  /* Row of output image */
-    int row;
-    struct cmdlineInfo cmdline;
+    unsigned int row;
+    struct CmdlineInfo cmdline;
     struct pam inpam;   /* Input PAM image */
     struct pam outpam;  /* Output PAM image */
+    bool mustChangeRaster;
 
-    pnm_init(&argc, argv);
+    pm_proginit(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
 
-    ifP = pm_openr(cmdline.inputFilespec);
+    ifP = pm_openr(cmdline.inputFileName);
 
     pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
 
@@ -355,16 +412,21 @@ main(int argc, char *argv[]) {
     outpam = inpam;    /* Initial value -- most fields should be same */
     outpam.file = stdout;
 
+    planTransform(cmdline, inpam.maxval, &outpam.maxval, &mustChangeRaster);
+
     pnm_writepaminit(&outpam);
 
     outputRow = pnm_allocpamrow(&outpam);
 
-    for (row = 0; row < inpam.height; row++) {
+    for (row = 0; row < inpam.height; ++row) {
         pnm_readpamrow(&inpam, inputRow);
 
-        applyFunction(cmdline, inpam, outpam, inputRow, outputRow);
+        if (mustChangeRaster) {
+            applyFunction(cmdline, inpam, outpam, inputRow, outputRow);
 
-        pnm_writepamrow(&outpam, outputRow);
+            pnm_writepamrow(&outpam, outputRow);
+        } else
+            pnm_writepamrow(&outpam, inputRow);
     }
     pnm_freepamrow(outputRow);
     pnm_freepamrow(inputRow);
@@ -374,3 +436,5 @@ main(int argc, char *argv[]) {
     return 0;
 }
 
+
+
diff --git a/editor/pammasksharpen.c b/editor/pammasksharpen.c
index e61237ca..6e34ab20 100644
--- a/editor/pammasksharpen.c
+++ b/editor/pammasksharpen.c
@@ -17,7 +17,7 @@ struct cmdlineInfo {
 
 
 static void
-parseCommandLine(int argc, char ** const argv,
+parseCommandLine(int argc, const char ** const argv,
                  struct cmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
    Note that the file spec array we return is stored in the storage that
@@ -46,7 +46,7 @@ parseCommandLine(int argc, char ** const argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (sharpSpec) {
@@ -109,7 +109,7 @@ sharpened(sample const inputSample,
 
 
 int
-main(int argc, char *argv[]) {
+main(int argc, const char * *argv) {
 
     struct cmdlineInfo cmdline;
     struct pam inpam;
@@ -126,8 +126,8 @@ main(int argc, char *argv[]) {
            which they will be considered identical.
         */
     
-    pnm_init(&argc, argv);
-
+    pm_proginit(&argc, argv);
+    
     parseCommandLine(argc, argv, &cmdline);
 
     ifP = pm_openr(cmdline.inputFilespec);
diff --git a/editor/pamperspective.c b/editor/pamperspective.c
index 6bf8314e..16715c2e 100644
--- a/editor/pamperspective.c
+++ b/editor/pamperspective.c
@@ -18,6 +18,7 @@
     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
 
+#define _XOPEN_SOURCE 500  /* Make sure strdup() is in string.h */
 #define _BSD_SOURCE   /* Make sure strdup is int string.h */
 
 #include <assert.h>
@@ -486,7 +487,7 @@ parseCommandLine(int argc, const char * argv[],
   opt.opt_table = option_def;
   opt.short_allowed = FALSE;
   opt.allowNegNum = TRUE;
-  optParseOptions3 (&argc, (char **)argv, opt, sizeof(opt), 0);
+  pm_optParseOptions3 (&argc, (char **)argv, opt, sizeof(opt), 0);
 
   /* The non-option arguments are optionally all eight coordinates
      and optionally the input filename
@@ -536,7 +537,7 @@ parseCommandLine(int argc, const char * argv[],
                            parse_enum (bool_text[i], bool_token,
                                        bool_option_name[i]));
 
-  /* Propagate values where neccessary */
+  /* Propagate values where necessary */
 
   if (float_spec[10])           /* --margin */
     for (i=11; i<15; i++)       /* --top_margin through --right_margin */
diff --git a/editor/pamrecolor.c b/editor/pamrecolor.c
new file mode 100644
index 00000000..6937fd8d
--- /dev/null
+++ b/editor/pamrecolor.c
@@ -0,0 +1,528 @@
+/* ----------------------------------------------------------------------
+ *
+ * Replace every pixel in an image with one of equal luminance
+ *
+ * By Scott Pakin <scott+pbm@pakin.org>
+ *
+ * ----------------------------------------------------------------------
+ *
+ * Copyright (C) 2010 Scott Pakin <scott+pbm@pakin.org>
+ *
+ * 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 3 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, see http://www.gnu.org/licenses/.
+ *
+ * ----------------------------------------------------------------------
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+
+#include "mallocvar.h"
+#include "nstring.h"
+#include "shhopt.h"
+#include "pam.h"
+
+/* Two numbers less than REAL_EPSILON apart are considered equal. */
+#define REAL_EPSILON 0.00001
+
+/* Ensure a number N is no less than A and no greater than B. */
+#define CLAMPxy(N, A, B) MAX(MIN((float)(N), (float)(B)), (float)(A))
+
+
+struct rgbfrac {
+    /* This structure represents red, green, and blue, each expressed
+       as a fraction from 0.0 to 1.0.
+    */
+    float rfrac;
+    float gfrac;
+    float bfrac;
+};
+
+struct cmdlineInfo {
+    /* This structure represents all of the information the user
+       supplied in the command line but in a form that's easy for the
+       program to use.
+    */
+    const char *    inputFileName;  /* '-' if stdin */
+    const char *    colorfile;      /* NULL if unspecified */
+    struct rgbfrac  color2gray;
+        /* colorspace/rmult/gmult/bmult options.  Negative numbers if
+           unspecified.
+        */
+    unsigned int    targetcolorSpec;
+    struct rgbfrac  targetcolor;
+    unsigned int    randomseed;
+    unsigned int    randomseedSpec;
+};
+
+
+
+static float
+rgb2gray(struct rgbfrac * const color2grayP,
+         float            const red,
+         float            const grn,
+         float            const blu) {
+    return
+        color2grayP->rfrac * red +
+        color2grayP->gfrac * grn +
+        color2grayP->bfrac * blu;
+}
+
+
+
+static tuplen *
+getColorRow(struct pam  * const pamP,
+            tuplen     ** const imageData,
+            unsigned int  const row,
+            unsigned int  const desiredWidth) {
+/*----------------------------------------------------------------------
+  Return a row of color data.  If the number of columns is too small,
+  repeat the existing columns in tiled fashion.
+------------------------------------------------------------------------*/
+    unsigned int const imageRow = row % pamP->height;
+
+    static tuplen * oneRow = NULL;
+
+    tuplen * retval;
+
+    if (pamP->width >= desiredWidth)
+        retval = imageData[imageRow];
+    else {
+        unsigned int col;
+
+        if (!oneRow) {
+            struct pam widePam;
+
+            widePam = *pamP;
+            widePam.width = desiredWidth;
+
+            oneRow = pnm_allocpamrown(&widePam);
+        }
+        for (col = 0; col < desiredWidth; ++col)
+            oneRow[col] = imageData[imageRow][col % pamP->width];
+        retval = oneRow;
+    }
+    return retval;
+}
+
+
+
+static void
+convertRowToGray(struct pam     * const pamP,
+                 struct rgbfrac * const color2gray,
+                 tuplen         * const tupleRow,
+                 samplen        * const grayRow) {
+/*----------------------------------------------------------------------
+  Convert a row of RGB, grayscale, or black-and-white pixels to a row
+  of grayscale values in the range [0, 1].
+------------------------------------------------------------------------*/
+    switch (pamP->depth) {
+    case 1:
+    case 2: {
+        /* Black-and-white or grayscale */
+        unsigned int col;
+        for (col = 0; col < pamP->width; ++col)
+            grayRow[col] = tupleRow[col][0];
+    } break;
+
+    case 3:
+    case 4: {
+        /* RGB color */
+        unsigned int col;
+        for (col = 0; col < pamP->width; ++col)
+            grayRow[col] = rgb2gray(color2gray,
+                                    tupleRow[col][PAM_RED_PLANE],
+                                    tupleRow[col][PAM_GRN_PLANE],
+                                    tupleRow[col][PAM_BLU_PLANE]);
+    } break;
+
+    default:
+        pm_error("internal error: unexpected image depth %u", pamP->depth);
+        break;
+    }
+}
+
+
+
+static void
+explicitlyColorRow(struct pam *   const pamP,
+                   tuplen *       const rowData,
+                   struct rgbfrac const tint) {
+
+    unsigned int col;
+
+    for (col = 0; col < pamP->width; ++col) {
+        rowData[col][PAM_RED_PLANE] = tint.rfrac;
+        rowData[col][PAM_GRN_PLANE] = tint.gfrac;
+        rowData[col][PAM_BLU_PLANE] = tint.bfrac;
+    }
+}
+
+
+
+static void
+randomlyColorRow(struct pam *   const pamP,
+                 tuplen *       const rowData) {
+/*----------------------------------------------------------------------
+  Assign each tuple in a row a random color.
+------------------------------------------------------------------------*/
+    unsigned int col;
+
+    for (col = 0; col < pamP->width; ++col) {
+        rowData[col][PAM_RED_PLANE] = rand() / (float)RAND_MAX;
+        rowData[col][PAM_GRN_PLANE] = rand() / (float)RAND_MAX;
+        rowData[col][PAM_BLU_PLANE] = rand() / (float)RAND_MAX;
+    }
+}
+
+
+
+static void
+recolorRow(struct pam     * const inPamP,
+           tuplen         * const inRow,
+           struct rgbfrac * const color2grayP,
+           tuplen         * const colorRow,
+           struct pam     * const outPamP,
+           tuplen         * const outRow) {
+/*----------------------------------------------------------------------
+  Map each tuple in a given row to a random color with the same
+  luminance.
+------------------------------------------------------------------------*/
+    static samplen * grayRow = NULL;
+
+    unsigned int col;
+
+    if (!grayRow)
+        MALLOCARRAY_NOFAIL(grayRow, inPamP->width);
+
+    convertRowToGray(inPamP, color2grayP, inRow, grayRow);
+
+    for (col = 0; col < inPamP->width; ++col) {
+        float targetgray;
+        float givengray;
+        float red, grn, blu;
+
+        red = colorRow[col][PAM_RED_PLANE];   /* initial value */
+        grn = colorRow[col][PAM_GRN_PLANE];   /* initial value */
+        blu = colorRow[col][PAM_BLU_PLANE];   /* initial value */
+
+        targetgray = grayRow[col];
+        givengray = rgb2gray(color2grayP, red, grn, blu);
+
+        if (givengray == 0.0) {
+            /* Special case for black so we don't divide by zero */
+            red = targetgray;
+            grn = targetgray;
+            blu = targetgray;
+        }
+        else {
+            /* Try simply scaling each channel equally. */
+            red *= targetgray / givengray;
+            grn *= targetgray / givengray;
+            blu *= targetgray / givengray;
+
+            if (red > 1.0 || grn > 1.0 || blu > 1.0) {
+                /* Repeatedly raise the level of all non-1.0 channels
+                 * until all channels are at 1.0 or we reach our
+                 * target gray. */
+                red = MIN(red, 1.0);
+                grn = MIN(grn, 1.0);
+                blu = MIN(blu, 1.0);
+                givengray = rgb2gray(color2grayP, red, grn, blu);
+
+                while (fabsf(givengray - targetgray) > REAL_EPSILON) {
+                    float increment;
+                        /* How much to increase each channel (unscaled
+                           amount)
+                        */
+                    int   subOne = 0;
+                        /* Number of channels with sub-1.0 values */
+
+                    /* Tally the number of channels that aren't yet maxed
+                       out.
+                    */
+                    if (red < 1.0)
+                        subOne++;
+                    if (grn < 1.0)
+                        subOne++;
+                    if (blu < 1.0)
+                        subOne++;
+
+                    /* Stop if we've reached our target or can't increment
+                     * any channel any further. */
+                    if (subOne == 0)
+                        break;
+
+                    /* Brighten each non-maxed channel equally. */
+                    increment = (targetgray - givengray) / subOne;
+                    if (red < 1.0)
+                        red = MIN(red + increment / color2grayP->rfrac, 1.0);
+                    if (grn < 1.0)
+                        grn = MIN(grn + increment / color2grayP->gfrac, 1.0);
+                    if (blu < 1.0)
+                        blu = MIN(blu + increment / color2grayP->bfrac, 1.0);
+
+                    /* Prepare to try again. */
+                    givengray = rgb2gray(color2grayP, red, grn, blu);
+                }
+            }
+            else
+                givengray = rgb2gray(color2grayP, red, grn, blu);
+        }
+
+        outRow[col][PAM_RED_PLANE] = red;
+        outRow[col][PAM_GRN_PLANE] = grn;
+        outRow[col][PAM_BLU_PLANE] = blu;
+        if (outPamP->depth == 4)
+            outRow[col][PAM_TRN_PLANE] = inRow[col][PAM_TRN_PLANE];
+    }
+}
+
+
+
+static struct rgbfrac
+color2GrayFromCsName(const char * const csName) {
+
+    struct rgbfrac retval;
+
+    /* Thanks to
+       http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+       for these values.
+    */
+    if (streq(csName, "ntsc")) {
+        /* NTSC RGB with an Illuminant C reference white */
+        retval.rfrac = 0.2989164;
+        retval.gfrac = 0.5865990;
+        retval.bfrac = 0.1144845;
+    } else if (streq(csName, "srgb")) {
+        /* sRGB with a D65 reference white */
+        retval.rfrac = 0.2126729;
+        retval.gfrac = 0.7151522;
+        retval.bfrac = 0.0721750;
+    } else if (streq(csName, "adobe")) {
+        /* Adobe RGB (1998) with a D65 reference white */
+        retval.rfrac = 0.2973769;
+        retval.gfrac = 0.6273491;
+        retval.bfrac = 0.0752741;
+    } else if (streq(csName, "apple")) {
+        /* Apple RGB with a D65 reference white */
+        retval.rfrac = 0.2446525;
+        retval.gfrac = 0.6720283;
+        retval.bfrac = 0.0833192;
+    } else if (streq(csName, "cie")) {
+        /* CIE with an Illuminant E reference white */
+        retval.rfrac = 0.1762044;
+        retval.gfrac = 0.8129847;
+        retval.bfrac = 0.0108109;
+    } else if (streq(csName, "pal")) {
+        /* PAL/SECAM with a D65 reference white */
+        retval.rfrac = 0.2220379;
+        retval.gfrac = 0.7066384;
+        retval.bfrac = 0.0713236;
+    } else if (streq(csName, "smpte-c")) {
+        /* SMPTE-C with a D65 reference white */
+        retval.rfrac = 0.2124132;
+        retval.gfrac = 0.7010437;
+        retval.bfrac = 0.0865432;
+    } else if (streq(csName, "wide")) {
+        /* Wide gamut with a D50 reference white */
+        retval.rfrac = 0.2581874;
+        retval.gfrac = 0.7249378;
+        retval.bfrac = 0.0168748;
+    } else
+        pm_error("Unknown color space name \"%s\"", csName);
+
+    return retval;
+}
+
+
+
+static void
+parseCommandLine(int argc, const char ** const argv,
+                 struct cmdlineInfo * const cmdlineP ) {
+
+    optEntry     * option_def;
+        /* Instructions to OptParseOptions3 on how to parse our options */
+    optStruct3     opt;
+    unsigned int   option_def_index;
+    const char *   colorspaceOpt;
+    const char *   targetcolorOpt;
+    unsigned int   csSpec, rmultSpec, gmultSpec, bmultSpec,
+        colorfileSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+    option_def_index = 0;          /* Incremented by OPTENTRY */
+
+    OPTENT3(0, "colorspace",   OPT_STRING, &colorspaceOpt, &csSpec, 0);
+    OPTENT3(0, "rmult",        OPT_FLOAT,  &cmdlineP->color2gray.rfrac,
+            &rmultSpec, 0);
+    OPTENT3(0, "gmult",        OPT_FLOAT,  &cmdlineP->color2gray.gfrac,
+            &gmultSpec, 0);
+    OPTENT3(0, "bmult",        OPT_FLOAT,  &cmdlineP->color2gray.bfrac,
+            &bmultSpec, 0);
+    OPTENT3(0, "colorfile",    OPT_STRING, &cmdlineP->colorfile,
+            &colorfileSpec, 0);
+    OPTENT3(0, "targetcolor",  OPT_STRING, &targetcolorOpt,
+            &cmdlineP->targetcolorSpec, 0);
+    OPTENT3(0,   "randomseed",   OPT_UINT,    &cmdlineP->randomseed,
+            &cmdlineP->randomseedSpec,      0);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = 0;
+    opt.allowNegNum = 0;
+
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+
+    if (rmultSpec || gmultSpec || bmultSpec) {
+        /* If the user explicitly specified RGB multipliers, ensure that
+         * (a) he didn't specify --colorspace,
+         * (b) he specified all three channels, and
+         * (c) the values add up to 1.
+         */
+        float maxLuminance;
+
+        if (csSpec)
+            pm_error("The --colorspace option is mutually exclusive with "
+                     "the --rmult, --gmult, and --bmult options");
+        if (!(rmultSpec && gmultSpec && bmultSpec))
+            pm_error("If you specify any of --rmult, --gmult, or --bmult, "
+                     "you must specify all of them");
+        maxLuminance =
+            cmdlineP->color2gray.rfrac +
+            cmdlineP->color2gray.gfrac +
+            cmdlineP->color2gray.bfrac;
+        if (fabsf(1.0 - maxLuminance) > REAL_EPSILON)
+            pm_error("The values given for --rmult, --gmult, and --bmult must "
+                     "sum to 1.0, not %.10g", maxLuminance);
+    } else if (csSpec)
+        cmdlineP->color2gray = color2GrayFromCsName(colorspaceOpt);
+    else
+        cmdlineP->color2gray = color2GrayFromCsName("ntsc");
+
+    if (colorfileSpec && cmdlineP->targetcolorSpec)
+        pm_error("The --colorfile option and the --targetcolor option are "
+                 "mutually exclusive");
+
+    if (!colorfileSpec)
+        cmdlineP->colorfile = NULL;
+
+    if (cmdlineP->targetcolorSpec) {
+        sample const colorMaxVal = (1<<16) - 1;
+            /* Maximum PAM maxval for precise sample-to-float conversion */
+        tuple const targetTuple = pnm_parsecolor(targetcolorOpt, colorMaxVal);
+        cmdlineP->targetcolor.rfrac =
+            targetTuple[PAM_RED_PLANE] / (float)colorMaxVal;
+        cmdlineP->targetcolor.gfrac =
+            targetTuple[PAM_GRN_PLANE] / (float)colorMaxVal;
+        cmdlineP->targetcolor.bfrac =
+            targetTuple[PAM_BLU_PLANE] / (float)colorMaxVal;
+    }
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else {
+        cmdlineP->inputFileName = argv[1];
+        if (argc-1 > 1)
+            pm_error("Too many arguments: %u.  The only argument is the "
+                     "optional input file name", argc-1);
+    }
+}
+
+
+
+int
+main(int argc, const char *argv[]) {
+    struct cmdlineInfo cmdline;          /* Command-line parameters */
+    struct pam         inPam;
+    struct pam         outPam;
+    struct pam         colorPam;
+    FILE *             ifP;
+    FILE *             colorfP;
+    const char *       comments;
+    tuplen *           inRow;
+    tuplen *           outRow;
+    tuplen **          colorData;
+    tuplen *           colorRowBuffer;
+    unsigned int       row;
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    srand(cmdline.randomseedSpec ? cmdline.randomseed : pm_randseed());
+
+    ifP = pm_openr(cmdline.inputFileName);
+    inPam.comment_p = &comments;
+    pnm_readpaminit(ifP, &inPam, PAM_STRUCT_SIZE(comment_p));
+
+    outPam = inPam;
+    outPam.file = stdout;
+    outPam.format = PAM_FORMAT;
+    outPam.depth = 4 - (inPam.depth % 2);
+    outPam.allocation_depth = outPam.depth;
+    strcpy(outPam.tuple_type, PAM_PPM_TUPLETYPE);
+    pnm_writepaminit(&outPam);
+
+    if (cmdline.colorfile) {
+        colorfP = pm_openr(cmdline.colorfile);
+        colorPam.comment_p = NULL;
+        colorData =
+            pnm_readpamn(colorfP, &colorPam, PAM_STRUCT_SIZE(comment_p));
+    } else {
+        colorfP = NULL;
+        colorPam = outPam;
+        colorData = NULL;
+    }
+
+    inRow = pnm_allocpamrown(&inPam);
+    outRow = pnm_allocpamrown(&outPam);
+
+    colorRowBuffer = pnm_allocpamrown(&outPam);
+
+    for (row = 0; row < inPam.height; ++row) {
+        tuplen * colorRow;
+
+        pnm_readpamrown(&inPam, inRow);
+
+        if (cmdline.colorfile)
+            colorRow = getColorRow(&colorPam, colorData, row, outPam.width);
+        else {
+            colorRow = colorRowBuffer;
+
+            if (cmdline.targetcolorSpec)
+                explicitlyColorRow(&colorPam, colorRow, cmdline.targetcolor);
+            else
+                randomlyColorRow(&colorPam, colorRow);
+        }
+        recolorRow(&inPam, inRow,
+                   &cmdline.color2gray, colorRow,
+                   &outPam, outRow);
+        pnm_writepamrown(&outPam, outRow);
+    }
+    pnm_freepamrown(outRow);
+    pnm_freepamrown(inRow);
+    pnm_freepamrown(colorRowBuffer);
+
+    if (colorData)
+        pnm_freepamarrayn(colorData, &colorPam);
+
+    if (colorfP)
+        pm_close(colorfP);
+    pm_close(ifP);
+
+    return 0;
+}
+
diff --git a/editor/pamrubber.c b/editor/pamrubber.c
new file mode 100644
index 00000000..4378c340
--- /dev/null
+++ b/editor/pamrubber.c
@@ -0,0 +1,1475 @@
+/*----------------------------------------------------------------------------*/
+
+/* pamrubber.c - transform images using Rubber Sheeting algorithm
+**               see: http://www.schaik.com/netpbm/rubber/
+**
+** Copyright (C) 2011 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.
+*/
+
+/*----------------------------------------------------------------------------*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <assert.h>
+#include <limits.h>
+#include <math.h>
+#include <time.h>
+
+#include "pm_c_util.h"
+#include "mallocvar.h"
+#include "shhopt.h"
+#include "pam.h"
+#include "pamdraw.h"
+
+
+
+typedef struct {
+  double x;
+  double y;
+} point;
+
+typedef struct {
+  point p1;
+  point p2;
+} line;
+
+typedef struct {
+  point p1;
+  point p2;
+  point p3;
+} triangle;
+
+typedef struct {
+    point tl;  /* top left     */
+    point tr;  /* top right    */
+    point bl;  /* bottom left  */
+    point br;  /* bottom right */
+} quadrilateral;
+
+struct cmdlineInfo {
+    unsigned int nCP;
+    point        oldCP[4];
+    point        newCP[4];
+    const char * fileName;
+    unsigned int quad;
+    unsigned int tri;
+    unsigned int frame;
+    unsigned int linear;
+    unsigned int verbose;
+    unsigned int randseedSpec;
+    unsigned int randseed;
+};
+
+
+static void
+parseCmdline(int argc, const char ** argv,
+             struct cmdlineInfo * const cmdlineP) {
+
+/* parse all parameters from the command line */
+
+    unsigned int option_def_index;
+    char * endptr;
+    unsigned int i;
+    unsigned int nCP;
+
+    /* instructions to optParseOptions3 on how to parse our options. */
+    optEntry * option_def;
+    optStruct3 opt;
+    
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "quad",     OPT_FLAG, NULL, &cmdlineP->quad,     0);
+    OPTENT3(0, "tri",      OPT_FLAG, NULL, &cmdlineP->tri,      0);
+    OPTENT3(0, "frame",    OPT_FLAG, NULL, &cmdlineP->frame,    0);
+    OPTENT3(0, "linear",   OPT_FLAG, NULL, &cmdlineP->linear,   0);
+    OPTENT3(0, "verbose",  OPT_FLAG, NULL, &cmdlineP->verbose,  0);
+    OPTENT3(0, "randseed", OPT_UINT, &cmdlineP->randseed,
+            &cmdlineP->randseedSpec, 0);
+    OPTENT3(0, "randomseed", OPT_UINT, &cmdlineP->randseed,
+            &cmdlineP->randseedSpec, 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 */
+
+    /* uses and sets argc, argv, and some of *cmdlineP and others. */
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+
+    if (!cmdlineP->tri && !cmdlineP->quad)
+        pm_error("You must specify either -tri or -quad");
+
+    if (cmdlineP->tri && cmdlineP->quad)
+        pm_error("You may not specify both -tri and -quad");
+
+    /* Parameters are the control points (in qty of 4) and possibly a file name
+     */
+    nCP = (argc-1) / 4;
+
+    if (nCP > 4)
+        pm_error("Too many arguments: %u.  Arguments are "
+                 "control point coordinates and an optional file name, "
+                 "with a maximum of 4 control points", argc-1);
+
+    cmdlineP->nCP = nCP;
+
+    assert(nCP <= ARRAY_SIZE(cmdlineP->oldCP));
+    assert(nCP <= ARRAY_SIZE(cmdlineP->newCP));
+
+    for (i = 0; i < nCP; ++i) {
+        cmdlineP->oldCP[i].x = strtol(argv[i * 2 + 1], &endptr, 10);
+        cmdlineP->oldCP[i].y = strtol(argv[i * 2 + 2], &endptr, 10);
+        cmdlineP->newCP[i].x = strtol(argv[4 * nCP / 2 + i * 2 + 1],
+                                      &endptr, 10);
+        cmdlineP->newCP[i].y = strtol(argv[4 * nCP / 2 + i * 2 + 2],
+                                      &endptr, 10);
+    }
+
+    if (argc - 1 == 4 * nCP)
+        cmdlineP->fileName = "-";
+    else if (argc - 2 == 4 * nCP)
+        cmdlineP->fileName = argv[nCP * 4 + 1];
+    else
+        pm_error("Invalid number of arguments.  Arguments are "
+                 "control point coordinates and an optional file name, "
+                 "so there must be a multiple of 4 or a multiple of 4 "
+                 "plus 1.");
+}
+
+
+
+/* global variables */
+
+static int nCP;
+static point oldCP[4];
+static point newCP[4];
+static int nTri;
+static triangle tri1s[10];
+static triangle tri2s[10];
+static quadrilateral quad1;
+static quadrilateral quad2;
+static tuple black;
+
+static point
+makepoint(double const x,
+          double const y) {
+
+    point retval;
+
+    retval.x = x;
+    retval.y = y;
+
+    return retval;
+}
+
+
+
+static double
+distance(point const p1,
+         point const p2) {
+
+    return sqrt(SQR(p1.x - p2.x) + SQR(p1.y - p2.y));
+}
+
+
+
+static line
+makeline(point const p1,
+         point const p2) {
+
+    line retval;
+
+    retval.p1 = p1;
+    retval.p2 = p2;
+
+    return retval;
+}
+
+
+
+static bool
+intersect(line *  const l1P,
+          line *  const l2P,
+          point * const pP) {
+
+    bool cross;
+
+    if (((l2P->p2.y - l2P->p1.y) * (l1P->p2.x - l1P->p1.x) -
+         (l2P->p2.x - l2P->p1.x) * (l1P->p2.y - l1P->p1.y)) == 0) {
+        /* parallel lines */
+
+        cross = false;
+
+        if ((l1P->p1.x == l1P->p2.x) && (l2P->p1.x == l2P->p2.x)) {
+            /* two vertical lines */
+            pP->x = (l1P->p1.x + l2P->p1.x) / 2.0;
+            pP->y = 1e10;
+        } else if ((l1P->p1.y == l1P->p2.y) && (l2P->p1.y == l2P->p2.y)) {
+            /* two horizontal lines */
+            pP->x = 1e10;
+            pP->y = (l1P->p1.y + l2P->p1.y) / 2.0;
+        } else {
+            if (fabs(l1P->p2.y - l1P->p1.y) > fabs(l1P->p2.x - l1P->p1.x)) {
+                /* steep slope */
+                pP->y = 1e10;
+                pP->x = (l1P->p2.x - l1P->p1.x) / (l1P->p2.y - l1P->p1.y)
+                    * 1e10;
+            } else {
+                /* even slope */
+                pP->x = 1e10;
+                pP->y = (l1P->p2.y - l1P->p1.y) / (l1P->p2.x - l1P->p1.x)
+                    * 1e10;
+            }
+        }
+    } else {
+        /* intersecting lines */
+        double const ua =
+            ((l2P->p2.x - l2P->p1.x) * (l1P->p1.y - l2P->p1.y)
+              - (l2P->p2.y - l2P->p1.y) * (l1P->p1.x - l2P->p1.x))
+            / ((l2P->p2.y - l2P->p1.y)
+               * (l1P->p2.x - l1P->p1.x) - (l2P->p2.x - l2P->p1.x)
+               * (l1P->p2.y - l1P->p1.y));
+        double const ub =
+            ((l1P->p2.x - l1P->p1.x) * (l1P->p1.y - l2P->p1.y)
+              - (l1P->p2.y - l1P->p1.y) * (l1P->p1.x - l2P->p1.x))
+            / ((l2P->p2.y - l2P->p1.y)
+               * (l1P->p2.x - l1P->p1.x) - (l2P->p2.x - l2P->p1.x)
+               * (l1P->p2.y - l1P->p1.y));
+
+        pP->x = l1P->p1.x + ua * (l1P->p2.x - l1P->p1.x);
+        pP->y = l1P->p1.y + ua * (l1P->p2.y - l1P->p1.y);
+
+        if ((ua >= 0.0) && (ua <= 1.0) && (ub >= 0.0) && (ub <= 1.0))
+            cross = true;
+        else
+            cross = false;
+    }
+
+    return cross;
+}
+
+
+
+static triangle
+maketriangle(point const p1,
+             point const p2,
+             point const p3) {
+
+    triangle retval;
+
+    retval.p1 = p1;
+    retval.p2 = p2;
+    retval.p3 = p3;
+    
+    return retval;
+}
+
+
+
+static int
+insidetri(triangle * const triP,
+          point      const p) {
+
+    int cnt;
+
+    cnt = 0;  /* initial value */
+
+    if ((((triP->p1.y <= p.y) && (p.y < triP->p3.y))
+         || ((triP->p3.y <= p.y) && (p.y < triP->p1.y)))
+        &&
+        (p.x < (triP->p3.x - triP->p1.x) * (p.y - triP->p1.y)
+         / (triP->p3.y - triP->p1.y) + triP->p1.x))
+        cnt = !cnt;
+
+    if ((((triP->p2.y <= p.y) && (p.y < triP->p1.y))
+         || ((triP->p1.y <= p.y) && (p.y < triP->p2.y)))
+        &&
+        (p.x < (triP->p1.x - triP->p2.x) * (p.y - triP->p2.y)
+         / (triP->p1.y - triP->p2.y) + triP->p2.x))
+        cnt = !cnt;
+
+    if ((((triP->p3.y <= p.y) && (p.y < triP->p2.y))
+         || ((triP->p2.y <= p.y) && (p.y < triP->p3.y)))
+        &&
+        (p.x < (triP->p2.x - triP->p3.x) * (p.y - triP->p3.y)
+         / (triP->p2.y - triP->p3.y) + triP->p3.x))
+        cnt = !cnt;
+
+    return cnt;
+}
+
+
+
+static bool
+windtriangle(triangle * const tP,
+             point      const p1,
+             point      const p2,
+             point      const p3) {
+    point f, c;
+    line le, lv;
+    bool cw;
+
+    /* find cross of vertical through p3 and the edge p1-p2 */
+    f.x = p3.x;
+    f.y = -1.0;
+    lv = makeline(p3, f);
+    le = makeline(p1, p2);
+    intersect(&le, &lv, &c);
+
+    /* check for clockwise winding triangle */
+    if (((p1.x > p2.x) && (p3.y < c.y)) ||
+        ((p1.x < p2.x) && (p3.y > c.y))) {
+        *tP = maketriangle(p1, p2, p3);
+        cw = true;
+    } else { /* p1/2/3 were counter clockwise */
+        *tP = maketriangle(p1, p3, p2);
+        cw = false;
+    }
+    return cw;
+}
+
+
+
+static double
+tiny(void) {
+
+    if (rand() % 2)
+        return +1E-6 * (double) ((rand() % 90) + 9);
+    else
+        return -1E-6 * (double) ((rand() % 90) + 9);
+}
+
+
+
+static void
+angle(point * const p1P,
+      point * const p2P) {
+/*----------------------------------------------------------------------------
+   Move *p2P slightly if necessary to make sure the line (*p1P, *p2P)
+   is not horizontal or vertical.
+-----------------------------------------------------------------------------*/
+    if (p1P->x == p2P->x) { /* vertical line */
+        p2P->x += tiny();
+    }
+    if (p1P->y == p2P->y) { /* horizontal line */
+        p2P->y += tiny();
+    }
+}
+
+
+
+static void
+sideTriangleVerticalEdge(unsigned int const n,
+                         triangle *   const trig1P,
+                         point        const p11,
+                         point        const p12,
+                         point        const p13,
+                         point        const p14,
+                         point        const r11,
+                         point        const r12,
+                         triangle *   const trig2P,
+                         point        const p21,
+                         point        const p22,
+                         point        const p23,
+                         point        const p24,
+                         point        const r21,
+                         point        const r22) {
+                                   
+    if (((n >= 4) && (r11.x < p11.x)
+         && (p14.x < p13.x) && (p14.x < p12.x)
+         && (p14.x < p11.x)) /* left edge */
+        || 
+        ((n >= 4) && (r11.x > p11.x)
+         && (p14.x > p13.x) && (p14.x > p12.x)
+         && (p14.x > p11.x))) /* right edge */ {
+        *trig1P = maketriangle(r11, r12, p14);
+        *trig2P = maketriangle(r21, r22, p24);
+    } else if (((n >= 3) && (r11.x < p11.x) && (p13.x < p12.x)
+                && (p13.x < p11.x)) /* left edge */
+               || 
+               ((n >= 3) && (r11.x > p11.x) && (p13.x > p12.x)
+                && (p13.x > p11.x))) /* right edge */ {
+        *trig1P = maketriangle(r11, r12, p13);
+        *trig2P = maketriangle(r21, r22, p23);
+    } else if (((n >= 2) && (r11.x < p11.x)
+                && (p12.x < p11.x)) /* left edge */
+               ||
+               ((n >= 2) && (r11.x > p11.x)
+                && (p12.x > p11.x))) /* right edge */ { 
+        *trig1P = maketriangle(r11, r12, p12);
+        *trig2P = maketriangle(r21, r22, p22);
+    } else if (n >= 1) {
+        *trig1P = maketriangle(r11, r12, p11);
+        *trig2P = maketriangle(r21, r22, p21);
+    }
+}
+
+
+
+static void
+sideTriangleHorizontalEdge(unsigned int const n,
+                           triangle *   const trig1P,
+                           point        const p11,
+                           point        const p12,
+                           point        const p13,
+                           point        const p14,
+                           point        const r11,
+                           point        const r12,
+                           triangle *   const trig2P,
+                           point        const p21,
+                           point        const p22,
+                           point        const p23,
+                           point        const p24,
+                           point        const r21,
+                           point        const r22) {
+                                   
+    if (((n >= 4) && (r11.y < p11.y) && (p14.y < p13.y)
+         && (p14.y < p12.y) && (p14.y < p11.y)) /* top edge */
+        || 
+        ((n >= 4) && (r11.y > p11.y) && (p14.y > p13.y)
+         && (p14.y > p12.y) && (p14.y > p11.y))) /* bottom edge */ {
+        *trig1P = maketriangle(r11, r12, p14);
+        *trig2P = maketriangle(r21, r22, p24);
+    } else if (((n >= 3) && (r11.y < p11.y) && (p13.y < p12.y)
+                && (p13.y < p11.y)) /* top edge */
+               || 
+               ((n >= 3) && (r11.y > p11.y) && (p13.y > p12.y)
+                && (p13.y > p11.y))) /* bottom edge */ {
+        *trig1P = maketriangle(r11, r12, p13);
+        *trig2P = maketriangle(r21, r22, p23);
+    } else if (((n >= 2) && (r11.y < p11.y)
+                && (p12.y < p11.y)) /* top edge */
+               || 
+               ((n >= 2) && (r11.y > p11.y)
+                && (p12.y > p11.y))) /* bottom edge */ {
+        *trig1P = maketriangle(r11, r12, p12);
+        *trig2P = maketriangle(r21, r22, p22);
+    } else if (n >= 1) {
+        *trig1P = maketriangle(r11, r12, p11);
+        *trig2P = maketriangle(r21, r22, p21);
+    }
+}
+
+
+
+static void
+sideTriangle(unsigned int const n,
+             triangle *   const trig1P,
+             point        const p11,
+             point        const p12,
+             point        const p13,
+             point        const p14,
+             point        const r11,
+             point        const r12,
+             triangle *   const trig2P,
+             point        const p21,
+             point        const p22,
+             point        const p23,
+             point        const p24,
+             point        const r21,
+             point        const r22) {
+
+    if (fabs (r11.x - r12.x) < 1.0)
+        sideTriangleVerticalEdge(n,
+                                 trig1P, p11, p12, p13, p14, r11, r12,
+                                 trig2P, p21, p22, p23, p24, r21, r22);
+
+    else if (fabs (r11.y - r12.y) < 1.0)
+        sideTriangleHorizontalEdge(n,
+                                 trig1P, p11, p12, p13, p14, r11, r12,
+                                 trig2P, p21, p22, p23, p24, r21, r22);
+}
+
+
+
+static void
+edgeTriangle(triangle * const trig1P,
+             point      const p11,
+             point      const p12,
+             point      const tl1,
+             point      const tr1,
+             point      const bl1,
+             point      const br1,
+             triangle * const trig2P,
+             point      const p21,
+             point      const p22,
+             point      const tl2,
+             point      const tr2,
+             point      const bl2,
+             point      const br2) {
+             
+    if ((p11.x < p12.x) && (p11.y < p12.y)) {
+        /* up/left to down/right */
+        *trig1P = maketriangle(tr1, p12, p11);
+        *trig2P = maketriangle(tr2, p22, p21);
+    } else if ((p11.x > p12.x) && (p11.y > p12.y)) {
+        /* down/right to up/left */
+        *trig1P = maketriangle(bl1, p12, p11);
+        *trig2P = maketriangle(bl2, p22, p21);
+    } else if ((p11.x < p12.x) && (p11.y > p12.y)) {
+        /* down/left to up/right */
+        *trig1P = maketriangle(tl1, p12, p11);
+        *trig2P = maketriangle(tl2, p22, p21);
+    } else if ((p11.x > p12.x) && (p11.y < p12.y)) {
+        /* up/right to down/left */
+        *trig1P = maketriangle(br1, p12, p11);
+        *trig2P = maketriangle(br2, p22, p21);
+    }
+}
+
+
+
+static quadrilateral
+quadRect(double  const lft,
+         double  const rgt,
+         double  const top,
+         double  const bot) {
+
+    quadrilateral retval;
+
+    retval.tl = makepoint(lft, top);
+    retval.tr = makepoint(rgt, top);
+    retval.bl = makepoint(lft, bot);
+    retval.br = makepoint(rgt, bot);
+
+    return retval;
+}
+
+
+
+static void
+quadCornerSized(point           const p0,
+                point           const p1,
+                point           const p2,
+                point           const p3,
+                point           const mid,
+                quadrilateral * const quadP,
+                triangle *      const triP) {
+
+/* P0-P1 and P2-P3 are the diagonals */
+/* P0-P1 are further apart than P2-P3 */
+
+    if ((p0.x < p1.x) && (p0.y < p1.y)) {
+        /* p0 is top-left */
+        quadP->tl = p0; quadP->br = p1;
+        if (windtriangle(triP, p0, p2, p1)) {
+            /* p2 is top-right */
+            quadP->tr = p2; quadP->bl = p3;
+        } else {
+            /* p3 is top-right */
+            quadP->tr = p3; quadP->bl = p2;
+        }
+    } else if ((p0.x > p1.x) && (p0.y < p1.y)) {
+        /* p0 is top-right */
+        quadP->tr = p0; quadP->bl = p1;
+        if (windtriangle(triP, p0, p2, p1)) {
+            /* p2 is bottom-right */
+            quadP->br = p2; quadP->tl = p3;
+        } else {
+            /* p3 is bottom-right */
+            quadP->br = p3; quadP->tl = p2;
+        }
+    } else if ((p0.x < p1.x) && (p0.y > p1.y)) {
+        /* p0 is bottom-left */
+        quadP->bl = p0; quadP->tr = p1;
+        if (windtriangle(triP, p0, p2, p1)) {
+            /* p2 is top-left */
+            quadP->tl = p2; quadP->br = p3;
+        } else {
+            /* p3 is top-left */
+            quadP->tl = p3; quadP->br = p2;
+        }
+    } else if ((p0.x > p1.x) && (p0.y > p1.y)) {
+        /* p0 is bottom-right */
+        quadP->br = p0; quadP->tl = p1;
+        if (windtriangle(triP, p0, p2, p1)) {
+            /* p2 is bottom-left */
+            quadP->bl = p2; quadP->tr = p3;
+        } else {
+            /* p3 is bottom-left */
+            quadP->bl = p3; quadP->tr = p2;
+        }
+    }
+}
+
+
+
+static void
+quadCorner(point           const p0,
+           point           const p1,
+           point           const p2,
+           point           const p3,
+           point           const mid,
+           quadrilateral * const quadP,
+           triangle *      const triP) {
+
+    /* p0-p1 and p2-p3 are the diagonals */
+
+    if (fabs(p0.x - p1.x) + fabs(p0.y - p1.y) >=
+        fabs(p2.x - p3.x) + fabs(p2.y - p3.y)) {
+        quadCornerSized(p0, p1, p2, p3, mid, quadP, triP);
+    } else {
+        quadCornerSized(p2, p3, p0, p1, mid, quadP, triP);
+    }
+}
+
+
+
+static pamd_drawproc frameDrawproc;
+
+static void
+frameDrawproc (tuple **     const tuples,
+               unsigned int const cols,
+               unsigned int const rows,
+               unsigned int const depth,
+               sample       const maxval,
+               pamd_point   const p,
+               const void * const clientdata) {
+    
+    int yy;
+
+    for (yy = p.y - 1; yy <= p.y + 1; ++yy) {
+        int xx;
+        for (xx = p.x - 1; xx <= p.x + 1; ++xx) {
+            if (xx >= 0 && xx < cols && yy >= 0 && yy < rows) {
+                unsigned int i;
+                for (i = 0; i < depth; ++i)
+                    tuples[yy][xx][i] = (sample) *((tuple *) clientdata + i);
+            }
+        }
+    }
+}
+
+
+
+static void
+drawExtendedLine(const struct pam * const pamP,
+                 tuple **           const outTuples,
+                 point              const p1,
+                 point              const p2) {
+
+    pamd_point const p1ext =
+        pamd_makePoint(p1.x - 10 * (p2.x - p1.x),
+                       p1.y - 10 * (p2.y - p1.y));
+
+    pamd_point const p2ext =
+        pamd_makePoint(p2.x + 10 * (p2.x - p1.x),
+                       p2.y + 10 * (p2.y - p1.y));
+
+    pamd_line(outTuples, pamP->width, pamP->height, pamP->depth, pamP->maxval,
+              p1ext, p2ext, frameDrawproc, black);
+}
+
+
+
+static pamd_point
+clippedPoint(const struct pam * const pamP,
+             point              const p) {
+
+    int const roundedX = ROUND(p.x);
+    int const roundedY = ROUND(p.x);
+
+    int clippedX, clippedY;
+
+    assert(pamP->width  >= 2);
+    assert(pamP->height >= 2);
+
+    if (roundedX <= 0)
+        clippedX = 1;
+    else if (roundedX > pamP->width - 1)
+        clippedX = pamP->width - 2;
+    else
+        clippedX = roundedX;
+        
+    if (roundedY <= 0)
+        clippedY = 1;
+    else if (roundedY > pamP->width - 1)
+        clippedY = pamP->width - 2;
+    else
+        clippedY = roundedY;
+        
+    return pamd_makePoint(clippedX, clippedY);
+}
+
+
+
+static void drawClippedTriangle(const struct pam * const pamP,
+                                tuple **           const tuples,
+                                triangle           const tri) {
+
+    pamd_point const p1 = clippedPoint(pamP, tri.p1);
+    pamd_point const p2 = clippedPoint(pamP, tri.p2);
+    pamd_point const p3 = clippedPoint(pamP, tri.p3);
+
+    pamd_line(tuples, pamP->width, pamP->height, pamP->depth, pamP->maxval,
+              p1, p2, frameDrawproc, black);
+    pamd_line(tuples, pamP->width, pamP->height, pamP->depth, pamP->maxval,
+              p2, p3, frameDrawproc, black);
+    pamd_line(tuples, pamP->width, pamP->height, pamP->depth, pamP->maxval,
+              p3, p1, frameDrawproc, black);
+}
+
+
+
+static void
+prepTrig(int const wd,
+         int const ht) {
+
+/* create triangles using control points */
+
+    point rtl1, rtr1, rbl1, rbr1;
+    point rtl2, rtr2, rbl2, rbr2;
+    point c1p1, c1p2, c1p3, c1p4;
+    point c2p1, c2p2, c2p3, c2p4;
+    line l1, l2;
+    point p0;
+
+    rtl1 = makepoint(0.0 + tiny(),               0.0 + tiny());
+    rtr1 = makepoint((double) wd - 1.0 + tiny(), 0.0 + tiny());
+    rbl1 = makepoint(0.0 + tiny(),               (double) ht - 1.0 + tiny());
+    rbr1 = makepoint((double) wd - 1.0 + tiny(), (double) ht - 1.0 + tiny());
+
+    rtl2 = makepoint(0.0 + tiny(),               0.0 + tiny());
+    rtr2 = makepoint((double) wd - 1.0 + tiny(), 0.0 + tiny());
+    rbl2 = makepoint(0.0 + tiny(),               (double) ht - 1.0 + tiny());
+    rbr2 = makepoint((double) wd - 1.0 + tiny(), (double) ht - 1.0 + tiny());
+
+    if (nCP == 1) {
+        c1p1 = oldCP[0];
+        c2p1 = newCP[0];
+
+        /* connect control point to all corners to get 4 triangles */
+        /* left side triangle */
+        sideTriangle(nCP,
+                     &tri1s[0], c1p1, p0, p0, p0, rbl1, rtl1, 
+                     &tri2s[0], c2p1, p0, p0, p0, rbl2, rtl2);
+        /* top side triangle */
+        sideTriangle(nCP,
+                     &tri1s[1], c1p1, p0, p0, p0, rtl1, rtr1, 
+                     &tri2s[1], c2p1, p0, p0, p0, rtl2, rtr2);
+        /* right side triangle */
+        sideTriangle(nCP,
+                     &tri1s[2], c1p1, p0, p0, p0, rtr1, rbr1, 
+                     &tri2s[2], c2p1, p0, p0, p0, rtr2, rbr2);
+        /* bottom side triangle */
+        sideTriangle(nCP,
+                     &tri1s[3], c1p1, p0, p0, p0, rbr1, rbl1, 
+                     &tri2s[3], c2p1, p0, p0, p0, rbr2, rbl2);
+
+        nTri = 4;
+    } else if (nCP == 2) {
+        c1p1 = oldCP[0];
+        c1p2 = oldCP[1];
+        c2p1 = newCP[0];
+        c2p2 = newCP[1];
+
+        /* check for hor/ver edges */
+        angle (&c1p1, &c1p2);
+        angle (&c2p1, &c2p2);
+
+        /* connect two control points to corners to get 6 triangles */
+        /* left side */
+        sideTriangle(nCP,
+                     &tri1s[0], c1p1, c1p2, p0, p0, rbl1, rtl1, 
+                     &tri2s[0], c2p1, c2p2, p0, p0, rbl2, rtl2);
+        /* top side */
+        sideTriangle(nCP, 
+                     &tri1s[1], c1p1, c1p2, p0, p0, rtl1, rtr1, 
+                     &tri2s[1], c2p1, c2p2, p0, p0, rtl2, rtr2);
+        /* right side */
+        sideTriangle(nCP, 
+                     &tri1s[2], c1p1, c1p2, p0, p0, rtr1, rbr1, 
+                     &tri2s[2], c2p1, c2p2, p0, p0, rtr2, rbr2);
+        /* bottom side */
+        sideTriangle(nCP, 
+                     &tri1s[3], c1p1, c1p2, p0, p0, rbr1, rbl1, 
+                     &tri2s[3], c2p1, c2p2, p0, p0, rbr2, rbl2);
+
+        /* edge to corner triangles */
+        edgeTriangle(&tri1s[4], c1p1, c1p2, rtl1, rtr1, rbl1, rbr1,
+                     &tri2s[4], c2p1, c2p2, rtl2, rtr2, rbl2, rbr2);
+        edgeTriangle(&tri1s[5], c1p2, c1p1, rtl1, rtr1, rbl1, rbr1,
+                     &tri2s[5], c2p2, c2p1, rtl2, rtr2, rbl2, rbr2);
+        nTri = 6;
+    } else if (nCP == 3) {
+        c1p1 = oldCP[0];
+        c1p2 = oldCP[1];
+        c1p3 = oldCP[2];
+         
+        c2p1 = newCP[0];
+        c2p2 = newCP[1];
+        c2p3 = newCP[2];
+
+        /* Move vertices slightly if necessary to make sure no edge is
+           horizontal or vertical.
+        */
+        angle(&c1p1, &c1p2);
+        angle(&c1p2, &c1p3);
+        angle(&c1p3, &c1p1);
+
+        angle(&c2p1, &c2p2);
+        angle(&c2p2, &c2p3);
+        angle(&c2p3, &c2p1);
+
+        if (windtriangle(&tri1s[0], c1p1, c1p2, c1p3)) {
+            tri2s[0] = maketriangle(c2p1, c2p2, c2p3);
+        } else {
+            tri2s[0] = maketriangle(c2p1, c2p3, c2p2);
+        }
+
+        c1p1 = tri1s[0].p1;
+        c1p2 = tri1s[0].p2;
+        c1p3 = tri1s[0].p3;
+         
+        c2p1 = tri2s[0].p1;
+        c2p2 = tri2s[0].p2;
+        c2p3 = tri2s[0].p3;
+
+        /* point to side triangles */
+        /* left side */
+        sideTriangle(nCP,
+                     &tri1s[1], c1p1, c1p2, c1p3, p0, rbl1, rtl1, 
+                     &tri2s[1], c2p1, c2p2, c2p3, p0, rbl2, rtl2);
+        /* top side */
+        sideTriangle(nCP, 
+                     &tri1s[2], c1p1, c1p2, c1p3, p0, rtl1, rtr1, 
+                     &tri2s[2], c2p1, c2p2, c2p3, p0, rtl2, rtr2);
+        /* right side */
+        sideTriangle(nCP, 
+                     &tri1s[3], c1p1, c1p2, c1p3, p0, rtr1, rbr1, 
+                     &tri2s[3], c2p1, c2p2, c2p3, p0, rtr2, rbr2);
+        /* bottom side */
+        sideTriangle(nCP, 
+                     &tri1s[4], c1p1, c1p2, c1p3, p0, rbr1, rbl1, 
+                     &tri2s[4], c2p1, c2p2, c2p3, p0, rbr2, rbl2);
+
+        /* edge to corner triangles */
+        edgeTriangle(&tri1s[5], c1p1, c1p2, rtl1, rtr1, rbl1, rbr1,
+                     &tri2s[5], c2p1, c2p2, rtl2, rtr2, rbl2, rbr2);
+        edgeTriangle(&tri1s[6], c1p2, c1p3, rtl1, rtr1, rbl1, rbr1,
+                     &tri2s[6], c2p2, c2p3, rtl2, rtr2, rbl2, rbr2);
+        edgeTriangle(&tri1s[7], c1p3, c1p1, rtl1, rtr1, rbl1, rbr1,
+                     &tri2s[7], c2p3, c2p1, rtl2, rtr2, rbl2, rbr2);
+        nTri = 8;
+    } else if (nCP == 4) {
+        c1p1 = oldCP[0];
+        c1p2 = oldCP[1];
+        c1p3 = oldCP[2];
+        c1p4 = oldCP[3];
+         
+        c2p1 = newCP[0];
+        c2p2 = newCP[1];
+        c2p3 = newCP[2];
+        c2p4 = newCP[3];
+
+        /* check for hor/ver edges */
+        angle (&c1p1, &c1p2);
+        angle (&c1p2, &c1p3);
+        angle (&c1p3, &c1p4);
+        angle (&c1p4, &c1p1);
+        angle (&c1p1, &c1p3);
+        angle (&c1p2, &c1p4);
+
+        angle (&c2p1, &c2p2);
+        angle (&c2p2, &c2p3);
+        angle (&c2p3, &c2p4);
+        angle (&c2p4, &c2p1);
+        angle (&c2p1, &c2p3);
+        angle (&c2p2, &c2p4);
+
+        /*-------------------------------------------------------------------*/
+        /*        -1-      -2-        -3-      -4-        -5-      -6-       */
+        /*       1   2    1   3      1   2    1   4      1   3    1   4      */
+        /*         X        X          X        X          X        X        */
+        /*       3   4    2   4      4   3    2   3      4   2    3   2      */
+        /*-------------------------------------------------------------------*/
+
+        /* center two triangles */
+        l1 = makeline(c1p1, c1p4);
+        l2 = makeline(c1p2, c1p3);
+        if (intersect(&l1, &l2, &p0)) {
+            if (windtriangle(&tri1s[0], c1p1, c1p2, c1p3)) {
+                tri1s[1] = maketriangle(c1p4, c1p3, c1p2);
+                tri2s[0] = maketriangle(c2p1, c2p2, c2p3);
+                tri2s[1] = maketriangle(c2p4, c2p3, c2p2);
+            } else {
+                tri1s[1] = maketriangle(c1p4, c1p2, c1p3);
+                tri2s[0] = maketriangle(c2p1, c2p3, c2p2);
+                tri2s[1] = maketriangle(c2p4, c2p2, c2p3);
+            }
+        }
+        l1 = makeline(c1p1, c1p3);
+        l2 = makeline(c1p2, c1p4);
+        if (intersect(&l1, &l2, &p0)) {
+            if (windtriangle(&tri1s[0], c1p1, c1p2, c1p4)) {
+                tri1s[1] = maketriangle(c1p3, c1p4, c1p2);
+                tri2s[0] = maketriangle(c2p1, c2p2, c2p4);
+                tri2s[1] = maketriangle(c2p3, c2p4, c2p2);
+            } else {
+                tri1s[1] = maketriangle(c1p3, c1p2, c1p4);
+                tri2s[0] = maketriangle(c2p1, c2p4, c2p2);
+                tri2s[1] = maketriangle(c2p3, c2p2, c2p4);
+            }
+        }
+        l1 = makeline(c1p1, c1p2);
+        l2 = makeline(c1p3, c1p4);
+        if (intersect(&l1, &l2, &p0)) {
+            if (windtriangle(&tri1s[0], c1p1, c1p3, c1p4)) {
+                tri1s[1] = maketriangle(c1p2, c1p4, c1p3);
+                tri2s[0] = maketriangle(c2p1, c2p3, c2p4);
+                tri2s[1] = maketriangle(c2p2, c2p4, c2p3);
+            } else {
+                tri1s[1] = maketriangle(c1p2, c1p3, c1p4);
+                tri2s[0] = maketriangle(c2p1, c2p4, c2p3);
+                tri2s[1] = maketriangle(c2p2, c2p3, c2p4);
+            }
+        }
+
+        /* control points in correct orientation */
+        c1p1 = tri1s[0].p1;
+        c1p2 = tri1s[0].p2;
+        c1p3 = tri1s[0].p3;
+        c1p4 = tri1s[1].p1;
+        c2p1 = tri2s[0].p1;
+        c2p2 = tri2s[0].p2;
+        c2p3 = tri2s[0].p3;
+        c2p4 = tri2s[1].p1;
+
+        /* triangle from triangle point to side of image */
+        /* left side triangle */
+        sideTriangle(nCP, 
+                     &tri1s[2], c1p1, c1p2, c1p3, c1p4, rbl1, rtl1, 
+                     &tri2s[2], c2p1, c2p2, c2p3, c2p4, rbl2, rtl2);
+        /* top side triangle */
+        sideTriangle(nCP, 
+                     &tri1s[3], c1p1, c1p2, c1p3, c1p4, rtl1, rtr1, 
+                     &tri2s[3], c2p1, c2p2, c2p3, c2p4, rtl2, rtr2);
+        /* right side triangle */
+        sideTriangle(nCP, 
+                     &tri1s[4], c1p1, c1p2, c1p3, c1p4, rtr1, rbr1, 
+                     &tri2s[4], c2p1, c2p2, c2p3, c2p4, rtr2, rbr2);
+        /* bottom side triangle */
+        sideTriangle(nCP, 
+                     &tri1s[5], c1p1, c1p2, c1p3, c1p4, rbr1, rbl1, 
+                     &tri2s[5], c2p1, c2p2, c2p3, c2p4, rbr2, rbl2);
+
+        /*-------------------------------------------------------------------*/
+        /*        -1-      -2-        -3-      -4-        -5-      -6-       */
+        /*       1   2    1   3      1   2    1   4      1   3    1   4      */
+        /*         X        X          X        X          X        X        */
+        /*       3   4    2   4      4   3    2   3      4   2    3   2      */
+        /*-------------------------------------------------------------------*/
+
+        /* edge-corner triangles */
+        edgeTriangle(&tri1s[6], c1p1, c1p2, rtl1, rtr1, rbl1, rbr1,
+                     &tri2s[6], c2p1, c2p2, rtl2, rtr2, rbl2, rbr2);
+        edgeTriangle(&tri1s[7], c1p2, c1p4, rtl1, rtr1, rbl1, rbr1,
+                     &tri2s[7], c2p2, c2p4, rtl2, rtr2, rbl2, rbr2);
+        edgeTriangle(&tri1s[8], c1p4, c1p3, rtl1, rtr1, rbl1, rbr1,
+                     &tri2s[8], c2p4, c2p3, rtl2, rtr2, rbl2, rbr2);
+        edgeTriangle(&tri1s[9], c1p3, c1p1, rtl1, rtr1, rbl1, rbr1,
+                     &tri2s[9], c2p3, c2p1, rtl2, rtr2, rbl2, rbr2);
+        nTri = 10;
+    }
+}
+
+
+
+static void
+prepQuad(void) {
+
+/* order quad control points */
+
+    double d01, d12, d20;
+    line l1, l2;
+    point mid;
+    triangle tri;
+
+    if (nCP == 1) {
+        /* create a rectangle from top-left corner of image and control
+           point
+        */
+        quad1 = quadRect(0.0, oldCP[0].x, 0.0, oldCP[0].y);
+        quad2 = quadRect(0.0, newCP[0].x, 0.0, newCP[0].y);
+    } else if (nCP == 2) {
+        /* create a rectangle with the two points as opposite corners */
+        if ((oldCP[0].x < oldCP[1].x) && (oldCP[0].y < oldCP[1].y)) {
+            /* top-left and bottom-right */
+            quad1 = quadRect(oldCP[0].x,oldCP[1].x, oldCP[0].y, oldCP[1].y);
+        } else if ((oldCP[0].x > oldCP[1].x) && (oldCP[0].y < oldCP[1].y)) {
+            /* top-right and bottom-left */
+            quad1 = quadRect(oldCP[1].x, oldCP[0].x, oldCP[0].y, oldCP[1].y);
+        } else if ((oldCP[0].x < oldCP[1].x) && (oldCP[0].y < oldCP[1].y)) {
+            /* bottom-left and top-right */
+            quad1 = quadRect(oldCP[0].x, oldCP[1].x, oldCP[1].y, oldCP[0].y);
+        } else if ((oldCP[0].x > oldCP[1].x) && (oldCP[0].y < oldCP[1].y)) {
+            /* bottom-right and top-left */
+            quad1 = quadRect(oldCP[1].x, oldCP[0].x, oldCP[1].y, oldCP[0].y);
+        }
+        
+        if ((newCP[0].x < newCP[1].x) && (newCP[0].y < newCP[1].y)) {
+            /* top-left and bottom-right */
+            quad2 = quadRect(newCP[0].x, newCP[1].x, newCP[0].y, newCP[1].y);
+        } else if ((newCP[0].x > newCP[1].x) && (newCP[0].y < newCP[1].y)) {
+            /* top-right and bottom-left */
+            quad2 = quadRect(newCP[1].x, newCP[0].x, newCP[0].y, newCP[1].y);
+        } else if ((newCP[0].x < newCP[1].x) && (newCP[0].y < newCP[1].y)) {
+            /* bottom-left and top-right */
+            quad2 = quadRect(newCP[0].x, newCP[1].x, newCP[1].y, newCP[0].y);
+        } else if ((newCP[0].x > newCP[1].x) && (newCP[0].y < newCP[1].y)) {
+            /* bottom-right and top-left */
+            quad2 = quadRect(newCP[1].x, newCP[0].x, newCP[1].y, newCP[0].y);
+        }
+    } else {
+        if (nCP == 3) {
+            /* add the fourth corner of a parallelogram */
+            /* diagonal of the parallelogram is the two control points
+               furthest apart
+            */
+            
+            d01 = distance(newCP[0], newCP[1]);
+            d12 = distance(newCP[1], newCP[2]);
+            d20 = distance(newCP[2], newCP[0]);
+
+            if ((d01 > d12) && (d01 > d20)) {
+                oldCP[3].x = oldCP[0].x + oldCP[1].x - oldCP[2].x;
+                oldCP[3].y = oldCP[0].y + oldCP[1].y - oldCP[2].y;
+            } else
+                if (d12 > d20) {
+                    oldCP[3].x = oldCP[1].x + oldCP[2].x - oldCP[0].x;
+                    oldCP[3].y = oldCP[1].y + oldCP[2].y - oldCP[0].y;
+                } else {
+                    oldCP[3].x = oldCP[2].x + oldCP[0].x - oldCP[1].x;
+                    oldCP[3].y = oldCP[2].y + oldCP[0].y - oldCP[1].y;
+                }
+
+            if ((d01 > d12) && (d01 > d20)) {
+                newCP[3].x = newCP[0].x + newCP[1].x - newCP[2].x;
+                newCP[3].y = newCP[0].y + newCP[1].y - newCP[2].y;
+            } else
+                if (d12 > d20) {
+                    newCP[3].x = newCP[1].x + newCP[2].x - newCP[0].x;
+                    newCP[3].y = newCP[1].y + newCP[2].y - newCP[0].y;
+                } else {
+                    newCP[3].x = newCP[2].x + newCP[0].x - newCP[1].x;
+                    newCP[3].y = newCP[2].y + newCP[0].y - newCP[1].y;
+                }
+
+        } /* end nCP = 3 */
+
+        /* nCP = 3 or 4 */
+
+        /* find the intersection of the diagonals */
+        l1 = makeline(oldCP[0], oldCP[1]);
+        l2 = makeline(oldCP[2], oldCP[3]);
+        if (intersect(&l1, &l2, &mid)) {
+            quadCorner(oldCP[0], oldCP[1], oldCP[2], oldCP[3],
+                       mid, &quad1, &tri);
+        } else {
+            l1 = makeline(oldCP[0], oldCP[2]);
+            l2 = makeline(oldCP[1], oldCP[3]);
+            if (intersect(&l1, &l2, &mid))
+                quadCorner(oldCP[0], oldCP[2], oldCP[1], oldCP[3],
+                           mid, &quad1, &tri);
+            else {
+                l1 = makeline(oldCP[0], oldCP[3]);
+                l2 = makeline(oldCP[1], oldCP[2]);
+                if (intersect(&l1, &l2, &mid))
+                    quadCorner(oldCP[0], oldCP[3],
+                               oldCP[1], oldCP[2], mid, &quad1, &tri);
+                else
+                    pm_error("The four old control points don't seem "
+                             "to be corners.");
+            }
+        }
+
+        /* repeat for the "to-be" control points */
+        l1 = makeline(newCP[0], newCP[1]);
+        l2 = makeline(newCP[2], newCP[3]);
+        if (intersect(&l1, &l2, &mid))
+            quadCorner(newCP[0], newCP[1], newCP[2], newCP[3],
+                       mid, &quad2, &tri);
+        else {
+            l1 = makeline(newCP[0], newCP[2]);
+            l2 = makeline(newCP[1], newCP[3]);
+            if (intersect(&l1, &l2, &mid))
+                quadCorner(newCP[0], newCP[2], newCP[1], newCP[3],
+                           mid, &quad2, &tri);
+            else {
+                l1 = makeline(newCP[0], newCP[3]);
+                l2 = makeline(newCP[1], newCP[2]);
+                if (intersect(&l1, &l2, &mid))
+                    quadCorner(newCP[0], newCP[3],
+                               newCP[1], newCP[2], mid, &quad2, &tri);
+                else
+                    pm_error("The four new control points don't seem "
+                             "to be corners.");
+            }
+        }
+    }
+}
+
+
+
+static void
+warpTrig(point   const p2,
+         point * const p1P) {
+
+/* map target to source by triangulation */
+
+    point e1p1, e1p2, e1p3;
+    point e2p1, e2p2, e2p3;
+    line lp, le;
+    line l1, l2, l3;
+    double d1, d2, d3;
+    int i;
+
+    /* find in which triangle p2 lies */
+    for (i = 0; i < nTri; i++) {
+        if (insidetri (&tri2s[i], p2))
+            break;
+    }
+
+    if (i == nTri)
+        *p1P = makepoint(0.0, 0.0);
+    else {
+        /* where in triangle is point */
+        d1 = fabs (p2.x - tri2s[i].p1.x) + fabs (p2.y - tri2s[i].p1.y);
+        d2 = fabs (p2.x - tri2s[i].p2.x) + fabs (p2.y - tri2s[i].p2.y);
+        d3 = fabs (p2.x - tri2s[i].p3.x) + fabs (p2.y - tri2s[i].p3.y);
+
+        /* line through p1 and p intersecting with edge p2-p3 */
+        lp = makeline(tri2s[i].p1, p2);
+        le = makeline(tri2s[i].p2, tri2s[i].p3);
+        intersect (&lp, &le, &e2p1);
+
+        /* line through p2 and p intersecting with edge p3-p1 */
+        lp = makeline(tri2s[i].p2, p2);
+        le = makeline(tri2s[i].p3, tri2s[i].p1);
+        intersect (&lp, &le, &e2p2);
+
+        /* line through p3 and p intersecting with edge p1-p2 */
+        lp = makeline(tri2s[i].p3, p2);
+        le = makeline(tri2s[i].p1, tri2s[i].p2);
+        intersect (&lp, &le, &e2p3);
+
+        /* map target control points to source control points */
+        e1p1.x = tri1s[i].p2.x
+            + (e2p1.x - tri2s[i].p2.x)/(tri2s[i].p3.x - tri2s[i].p2.x) 
+            * (tri1s[i].p3.x - tri1s[i].p2.x);
+        e1p1.y = tri1s[i].p2.y
+            + (e2p1.y - tri2s[i].p2.y)/(tri2s[i].p3.y - tri2s[i].p2.y)
+            * (tri1s[i].p3.y - tri1s[i].p2.y);
+        e1p2.x = tri1s[i].p3.x
+            + (e2p2.x - tri2s[i].p3.x)/(tri2s[i].p1.x - tri2s[i].p3.x)
+            * (tri1s[i].p1.x - tri1s[i].p3.x);
+        e1p2.y = tri1s[i].p3.y
+            + (e2p2.y - tri2s[i].p3.y)/(tri2s[i].p1.y - tri2s[i].p3.y)
+            * (tri1s[i].p1.y - tri1s[i].p3.y);
+        e1p3.x = tri1s[i].p1.x
+            + (e2p3.x - tri2s[i].p1.x)/(tri2s[i].p2.x - tri2s[i].p1.x)
+            * (tri1s[i].p2.x - tri1s[i].p1.x);
+        e1p3.y = tri1s[i].p1.y
+            + (e2p3.y - tri2s[i].p1.y)/(tri2s[i].p2.y - tri2s[i].p1.y)
+            * (tri1s[i].p2.y - tri1s[i].p1.y);
+
+        /* intersect grid lines in source */
+        l1 = makeline(tri1s[i].p1, e1p1);
+        l2 = makeline(tri1s[i].p2, e1p2);
+        l3 = makeline(tri1s[i].p3, e1p3);
+
+        if ((d1 < d2) && (d1 < d3))
+            intersect (&l2, &l3, p1P);
+        else if (d2 < d3)
+            intersect (&l1, &l3, p1P);
+        else
+            intersect (&l1, &l2, p1P);
+    }
+}
+
+
+
+static void
+warpQuad(point   const p2,
+         point * const p1P) {
+
+/* map target to source for quad control points */
+
+    point h2, v2;
+    point c1tl, c1tr, c1bl, c1br;
+    point c2tl, c2tr, c2bl, c2br;
+    point e1t, e1b, e1l, e1r;
+    point e2t, e2b, e2l, e2r;
+    line l2t, l2b, l2l, l2r;
+    line lh, lv;
+
+    c1tl = quad1.tl;
+    c1tr = quad1.tr;
+    c1bl = quad1.bl;
+    c1br = quad1.br;
+       
+    c2tl = quad2.tl;
+    c2tr = quad2.tr;
+    c2bl = quad2.bl;
+    c2br = quad2.br;
+
+    l2t = makeline(c2tl, c2tr);
+    l2b = makeline(c2bl, c2br);
+    l2l = makeline(c2tl, c2bl);
+    l2r = makeline(c2tr, c2br);
+
+    /* find intersections of lines through control points */
+    intersect (&l2t, &l2b, &h2);
+    intersect (&l2l, &l2r, &v2);
+
+    /* find intersections of axes through P with control point box */
+    lv = makeline(p2, v2);
+    intersect (&l2t, &lv, &e2t);
+    intersect (&l2b, &lv, &e2b);
+
+    lh = makeline(p2, h2);
+    intersect (&l2l, &lh, &e2l);
+    intersect (&l2r, &lh, &e2r);
+
+    /* map target control points to source control points */
+    e1t.x = c1tl.x + (e2t.x - c2tl.x)/(c2tr.x - c2tl.x) * (c1tr.x - c1tl.x);
+    if (c1tl.y == c1tr.y)
+        e1t.y = c1tl.y;
+    else
+        e1t.y =
+            c1tl.y + (e2t.x - c2tl.x)/(c2tr.x - c2tl.x) * (c1tr.y - c1tl.y);
+
+    e1b.x = c1bl.x + (e2b.x - c2bl.x)/(c2br.x - c2bl.x) * (c1br.x - c1bl.x);
+    if (c1bl.y == c1br.y)
+        e1b.y = c1bl.y;
+    else
+        e1b.y =
+            c1bl.y + (e2b.x - c2bl.x)/(c2br.x - c2bl.x) * (c1br.y - c1bl.y);
+
+    if (c1tl.x == c1bl.x)
+        e1l.x = c1tl.x;
+    else
+        e1l.x =
+            c1tl.x + (e2l.y - c2tl.y)/(c2bl.y - c2tl.y) * (c1bl.x - c1tl.x);
+    e1l.y = c1tl.y + (e2l.y - c2tl.y)/(c2bl.y - c2tl.y) * (c1bl.y - c1tl.y);
+
+    if (c1tr.x == c1br.x)
+        e1r.x = c1tr.x;
+    else
+        e1r.x
+            = c1tr.x + (e2r.y - c2tr.y)/(c2br.y - c2tr.y) * (c1br.x - c1tr.x);
+    e1r.y = c1tr.y + (e2r.y - c2tr.y)/(c2br.y - c2tr.y) * (c1br.y - c1tr.y);
+
+    /* intersect grid lines in source */
+    lv = makeline(e1t, e1b);
+    lh = makeline(e1l, e1r);
+    intersect (&lh, &lv, p1P);
+}
+
+
+
+static void
+setGlobalCP(struct cmdlineInfo const cmdline) {
+
+    unsigned int i;
+
+    for (i = 0; i < cmdline.nCP; ++i) {
+        oldCP[i] = cmdline.oldCP[i];
+        newCP[i] = cmdline.newCP[i];
+    }
+    nCP = cmdline.nCP;
+    for (i = nCP; i < ARRAY_SIZE(oldCP); ++i)
+        oldCP[i] = makepoint(-1.0, -1.0);
+    for (i = nCP; i < ARRAY_SIZE(newCP); ++i)
+        newCP[i] = makepoint(-1.0, -1.0);
+}
+
+
+
+static void
+createWhiteTuple(const struct pam * const pamP,
+                 tuple *            const tupleP) {
+
+    tuple white;
+    unsigned int plane;
+
+    white = pnm_allocpamtuple(pamP);
+
+    for (plane = 0; plane < pamP->depth; ++plane)
+        white[plane] = pamP->maxval;
+
+    *tupleP = white;
+}
+
+
+
+static void
+makeAllWhite(struct pam * const pamP,
+             tuple **     const tuples) {
+
+    tuple white;
+    unsigned int row;
+
+    createWhiteTuple(pamP, &white);
+
+    for (row = 0; row < pamP->height; ++row)  {
+        unsigned int col;
+        for (col = 0; col < pamP->width; ++col)
+            pnm_assigntuple(pamP, tuples[row][col], white);
+    }
+
+    pnm_freepamtuple(white);
+}
+
+
+
+static sample
+pix(tuple **     const tuples,
+    unsigned int const width,
+    unsigned int const height,
+    point        const p1,
+    unsigned int const plane,
+    bool         const linear) {
+
+    point p;
+    double pix;
+
+    p.x = p1.x + 1E-3;
+    p.y = p1.y + 1E-3;
+
+    if (p.x < 0.0)
+        p.x = 0.0;
+    if (p.x > (double) width - 1.0)
+        p.x = (double) width - 1.0;
+    if (p.y < 0.0)
+        p.y = 0.0;
+    if (p.y > (double) height - 1.0)
+        p.y = (double) height - 1.0;
+
+    if (!linear) {
+        pix = tuples
+            [(int) floor(p.y + 0.5)]
+            [(int) floor(p.x + 0.5)][plane];
+    } else {
+        double const rx = p.x - floor(p.x);
+        double const ry = p.y - floor(p.y);
+        pix = 0.0;
+        if (((int) floor(p.x) >= 0) && ((int) floor(p.x) < width) &&
+            ((int) floor(p.y) >= 0) && ((int) floor(p.y) < height)) {
+            pix += (1.0 - rx) * (1.0 - ry) 
+                * tuples[(int) floor(p.y)][(int) floor(p.x)][plane];
+        }
+        if (((int) floor(p.x) + 1 >= 0) && ((int) floor(p.x) + 1 < width) &&
+            ((int) floor(p.y) >= 0) && ((int) floor(p.y) < height)) {
+            pix += rx * (1.0 - ry) 
+                * tuples[(int) floor(p.y)][(int) floor(p.x) + 1][plane];
+        }
+        if (((int) floor(p.x) >= 0) && ((int) floor(p.x) < width) &&
+            ((int) floor(p.y) + 1 >= 0) && ((int) floor(p.y) + 1 < height)) {
+            pix += (1.0 - rx) * ry 
+                * tuples[(int) floor(p.y) + 1][(int) floor(p.x)][plane];
+        }
+        if (((int) floor(p.x) + 1 >= 0) && ((int) floor(p.x) + 1 < width) &&
+            ((int) floor(p.y) + 1 >= 0) && ((int) floor(p.y) + 1 < height)) {
+            pix += rx * ry 
+                * tuples[(int) floor(p.y) + 1][(int) floor(p.x) + 1][plane];
+        }
+    }
+
+    return (int) floor(pix);
+}
+
+
+
+int
+main(int argc, const char ** const argv) {
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    struct pam inpam, outpam;
+    tuple ** inTuples;
+    tuple ** outTuples;
+    unsigned int p2y;
+  
+    pm_proginit(&argc, argv);
+
+    parseCmdline(argc, argv, &cmdline);
+
+    setGlobalCP(cmdline);
+
+    srand(cmdline.randseedSpec ? cmdline.randseed : time(NULL));
+
+    ifP = pm_openr(cmdline.fileName);
+
+    inTuples = pnm_readpam(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    outpam = inpam;  /* initial value */
+    outpam.file = stdout;
+
+    outTuples = pnm_allocpamarray(&outpam);
+
+    pnm_createBlackTuple(&outpam, &black);
+
+    makeAllWhite(&outpam, outTuples);
+
+    if (cmdline.tri)
+        prepTrig(inpam.width, inpam.height);
+    if (cmdline.quad)
+        prepQuad();
+
+    for (p2y = 0; p2y < inpam.height; ++p2y) {
+        unsigned int p2x;
+        for (p2x = 0; p2x < inpam.width; ++p2x) {
+            point p1, p2;
+            unsigned int plane;
+
+            p2 = makepoint(p2x, p2y);
+            if (cmdline.quad)
+                warpQuad(p2, &p1);
+            if (cmdline.tri)
+                warpTrig(p2, &p1);
+
+            for (plane = 0; plane < inpam.depth; ++plane) {
+                outTuples[p2y][p2x][plane] =
+                    pix(inTuples, inpam.width, inpam.height, p1, plane,
+                        cmdline.linear);
+            }
+        }
+    }
+
+    if (cmdline.frame) {
+        if (cmdline.quad) {
+            drawExtendedLine(&outpam, outTuples, quad2.tl, quad2.tr);
+            drawExtendedLine(&outpam, outTuples, quad2.bl, quad2.br);
+            drawExtendedLine(&outpam, outTuples, quad2.tl, quad2.bl);
+            drawExtendedLine(&outpam, outTuples, quad2.tr, quad2.br);
+        }
+        if (cmdline.tri) {
+            unsigned int i;
+            for (i = 0; i < nTri; ++i)
+                drawClippedTriangle(&outpam, outTuples, tri2s[i]);
+        }
+    }
+
+    pnm_writepam(&outpam, outTuples);
+
+    pnm_freepamtuple(black);
+    pnm_freepamarray(outTuples, &outpam);
+    pnm_freepamarray(inTuples, &inpam);
+
+    pm_close(ifP);
+    pm_close(stdout);
+
+    return 0;
+}
+
+
+
diff --git a/editor/pamscale.c b/editor/pamscale.c
index 0abbc205..7c6ee256 100644
--- a/editor/pamscale.c
+++ b/editor/pamscale.c
@@ -31,9 +31,9 @@
 #include <assert.h>
 
 #include "pm_c_util.h"
-#include "pam.h"
-#include "shhopt.h"
 #include "mallocvar.h"
+#include "shhopt.h"
+#include "pam.h"
 
 
 /****************************/
@@ -52,20 +52,22 @@
 #define EPSILON 1e-7
 
 
+
 /* x^2 and x^3 helper functions */
 static __inline__ double 
-pow2 (double x)
-{
-  return x*x;
+pow2(double const x) {
+    return x * x;
 }
 
+
+
 static __inline__ double 
-pow3 (double x) 
-{
-  return x*x*x;
+pow3(double const x) {
+    return x * x * x;
 }
 
 
+
 /* box, pulse, Fourier window, */
 /* box function also know as rectangle function */
 /* 1st order (constant) b-spline */
@@ -74,14 +76,15 @@ pow3 (double x)
 #define radius_box (0.5)
 
 static double 
-filter_box (double x)
-{
-    if (x <  0.0) x = -x;
-    if (x <= 0.5) return 1.0;
-    return 0.0;
+filter_box(double const x) {
+
+    double const absx = x < 0.0 ? -x : x;
+
+    return (absx <= 0.5) ? 1.0 : 0.0;
 }
 
 
+
 /* triangle, Bartlett window, */
 /* triangle function also known as lambda function */
 /* 2nd order (linear) b-spline */
@@ -89,56 +92,66 @@ filter_box (double x)
 #define radius_triangle (1.0)
 
 static double 
-filter_triangle (double x)
-{
-    if (x <  0.0) x = -x;
-    if (x < 1.0) return 1.0-x;
-    return 0.0;
+filter_triangle(double const x) {
+
+    double const absx = x < 0.0 ? -x : x;
+
+    return absx < 1.0 ? 1.0 - absx : 0.0;
 }
 
 
+
 /* 3rd order (quadratic) b-spline */
 
 #define radius_quadratic (1.5)
 
 static double 
-filter_quadratic(double x)
-{
-    if (x <  0.0) x = -x;
-    if (x < 0.5) return 0.75-pow2(x);
-    if (x < 1.5) return 0.50*pow2(x-1.5);
-    return 0.0;
+filter_quadratic(double const x) {
+
+    double const absx = x < 0.0 ? -x : x;
+
+    return
+        absx < 0.5 ? 0.75 - pow2(absx) :
+        absx < 1.5 ? 0.50 * pow2(absx - 1.5) :
+        0.0;
 }
 
 
+
 /* 4th order (cubic) b-spline */
 
 #define radius_cubic (2.0)
 
 static double 
-filter_cubic(double x)
-{
-    if (x <  0.0) x = -x;
-    if (x < 1.0) return 0.5*pow3(x) - pow2(x) + 2.0/3.0;
-    if (x < 2.0) return pow3(2.0-x)/6.0;
-    return 0.0;
+filter_cubic(double const x) {
+
+    double const absx = x < 0.0 ? -x : x;
+
+    return
+        absx < 1.0 ? 0.5 * pow3(absx) - pow2(absx) + 2.0/3.0 :
+        absx < 2.0 ? pow3(2.0-absx)/6.0 :
+        0.0;
 }
 
 
+
 /* Catmull-Rom spline, Overhauser spline */
 
 #define radius_catrom (2.0)
 
 static double 
-filter_catrom(double x)
-{
-    if (x <  0.0) x = -x;
-    if (x < 1.0) return  1.5*pow3(x) - 2.5*pow2(x)         + 1.0;
-    if (x < 2.0) return -0.5*pow3(x) + 2.5*pow2(x) - 4.0*x + 2.0;
-    return 0.0;
+filter_catrom(double const x) {
+
+    double const absx = x < 0.0 ? -x : x;
+
+    return
+        absx < 1.0 ?  1.5 * pow3(absx) - 2.5 * pow2(absx)         + 1.0 :
+        absx < 2.0 ? -0.5 * pow3(absx) + 2.5 * pow2(absx) - 4.0 * absx + 2.0 :
+        0.0;
 }
 
 
+
 /* Mitchell & Netravali's two-param cubic */
 /* see Mitchell&Netravali,  */
 /* "Reconstruction Filters in Computer Graphics", SIGGRAPH 88 */
@@ -149,98 +162,106 @@ static double
 filter_mitchell(double x)
 {
 
-    double b = 1.0/3.0;
-    double c = 1.0/3.0;
-
-    double p0 = (  6.0 -  2.0*b         ) / 6.0;
-    double p2 = (-18.0 + 12.0*b +  6.0*c) / 6.0;
-    double p3 = ( 12.0 -  9.0*b -  6.0*c) / 6.0;
-    double q0 = (         8.0*b + 24.0*c) / 6.0;
-    double q1 = (      - 12.0*b - 48.0*c) / 6.0;
-    double q2 = (         6.0*b + 30.0*c) / 6.0;
-    double q3 = (      -      b -  6.0*c) / 6.0;
-
-    if (x <  0.0) x = -x;
-    if (x <  1.0) return p3*pow3(x) + p2*pow2(x)        + p0;
-    if (x < 2.0) return q3*pow3(x) + q2*pow2(x) + q1*x + q0;
-    return 0.0;
+    double const b = 1.0/3.0;
+    double const c = 1.0/3.0;
+
+    double const p0 = (  6.0 -  2.0*b         ) / 6.0;
+    double const p2 = (-18.0 + 12.0*b +  6.0*c) / 6.0;
+    double const p3 = ( 12.0 -  9.0*b -  6.0*c) / 6.0;
+    double const q0 = (         8.0*b + 24.0*c) / 6.0;
+    double const q1 = (      - 12.0*b - 48.0*c) / 6.0;
+    double const q2 = (         6.0*b + 30.0*c) / 6.0;
+    double const q3 = (      -      b -  6.0*c) / 6.0;
+
+    double const absx = x < 0.0 ? -x : x;
+
+    return
+        absx <  1.0 ? p3 * pow3(absx) + p2 * pow2(absx)        + p0 :
+        absx < 2.0 ? q3 * pow3(absx) + q2 * pow2(absx) + q1 * absx + q0 :
+        0.0;
 }
 
 
+
 /* Gaussian filter (infinite) */
 
 #define radius_gauss (1.25)
 
 static double 
-filter_gauss(double x)
-{
+filter_gauss(double const x) {
+
     return exp(-2.0*pow2(x)) * sqrt(2.0/M_PI);
 }
 
 
+
 /* sinc, perfect lowpass filter (infinite) */
 
 #define radius_sinc (4.0)
 
 static double 
-filter_sinc(double x)
-{
+filter_sinc(double const x) {
     /* Note: Some people say sinc(x) is sin(x)/x.  Others say it's
        sin(PI*x)/(PI*x), a horizontal compression of the former which is
        zero at integer values.  We use the latter, whose Fourier transform
        is a canonical rectangle function (edges at -1/2, +1/2, height 1).
     */
-    if (x == 0.0) return 1.0;
-    return sin(M_PI*x)/(M_PI*x);
+    return 
+        x == 0.0 ? 1.0 :
+        sin(M_PI*x)/(M_PI*x);
 }
 
 
+
 /* Bessel (for circularly symm. 2-d filt, infinite) */
 /* See Pratt "Digital Image Processing" p. 97 for Bessel functions */
 
 #define radius_bessel (3.2383)
 
 static double 
-filter_bessel(double x)
-{
-    if (x == 0.0) return M_PI/4.0;
-    return j1(M_PI*x)/(2.0*x);
+filter_bessel(double const x) {
+
+    return 
+        x == 0.0 ? M_PI/4.0 :
+        j1(M_PI * x) / (2.0 * x);
 }
 
 
+
 /* Hanning window (infinite) */
 
 #define radius_hanning (1.0)
 
 static double 
-filter_hanning(double x)
-{
-    return 0.5*cos(M_PI*x) + 0.5;
+filter_hanning(double const x) {
+
+    return 0.5 * cos(M_PI * x) + 0.5;
 }
 
 
+
 /* Hamming window (infinite) */
 
 #define radius_hamming (1.0)
 
 static double 
-filter_hamming(double x)
-{
-  return 0.46*cos(M_PI*x) + 0.54;
+filter_hamming(double const x) {
+    return 0.46 * cos(M_PI * x) + 0.54;
 }
 
 
+
 /* Blackman window (infinite) */
 
 #define radius_blackman (1.0)
 
 static double 
-filter_blackman(double x)
-{
-    return 0.5*cos(M_PI*x) + 0.08*cos(2.0*M_PI*x) + 0.42;
+filter_blackman(double const x) {
+    return 0.5 * cos(M_PI * x) + 0.08 * cos(2.0 * M_PI * x) + 0.42;
 }
 
 
+
 /* parameterized Kaiser window (infinite) */
 /* from Oppenheim & Schafer, Hamming */
 
@@ -248,8 +269,7 @@ filter_blackman(double x)
 
 /* modified zeroth order Bessel function of the first kind. */
 static double 
-bessel_i0(double x)
-{
+bessel_i0(double const x) {
   
     int i;
     double sum, y, t;
@@ -257,64 +277,70 @@ bessel_i0(double x)
     sum = 1.0;
     y = pow2(x)/4.0;
     t = y;
-    for (i=2; t>EPSILON; i++) {
+    for (i=2; t>EPSILON; ++i) {
         sum += t;
         t   *= (double)y/pow2(i);
     }
     return sum;
 }
 
+
+
 static double 
-filter_kaiser(double x)
-{
-    /* typically 4<a<9 */
+filter_kaiser(double const x) {
+    /* typically 4 < a < 9 */
     /* param a trades off main lobe width (sharpness) */
     /* for side lobe amplitude (ringing) */
   
-    double a   = 6.5;
-    double i0a = 1.0/bessel_i0(a);
+    double const a   = 6.5;
+    double const i0a = 1.0/bessel_i0(a);
   
-    return i0a*bessel_i0(a*sqrt(1.0-pow2(x)));
+    return i0a * bessel_i0(a * sqrt(1.0-pow2(x)));
 }
 
 
+
 /* normal distribution (infinite) */
 /* Normal(x) = Gaussian(x/2)/2 */
 
 #define radius_normal (1.0)
 
 static double 
-filter_normal(double x)
-{
+filter_normal(double const x) {
     return exp(-pow2(x)/2.0) / sqrt(2.0*M_PI);
-    return 0.0;
 }
 
 
+
 /* Hermite filter */
 
 #define radius_hermite  (1.0)
 
 static double 
-filter_hermite(double x)
-{
+filter_hermite(double const x) {
     /* f(x) = 2|x|^3 - 3|x|^2 + 1, -1 <= x <= 1 */
-    if (x <  0.0) x = -x;
-    if (x <  1.0) return 2.0*pow3(x) - 3.0*pow2(x) + 1.0;
-    return 0.0;
+
+    double const absx = x < 0.0 ? -x : x;
+
+    return
+        absx <  1.0 ? 2.0 * pow3(absx) - 3.0 * pow2(absx) + 1.0 :
+        0.0;
 }
 
 
+
 /* Lanczos filter */
 
 #define radius_lanczos (3.0)
 
 static double 
-filter_lanczos(double x)
-{
-    if (x <  0.0) x = -x;
-    if (x <  3.0) return filter_sinc(x) * filter_sinc(x/3.0);
-    return(0.0);
+filter_lanczos(double const x) {
+
+    double const absx = x < 0.0 ? -x : x;
+
+    return
+        x <  3.0 ? filter_sinc(absx) * filter_sinc(absx/3.0) :
+        0.0;
 }
 
 
@@ -385,7 +411,7 @@ enum scaleType {SCALE_SEPARATE, SCALE_BOXFIT, SCALE_BOXFILL, SCALE_PIXELMAX};
        size.
     */
 
-struct cmdlineInfo {
+struct CmdlineInfo {
     /* All the information the user supplied in the command line,
      * in a form easy for the program to use.
      */
@@ -458,7 +484,7 @@ processFilterOptions(unsigned int const         filterSpec,
                      const char                 filterOpt[],
                      unsigned int const         windowSpec,
                      const char                 windowOpt[],
-                     struct cmdlineInfo * const cmdlineP) {
+                     struct CmdlineInfo * const cmdlineP) {
 
     if (filterSpec) {
         filter baseFilter;
@@ -490,9 +516,35 @@ processFilterOptions(unsigned int const         filterSpec,
 
 
 static void
+parseSizeParm(const char *   const sizeString,
+              const char *   const description,
+              unsigned int * const sizeP) {
+
+    char * endptr;
+    long int sizeLong;
+
+
+    sizeLong = strtol(sizeString, &endptr, 10);
+    if (strlen(sizeString) > 0 && *endptr != '\0')
+        pm_error("%s size argument not an integer: '%s'", 
+                 description, sizeString);
+    else if (sizeLong > INT_MAX - 2)
+        pm_error("%s size argument is too large "
+                 "for computations: %ld", 
+                 description, sizeLong);
+    else if (sizeLong <= 0)
+        pm_error("%s size argument is not positive: %ld", 
+                 description, sizeLong);
+    else
+        *sizeP = (unsigned int) sizeLong;
+}        
+
+
+
+static void
 parseXyParms(int                  const argc, 
-             char **              const argv,
-             struct cmdlineInfo * const cmdlineP) {
+             const char **        const argv,
+             struct CmdlineInfo * const cmdlineP) {
 
     /* parameters are box width (columns), box height (rows), and
        optional filespec 
@@ -505,23 +557,10 @@ parseXyParms(int                  const argc,
         pm_error("Too many arguments.  With -xyfit/xyfill/xysize, "
                  "you need 2 or 3 arguments.");
     else {
-        char * endptr;
-        cmdlineP->xsize = strtol(argv[1], &endptr, 10);
-        if (strlen(argv[1]) > 0 && *endptr != '\0')
-            pm_error("horizontal size argument not an integer: '%s'", 
-                     argv[1]);
-        if (cmdlineP->xsize <= 0)
-            pm_error("horizontal size argument is not positive: %d", 
-                     cmdlineP->xsize);
-        
-        cmdlineP->ysize = strtol(argv[2], &endptr, 10);
-        if (strlen(argv[2]) > 0 && *endptr != '\0')
-            pm_error("vertical size argument not an integer: '%s'", 
-                     argv[2]);
-        if (cmdlineP->ysize <= 0)
-            pm_error("vertical size argument is not positive: %d", 
-                     cmdlineP->ysize);
-        
+        parseSizeParm(argv[1], "horizontal", &cmdlineP->xsize);
+
+        parseSizeParm(argv[2], "vertical", &cmdlineP->ysize);
+
         if (argc-1 < 3)
             cmdlineP->inputFileName = "-";
         else
@@ -533,24 +572,33 @@ parseXyParms(int                  const argc,
 
 static void
 parseScaleParms(int                   const argc, 
-                char **               const argv,
-                struct cmdlineInfo  * const cmdlineP) {
-
-    /* parameters are scale factor and optional filespec */
+                const char **         const argv,
+                struct CmdlineInfo  * const cmdlineP) {
+/*----------------------------------------------------------------------------
+   Parse the parameters as a scale factor and optional filespec
+   (e.g. 'pamscale .5' or 'pamscale .5 testimg.ppm').
+-----------------------------------------------------------------------------*/
     if (argc-1 < 1)
         pm_error("With no dimension options, you must supply at least "
                  "one parameter: the scale factor.");
     else {
         cmdlineP->xscale = cmdlineP->yscale = atof(argv[1]);
         
-        if (cmdlineP->xscale == 0.0)
+        if (cmdlineP->xscale <= 0.0)
             pm_error("The scale parameter %s is not a positive number.",
                      argv[1]);
         else {
             if (argc-1 < 2)
                 cmdlineP->inputFileName = "-";
-            else
+            else {
                 cmdlineP->inputFileName = argv[2];
+                
+                if (argc-1 > 2)
+                    pm_error("Too many arguments.  There are at most two "
+                             "arguments with this set of options: "
+                             "scale factor and input file name.  "
+                             "You specified %u", argc-1);
+            }
         }
     }
 }
@@ -559,8 +607,8 @@ parseScaleParms(int                   const argc,
 
 static void
 parseFilespecOnlyParms(int                   const argc, 
-                       char **               const argv,
-                       struct cmdlineInfo  * const cmdlineP) {
+                       const char **         const argv,
+                       struct CmdlineInfo  * const cmdlineP) {
 
     /* Only parameter allowed is optional filespec */
     if (argc-1 < 1)
@@ -572,8 +620,8 @@ parseFilespecOnlyParms(int                   const argc,
 
 static void 
 parseCommandLine(int argc, 
-                 char ** argv, 
-                 struct cmdlineInfo  * const cmdlineP) {
+                 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.  
@@ -584,9 +632,9 @@ parseCommandLine(int argc,
    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 optParseOptions3 on how to parse our options. */
+    optEntry * option_def;
     optStruct3 opt;
+        /* Instructions to pm_optParseOptions3 on how to parse our options. */
   
     unsigned int option_def_index;
     unsigned int xyfit, xyfill;
@@ -595,45 +643,34 @@ parseCommandLine(int argc,
     float xscale, yscale;
     const char *filterOpt, *window;
     unsigned int filterSpec, windowSpec;
+    unsigned int xscaleSpec, yscaleSpec, xsizeSpec, ysizeSpec;
+    unsigned int pixelsSpec, reduceSpec;
 
     MALLOCARRAY_NOFAIL(option_def, 100);
 
     option_def_index = 0;   /* incremented by OPTENT3 */
-    OPTENT3(0, "xsize",     OPT_UINT,    &xsize,     NULL,                 0);
-    OPTENT3(0, "width",     OPT_UINT,    &xsize,     NULL,                 0);
-    OPTENT3(0, "ysize",     OPT_UINT,    &ysize,     NULL,                 0);
-    OPTENT3(0, "height",    OPT_UINT,    &ysize,     NULL,                 0);
-    OPTENT3(0, "xscale",    OPT_FLOAT,   &xscale,    NULL,                 0);
-    OPTENT3(0, "yscale",    OPT_FLOAT,   &yscale,    NULL,                 0);
-    OPTENT3(0, "pixels",    OPT_UINT,    &pixels,    NULL,                 0);
-    OPTENT3(0, "reduce",    OPT_UINT,    &reduce,    NULL,                 0);
+    OPTENT3(0, "xsize",     OPT_UINT,    &xsize,     &xsizeSpec,           0);
+    OPTENT3(0, "width",     OPT_UINT,    &xsize,     &xsizeSpec,           0);
+    OPTENT3(0, "ysize",     OPT_UINT,    &ysize,     &ysizeSpec,           0);
+    OPTENT3(0, "height",    OPT_UINT,    &ysize,     &ysizeSpec,           0);
+    OPTENT3(0, "xscale",    OPT_FLOAT,   &xscale,    &xscaleSpec,          0);
+    OPTENT3(0, "yscale",    OPT_FLOAT,   &yscale,    &yscaleSpec,          0);
+    OPTENT3(0, "pixels",    OPT_UINT,    &pixels,    &pixelsSpec,          0);
+    OPTENT3(0, "reduce",    OPT_UINT,    &reduce,    &reduceSpec,          0);
     OPTENT3(0, "xysize",    OPT_FLAG,    NULL,       &xyfit,               0);
     OPTENT3(0, "xyfit",     OPT_FLAG,    NULL,       &xyfit,               0);
     OPTENT3(0, "xyfill",    OPT_FLAG,    NULL,       &xyfill,              0);
-    OPTENT3(0, "verbose",   OPT_FLAG,    NULL,       &cmdlineP->verbose,  0);
+    OPTENT3(0, "verbose",   OPT_FLAG,    NULL,       &cmdlineP->verbose,   0);
     OPTENT3(0, "filter",    OPT_STRING,  &filterOpt, &filterSpec,          0);
     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, "nomix",     OPT_FLAG,    NULL,       &cmdlineP->nomix,     0);
+    OPTENT3(0, "linear",    OPT_FLAG,    NULL,       &cmdlineP->linear,    0);
   
-    /* Set the defaults. -1 = unspecified */
-
-    /* (Now that we're using ParseOptions3, we don't have to do this -1
-     * nonsense, but we don't want to risk screwing these complex 
-     * option compatibilities up, so we'll convert that later.
-     */
-    xsize = -1;
-    ysize = -1;
-    xscale = -1.0;
-    yscale = -1.0;
-    pixels = -1;
-    reduce = -1;
-    
     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 */
 
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0 );
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
     /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (cmdlineP->nomix && filterSpec) 
@@ -642,38 +679,38 @@ parseCommandLine(int argc,
     processFilterOptions(filterSpec, filterOpt, windowSpec, window,
                          cmdlineP);
 
-    if (xsize == 0)
+    if (xsizeSpec && xsize == 0)
         pm_error("-xsize/width must be greater than zero.");
-    if (ysize == 0)
+    if (ysizeSpec && ysize == 0)
         pm_error("-ysize/height must be greater than zero.");
-    if (xscale != -1.0 && xscale <= 0.0)
+    if (xscaleSpec && xscale <= 0.0)
         pm_error("-xscale must be greater than zero.");
-    if (yscale != -1.0 && yscale <= 0.0)
+    if (yscaleSpec && yscale <= 0.0)
         pm_error("-yscale must be greater than zero.");
-    if (reduce <= 0 && reduce != -1)
+    if (reduceSpec && reduce <= 0)
         pm_error("-reduce must be greater than zero.");
 
-    if (xsize != -1 && xscale != -1)
+    if (xsizeSpec && xscaleSpec)
         pm_error("Cannot specify both -xsize/width and -xscale.");
-    if (ysize != -1 && yscale != -1)
+    if (ysizeSpec && yscaleSpec)
         pm_error("Cannot specify both -ysize/height and -yscale.");
     
     if ((xyfit || xyfill) &&
-        (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 || 
-         reduce != -1 || pixels != -1) )
+        (xsizeSpec || xscaleSpec || ysizeSpec || yscaleSpec || 
+         reduceSpec || pixelsSpec) )
         pm_error("Cannot specify -xyfit/xyfill/xysize with other "
                  "dimension options.");
     if (xyfit && xyfill)
         pm_error("Cannot specify both -xyfit and -xyfill");
-    if (pixels != -1 && 
-        (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 ||
-         reduce != -1) )
+    if (pixelsSpec && 
+        (xsizeSpec || xscaleSpec || ysizeSpec || yscaleSpec ||
+         reduceSpec) )
         pm_error("Cannot specify -pixels with other dimension options.");
-    if (reduce != -1 && 
-        (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1) )
+    if (reduceSpec && 
+        (xsizeSpec || xscaleSpec || ysizeSpec || yscaleSpec) )
         pm_error("Cannot specify -reduce with other dimension options.");
 
-    if (pixels == 0)
+    if (pixelsSpec && pixels == 0)
         pm_error("-pixels must be greater than zero");
 
     /* Get the program parameters */
@@ -681,7 +718,7 @@ parseCommandLine(int argc,
     if (xyfit || xyfill) {
         cmdlineP->scaleType = xyfit ? SCALE_BOXFIT : SCALE_BOXFILL;
         parseXyParms(argc, argv, cmdlineP);
-    } else if (reduce != -1) {
+    } else if (reduceSpec) {
         cmdlineP->scaleType = SCALE_SEPARATE;
         parseFilespecOnlyParms(argc, argv, cmdlineP);
         cmdlineP->xsize = cmdlineP->ysize = 0;
@@ -689,44 +726,50 @@ parseCommandLine(int argc,
             ((double) 1.0) / ((double) reduce);
         pm_message("reducing by %d gives scale factor of %f.", 
                    reduce, cmdlineP->xscale);
-    } else if (pixels != -1) {
+    } else if (pixelsSpec) {
         cmdlineP->scaleType = SCALE_PIXELMAX;
         parseFilespecOnlyParms(argc, argv, cmdlineP);
         cmdlineP->pixels = pixels;
-    } else if (xsize == -1 && xscale == -1 && ysize == -1 && yscale == -1
-               && pixels == -1 && reduce == -1) {
+    } else if (xsizeSpec || xscaleSpec || ysizeSpec || yscaleSpec) {
         cmdlineP->scaleType = SCALE_SEPARATE;
-        parseScaleParms(argc, argv, cmdlineP);
-        cmdlineP->xsize = cmdlineP->ysize = 0;
+        parseFilespecOnlyParms(argc, argv, cmdlineP);
+        cmdlineP->xsize = xsizeSpec ? xsize : 0;
+        cmdlineP->ysize = ysizeSpec ? ysize : 0;
+        cmdlineP->xscale = xscaleSpec ? xscale : 0.0;
+        cmdlineP->yscale = yscaleSpec ? yscale : 0.0;
     } else {
         cmdlineP->scaleType = SCALE_SEPARATE;
-        parseFilespecOnlyParms(argc, argv, cmdlineP);
-        cmdlineP->xsize = xsize == -1 ? 0 : xsize;
-        cmdlineP->ysize = ysize == -1 ? 0 : ysize;
-        cmdlineP->xscale = xscale == -1.0 ? 0.0 : xscale;
-        cmdlineP->yscale = yscale == -1.0 ? 0.0 : yscale;
+        parseScaleParms(argc, argv, cmdlineP);
+        cmdlineP->xsize = cmdlineP->ysize = 0;
     }
 }
 
 
 
 static void 
-computeOutputDimensions(struct cmdlineInfo  const cmdline, 
-                        int                 const rows, 
-                        int                 const cols, 
-                        int *               const newrowsP, 
-                        int *               const newcolsP) {
+computeOutputDimensions(struct CmdlineInfo  const cmdline, 
+                        unsigned int        const cols, 
+                        unsigned int        const rows, 
+                        int *               const newcolsP,
+                        int *               const newrowsP) { 
+
+    double newcolsD, newrowsD;
+        /* Intermediate calculation of the output dimensions, in double
+           precision floating point to avoid arithmetic overflow.
+        */
+    unsigned int newcols, newrows;
+        /* The output dimensions we return */
 
     switch(cmdline.scaleType) {
     case SCALE_PIXELMAX: {
         if (rows * cols <= cmdline.pixels) {
-            *newrowsP = rows;
-            *newcolsP = cols;
+            newrowsD = rows;
+            newcolsD = cols;
         } else {
             const double scale =
                 sqrt( (float) cmdline.pixels / ((float) cols * (float) rows));
-            *newrowsP = rows * scale;
-            *newcolsP = cols * scale;
+            newrowsD = rows * scale;
+            newcolsD = cols * scale;
         }
     } break;
     case SCALE_BOXFIT:
@@ -739,48 +782,56 @@ computeOutputDimensions(struct cmdlineInfo  const cmdline,
              cmdline.scaleType == SCALE_BOXFIT) ||
             (box_aspect_ratio < aspect_ratio &&
              cmdline.scaleType == SCALE_BOXFILL)) {
-            *newrowsP = cmdline.ysize;
-            *newcolsP = *newrowsP * aspect_ratio + 0.5;
+            newrowsD = cmdline.ysize;
+            newcolsD = newrowsD * aspect_ratio;
         } else {
-            *newcolsP = cmdline.xsize;
-            *newrowsP = *newcolsP / aspect_ratio + 0.5;
+            newcolsD = cmdline.xsize;
+            newrowsD = newcolsD / aspect_ratio;
         }
     } break;
     case SCALE_SEPARATE: {
         if (cmdline.xsize)
-            *newcolsP = cmdline.xsize;
+            newcolsD = cmdline.xsize;
         else if (cmdline.xscale)
-            *newcolsP = cmdline.xscale * cols + .5;
+            newcolsD = cmdline.xscale * cols;
         else if (cmdline.ysize)
-            *newcolsP = cols * ((float) cmdline.ysize/rows) +.5;
+            newcolsD = cols * ((float) cmdline.ysize/rows);
         else
-            *newcolsP = cols;
+            newcolsD = cols;
 
         if (cmdline.ysize)
-            *newrowsP = cmdline.ysize;
+            newrowsD = cmdline.ysize;
         else if (cmdline.yscale)
-            *newrowsP = cmdline.yscale * rows +.5;
+            newrowsD = cmdline.yscale * rows;
         else if (cmdline.xsize)
-            *newrowsP = rows * ((float) cmdline.xsize/cols) +.5;
+            newrowsD = rows * ((float) cmdline.xsize/cols);
         else
-            *newrowsP = rows;
+            newrowsD = rows;
     }
     }
-
-    /* If the calculations above yielded (due to rounding) a zero 
-     * dimension, we fudge it up to 1.  We do this rather than considering
-     * it a specification error (and dying) because it's friendlier to 
-     * automated processes that work on arbitrary input.  It saves them
-     * having to check their numbers to avoid catastrophe.
-     */
-  
-    if (*newcolsP < 1) *newcolsP = 1;
-    if (*newrowsP < 1) *newrowsP = 1;
+    
+    /* If the rounding yields a zero dimension, we fudge it up to 1.  We do
+       this rather than considering it a specification error (and dying)
+       because it's friendlier to automated processes that work on arbitrary
+       input.  It saves them having to check their numbers to avoid
+       catastrophe.
+    */
+    newcols = MAX(1, ROUNDU(newcolsD));
+    newrows = MAX(1, ROUNDU(newrowsD));
+
+    if (newcols > INT_MAX - 2)
+        pm_error("output image width (%u) too large for computations",
+                 newcols);
+    if (newrows > INT_MAX - 2)
+        pm_error("output image height (%u) too large for computation",
+                 newrows);
+
+    *newcolsP = newcols;
+    *newrowsP = newrows;
 }
 
 
 
-
 /****************************/
 /****************************/
 /******* resampling *********/
@@ -876,7 +927,7 @@ typedef struct {
            window.  The index order is NOT the order of the rows in the
            image.  E.g. line[0] isn't always the topmost row of the window.
            Rather, the rows are arranged in a cycle and you have to know
-           indpendently where the topmost one is.  E.g. the rows of a 5
+           independently where the topmost one is.  E.g. the rows of a 5
            line window with topmost row at index 3 might be:
 
               line[0] = Row 24
@@ -974,12 +1025,12 @@ createWeightList(unsigned int          const targetPos,
    row.  Assume 'filter' is a triangle function -- 1 at 0, sloping
    down to 0 at -1 and 1.
 
-   Now assume that the scale factor is 2 -- the target image will be
-   twice the size of the source image.  That means the two-pixel-wide
-   window of the source row that affects Column 5 of the target row
-   (centered at target position 5.5) goes from position 1.75 to
-   3.75, centered at 2.75.  That means the window covers 1/4 of
-   Column 1, all of Column 2, and 3/4 of Column 3 of the source row.
+   Now assume that the scale factor is 2 -- the target image will be twice the
+   size of the source image.  That means the two-pixel-wide window of the
+   source row that affects Column 5 of the target row, which is centered at
+   target position 5.5, is centered at source position 5.5/2 = 2.75.  So it
+   goes from source position 1.75 to 3.75.  That means the window covers 1/4
+   of Column 1, all of Column 2, and 3/4 of Column 3 of the source row.
 
    We want to calculate 3 weights, one to be applied to each source pixel
    in computing the target pixel.  Ideally, we would compute the average
@@ -992,22 +1043,22 @@ createWeightList(unsigned int          const targetPos,
    -.875 from the center of the window, we assume a constant function
    value of triangle(-.875), which equals .125.  For the 2.00-3.00
    region, we get triangle(-.25) = .75.  For the 3.00-3.75 region, we
-   get triangle(.125) = .875.  So the weights for the 3 regions, which
+   get triangle(.625) = .375.  So the weights for the 3 regions, which
    we get by multiplying this constant function value by the width of
    the region and normalizing so they add up to 1 are:
 
-      Source Column 1:  .125*.25 / 1.4375 = .022
-      Source Column 2:  .75*1.00 / 1.4375 = .521
-      Source Column 3:  .875*.75 / 1.4375 = .457
+      Source Column 1:  .125*.25 / 1.0625 = .029
+      Source Column 2:  .75*1.00 / 1.0625 = .706
+      Source Column 3:  .375*.75 / 1.0625 = .265
 
    These are the weights we return.  Thus, if we assume that the source
    pixel 1 has value 10, source pixel 2 has value 20, and source pixel 3
    has value 30, Caller would compute target pixel 5's value as
 
-      10*.022 + 20*.521 + 30*.457 = 24
+      10*.029 + 20*.706 + 30*.265 = 22
 
 -----------------------------------------------------------------------------*/
-    /* 'windowCenter', is the continous position within the source of
+    /* 'windowCenter', is the continuous position within the source of
        the center of the window that will influence target pixel
        'targetPos'.  'left' and 'right' are the edges of the window.
        'leftPixel' and 'rightPixel' are the pixel positions of the
@@ -1092,7 +1143,7 @@ createWeightListSet(unsigned int          const sourceSize,
       
    2) Filter out any frequencies that are too high to be captured
       by the new sampling -- i.e. frequencies above 1/2 the new
-      sample rate.  This is the information we must lose due to low
+      sample rate.  This is the information we must lose because of low
       sample rate.
       
    3) Sample the result at the new sample rate.
@@ -1341,7 +1392,7 @@ outputOneResampledRow(const struct pam * const outpamP,
 -----------------------------------------------------------------------------*/
     unsigned int col;
 
-    bool haveOpacity;           /* There is an opacity plane */
+    int haveOpacity;           /* There is an opacity plane */
     unsigned int opacityPlane;  /* Plane number of opacity plane, if any */
 
     pnm_getopacity(outpamP, &haveOpacity, &opacityPlane);
@@ -1601,11 +1652,11 @@ horizontalScale(tuplen *     const inputtuplenrow,
                 float        const xscale,
                 float *      const stretchP) {
 /*----------------------------------------------------------------------------
-  Take the input row 'inputtuplenrow', decribed by *inpamP, and scale
+  Take the input row 'inputtuplenrow', described by *inpamP, and scale
   it by a factor of 'xscale', to create the output row 'newtuplenrow',
   described by *outpamP.
 
-  Due to arithmetic imprecision, we may have to stretch slightly the
+  Because of arithmetic imprecision, we may have to stretch slightly the
   contents of the last pixel of the output row to make a full pixel.
   Return as *stretchP the fraction of a pixel by which we had to
   stretch in this way.
@@ -1646,7 +1697,7 @@ horizontalScale(tuplen *     const inputtuplenrow,
         }
         /* There's not enough left in the current input pixel to fill up 
            a whole output column, so just accumulate the remainder of the
-           pixel into the current output column.  Due to rounding, we may
+           pixel into the current output column.  Because of rounding, we may
            have a tiny bit of pixel left and have run out of output pixels.
            In that case, we throw away what's left.
         */
@@ -1799,7 +1850,7 @@ issueStretchWarning(bool   const verbose,
        row.  
     */
     if (verbose)
-        pm_message("%f of bottom row stretched due to "
+        pm_message("%f of bottom row stretched because of "
                    "arithmetic imprecision", 
                    fracrowtofill);
 }
@@ -1833,7 +1884,7 @@ scaleHorizontallyAndOutputRow(struct pam *             const inpamP,
                         xscale, &stretch);
             
         if (verbose && row == 0)
-            pm_message("%f of right column stretched due to "
+            pm_message("%f of right column stretched because of "
                        "arithmetic imprecision", 
                        stretch);
             
@@ -2085,24 +2136,18 @@ scaleWithoutMixing(const struct pam * const inpamP,
 
 
 
-int
-main(int argc, char **argv ) {
-
-    struct cmdlineInfo cmdline;
-    FILE* ifP;
+static void
+pamscale(FILE *             const ifP,
+         FILE *             const ofP,
+         struct CmdlineInfo const cmdline) {
+    
     struct pam inpam, outpam;
     float xscale, yscale;
 
-    pnm_init(&argc, argv);
-
-    parseCommandLine(argc, argv, &cmdline);
-
-    ifP = pm_openr(cmdline.inputFileName);
-
     pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
 
     outpam = inpam;  /* initial value */
-    outpam.file = stdout;
+    outpam.file = ofP;
 
     if (PNM_FORMAT_TYPE(inpam.format) == PBM_TYPE && !cmdline.nomix) {
         outpam.format = PGM_TYPE;
@@ -2113,8 +2158,8 @@ main(int argc, char **argv ) {
         outpam.maxval = inpam.maxval;
     }
 
-    computeOutputDimensions(cmdline, inpam.height, inpam.width,
-                            &outpam.height, &outpam.width);
+    computeOutputDimensions(cmdline, inpam.width, inpam.height, 
+                            &outpam.width, &outpam.height);
 
     xscale = (float) outpam.width / inpam.width;
     yscale = (float) outpam.height / inpam.height;
@@ -2151,6 +2196,29 @@ main(int argc, char **argv ) {
                  cmdline.windowFunction, cmdline.verbose,
                  cmdline.linear);
     }
+}
+
+
+
+int
+main(int argc, const char **argv ) {
+
+    struct CmdlineInfo cmdline;
+    FILE * ifP;
+    int eof;
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    eof = FALSE;
+    while (!eof) {
+        pamscale(ifP, stdout, cmdline);
+        pnm_nextimage(ifP, &eof);
+    }
+
     pm_close(ifP);
     pm_close(stdout);
     
diff --git a/editor/pamsistoaglyph.c b/editor/pamsistoaglyph.c
index 1c92658c..6b093520 100644
--- a/editor/pamsistoaglyph.c
+++ b/editor/pamsistoaglyph.c
@@ -83,7 +83,7 @@ parseCommandLine( int argc, const char ** const argv,
     opt.short_allowed = 1;
     opt.allowNegNum = 0;
 
-    optParseOptions3( &argc, (char **)argv, opt, sizeof(opt), 0 );
+    pm_optParseOptions3( &argc, (char **)argv, opt, sizeof(opt), 0 );
 
     if (argc-1 < 1)
         cmdlineP->inputFilename = "-";
@@ -247,12 +247,19 @@ findRegionEyeSeparation( gray ** const grayArray,
 
 
 
+#ifndef LITERAL_FN_DEF_MATCH
+static qsort_comparison_fn compareInts;
+#endif
+
 static int
-compare_ints( const void * const firstP,
-              const void * const secondP ) {
+compareInts(const void * const a,
+            const void * const b) {
+
+    const int * const firstP = a;
+    const int * const secondP = b;
 
-    int const first  = *(int *)firstP;
-    int const second = *(int *)secondP;
+    int const first  = *firstP;
+    int const second = *secondP;
 
     int retval;
 
@@ -311,7 +318,7 @@ findEyeSeparation( struct pam *  const pamP,
                 rowSeparation[numValidRows++] = sep;
         }
         if (numValidRows > 0) {
-            qsort( rowSeparation, numValidRows, sizeof(int), compare_ints );
+            qsort(rowSeparation, numValidRows, sizeof(int), compareInts);
             bestSeparation = rowSeparation[numValidRows/2];
         }
         free( rowSeparation );
diff --git a/editor/pamstretch.c b/editor/pamstretch.c
index 87c105f9..8980dd0b 100644
--- a/editor/pamstretch.c
+++ b/editor/pamstretch.c
@@ -87,7 +87,7 @@ parse_command_line(int argc, char ** argv,
     opt.short_allowed = FALSE; /* We have some short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdline_p and others. */
 
     if (blackedge && dropedge) 
diff --git a/editor/pamthreshold.c b/editor/pamthreshold.c
index c5f48147..8d28bc4a 100644
--- a/editor/pamthreshold.c
+++ b/editor/pamthreshold.c
@@ -103,15 +103,15 @@ parseGeometry(const char *   const wxl,
 
     char * const xPos = strchr(wxl, 'x');
     if (!xPos)
-        asprintfN(errorP, "There is no 'x'.  It should be WIDTHxHEIGHT");
+        pm_asprintf(errorP, "There is no 'x'.  It should be WIDTHxHEIGHT");
     else {
         *widthP  = atoi(wxl);
         *heightP = atoi(xPos + 1);
 
         if (*widthP == 0)
-            asprintfN(errorP, "Width is zero.");
+            pm_asprintf(errorP, "Width is zero.");
         else if (*heightP == 0)
-            asprintfN(errorP, "Height is zero.");
+            pm_asprintf(errorP, "Height is zero.");
         else
             *errorP = NULL;
     }
@@ -161,13 +161,13 @@ parseCommandLine(int                 argc,
     /* set the defaults */
     cmdlineP->width = cmdlineP->height = 0U;
 
-    /* set the option description for optParseOptions3 */
+    /* set the option description for pm_optParseOptions3 */
     opt.opt_table     = option_def;
     opt.short_allowed = FALSE;           /* long options only */
     opt.allowNegNum   = FALSE;           /* we have no numbers at all */
 
     /* parse commandline, change argc, argv, and *cmdlineP */
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
 
     if (cmdlineP->simple + localSpec + dualSpec > 1)
         pm_error("You may specify only one of -simple, -local, and -dual");
@@ -194,7 +194,7 @@ parseCommandLine(int                 argc,
 
         if (error) {
             pm_error("Invalid -local value '%s'.  %s", localOpt, error);
-            strfree(error);
+            pm_strfree(error);
         }
     } else
         cmdlineP->local = FALSE;
@@ -207,7 +207,7 @@ parseCommandLine(int                 argc,
 
         if (error) {
             pm_error("Invalid -dual value '%s'.  %s", dualOpt, error);
-            strfree(error);
+            pm_strfree(error);
         }
     } else
         cmdlineP->dual = FALSE;
@@ -302,7 +302,7 @@ analyzeDistribution(struct pam *          const inpamP,
         pm_error("Unable to allocate space for %lu-entry histogram",
                  inpamP->maxval+1);
 
-    /* Initialize histogram -- zero occurences of everything */
+    /* Initialize histogram -- zero occurrences of everything */
     for (i = 0; i <= inpamP->maxval; ++i)
         histogram[i] = 0;
 
@@ -376,7 +376,7 @@ computeGlobalThreshold(struct pam *         const inpamP,
    Compute the proper threshold to use for the image described by
    *inpamP, and:
 
-     'histogram' describes the frequency of occurence of the various sample
+     'histogram' describes the frequency of occurrence of the various sample
      values in the image.
 
      'globalRange' describes the range (minimum, maximum) of sample values
@@ -658,7 +658,7 @@ main(int argc, char **argv) {
     FILE * ifP; 
     struct cmdlineInfo cmdline;
     struct pam inpam, outpam;
-    bool eof;  /* No more images in input stream */
+    int eof;  /* No more images in input stream */
 
     pnm_init(&argc, argv);
 
diff --git a/editor/pamundice.c b/editor/pamundice.c
index 89d8b6b5..9a80e46d 100644
--- a/editor/pamundice.c
+++ b/editor/pamundice.c
@@ -50,7 +50,7 @@ parseCommandLine(int argc, char ** argv,
    was passed to us as the argv array.  We also trash *argv.
 -----------------------------------------------------------------------------*/
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
     
@@ -76,7 +76,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0 );
+    pm_optParseOptions3( &argc, argv, opt, sizeof(opt), 0 );
         /* Uses and sets argc, argv, and some of *cmdline_p and others. */
 
     if (!acrossSpec)
@@ -232,12 +232,12 @@ doSubstitution(const char *    const pattern,
 
             switch (pattern[inCursor]) {
             case 'a':
-                asprintfN(&substString, "%0*u", precision, file);
-                asprintfN(&desc, "file (across)");
+                pm_asprintf(&substString, "%0*u", precision, file);
+                pm_asprintf(&desc, "file (across)");
                 break;
             case 'd':
-                asprintfN(&substString, "%0*u", precision, rank);
-                asprintfN(&desc, "rank (down)");
+                pm_asprintf(&substString, "%0*u", precision, rank);
+                pm_asprintf(&desc, "rank (down)");
                 break;
             default:
                 pm_error("Unknown format specifier '%c' in input file "
@@ -249,12 +249,12 @@ doSubstitution(const char *    const pattern,
                 pm_error("%s number %u is wider than "
                          "the %u characters specified in the "
                          "input file pattern",
-                         desc, strlen(substString), precision);
+                         desc, (unsigned)strlen(substString), precision);
             else
                 buffer_addString(bufferP, substString);
             
-            strfree(desc);
-            strfree(substString);
+            pm_strfree(desc);
+            pm_strfree(substString);
 
             ++inCursor;
         }
@@ -288,7 +288,7 @@ computeInputFileName(const char *  const pattern,
             buffer_addChar(&buffer, pattern[inCursor++]);
     }
 
-    asprintfN(fileNameP, "%s", buffer.string);
+    pm_asprintf(fileNameP, "%s", buffer.string);
 
     buffer_term(&buffer);
 }
@@ -327,7 +327,7 @@ getCommonInfo(const char *   const inputFilePattern,
 
     pm_close(ifP);
 
-    strfree(fileName);
+    pm_strfree(fileName);
 }
 
 
@@ -344,7 +344,7 @@ openInputImage(const char * const inputFilePattern,
 
     retval = pm_openr(fileName);
     
-    strfree(fileName);
+    pm_strfree(fileName);
 
     return retval;
 }
@@ -491,7 +491,12 @@ openInStreams(struct pam         inpam[],
 /*----------------------------------------------------------------------------
    Open the input files for a single horizontal slice (there's one file
    for each vertical slice) and read the Netpbm headers from them.  Return
-   the pam structures to describe each.
+   the pam structures to describe each as inpam[].
+
+   Open the files for horizontal slice number 'rank', assuming there are
+   'fileCount' vertical slices (so open 'fileCount' files).  Use
+   inputFilePattern[] with each rank and file number to compute the name of
+   each file.
 -----------------------------------------------------------------------------*/
     unsigned int file;
 
@@ -507,7 +512,9 @@ openInStreams(struct pam         inpam[],
 static void
 closeInFiles(struct pam         pam[],
              unsigned int const fileCount) {
-
+/*----------------------------------------------------------------------------
+   Close the 'fileCount' input file streams represented by pam[].
+-----------------------------------------------------------------------------*/
     unsigned int file;
     
     for (file = 0; file < fileCount; ++file)
diff --git a/editor/pamwipeout.c b/editor/pamwipeout.c
new file mode 100644
index 00000000..0fff3fca
--- /dev/null
+++ b/editor/pamwipeout.c
@@ -0,0 +1,229 @@
+/* pamwipeout.c - read a bitmap and replace it with a gradient between two
+** edges
+**
+** Copyright (C) 2011 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.
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <math.h>
+#include "pm_c_util.h"
+#include "mallocvar.h"
+#include "shhopt.h"
+#include "pam.h"
+
+
+enum Direction {DIR_LR, DIR_TB};
+
+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 */
+    enum Direction direction;  /* top-bottom or left-right */
+};
+
+
+
+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 lr, tb;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "lr",     OPT_FLAG,   NULL,                  
+            &lr,       0);
+    OPTENT3(0, "tb",     OPT_FLAG,   NULL,                  
+            &tb,       0);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
+
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (!lr && !tb)
+        pm_error("You must specify either -lr or -tb");
+    else if (lr && tb)
+        pm_error("You may not specify both -lr and -tb");
+    else 
+        cmdlineP->direction = lr ? DIR_LR : DIR_TB;
+
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else {
+        cmdlineP->inputFileName = argv[1];
+        if (argc-1 > 1)
+            pm_error("Too many arguments: %u.  "
+                     "The only possible argument is the "
+                     "optional input file name", argc-1);
+    }
+}
+
+
+
+
+static void 
+wipeImgByRow (struct pam const inpam, 
+              tuple **   const tuples) {
+
+    double const h = (double) inpam.height;
+
+    unsigned int row;
+    unsigned int col;
+
+    for (row = 0; row < inpam.height; ++row)  {
+        double const y = (double) row;
+        for (col = 0; col < inpam.width; ++col)  {
+            unsigned int i;
+            for (i = 0; i < inpam.depth; ++i) {
+                sample const top = tuples[0][col][i];
+                sample const bot = tuples[inpam.height - 1][col][i];
+                tuples[row][col][i] = (int)
+                    floor(((h - y) / h)
+                          * (double) top + (y / h)
+                          * (double) bot);
+            }
+        }
+    }
+}
+
+
+
+static void 
+wipeRowByCol(struct pam const inpam, 
+             tuple **   const tuples, 
+             tuple *    const tuplerow) {
+
+    double const w = (double) inpam.width;
+
+    unsigned int col;
+
+    for (col = 0; col < inpam.width; ++col)  {
+        double const x = (double) col;
+        unsigned int i;
+        for (i = 0; i < inpam.depth; ++i) {
+            sample const lft = tuplerow[0][i];
+            sample const rgt = tuplerow[inpam.width - 1][i];
+            tuplerow[col][i] = (int)
+                floor( ((w - x) / w)
+                       * (double) lft + (x / w)
+                       * (double) rgt );
+        }
+    }
+}
+
+
+
+static void
+wipeoutTb(FILE * const ifP,
+          FILE * const ofP) {
+
+    /* top-bottom we have to read the full image */
+
+    struct pam inpam, outpam;
+    tuple ** tuples;
+    
+    tuples = pnm_readpam(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    outpam = inpam; 
+    outpam.file = ofP;
+
+    wipeImgByRow(inpam, tuples);
+
+    pnm_writepam(&outpam, tuples);
+       
+    pnm_freepamarray(tuples, &inpam);
+}
+
+
+
+static void
+wipeoutLr(FILE * const ifP,
+          FILE * const ofP) {
+    
+    /* left-right we can read row-by-row */
+
+    struct pam inpam, outpam;
+    tuple ** tuples;
+    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);
+
+        wipeRowByCol(inpam, tuples, tuplerow);
+
+        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);
+
+    switch (cmdline.direction) {
+    case DIR_TB:
+        wipeoutTb(ifP, stdout);
+        break;
+    case DIR_LR:
+        wipeoutLr(ifP, stdout);
+        break;
+    }
+
+    pm_close(ifP);
+    pm_close(stdout);
+
+    return 0;
+}
diff --git a/editor/pbmclean.c b/editor/pbmclean.c
index 65b53e1c..46e7dee6 100644
--- a/editor/pbmclean.c
+++ b/editor/pbmclean.c
@@ -1,99 +1,98 @@
-/* pbmclean.c - pixel cleaning. Remove pixel if less than n connected
- *              identical neighbours, n=1 default.
- * AJCD 20/9/90
- * stern, Fri Oct 19 00:10:38 MET DST 2001
- *     add '-white/-black' flags to restrict operation to given blobs
- */
+/*=============================================================================
+                                 pbmclean
+===============================================================================
+  Pixel cleaner:   Remove pixel if less than N connected identical neighbors
 
+=============================================================================*/
+#include <assert.h>
 #include <stdio.h>
 
 #include "pm_c_util.h"
-#include "pbm.h"
+#include "mallocvar.h"
 #include "shhopt.h"
+#include "pbm.h"
 
 struct cmdlineInfo {
     /* All the information the user supplied in the command line,
        in a form easy for the program to use.
     */
-    const char *inputFilespec;  /* Filespecs of input files */
+    const char * inputFileName;  /* File name of input file */
     bool flipWhite;
     bool flipBlack;
-    unsigned int connect;
+    unsigned int minneighbors;
     unsigned int verbose;
+    unsigned int extended;
 };
 
-#define PBM_INVERT(p) ((p) == PBM_WHITE ? PBM_BLACK : PBM_WHITE)
-
-/* input bitmap size and storage */
-static bit *inrow[3] ;
-
-#define THISROW (1)
 
-enum compass_heading {
-    WEST=0,
-    NORTHWEST=1,
-    NORTH=2,
-    NORTHEAST=3,
-    EAST=4,
-    SOUTHEAST=5,
-    SOUTH=6,
-    SOUTHWEST=7
-};
-/* compass directions from west clockwise.  Indexed by enum compass_heading */
-int const xd[] = { -1, -1,  0,  1, 1, 1, 0, -1 } ;
-int const yd[] = {  0, -1, -1, -1, 0, 1, 1,  1 } ;
 
 static void
-parseCommandLine(int argc, char ** argv,
+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.
 -----------------------------------------------------------------------------*/
     optStruct3 opt;  /* set by OPTENT3 */
-    optEntry *option_def = malloc(100*sizeof(optEntry));
+    optEntry * option_def;
     unsigned int option_def_index;
 
     unsigned int black, white;
     unsigned int minneighborsSpec;
 
+    MALLOCARRAY(option_def, 100);
+
     option_def_index = 0;   /* incremented by OPTENT3 */
-    OPTENT3(0,   "verbose", OPT_FLAG, NULL, &cmdlineP->verbose, 0);
-    OPTENT3(0,   "black", OPT_FLAG, NULL, &black, 0);
-    OPTENT3(0,   "white", OPT_FLAG, NULL, &white, 0);
-    OPTENT3(0,   "minneighbors", OPT_UINT, &cmdlineP->connect, 
+    OPTENT3(0,   "verbose",          OPT_FLAG, NULL, &cmdlineP->verbose, 0);
+    OPTENT3(0,   "black",            OPT_FLAG, NULL, &black, 0);
+    OPTENT3(0,   "white",            OPT_FLAG, NULL, &white, 0);
+    OPTENT3(0,   "minneighbors",     OPT_UINT, &cmdlineP->minneighbors, 
             &minneighborsSpec, 0);
+    OPTENT3(0,   "extended",         OPT_FLAG, NULL, &cmdlineP->extended, 0);
 
     opt.opt_table = option_def;
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = TRUE;  /* We sort of allow negative numbers as parms */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
-    if (!black && !white) {
-        cmdlineP->flipBlack = TRUE;
-        cmdlineP->flipWhite = TRUE;
+    free(option_def);
+
+    if (cmdlineP->extended) {
+        if (black && white)
+            pm_error("With -extended, you cannot specify both "
+                     "-black and -white");
+        else if (!black & !white) {
+            cmdlineP->flipBlack = TRUE;
+            cmdlineP->flipWhite = FALSE;
+        } else {
+            cmdlineP->flipBlack = !!black;
+            cmdlineP->flipWhite = !!white;
+        }
     } else {
-        cmdlineP->flipBlack = !!black;
-        cmdlineP->flipWhite = !!white;
-    }    
-
-
+        if (!black && !white) {
+            cmdlineP->flipBlack = TRUE;
+            cmdlineP->flipWhite = TRUE;
+        } else {
+            cmdlineP->flipBlack = !!black;
+            cmdlineP->flipWhite = !!white;
+        }    
+    }
     if (!minneighborsSpec) {
         /* Now we do a sleazy tour through the parameters to see if
            one is -N where N is a positive integer.  That's for
            backward compatibility, since Pbmclean used to have
            unconventional syntax where a -N option was used instead of
            the current -minneighbors option.  The only reason -N didn't
-           get processed by pm_optParseOptions3() is that it looked
+           get processed by pm_pm_optParseOptions3() is that it looked
            like a negative number parameter instead of an option.  
            If we find a -N, we make like it was a -minneighbors=N option.
         */
         int i;
         bool foundNegative;
 
-        cmdlineP->connect = 1;  /* default */
+        cmdlineP->minneighbors = 1;  /* default */
         foundNegative = FALSE;
 
         for (i = 1; i < argc; ++i) {
@@ -101,7 +100,7 @@ parseCommandLine(int argc, char ** argv,
                 argv[i-1] = argv[i];
             else {
                 if (atoi(argv[i]) < 0) {
-                    cmdlineP->connect = - atoi(argv[i]);
+                    cmdlineP->minneighbors = - atoi(argv[i]);
                     foundNegative = TRUE;
                 }
             }
@@ -111,9 +110,9 @@ parseCommandLine(int argc, char ** argv,
     }
 
     if (argc-1 < 1) 
-        cmdlineP->inputFilespec = "-";
+        cmdlineP->inputFileName = "-";
     else if (argc-1 == 1)
-        cmdlineP->inputFilespec = argv[1];
+        cmdlineP->inputFileName = argv[1];
     else
         pm_error("You specified too many arguments (%d).  The only "
                  "argument is the optional input file specification.",
@@ -122,120 +121,698 @@ parseCommandLine(int argc, char ** argv,
 
 
 
+static unsigned int
+bitpop8(unsigned char const x) {
+/*----------------------------------------------------------------------------
+   Return the number of 1 bits in 'x'
+-----------------------------------------------------------------------------*/
+static unsigned int const p[256] = {
+    0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
+    1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+    1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+    1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+    3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+    1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
+    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+    3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+    2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
+    3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+    3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
+    4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8 };
+
+    return p[x];
+}
+
 
 
-static void 
-nextrow(FILE * const ifd,
-        int    const row,
-        int    const cols,
-        int    const rows,
-        int    const format) {
+static unsigned int
+bitpop24(uint32_t const w){
 /*----------------------------------------------------------------------------
-   Advance one row in the input.
+   Return the number of 1 bits in the lower 24 bits of 'w'
+   A GCC builtin, __builtin_popcountl(), is available, but it
+   emits a call to an external function instead of inlining (GCC 4.4.3).
 
-   'row' is the row number that will be the current row.
+   This table lookup method is faster.
 -----------------------------------------------------------------------------*/
-    bit * shuffle;
+    return (bitpop8((w >> 16) & 0xff) +
+            bitpop8((w >>  8) & 0xff) +
+            bitpop8((w >>  0) & 0xff));  
+}
 
-    /* First, get the "next" row in inrow[2] if this is the very first
-       call to nextrow().
-    */
-    if (inrow[2] == NULL && row < rows) {
-        inrow[2] = pbm_allocrow(cols);
-        pbm_readpbmrow(ifd, inrow[2], cols, format);
-    }
-    /* Now advance the inrow[] window, rotating the buffer that now holds
-       the "previous" row to use it for the new "next" row.
-    */
-    shuffle = inrow[0];
-
-    inrow[0] = inrow[1];
-    inrow[1] = inrow[2];
-    inrow[2] = shuffle ;
-    if (row+1 < rows) {
-        /* Read the "next" row in from the file.  Allocate buffer if needed */
-        if (inrow[2] == NULL)
-            inrow[2] = pbm_allocrow(cols);
-        pbm_readpbmrow(ifd, inrow[2], cols, format);
-    } else {
-        /* There is no next row */
-        if (inrow[2]) {
-            pbm_freerow(inrow[2]);
-            inrow[2] = NULL; 
+
+
+/*----------------------------------------------------------------------------
+Fast algorithm for counting friendly neighbor pixels
+
+In this program both input and output rows are in raw (packed) PBM format.
+
+We handle input rows in groups of three, named "prevrow", "thisrow",
+"nextrow" and scan from left to right.  At every byte boundary, 10 bits
+are read from each of the three rows and placed into a temporary storage
+we call "sample".
+
+prevrow: ... ... _______M NNNNNNNN O_______ ...
+thisrow: ... ... _______W cCCCCCCC E_______ ...
+nextrow: ... ... _______R SSSSSSSS T_______ ...
+
+sample : xxMNNNNNNNNOWcCCCCCCCERSSSSSSST
+
+We count bits by taking the logical and of "sample" and a bit-mask called
+"selection", and feeding the result to a table-based bit-population counter.
+
+For example, the bits around the leftmost bit of the byte ("c") are selected
+like this:
+
+sample :       xxMNNNNNNNNOWcCCCCCCCERSSSSSSST
+selection: & | __111_______1_1_______111______
+
+(In the actual process, "sample" is shifted right and anded against a
+ constant "selection" mask.)
+
+The above reports one bits.  For the zero (white) bits we replace "sample"
+with its inverse.
+
+If the friendly neighbor count is below a threshold (default 1), we record
+that as a one bit in "flipmask".  Bits are flipped in units of eight
+and written to outrow at the byte boundary.
+-----------------------------------------------------------------------------*/
+
+
+
+static unsigned int
+likeNeighbors(uint32_t     const blackSample, 
+              unsigned int const offset) {
+
+    bool const thispoint = ( blackSample >> (18-offset) ) & 0x01;
+    uint32_t const sample = (thispoint == PBM_BLACK ) 
+                          ?   blackSample
+                          : ~ blackSample ;
+    uint32_t const selection = 0x701407;
+
+    return (bitpop24((sample >> (7-offset)) & selection));
+}
+
+
+
+static uint32_t
+setSample(const bit *  const prevrow,
+          const bit *  const thisrow,
+          const bit *  const nextrow,
+          unsigned int const col){
+
+    int const col8 = col/8;
+
+    uint32_t sample;
+
+    sample =
+        ((prevrow[col8 - 1]       )  << 29) |
+        ((prevrow[col8]           )  << 21) |
+        ((prevrow[col8 + 1] & 0x80)  << 13) |
+        ((thisrow[col8 - 1] & 0x01)  << 19) |
+        ((thisrow[col8]           )  << 11) |
+        ((thisrow[col8 + 1] & 0x80)  <<  3) |
+        ((nextrow[col8 - 1] & 0x01)  <<  9) |
+        ((nextrow[col8]           )  <<  1) |
+        ((nextrow[col8 + 1] & 0x80)  >>  7);
+    
+    return sample;
+}
+
+
+
+static unsigned char
+setTestmask(unsigned char const whiteTestmask,
+            bool          const testWhite,
+            bool          const testBlack) {
+/* -----------------------------------------------------------------------
+  Make a byte pattern of what bits should be tested within a given "thisrow"
+  (current inrow) byte.  0 means test, 1 means skip.
+-------------------------------------------------------------------------- */
+    if (testWhite == testBlack) {
+        assert(testWhite); assert(testBlack);
+        return 0x00;
+    } else if (testWhite == TRUE) {
+        assert(!testBlack);
+        return whiteTestmask;
+    } else
+        return ~whiteTestmask;
+}
+
+
+
+static void
+cleanrow(const bit *    const prevrow,
+         const bit *    const thisrow,
+         const bit *    const nextrow,
+         bit *          const outrow,
+         unsigned int   const cols,
+         unsigned int   const threshold,
+         bool           const flipWhite,
+         bool           const flipBlack,
+         unsigned int * const nFlippedP) {
+/* ----------------------------------------------------------------------
+  Work through row, scanning for bits that require flipping, and write
+  the results to 'outrow'.
+  
+  Returns the number of bits flipped within this one row as *nFlippedP.
+-------------------------------------------------------------------------*/
+    uint32_t sample;
+    unsigned char testmask;
+    unsigned char flipmask;
+    unsigned int col;
+    unsigned int nFlipped;
+
+    flipmask = 0x00;  /* initial value */
+    nFlipped = 0;     /* initial value */
+
+    for (col=0 ; col < cols ; ++col) {
+        unsigned int const col8 = col / 8;
+        unsigned int const offset = col % 8;
+
+        if (offset == 0) {
+            if (flipmask != 0x00) {
+                /* Some bits have to be flipped */
+                outrow[col8 -1] = thisrow [col8 -1] ^ flipmask;
+                nFlipped += bitpop8(flipmask);
+                flipmask = 0x00;
+            } else if (col8 > 0)
+                outrow[col8 -1] = thisrow [col8 -1];
+
+            sample = setSample(prevrow, thisrow, nextrow, col);
+            testmask = setTestmask(thisrow[col8], flipWhite, flipBlack);
+        }
+
+        if (((testmask << offset) & 0x80 ) ==0) {
+            if (likeNeighbors(sample, offset ) < threshold)
+                flipmask |= (0x80 >> offset);
         }
     }
+
+    {
+        /* Write out last byte */
+        unsigned int const col8Last = pbm_packed_bytes(cols) -1;
+
+        if (flipmask != 0x00) {
+            outrow[col8Last] = thisrow[col8Last] ^ flipmask;
+            nFlipped += bitpop8(flipmask);
+        } else
+            outrow[col8Last] = thisrow[col8Last];
+    }
+    *nFlippedP = nFlipped;
+}
+
+
+
+static void
+setupInputBuffers(FILE *       const ifP,
+                  unsigned int const cols,
+                  int          const format,
+                  bit ***      const bufferP,
+                  bit **       const edgeRowP,
+                  bit **       const thisRowP,
+                  bit **       const nextRowP) {
+/*----------------------------------------------------------------------------
+  Initialize input buffers.
+  We add a margin of 8 bits each on the left and right of the rows.
+
+  On the top and bottom of the image we place an imaginary blank row
+  ("edgerow") to facilitate the process.
+-----------------------------------------------------------------------------*/
+    bit ** const buffer  = pbm_allocarray_packed(cols+16, 3);
+    bit *  const edgeRow = pbm_allocrow_packed(cols+16);
+
+    bit * nextRow;
+    unsigned int i;
+
+    for (i = 0; i < pbm_packed_bytes(cols+16); ++i)
+        edgeRow[i] = 0x00;
+        
+    for (i = 0; i < 3; ++i) {
+        /* Add blank (all white) bytes beside the edges */ 
+        buffer[i][0] = buffer[i][ pbm_packed_bytes( cols +16 ) - 1] = 0x00;
+    }
+    nextRow = &buffer[0][1];
+
+    /* Read the top line into nextrow and clean the right end. */
+
+    pbm_readpbmrow_packed(ifP, nextRow, cols, format);
+
+    pbm_cleanrowend_packed(nextRow, cols);
+
+    *bufferP  = buffer;
+    *edgeRowP = edgeRow;
+    *thisRowP = &edgeRow[1];
+    *nextRowP = nextRow;
+}
+
+
+
+static void
+cleanSimple(FILE *             const ifP,
+            FILE *             const ofP,
+            struct cmdlineInfo const cmdline,
+            double *           const nFlippedP) {
+/*----------------------------------------------------------------------------
+   Do the traditional clean where you look only at the immediate neighboring
+   pixels of a subject pixel to determine whether to erase that pixel.
+-----------------------------------------------------------------------------*/
+    bit ** buffer;
+        /* The rows of the input relevant to our current processing:
+           the current row and the one above and below it.
+        */
+    bit * edgeRow;
+        /* A blank (all white) row.  Constant */
+    bit * prevRow;
+    bit * thisRow;
+    bit * nextRow;
+    bit * outRow;
+    int cols, rows, format;
+    unsigned int row;
+
+    pbm_readpbminit(ifP, &cols, &rows, &format);
+
+    setupInputBuffers(ifP, cols, format, &buffer, &edgeRow,
+                      &thisRow, &nextRow);
+
+    outRow = pbm_allocrow(cols);
+
+    pbm_writepbminit(ofP, cols, rows, 0) ;
+
+    *nFlippedP = 0;  /* none flipped yet */
+
+    for (row = 0; row < rows; ++row) {
+        unsigned int nFlipped;
+
+        prevRow = thisRow;  /* Slide up the input row window */
+        thisRow = nextRow;
+        if (row < rows -1){
+            nextRow = &buffer[(row+1)%3][1];
+            /* We take the address directly instead of shuffling the rows
+               with the help of a temporary.  This provision is for proper 
+               handling of the initial edgerow.
+            */
+            pbm_readpbmrow_packed(ifP, nextRow, cols, format);
+            pbm_cleanrowend_packed(nextRow, cols);
+
+        } else  /* Bottom of image.  */
+            nextRow = &edgeRow[1];
+
+        cleanrow(prevRow, thisRow, nextRow, outRow, cols, cmdline.minneighbors,
+                 cmdline.flipWhite, cmdline.flipBlack, &nFlipped);
+        
+        *nFlippedP += nFlipped;
+
+        pbm_writepbmrow_packed(ofP, outRow, cols, 0) ;
+    }
+
+    pbm_freearray(buffer, 3);
+    pbm_freerow(edgeRow);
+    pbm_freerow(outRow);
+}
+
+
+
+struct PixQueueElt {
+    struct PixQueueElt * nextP;
+    pm_pixelcoord        coord;
+};
+
+typedef struct {
+/*----------------------------------------------------------------------------
+   A queue of pixel locations.
+-----------------------------------------------------------------------------*/
+    unsigned int size;
+    
+    struct PixQueueElt * headP;
+    struct PixQueueElt * tailP;
+} PixQueue;
+
+
+
+static void
+pixQueue_init(PixQueue * const queueP) {
+
+    queueP->size = 0;
+    queueP->headP = NULL;
+    queueP->tailP = NULL;
 }
 
 
 
 static unsigned int
-likeNeighbors(bit *        const inrow[3], 
-              unsigned int const col, 
-              unsigned int const cols) {
+pixQueue_size(PixQueue * const queueP) {
+
+    return queueP->size;
+}
+
+
+
+static bool
+pixQueue_isEmpty(PixQueue * const queueP) {
+
+    return !queueP->headP;
+}
+
+
+
+static void
+pixQueue_push(PixQueue *    const queueP,
+              pm_pixelcoord const newValue) {
+
+    struct PixQueueElt * newEltP;
+
+    MALLOCVAR(newEltP);
+
+    if (!newEltP)
+        pm_error("Out of memory putting a pixel on a queue");
+
+    newEltP->coord = newValue;
+
+    newEltP->nextP = NULL;
+    if (queueP->tailP)
+        queueP->tailP->nextP = newEltP;
+    
+    queueP->tailP = newEltP;
+
+    if (!queueP->headP)
+        queueP->headP = newEltP;
+
+    ++queueP->size;
+}
+
+
+
+static pm_pixelcoord
+pixQueue_pop(PixQueue * const queueP) {
+/*----------------------------------------------------------------------------
+   Pop and return the pixel location at the head of queue *queueP.
+-----------------------------------------------------------------------------*/
+    struct PixQueueElt * const newHeadP = queueP->headP->nextP;
+
+    pm_pixelcoord retval;
+
+    assert(queueP->headP);
+
+    retval = queueP->headP->coord;
+
+    if (queueP->tailP == queueP->headP)
+        queueP->tailP = NULL;
+
+    free(queueP->headP);
+
+    queueP->headP = newHeadP;
+
+    --queueP->size;
+
+    return retval;
+}
+
+
+
+static void
+pixQueue_term(PixQueue * const queueP) {
+
+    struct PixQueueElt * p;
+    struct PixQueueElt * nextP;
     
-    int const point = inrow[THISROW][col];
-    enum compass_heading heading;
-    int joined;
-
-    joined = 0;  /* initial value */
-    for (heading = WEST; heading <= SOUTHWEST; ++heading) {
-        int x = col + xd[heading] ;
-        int y = THISROW + yd[heading] ;
-        if (x < 0 || x >= cols || !inrow[y]) {
-            if (point == PBM_WHITE) joined++;
-        } else if (inrow[y][x] == point) joined++ ;
+    for (p = queueP->headP; p; p = nextP) {
+        nextP = p->nextP;
+        free(p);
     }
-    return joined;
 }
 
 
 
-int
-main(int argc, char *argv[]) {
+static void
+queueNeighbors(pm_pixelcoord const center,
+               bit **        const pixels,
+               unsigned int  const cols,
+               unsigned int  const rows,
+               bool **       const visited,
+               PixQueue *    const queueP) {
+/*----------------------------------------------------------------------------
+   Add to queue *queueP all the pixels in 'pixels' that touch 'center' and are
+   the same color as 'center'.
 
-    struct cmdlineInfo cmdline;
-    FILE *ifp;
-    bit *outrow;
-    int cols, rows, format;
-    unsigned int row;
-    unsigned int nFlipped;  /* Number of pixels we have flipped so far */
+   But ignore pixels that 'visited' says have already been queued and
+   mark everything we queue as visited.
+-----------------------------------------------------------------------------*/
+    bit const blobColor = pixels[center.row][center.col];
 
-    pbm_init( &argc, argv );
+    int row;
+        /* Row number of a neighbor; might be off the canvas; even negative */
 
-    parseCommandLine(argc, argv, &cmdline);
+    /* Note that we consider the center pixel here, but it has necessarily
+       already been visited, so we don't queue it.
+    */
+
+    for (row = (int)center.row - 1; row <= (int)center.row + 1; ++row) {
+        int col;  /* Analogous to 'row' */
+
+        for (col = (int)center.col - 1; col <= (int)center.col + 1; ++col) {
+            if (row < 0 || row >= rows || col < 0 || col >= cols) {
+                /* It's off the canvas; nothing to queue */
+            } else {
+                if (pixels[row][col] == blobColor) {
+                    if (visited[row][col]) {
+                        /* We've already explored this one */
+                    } else {
+                        /* Queue it! */
+                        pm_pixelcoord neighbor;
+                        neighbor.row = row;
+                        neighbor.col = col;
+                        pixQueue_push(queueP, neighbor);
+                        visited[row][col] = true;
+                    }
+                }
+            }
+        }
+    }
+}
+
+
+
+static void
+setColor(PixQueue * const blobP,
+         bit **     const pixels,
+         bit        const newColor) {
+/*----------------------------------------------------------------------------
+   Change all the pixels in (blobP) to 'newColor'.  More precisely, change
+   the pixels in 'pixels' that are listed in *blobP.
+-----------------------------------------------------------------------------*/
+    while (!pixQueue_isEmpty(blobP)) {
+        pm_pixelcoord const thisPix = pixQueue_pop(blobP);
+
+        pixels[thisPix.row][thisPix.col] = newColor;
+    }
+}
+
+
+
+static void
+processBlob(pm_pixelcoord const start,
+            bit **        const pixels,
+            unsigned int  const cols,
+            unsigned int  const rows,
+            unsigned int  const trivialSize,
+            bool **       const visited,
+            double *      const nFlippedP) {
+/*----------------------------------------------------------------------------
+   Process the blob that contains the pixel at 'start'.
+
+   That pixel is part of a blob.  A blob is a maximal set of contiguous
+   pixels of the same color.
+
+   None of the blob is marked visited in visited[][].
+
+   If the blob has fewer than 'trivialSize' pixels, erase it (flip its color).
+
+   Update visited[][] to flag all pixels of the blob as visited.
+
+   Return as *nFlippedP how many pixels we flipped (i.e. either zero or the
+   size of the blob).
+-----------------------------------------------------------------------------*/
+    /* In addition to putting output in it, we use visited[][] for working
+       memory.  It indicates pixels of the blob that we've queued for
+       processing so far.
+    */
+    PixQueue toExplore;
+    PixQueue blob;
+
+    pixQueue_init(&toExplore);
+    pixQueue_init(&blob);
+
+    pixQueue_push(&toExplore, start);
+    visited[start.row][start.col] = true;
+
+    while (!pixQueue_isEmpty(&toExplore)) {
+        pm_pixelcoord const thisPix = pixQueue_pop(&toExplore);
+
+        pixQueue_push(&blob, thisPix);
+
+        queueNeighbors(thisPix, pixels, cols, rows, visited, &toExplore);
+    }
+
+    if (pixQueue_size(&blob) <= trivialSize) {
+        bit const blobColor = pixels[start.row][start.col];
+
+        *nFlippedP = pixQueue_size(&blob);
+
+        setColor(&blob, pixels,
+                 blobColor == PBM_WHITE ? PBM_BLACK : PBM_WHITE);
+    } else
+        *nFlippedP = 0;
 
-    ifp = pm_openr(cmdline.inputFilespec);
+    pixQueue_term(&blob);
+    pixQueue_term(&toExplore);
+}
 
-    inrow[0] = inrow[1] = inrow[2] = NULL;
-    pbm_readpbminit(ifp, &cols, &rows, &format);
 
-    outrow = pbm_allocrow(cols);
 
-    pbm_writepbminit(stdout, cols, rows, 0) ;
+static void
+setAllNotVisited(bool **      const visited,
+                 unsigned int const cols,
+                 unsigned int const rows)  {
 
-    nFlipped = 0;  /* No pixels flipped yet */
+    unsigned int row;
     for (row = 0; row < rows; ++row) {
         unsigned int col;
-        nextrow(ifp, row, cols, rows, format);
-        for (col = 0; col < cols; ++col) {
-            bit const thispoint = inrow[THISROW][col];
-            if ((cmdline.flipWhite && thispoint == PBM_WHITE) ||
-                (cmdline.flipBlack && thispoint == PBM_BLACK)) {
-                if (likeNeighbors(inrow, col, cols) < cmdline.connect) {
-                    outrow[col] = PBM_INVERT(thispoint);
-                    ++nFlipped;
-                } else
-                    outrow[col] = thispoint;
-            } else 
-                outrow[col] = thispoint;
+        for (col = 0; col < cols; ++col)
+            visited[row][col] = false;
+    }
+}
+
+
+
+static void
+cleanPixels(bit **       const pixels,
+            unsigned int const cols,
+            unsigned int const rows,
+            bit          const foregroundColor,
+            unsigned int const trivialSize,
+            double *     const nFlippedP) {
+/*----------------------------------------------------------------------------
+   Same as cleanExtended(), except we work on the pixels 'pixels' instead
+   of input and output files.
+-----------------------------------------------------------------------------*/
+    pm_pixelcoord thisPix;
+
+    bool ** visited;  /* malloced */
+        /* visited[row][col] means we have processed the pixel at (row, col)
+           and flipped it if it needed to be flipped.
+        */
+
+    MALLOCARRAY2(visited, rows, cols);
+
+    if (!visited)
+        pm_error("Could not allocate a %u x %u array for visited flags",
+                 rows, cols);
+
+    setAllNotVisited(visited, cols, rows);
+
+    *nFlippedP = 0;  /* initial value */
+
+    for (thisPix.row = 0; thisPix.row < rows; ++thisPix.row) {
+        for (thisPix.col = 0; thisPix.col < cols; ++thisPix.col) {
+            if (pixels[thisPix.row][thisPix.col] == foregroundColor
+                && !visited[thisPix.row][thisPix.col]) {
+                
+                double nFlipped;
+                
+                processBlob(thisPix, pixels, cols, rows, trivialSize,
+                            visited, &nFlipped);
+
+                *nFlippedP += nFlipped;
+            } else
+                visited[thisPix.row][thisPix.col] = true;
         }
-        pbm_writepbmrow(stdout, outrow, cols, 0) ;
     }
-    pbm_freerow(outrow);
-    pm_close(ifp);
+
+    pm_freearray2((void **)visited);
+}
+
+
+
+static void
+cleanExtended(FILE *             const ifP,
+              FILE *             const ofP,
+              bit                const foregroundColor,
+              unsigned int       const trivialSize,
+              double *           const nFlippedP) {
+/*----------------------------------------------------------------------------
+   Clean the image on *ifP and write the result to *ofP.
+
+   Look at arbitrarily shaped and sized blobs to determine what to erase.
+
+   A blob is a contiguous set of pixels of the foreground color
+   ('foregroundColor') which is not contiguous with any other pixels of that
+   color.
+
+   We erase (flip) every pixel in every trivial blob.  A trivial blob is
+   one with 'trivialSize' pixels or fewer.
+-----------------------------------------------------------------------------*/
+    bit ** pixels;
+    int cols, rows;
+
+    pixels = pbm_readpbm(ifP, &cols, &rows);
+
+	cleanPixels(pixels, cols, rows, foregroundColor, trivialSize, nFlippedP);
+
+    pbm_writepbm(ofP, pixels, cols, rows, 0);
+
+    pbm_freearray(pixels, rows);
+}
+
+
+
+static void
+pbmclean(FILE *             const ifP,
+         FILE *             const ofP,
+         struct cmdlineInfo const cmdline,
+         double *           const nFlippedP) {
+
+    if (cmdline.extended) {
+        bit const foregroundColor = cmdline.flipWhite ? PBM_WHITE : PBM_BLACK;
+
+        assert(cmdline.flipWhite + cmdline.flipBlack == 1);
+
+        cleanExtended(ifP, ofP, foregroundColor, cmdline.minneighbors,
+                      nFlippedP);
+    } else
+        cleanSimple(ifP, ofP, cmdline, nFlippedP);
+}
+
+
+
+int
+main(int argc, const char *argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    double nFlipped;
+        /* Number of pixels we have flipped so far.  Use type double to
+           prevent overflow.
+        */
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    pbmclean(ifP, stdout, cmdline, &nFlipped);
 
     if (cmdline.verbose)
-        pm_message("%d pixels flipped", nFlipped);
+        pm_message("%f pixels flipped", nFlipped);
+
+    pm_close(ifP);
 
     return 0;
 }
+
+
+
diff --git a/editor/pbmpscale.c b/editor/pbmpscale.c
index 2e24f3cd..9ab89350 100644
--- a/editor/pbmpscale.c
+++ b/editor/pbmpscale.c
@@ -3,22 +3,63 @@
  */
 
 #include <stdio.h>
-#include "pbm.h"
+#include "pm_c_util.h"
 #include "mallocvar.h"
+#include "shhopt.h"
+#include "pbm.h"
+#include "bitarith.h"
 
-/* prototypes */
-void nextrow_pscale ARGS((FILE *ifd, int row));
-int corner ARGS((int pat));
+#define LEFTBITS pm_byteLeftBits
+#define RIGHTBITS pm_byteRightBits
 
-/* input bitmap size and storage */
-int rows, columns, format ;
-bit *inrow[3] ;
+/* Table for translating bit pattern into "corners" flag element */ 
 
-#define thisrow (1)
+unsigned char const
+transTable[512] = {
+     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04,
+     0x00, 0x00, 0x04, 0x04, 0xaa, 0xa2, 0x82, 0x82, 0x8a, 0x82, 0x82, 0x82,
+     0xa0, 0xa0, 0x40, 0x40, 0xc0, 0xc0, 0xc0, 0xc0, 0x00, 0x00, 0x10, 0x10,
+     0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x28, 0x28, 0x00, 0x00, 0x28, 0x28,
+     0x0a, 0x03, 0x01, 0x03, 0x0a, 0x03, 0x01, 0x03, 0xff, 0xff, 0xff, 0xff,
+     0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+     0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x04, 0x04, 0xa8, 0xa0, 0xc0, 0xc0,
+     0x88, 0x80, 0x80, 0x80, 0xa0, 0xa0, 0xc0, 0xc0, 0x80, 0x80, 0x80, 0x80,
+     0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x28, 0x28,
+     0x00, 0x00, 0x28, 0x28, 0x0c, 0xff, 0xff, 0xff, 0x08, 0xff, 0xff, 0xff,
+     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+     0xff, 0xff, 0xff, 0xff, 0x01, 0x01, 0x0a, 0x0a, 0x01, 0x01, 0x0a, 0x0a,
+     0x28, 0x30, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x10, 0x30, 0x00, 0x00,
+     0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0xa0, 0xa0, 0x40, 0x40, 0xa0, 0xa0,
+     0x82, 0x82, 0xff, 0xff, 0x82, 0x82, 0xff, 0xff, 0x04, 0x00, 0x00, 0x00,
+     0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x01, 0x0a, 0x0a,
+     0x01, 0x01, 0x0a, 0x0a, 0x28, 0x30, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
+     0x10, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0xa0, 0xa0,
+     0x40, 0x40, 0xa0, 0xa0, 0x82, 0x82, 0xff, 0xff, 0x82, 0x82, 0xff, 0xff,
+     0x0c, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+     0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x04, 0x04, 0x2a, 0x22, 0x03, 0x02,
+     0x0a, 0x02, 0x03, 0x02, 0x30, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+     0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x28, 0x28,
+     0x00, 0x00, 0x28, 0x28, 0x0a, 0x02, 0x03, 0x02, 0x0a, 0x02, 0x03, 0x02,
+     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
+     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x04, 0x04,
+     0x28, 0x20, 0xff, 0xff, 0x08, 0xff, 0xff, 0xff, 0x30, 0x20, 0xff, 0xff,
+     0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x10, 0x10,
+     0x00, 0x00, 0x28, 0x28, 0x00, 0x00, 0x28, 0x28, 0x0c, 0xff, 0xff, 0xff,
+     0x08, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x01, 0x0a, 0x0a,
+     0x01, 0x01, 0x0a, 0x0a, 0x28, 0x20, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00,
+     0x30, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0xa0, 0xa0,
+     0x40, 0x40, 0xa0, 0xa0, 0x82, 0x82, 0xff, 0xff, 0x82, 0x82, 0xff, 0xff,
+     0x04, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+     0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+     0x01, 0x01, 0x0a, 0x0a, 0x01, 0x01, 0x0a, 0x0a, 0x28, 0x20, 0x00, 0x00,
+     0x08, 0x00, 0x00, 0x00, 0x30, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+     0x40, 0x40, 0xa0, 0xa0, 0x40, 0x40, 0xa0, 0xa0, 0x82, 0x82, 0xff, 0xff,
+     0x82, 0x82, 0xff, 0xff, 0x0c, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
+     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  };
 
-/* compass directions from west clockwise */
-int xd_pscale[] = { -1, -1,  0,  1, 1, 1, 0, -1 } ;
-int yd_pscale[] = {  0, -1, -1, -1, 0, 1, 1,  1 } ;
 
 /* starting positions for corners */
 #define NE(f) ((f) & 3)
@@ -26,180 +67,389 @@ int yd_pscale[] = {  0, -1, -1, -1, 0, 1, 1,  1 } ;
 #define SW(f) (((f) >> 4) & 3)
 #define NW(f) (((f) >> 6) & 3)
 
-typedef unsigned short sixteenbits ;
 
-/* list of corner patterns; bit 7 is current color, bits 0-6 are squares
- * around (excluding square behind), going clockwise.
- * The high byte of the patterns is a mask, which determines which bits are
- * not ignored.
- */
 
-sixteenbits patterns[] = { 0x0000, 0xd555,         /* no corner */
-                           0x0001, 0xffc1, 0xd514, /* normal corner */
-                           0x0002, 0xd554, 0xd515, /* reduced corners */
-                           0xbea2, 0xdfc0, 0xfd81,
-                           0xfd80, 0xdf80,
-                           0x0003, 0xbfa1, 0xfec2 /* reduced if > 1 */
-                           };
-
-/* search for corner patterns, return type of corner found:
- *  0 = no corner,
- *  1 = normal corner,
- *  2 = reduced corner,
- *  3 = reduced if cutoff > 1
- */
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    unsigned int scale;
+    const char * inputFileName;  /* File name of input file */
+};
+
+
+
+static void
+parseCommandLine(int argc, const char ** const argv,
+                 struct cmdlineInfo * const cmdlineP) {
+/*----------------------------------------------------------------------------
+   Note that the file spec array we return is stored in the storage that
+   was passed to us as the argv array.
+-----------------------------------------------------------------------------*/
+    optEntry * option_def;
+        /* Instructions to OptParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+    unsigned int option_def_index;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+
+    OPTENTINIT;
+
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = FALSE;  /* We may have parms that are negative numbers */
 
-int corner(pat)
-     int pat;
-{
-   register int i, r=0;
-   for (i = 0; i < sizeof(patterns)/sizeof(sixteenbits); i++)
-      if (patterns[i] < 0x100)
-         r = patterns[i];
-      else if ((pat & (patterns[i] >> 8)) ==
-               (patterns[i] & (patterns[i] >> 8)))
-         return r;
-   return 0;
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (argc-1 < 1)
+        pm_error("You must specify the scale factor as an argument");
+    else {
+        int const scale = atoi(argv[1]);
+        if (scale < 1)
+            pm_error("Scale argument must be at least one.  You specified %d",
+                     scale);
+        else
+            cmdlineP->scale = scale;
+
+        if (argc-1 < 2)
+            cmdlineP->inputFileName = "-";
+        else {
+            cmdlineP->inputFileName = argv[2];
+
+            if (argc-1 > 2)
+                pm_error("Too many arguments.  The only arguments are the "
+                         "scale factor and optional input file name.  "
+                         "You specified %u", argc-1);
+        }
+    }
+    free(option_def);
 }
 
-/* get a new row
- */
 
-void nextrow_pscale(ifd, row)
-     FILE *ifd;
-     int row;
-{
-   bit *shuffle = inrow[0] ;
-   inrow[0] = inrow[1];
-   inrow[1] = inrow[2];
-   inrow[2] = shuffle ;
-   if (row < rows) {
-      if (shuffle == NULL)
-         inrow[2] = shuffle = pbm_allocrow(columns);
-      pbm_readpbmrow(ifd, inrow[2], columns, format) ;
-   } else inrow[2] = NULL; /* discard storage */
 
+static void
+validateComputableDimensions(unsigned int const width,
+                             unsigned int const height,
+                             unsigned int const scaleFactor) {
+/*----------------------------------------------------------------------------
+   Make sure that multiplication for output image width and height do not
+   overflow.
+   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);
+
+    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);
 }
 
 
 
-int
-main(int argc, char ** argv) {
+static void
+writeBitSpan(unsigned char * const packedBitrow,
+             int             const cols,
+             int             const offset,
+             int             const color) {
+/*----------------------------------------------------------------------------
+   Write white (color="0") or black (="1") bits into packedBitrow[],
+   starting at 'offset', length 'cols'.
+-----------------------------------------------------------------------------*/
+    unsigned char * const dest     = &packedBitrow[offset/8];
+    unsigned int    const rs       = offset % 8;
+    unsigned int    const trs      = (cols + rs) % 8;
+    unsigned int    const colBytes = pbm_packed_bytes(cols + rs);
+    unsigned int    const last     = colBytes - 1;
 
-    FILE * ifP;
-    bit * outrow;
-    unsigned int row;
-    int scale, cutoff, ucutoff ;
-    unsigned char *flags;
+    unsigned char const origHead = dest[0];
+    unsigned char const origEnd =  dest[last];
 
-    pbm_init( &argc, argv );
+    unsigned int i;
 
-    if (argc < 2)
-        pm_usage("scale [pbmfile]");
+    for (i = 0; i < colBytes; ++i)
+        dest[i] = color * 0xff;
 
-    scale = atoi(argv[1]);
-    if (scale < 1)
-        pm_error("Scale argument must be at least one.  You specified '%s'",
-                 argv[1]);
+    if (rs > 0)
+        dest[0] = LEFTBITS(origHead, rs) | RIGHTBITS(dest[0], 8-rs);
 
-    if (argc == 3)
-        ifP = pm_openr(argv[2]);
-    else
-        ifP = stdin ;
+    if (trs > 0)
+        dest[last] = LEFTBITS(dest[last], trs) | RIGHTBITS(origEnd, 8-trs);
+}
 
-    inrow[0] = inrow[1] = inrow[2] = NULL;
-    pbm_readpbminit(ifP, &columns, &rows, &format) ;
 
-    outrow = pbm_allocrow(columns*scale) ;
-    MALLOCARRAY(flags, columns);
-    if (flags == NULL) 
-        pm_error("out of memory") ;
 
-    pbm_writepbminit(stdout, columns*scale, rows*scale, 0) ;
+static void
+setFlags(const bit *     const prevrow,
+         const bit *     const thisrow,
+         const bit *     const nextrow,
+         unsigned char * const flags,
+         unsigned int    const cols ) {
+/*----------------------------------------------------------------------------
+   Scan one row, examining the row above and row below, and determine 
+   whether there are "corners" for each pixel.  Feed a 9 bit sample into 
+   pre-calculated array transTable[512] to calculate all four corner statuses
+   at once.
 
-    cutoff = scale / 2;
-    ucutoff = scale - 1 - cutoff;
-    nextrow_pscale(ifP, 0);
-    for (row = 0; row < rows; ++row) {
-        unsigned int col;
-        unsigned int i;
-        nextrow_pscale(ifP, row+1);
-        for (col = 0; col < columns; ++col) {
-            unsigned int i;
-            flags[col] = 0 ;
-            for (i = 0; i != 8; i += 2) {
-                int vec = inrow[thisrow][col] != PBM_WHITE;
-                unsigned int k;
-                for (k = 0; k < 7; ++k) {
-                    int x = col + xd_pscale[(k+i)&7] ;
-                    int y = thisrow + yd_pscale[(k+i)&7] ;
-                    vec <<= 1;
-                    if (x >=0 && x < columns && inrow[y])
-                        vec |= (inrow[y][x] != PBM_WHITE) ;
-                }
-                flags[col] |= corner(vec)<<i ;
-            }
+   Bits in the 9 bit sample represent the current pixel and neighbors:
+       NW : N : NE : W: Current : E : SW : S : SE
+
+   Bits in flag are divided into 4 fields of width 2 each:
+       NW : SW : SE : NE
+
+   Code 0xff is an exception.  It is a variation of 0x00.
+     0x00 : no corners, no color change from above row (Current == N)
+     0xff : no corners, but color changed (Current != N)
+
+   Most transTable[] entries are "no corners".
+   0x00 appears 180 times, 0xff 109 times.
+-----------------------------------------------------------------------------*/
+
+#if 0
+    /* The following code is from the previous version, which examined
+       the corners one by one:
+    */
+
+    /* list of corner patterns; bit 7 is current color, bits 0-6 are squares
+       around (excluding square behind), going clockwise.
+       The high byte of the patterns is a mask, which determines which bits are
+       not ignored.
+    */
+    uint16_t const patterns[] 
+        = { 0x0000,   0xd555,            /* no corner */
+            0x0001,   0xffc1, 0xd514,    /* normal corner */
+            0x0002,   0xd554, 0xd515, 0xbea2, 0xdfc0, 0xfd81, 0xfd80, 0xdf80,
+            /* reduced corners */
+            0x0003,   0xbfa1, 0xfec2 };  /* reduced if cutoff > 1 */
+
+    /*
+      For example, the NE corner is examined with the following 8 bit sample:
+      Current : W : NW : N : NE : E : SE : S
+      (SW is the "square behind") 
+      */
+#endif
+
+    uint32_t prevrow24, thisrow24, nextrow24;
+    unsigned int col;
+
+    /* higher bits are set to 0 */
+    prevrow24 = prevrow[0];  /* initial value */
+    thisrow24 = thisrow[0];  /* initial value */
+    nextrow24 = nextrow[0];  /* initial value */
+
+    for (col = 0; col < cols; ++col) {
+        unsigned int const col8   = col / 8;
+        unsigned int const offset = col % 8;
+
+        unsigned int sample;
+
+        if (offset == 0) {
+            prevrow24 = prevrow24 << 8 | prevrow[col8 + 1];
+            thisrow24 = thisrow24 << 8 | thisrow[col8 + 1];
+            nextrow24 = nextrow24 << 8 | nextrow[col8 + 1];
         }
-        for (i = 0; i < scale; i++) {
-            bit *ptr = outrow ;
-            int zone = (i > ucutoff) - (i < cutoff) ;
-            int cut = (zone < 0) ? (cutoff - i) :
-                (zone > 0) ? (i - ucutoff) : 0 ;
-
-            for (col = 0; col < columns; ++col) {
-                int pix = inrow[thisrow][col] ;
-                int flag = flags[col] ;
-                int cutl, cutr;
-                unsigned int k;
 
+        sample = ( ( prevrow24 >> ( 8 -offset) ) & 0x01c0 )
+            | ( ( thisrow24 >> (11 -offset) ) & 0x0038 )
+            | ( ( nextrow24 >> (14 -offset) ) & 0x0007 );
+        
+        flags[col] =  transTable[sample];
+    }
+}
+
+
+
+static void
+expandRow(const bit *     const thisrow,
+          const bit *     const prevrow,
+          bit *           const outrow,
+          unsigned char * const flags,
+          unsigned int    const cols,
+          int             const scale,
+          int             const cutoff,
+          int             const ucutoff) {
+/*----------------------------------------------------------------------------
+  Process one row, using flags array as reference.  If pixel has no corners
+  output a NxN square of the given color, otherwise output with the 
+  specified corner area(s) clipped off.
+-----------------------------------------------------------------------------*/
+    unsigned int const outcols = cols * scale;
+
+    unsigned int i;
+    unsigned int col;
+    
+    for (i = 0; i < scale; ++i) {
+        int const zone = (i > ucutoff) - (i < cutoff);
+        int const cut1 =
+            (zone < 0) ? (cutoff - i) : (zone > 0) ? (i - ucutoff) : 0;
+
+        unsigned int outcol;
+        int cut[4];
+
+        outcol = 0; /* initial value */
+
+        cut[0] = 0;
+        cut[1] = cut1;
+        cut[2] = cut1 ? cut1 - 1 : 0;
+        cut[3] = (cut1 && cutoff > 1) ? cut1 - 1 : cut1;
+      
+        for (col = 0; col < cols; ++col) {
+            unsigned int const col8 = col / 8;
+            unsigned int const offset = col % 8;
+            int const pix = (thisrow[col8] >> (7-offset) ) & 0x01;
+            int const flag = flags[col];
+
+            int cutl, cutr;
+
+            if (flag == 0x00) {
+                /* There are no corners, no color change */
+                outcol += scale;
+            } else { 
                 switch (zone) {
                 case -1:
-                    switch (NW(flag)) {
-                    case 0: cutl = 0; break;
-                    case 1: cutl = cut; break;
-                    case 2: cutl = cut ? cut-1 : 0; break;
-                    case 3: cutl = (cut && cutoff > 1) ? cut-1 : cut; break;
-                    default: cutl = 0;  /* Should never reach here */
-                    }
-                    switch (NE(flag)) {
-                    case 0: cutr = 0; break;
-                    case 1: cutr = cut; break;
-                    case 2: cutr = cut ? cut-1 : 0; break;
-                    case 3: cutr = (cut && cutoff > 1) ? cut-1 : cut; break;
-                    default: cutr = 0;  /* Should never reach here */
+                    if (i==0 && flag == 0xff) {
+                        /* No corners, color changed */ 
+                        cutl = cutr = 0;
+                        flags[col] = 0x00;
+                            /* Use above skip procedure next cycle */
+                    } else {
+                        cutl = cut[NW(flag)];
+                        cutr = cut[NE(flag)];
                     }
                     break;
                 case 0:
                     cutl = cutr = 0;
                     break ;
                 case 1:
-                    switch (SW(flag)) {
-                    case 0: cutl = 0; break;
-                    case 1: cutl = cut; break;
-                    case 2: cutl = cut ? cut-1 : 0; break;
-                    case 3: cutl = (cut && cutoff > 1) ? cut-1 : cut; break;
-                    default: cutl = 0;  /* should never reach here */
-                    }
-                    switch (SE(flag)) {
-                    case 0: cutr = 0; break;
-                    case 1: cutr = cut; break;
-                    case 2: cutr = cut ? cut-1 : 0; break;
-                    case 3: cutr = (cut && cutoff > 1) ? cut-1 : cut; break;
-                    default: cutr = 0;  /* should never reach here */
-                    }
+                    cutl = cut[SW(flag)];
+                    cutr = cut[SE(flag)];
                     break;
-                default: cutl = 0; cutr = 0;  /* Should never reach here */
                 }
-                for (k = 0; k < cutl; ++k) /* left part */
-                    *ptr++ = !pix ;
-                for (k = 0; k < scale-cutl-cutr; ++k)  /* center part */
-                    *ptr++ = pix ;
-                for (k = 0; k < cutr; ++k) /* right part */
-                    *ptr++ = !pix ;
+                
+                if (cutl > 0) {
+                    writeBitSpan(outrow, cutl, outcol, !pix);
+                    outcol += cutl;
+                }
+                {
+                    unsigned int const center = scale - cutl - cutr;
+
+                    if (center > 0) {
+                        writeBitSpan(outrow, center, outcol,  pix);
+                        outcol += center;
+                    }
+                }
+                if (cutr > 0) {
+                    writeBitSpan(outrow, cutr, outcol, !pix);
+                    outcol += cutr;
+                }
             }
-            pbm_writepbmrow(stdout, outrow, scale*columns, 0) ;
         }
+        pbm_writepbmrow_packed(stdout, outrow, outcols, 0) ;
+    }
+}
+
+
+
+int
+main(int argc, const char ** argv) {
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    bit ** buffer;
+    bit * prevrow;
+    bit * thisrow;
+    bit * nextrow;
+    bit * edgerow;
+    bit * outrow;
+    unsigned int row;
+    unsigned int i;
+    int cols, rows;
+    int format;
+    unsigned int outcols;
+    unsigned int outrows;
+    int cutoff;
+    int ucutoff ;
+    unsigned char * flags;  /* malloc'ed */
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    pbm_readpbminit(ifP, &cols, &rows, &format) ;
+
+    validateComputableDimensions(cols, rows, cmdline.scale); 
+
+    outcols = cols * cmdline.scale;
+    outrows = rows * cmdline.scale; 
+
+    /* Initialize input buffers.
+       We add a margin of 8 bits on the right of the three rows.
+
+       On the top and bottom of the image we place an imaginary blank row
+       ("edgerow") to facilitate the process.
+    */
+
+    buffer  = pbm_allocarray_packed(cols + 8, 3);
+    edgerow = pbm_allocrow_packed(cols + 8);
+
+    for (i = 0; i < pbm_packed_bytes(cols + 8); ++i)
+        edgerow[i] = 0x00;
+
+    /* Add blank bytes at right edges */ 
+    for (i = 0; i < 3; ++i)
+        buffer[i][pbm_packed_bytes(cols + 8) - 1] = 0x00;
+
+    thisrow = edgerow;
+    nextrow = buffer[0];
+
+    /* Read the top line into nextrow and clean the right end. */
+
+    pbm_readpbmrow_packed(ifP, nextrow, cols, format);
+    pbm_cleanrowend_packed(nextrow, cols);
+
+    outrow = pbm_allocrow_packed(outcols);
+    for (i = 0; i < pbm_packed_bytes(outcols); ++i)
+        outrow[i] = 0x00;
+
+    MALLOCARRAY(flags, cols);
+    if (flags == NULL)
+        pm_error("Couldn't get memory for %u columns of flags", cols);
+
+    pbm_writepbminit(stdout, outcols, outrows, 0) ;
+
+    cutoff = cmdline.scale / 2;
+    ucutoff = cmdline.scale - 1 - cutoff;
+
+    for (row = 0; row < rows; ++row) {
+        prevrow = thisrow;  /* Slide up the input row window */
+        thisrow = nextrow;
+        if (row < rows - 1) {
+            nextrow = buffer[(row + 1) % 3];
+            /* We take the address directly instead of shuffling the rows.
+               This provision is for proper handling of the initial edgerow.
+            */
+            pbm_readpbmrow_packed(ifP, nextrow, cols, format);
+            pbm_cleanrowend_packed(nextrow, cols);
+        } else
+            /* Bottom of image.  */
+            nextrow = edgerow;
+
+        setFlags(prevrow, thisrow, nextrow, flags, cols);
+
+        expandRow(thisrow, prevrow, outrow, flags, cols, cmdline.scale,
+                  cutoff, ucutoff);
     }
+    pbm_freearray(buffer,3);
+    pbm_freerow(edgerow);
+    pbm_freerow(outrow);
+    free (flags);
     pm_close(ifP);
     return 0;
 }
diff --git a/editor/pgmdeshadow.c b/editor/pgmdeshadow.c
index 948c6cba..2c3a90f8 100644
--- a/editor/pgmdeshadow.c
+++ b/editor/pgmdeshadow.c
@@ -21,7 +21,7 @@
 */
 
 /*
- * Algorithm reference: Luc Vincent, "Morphological Grayscale Reruction
+ * Algorithm reference: Luc Vincent, "Morphological Grayscale Reconstruction
  * in Image Analysis: Applications and Efficient Algorithms," IEEE
  * Transactions on Image Processing, vol. 2, no. 2, April 1993, pp. 176-201.
  *
@@ -75,7 +75,7 @@ parseCommandLine(int argc, char ** argv,
    was passed to us as the argv array.
 -----------------------------------------------------------------------------*/
     optEntry * option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -91,7 +91,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We may have parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (argc-1 < 1)
diff --git a/editor/pgmmedian.c b/editor/pgmmedian.c
index f911475d..9d90b6b3 100644
--- a/editor/pgmmedian.c
+++ b/editor/pgmmedian.c
@@ -66,7 +66,7 @@ parseCommandLine(int argc, char ** argv,
    was passed to us as the argv array.
 -----------------------------------------------------------------------------*/
     optEntry * option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -91,7 +91,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We may have parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (!widthSpec)
diff --git a/editor/pnmcat.c b/editor/pnmcat.c
index 3cf443bb..a26dcf3e 100644
--- a/editor/pnmcat.c
+++ b/editor/pnmcat.c
@@ -16,6 +16,7 @@
 #include "mallocvar.h"
 #include "shhopt.h"
 #include "bitarith.h"
+#include "nstring.h"
 #include "pnm.h"
 
 #define LEFTBITS pm_byteLeftBits
@@ -89,9 +90,11 @@ parseCommandLine(int argc, const char ** const argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    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. */
 
+    free(option_def);
+
     if (leftright + topbottom > 1)
         pm_error("You may specify only one of -topbottom (-tb) and "
                  "-leftright (-lr)");
@@ -153,12 +156,20 @@ parseCommandLine(int argc, const char ** const argv,
         cmdlineP->nfiles = 1;
     } else {
         unsigned int i;
+        unsigned int stdinCt;
+            /* Number of input files user specified as Standard Input */
 
         MALLOCARRAY_NOFAIL(cmdlineP->inputFilespec, argc-1);
 
-        for (i = 0; i < argc-1; ++i)
+        for (i = 0, stdinCt = 0; i < argc-1; ++i) {
             cmdlineP->inputFilespec[i] = argv[1+i];
+            if (streq(argv[1+i], "-"))
+                ++stdinCt;
+        }
         cmdlineP->nfiles = argc-1;
+        if (stdinCt > 1)
+            pm_error("At most one input image can come from Standard Input.  "
+                     "You specified %u", stdinCt);
     }
 }
 
@@ -418,6 +429,8 @@ concatenateLeftRightPbm(FILE *             const ofP,
 
     getPbmImageInfo(img, nfiles, newrows, justification, backcolor, &img2);
 
+    outrow[pbm_packed_bytes(newcols)-1] = 0x00;
+
     for (row = 0; row < newrows; ++row) {
         unsigned int i;
 
@@ -556,7 +569,7 @@ concatenateTopBottomPbm(FILE *             const ofP,
 
         backgroundPrev = background;
     }
-    free(outrow);
+    pbm_freerow_packed(outrow);
 }
 
 
@@ -851,6 +864,7 @@ main(int           argc,
     for (i = 0; i < cmdline.nfiles; ++i)
         pm_close(img[i].ifP);
     free(cmdline.inputFilespec);
+    free(img);
     pm_close(stdout);
 
     return 0;
diff --git a/editor/pnmcomp.c b/editor/pnmcomp.c
deleted file mode 100644
index 04bb5365..00000000
--- a/editor/pnmcomp.c
+++ /dev/null
@@ -1,460 +0,0 @@
-/* +-------------------------------------------------------------------+ */
-/* | Copyright 1992, David Koblas.                                     | */
-/* |   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.           | */
-/* +-------------------------------------------------------------------+ */
-
-/*
-
-    DON'T ADD NEW FUNCTION TO THIS PROGRAM.  ADD IT TO pamcomp.c INSTEAD.
-
-*/
-
-
-
-#define _BSD_SOURCE    /* Make sure strcasecmp() is in string.h */
-#include <string.h>
-
-#include "pm_c_util.h"
-#include "pnm.h"
-#include "shhopt.h"
-#include "mallocvar.h"
-
-enum horizPos {BEYONDLEFT, LEFT, CENTER, RIGHT, BEYONDRIGHT};
-enum vertPos {ABOVE, TOP, MIDDLE, BOTTOM, BELOW};
-
-
-struct cmdlineInfo {
-    /* All the information the user supplied in the command line,
-       in a form easy for the program to use.
-    */
-    const char *underlyingFilespec;  /* '-' if stdin */
-    const char *overlayFilespec;
-    const char *alphaFilespec;
-    const char *outputFilespec;  /* '-' if stdout */
-    int xoff, yoff;   /* value of xoff, yoff options */
-    float opacity;
-    unsigned int alphaInvert;
-    enum horizPos align;
-    enum vertPos valign;
-};
-
-
-
-
-static void
-parseCommandLine(int argc, 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 optParseOptions3 on how to parse our options.
-         */
-    optStruct3 opt;
-
-    unsigned int option_def_index;
-
-    char *align, *valign;
-    unsigned int xoffSpec, yoffSpec, alignSpec, valignSpec, opacitySpec,
-        alphaSpec;
-
-    MALLOCARRAY_NOFAIL(option_def, 100);
-
-    option_def_index = 0;   /* incremented by OPTENT3 */
-    OPTENT3(0, "invert",     OPT_FLAG,   NULL,                  
-            &cmdlineP->alphaInvert,       0 );
-    OPTENT3(0, "xoff",       OPT_INT,    &cmdlineP->xoff,       
-            &xoffSpec,       0 );
-    OPTENT3(0, "yoff",       OPT_INT,    &cmdlineP->yoff,       
-            &yoffSpec,       0 );
-    OPTENT3(0, "opacity",    OPT_FLOAT, &cmdlineP->opacity,
-            &opacitySpec,       0 );
-    OPTENT3(0, "alpha",      OPT_STRING, &cmdlineP->alphaFilespec,
-            &alphaSpec,  0 );
-    OPTENT3(0, "align",      OPT_STRING, &align,
-            &alignSpec,  0 );
-    OPTENT3(0, "valign",     OPT_STRING, &valign,
-            &valignSpec,  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 */
-
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0);
-        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
-
-
-    if (!xoffSpec)
-        cmdlineP->xoff = 0;
-    if (!yoffSpec)
-        cmdlineP->yoff = 0;
-    if (!alphaSpec)
-        cmdlineP->alphaFilespec = NULL;
-
-    if (alignSpec) {
-        if (strcasecmp(align, "BEYONDLEFT") == 0)
-            cmdlineP->align = BEYONDLEFT;
-        else if (strcasecmp(align, "LEFT") == 0)
-            cmdlineP->align = LEFT;
-        else if (strcasecmp(align, "CENTER") == 0)
-            cmdlineP->align = CENTER;
-        else if (strcasecmp(align, "RIGHT") == 0)
-            cmdlineP->align = RIGHT;
-        else if (strcasecmp(align, "BEYONDRIGHT") == 0)
-            cmdlineP->align = BEYONDRIGHT;
-        else
-            pm_error("Invalid value for align option: '%s'.  Only LEFT, "
-                     "RIGHT, CENTER, BEYONDLEFT, and BEYONDRIGHT are valid.", 
-                     align);
-    } else 
-        cmdlineP->align = LEFT;
-
-    if (valignSpec) {
-        if (strcasecmp(valign, "ABOVE") == 0)
-            cmdlineP->valign = ABOVE;
-        else if (strcasecmp(valign, "TOP") == 0)
-            cmdlineP->valign = TOP;
-        else if (strcasecmp(valign, "MIDDLE") == 0)
-            cmdlineP->valign = MIDDLE;
-        else if (strcasecmp(valign, "BOTTOM") == 0)
-            cmdlineP->valign = BOTTOM;
-        else if (strcasecmp(valign, "BELOW") == 0)
-            cmdlineP->valign = BELOW;
-        else
-            pm_error("Invalid value for valign option: '%s'.  Only TOP, "
-                     "BOTTOM, MIDDLE, ABOVE, and BELOW are valid.", 
-                     align);
-    } else 
-        cmdlineP->valign = TOP;
-
-    if (!opacitySpec) 
-        cmdlineP->opacity = 1.0;
-
-    if (argc-1 < 1)
-        pm_error("Need at least one argument: file specification of the "
-                 "overlay image.");
-
-    cmdlineP->overlayFilespec = argv[1];
-
-    if (argc-1 >= 2)
-        cmdlineP->underlyingFilespec = argv[2];
-    else
-        cmdlineP->underlyingFilespec = "-";
-
-    if (argc-1 >= 3)
-        cmdlineP->outputFilespec = argv[3];
-    else
-        cmdlineP->outputFilespec = "-";
-
-    if (argc-1 > 3)
-        pm_error("Too many arguments.  Only acceptable arguments are: "
-                 "overlay image, underlying image, output image");
-}
-
-
-
-
-static void
-warnOutOfFrame( int const originLeft,
-                int const originTop, 
-                int const overCols,
-                int const overRows,
-                int const underCols,
-                int const underRows ) {
-    if (originLeft >= underCols)
-        pm_message("WARNING: the overlay is entirely off the right edge "
-                   "of the underlying image.  "
-                   "It will not be visible in the result.  The horizontal "
-                   "overlay position you selected is %d, "
-                   "and the underlying image "
-                   "is only %d pixels wide.", originLeft, underCols );
-    else if (originLeft + overCols <= 0)
-        pm_message("WARNING: the overlay is entirely off the left edge "
-                   "of the underlying image.  "
-                   "It will not be visible in the result.  The horizontal "
-                   "overlay position you selected is %d and the overlay is "
-                   "only %d pixels wide.", originLeft, overCols);
-    else if (originTop >= underRows)
-        pm_message("WARNING: the overlay is entirely off the bottom edge "
-                   "of the underlying image.  "
-                   "It will not be visible in the result.  The vertical "
-                   "overlay position you selected is %d, "
-                   "and the underlying image "
-                   "is only %d pixels high.", originTop, underRows );
-    else if (originTop + overRows <= 0)
-        pm_message("WARNING: the overlay is entirely off the top edge "
-                   "of the underlying image.  "
-                   "It will not be visible in the result.  The vertical "
-                   "overlay position you selected is %d and the overlay is "
-                   "only %d pixels high.", originTop, overRows);
-}
-
-
-
-static void
-computeOverlayPosition(const int underCols, const int underRows,
-                       const int overCols, const int overRows,
-                       const struct cmdlineInfo cmdline, 
-                       int * const originLeftP,
-                       int * const originTopP) {
-/*----------------------------------------------------------------------------
-   Determine where to overlay the overlay image, based on the options the
-   user specified and the realities of the image dimensions.
-
-   The origin may be outside the underlying image (so e.g. *originLeftP may
-   be negative or > image width).  That means not all of the overlay image
-   actually gets used.  In fact, there may be no overlap at all.
------------------------------------------------------------------------------*/
-    int xalign, yalign;
-
-    switch (cmdline.align) {
-    case BEYONDLEFT:  xalign = -overCols;              break;
-    case LEFT:        xalign = 0;                      break;
-    case CENTER:      xalign = (underCols-overCols)/2; break;
-    case RIGHT:       xalign = underCols - overCols;   break;
-    case BEYONDRIGHT: xalign = underCols;              break;
-    }
-    switch (cmdline.valign) {
-    case ABOVE:       yalign = -overRows;              break;
-    case TOP:         yalign = 0;                      break;
-    case MIDDLE:      yalign = (underRows-overRows)/2; break;
-    case BOTTOM:      yalign = underRows - overRows;   break;
-    case BELOW:       yalign = underRows;              break;
-    }
-    *originLeftP = xalign + cmdline.xoff;
-    *originTopP  = yalign + cmdline.yoff;
-
-    warnOutOfFrame( *originLeftP, *originTopP, 
-                    overCols, overRows, underCols, underRows );    
-}
-
-
-
-static pixval
-composeComponents(pixval const compA, 
-                  pixval const compB,
-                  float  const distrib,
-                  pixval const maxval) {
-/*----------------------------------------------------------------------------
-  Compose a single component of each of two pixels, with 'distrib' being
-  the fraction of 'compA' in the result, 1-distrib the fraction of 'compB'.
-  
-  Both inputs are based on a maxval of 'maxval', and so is our result.
-  
-  Note that while 'distrib' in the straightforward case is always in
-  [0,1], it can in fact be negative or greater than 1.  We clip the
-  result as required to return a legal pixval.
------------------------------------------------------------------------------*/
-    return MIN(maxval, MAX(0, (int)compA * distrib +
-                              (int)compB * (1.0 - distrib) + 
-                              0.5
-                          )
-              );
-}
-
-
-
-static pixel
-composePixels(pixel  const pixelA,
-              pixel  const pixelB,
-              float  const distrib,
-              pixval const maxval) {
-/*----------------------------------------------------------------------------
-  Compose two pixels 'pixelA' and 'pixelB', with 'distrib' being the
-  fraction of 'pixelA' in the result, 1-distrib the fraction of 'pixelB'.
-
-  Both inputs are based on a maxval of 'maxval', and so is our result.
-  
-  Note that while 'distrib' in the straightforward case is always in
-  [0,1], it can in fact be negative or greater than 1.  We clip the
-  result as required to return a legal pixval.
------------------------------------------------------------------------------*/
-    pixel retval;
-
-    pixval const red = 
-        composeComponents(PPM_GETR(pixelA), PPM_GETR(pixelB), distrib, maxval);
-    pixval const grn =
-        composeComponents(PPM_GETG(pixelA), PPM_GETG(pixelB), distrib, maxval);
-    pixval const blu = 
-        composeComponents(PPM_GETB(pixelA), PPM_GETB(pixelB), distrib, maxval);
-
-    PPM_ASSIGN(retval, red, grn, blu);
-
-    return retval;
-}
-
-
-
-static void
-composite(int      const originleft, 
-          int      const origintop, 
-          pixel ** const overlayImage, 
-          int      const overlayCols, 
-          int      const overlayRows,
-          xelval   const overlayMaxval, 
-          int      const overlayType,
-          int      const cols, 
-          int      const rows, 
-          xelval   const maxval, 
-          int      const type,
-          gray **  const alpha, 
-          gray     const alphaMax, 
-          bool     const invertAlpha,
-          float    const opacity,
-          FILE *   const ifp, 
-          FILE *   const ofp) {
-/*----------------------------------------------------------------------------
-   Overlay the overlay image 'overlayImage' onto the underlying image
-   which is in file 'ifp', and output the composite to file 'ofp'.
-
-   The underlying image file 'ifp' is positioned after its header.  The
-   width, height, format, and maxval of the underlying image are 'cols',
-   'rows', 'type', and 'maxval'.
-
-   The width, height, format, and maxval of the overlay image are
-   overlayCols, overlayRows, overlayType and overlayMaxval.
-
-   'originleft' and 'origintop' are the coordinates in the underlying
-   image plane where the top left corner of the overlay image is
-   to go.  It is not necessarily inside the underlying image (in fact,
-   may be negative).  Only the part of the overlay that actually intersects
-   the underlying image, if any, gets into the output.
-
-   Note that we modify the overlay image 'overlayImage' to change its
-   format and maxval to the format and maxval of the output.
------------------------------------------------------------------------------*/
-    /* otype and oxmaxv are the type and maxval for the composed (output)
-       image, and are derived from that of the underlying and overlay
-       images.
-    */
-    int    const otype = (overlayType < type) ? type : overlayType;
-    xelval const omaxv = pm_lcm(maxval, overlayMaxval, 1, PNM_OVERALLMAXVAL);
-
-    int     row;
-    xel     *pixelrow;
-
-    pixelrow = pnm_allocrow(cols);
-
-    if (overlayType != otype || overlayMaxval != omaxv) {
-        pnm_promoteformat(overlayImage, overlayCols, overlayRows,
-                          overlayMaxval, overlayType, omaxv, otype);
-    }
-
-    pnm_writepnminit(ofp, cols, rows, omaxv, otype, 0);
-
-    for (row = 0; row < rows; ++row) {
-        int col;
-
-        /* Read a row and convert it to the output type */
-        pnm_readpnmrow(ifp, pixelrow, cols, maxval, type);
-
-        if (type != otype || maxval != omaxv)
-            pnm_promoteformatrow(pixelrow, cols, maxval, type, omaxv, otype);
-
-        /* Now overlay the overlay with alpha (if defined) */
-        for (col = 0; col < cols; ++col) {
-            int const ovlcol = col - originleft;
-            int const ovlrow = row - origintop;
-
-            double overlayWeight;
-
-            if (ovlcol >= 0 && ovlcol < overlayCols &&
-                ovlrow >= 0 && ovlrow < overlayRows) {
-
-                if (alpha == NULL) {
-                    overlayWeight = opacity;
-                } else {
-                    double alphaval;
-                    alphaval = 
-                        (double)alpha[ovlrow][ovlcol] / (double)alphaMax;
-                    if (invertAlpha)
-                        alphaval = 1.0 - alphaval;
-                    overlayWeight = alphaval * opacity;
-                }
-
-                pixelrow[col] = composePixels(overlayImage[ovlrow][ovlcol],
-                                              pixelrow[col], 
-                                              overlayWeight, omaxv);
-            }
-        }
-        pnm_writepnmrow(ofp, pixelrow, cols, omaxv, otype, 0);
-    }
-    pnm_freerow(pixelrow);
-}
-
-
-
-int
-main(int argc, char *argv[]) {
-
-    FILE    *ifp, *ofp;
-    pixel   **image;
-    int     imageCols, imageRows, imageType;
-    xelval  imageMax;
-    int     cols, rows, type;
-    xelval  maxval;
-    gray    **alpha;
-    int     alphaCols, alphaRows;
-    xelval  alphaMax;
-    struct cmdlineInfo cmdline;
-    int originLeft, originTop;
-
-    pnm_init(&argc, argv);
-
-    parseCommandLine(argc, argv, &cmdline);
-        
-    { /* Read the overlay image into 'image' */
-        FILE *fp;
-        fp = pm_openr(cmdline.overlayFilespec);
-        image = 
-            pnm_readpnm(fp, &imageCols, &imageRows, &imageMax, &imageType);
-        pm_close(fp);
-    }
-    if (cmdline.alphaFilespec) {
-        /* Read the alpha mask file into 'alpha' */
-        FILE *fp = pm_openr(cmdline.alphaFilespec);
-        alpha = pgm_readpgm(fp, &alphaCols, &alphaRows, &alphaMax);
-        pm_close(fp);
-            
-        if (imageCols != alphaCols || imageRows != alphaRows)
-            pm_error("Alpha map and overlay image are not the same size");
-    } else
-        alpha = NULL;
-
-    ifp = pm_openr(cmdline.underlyingFilespec);
-
-    ofp = pm_openw(cmdline.outputFilespec);
-
-    pnm_readpnminit(ifp, &cols, &rows, &maxval, &type);
-
-    computeOverlayPosition(cols, rows, imageCols, imageRows, 
-                           cmdline, &originLeft, &originTop);
-
-    composite(originLeft, originTop,
-              image, imageCols, imageRows, imageMax, imageType, 
-              cols, rows, maxval, type, 
-              alpha, alphaMax, cmdline.alphaInvert, cmdline.opacity,
-              ifp, ofp);
-
-    pm_close(ifp);
-    pm_close(ofp);
-
-    /* If the program failed, it previously aborted with nonzero completion
-       code, via various function calls.
-    */
-    return 0;
-}
-
-
diff --git a/editor/pnmconvol.c b/editor/pnmconvol.c
index 2033a1a3..8d9bb83a 100644
--- a/editor/pnmconvol.c
+++ b/editor/pnmconvol.c
@@ -1,6 +1,4 @@
-/* pnmconvol.c - general MxN convolution on a PNM image
-**
-** Version 2.0.1 January 30, 1995
+/* pnmconvol.c - general MxN convolution on a Netpbm image
 **
 ** Major rewriting by Mike Burns
 ** Copyright (C) 1994, 1995 by Mike Burns (burns@chem.psu.edu)
@@ -17,22 +15,297 @@
 
 /* A change history is at the bottom */
 
+#include <stdlib.h>
 #include <assert.h>
 
 #include "pm_c_util.h"
-#include "pnm.h"
-#include "shhopt.h"
 #include "mallocvar.h"
+#include "nstring.h"
+#include "token.h"
+#include "io.h"
+#include "shhopt.h"
+#include "pam.h"
+
+
+
+static sample const
+clipSample(sample const unclipped,
+           sample const maxval) {
+
+    return MIN(maxval, unclipped);
+}
+
+
+
+static sample const
+makeSample(float  const arg,
+           sample const maxval) {
+/*----------------------------------------------------------------------------
+   From a tentative sample value that could be fractional or negative,
+   produce an actual sample value by rounding and clipping.
+-----------------------------------------------------------------------------*/
+    return MIN(maxval, ROUNDU(MAX(0.0, arg)));
+}
+
+
+
+static void
+validateKernelDimensions(unsigned int const width,
+                         unsigned int const height) {
+
+    if (height == 0)
+        pm_error("Convolution matrix height is zero");
+    if (width == 0)
+        pm_error("Convolution matrix width is zero");
+
+    if (height % 2 != 1)
+        pm_error("The convolution matrix must have an odd number of rows.  "
+                 "Yours has %u", height);
+
+    if (width % 2 != 1)
+        pm_error("The convolution matrix must have an odd number of columns.  "
+                 "Yours has %u", width);
+}
+
+
+
+struct matrixOpt {
+    unsigned int width;
+    unsigned int height;
+    float ** weight;
+};
+
+
+
 
 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 *kernelFilespec;
+    const char * inputFileName;  /* '-' if stdin */
+    const char * pnmMatrixFileName;
     unsigned int nooffset;
+    const char ** matrixfile;
+    unsigned int matrixSpec;
+    struct matrixOpt matrix;
+    unsigned int normalize;
+    unsigned int bias;
 };
 
+
+
+static void
+countMatrixOptColumns(const char *   const rowString,
+                      unsigned int * const colCtP) {
+
+    const char * cursor;
+    unsigned int colCt;
+
+    for (cursor = &rowString[0], colCt = 0; *cursor; ) {
+        const char * colString;
+        const char * next;
+        const char * error;
+
+        pm_gettoken(cursor, ',', &colString, &next, &error);
+
+        if (error) {
+            pm_error("Unable to parse -matrix value row '%s'.  %s",
+                     rowString, error);
+            pm_strfree(error);
+        } else {
+            ++colCt;
+
+            cursor = next;
+            if (*cursor) {
+                assert(*cursor == ',');
+                ++cursor;  /* advance over comma to next column */
+            }
+            pm_strfree(colString);
+        }
+    }
+    *colCtP = colCt;
+}
+
+
+
+static void
+getMatrixOptDimensions(const char *   const matrixOptString,
+                       unsigned int * const widthP,
+                       unsigned int * const heightP) {
+/*----------------------------------------------------------------------------
+   Given the value of a -matrix option, 'matrixOptString', return the
+   height and width of the matrix it describes.
+
+   If it's not valid enough to determine that (e.g. it has rows of different
+   widths), abort.
+
+   An example of 'matrixOptString':
+
+     ".04,.15,.04;.15,.24,.15;.04,.15,.04"
+-----------------------------------------------------------------------------*/
+    unsigned int rowCt;
+    const char * cursor;
+
+    for (cursor = &matrixOptString[0], rowCt = 0; *cursor; ) {
+        const char * rowString;
+        const char * next;
+        const char * error;
+
+        pm_gettoken(cursor, ';', &rowString, &next, &error);
+
+        if (error) {
+            pm_error("Unable to parse -matrix value '%s'.  %s",
+                     matrixOptString, error);
+            pm_strfree(error);
+        } else {
+            unsigned int colCt;
+            ++rowCt;
+
+            countMatrixOptColumns(rowString, &colCt);
+
+            if (rowCt == 1)
+                *widthP = colCt;
+            else {
+                if (colCt != *widthP)
+                pm_error("-matrix option value contains rows of different "
+                         "widths: %u and %u", *widthP, colCt);
+            }            
+            pm_strfree(rowString);
+            cursor = next;
+
+            if (*cursor) {
+                assert(*cursor == ';');
+                ++cursor;  /* advance cursor over semicolon to next row */
+            }
+        }
+    }
+    *heightP = rowCt;
+}
+
+
+
+static void
+parseMatrixRow(const char * const matrixOptRowString,
+               unsigned int const width,
+               float *      const weight) {
+
+    unsigned int col;
+    const char * cursor;
+
+    for (col = 0, cursor = &matrixOptRowString[0]; col < width; ++col) {
+        const char * colString;
+        const char * next;
+        const char * error;
+
+        pm_gettoken(cursor, ',', &colString, &next, &error);
+
+        if (error) {
+            pm_error("Failed parsing a row in the -matrix value.  %s", error);
+            pm_strfree(error);
+        } else {
+            if (colString[0] == '\0')
+                pm_error("The Column %u element of the row '%s' in the "
+                         "-matrix value is a null string", col,
+                         matrixOptRowString);
+            else {
+                char * trailingJunk;
+                weight[col] = strtod(colString, &trailingJunk);
+
+                if (*trailingJunk != '\0') 
+                    pm_error("The Column %u element of the row '%s' in the "
+                             "-matrix value is not a valid floating point "
+                             "number", col, matrixOptRowString);
+            }
+            pm_strfree(colString);
+
+            cursor = next;
+
+            if (*cursor) {
+                assert(*cursor == ',');
+                ++cursor;  /* advance over comma to next column */
+            }
+        }
+    }
+}
+
+
+
+static void
+parseMatrixOptWithDimensions(const char * const matrixOptString,
+                             unsigned int const width,
+                             unsigned int const height,
+                             float **     const weight) {
+    
+    unsigned int row;
+    const char * cursor;
+
+    for (row = 0, cursor = &matrixOptString[0]; row < height; ++row) {
+        const char * rowString;
+        const char * next;
+        const char * error;
+
+        pm_gettoken(cursor, ';', &rowString, &next, &error);
+
+        if (error) {
+            pm_error("Failed parsing -matrix value.  %s", error);
+            pm_strfree(error);
+        } else {
+            parseMatrixRow(rowString, width, weight[row]);
+
+            pm_strfree(rowString);
+
+            cursor = next;
+
+            if (*cursor) {
+                assert(*cursor == ';');
+                ++cursor;  /* advance over semicolon to next row */
+            }
+        }
+    }
+}    
+
+
+
+static void
+parseMatrixOpt(const char *         const matrixOptString,
+               struct matrixOpt *   const matrixOptP) {
+/*----------------------------------------------------------------------------
+   An example of 'matrixOptString':
+
+     ".04,.15,.04;.15,.24,.15;.04,.15,.04"
+
+-----------------------------------------------------------------------------*/
+    unsigned int width, height;
+
+    getMatrixOptDimensions(matrixOptString, &width, &height);
+
+    validateKernelDimensions(width, height);
+
+    matrixOptP->height = height;
+    matrixOptP->width  = width;
+
+    {
+        unsigned int row;
+        MALLOCARRAY_NOFAIL(matrixOptP->weight, height);
+        for (row = 0; row < height; ++row)
+            MALLOCARRAY_NOFAIL(matrixOptP->weight[row], width);
+    }
+    parseMatrixOptWithDimensions(matrixOptString, width, height,
+                                 matrixOptP->weight);
+}
+
+
+
+static void
+validateMatrixfileOpt(const char ** const matrixFileOpt) {
+
+    if (matrixFileOpt[0] == NULL)
+        pm_error("You specified an empty string as the value of "
+                 "-matrixfile.  You must specify at least one file name");
+}
+
+
+
 static void
 parseCommandLine(int argc, char ** argv,
                  struct cmdlineInfo * const cmdlineP) {
@@ -47,1855 +320,1929 @@ parseCommandLine(int argc, char ** argv,
    was passed to us as the argv array.  We also trash *argv.
 -----------------------------------------------------------------------------*/
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
     unsigned int option_def_index;
+    unsigned int matrixfileSpec;
+    const char * matrixOpt;
+    unsigned int biasSpec;
 
     MALLOCARRAY_NOFAIL(option_def, 100);
 
     option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "matrix",       OPT_STRING, &matrixOpt,
+            &cmdlineP->matrixSpec,     0)
+    OPTENT3(0, "matrixfile",   OPT_STRINGLIST, &cmdlineP->matrixfile,
+            &matrixfileSpec,           0)
     OPTENT3(0, "nooffset",     OPT_FLAG,   NULL,                  
-            &cmdlineP->nooffset,       0 );
+            &cmdlineP->nooffset,       0);
+    OPTENT3(0, "normalize",    OPT_FLAG,   NULL,                  
+            &cmdlineP->normalize,      0);
+    OPTENT3(0, "bias",         OPT_UINT,   &cmdlineP->bias,
+            &biasSpec,                 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 */
 
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3( &argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
-    if (argc-1 < 1)
-        pm_error("Need at least one argument: file specification of the "
-                 "convolution kernel image.");
+    if (!biasSpec)
+        cmdlineP->bias = 0;
 
-    cmdlineP->kernelFilespec = argv[1];
+    if (matrixfileSpec && cmdlineP->matrixSpec)
+        pm_error("You can't specify by -matrix and -matrixfile");
 
-    if (argc-1 >= 2)
-        cmdlineP->inputFilespec = argv[2];
+    if (cmdlineP->matrixSpec)
+        parseMatrixOpt(matrixOpt, &cmdlineP->matrix);
+
+    if (matrixfileSpec)
+        validateMatrixfileOpt(cmdlineP->matrixfile);
     else
-        cmdlineP->inputFilespec = "-";
+        cmdlineP->matrixfile = NULL;
+
+    if (matrixfileSpec || cmdlineP->matrixSpec) {
+        if (cmdlineP->nooffset)
+            pm_error("-nooffset is meaningless and not allowed with "
+                     "-matrix or -matrixfile");
+
+        cmdlineP->pnmMatrixFileName = NULL;
 
-    if (argc-1 > 2)
-        pm_error("Too many arguments.  Only acceptable arguments are: "
-                 "convolution file name and input file name");
+        if (argc-1 >= 1)
+            cmdlineP->inputFileName = argv[1];
+        else
+            cmdlineP->inputFileName = "-";
+
+        if (argc-1 > 1)
+            pm_error("Too many arguments.  When you specify -matrix "
+                     "or -matrixfile, the only allowable non-option "
+                     "argument is the input file name");
+    } else {
+        /* It's an old style invocation we accept for backward compatibility */
+        
+        if (argc-1 < 1)
+            pm_error("You must specify either -matrix or -matrixfile "
+                     "at least one argument which names an old-style PGM "
+                     "convolution matrix file.");
+        else {
+            cmdlineP->pnmMatrixFileName = argv[1];
+
+            if (argc-1 >= 2)
+                cmdlineP->inputFileName = argv[2];
+            else
+                cmdlineP->inputFileName = "-";
+            
+            if (argc-1 > 2)
+                pm_error("Too many arguments.  Only acceptable arguments are: "
+                         "convolution matrix file name and input file name");
+        }
+    }
 }
 
 
-/* Macros to verify that r,g,b values are within proper range */
 
-#define CHECK_GRAY \
-    if (tempgsum < 0L) g = 0; \
-    else if (tempgsum > maxval) g = maxval; \
-    else g = tempgsum;
+struct ConvKernel {
+    unsigned int cols;
+        /* Width of the convolution window */
+    unsigned int rows;
+        /* Height of the convolution window */
+    unsigned int planes;
+        /* Depth of the kernel -- this had better be the same as the
+           depth of the image being convolved.
+        */
+    float ** weight[3];
+        /* weight[PLANE][ROW][COL] is the weight to give to Plane PLANE
+           of the pixel at row ROW, column COL within the convolution window.
+           
+           One means full weight.
+
+           It can have magnitude greater than or less than one.  It can be
+           positive or negative.  
+        */
+    unsigned int bias;
+        /* The amount to be added to the linear combination of sample values.
+           We take a little liberty with the term "convolution kernel" to
+           include this value, since convolution per se does not involve any
+           such biasing.
+        */
+};
 
-#define CHECK_RED \
-    if (temprsum < 0L) r = 0; \
-    else if (temprsum > maxval) r = maxval; \
-    else r = temprsum;
 
-#define CHECK_GREEN \
-    if (tempgsum < 0L) g = 0; \
-    else if (tempgsum > maxval) g = maxval; \
-    else g = tempgsum;
 
-#define CHECK_BLUE \
-    if (tempbsum < 0L) b = 0; \
-    else if (tempbsum > maxval) b = maxval; \
-    else b = tempbsum;
+static void
+warnBadKernel(struct ConvKernel * const convKernelP) {
 
-struct convolveType {
-    void (*ppmConvolver)(const float ** const rweights,
-                         const float ** const gweights,
-                         const float ** const bweights);
-    void (*pgmConvolver)(const float ** const weights);
-};
+    float sum[3];
+    unsigned int plane;
+    unsigned int row;
 
-static FILE * ifp;
-static int crows, ccols, ccolso2, crowso2;
-static int cols, rows;
-static xelval maxval;
-static int format, newformat;
+    for (plane = 0; plane < convKernelP->planes; ++plane)
+        sum[plane] = 0.0; /* initial value */
+    
+    for (row = 0; row < convKernelP->rows; ++row) {
+        unsigned int col;
+        for (col = 0; col < convKernelP->cols; ++col) {
+            unsigned int plane;
+            for (plane = 0; plane < convKernelP->planes; ++plane)
+                sum[plane] += convKernelP->weight[plane][row][col];
+        }
+    }
+
+    if (convKernelP->planes == 3) {
+        unsigned int plane;
+        bool biased, negative;
+        for (plane = 0, biased = false, negative = false;
+             plane < convKernelP->planes;
+             ++plane) {
+            if (sum[plane] < 0.9 || sum[plane] > 1.1)
+                biased = true;
+            if (sum[plane] < 0.0)
+                negative = true;
+        }
+    
+        if (biased) {
+            pm_message("WARNING - this convolution matrix is biased.  " 
+                       "red, green, and blue average weights: %f, %f, %f "
+                       "(unbiased would be 1).",
+                       sum[PAM_RED_PLANE],
+                       sum[PAM_GRN_PLANE],
+                       sum[PAM_BLU_PLANE]);
+
+            if (negative)
+                pm_message("Maybe you want the -nooffset option?");
+        }
+    } else if (convKernelP->planes == 1) {
+        if (sum[0] < 0.9 || sum[0] > 1.1)
+            pm_message("WARNING - this convolution matrix is biased.  "
+                       "average weight = %f (unbiased would be 1)",
+                       sum[0]);
+        if (sum[0] < 0.0)
+            pm_message("Maybe you want the -nooffset option?");
+    }
+}
 
 
 
 static void
-computeWeights(xel * const *   const cxels, 
-               int             const ccols, 
-               int             const crows,
-               int             const cformat, 
-               xelval          const cmaxval,
-               bool            const offsetPgm,
-               float ***       const rweightsP,
-               float ***       const gweightsP,
-               float ***       const bweightsP) {
+convKernelCreatePnm(struct pam *         const cpamP,
+                    tuple * const *      const ctuples, 
+                    unsigned int         const depth,
+                    bool                 const offsetPnm,
+                    struct ConvKernel ** const convKernelPP) {
 /*----------------------------------------------------------------------------
-   Compute the convolution matrix in normalized form from the PGM
-   form.  Each element of the output matrix is the actual weight we give an
-   input pixel -- i.e. the thing by which we multiple a value from the
-   input image.
-
-   'offsetPgm' means the PGM convolution matrix is defined in offset form so
+   Compute the convolution matrix in normalized form from the PGM form
+   'ctuples'/'cpamP'.  Each element of the output matrix is the actual weight
+   we give an input pixel -- i.e. the thing by which we multiple a value from
+   the input image.
+
+   'depth' is the required number of planes in the kernel.  If 'ctuples' has
+   fewer planes than that, we duplicate as necessary.  E.g. if 'ctuples' is
+   from a PGM input file and we're convolving a PPM image, we'll make a
+   3-plane convolution kernel by repeating the one plane in 'ctuples'.  If
+   'ctuples' has more planes than specified, we ignore the higher numbered
+   ones.
+
+   'offsetPnm' means the PNM convolution matrix is defined in offset form so
    that it can represent negative values.  E.g. with maxval 100, 50 means
    0, 100 means 50, and 0 means -50.  If 'offsetPgm' is false, 0 means 0
    and there are no negative weights.
 -----------------------------------------------------------------------------*/
-    double const scale = (offsetPgm ? 2.0 : 1.0) / cmaxval;
-    double const offset = offsetPgm ? - 1.0 : 0.0;
+    double const scale = (offsetPnm ? 2.0 : 1.0) / cpamP->maxval;
+    double const offset = offsetPnm ? - 1.0 : 0.0;
+    unsigned int const planes = MIN(3, depth);
 
-    float** rweights;
-    float** gweights;
-    float** bweights;
+    struct ConvKernel * convKernelP;
+    unsigned int plane;
 
-    float rsum, gsum, bsum;
+    MALLOCVAR_NOFAIL(convKernelP);
 
-    unsigned int crow;
+    convKernelP->cols   = cpamP->width;
+    convKernelP->rows   = cpamP->height;
+    convKernelP->planes = planes;
 
-    /* Set up the normalized weights. */
-    rweights = (float**) pm_allocarray(ccols, crows, sizeof(float));
-    gweights = (float**) pm_allocarray(ccols, crows, sizeof(float));
-    bweights = (float**) pm_allocarray(ccols, crows, sizeof(float));
+    for (plane = 0; plane < planes; ++plane) {
+        unsigned int row;
 
-    rsum = gsum = bsum = 0.0;  /* initial value */
+        MALLOCARRAY_NOFAIL(convKernelP->weight[plane], cpamP->height);
     
-    for (crow = 0; crow < crows; ++crow) {
-        unsigned int ccol;
-        for (ccol = 0; ccol < ccols; ++ccol) {
-            switch (PNM_FORMAT_TYPE(cformat)) {
-            case PPM_TYPE:
-                rsum += rweights[crow][ccol] =
-                    (PPM_GETR(cxels[crow][ccol]) * scale + offset);
-                gsum += gweights[crow][ccol] =
-                    (PPM_GETG(cxels[crow][ccol]) * scale + offset);
-                bsum += bweights[crow][ccol] =
-                    (PPM_GETB(cxels[crow][ccol]) * scale + offset);
-                break;
-                
-            default:
-                gsum += gweights[crow][ccol] =
-                    (PNM_GET1(cxels[crow][ccol]) * scale + offset);
-                break;
+        for (row = 0; row < cpamP->height; ++row) {
+            unsigned int col;
+
+            MALLOCARRAY_NOFAIL(convKernelP->weight[plane][row], cpamP->width);
+
+            for (col = 0; col < cpamP->width; ++col) {
+                sample const inValue = plane < cpamP->depth ?
+                    ctuples[row][col][plane] : ctuples[row][col][0];
+
+                convKernelP->weight[plane][row][col] =
+                    inValue * scale + offset;
             }
         }
     }
-    *rweightsP = rweights;
-    *gweightsP = gweights;
-    *bweightsP = bweights;
-
-    switch (PNM_FORMAT_TYPE(format)) {
-    case PPM_TYPE:
-        if (rsum < 0.9 || rsum > 1.1 || gsum < 0.9 || gsum > 1.1 ||
-            bsum < 0.9 || bsum > 1.1) {
-            pm_message("WARNING - this convolution matrix is biased.  " 
-                       "red, green, and blue average weights: %f, %f, %f "
-                       "(unbiased would be 1).",
-                       rsum, gsum, bsum);
+    convKernelP->bias = 0;
 
-            if (rsum < 0 && gsum < 0 && bsum < 0)
-                pm_message("Maybe you want the -nooffset option?");
-        }
-        break;
+    *convKernelPP = convKernelP;
+}
 
-    default:
-        if (gsum < 0.9 || gsum > 1.1)
-            pm_message("WARNING - this convolution matrix is biased.  "
-                       "average weight = %f (unbiased would be 1)",
-                       gsum);
-        break;
+
+
+static void
+convKernelDestroy(struct ConvKernel * const convKernelP) {
+
+    unsigned int plane;
+
+    for (plane = 0; plane < convKernelP->planes; ++plane) {
+        unsigned int row;
+
+        for (row = 0; row < convKernelP->rows; ++row)
+            free(convKernelP->weight[plane][row]);
+
+        free(convKernelP->weight[plane]);
     }
+    free(convKernelP);
 }
 
 
 
-/* General PGM Convolution
-**
-** No useful redundancy in convolution matrix.
-*/
-
 static void
-pgm_general_convolve(const float ** const weights) {
-    xel** xelbuf;
-    xel* outputrow;
-    xelval g;
-    int row;
-    xel **rowptr, *temprptr;
-    int toprow, temprow;
-    int i, irow;
-    long tempgsum;
-
-    /* Allocate space for one convolution-matrix's worth of rows, plus
-       a row output buffer.
-    */
-    xelbuf = pnm_allocarray(cols, crows);
-    outputrow = pnm_allocrow(cols);
-
-    /* Allocate array of pointers to xelbuf */
-    rowptr = (xel **) pnm_allocarray(1, crows);
-
-    pnm_writepnminit(stdout, cols, rows, maxval, newformat, 0);
-
-    /* Read in one convolution-matrix's worth of image, less one row. */
-    for (row = 0; row < crows - 1; ++row) {
-        pnm_readpnmrow(ifp, xelbuf[row], cols, maxval, format);
-        if (PNM_FORMAT_TYPE(format) != newformat)
-            pnm_promoteformatrow(xelbuf[row], cols, maxval, format, 
-                                 maxval, newformat);
-        /* Write out just the part we're not going to convolve. */
-        if (row < crowso2)
-            pnm_writepnmrow(stdout, xelbuf[row], cols, maxval, newformat, 0);
+normalizeKernelPlane(struct ConvKernel * const convKernelP,
+                     unsigned int        const plane) {
+
+    unsigned int row;
+    float sum;
+
+    for (row = 0, sum = 0.0; row < convKernelP->rows; ++row) {
+        unsigned int col;
+            
+        for (col = 0; col < convKernelP->cols; ++col) {
+                
+            sum += convKernelP->weight[plane][row][col];
+        }
     }
 
-    /* Now the rest of the image - read in the row at the end of
-       xelbuf, and convolve and write out the row in the middle.
-    */
-    for (; row < rows; ++row) {
-        int col;
-        toprow = row + 1;
-        temprow = row % crows;
-        pnm_readpnmrow(ifp, xelbuf[temprow], cols, maxval, format);
-        if (PNM_FORMAT_TYPE(format) != newformat)
-            pnm_promoteformatrow(xelbuf[temprow], cols, maxval, format, 
-                                 maxval, newformat);
-
-        /* Arrange rowptr to eliminate the use of mod function to determine
-           which row of xelbuf is 0...crows.  Mod function can be very costly.
-        */
-        temprow = toprow % crows;
-        i = 0;
-        for (irow = temprow; irow < crows; ++i, ++irow)
-            rowptr[i] = xelbuf[irow];
-        for (irow = 0; irow < temprow; ++irow, ++i)
-            rowptr[i] = xelbuf[irow];
-
-        for (col = 0; col < cols; ++col) {
-            if (col < ccolso2 || col >= cols - ccolso2)
-                outputrow[col] = rowptr[crowso2][col];
-            else {
-                int const leftcol = col - ccolso2;
-                int crow;
-                float gsum;
-                gsum = 0.0;
-                for (crow = 0; crow < crows; ++crow) {
-                    int ccol;
-                    temprptr = rowptr[crow] + leftcol;
-                    for (ccol = 0; ccol < ccols; ++ccol)
-                        gsum += PNM_GET1(*(temprptr + ccol))
-                            * weights[crow][ccol];
-                }
-                tempgsum = gsum + 0.5;
-            CHECK_GRAY;
-            PNM_ASSIGN1( outputrow[col], g );
-            }
+    {
+        float const scaler = 1.0/sum;
+
+        unsigned int row;
+
+        for (row = 0; row < convKernelP->rows; ++row) {
+            unsigned int col;
+                
+            for (col = 0; col < convKernelP->cols; ++col)
+                convKernelP->weight[plane][row][col] *= scaler;
         }
-        pnm_writepnmrow(stdout, outputrow, cols, maxval, newformat, 0);
     }
+}
 
-    /* Now write out the remaining unconvolved rows in xelbuf. */
-    for (irow = crowso2 + 1; irow < crows; ++irow)
-        pnm_writepnmrow(stdout, rowptr[irow], cols, maxval, newformat, 0 );
+
+
+static void
+normalizeKernel(struct ConvKernel * const convKernelP) {
+/*----------------------------------------------------------------------------
+   Modify *convKernelP by scaling every weight in a plane by the same factor
+   such that the weights in the plane all add up to 1.
+-----------------------------------------------------------------------------*/
+    unsigned int plane;
+
+    for (plane = 0; plane < convKernelP->planes; ++plane)
+        normalizeKernelPlane(convKernelP, plane);
 }
 
 
 
-/* PGM Mean Convolution
-**
-** This is the common case where you just want the target pixel replaced with
-** the average value of its neighbors.  This can work much faster than the
-** general case because you can reduce the number of floating point operations
-** that are required since all the weights are the same.  You will only need
-** to multiply by the weight once, not for every pixel in the convolution
-** matrix.
-**
-** This algorithm works by creating sums for each column of crows height for
-** the whole width of the image.  Then add ccols column sums together to obtain
-** the total sum of the neighbors and multiply that sum by the weight.  As you
-** move right to left to calculate the next pixel, take the total sum you just
-** generated, add in the value of the next column and subtract the value of the
-** leftmost column.  Multiply that by the weight and that's it.  As you move
-** down a row, calculate new column sums by using previous sum for that column
-** and adding in pixel on current row and subtracting pixel in top row.
-**
-*/
+static void
+getKernelPnm(const char *         const fileName,
+             unsigned int         const depth,
+             bool                 const offset,
+             struct ConvKernel ** const convKernelPP) {
+/*----------------------------------------------------------------------------
+   Get the convolution kernel from the PNM file named 'fileName'.
+   'offset' means the PNM convolution matrix is defined in offset form so
+   that it can represent negative values.  E.g. with maxval 100, 50 means
+   0, 100 means 50, and 0 means -50.  If 'offsetPgm' is false, 0 means 0
+   and there are no negative weights.
+
+   Make the kernel suitable for convolving an image of depth 'depth'.
+
+   Return the kernel as *convKernelPP.
+-----------------------------------------------------------------------------*/
+    struct pam cpam;
+    FILE * cifP;
+    tuple ** ctuples;
+
+    cifP = pm_openr(fileName);
+
+    /* Read in the convolution matrix. */
+    ctuples = pnm_readpam(cifP, &cpam, PAM_STRUCT_SIZE(tuple_type));
+    pm_close(cifP);
+    
+    validateKernelDimensions(cpam.width, cpam.height);
+
+    convKernelCreatePnm(&cpam, ctuples, depth, offset, convKernelPP);
+}
+
 
 
 static void
-pgm_mean_convolve(const float ** const weights) {
-    float const gmeanweight = weights[0][0];
-
-    int ccol, col;
-    xel** xelbuf;
-    xel* outputrow;
-    xelval g;
-    int row, crow;
-    xel **rowptr, *temprptr;
-    int leftcol;
-    int i, irow;
-    int toprow, temprow;
-    int subrow, addrow;
-    int subcol, addcol;
-    long gisum;
-    int tempcol, crowsp1;
-    long tempgsum;
-    long *gcolumnsum;
-
-    /* Allocate space for one convolution-matrix's worth of rows, plus
-    ** a row output buffer.  MEAN uses an extra row. */
-    xelbuf = pnm_allocarray( cols, crows + 1 );
-    outputrow = pnm_allocrow( cols );
-
-    /* Allocate array of pointers to xelbuf. MEAN uses an extra row. */
-    rowptr = (xel **) pnm_allocarray( 1, crows + 1);
-
-    /* Allocate space for intermediate column sums */
-    gcolumnsum = (long *) pm_allocrow( cols, sizeof(long) );
-    for ( col = 0; col < cols; ++col )
-    gcolumnsum[col] = 0L;
-
-    pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 );
-
-    /* Read in one convolution-matrix's worth of image, less one row. */
-    for ( row = 0; row < crows - 1; ++row )
-    {
-    pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[row], cols, maxval, format, maxval, newformat );
-    /* Write out just the part we're not going to convolve. */
-    if ( row < crowso2 )
-        pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 );
-    }
+convKernelCreateMatrixOpt(struct matrixOpt     const matrixOpt,
+                          bool                 const normalize,
+                          unsigned int         const depth,
+                          unsigned int         const bias,
+                          struct ConvKernel ** const convKernelPP) {
+/*----------------------------------------------------------------------------
+   Create a convolution kernel as described by a -matrix command line
+   option.
+   
+   The option value is 'matrixOpt'.
+
+   If 'normalize' is true, we normalize whatever numbers the option specifies
+   so that they add up to one; otherwise, we take the numbers as we find them,
+   so they may form a biased matrix -- i.e. one which brightens or dims the
+   image overall.
+-----------------------------------------------------------------------------*/
+    struct ConvKernel * convKernelP;
+    unsigned int plane;
 
-    /* Do first real row only */
-    subrow = crows;
-    addrow = crows - 1;
-    toprow = row + 1;
-    temprow = row % crows;
-    pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-    pnm_promoteformatrow(
-        xelbuf[temprow], cols, maxval, format, maxval, newformat );
-
-    temprow = toprow % crows;
-    i = 0;
-    for (irow = temprow; irow < crows; ++i, ++irow)
-    rowptr[i] = xelbuf[irow];
-    for (irow = 0; irow < temprow; ++irow, ++i)
-    rowptr[i] = xelbuf[irow];
+    MALLOCVAR(convKernelP);
 
-    gisum = 0L;
-    for ( col = 0; col < cols; ++col )
-    {
-    if ( col < ccolso2 || col >= cols - ccolso2 )
-        outputrow[col] = rowptr[crowso2][col];
-    else if ( col == ccolso2 )
-        {
-        leftcol = col - ccolso2;
-        for ( crow = 0; crow < crows; ++crow )
-        {
-        temprptr = rowptr[crow] + leftcol;
-        for ( ccol = 0; ccol < ccols; ++ccol )
-            gcolumnsum[leftcol + ccol] += 
-            PNM_GET1( *(temprptr + ccol) );
-        }
-        for ( ccol = 0; ccol < ccols; ++ccol)
-        gisum += gcolumnsum[leftcol + ccol];
-        tempgsum = (float) gisum * gmeanweight + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
-        }
-    else
-        {
-        /* Column numbers to subtract or add to isum */
-        subcol = col - ccolso2 - 1;
-        addcol = col + ccolso2;  
-        for ( crow = 0; crow < crows; ++crow )
-        gcolumnsum[addcol] += PNM_GET1( rowptr[crow][addcol] );
-        gisum = gisum - gcolumnsum[subcol] + gcolumnsum[addcol];
-        tempgsum = (float) gisum * gmeanweight + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
-        }
-    }
-    pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 );
+    convKernelP->cols = matrixOpt.width;
+    convKernelP->rows = matrixOpt.height;
+    convKernelP->planes = depth;
 
-    ++row;
-    /* For all subsequent rows do it this way as the columnsums have been
-    ** generated.  Now we can use them to reduce further calculations.
-    */
-    crowsp1 = crows + 1;
-    for ( ; row < rows; ++row )
-    {
-    toprow = row + 1;
-    temprow = row % (crows + 1);
-    pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[temprow], cols, maxval, format, maxval, newformat );
-
-    /* This rearrangement using crows+1 rowptrs and xelbufs will cause
-    ** rowptr[0..crows-1] to always hold active xelbufs and for 
-    ** rowptr[crows] to always hold the oldest (top most) xelbuf.
-    */
-    temprow = (toprow + 1) % crowsp1;
-    i = 0;
-    for (irow = temprow; irow < crowsp1; ++i, ++irow)
-        rowptr[i] = xelbuf[irow];
-    for (irow = 0; irow < temprow; ++irow, ++i)
-        rowptr[i] = xelbuf[irow];
-
-    gisum = 0L;
-    for ( col = 0; col < cols; ++col )
-        {
-        if ( col < ccolso2 || col >= cols - ccolso2 )
-        outputrow[col] = rowptr[crowso2][col];
-        else if ( col == ccolso2 )
-        {
-        leftcol = col - ccolso2;
-        for ( ccol = 0; ccol < ccols; ++ccol )
-            {
-            tempcol = leftcol + ccol;
-            gcolumnsum[tempcol] = gcolumnsum[tempcol]
-            - PNM_GET1( rowptr[subrow][ccol] )
-            + PNM_GET1( rowptr[addrow][ccol] );
-            gisum += gcolumnsum[tempcol];
-            }
-        tempgsum = (float) gisum * gmeanweight + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
-        }
-        else
-        {
-        /* Column numbers to subtract or add to isum */
-        subcol = col - ccolso2 - 1;
-        addcol = col + ccolso2;  
-        gcolumnsum[addcol] = gcolumnsum[addcol]
-            - PNM_GET1( rowptr[subrow][addcol] )
-            + PNM_GET1( rowptr[addrow][addcol] );
-        gisum = gisum - gcolumnsum[subcol] + gcolumnsum[addcol];
-        tempgsum = (float) gisum * gmeanweight + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
-        }
+    for (plane = 0; plane < depth; ++plane) {
+        unsigned int row;
+        MALLOCARRAY_NOFAIL(convKernelP->weight[plane], matrixOpt.height);
+
+        for (row = 0; row < matrixOpt.height; ++row) {
+            unsigned int col;
+
+            MALLOCARRAY_NOFAIL(convKernelP->weight[plane][row],
+                               matrixOpt.width);
+    
+            for (col = 0; col < matrixOpt.width; ++col)
+                convKernelP->weight[plane][row][col] =
+                    matrixOpt.weight[row][col];
         }
-    pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 );
     }
+    if (normalize)
+        normalizeKernel(convKernelP);
 
-    /* Now write out the remaining unconvolved rows in xelbuf. */
-    for ( irow = crowso2 + 1; irow < crows; ++irow )
-    pnm_writepnmrow(
-            stdout, rowptr[irow], cols, maxval, newformat, 0 );
+    convKernelP->bias = bias;
 
-    }
+    *convKernelPP = convKernelP;
+}
 
 
-/* PGM Horizontal Convolution
-**
-** Similar idea to using columnsums of the Mean and Vertical convolution,
-** but uses temporary sums of row values.  Need to multiply by weights crows
-** number of times.  Each time a new line is started, must recalculate the
-** initials rowsums for the newest row only.  Uses queue to still access
-** previous row sums.
-**
-*/
 
 static void
-pgm_horizontal_convolve(const float ** const weights) {
-    int ccol, col;
-    xel** xelbuf;
-    xel* outputrow;
-    xelval g;
-    int row, crow;
-    xel **rowptr, *temprptr;
-    int leftcol;
-    int i, irow;
-    int temprow;
-    int subcol, addcol;
-    float gsum;
-    int addrow, subrow;
-    long **growsum, **growsumptr;
-    int crowsp1;
-    long tempgsum;
-
-    /* Allocate space for one convolution-matrix's worth of rows, plus
-    ** a row output buffer. */
-    xelbuf = pnm_allocarray( cols, crows + 1 );
-    outputrow = pnm_allocrow( cols );
-
-    /* Allocate array of pointers to xelbuf */
-    rowptr = (xel **) pnm_allocarray( 1, crows + 1);
-
-    /* Allocate intermediate row sums.  HORIZONTAL uses an extra row. */
-    /* crows current rows and 1 extra for newest added row.           */
-    growsum = (long **) pm_allocarray( cols, crows + 1, sizeof(long) );
-    growsumptr = (long **) pnm_allocarray( 1, crows + 1);
-
-    pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 );
-
-    /* Read in one convolution-matrix's worth of image, less one row. */
-    for ( row = 0; row < crows - 1; ++row )
-    {
-    pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[row], cols, maxval, format, maxval, newformat );
-    /* Write out just the part we're not going to convolve. */
-    if ( row < crowso2 )
-        pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 );
-    }
+parsePlaneFileLine(const char *   const line,
+                   unsigned int * const widthP,
+                   float **       const weightP) {
 
-    /* First row only */
-    temprow = row % crows;
-    pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-    pnm_promoteformatrow(
-        xelbuf[temprow], cols, maxval, format, maxval, newformat );
+    unsigned int colCt;
+    const char * error;
+    float * weight;
+    const char * cursor;
 
-    temprow = (row + 1) % crows;
-    i = 0;
-    for (irow = temprow; irow < crows; ++i, ++irow)
-    rowptr[i] = xelbuf[irow];
-    for (irow = 0; irow < temprow; ++irow, ++i)
-    rowptr[i] = xelbuf[irow];
+    colCt = 0;  /* initial value */
+    weight = NULL;
 
-    for ( crow = 0; crow < crows; ++crow )
-    growsumptr[crow] = growsum[crow];
- 
-    for ( col = 0; col < cols; ++col )
-    {
-    if ( col < ccolso2 || col >= cols - ccolso2 )
-        outputrow[col] = rowptr[crowso2][col];
-    else if ( col == ccolso2 )
-        {
-        leftcol = col - ccolso2;
-        gsum = 0.0;
-        for ( crow = 0; crow < crows; ++crow )
-        {
-        temprptr = rowptr[crow] + leftcol;
-        growsumptr[crow][leftcol] = 0L;
-        for ( ccol = 0; ccol < ccols; ++ccol )
-            growsumptr[crow][leftcol] += 
-                PNM_GET1( *(temprptr + ccol) );
-        gsum += growsumptr[crow][leftcol] * weights[crow][0];
-        }
-        tempgsum = gsum + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
-        }
-    else
-        {
-        gsum = 0.0;
-        leftcol = col - ccolso2;
-        subcol = col - ccolso2 - 1;
-        addcol = col + ccolso2;
-        for ( crow = 0; crow < crows; ++crow )
-        {
-        growsumptr[crow][leftcol] = growsumptr[crow][subcol]
-            - PNM_GET1( rowptr[crow][subcol] )
-            + PNM_GET1( rowptr[crow][addcol] );
-        gsum += growsumptr[crow][leftcol] * weights[crow][0];
-        }
-        tempgsum = gsum + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
-        }
-        }
-    pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 );
+    for (cursor = &line[0]; *cursor; ) {
+        const char * token;
+        const char * next;
 
+        REALLOCARRAY(weight, colCt + 1);
 
-    /* For all subsequent rows */
+        pm_gettoken(cursor, ' ', &token, &next, &error);
 
-    subrow = crows;
-    addrow = crows - 1;
-    crowsp1 = crows + 1;
-    ++row;
-    for ( ; row < rows; ++row )
-    {
-    temprow = row % crowsp1;
-    pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[temprow], cols, maxval, format, maxval, newformat );
+        if (error)
+            pm_error("Invalid format of line in convolution matrix file: "
+                     "'%s'.  %s", line, error);
 
-    temprow = (row + 2) % crowsp1;
-    i = 0;
-    for (irow = temprow; irow < crowsp1; ++i, ++irow)
-        {
-        rowptr[i] = xelbuf[irow];
-        growsumptr[i] = growsum[irow];
-        }
-    for (irow = 0; irow < temprow; ++irow, ++i)
-        {
-        rowptr[i] = xelbuf[irow];
-        growsumptr[i] = growsum[irow];
-        }
+        cursor = next;
 
-    for ( col = 0; col < cols; ++col )
-        {
-        if ( col < ccolso2 || col >= cols - ccolso2 )
-        outputrow[col] = rowptr[crowso2][col];
-        else if ( col == ccolso2 )
-        {
-        gsum = 0.0;
-        leftcol = col - ccolso2;
-        growsumptr[addrow][leftcol] = 0L;
-        for ( ccol = 0; ccol < ccols; ++ccol )
-            growsumptr[addrow][leftcol] += 
-            PNM_GET1( rowptr[addrow][leftcol + ccol] );
-        for ( crow = 0; crow < crows; ++crow )
-            gsum += growsumptr[crow][leftcol] * weights[crow][0];
-        tempgsum = gsum + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
-        }
-        else
-        {
-        gsum = 0.0;
-        leftcol = col - ccolso2;
-        subcol = col - ccolso2 - 1;
-        addcol = col + ccolso2;  
-        growsumptr[addrow][leftcol] = growsumptr[addrow][subcol]
-            - PNM_GET1( rowptr[addrow][subcol] )
-            + PNM_GET1( rowptr[addrow][addcol] );
-        for ( crow = 0; crow < crows; ++crow )
-            gsum += growsumptr[crow][leftcol] * weights[crow][0];
-        tempgsum = gsum + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
+        if (*cursor) {
+            assert(*next == ' ');
+            ++cursor;  /* advance over space */
         }
+        if (strlen(token) == 0)
+            pm_error("Column %u value in line '%s' of convolution matrix file "
+                     "is null string.", colCt, line);
+        else {
+            char * trailingJunk;
+            weight[colCt] = strtod(token, &trailingJunk);
+            if (*trailingJunk != '\0') 
+                pm_error("The Column %u element of the row '%s' in the "
+                         "-matrix value is not a valid floating point "
+                         "number", colCt, line);
+
+                ++colCt;
         }
-    pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 );
+        pm_strfree(token);
     }
+    *weightP = weight;
+    *widthP = colCt;
+}
+
 
-    /* Now write out the remaining unconvolved rows in xelbuf. */
-    for ( irow = crowso2 + 1; irow < crows; ++irow )
-    pnm_writepnmrow(
-            stdout, rowptr[irow], cols, maxval, newformat, 0 );
 
+static void
+readPlaneFile(FILE *         const ifP, 
+              float ***      const weightP,
+              unsigned int * const widthP,
+              unsigned int * const heightP) {
+/*----------------------------------------------------------------------------
+   Read weights of one plane from a file.
+
+   The file is a simple matrix, one line per row, with columns separated
+   by a single space.
+
+   Each column is a floating point decimal ASCII number, positive zero,
+   or negative, with any magnitude.
+
+   If the rows don't all have the same number of columns, we abort.
+
+   Return the dimensions seen in the file as *widthP and *heightP.
+-----------------------------------------------------------------------------*/
+    unsigned int rowCt;
+    float ** weight;
+    unsigned int width;
+    bool eof;
+
+    weight = NULL;  /* initial value */
+
+    for (eof = false, rowCt = 0; !eof; ) {
+        const char * error;
+        const char * line;
+
+        pm_freadline(ifP, &line, &error);
+
+        if (error)
+            pm_error("Failed to read row %u "
+                     "from the convolutionmatrix file.  %s",
+                     rowCt, error);
+        else {
+            if (line == NULL)
+                eof = true;
+            else {
+                REALLOCARRAY(weight, rowCt + 1);
+            
+                if (weight == NULL)
+                    pm_error("Unable to allocate memory for "
+                             "convolution matrix");
+                else {
+                    unsigned int thisWidth;
+
+                    parsePlaneFileLine(line, &thisWidth, &weight[rowCt]);
+
+                    if (rowCt == 0)
+                        width = thisWidth;
+                    else {
+                        if (thisWidth != width)
+                            pm_error("Multiple row widths in the convolution "
+                                     "matrix file: %u columns and %u columns.",
+                                     width, thisWidth);
+                    }                    
+                    ++rowCt;
+                }
+                pm_strfree(line);
+            }
+        }
     }
+    validateKernelDimensions(width, rowCt);
 
+    *weightP = weight;
+    *heightP = rowCt;
+    *widthP = width;
+}
 
-/* PGM Vertical Convolution
-**
-** Uses column sums as in Mean Convolution.
-**
-*/
 
 
 static void
-pgm_vertical_convolve(const float ** const weights) {
-    int ccol, col;
-    xel** xelbuf;
-    xel* outputrow;
-    xelval g;
-    int row, crow;
-    xel **rowptr, *temprptr;
-    int leftcol;
-    int i, irow;
-    int toprow, temprow;
-    int subrow, addrow;
-    int tempcol;
-    float gsum;
-    long *gcolumnsum;
-    int crowsp1;
-    int addcol;
-    long tempgsum;
-
-    /* Allocate space for one convolution-matrix's worth of rows, plus
-    ** a row output buffer. VERTICAL uses an extra row. */
-    xelbuf = pnm_allocarray( cols, crows + 1 );
-    outputrow = pnm_allocrow( cols );
-
-    /* Allocate array of pointers to xelbuf */
-    rowptr = (xel **) pnm_allocarray( 1, crows + 1 );
-
-    /* Allocate space for intermediate column sums */
-    gcolumnsum = (long *) pm_allocrow( cols, sizeof(long) );
-    for ( col = 0; col < cols; ++col )
-    gcolumnsum[col] = 0L;
-
-    pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 );
-
-    /* Read in one convolution-matrix's worth of image, less one row. */
-    for ( row = 0; row < crows - 1; ++row )
-    {
-    pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[row], cols, maxval, format, maxval, newformat );
-    /* Write out just the part we're not going to convolve. */
-    if ( row < crowso2 )
-        pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 );
-    }
+copyWeight(float **       const srcWeight,
+           unsigned int   const width,
+           unsigned int   const height, 
+           float ***      const dstWeightP) {
+/*----------------------------------------------------------------------------
+   Make a copy, in dynamically allocated memory, of the weight matrix
+   'srcWeight', whose dimensions are 'width' by 'height'.  Return the
+   new matrix as *dstWeightP.
+-----------------------------------------------------------------------------*/
+    unsigned int row;
+    float ** dstWeight;
 
-    /* Now the rest of the image - read in the row at the end of
-    ** xelbuf, and convolve and write out the row in the middle.
-    */
-    /* For first row only */
+    MALLOCARRAY(dstWeight, height);
 
-    toprow = row + 1;
-    temprow = row % crows;
-    pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-    pnm_promoteformatrow(
-        xelbuf[temprow], cols, maxval, format, maxval, newformat );
+    if (dstWeight == NULL)
+        pm_error("Could not allocate memory for convolution matrix");
+   
+    for (row = 0; row < height; ++row) {
+        unsigned int col;
 
-    /* Arrange rowptr to eliminate the use of mod function to determine
-    ** which row of xelbuf is 0...crows.  Mod function can be very costly.
-    */
-    temprow = toprow % crows;
-    i = 0;
-    for (irow = temprow; irow < crows; ++i, ++irow)
-    rowptr[i] = xelbuf[irow];
-    for (irow = 0; irow < temprow; ++irow, ++i)
-    rowptr[i] = xelbuf[irow];
+        MALLOCARRAY(dstWeight[row], width);
 
-    for ( col = 0; col < cols; ++col )
-    {
-    if ( col < ccolso2 || col >= cols - ccolso2 )
-        outputrow[col] = rowptr[crowso2][col];
-    else if ( col == ccolso2 )
-        {
-        gsum = 0.0;
-        leftcol = col - ccolso2;
-        for ( crow = 0; crow < crows; ++crow )
-        {
-        temprptr = rowptr[crow] + leftcol;
-        for ( ccol = 0; ccol < ccols; ++ccol )
-            gcolumnsum[leftcol + ccol] += 
-            PNM_GET1( *(temprptr + ccol) );
-        }
-        for ( ccol = 0; ccol < ccols; ++ccol)
-        gsum += gcolumnsum[leftcol + ccol] * weights[0][ccol];
-        tempgsum = gsum + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
-        }
-    else
-        {
-        gsum = 0.0;
-        leftcol = col - ccolso2;
-        addcol = col + ccolso2;  
-        for ( crow = 0; crow < crows; ++crow )
-        gcolumnsum[addcol] += PNM_GET1( rowptr[crow][addcol] );
-        for ( ccol = 0; ccol < ccols; ++ccol )
-        gsum += gcolumnsum[leftcol + ccol] * weights[0][ccol];
-        tempgsum = gsum + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
+        if (dstWeight[row] == NULL)
+            pm_error("Could not allocation memory for a "
+                     "convolution matrix row");
+
+        for (col = 0; col < width; ++col) {
+            dstWeight[row][col] = srcWeight[row][col];
         }
     }
-    pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 );
-
-    /* For all subsequent rows */
-    subrow = crows;
-    addrow = crows - 1;
-    crowsp1 = crows + 1;
-    ++row;
-    for ( ; row < rows; ++row )
-    {
-    toprow = row + 1;
-    temprow = row % (crows +1);
-    pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[temprow], cols, maxval, format, maxval, newformat );
-
-    /* Arrange rowptr to eliminate the use of mod function to determine
-    ** which row of xelbuf is 0...crows.  Mod function can be very costly.
-    */
-    temprow = (toprow + 1) % crowsp1;
-    i = 0;
-    for (irow = temprow; irow < crowsp1; ++i, ++irow)
-        rowptr[i] = xelbuf[irow];
-    for (irow = 0; irow < temprow; ++irow, ++i)
-        rowptr[i] = xelbuf[irow];
-
-    for ( col = 0; col < cols; ++col )
-        {
-        if ( col < ccolso2 || col >= cols - ccolso2 )
-        outputrow[col] = rowptr[crowso2][col];
-        else if ( col == ccolso2 )
-        {
-        gsum = 0.0;
-        leftcol = col - ccolso2;
-        for ( ccol = 0; ccol < ccols; ++ccol )
-            {
-            tempcol = leftcol + ccol;
-            gcolumnsum[tempcol] = gcolumnsum[tempcol] 
-            - PNM_GET1( rowptr[subrow][ccol] )
-            + PNM_GET1( rowptr[addrow][ccol] );
-            gsum = gsum + gcolumnsum[tempcol] * weights[0][ccol];
+    *dstWeightP = dstWeight;
+}
+
+
+
+static void
+convKernelCreateSimpleFile(const char **        const fileNameList,
+                           bool                 const normalize,
+                           unsigned int         const depth,
+                           unsigned int         const bias,
+                           struct ConvKernel ** const convKernelPP) {
+/*----------------------------------------------------------------------------
+   Create a convolution kernel as described by a convolution matrix file.
+   This is the simple file with floating point numbers in it, not the
+   legacy pseudo-PNM thing.
+
+   The name of the file is 'fileNameList'.
+
+   If 'normalize' is true, we normalize whatever numbers we find in the file
+   so that they add up to one; otherwise, we take the numbers as we find them,
+   so they may form a biased matrix -- i.e. one which brightens or dims the
+   image overall.
+-----------------------------------------------------------------------------*/
+    struct ConvKernel * convKernelP;
+    unsigned int fileCt;
+    unsigned int planeCt;
+    unsigned int plane;
+    unsigned int width, height;
+
+    fileCt = 0;
+    while (fileNameList[fileCt])
+        ++fileCt;
+    assert(fileCt > 0);
+
+    planeCt = MIN(3, depth);
+
+    MALLOCVAR_NOFAIL(convKernelP);
+
+    convKernelP->planes = planeCt;
+
+    for (plane = 0; plane < planeCt; ++plane) {
+        if (plane < fileCt) {
+            const char * const fileName = fileNameList[plane];
+
+            FILE * ifP;
+            unsigned int thisWidth, thisHeight;
+
+            ifP = pm_openr(fileName);
+
+            readPlaneFile(ifP, &convKernelP->weight[plane],
+                          &thisWidth, &thisHeight);
+
+            if (plane == 0) {
+                width = thisWidth;
+                height = thisHeight;
+            } else {
+                if (thisWidth != width)
+                    pm_error("Convolution matrix files show two different "
+                             "widths: %u and %u", width, thisWidth);
+                if (thisHeight != height)
+                    pm_error("Convolution matrix files show two different "
+                             "heights: %u and %u", height, thisHeight);
             }
-        tempgsum = gsum + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
-        }
-        else
-        {
-        gsum = 0.0;
-        leftcol = col - ccolso2;
-        addcol = col + ccolso2;
-        gcolumnsum[addcol] = gcolumnsum[addcol]
-            - PNM_GET1( rowptr[subrow][addcol] )
-            + PNM_GET1( rowptr[addrow][addcol] );
-        for ( ccol = 0; ccol < ccols; ++ccol )
-            gsum += gcolumnsum[leftcol + ccol] * weights[0][ccol];
-        tempgsum = gsum + 0.5;
-        CHECK_GRAY;
-        PNM_ASSIGN1( outputrow[col], g );
-        }
+            pm_close(ifP);
+        } else {
+            assert(plane > 0);
+            copyWeight(convKernelP->weight[0], width, height,
+                       &convKernelP->weight[plane]);
         }
-    pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 );
     }
 
-    /* Now write out the remaining unconvolved rows in xelbuf. */
-    for ( irow = crowso2 + 1; irow < crows; ++irow )
-    pnm_writepnmrow(
-            stdout, rowptr[irow], cols, maxval, newformat, 0 );
+    if (normalize)
+        normalizeKernel(convKernelP);
 
-    }
+    convKernelP->cols = width;
+    convKernelP->rows = height;
+    convKernelP->bias = bias;
 
+    *convKernelPP = convKernelP;
+}
 
 
 
-/* PPM General Convolution Algorithm
-**
-** No redundancy in convolution matrix.  Just use brute force.
-** See pgm_general_convolve() for more details.
-*/
+static void
+getKernel(struct cmdlineInfo   const cmdline,
+          unsigned int         const depth,
+          struct ConvKernel ** const convKernelPP) {
+/*----------------------------------------------------------------------------
+   Figure out what the convolution kernel is.  It can come from various
+   sources in various forms, as described on the command line, represented
+   by 'cmdline'.
+
+   We generate a kernel object in standard form (free of any indication of
+   where it came from) and return a handle to it as *convKernelPP.
+----------------------------------------------------------------------------*/
+    struct ConvKernel * convKernelP;
+
+    if (cmdline.pnmMatrixFileName)
+        getKernelPnm(cmdline.pnmMatrixFileName, depth, !cmdline.nooffset,
+                     &convKernelP);
+    else if (cmdline.matrixfile)
+        convKernelCreateSimpleFile(cmdline.matrixfile, cmdline.normalize,
+                                   depth, cmdline.bias, &convKernelP);
+    else if (cmdline.matrixSpec)
+        convKernelCreateMatrixOpt(cmdline.matrix, cmdline.normalize,
+                                  depth, cmdline.bias, &convKernelP);
+
+    warnBadKernel(convKernelP);
+
+    *convKernelPP = convKernelP;
+}
+
+
 
 static void
-ppm_general_convolve(const float ** const rweights,
-                     const float ** const gweights,
-                     const float ** const bweights) {
-    int ccol, col;
-    xel** xelbuf;
-    xel* outputrow;
-    xelval r, g, b;
-    int row, crow;
-    float rsum, gsum, bsum;
-    xel **rowptr, *temprptr;
-    int toprow, temprow;
-    int i, irow;
-    int leftcol;
-    long temprsum, tempgsum, tempbsum;
-
-    /* Allocate space for one convolution-matrix's worth of rows, plus
-    ** a row output buffer. */
-    xelbuf = pnm_allocarray( cols, crows );
-    outputrow = pnm_allocrow( cols );
-
-    /* Allocate array of pointers to xelbuf */
-    rowptr = (xel **) pnm_allocarray( 1, crows );
-
-    pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 );
-
-    /* Read in one convolution-matrix's worth of image, less one row. */
-    for ( row = 0; row < crows - 1; ++row )
-    {
-    pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[row], cols, maxval, format, maxval, newformat );
-    /* Write out just the part we're not going to convolve. */
-    if ( row < crowso2 )
-        pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 );
+validateEnoughImageToConvolve(const struct pam *        const inpamP,
+                              const struct ConvKernel * const convKernelP) {
+/*----------------------------------------------------------------------------
+   Abort program if the image isn't big enough in both directions to have
+   at least one convolved pixel.
+
+   The program could theoretically operate with an image smaller than that by
+   simply outputting the input unchanged (like it does with the edges of an
+   image anyway), but we're too lazy to write code for this special case.  The
+   simple code expects the unconvolved edges to exist full-size and some of it
+   convolves the first convolveable row and/or column specially and expects it
+   to exist.
+-----------------------------------------------------------------------------*/
+
+    if (inpamP->height < convKernelP->rows + 1)
+        pm_error("Image is too short (%u rows) to convolve with this "
+                 "%u-row convolution kernel.",
+                 inpamP->height, convKernelP->rows);
+    
+    if (inpamP->width < convKernelP->cols + 1)
+        pm_error("Image is too narrow (%u columns) to convolve with this "
+                 "%u-column convolution kernel.",
+                 inpamP->width, convKernelP->cols);
+}
+
+
+
+static tuple **
+allocRowbuf(struct pam * const pamP,
+            unsigned int const height) {
+
+    tuple ** rowbuf;
+
+    MALLOCARRAY(rowbuf, height);
+
+    if (rowbuf == NULL)
+        pm_error("Failed to allocate %u-row buffer", height);
+    else {
+        unsigned int row;
+    
+        for (row = 0; row < height; ++row)
+            rowbuf[row] = pnm_allocpamrow(pamP);
     }
 
-    /* Now the rest of the image - read in the row at the end of
-    ** xelbuf, and convolve and write out the row in the middle.
-    */
-    for ( ; row < rows; ++row )
-    {
-    toprow = row + 1;
-    temprow = row % crows;
-    pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[temprow], cols, maxval, format, maxval, newformat );
-
-    /* Arrange rowptr to eliminate the use of mod function to determine
-    ** which row of xelbuf is 0...crows.  Mod function can be very costly.
-    */
-    temprow = toprow % crows;
-    i = 0;
-    for (irow = temprow; irow < crows; ++i, ++irow)
-        rowptr[i] = xelbuf[irow];
-    for (irow = 0; irow < temprow; ++irow, ++i)
-        rowptr[i] = xelbuf[irow];
-
-    for ( col = 0; col < cols; ++col )
-        {
-        if ( col < ccolso2 || col >= cols - ccolso2 )
-        outputrow[col] = rowptr[crowso2][col];
-        else
-        {
-        leftcol = col - ccolso2;
-        rsum = gsum = bsum = 0.0;
-        for ( crow = 0; crow < crows; ++crow )
-            {
-            temprptr = rowptr[crow] + leftcol;
-            for ( ccol = 0; ccol < ccols; ++ccol )
-            {
-            rsum += PPM_GETR( *(temprptr + ccol) )
-                * rweights[crow][ccol];
-            gsum += PPM_GETG( *(temprptr + ccol) )
-                * gweights[crow][ccol];
-            bsum += PPM_GETB( *(temprptr + ccol) )
-                * bweights[crow][ccol];
-            }
+    return rowbuf;
+}
+
+
+
+static void
+freeRowbuf(tuple **     const rowbuf,
+           unsigned int const height) {
+
+    unsigned int row;
+
+    for (row = 0; row < height; ++row)
+        pnm_freepamrow(rowbuf[row]);
+
+    free(rowbuf);
+}
+
+
+
+static void
+readAndScaleRow(struct pam * const inpamP,
+                tuple *      const inrow,
+                sample       const newMaxval,
+                unsigned int const newDepth) {
+
+    pnm_readpamrow(inpamP, inrow);
+
+    if (newMaxval != inpamP->maxval)
+        pnm_scaletuplerow(inpamP, inrow, inrow, newMaxval);
+
+    if (newDepth == 3 && inpamP->depth == 1)
+        pnm_makerowrgb(inpamP, inrow);
+}
+
+
+
+static void
+readAndScaleRows(struct pam *              const inpamP,
+                 unsigned int              const count,
+                 tuple **                  const rowbuf,
+                 sample                    const outputMaxval,
+                 unsigned int              const outputDepth) {
+/*----------------------------------------------------------------------------
+  Read in 'count' rows into rowbuf[].
+  
+  Scale the contents to maxval 'outputMaxval' and expand to depth
+  'outputDepth'.
+-----------------------------------------------------------------------------*/
+    unsigned int row;
+
+    for (row = 0; row < count; ++row)
+        readAndScaleRow(inpamP, rowbuf[row], outputMaxval, outputDepth);
+}
+
+
+
+static void
+writePamRowBiased(struct pam * const outpamP,
+                  tuple *      const row,
+                  unsigned int const bias) {
+/*----------------------------------------------------------------------------
+   Write row[] to the output file according to *outpamP, but with
+   'bias' added to each sample value, clipped to maxval.
+-----------------------------------------------------------------------------*/
+    if (bias == 0)
+        pnm_writepamrow(outpamP, row);
+    else {
+        unsigned int col;
+
+        tuple * const outrow = pnm_allocpamrow(outpamP);
+
+        for (col = 0; col < outpamP->width; ++col) {
+            unsigned int plane;
+
+            for (plane = 0; plane < outpamP->depth; ++plane) {
+                outrow[col][plane] =
+                    MIN(outpamP->maxval, bias + row[col][plane]);
             }
-            temprsum = rsum + 0.5;
-            tempgsum = gsum + 0.5;
-            tempbsum = bsum + 0.5;
-            CHECK_RED;
-            CHECK_GREEN;
-            CHECK_BLUE;
-            PPM_ASSIGN( outputrow[col], r, g, b );
         }
-        }
-    pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 );
+        pnm_writepamrow(outpamP, outrow);
+
+        pnm_freepamrow(outrow);
     }
+}
 
-    /* Now write out the remaining unconvolved rows in xelbuf. */
-    for ( irow = crowso2 + 1; irow < crows; ++irow )
-    pnm_writepnmrow(
-            stdout, rowptr[irow], cols, maxval, newformat, 0 );
 
-    }
+
+static void
+writeUnconvolvedTop(struct pam *              const outpamP,
+                    const struct ConvKernel * const convKernelP,
+                    tuple **                  const rowbuf) {
+/*----------------------------------------------------------------------------
+   Write out the top part that we can't convolve because the convolution
+   kernel runs off the top of the image.
+
+   Assume those rows are in the window rowbuf[], with the top row of the
+   image as the first row in rowbuf[].
+-----------------------------------------------------------------------------*/
+    unsigned int row;
+
+    for (row = 0; row < convKernelP->rows/2; ++row)
+        writePamRowBiased(outpamP, rowbuf[row], convKernelP->bias);
+}
 
 
-/* PPM Mean Convolution
-**
-** Same as pgm_mean_convolve() but for PPM.
-**
-*/
 
 static void
-ppm_mean_convolve(const float ** const rweights,
-                  const float ** const gweights,
-                  const float ** const bweights) {
-    /* All weights of a single color are the same so just grab any one
-       of them.  
-    */
-    float const rmeanweight = rweights[0][0];
-    float const gmeanweight = gweights[0][0];
-    float const bmeanweight = bweights[0][0];
-
-    int ccol, col;
-    xel** xelbuf;
-    xel* outputrow;
-    xelval r, g, b;
-    int row, crow;
-    xel **rowptr, *temprptr;
-    int leftcol;
-    int i, irow;
-    int toprow, temprow;
-    int subrow, addrow;
-    int subcol, addcol;
-    long risum, gisum, bisum;
-    long temprsum, tempgsum, tempbsum;
-    int tempcol, crowsp1;
-    long *rcolumnsum, *gcolumnsum, *bcolumnsum;
-
-
-
-    /* Allocate space for one convolution-matrix's worth of rows, plus
-    ** a row output buffer.  MEAN uses an extra row. */
-    xelbuf = pnm_allocarray( cols, crows + 1 );
-    outputrow = pnm_allocrow( cols );
-
-    /* Allocate array of pointers to xelbuf. MEAN uses an extra row. */
-    rowptr = (xel **) pnm_allocarray( 1, crows + 1);
-
-    /* Allocate space for intermediate column sums */
-    rcolumnsum = (long *) pm_allocrow( cols, sizeof(long) );
-    gcolumnsum = (long *) pm_allocrow( cols, sizeof(long) );
-    bcolumnsum = (long *) pm_allocrow( cols, sizeof(long) );
-    for ( col = 0; col < cols; ++col )
-    {
-    rcolumnsum[col] = 0L;
-    gcolumnsum[col] = 0L;
-    bcolumnsum[col] = 0L;
-    }
+writeUnconvolvedBottom(struct pam *              const outpamP,
+                       const struct ConvKernel * const convKernelP,
+                       unsigned int              const windowHeight,
+                       tuple **                  const circMap) {
+/*----------------------------------------------------------------------------
+  Write out the bottom part that we can't convolve because the convolution
+  kernel runs off the bottom of the image.
 
-    pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 );
+  Assume the 'windowHeight' rows at the bottom of the image are in the row
+  buffer, mapped by 'circMap' such that the top of the window is circMap[0].
+-----------------------------------------------------------------------------*/
+    unsigned int row;
 
-    /* Read in one convolution-matrix's worth of image, less one row. */
-    for ( row = 0; row < crows - 1; ++row )
-    {
-    pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[row], cols, maxval, format, maxval, newformat );
-    /* Write out just the part we're not going to convolve. */
-    if ( row < crowso2 )
-        pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 );
+    for (row = windowHeight - convKernelP->rows / 2;
+         row < windowHeight;
+         ++row) {
+
+        writePamRowBiased(outpamP, circMap[row], convKernelP->bias);
     }
+}
+
+
+
+static void
+setupCircMap(tuple **     const circMap,
+             tuple **     const rowbuf,
+             unsigned int const windowHeight,
+             unsigned int const topRowbufRow) {
+/*----------------------------------------------------------------------------
+  Set up circMap[] to reflect the case that index 'topRowbufRow' of rowbuf[]
+  is for the topmost row in the window.
+-----------------------------------------------------------------------------*/
+    unsigned int row;
+    unsigned int i;
 
-    /* Do first real row only */
-    subrow = crows;
-    addrow = crows - 1;
-    toprow = row + 1;
-    temprow = row % crows;
-    pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-    pnm_promoteformatrow(
-        xelbuf[temprow], cols, maxval, format, maxval, newformat );
-
-    temprow = toprow % crows;
     i = 0;
-    for (irow = temprow; irow < crows; ++i, ++irow)
-    rowptr[i] = xelbuf[irow];
-    for (irow = 0; irow < temprow; ++irow, ++i)
-    rowptr[i] = xelbuf[irow];
-
-    risum = 0L;
-    gisum = 0L;
-    bisum = 0L;
-    for ( col = 0; col < cols; ++col )
-    {
-    if ( col < ccolso2 || col >= cols - ccolso2 )
-        outputrow[col] = rowptr[crowso2][col];
-    else if ( col == ccolso2 )
-        {
-        leftcol = col - ccolso2;
-        for ( crow = 0; crow < crows; ++crow )
-        {
-        temprptr = rowptr[crow] + leftcol;
-        for ( ccol = 0; ccol < ccols; ++ccol )
-            {
-            rcolumnsum[leftcol + ccol] += 
-            PPM_GETR( *(temprptr + ccol) );
-            gcolumnsum[leftcol + ccol] += 
-            PPM_GETG( *(temprptr + ccol) );
-            bcolumnsum[leftcol + ccol] += 
-            PPM_GETB( *(temprptr + ccol) );
+
+    for (row = topRowbufRow; row < windowHeight; ++i, ++row)
+        circMap[i] = rowbuf[row];
+
+    for (row = 0; row < topRowbufRow; ++row, ++i)
+        circMap[i] = rowbuf[row];
+}
+
+
+
+static void
+convolveGeneralRowPlane(struct pam *              const pamP,
+                        tuple **                  const window,
+                        const struct ConvKernel * const convKernelP,
+                        unsigned int              const plane,
+                        tuple *                   const outputrow) {
+/*----------------------------------------------------------------------------
+   Given a window of input window[], where window[0] is the top row of the
+   window and the window is the height of the convolution kernel, convolve
+   Plane 'plane' of the row at the center of the window.
+
+   Return the convolved row as outputrow[].
+
+   *pamP describes the rows in window[] (but not the number of rows).
+
+   *convKernelP is the convolution kernel to use.
+-----------------------------------------------------------------------------*/
+    unsigned int const crowso2 = convKernelP->rows / 2;
+    unsigned int const ccolso2 = convKernelP->cols / 2;
+
+    unsigned int col;
+    
+    for (col = 0; col < pamP->width; ++col) {
+        if (col < ccolso2 || col >= pamP->width - ccolso2)
+            /* The unconvolved left or right edge */
+            outputrow[col][plane] =
+                clipSample(convKernelP->bias + window[crowso2][col][plane],
+                           pamP->maxval);
+        else {
+            unsigned int const leftcol = col - ccolso2;
+            unsigned int crow;
+            float sum;
+            sum = 0.0;
+            for (crow = 0; crow < convKernelP->rows; ++crow) {
+                const tuple * const leftrptr = &window[crow][leftcol];
+                unsigned int ccol;
+                for (ccol = 0; ccol < convKernelP->cols; ++ccol)
+                    sum += leftrptr[ccol][plane] *
+                        convKernelP->weight[plane][crow][ccol];
             }
-        }
-        for ( ccol = 0; ccol < ccols; ++ccol)
-        {
-        risum += rcolumnsum[leftcol + ccol];
-        gisum += gcolumnsum[leftcol + ccol];
-        bisum += bcolumnsum[leftcol + ccol];
-        }
-        temprsum = (float) risum * rmeanweight + 0.5;
-        tempgsum = (float) gisum * gmeanweight + 0.5;
-        tempbsum = (float) bisum * bmeanweight + 0.5;
-        CHECK_RED;
-        CHECK_GREEN;
-        CHECK_BLUE;
-        PPM_ASSIGN( outputrow[col], r, g, b );
-        }
-    else
-        {
-        /* Column numbers to subtract or add to isum */
-        subcol = col - ccolso2 - 1;
-        addcol = col + ccolso2;  
-        for ( crow = 0; crow < crows; ++crow )
-        {
-        rcolumnsum[addcol] += PPM_GETR( rowptr[crow][addcol] );
-        gcolumnsum[addcol] += PPM_GETG( rowptr[crow][addcol] );
-        bcolumnsum[addcol] += PPM_GETB( rowptr[crow][addcol] );
-        }
-        risum = risum - rcolumnsum[subcol] + rcolumnsum[addcol];
-        gisum = gisum - gcolumnsum[subcol] + gcolumnsum[addcol];
-        bisum = bisum - bcolumnsum[subcol] + bcolumnsum[addcol];
-        temprsum = (float) risum * rmeanweight + 0.5;
-        tempgsum = (float) gisum * gmeanweight + 0.5;
-        tempbsum = (float) bisum * bmeanweight + 0.5;
-        CHECK_RED;
-        CHECK_GREEN;
-        CHECK_BLUE;
-        PPM_ASSIGN( outputrow[col], r, g, b );
+            outputrow[col][plane] =
+                makeSample(convKernelP->bias + sum, pamP->maxval);
         }
     }
-    pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 );
+}
 
-    ++row;
-    /* For all subsequent rows do it this way as the columnsums have been
-    ** generated.  Now we can use them to reduce further calculations.
-    */
-    crowsp1 = crows + 1;
-    for ( ; row < rows; ++row )
-    {
-    toprow = row + 1;
-    temprow = row % (crows + 1);
-    pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[temprow], cols, maxval, format, maxval, newformat );
-
-    /* This rearrangement using crows+1 rowptrs and xelbufs will cause
-    ** rowptr[0..crows-1] to always hold active xelbufs and for 
-    ** rowptr[crows] to always hold the oldest (top most) xelbuf.
+
+
+static void
+convolveGeneral(struct pam *              const inpamP,
+                struct pam *              const outpamP,
+                const struct ConvKernel * const convKernelP) {
+/*----------------------------------------------------------------------------
+   Do the convolution without taking advantage of any useful redundancy in the
+   convolution matrix.
+-----------------------------------------------------------------------------*/
+    tuple ** rowbuf;
+        /* A vertical window of the input image.  It holds as many rows as the
+           convolution kernel covers -- the rows we're currently using to
+           create output rows.  It is a circular buffer.
+        */
+    tuple ** circMap;
+        /* A map from image row number within window to element of rowbuf[].
+           E.g. if rowbuf[] if 5 rows high and rowbuf[2] contains the
+           topmost row, then circMap[0] == 2, circMap[1] = 3,
+           circMap[4] = 1.  You could calculate the same thing with a mod
+           function, but that is sometimes more expensive.
+        */
+    tuple * outputrow;
+        /* The convolved row to be output */
+    unsigned int row;
+        /* Row number of the bottom of the current convolution window;
+           i.e. the row to be read or just read from the input file.
+        */
+
+    rowbuf = allocRowbuf(outpamP, convKernelP->rows);
+    MALLOCARRAY_NOFAIL(circMap, convKernelP->rows);
+    outputrow = pnm_allocpamrow(outpamP);
+
+    pnm_writepaminit(outpamP);
+
+    assert(convKernelP->rows > 0);
+
+    readAndScaleRows(inpamP, convKernelP->rows - 1, rowbuf,
+                      outpamP->maxval, outpamP->depth);
+
+    writeUnconvolvedTop(outpamP, convKernelP, rowbuf);
+
+    /* Now the rest of the image - read in the row at the bottom of the
+       window, then convolve and write out the row in the middle of the
+       window.
     */
-    temprow = (toprow + 1) % crowsp1;
-    i = 0;
-    for (irow = temprow; irow < crowsp1; ++i, ++irow)
-        rowptr[i] = xelbuf[irow];
-    for (irow = 0; irow < temprow; ++irow, ++i)
-        rowptr[i] = xelbuf[irow];
-
-    risum = 0L;
-    gisum = 0L;
-    bisum = 0L;
-    for ( col = 0; col < cols; ++col )
-        {
-        if ( col < ccolso2 || col >= cols - ccolso2 )
-        outputrow[col] = rowptr[crowso2][col];
-        else if ( col == ccolso2 )
-        {
-        leftcol = col - ccolso2;
-        for ( ccol = 0; ccol < ccols; ++ccol )
-            {
-            tempcol = leftcol + ccol;
-            rcolumnsum[tempcol] = rcolumnsum[tempcol]
-            - PPM_GETR( rowptr[subrow][ccol] )
-            + PPM_GETR( rowptr[addrow][ccol] );
-            risum += rcolumnsum[tempcol];
-            gcolumnsum[tempcol] = gcolumnsum[tempcol]
-            - PPM_GETG( rowptr[subrow][ccol] )
-            + PPM_GETG( rowptr[addrow][ccol] );
-            gisum += gcolumnsum[tempcol];
-            bcolumnsum[tempcol] = bcolumnsum[tempcol]
-            - PPM_GETB( rowptr[subrow][ccol] )
-            + PPM_GETB( rowptr[addrow][ccol] );
-            bisum += bcolumnsum[tempcol];
-            }
-        temprsum = (float) risum * rmeanweight + 0.5;
-        tempgsum = (float) gisum * gmeanweight + 0.5;
-        tempbsum = (float) bisum * bmeanweight + 0.5;
-        CHECK_RED;
-        CHECK_GREEN;
-        CHECK_BLUE;
-        PPM_ASSIGN( outputrow[col], r, g, b );
-        }
-        else
-        {
-        /* Column numbers to subtract or add to isum */
-        subcol = col - ccolso2 - 1;
-        addcol = col + ccolso2;  
-        rcolumnsum[addcol] = rcolumnsum[addcol]
-            - PPM_GETR( rowptr[subrow][addcol] )
-            + PPM_GETR( rowptr[addrow][addcol] );
-        risum = risum - rcolumnsum[subcol] + rcolumnsum[addcol];
-        gcolumnsum[addcol] = gcolumnsum[addcol]
-            - PPM_GETG( rowptr[subrow][addcol] )
-            + PPM_GETG( rowptr[addrow][addcol] );
-        gisum = gisum - gcolumnsum[subcol] + gcolumnsum[addcol];
-        bcolumnsum[addcol] = bcolumnsum[addcol]
-            - PPM_GETB( rowptr[subrow][addcol] )
-            + PPM_GETB( rowptr[addrow][addcol] );
-        bisum = bisum - bcolumnsum[subcol] + bcolumnsum[addcol];
-        temprsum = (float) risum * rmeanweight + 0.5;
-        tempgsum = (float) gisum * gmeanweight + 0.5;
-        tempbsum = (float) bisum * bmeanweight + 0.5;
-        CHECK_RED;
-        CHECK_GREEN;
-        CHECK_BLUE;
-        PPM_ASSIGN( outputrow[col], r, g, b );
-        }
-        }
-    pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 );
+    for (row = convKernelP->rows - 1; row < inpamP->height; ++row) {
+        unsigned int const rowbufRow = row % convKernelP->rows;
+
+        unsigned int plane;
+
+        setupCircMap(circMap, rowbuf, convKernelP->rows,
+                     (row + 1) % convKernelP->rows);
+
+        readAndScaleRow(inpamP, rowbuf[rowbufRow],
+                        outpamP->maxval, outpamP->depth);
+
+        for (plane = 0; plane < outpamP->depth; ++plane)
+            convolveGeneralRowPlane(outpamP, circMap, convKernelP, plane,
+                                    outputrow);
+
+        pnm_writepamrow(outpamP, outputrow);
     }
+    writeUnconvolvedBottom(outpamP, convKernelP, convKernelP->rows, circMap);
+
+    freeRowbuf(rowbuf, convKernelP->rows);
+}
+
 
-    /* Now write out the remaining unconvolved rows in xelbuf. */
-    for ( irow = crowso2 + 1; irow < crows; ++irow )
-    pnm_writepnmrow(
-            stdout, rowptr[irow], cols, maxval, newformat, 0 );
 
+static sample **
+allocSum(unsigned int const depth,
+         unsigned int const size) {
+
+    sample ** sum;
+
+    MALLOCARRAY(sum, depth);
+
+    if (!sum)
+        pm_error("Could not allocate memory for %u planes of sums", depth);
+    else {
+        unsigned int plane;
+
+        for (plane = 0; plane < depth; ++plane) {
+            MALLOCARRAY(sum[plane], size);
+            
+            if (!sum[plane])
+                pm_error("Could not allocate memory for %u sums", size);
+        }
     }
+    return sum;
+}
 
 
-/* PPM Horizontal Convolution
-**
-** Same as pgm_horizontal_convolve()
-**
-**/
 
 static void
-ppm_horizontal_convolve(const float ** const rweights,
-                        const float ** const gweights,
-                        const float ** const bweights) {
-    int ccol, col;
-    xel** xelbuf;
-    xel* outputrow;
-    xelval r, g, b;
-    int row, crow;
-    xel **rowptr, *temprptr;
-    int leftcol;
-    int i, irow;
-    int temprow;
-    int subcol, addcol;
-    float rsum, gsum, bsum;
-    int addrow, subrow;
-    long **rrowsum, **rrowsumptr;
-    long **growsum, **growsumptr;
-    long **browsum, **browsumptr;
-    int crowsp1;
-    long temprsum, tempgsum, tempbsum;
-
-    /* Allocate space for one convolution-matrix's worth of rows, plus
-    ** a row output buffer. */
-    xelbuf = pnm_allocarray( cols, crows + 1 );
-    outputrow = pnm_allocrow( cols );
-
-    /* Allocate array of pointers to xelbuf */
-    rowptr = (xel **) pnm_allocarray( 1, crows + 1);
-
-    /* Allocate intermediate row sums.  HORIZONTAL uses an extra row */
-    rrowsum = (long **) pm_allocarray( cols, crows + 1, sizeof(long) );
-    rrowsumptr = (long **) pnm_allocarray( 1, crows + 1);
-    growsum = (long **) pm_allocarray( cols, crows + 1, sizeof(long) );
-    growsumptr = (long **) pnm_allocarray( 1, crows + 1);
-    browsum = (long **) pm_allocarray( cols, crows + 1, sizeof(long) );
-    browsumptr = (long **) pnm_allocarray( 1, crows + 1);
-
-    pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 );
-
-    /* Read in one convolution-matrix's worth of image, less one row. */
-    for ( row = 0; row < crows - 1; ++row )
-    {
-    pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[row], cols, maxval, format, maxval, newformat );
-    /* Write out just the part we're not going to convolve. */
-    if ( row < crowso2 )
-        pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 );
-    }
+freeSum(sample **    const sum,
+        unsigned int const depth) {
 
-    /* First row only */
-    temprow = row % crows;
-    pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-    pnm_promoteformatrow(
-        xelbuf[temprow], cols, maxval, format, maxval, newformat );
+    unsigned int plane;
 
-    temprow = (row + 1) % crows;
-    i = 0;
-    for (irow = temprow; irow < crows; ++i, ++irow)
-    rowptr[i] = xelbuf[irow];
-    for (irow = 0; irow < temprow; ++irow, ++i)
-    rowptr[i] = xelbuf[irow];
+    for (plane = 0; plane < depth; ++plane)
+        free(sum[plane]);
 
-    for ( crow = 0; crow < crows; ++crow )
-    {
-    rrowsumptr[crow] = rrowsum[crow];
-    growsumptr[crow] = growsum[crow];
-    browsumptr[crow] = browsum[crow];
+    free(sum);
+}
+
+
+
+static void
+computeInitialColumnSums(struct pam *              const pamP,
+                         tuple **                  const window,
+                         const struct ConvKernel * const convKernelP,
+                         sample **                 const convColumnSum) {
+/*----------------------------------------------------------------------------
+  Add up the sum of each column of window[][], whose rows are described
+  by *inpamP.  The window's height is that of tthe convolution kernel
+  *convKernelP.
+
+  Return it as convColumnSum[][].
+-----------------------------------------------------------------------------*/
+    unsigned int plane;
+
+    for (plane = 0; plane < pamP->depth; ++plane) {
+        unsigned int col;
+
+        for (col = 0; col < pamP->width; ++col) {
+            unsigned int row;
+            for (row = 0, convColumnSum[plane][col] = 0;
+                 row < convKernelP->rows;
+                 ++row)
+                convColumnSum[plane][col] += window[row][col][plane];
+        }            
     }
- 
-    for ( col = 0; col < cols; ++col )
-    {
-    if ( col < ccolso2 || col >= cols - ccolso2 )
-        outputrow[col] = rowptr[crowso2][col];
-    else if ( col == ccolso2 )
-        {
-        leftcol = col - ccolso2;
-        rsum = 0.0;
-        gsum = 0.0;
-        bsum = 0.0;
-        for ( crow = 0; crow < crows; ++crow )
-        {
-        temprptr = rowptr[crow] + leftcol;
-        rrowsumptr[crow][leftcol] = 0L;
-        growsumptr[crow][leftcol] = 0L;
-        browsumptr[crow][leftcol] = 0L;
-        for ( ccol = 0; ccol < ccols; ++ccol )
-            {
-            rrowsumptr[crow][leftcol] += 
-                PPM_GETR( *(temprptr + ccol) );
-            growsumptr[crow][leftcol] += 
-                PPM_GETG( *(temprptr + ccol) );
-            browsumptr[crow][leftcol] += 
-                PPM_GETB( *(temprptr + ccol) );
+}
+
+
+
+static void
+convolveRowWithColumnSumsMean(const struct ConvKernel * const convKernelP,
+                              struct pam *              const pamP,
+                              tuple **                  const window,
+                              tuple *                   const outputrow,
+                              sample **                 const convColumnSum) {
+/*----------------------------------------------------------------------------
+  Convolve the rows in window[][] -- one convolution kernel's worth, where
+  window[0] is the top.  Put the result in outputrow[].
+
+  Use convColumnSum[][]: the sum of the pixels in each column over the
+  convolution window, where convColumnSum[P][C] is the sum for Plane P of
+  Column C.
+
+  Assume the convolution weight is the same everywhere within the convolution
+  matrix.  Ergo, we don't need any more information about the contents of a
+  column than the sum of its pixels.
+
+  Except that we need the individual input pixels for the edges (which can't
+  be convolved because the convolution window runs off the edge).
+-----------------------------------------------------------------------------*/
+    unsigned int plane;
+    
+    for (plane = 0; plane < pamP->depth; ++plane) {
+        unsigned int const crowso2 = convKernelP->rows / 2;
+        unsigned int const ccolso2 = convKernelP->cols / 2;
+        float const weight = convKernelP->weight[plane][0][0];
+
+        unsigned int col;
+        sample gisum;
+
+        for (col = 0, gisum = 0; col < pamP->width; ++col) {
+            if (col < ccolso2 || col >= pamP->width - ccolso2) {
+                /* The unconvolved left or right edge */
+                outputrow[col][plane] =
+                    clipSample(convKernelP->bias +
+                               window[crowso2][col][plane],
+                               pamP->maxval);
+            } else {
+                if (col == ccolso2) {
+                    unsigned int const leftcol = col - ccolso2;
+
+                    unsigned int ccol;
+
+                    for (ccol = 0; ccol < convKernelP->cols; ++ccol)
+                        gisum += convColumnSum[plane][leftcol + ccol];
+                } else {
+                    /* Column numbers to subtract or add to isum */
+                    unsigned int const subcol = col - ccolso2 - 1;
+                    unsigned int const addcol = col + ccolso2;  
+
+                    gisum -= convColumnSum[plane][subcol];
+                    gisum += convColumnSum[plane][addcol];
+                }
+                outputrow[col][plane] =
+                    makeSample(convKernelP->bias + gisum * weight,
+                               pamP->maxval);
             }
-        rsum += rrowsumptr[crow][leftcol] * rweights[crow][0];
-        gsum += growsumptr[crow][leftcol] * gweights[crow][0];
-        bsum += browsumptr[crow][leftcol] * bweights[crow][0];
-        }
-        temprsum = rsum + 0.5;
-        tempgsum = gsum + 0.5;
-        tempbsum = bsum + 0.5;
-        CHECK_RED;
-        CHECK_GREEN;
-        CHECK_BLUE;
-        PPM_ASSIGN( outputrow[col], r, g, b );
-        }
-    else
-        {
-        rsum = 0.0;
-        gsum = 0.0;
-        bsum = 0.0;
-        leftcol = col - ccolso2;
-        subcol = col - ccolso2 - 1;
-        addcol = col + ccolso2;
-        for ( crow = 0; crow < crows; ++crow )
-        {
-        rrowsumptr[crow][leftcol] = rrowsumptr[crow][subcol]
-            - PPM_GETR( rowptr[crow][subcol] )
-            + PPM_GETR( rowptr[crow][addcol] );
-        rsum += rrowsumptr[crow][leftcol] * rweights[crow][0];
-        growsumptr[crow][leftcol] = growsumptr[crow][subcol]
-            - PPM_GETG( rowptr[crow][subcol] )
-            + PPM_GETG( rowptr[crow][addcol] );
-        gsum += growsumptr[crow][leftcol] * gweights[crow][0];
-        browsumptr[crow][leftcol] = browsumptr[crow][subcol]
-            - PPM_GETB( rowptr[crow][subcol] )
-            + PPM_GETB( rowptr[crow][addcol] );
-        bsum += browsumptr[crow][leftcol] * bweights[crow][0];
-        }
-        temprsum = rsum + 0.5;
-        tempgsum = gsum + 0.5;
-        tempbsum = bsum + 0.5;
-        CHECK_RED;
-        CHECK_GREEN;
-        CHECK_BLUE;
-        PPM_ASSIGN( outputrow[col], r, g, b );
         }
-        }
-    pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 );
+    }
+}
 
 
-    /* For all subsequent rows */
 
-    subrow = crows;
-    addrow = crows - 1;
-    crowsp1 = crows + 1;
-    ++row;
-    for ( ; row < rows; ++row )
-    {
-    temprow = row % crowsp1;
-    pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format );
-    if ( PNM_FORMAT_TYPE(format) != newformat )
-        pnm_promoteformatrow(
-        xelbuf[temprow], cols, maxval, format, maxval, newformat );
+static void
+convolveRowWithColumnSumsVertical(
+    const struct ConvKernel * const convKernelP,
+    struct pam *              const pamP,
+    tuple **                  const window,
+    tuple *                   const outputrow,
+    sample **                 const convColumnSum) {
+/*----------------------------------------------------------------------------
+  Convolve the rows in window[][] -- one convolution kernel's worth, where
+  window[0] is the top.  Put the result in outputrow[].
 
-    temprow = (row + 2) % crowsp1;
-    i = 0;
-    for (irow = temprow; irow < crowsp1; ++i, ++irow)
-        {
-        rowptr[i] = xelbuf[irow];
-        rrowsumptr[i] = rrowsum[irow];
-        growsumptr[i] = growsum[irow];
-        browsumptr[i] = browsum[irow];
-        }
-    for (irow = 0; irow < temprow; ++irow, ++i)
-        {
-        rowptr[i] = xelbuf[irow];
-        rrowsumptr[i] = rrowsum[irow];
-        growsumptr[i] = growsum[irow];
-        browsumptr[i] = browsum[irow];
-        }
+  Use convColumnSum[][]: the sum of the pixels in each column over the
+  convolution window, where convColumnSum[P][C] is the sum for Plane P of
+  Column C.
 
-    for ( col = 0; col < cols; ++col )
-        {
-        if ( col < ccolso2 || col >= cols - ccolso2 )
-        outputrow[col] = rowptr[crowso2][col];
-        else if ( col == ccolso2 )
-        {
-        rsum = 0.0;
-        gsum = 0.0;
-        bsum = 0.0;
-        leftcol = col - ccolso2;
-        rrowsumptr[addrow][leftcol] = 0L;
-        growsumptr[addrow][leftcol] = 0L;
-        browsumptr[addrow][leftcol] = 0L;
-        for ( ccol = 0; ccol < ccols; ++ccol )
-            {
-            rrowsumptr[addrow][leftcol] += 
-            PPM_GETR( rowptr[addrow][leftcol + ccol] );
-            growsumptr[addrow][leftcol] += 
-            PPM_GETG( rowptr[addrow][leftcol + ccol] );
-            browsumptr[addrow][leftcol] += 
-            PPM_GETB( rowptr[addrow][leftcol + ccol] );
-            }
-        for ( crow = 0; crow < crows; ++crow )
-            {
-            rsum += rrowsumptr[crow][leftcol] * rweights[crow][0];
-            gsum += growsumptr[crow][leftcol] * gweights[crow][0];
-            bsum += browsumptr[crow][leftcol] * bweights[crow][0];
+  Assume the convolution weight is the same everywhere within a column.  Ergo,
+  we don't need any more information about the contents of a column than the
+  sum of its pixels.
+
+  Except that we need the individual input pixels for the edges (which can't
+  be convolved because the convolution window runs off the edge).
+-----------------------------------------------------------------------------*/
+    unsigned int const crowso2 = convKernelP->rows / 2;
+    unsigned int const ccolso2 = convKernelP->cols / 2;
+
+    unsigned int plane;
+
+    for (plane = 0; plane < pamP->depth; ++plane) {
+        unsigned int col;
+    
+        for (col = 0; col < pamP->width; ++col) {
+            if (col < ccolso2 || col >= pamP->width - ccolso2) {
+                /* The unconvolved left or right edge */
+                outputrow[col][plane] =
+                    clipSample(convKernelP->bias +
+                               window[crowso2][col][plane],
+                               pamP->maxval);
+            } else {
+                unsigned int const leftcol = col - ccolso2;
+                unsigned int ccol;
+                float sum;
+
+                sum = 0.0;
+
+                for (ccol = 0; ccol < convKernelP->cols; ++ccol)
+                    sum += convColumnSum[plane][leftcol + ccol] *
+                        convKernelP->weight[plane][0][ccol];
+
+                outputrow[col][plane] =
+                    makeSample(convKernelP->bias + sum, pamP->maxval);
             }
-        temprsum = rsum + 0.5;
-        tempgsum = gsum + 0.5;
-        tempbsum = bsum + 0.5;
-        CHECK_RED;
-        CHECK_GREEN;
-        CHECK_BLUE;
-        PPM_ASSIGN( outputrow[col], r, g, b );
         }
-        else
-        {
-        rsum = 0.0;
-        gsum = 0.0;
-        bsum = 0.0;
-        leftcol = col - ccolso2;
-        subcol = col - ccolso2 - 1;
-        addcol = col + ccolso2;  
-        rrowsumptr[addrow][leftcol] = rrowsumptr[addrow][subcol]
-            - PPM_GETR( rowptr[addrow][subcol] )
-            + PPM_GETR( rowptr[addrow][addcol] );
-        growsumptr[addrow][leftcol] = growsumptr[addrow][subcol]
-            - PPM_GETG( rowptr[addrow][subcol] )
-            + PPM_GETG( rowptr[addrow][addcol] );
-        browsumptr[addrow][leftcol] = browsumptr[addrow][subcol]
-            - PPM_GETB( rowptr[addrow][subcol] )
-            + PPM_GETB( rowptr[addrow][addcol] );
-        for ( crow = 0; crow < crows; ++crow )
-            {
-            rsum += rrowsumptr[crow][leftcol] * rweights[crow][0];
-            gsum += growsumptr[crow][leftcol] * gweights[crow][0];
-            bsum += browsumptr[crow][leftcol] * bweights[crow][0];
+    }
+}
+
+
+
+static void
+convolveMeanRowPlane(struct pam *              const pamP,
+                     tuple **                  const window,
+                     const struct ConvKernel * const convKernelP,
+                     unsigned int              const plane,
+                     tuple *                   const outputrow,
+                     sample *                  const convColumnSum) {
+/*----------------------------------------------------------------------------
+  Convolve plane 'plane' of one row of the image.  window[] is a vertical
+  window of the input image, one convolution kernel plus one row high.  The
+  top row (window[0] is the row that just passed out of the convolution
+  window, whereas the bottom row is the row that just entered it.
+
+  *pamP describes the tuple rows in window[] and also 'outputrow' (they are
+  the same).
+
+  Return the convolution result as outputrow[].
+
+  We update convColumnSum[] for use in convolving later rows.
+-----------------------------------------------------------------------------*/
+    unsigned int const crowso2 = convKernelP->rows / 2;
+    unsigned int const ccolso2 = convKernelP->cols / 2;
+    float const weight = convKernelP->weight[plane][0][0];
+    unsigned int const subrow = 0;
+        /* Row just above convolution window -- what we subtract from
+           running sum
+        */
+    unsigned int const addrow = 1 + (convKernelP->rows - 1);
+        /* Bottom row of convolution window: What we add to running sum */
+
+    unsigned int col;
+    sample gisum;
+
+    for (col = 0, gisum = 0; col < pamP->width; ++col) {
+        if (col < ccolso2 || col >= pamP->width - ccolso2) {
+            /* The unconvolved left or right edge */
+            outputrow[col][plane] =
+                clipSample(convKernelP->bias + window[crowso2][col][plane],
+                           pamP->maxval);
+        } else {
+            if (col == ccolso2) {
+                unsigned int const leftcol = col - ccolso2;
+
+                unsigned int ccol;
+
+                for (ccol = 0; ccol < convKernelP->cols; ++ccol) {
+                    sample * const thisColumnSumP =
+                        &convColumnSum[leftcol + ccol];
+                    *thisColumnSumP = *thisColumnSumP
+                        - window[subrow][ccol][plane]
+                        + window[addrow][ccol][plane];
+                    gisum += *thisColumnSumP;
+                }
+            } else {
+                /* Column numbers to subtract or add to isum */
+                unsigned int const subcol = col - ccolso2 - 1;
+                unsigned int const addcol = col + ccolso2;  
+                
+                convColumnSum[addcol] = convColumnSum[addcol]
+                    - window[subrow][addcol][plane]
+                    + window[addrow][addcol][plane];
+                
+                gisum = gisum - convColumnSum[subcol] + convColumnSum[addcol];
             }
-        temprsum = rsum + 0.5;
-        tempgsum = gsum + 0.5;
-        tempbsum = bsum + 0.5;
-        CHECK_RED;
-        CHECK_GREEN;
-        CHECK_BLUE;
-        PPM_ASSIGN( outputrow[col], r, g, b );
-        }
+            outputrow[col][plane] =
+                makeSample(convKernelP->bias + gisum * weight, pamP->maxval);
         }
-    pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 );
     }
+}
 
-    /* Now write out the remaining unconvolved rows in xelbuf. */
-    for ( irow = crowso2 + 1; irow < crows; ++irow )
-    pnm_writepnmrow(
-            stdout, rowptr[irow], cols, maxval, newformat, 0 );
 
-    }
 
+typedef void convolver(struct pam *              const inpamP,
+                       struct pam *              const outpamP,
+                       const struct ConvKernel * const convKernelP);
 
-/* PPM Vertical Convolution
-**
-** Same as pgm_vertical_convolve()
-**
-*/
+
+
+static convolver convolveMean;
 
 static void
-ppm_vertical_convolve(const float ** const rweights,
-                      const float ** const gweights,
-                      const float ** const bweights) {
-    int ccol, col;
-    xel** xelbuf;
-    xel* outputrow;
-    xelval r, g, b;
-    int row, crow;
-    xel **rowptr, *temprptr;
-    int i, irow;
-    int toprow, temprow;
-    int subrow, addrow;
-    int tempcol;
-    long *rcolumnsum, *gcolumnsum, *bcolumnsum;
-    int crowsp1;
-    int addcol;
-    long temprsum, tempgsum, tempbsum;
-
-    /* Allocate space for one convolution-matrix's worth of rows, plus
-    ** a row output buffer. VERTICAL uses an extra row. */
-    xelbuf = pnm_allocarray(cols, crows + 1);
-    outputrow = pnm_allocrow(cols);
-
-    /* Allocate array of pointers to xelbuf */
-    rowptr = (xel **) pnm_allocarray(1, crows + 1);
-
-    /* Allocate space for intermediate column sums */
-    MALLOCARRAY_NOFAIL(rcolumnsum, cols);
-    MALLOCARRAY_NOFAIL(gcolumnsum, cols);
-    MALLOCARRAY_NOFAIL(bcolumnsum, cols);
-
-    for (col = 0; col < cols; ++col) {
-        rcolumnsum[col] = 0L;
-        gcolumnsum[col] = 0L;
-        bcolumnsum[col] = 0L;
-    }
+convolveMean(struct pam *              const inpamP,
+             struct pam *              const outpamP,
+             const struct ConvKernel * const convKernelP) {
+/*----------------------------------------------------------------------------
+  Mean Convolution
+
+  This is for the common case where you just want the target pixel replaced
+  with the average value of its neighbors.  This can work much faster than the
+  general case because you can reduce the number of floating point operations
+  that are required since all the weights are the same.  You will only need to
+  multiply by the weight once, not for every pixel in the convolution matrix.
+
+  This algorithm works as follows: At a certain vertical position in the
+  image, create sums for each column fragment of the convolution height all
+  the way across the image.  Then add those sums across the convolution width
+  to obtain the total sum over the convolution area and multiply that sum by
+  the weight.  As you move left to right, to calculate the next output pixel,
+  take the total sum you just generated, add in the value of the next column
+  and subtract the value of the leftmost column.  Multiply that by the weight
+  and that's it.  As you move down a row, calculate new column sums by using
+  previous sum for that column and adding in pixel on current row and
+  subtracting pixel in top row.
+
+  We assume the convolution kernel is uniform -- same weights everywhere.
+
+  We assume the output is PGM and the input is PGM or PBM.
+-----------------------------------------------------------------------------*/
+    unsigned int const windowHeight = convKernelP->rows + 1;
+        /* The height of the window we keep in the row buffer.  The buffer
+           contains the rows covered by the convolution kernel, plus the row
+           immediately above that.  The latter is there because to compute
+           the sliding mean, we need to subtract off the row that the
+           convolution kernel just slid past.
+        */
+    unsigned int const crowso2 = convKernelP->rows / 2;
+        /* Number of rows of the convolution window above/below the current
+           row.  Note that the convolution window is always an odd number
+           of rows, so this rounds down.
+        */
+    tuple ** rowbuf;
+        /* Same as in convolveGeneral */
+    tuple ** circMap;
+        /* Same as in convolveGeneral */
+    tuple * outputrow;
+        /* Same as in convolveGeneral */
+    unsigned int row;
+        /* Row number of the row currently being convolved; i.e. the row
+           at the center of the current convolution window and the row of
+           the output file to be output next.
+        */
+    sample ** convColumnSum;  /* Malloc'd */
+        /* convColumnSum[plane][col] is the sum of Plane 'plane' of all the
+           pixels in the Column 'col' of the image within the current vertical
+           convolution window.  I.e. if our convolution kernel is 5 rows high
+           and we're now looking at Rows 10-15, convColumn[0][3] is the sum of
+           Plane 0 of Column 3, Rows 10-15.
+        */
 
-    pnm_writepnminit(stdout, cols, rows, maxval, newformat, 0);
-
-    /* Read in one convolution-matrix's worth of image, less one row. */
-    for (row = 0; row < crows - 1; ++row) {
-        pnm_readpnmrow(ifp, xelbuf[row], cols, maxval, format);
-        if (PNM_FORMAT_TYPE(format) != newformat)
-            pnm_promoteformatrow(xelbuf[row], cols, maxval, format, 
-                                 maxval, newformat);
-        /* Write out just the part we're not going to convolve. */
-        if (row < crowso2)
-            pnm_writepnmrow(stdout, xelbuf[row], cols, maxval, newformat, 0);
-    }
+    rowbuf = allocRowbuf(outpamP, windowHeight);
+    MALLOCARRAY_NOFAIL(circMap, windowHeight);
+    outputrow = pnm_allocpamrow(outpamP);
 
-    /* Now the rest of the image - read in the row at the end of
-    ** xelbuf, and convolve and write out the row in the middle.
-    */
-    /* For first row only */
+    convColumnSum = allocSum(outpamP->depth, outpamP->width);
+
+    pnm_writepaminit(outpamP);
+
+    readAndScaleRows(inpamP, convKernelP->rows, rowbuf,
+                      outpamP->maxval, outpamP->depth);
 
-    toprow = row + 1;
-    temprow = row % crows;
-    pnm_readpnmrow(ifp, xelbuf[temprow], cols, maxval, format);
-    if (PNM_FORMAT_TYPE(format) != newformat)
-        pnm_promoteformatrow(xelbuf[temprow], cols, maxval, format, maxval, 
-                             newformat);
+    writeUnconvolvedTop(outpamP, convKernelP, rowbuf);
 
-    /* Arrange rowptr to eliminate the use of mod function to determine
-    ** which row of xelbuf is 0...crows.  Mod function can be very costly.
+    setupCircMap(circMap, rowbuf, windowHeight, 0);
+
+    /* Convolve the first window the long way */
+    computeInitialColumnSums(inpamP, circMap, convKernelP, convColumnSum);
+
+    convolveRowWithColumnSumsMean(convKernelP, outpamP, circMap,
+                                  outputrow, convColumnSum);
+
+    pnm_writepamrow(outpamP, outputrow);
+
+    /* For all subsequent rows do it this way as the columnsums have been
+       generated.  Now we can use them to reduce further calculations.  We
+       slide the window down a row at a time by reading a row into the bottom
+       of the circular buffer, adding it to the column sums, then subtracting
+       out the row at the top of the circular buffer.
     */
-    temprow = toprow % crows;
-    i = 0;
-    for (irow = temprow; irow < crows; ++i, ++irow)
-        rowptr[i] = xelbuf[irow];
-    for (irow = 0; irow < temprow; ++irow, ++i)
-        rowptr[i] = xelbuf[irow];
-
-    for (col = 0; col < cols; ++col) {
-        if (col < ccolso2 || col >= cols - ccolso2)
-            outputrow[col] = rowptr[crowso2][col];
-        else if (col == ccolso2) {
-            int const leftcol = col - ccolso2;
-            float rsum, gsum, bsum;
-            rsum = 0.0;
-            gsum = 0.0;
-            bsum = 0.0;
-            for (crow = 0; crow < crows; ++crow) {
-                temprptr = rowptr[crow] + leftcol;
-                for (ccol = 0; ccol < ccols; ++ccol) {
-                    rcolumnsum[leftcol + ccol] += 
-                        PPM_GETR(*(temprptr + ccol));
-                    gcolumnsum[leftcol + ccol] += 
-                        PPM_GETG(*(temprptr + ccol));
-                    bcolumnsum[leftcol + ccol] += 
-                        PPM_GETB(*(temprptr + ccol));
+    for (row = crowso2 + 1; row < inpamP->height - crowso2; ++row) {
+        unsigned int const windowBotRow = row + crowso2;
+            /* Row number of bottom-most row present in rowbuf[],
+               which is the bottom of the convolution window for the current
+               row.
+            */
+        unsigned int const windowTopRow = row - crowso2 - 1;
+            /* Row number of top-most row present in rowbuf[], which is
+               the top row of the convolution window for the previous row:
+               just above the convolution window for the current row.
+            */
+        unsigned int plane;
+
+        readAndScaleRow(inpamP, rowbuf[windowBotRow % windowHeight],
+                        outpamP->maxval, outpamP->depth);
+
+        setupCircMap(circMap, rowbuf, windowHeight,
+                     windowTopRow % windowHeight);
+
+        for (plane = 0; plane < outpamP->depth; ++plane)
+            convolveMeanRowPlane(outpamP, circMap, convKernelP, plane,
+                                 outputrow, convColumnSum[plane]);
+
+        pnm_writepamrow(outpamP, outputrow);
+    }
+    writeUnconvolvedBottom(outpamP, convKernelP, windowHeight, circMap);
+
+    freeSum(convColumnSum, outpamP->depth);
+    freeRowbuf(rowbuf, windowHeight);
+}
+
+
+
+static sample ***
+allocRowSum(unsigned int const depth,
+            unsigned int const height,
+            unsigned int const width) {
+
+    sample *** sum;
+
+    MALLOCARRAY(sum, depth);
+
+    if (!sum)
+        pm_error("Could not allocate memory for %u planes of sums", depth);
+    else {
+        unsigned int plane;
+
+        for (plane = 0; plane < depth; ++plane) {
+            MALLOCARRAY(sum[plane], height);
+            
+            if (!sum[plane])
+                pm_error("Could not allocate memory for %u rows of sums",
+                         height);
+            else {
+                unsigned int row;
+
+                for (row = 0; row < height; ++row) {
+                    MALLOCARRAY(sum[plane][row], width);
+                    
+                    if (!sum[plane][row])
+                        pm_error("Could not allocate memory "
+                                 "for a row of sums");
                 }
             }
-            for (ccol = 0; ccol < ccols; ++ccol) {
-                rsum += rcolumnsum[leftcol + ccol] * rweights[0][ccol];
-                gsum += gcolumnsum[leftcol + ccol] * gweights[0][ccol];
-                bsum += bcolumnsum[leftcol + ccol] * bweights[0][ccol];
-            }
-            temprsum = rsum + 0.5;
-            tempgsum = gsum + 0.5;
-            tempbsum = bsum + 0.5;
-            CHECK_RED;
-            CHECK_GREEN;
-            CHECK_BLUE;
-            PPM_ASSIGN(outputrow[col], r, g, b);
-        } else {
-            int const leftcol = col - ccolso2;
-            float rsum, gsum, bsum;
-            rsum = 0.0;
-            gsum = 0.0;
-            bsum = 0.0;
-            addcol = col + ccolso2;  
-            for (crow = 0; crow < crows; ++crow) {
-                rcolumnsum[addcol] += PPM_GETR( rowptr[crow][addcol]);
-                gcolumnsum[addcol] += PPM_GETG( rowptr[crow][addcol]);
-                bcolumnsum[addcol] += PPM_GETB( rowptr[crow][addcol]);
-            }
-            for (ccol = 0; ccol < ccols; ++ccol) {
-                rsum += rcolumnsum[leftcol + ccol] * rweights[0][ccol];
-                gsum += gcolumnsum[leftcol + ccol] * gweights[0][ccol];
-                bsum += bcolumnsum[leftcol + ccol] * bweights[0][ccol];
-            }
-            temprsum = rsum + 0.5;
-            tempgsum = gsum + 0.5;
-            tempbsum = bsum + 0.5;
-            CHECK_RED;
-            CHECK_GREEN;
-            CHECK_BLUE;
-            PPM_ASSIGN(outputrow[col], r, g, b);
         }
     }
-    pnm_writepnmrow(stdout, outputrow, cols, maxval, newformat, 0);
-    
-    /* For all subsequent rows */
-    subrow = crows;
-    addrow = crows - 1;
-    crowsp1 = crows + 1;
-    ++row;
-    for (; row < rows; ++row) {
-        toprow = row + 1;
-        temprow = row % (crows +1);
-        pnm_readpnmrow(ifp, xelbuf[temprow], cols, maxval, format);
-        if (PNM_FORMAT_TYPE(format) != newformat)
-            pnm_promoteformatrow(xelbuf[temprow], cols, maxval, format, 
-                                 maxval, newformat);
-
-        /* Arrange rowptr to eliminate the use of mod function to determine
-        ** which row of xelbuf is 0...crows.  Mod function can be very costly.
-        */
-        temprow = (toprow + 1) % crowsp1;
-        i = 0;
-        for (irow = temprow; irow < crowsp1; ++i, ++irow)
-            rowptr[i] = xelbuf[irow];
-        for (irow = 0; irow < temprow; ++irow, ++i)
-            rowptr[i] = xelbuf[irow];
-
-        for (col = 0; col < cols; ++col) {
-            if (col < ccolso2 || col >= cols - ccolso2)
-                outputrow[col] = rowptr[crowso2][col];
-            else if (col == ccolso2) {
-                int const leftcol = col - ccolso2;
-                float rsum, gsum, bsum;
-                rsum = 0.0;
-                gsum = 0.0;
-                bsum = 0.0;
-
-                for (ccol = 0; ccol < ccols; ++ccol) {
-                    tempcol = leftcol + ccol;
-                    rcolumnsum[tempcol] = rcolumnsum[tempcol] 
-                        - PPM_GETR(rowptr[subrow][ccol])
-                        + PPM_GETR(rowptr[addrow][ccol]);
-                    rsum = rsum + rcolumnsum[tempcol] * rweights[0][ccol];
-                    gcolumnsum[tempcol] = gcolumnsum[tempcol] 
-                        - PPM_GETG(rowptr[subrow][ccol])
-                        + PPM_GETG(rowptr[addrow][ccol]);
-                    gsum = gsum + gcolumnsum[tempcol] * gweights[0][ccol];
-                    bcolumnsum[tempcol] = bcolumnsum[tempcol] 
-                        - PPM_GETB(rowptr[subrow][ccol])
-                        + PPM_GETB(rowptr[addrow][ccol]);
-                    bsum = bsum + bcolumnsum[tempcol] * bweights[0][ccol];
+    return sum;
+}
+
+
+
+static void
+freeRowSum(sample ***   const sum,
+           unsigned int const depth,
+           unsigned int const height) {
+
+    unsigned int plane;
+
+    for (plane = 0; plane < depth; ++plane) {
+        unsigned int row;
+
+        for (row = 0; row < height; ++row)
+            free(sum[plane][row]);
+
+        free(sum[plane]);
+    }
+    free(sum);
+}
+
+
+
+static void
+convolveHorizontalRowPlane0(struct pam *              const outpamP,
+                            tuple **                  const window,
+                            const struct ConvKernel * const convKernelP,
+                            unsigned int              const plane,
+                            tuple *                   const outputrow,
+                            sample **                 const sumWindow) {
+/*----------------------------------------------------------------------------
+   Convolve the first convolvable row and generate the row sums from scratch.
+   (For subsequent rows, Caller can just incrementally modify the row sums).
+-----------------------------------------------------------------------------*/
+    unsigned int const crowso2 = convKernelP->rows / 2;
+    unsigned int const ccolso2 = convKernelP->cols / 2;
+
+    unsigned int col;
+
+    for (col = 0; col < outpamP->width; ++col) {
+        if (col < ccolso2 || col >= outpamP->width - ccolso2) {
+            /* The unconvolved left or right edge */
+            outputrow[col][plane] =
+                clipSample(convKernelP->bias + window[crowso2][col][plane],
+                           outpamP->maxval);
+        } else {
+            float matrixSum;
+
+            if (col == ccolso2) {
+                /* This is the first column for which the entire convolution
+                   kernel fits within the image horizontally.  I.e. the window
+                   starts at the left edge of the image.
+                */
+                unsigned int const leftcol = 0;
+            
+                unsigned int crow;
+
+                for (crow = 0, matrixSum = 0.0;
+                     crow < convKernelP->rows;
+                     ++crow) {
+                    tuple * const tuplesInWindow = &window[crow][leftcol];
+
+                    unsigned int ccol;
+                
+                    sumWindow[crow][col] = 0;
+                    for (ccol = 0; ccol < convKernelP->cols; ++ccol)
+                        sumWindow[crow][col] += tuplesInWindow[ccol][plane];
+                    matrixSum +=
+                        sumWindow[crow][col] *
+                        convKernelP->weight[plane][crow][0];
                 }
-                temprsum = rsum + 0.5;
-                tempgsum = gsum + 0.5;
-                tempbsum = bsum + 0.5;
-                CHECK_RED;
-                CHECK_GREEN;
-                CHECK_BLUE;
-                PPM_ASSIGN(outputrow[col], r, g, b);
             } else {
-                int const leftcol = col - ccolso2;
-                float rsum, gsum, bsum;
-                rsum = 0.0;
-                gsum = 0.0;
-                bsum = 0.0;
-                addcol = col + ccolso2;
-                rcolumnsum[addcol] = rcolumnsum[addcol]
-                    - PPM_GETR(rowptr[subrow][addcol])
-                    + PPM_GETR(rowptr[addrow][addcol]);
-                gcolumnsum[addcol] = gcolumnsum[addcol]
-                    - PPM_GETG(rowptr[subrow][addcol])
-                    + PPM_GETG(rowptr[addrow][addcol]);
-                bcolumnsum[addcol] = bcolumnsum[addcol]
-                    - PPM_GETB(rowptr[subrow][addcol])
-                    + PPM_GETB(rowptr[addrow][addcol]);
-                for (ccol = 0; ccol < ccols; ++ccol) {
-                    rsum += rcolumnsum[leftcol + ccol] * rweights[0][ccol];
-                    gsum += gcolumnsum[leftcol + ccol] * gweights[0][ccol];
-                    bsum += bcolumnsum[leftcol + ccol] * bweights[0][ccol];
+                unsigned int const subcol  = col - ccolso2 - 1;
+                unsigned int const addcol  = col + ccolso2;
+
+                unsigned int crow;
+                
+                for (crow = 0, matrixSum = 0.0;
+                     crow < convKernelP->rows;
+                     ++crow) {
+                    sumWindow[crow][col] = sumWindow[crow][col-1] +
+                        + window[crow][addcol][plane]
+                        - window[crow][subcol][plane];
+                    matrixSum +=
+                        sumWindow[crow][col] *
+                        convKernelP->weight[plane][crow][0];
                 }
-                temprsum = rsum + 0.5;
-                tempgsum = gsum + 0.5;
-                tempbsum = bsum + 0.5;
-                CHECK_RED;
-                CHECK_GREEN;
-                CHECK_BLUE;
-                PPM_ASSIGN(outputrow[col], r, g, b);
             }
+            outputrow[col][plane] =
+                makeSample(convKernelP->bias + matrixSum, outpamP->maxval);
         }
-        pnm_writepnmrow(stdout, outputrow, cols, maxval, newformat, 0);
     }
+}
+
 
-    /* Now write out the remaining unconvolved rows in xelbuf. */
-    for (irow = crowso2 + 1; irow < crows; ++irow)
-        pnm_writepnmrow(stdout, rowptr[irow], cols, maxval, newformat, 0);
 
+static void
+setupCircMap2(tuple **     const rowbuf,
+              sample **    const convRowSum,
+              tuple **     const circMap,
+              sample **    const sumCircMap,
+              unsigned int const windowTopRow,
+              unsigned int const windowHeight) {
+
+    unsigned int const toprow = windowTopRow % windowHeight;
+    
+    unsigned int crow;
+    unsigned int i;
+
+
+    i = 0;
+
+    for (crow = toprow; crow < windowHeight; ++i, ++crow) {
+        circMap[i] = rowbuf[crow];
+        sumCircMap[i] = convRowSum[crow];
+    }
+    for (crow = 0; crow < toprow; ++crow, ++i) {
+        circMap[i] = rowbuf[crow];
+        sumCircMap[i] = convRowSum[crow];
+    }
 }
 
 
 
 static void
-determineConvolveType(xel * const *         const cxels,
-                      struct convolveType * const typeP) {
+convolveHorizontalRowPlane(struct pam *              const pamP,
+                           tuple **                  const window,
+                           const struct ConvKernel * const convKernelP,
+                           unsigned int              const plane,
+                           tuple *                   const outputrow,
+                           sample **                 const sumWindow) {
 /*----------------------------------------------------------------------------
-   Determine which form of convolution is best.  The general form always
-   works, but with some special case convolution matrices, faster forms
-   of convolution are possible.
-
-   We don't check for the case that one of the PPM colors can have 
-   differing types.  We handle only cases where all PPMs are of the same
-   special case.
+   Convolve the row at the center of the convolution window described
+   by *convKernelP, where window[][] contains the input image tuples
+   for the window.  *pamP describes the rows in it, but its height is
+   one convolution window.
+
+   Convolve only the Plane 'plane' samples.
+
+   sumWindow[][] mirrors window[].  sumWindow[R] applies to window[R].
+   sumWindow[R][C] is the sum of samples in row R of the convolution window
+   centered on Column C.  We assume the convolution weights are the same
+   everywhere within a row of the kernel, so that we can generate these
+   sums incrementally, moving to the right through the image.
 -----------------------------------------------------------------------------*/
-    int horizontal, vertical;
-    int tempcxel, rtempcxel, gtempcxel, btempcxel;
-    int crow, ccol;
-
-    switch (PNM_FORMAT_TYPE(format)) {
-    case PPM_TYPE:
-        horizontal = TRUE;  /* initial assumption */
-        crow = 0;
-        while (horizontal && (crow < crows)) {
-            ccol = 1;
-            rtempcxel = PPM_GETR(cxels[crow][0]);
-            gtempcxel = PPM_GETG(cxels[crow][0]);
-            btempcxel = PPM_GETB(cxels[crow][0]);
-            while (horizontal && (ccol < ccols)) {
-                if ((PPM_GETR(cxels[crow][ccol]) != rtempcxel) ||
-                    (PPM_GETG(cxels[crow][ccol]) != gtempcxel) ||
-                    (PPM_GETB(cxels[crow][ccol]) != btempcxel)) 
-                    horizontal = FALSE;
-                ++ccol;
-            }
-            ++crow;
-        }
+    unsigned int const ccolso2 = convKernelP->cols / 2;
+    unsigned int const crowso2 = convKernelP->rows / 2;
 
-        vertical = TRUE;   /* initial assumption */
-        ccol = 0;
-        while (vertical && (ccol < ccols)) {
-            crow = 1;
-            rtempcxel = PPM_GETR(cxels[0][ccol]);
-            gtempcxel = PPM_GETG(cxels[0][ccol]);
-            btempcxel = PPM_GETB(cxels[0][ccol]);
-            while (vertical && (crow < crows)) {
-                if ((PPM_GETR(cxels[crow][ccol]) != rtempcxel) |
-                    (PPM_GETG(cxels[crow][ccol]) != gtempcxel) |
-                    (PPM_GETB(cxels[crow][ccol]) != btempcxel))
-                    vertical = FALSE;
-                ++crow;
+    unsigned int const newrow  = convKernelP->rows - 1;
+
+    unsigned int col;
+
+    for (col = 0; col < pamP->width; ++col) {
+        float matrixSum;
+
+        if (col < ccolso2 || col >= pamP->width - ccolso2) {
+            outputrow[col][plane] =
+                clipSample(convKernelP->bias + window[crowso2][col][plane],
+                           pamP->maxval);
+        } else if (col == ccolso2) {
+            unsigned int const leftcol = 0;
+                /* Window is up againt left edge of image */
+
+            {
+                unsigned int ccol;
+                sumWindow[newrow][col] = 0;
+                for (ccol = 0; ccol < convKernelP->cols; ++ccol)
+                    sumWindow[newrow][col] +=
+                        window[newrow][leftcol + ccol][plane];
             }
-            ++ccol;
-        }
-        break;
-        
-    default:
-        horizontal = TRUE; /* initial assumption */
-        crow = 0;
-        while (horizontal && (crow < crows)) {
-            ccol = 1;
-            tempcxel = PNM_GET1(cxels[crow][0]);
-            while (horizontal && (ccol < ccols)) {
-                if (PNM_GET1(cxels[crow][ccol]) != tempcxel)
-                    horizontal = FALSE;
-                ++ccol;
+            {
+                unsigned int crow;
+                for (crow = 0, matrixSum = 0.0;
+                     crow < convKernelP->rows;
+                     ++crow) {
+                    matrixSum += sumWindow[crow][col] *
+                        convKernelP->weight[plane][crow][0];
+                }
             }
-            ++crow;
-        }
-        
-        vertical = TRUE;  /* initial assumption */
-        ccol = 0;
-        while (vertical && (ccol < ccols)) {
-            crow = 1;
-            tempcxel = PNM_GET1(cxels[0][ccol]);
-            while (vertical && (crow < crows)) {
-                if (PNM_GET1(cxels[crow][ccol]) != tempcxel)
-                    vertical = FALSE;
-                ++crow;
+        } else {
+            unsigned int const subcol  = col - ccolso2 - 1;
+            unsigned int const addcol  = col + ccolso2;  
+
+            unsigned int crow;
+
+            sumWindow[newrow][col] =
+                sumWindow[newrow][col-1]
+                + window[newrow][addcol][plane]
+                - window[newrow][subcol][plane];
+
+            for (crow = 0, matrixSum = 0.0; crow < convKernelP->rows; ++crow) {
+                matrixSum += sumWindow[crow][col] *
+                    convKernelP->weight[plane][crow][0];
             }
-            ++ccol;
         }
-        break;
+        outputrow[col][plane] =
+            makeSample(convKernelP->bias + matrixSum, pamP->maxval);
     }
-    
-    /* Which type do we have? */
-    if (horizontal && vertical) {
-        typeP->ppmConvolver = ppm_mean_convolve;
-        typeP->pgmConvolver = pgm_mean_convolve;
-    } else if (horizontal) {
-        typeP->ppmConvolver = ppm_horizontal_convolve;
-        typeP->pgmConvolver = pgm_horizontal_convolve;
-    } else if (vertical) {
-        typeP->ppmConvolver = ppm_vertical_convolve;
-        typeP->pgmConvolver = pgm_vertical_convolve;
-    } else {
-        typeP->ppmConvolver = ppm_general_convolve;
-        typeP->pgmConvolver = pgm_general_convolve;
+}
+
+
+
+static convolver convolveHorizontal;
+
+static void
+convolveHorizontal(struct pam *              const inpamP,
+                   struct pam *              const outpamP,
+                   const struct ConvKernel * const convKernelP) {
+/*----------------------------------------------------------------------------
+  Horizontal Convolution
+
+  Similar idea to using columnsums of the Mean and Vertical convolution, but
+  uses temporary sums of row values.  Need to multiply by weights once for
+  each row in the convolution kernel.  Each time we start a new line, we must
+  recalculate the initials rowsums for the newest row only.  Uses queue to
+  still access previous row sums.
+-----------------------------------------------------------------------------*/
+    unsigned int const crowso2 = convKernelP->rows / 2;
+        /* Same as in convolveMean */
+    unsigned int const windowHeight = convKernelP->rows;
+        /* Same as in convolveMean */
+
+    tuple ** rowbuf;
+        /* Same as in convolveGeneral */
+    tuple ** circMap;
+        /* Same as in convolveGeneral */
+    tuple * outputrow;
+        /* Same as in convolveGeneral */
+    unsigned int plane;
+    sample *** convRowSum;  /* Malloc'd */
+    sample ** sumCircMap;  /* Malloc'd */
+
+    rowbuf = allocRowbuf(inpamP, windowHeight);
+    MALLOCARRAY_NOFAIL(circMap, windowHeight);
+    outputrow = pnm_allocpamrow(outpamP);
+
+    convRowSum = allocRowSum(outpamP->depth, windowHeight, outpamP->width);
+    MALLOCARRAY_NOFAIL(sumCircMap, windowHeight);
+
+    pnm_writepaminit(outpamP);
+
+    readAndScaleRows(inpamP, convKernelP->rows, rowbuf,
+                      outpamP->maxval, outpamP->depth);
+
+    writeUnconvolvedTop(outpamP, convKernelP, rowbuf);
+
+    setupCircMap(circMap, rowbuf, windowHeight, 0);
+
+    /* Convolve the first convolvable row and generate convRowSum[][] */
+    for (plane = 0; plane < outpamP->depth; ++plane) {
+        unsigned int crow;
+
+        for (crow = 0; crow < convKernelP->rows; ++crow)
+            sumCircMap[crow] = convRowSum[plane][crow];
+ 
+        convolveHorizontalRowPlane0(outpamP, circMap, convKernelP, plane,
+                                    outputrow, sumCircMap);
     }
+    pnm_writepamrow(outpamP, outputrow);
+
+    /* Convolve the rest of the rows, using convRowSum[] */
+
+    for (plane = 0; plane < outpamP->depth; ++plane) {
+        unsigned int row;
+            /* Same as in convolveMean */
+
+        for (row = convKernelP->rows/2 + 1;
+             row < inpamP->height - convKernelP->rows/2;
+             ++row) {
+
+            unsigned int const windowBotRow = row + crowso2;
+            unsigned int const windowTopRow = row - crowso2;
+                /* Same as in convolveMean */
+
+            readAndScaleRow(inpamP, rowbuf[windowBotRow % windowHeight],
+                            outpamP->maxval, outpamP->depth);
+            
+            setupCircMap2(rowbuf, convRowSum[plane], circMap, sumCircMap,
+                          windowTopRow, windowHeight);
+
+            convolveHorizontalRowPlane(outpamP, circMap, convKernelP, plane,
+                                       outputrow, sumCircMap);
+            
+            pnm_writepamrow(outpamP, outputrow);
+        }
+    }
+
+    writeUnconvolvedBottom(outpamP, convKernelP, windowHeight, circMap);
+
+    freeRowSum(convRowSum, outpamP->depth, windowHeight);
+    freeRowbuf(rowbuf, windowHeight);
 }
 
 
 
 static void
-convolveIt(int                 const format,
-           struct convolveType const convolveType,
-           const float**       const rweights,
-           const float**       const gweights,
-           const float**       const bweights) {
-
-    switch (PNM_FORMAT_TYPE(format)) {
-    case PPM_TYPE:
-        convolveType.ppmConvolver(rweights, gweights, bweights);
-        break;
-
-    default:
-        convolveType.pgmConvolver(gweights);
+convolveVerticalRowPlane(struct pam *              const pamP,
+                         tuple **                  const circMap,
+                         const struct ConvKernel * const convKernelP,
+                         unsigned int              const plane,
+                         tuple *                   const outputrow,
+                         sample *                  const convColumnSum) {
+
+    unsigned int const crowso2 = convKernelP->rows / 2;
+    unsigned int const ccolso2 = convKernelP->cols / 2;
+
+    unsigned int const subrow = 0;
+        /* Row just above convolution window -- what we subtract from
+           running sum
+        */
+    unsigned int const addrow = 1 + (convKernelP->rows - 1);
+        /* Bottom row of convolution window: What we add to running sum */
+    
+    unsigned int col;
+
+    for (col = 0; col < pamP->width; ++col) {
+        if (col < ccolso2 || col >= pamP->width - ccolso2) {
+            /* The unconvolved left or right edge */
+            outputrow[col][plane] =
+                clipSample(convKernelP->bias + circMap[crowso2][col][plane],
+                           pamP->maxval);
+        } else {
+            float matrixSum;
+
+            if (col == ccolso2) {
+                unsigned int const leftcol = 0;
+                    /* Convolution window is againt left edge of image */
+
+                unsigned int ccol;
+
+                /* Slide window down in the first kernel's worth of columns */
+                for (ccol = 0; ccol < convKernelP->cols; ++ccol) {
+                    convColumnSum[leftcol + ccol] +=
+                        circMap[addrow][leftcol + ccol][plane];
+                    convColumnSum[leftcol + ccol] -=
+                        circMap[subrow][leftcol + ccol][plane];
+                }
+                for (ccol = 0, matrixSum = 0.0;
+                     ccol < convKernelP->cols;
+                     ++ccol) {
+                    matrixSum += convColumnSum[leftcol + ccol] *
+                        convKernelP->weight[plane][0][ccol];
+                }
+            } else {
+                unsigned int const leftcol = col - ccolso2;
+                unsigned int const addcol  = col + ccolso2;
+
+                unsigned int ccol;
+
+                /* Slide window down in the column that just entered the
+                   window
+                */
+                convColumnSum[addcol] += circMap[addrow][addcol][plane];
+                convColumnSum[addcol] -= circMap[subrow][addcol][plane];
+
+                for (ccol = 0, matrixSum = 0.0;
+                     ccol < convKernelP->cols;
+                     ++ccol) {
+                    matrixSum += convColumnSum[leftcol + ccol] *
+                        convKernelP->weight[plane][0][ccol];
+                }
+            }
+            outputrow[col][plane] =
+                makeSample(convKernelP->bias + matrixSum, pamP->maxval);
+        }
     }
 }
 
 
 
+static convolver convolveVertical;
+
 static void
-readKernel(const char * const fileName,
-           int *        const colsP,
-           int *        const rowsP,
-           xelval *     const maxvalP,
-           int *        const formatP,
-           xel ***      const xelsP) {
-/*----------------------------------------------------------------------------
-   Read in the pseudo-PNM that is the convolution matrix.
+convolveVertical(struct pam *              const inpamP,
+                 struct pam *              const outpamP,
+                 const struct ConvKernel * const convKernelP) {
+
+    /* Uses column sums as in mean convolution, above */
+
+    unsigned int const windowHeight = convKernelP->rows + 1;
+        /* Same as in convolveMean */
+    unsigned int const crowso2 = convKernelP->rows / 2;
+        /* Same as in convolveMean */
+
+    tuple ** rowbuf;
+        /* Same as in convolveGeneral */
+    tuple ** circMap;
+        /* Same as in convolveGeneral */
+    tuple * outputrow;
+        /* Same as in convolveGeneral */
+    unsigned int row;
+        /* Row number of next row to read in from the file */
+    sample ** convColumnSum;  /* Malloc'd */
+        /* Same as in convolveMean() */
 
-   This is essentially pnm_readpnm(), except that it can take sample values
-   that exceed the maxval, which is not legal in PNM.  That's why it's
-   psuedo-PNM and not true PNM.
------------------------------------------------------------------------------*/
+    rowbuf = allocRowbuf(inpamP, windowHeight);
+    MALLOCARRAY_NOFAIL(circMap, windowHeight);
+    outputrow = pnm_allocpamrow(outpamP);
 
-    /* pm_getuint() is supposed to be internal to libnetpbm, but since we're
-       doing this backward compatibility hack here, we use it anyway.
-    */
+    convColumnSum = allocSum(outpamP->depth, outpamP->width);
+
+    pnm_writepaminit(outpamP);
+
+    readAndScaleRows(inpamP, convKernelP->rows, rowbuf,
+                      outpamP->maxval, outpamP->depth);
+
+    writeUnconvolvedTop(outpamP, convKernelP, rowbuf);
+
+    setupCircMap(circMap, rowbuf, windowHeight, 0);
 
-    unsigned int
-    pm_getuint(FILE * const file);
+    /* Convolve the first window the long way */
+    computeInitialColumnSums(inpamP, circMap, convKernelP, convColumnSum);
+
+    convolveRowWithColumnSumsVertical(convKernelP, outpamP, circMap,
+                                      outputrow, convColumnSum);
+
+    pnm_writepamrow(outpamP, outputrow);
+
+    for (row = crowso2 + 1; row < inpamP->height - crowso2; ++row) {
+        unsigned int const windowBotRow = row + crowso2;
+            /* Same as in convolveMean */
+        unsigned int const windowTopRow = row - crowso2 - 1;
+            /* Same as in convolveMean */
+        unsigned int plane;
+
+        readAndScaleRow(inpamP, rowbuf[windowBotRow % windowHeight],
+                        outpamP->maxval, outpamP->depth);
+
+        /* Remember the window is one row higher than the convolution
+           kernel.  The top row in the window is not part of this convolution.
+        */
+
+        setupCircMap(circMap, rowbuf, windowHeight,
+                     windowTopRow % windowHeight);
+
+        for (plane = 0; plane < outpamP->depth; ++plane)
+            convolveVerticalRowPlane(outpamP, circMap, convKernelP, plane,
+                                     outputrow, convColumnSum[plane]);
+
+        pnm_writepamrow(outpamP, outputrow);
+    }
+    writeUnconvolvedBottom(outpamP, convKernelP, windowHeight, circMap);
+
+    freeSum(convColumnSum, outpamP->depth);
+    freeRowbuf(rowbuf, windowHeight);
+}
 
-    FILE * fileP;
-    xel ** xels;
-    int cols, rows;
-    xelval maxval;
-    int format;
+
+
+struct convolveType {
+    convolver * convolve;
+};
+
+
+
+static bool
+convolutionIncludesHorizontal(const struct ConvKernel * const convKernelP) {
+
+    bool horizontal;
     unsigned int row;
 
-    fileP = pm_openr(fileName);
+    for (row = 0, horizontal = TRUE;
+         row < convKernelP->rows && horizontal;
+        ++row) {
+        unsigned int col;
+        for (col = 1, horizontal = TRUE;
+             col < convKernelP->cols && horizontal;
+             ++col) {
+
+            unsigned int plane;
+
+            for (plane = 0; plane < convKernelP->planes; ++plane) {
+                if (convKernelP->weight[plane][row][col] !=
+                    convKernelP->weight[plane][row][0])
+                    horizontal = FALSE;
+            }
+        }
+    }
+    return horizontal;
+}
+
 
-    pnm_readpnminit(fileP, &cols, &rows, &maxval, &format);
 
-    xels = pnm_allocarray(cols, rows);
+static bool
+convolutionIncludesVertical(const struct ConvKernel * const convKernelP) {
 
-    for (row = 0; row < rows; ++row) {
-        if (format == PGM_FORMAT || format == PPM_FORMAT) {
-            /* Plain format -- can't use pnm_readpnmrow() because it will
-               reject a sample > maxval
-            */
-            unsigned int col;
-            for (col = 0; col < cols; ++col) {
-                switch (format) {
-                case PGM_FORMAT: {
-                    gray const g = pm_getuint(fileP);
-                    PNM_ASSIGN1(xels[row][col], g);
-                    } break;
-                case PPM_FORMAT: {
-                    pixval const r = pm_getuint(fileP);
-                    pixval const g = pm_getuint(fileP);
-                    pixval const b = pm_getuint(fileP);
-
-                    PNM_ASSIGN(xels[row][col], r, g, b);
-                } break;
-                default:
-                    assert(false);
-                }
+    bool vertical;
+    unsigned int col;
+
+    for (col = 0, vertical = TRUE;
+         col < convKernelP->cols && vertical;
+        ++col) {
+        unsigned int row;
+        for (row = 1, vertical = TRUE;
+             row < convKernelP->rows && vertical;
+             ++row) {
+
+            unsigned int plane;
+
+            for (plane = 0; plane < convKernelP->planes; ++plane) {
+                if (convKernelP->weight[plane][row][col] !=
+                    convKernelP->weight[plane][0][col])
+                    vertical = FALSE;
             }
-        } else {
-            /* Raw or PBM format -- pnm_readpnmrow() won't do any maxval
-               checking
-            */
-            pnm_readpnmrow(fileP, xels[row], cols, maxval, format);
         }
     }
-    *colsP   = cols;
-    *rowsP   = rows;
-    *maxvalP = maxval;
-    *formatP = format;
-    *xelsP   = xels;
+    return vertical;
+}
+
+
+
+static void
+determineConvolveType(const struct ConvKernel * const convKernelP,
+                      struct convolveType *     const typeP) {
+/*----------------------------------------------------------------------------
+   Determine which form of convolution is best to convolve the kernel
+   *convKernelP over tuples[][].  The general form always works, but with some
+   special case convolution matrices, faster forms of convolution are
+   possible.
+
+   We don't check for the case that the planes can have differing types.  We
+   handle only cases where all planes are of the same special case.
+-----------------------------------------------------------------------------*/
+    bool const horizontal = convolutionIncludesHorizontal(convKernelP);
+    bool const vertical   = convolutionIncludesVertical(convKernelP);
 
-    pm_close(fileP);
+    if (horizontal && vertical) {
+        pm_message("Convolution is a simple mean horizontally and vertically");
+        typeP->convolve = convolveMean;
+    } else if (horizontal) {
+        pm_message("Convolution is a simple mean horizontally");
+        typeP->convolve = convolveHorizontal;
+    } else if (vertical) {
+        pm_message("Convolution is a simple mean vertically");
+        typeP->convolve = convolveVertical;
+    } else {
+        typeP->convolve = convolveGeneral;
+    }
 }
 
 
@@ -1904,65 +2251,50 @@ int
 main(int argc, char * argv[]) {
 
     struct cmdlineInfo cmdline;
-    xel** cxels;
-    int cformat;
-    xelval cmaxval;
+    FILE * ifP;
     struct convolveType convolveType;
-    float ** rweights;
-    float ** gweights;
-    float ** bweights;
+    struct ConvKernel * convKernelP;
+    struct pam inpam;
+    struct pam outpam;
 
     pnm_init(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
 
-    readKernel(cmdline.kernelFilespec,
-               &ccols, &crows, &cmaxval, &cformat, &cxels);
-
-    if (ccols % 2 != 1 || crows % 2 != 1)
-        pm_error("the convolution matrix must have an odd number of "
-                 "rows and columns" );
-
-    ccolso2 = ccols / 2;
-    crowso2 = crows / 2;
-
-    ifp = pm_openr(cmdline.inputFilespec);
-
-    pnm_readpnminit(ifp, &cols, &rows, &maxval, &format);
-    if (cols < ccols || rows < crows)
-        pm_error("the image is smaller than the convolution matrix" );
-
-    newformat = MAX(PNM_FORMAT_TYPE(cformat), PNM_FORMAT_TYPE(format));
-    if (PNM_FORMAT_TYPE(cformat) != newformat)
-        pnm_promoteformat(cxels, ccols, crows, cmaxval, cformat, 
-                          cmaxval, newformat);
-    if (PNM_FORMAT_TYPE(format) != newformat) {
-        switch (PNM_FORMAT_TYPE(newformat)) {
-        case PPM_TYPE:
-            if (PNM_FORMAT_TYPE(format) != newformat)
-                pm_message("promoting to PPM");
-            break;
-        case PGM_TYPE:
-            if (PNM_FORMAT_TYPE(format) != newformat)
-                pm_message("promoting to PGM");
-            break;
-        }
+    ifP = pm_openr(cmdline.inputFileName);
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(allocation_depth));
+
+    getKernel(cmdline, inpam.depth, &convKernelP);
+
+    outpam = inpam;  /* initial value */
+
+    outpam.file = stdout;
+
+    if ((PNM_FORMAT_TYPE(inpam.format) == PBM_TYPE ||
+         PNM_FORMAT_TYPE(inpam.format) == PGM_TYPE) &&
+        convKernelP->planes == 3) {
+
+        pm_message("promoting to PPM");
+        outpam.format = PPM_FORMAT;
     }
 
-    computeWeights(cxels, ccols, crows, newformat, cmaxval, !cmdline.nooffset,
-                   &rweights, &gweights, &bweights);
+    outpam.depth = MAX(inpam.depth, convKernelP->planes);
+
+    pnm_setminallocationdepth(&inpam, MAX(inpam.depth, outpam.depth));
+
+    validateEnoughImageToConvolve(&inpam, convKernelP);
 
     /* Handle certain special cases when runtime can be improved. */
 
-    determineConvolveType(cxels, &convolveType);
+    determineConvolveType(convKernelP, &convolveType);
 
-    convolveIt(format, convolveType, 
-               (const float **)rweights, 
-               (const float **)gweights, 
-               (const float **)bweights);
+    convolveType.convolve(&inpam, &outpam, convKernelP);
 
+    convKernelDestroy(convKernelP);
     pm_close(stdout);
-    pm_close(ifp);
+    pm_close(ifP);
+
     return 0;
 }
 
diff --git a/editor/pnmcrop.c b/editor/pnmcrop.c
index 1abfc7d4..c6aabff1 100644
--- a/editor/pnmcrop.c
+++ b/editor/pnmcrop.c
@@ -106,7 +106,7 @@ parseCommandLine(int argc, const char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    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. */
         
     free(option_def);
@@ -870,8 +870,8 @@ main(int argc, const char *argv[]) {
         */
     FILE * bdfP;
         /* The border file.  NULL if none. */
-    bool eof;    /* no more images in input stream */
-    bool beof;   /* no more images in borderfile stream */
+    int eof;    /* no more images in input stream */
+    int beof;   /* no more images in borderfile stream */
 
     pm_proginit(&argc, argv);
 
diff --git a/editor/pnmflip b/editor/pnmflip
index 44d95b45..07d4ddb9 100755
--- a/editor/pnmflip
+++ b/editor/pnmflip
@@ -1,5 +1,28 @@
-#!/usr/bin/perl -w
+#!/bin/sh
 
+##############################################################################
+# This is essentially a Perl program.  We exec the Perl interpreter specifying
+# this same file as the Perl program and use the -x option to cause the Perl
+# interpreter to skip down to the Perl code.  The reason we do this instead of
+# just making /usr/bin/perl the script interpreter (instead of /bin/sh) is
+# that the user may have multiple Perl interpreters and the one he wants to
+# use is properly located in the PATH.  The user's choice of Perl interpreter
+# may be crucial, such as when the user also has a PERL5LIB environment
+# variable and it selects modules that work with only a certain main
+# interpreter program.
+#
+# An alternative some people use is to have /usr/bin/env as the script
+# interpreter.  We don't do that because we think the existence and
+# compatibility of /bin/sh is more reliable.
+#
+# Note that we aren't concerned about efficiency because the user who needs
+# high efficiency can use directly the programs that this program invokes.
+#
+##############################################################################
+
+exec perl -w -x -S -- "$0" "$@"
+
+#!/usr/bin/perl
 #============================================================================
 #  This is a compatibility interface to Pamflip.
 #
diff --git a/editor/pnmgamma.c b/editor/pnmgamma.c
index b079adf1..b357b0d8 100644
--- a/editor/pnmgamma.c
+++ b/editor/pnmgamma.c
@@ -10,6 +10,7 @@
 ** implied warranty.
 */
 
+#include <assert.h>
 #include <math.h>
 #include <ctype.h>
 
@@ -145,7 +146,7 @@ parseCommandLine(int argc, char ** argv,
                  struct cmdlineInfo * const cmdlineP) {
 
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -190,7 +191,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = TRUE; 
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdline_p and others. */
 
     if (bt709tolinear + lineartobt709 + bt709ramp + srgbramp +
@@ -308,6 +309,7 @@ buildPowGamma(xelval       table[],
         double const normalized = ((double) i) / maxval;
             /* Xel sample value normalized to 0..1 */
         double const v = pow(normalized, oneOverGamma);
+
         table[i] = MIN((xelval)(v * newMaxval + 0.5), newMaxval);  
             /* denormalize, round and clip */
     }
@@ -509,11 +511,15 @@ buildBt709ToSrgbGamma(xelval       table[],
         else
             radiance = pow((normalized + 0.099) / 1.099, gamma709);
 
+        assert(radiance <= 1.0);
+
         if (radiance < linearCutoffSrgb * normalizer)
             srgb = radiance * linearExpansionSrgb;
         else
             srgb = 1.055 * pow(normalized, oneOverGammaSrgb) - 0.055;
 
+        assert(srgb <= 1.0);
+
         table[i] = srgb * newMaxval + 0.5;
     }
 }
@@ -563,11 +569,15 @@ buildSrgbToBt709Gamma(xelval       table[],
         else
             radiance = pow((normalized + 0.099) / 1.099, gammaSrgb);
 
+        assert(radiance <= 1.0);
+
         if (radiance < linearCutoff709 * normalizer)
             bt709 = radiance * linearExpansion709;
         else
             bt709 = 1.055 * pow(normalized, oneOverGamma709) - 0.055;
 
+        assert(bt709 <= 1.0);
+
         table[i] = bt709 * newMaxval + 0.5;
     }
 }
diff --git a/editor/pnmhisteq.c b/editor/pnmhisteq.c
index 1987efc3..8af42019 100644
--- a/editor/pnmhisteq.c
+++ b/editor/pnmhisteq.c
@@ -24,6 +24,8 @@ struct cmdlineInfo {
     */
     const char * inputFileName;
     unsigned int gray;
+    unsigned int noblack;
+    unsigned int nowhite;
     const char * wmap;
     const char * rmap;
     unsigned int verbose;
@@ -39,7 +41,7 @@ parseCommandLine(int argc, char ** argv,
    was passed to us as the argv array.
 -----------------------------------------------------------------------------*/
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -55,6 +57,10 @@ parseCommandLine(int argc, char ** argv,
             &wmapSpec,          0);
     OPTENT3(0, "gray",     OPT_FLAG,   NULL,
             &cmdlineP->gray,    0);
+    OPTENT3(0, "noblack",     OPT_FLAG,   NULL,
+            &cmdlineP->noblack,    0);
+    OPTENT3(0, "nowhite",     OPT_FLAG,   NULL,
+            &cmdlineP->nowhite,    0);
     OPTENT3(0, "verbose",  OPT_FLAG,   NULL,
             &cmdlineP->verbose, 0);
 
@@ -62,7 +68,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We may have parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
 
@@ -91,8 +97,6 @@ computeLuminosityHistogram(xel * const *   const xels,
                            int             const format,
                            bool            const monoOnly,
                            unsigned int ** const lumahistP,
-                           xelval *        const lminP,
-                           xelval *        const lmaxP,
                            unsigned int *  const pixelCountP) {
 /*----------------------------------------------------------------------------
   Scan the image and build the luminosity histogram.  If the input is
@@ -144,7 +148,7 @@ computeLuminosityHistogram(xel * const *   const xels,
             for (col = 0; col < cols; ++col) {
                 xel const thisXel = xels[row][col];
                 if (!monoOnly || PPM_ISGRAY(thisXel)) {
-                    xelval const l = PPM_LUMIN(thisXel);
+                    xelval const l = ppm_luminosity(thisXel);
 
                     lmin = MIN(lmin, l);
                     lmax = MAX(lmax, l);
@@ -162,25 +166,6 @@ computeLuminosityHistogram(xel * const *   const xels,
 
     *lumahistP = lumahist;
     *pixelCountP = pixelCount;
-    *lminP = lmin;
-    *lmaxP = lmax;
-}
-
-
-
-static void
-findMaxLuma(const xelval * const lumahist,
-            xelval         const maxval,
-            xelval *       const maxLumaP) {
-
-    xelval maxluma;
-    unsigned int i;
-
-    for (i = 0, maxluma = 0; i <= maxval; ++i)
-        if (lumahist[i] > 0)
-            maxluma = i;
-
-    *maxLumaP = maxluma;
 }
 
 
@@ -216,53 +201,159 @@ readMapFile(const char * const rmapFileName,
 
 
 
+static xelval
+maxLumaPresent(const xelval * const lumahist,
+               xelval         const darkestCandidate,
+               xelval         const brightestCandidate) {
+/*----------------------------------------------------------------------------
+    The maximum luminosity in the image, in the range ['darkestCandidate',
+   'brightestCandidate'], given that the luminosity histogram for the image is
+   'lumahist' (lumahist[N] is the number of pixels in the image with
+   luminosity N).
+-----------------------------------------------------------------------------*/
+    xelval maxluma;
+    xelval i;
+
+    for (i = darkestCandidate, maxluma = darkestCandidate;
+         i <= brightestCandidate;
+         ++i) {
+
+        if (lumahist[i] > 0)
+            maxluma = i;
+    }
+    return maxluma;
+}
+
+
+
 static void
-computeMap(const unsigned int * const lumahist,
-           xelval               const maxval,
-           unsigned int         const pixelCount,
-           gray *               const lumamap) {
+equalize(const unsigned int * const lumahist,
+         xelval               const darkestRemap,
+         xelval               const brightestRemap,
+         unsigned int         const remapPixelCount,
+         gray *               const lumamap) {
+/*----------------------------------------------------------------------------
+   Fill in the mappings of luminosities from 'darkestRemap' through
+   'brightestRemap' in 'lumamap', to achieve an equalization based on the
+   histogram 'lumahist'.  lumahist[N] is the number of pixels in the original
+   image of luminosity N.
+
+   'remapPixelCount' is the number of pixels in the given luminosity range.
+   It is redundant with 'lumahist'; we get it for computational convenience.
+-----------------------------------------------------------------------------*/
+    xelval const maxluma =
+        maxLumaPresent(lumahist, darkestRemap, brightestRemap);
 
-    /* Calculate initial histogram equalization curve. */
+    unsigned int const range = brightestRemap - darkestRemap;
     
-    unsigned int i;
-    unsigned int pixsum;
-    xelval maxluma;
+    {
+        xelval origLum;
+        unsigned int pixsum;
 
-    for (i = 0, pixsum = 0; i <= maxval; ++i) {
+        for (origLum = darkestRemap, pixsum = 0;
+             origLum <= brightestRemap;
+             ++origLum) {
             
-        /* With 16 bit grays, the following calculation can
-           overflow a 32 bit long.  So, we do it in floating
-           point.
-        */
+            /* With 16 bit grays, the following calculation can overflow a 32
+               bit long.  So, we do it in floating point.
+            */
 
-        lumamap[i] = ROUNDU((((double) pixsum * maxval)) / pixelCount);
+            lumamap[origLum] =
+                darkestRemap +
+                ROUNDU((((double) pixsum * range)) / remapPixelCount);
+            
+            pixsum += lumahist[origLum];
+        }
 
-        pixsum += lumahist[i];
     }
-
-    findMaxLuma(lumahist, maxval, &maxluma);
-
     {
-        double const lscale = (double)maxval /
-            ((lumahist[maxluma] > 0) ?
-             (double) lumamap[maxluma] : (double) maxval);
+        double const lscale = (double)range /
+            ((lumamap[maxluma] > darkestRemap) ?
+             (double) lumamap[maxluma] - darkestRemap : (double) range);
 
-        unsigned int i;
+        xelval origLum;
 
         /* Normalize so that the brightest pixels are set to maxval. */
 
-        for (i = 0; i <= maxval; ++i)
-            lumamap[i] = MIN(maxval, ROUNDU(lumamap[i] * lscale));
+        for (origLum = darkestRemap; origLum <= brightestRemap; ++origLum)
+            lumamap[origLum] =
+                MIN(brightestRemap, 
+                    darkestRemap + ROUNDU(lumamap[origLum] * lscale));
     }
 }
 
 
 
 static void
+computeMap(const unsigned int * const lumahist,
+           xelval               const maxval,
+           unsigned int         const pixelCount,
+           bool                 const noblack,
+           bool                 const nowhite,
+           gray *               const lumamap) {
+/*----------------------------------------------------------------------------
+  Calculate initial histogram equalization curve.
+
+  'lumahist' is the luminosity histogram for the image; lumahist[N] is
+  the number of pixels that have luminosity N.
+
+  'maxval' is the maxval of the image (ergo the maximum luminosity).
+
+  'pixelCount' is the number of pixels in the image, which is redundant
+  with 'lumahist' but provided for computational convenience.
+   
+  'noblack' means don't include the black pixels in the equalization and
+  make the black pixels in the output the same ones as in the input.
+
+  'nowhite' is equivalent for the white pixels.
+
+  We return the map as *lumamap, where lumamap[N] is the luminosity in the
+  output of a pixel with luminosity N in the input.  Its storage size must
+  be at least 'maxval' + 1.
+-----------------------------------------------------------------------------*/
+    xelval darkestRemap, brightestRemap;
+        /* The lowest and highest luminosity values that we will remap
+           according to the equalization strategy.  They're just 0 and maxval
+           unless modified by 'noblack' and 'nowhite'.
+        */
+    unsigned int remapPixelCount;
+        /* The number of pixels we map according to the equalization
+           strategy; it doesn't include black pixels and white pixels that
+           are excluded from the equalization because of 'noblack' and
+           'nowhite'
+        */
+
+    remapPixelCount = pixelCount;  /* Initial assumption */
+
+    if (noblack) {
+        lumamap[0] = 0;
+        darkestRemap = 1;
+        remapPixelCount -= lumahist[0];
+    } else {
+        darkestRemap = 0;
+    }
+
+    if (nowhite) {
+        lumamap[maxval] = maxval;
+        brightestRemap = maxval - 1;
+        remapPixelCount -= lumahist[maxval];
+    } else {
+        brightestRemap = maxval;
+    }
+
+    equalize(lumahist, darkestRemap, brightestRemap, remapPixelCount,
+             lumamap);
+}
+
+
+
+static void
 getMapping(const char *         const rmapFileName,
            const unsigned int * const lumahist,
            xelval               const maxval,
            unsigned int         const pixelCount,
+           bool                 const noblack,
+           bool                 const nowhite,
            gray **              const lumamapP) {
 /*----------------------------------------------------------------------------
   Calculate the luminosity mapping table which gives the
@@ -275,7 +366,7 @@ getMapping(const char *         const rmapFileName,
     if (rmapFileName)
         readMapFile(rmapFileName, maxval, lumamap);
     else
-        computeMap(lumahist, maxval, pixelCount, lumamap);
+        computeMap(lumahist, maxval, pixelCount, noblack, nowhite, lumamap);
 
     *lumamapP = lumamap;
 }
@@ -302,6 +393,48 @@ reportMap(const unsigned int * const lumahist,
 
 
 
+static xel
+scaleXel(xel    const thisXel,
+         double const scaler) {
+/*----------------------------------------------------------------------------
+   Scale the components of 'xel' by multiplying by 'scaler'.
+
+   Assume this doesn't cause it to exceed maxval.
+-----------------------------------------------------------------------------*/
+    xel retval;
+
+    PNM_ASSIGN(retval,
+               ((xelval)(PNM_GETR(thisXel) * scaler + 0.5)),
+               ((xelval)(PNM_GETG(thisXel) * scaler + 0.5)),
+               ((xelval)(PNM_GETB(thisXel) * scaler + 0.5)));
+    
+    return retval;
+}
+
+
+
+static xel
+remapRgbValue(xel          const thisXel,
+              xelval       const maxval,
+              const gray * const lumamap) {
+/*----------------------------------------------------------------------------
+  Return the color 'thisXel' with its HSV value changed per 'lumamap' but
+  the same hue and saturation.
+
+  'maxval' is the maxval for 'xel' and our return value.
+-----------------------------------------------------------------------------*/
+    struct hsv const hsv =
+        ppm_hsv_from_color(thisXel, maxval);
+    xelval const oldValue =
+        MIN(maxval, ROUNDU(hsv.v * maxval));
+    xelval const newValue =
+        lumamap[oldValue];
+    
+    return scaleXel(thisXel, (double)newValue/oldValue);
+}
+
+
+
 static void
 remap(xel **       const xels,
       unsigned int const cols,
@@ -323,16 +456,8 @@ remap(xel **       const xels,
                 if (monoOnly && PPM_ISGRAY(thisXel)) {
                     /* Leave this pixel alone */
                 } else {
-                    struct hsv hsv;
-                    xelval iv;
-
-                    hsv = ppm_hsv_from_color(thisXel, maxval);
-                    iv = MIN(maxval, ROUNDU(hsv.v * maxval));
-                    
-                    hsv.v = MIN(1.0, 
-                                ((double) lumamap[iv]) / ((double) maxval));
-
-                    xels[row][col] = ppm_color_from_hsv(hsv, maxval);
+                    xels[row][col] = remapRgbValue(xels[row][col],
+                                                   maxval, lumamap);
                 }
             }
         }
@@ -376,7 +501,6 @@ main(int argc, char * argv[]) {
 
     struct cmdlineInfo cmdline;
     FILE * ifP;
-    xelval lmin, lmax;
     gray * lumamap;           /* Luminosity map */
     unsigned int * lumahist;  /* Histogram of luminosity values */
     int rows, cols;           /* Rows, columns of input image */
@@ -395,11 +519,12 @@ main(int argc, char * argv[]) {
 
     pm_close(ifP);
 
-    computeLuminosityHistogram(xels, rows, cols, maxval, format,
-                               cmdline.gray, &lumahist, &lmin, &lmax,
-                               &pixelCount);
+    computeLuminosityHistogram(xels, rows, cols, maxval, format, cmdline.gray,
+                               &lumahist, &pixelCount);
 
-    getMapping(cmdline.rmap, lumahist, maxval, pixelCount, &lumamap);
+    getMapping(cmdline.rmap, lumahist, maxval, pixelCount,
+               cmdline.noblack > 0, cmdline.nowhite > 0,
+               &lumamap);
 
     if (cmdline.verbose)
         reportMap(lumahist, maxval, lumamap);
diff --git a/editor/pnminvert.c b/editor/pnminvert.c
index 40fee9be..4bee8837 100644
--- a/editor/pnminvert.c
+++ b/editor/pnminvert.c
@@ -12,6 +12,17 @@
 
 #include "pnm.h"
 
+/* Implementation note: A suitably advanced compiler, such as Gcc 4,
+   implements the for statements in our algorithm with instructions that do 16
+   bytes at a time on CPUs that have them (movdqa on x86).  This is "tree
+   vectorization."  A more primitive compiler will do one byte at a time; we
+   could change the code to use libnetpbm's wordaccess.h facility and it will
+   do one word at a time.  (But we don't think it's worth complicating the
+   code for that).
+*/
+
+
+
 #define CHARBITS (sizeof(unsigned char)*8)
 
 
@@ -25,9 +36,6 @@ invertPbm(FILE * const ifP,
 /*----------------------------------------------------------------------------
    Invert a PBM image.  Use the "packed" PBM functions for speed.
 -----------------------------------------------------------------------------*/
-    /* We could make this faster by inverting whole words at a time,
-       using libnetpbm's wordaccess.h facility.
-    */
     int const colChars = pbm_packed_bytes(cols);
 
     unsigned char * bitrow; 
@@ -42,11 +50,8 @@ invertPbm(FILE * const ifP,
         for (colChar = 0; colChar < colChars; ++colChar)
             bitrow[colChar] = ~ bitrow[colChar];
         
-        /* Clean off remainder of fractional last character */
-        if (cols % CHARBITS > 0) {
-            bitrow[colChars-1] >>= CHARBITS - cols % CHARBITS;
-            bitrow[colChars-1] <<= CHARBITS - cols % CHARBITS;
-        }
+        /* Clean off remainder of fractional last character and write */
+        pbm_cleanrowend_packed(bitrow, cols);
         pbm_writepbmrow_packed(ofP, bitrow, cols, 0);
     }
     pbm_freerow_packed(bitrow);
diff --git a/editor/pnminvert.test b/editor/pnminvert.test
deleted file mode 100644
index 5534f20d..00000000
--- a/editor/pnminvert.test
+++ /dev/null
@@ -1,15 +0,0 @@
-echo Test 1.  Should print 1240379484 41
-./pnminvert ../testgrid.pbm | cksum
-echo Test 2.  Should print 1416115901 101484
-./pnminvert ../testimg.ppm | cksum
-echo Test 3.  Should print 2961441369 33838
-ppmtopgm ../testimg.ppm | ./pnminvert | cksum
-echo Test 4.  Should print 2595564405 14
-pbmmake -w 7 7 | ./pnminvert | cksum
-echo Test 5.  Should print 2595564405 14
-pbmmake -b 7 7 | cksum
-echo Test 6.  Should print 2595564405 14
-pbmmake -b 7 7 | ./pnminvert | ./pnminvert | cksum
-echo Test 7.  Should print 2896726098 15
-pbmmake -g 8 8 | ./pnminvert | cksum
-
diff --git a/editor/pnmmargin b/editor/pnmmargin
index b31deefd..0f57d1d4 100755
--- a/editor/pnmmargin
+++ b/editor/pnmmargin
@@ -31,30 +31,30 @@ while true ; do
         plainopt="-plain"
         shift
         ;;
-	-w|-wh|-whi|-whit|-white )
-	color="-white"
-	shift
-	;;
-	-b|-bl|-bla|-blac|-black )
-	color="-black"
-	shift
-	;;
-	-c|-co|-col|-colo|-color )
-	shift
-	if [ ! ${1-""} ] ; then
-       	    echo "usage: $0 [-white|-black|-color <colorspec>] <size> [pnmfile]" 1>&2
-	    exit 1
-	fi
-	color="$1"
-	shift
-	;;
-	-* )
-	echo "usage: $0 [-white|-black|-color <colorspec>] <size> [pnmfile]" 1>&2
-	exit 1
-	;;
-	* )
-	break
-	;;
+        -w|-wh|-whi|-whit|-white )
+        color="-white"
+        shift
+        ;;
+        -b|-bl|-bla|-blac|-black )
+        color="-black"
+        shift
+        ;;
+        -c|-co|-col|-colo|-color )
+        shift
+        if [ ! ${1-""} ] ; then
+            echo "usage: $0 [-white|-black|-color <colorspec>] <size> [pnmfile]" 1>&2
+            exit 1
+        fi
+        color="$1"
+        shift
+        ;;
+        -* )
+        echo "usage: $0 [-white|-black|-color <colorspec>] <size> [pnmfile]" 1>&2
+        exit 1
+        ;;
+        * )
+        break
+        ;;
     esac
 done
 
@@ -82,7 +82,7 @@ if [ $size -eq 0 ] ; then
 else
     # If -white or -black, write output with pnmpad and exit.
     # Otherwise construct spacer files.
-    
+
     case "$color" in
         -gofigure )
         pnmcut 0 0 1 1 $tmp1 | pnmtile $size 1 > $tmp2
@@ -97,12 +97,10 @@ else
         ;;
     esac
     pamflip -rotate90 $tmp2 > $tmp3
-    
+
     # Cat things together.
     pnmcat -lr $tmp2 $tmp1 $tmp2 > $tmp4
     pnmcat -tb $plainopt $tmp3 $tmp4 $tmp3
 fi
 
 
-
-
diff --git a/editor/pnmmontage.c b/editor/pnmmontage.c
index 2e30a43b..e54afc45 100644
--- a/editor/pnmmontage.c
+++ b/editor/pnmmontage.c
@@ -10,6 +10,7 @@
  * implied warranty.
  */
 
+#define _XOPEN_SOURCE 500  /* Make sure strdup() is in string.h */
 #define _BSD_SOURCE  /* Make sure strdup() is in <string.h> */
 #include <assert.h>
 #include <limits.h>
@@ -23,7 +24,7 @@
 
 
 
-struct cmdlineInfo {
+struct CmdlineInfo {
     const char * header;
     const char * data;
     const char * prefix;
@@ -37,7 +38,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.  
@@ -78,7 +79,9 @@ parseCommandLine(int argc, const char ** argv,
     opt.short_allowed = FALSE;
     opt.allowNegNum = FALSE;
 
-    optParseOptions3(&argc, (char**)argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, (char**)argv, opt, sizeof(opt), 0);
+
+    free(option_def);
 
     if (!dataSpec)
         cmdlineP->data = NULL;
@@ -116,24 +119,27 @@ parseCommandLine(int argc, const char ** argv,
 
 typedef struct {
     int f[sizeof(int) * 8 + 1];
-} factorset;
+} Factorset;
 
 typedef struct {
-    int x; int y;
-} coord;
+    int x;
+    int y;
+} Coord;
 
 typedef struct {
-    coord ul;
-    coord size;
-} rectangle;
+    Coord ul;
+    Coord size;
+} Rectangle;
+
 
-static coord
-lr(rectangle const r) {
+
+static Coord
+lr(Rectangle const r) {
 /*----------------------------------------------------------------------------
-   Return the coordinates of the lower right corner of 'r'
-   (i.e. the pixel just beyond the lowest rightmost one).
+  The coordinates of the lower right corner of 'r' (i.e. the pixel just beyond
+  the lowest rightmost one).
 -----------------------------------------------------------------------------*/
-    coord retval;
+    Coord retval;
 
     retval.x = r.ul.x + r.size.x;
     retval.y = r.ul.y + r.size.y;
@@ -141,50 +147,67 @@ lr(rectangle const r) {
     return retval;
 }
 
-static factorset 
-factor(int n)
-{
-  int i, j;
-  factorset f;
-  for (i = 0; i < sizeof(int) * 8 + 1; ++i)
-    f.f[i] = 0;
-  for (i = 2, j = 0; n > 1; ++i)
-  {
-    if (n % i == 0)
-      f.f[j++] = i, n /= i, --i;
-  }
-  return (f);
+
+
+static Factorset 
+factor(unsigned int const arg) {
+/*----------------------------------------------------------------------------
+   The prime factors of 'arg'.
+-----------------------------------------------------------------------------*/
+    unsigned int n;
+    unsigned int i, j;
+    Factorset retval;
+
+    /* Initialize array element to zero */
+    for (i = 0; i < ARRAY_SIZE(retval.f); ++i)
+        retval.f[i] = 0;
+
+    /* Set array elements starting with the first to the factors */
+
+    for (i = 2, j = 0, n = arg; n > 1; ) {
+        if (n % i == 0) {
+            retval.f[j++] = i;
+            n /= i;
+        } else
+            ++i;
+    }
+    return retval;
 }
 
+
+
 static int 
-gcd(int n, int m)
-{
-  factorset nf, mf;
-  int i, j;
-  int g;
-
-  nf = factor(n);
-  mf = factor(m);
-
-  i = j = 0;
-  g = 1;
-  while (nf.f[i] && mf.f[j])
-  {
-    if (nf.f[i] == mf.f[j])
-      g *= nf.f[i], ++i, ++j;
-    else if (nf.f[i] < mf.f[j])
-      ++i;
-    else
-      ++j;
-  }
-  return (g);
+gcf(unsigned int const n,
+    unsigned int const m) {
+/*----------------------------------------------------------------------------
+   Greatest common factor of 'n' and 'm'
+-----------------------------------------------------------------------------*/
+    Factorset const nFactors = factor(n);
+    Factorset const mFactors = factor(m);
+
+    unsigned int i, j;
+    unsigned int g;
+
+    i = j = 0;
+    g = 1;
+    while (nFactors.f[i] && mFactors.f[j]) {
+        if (nFactors.f[i] == mFactors.f[j]) {
+            g *= nFactors.f[i];
+            ++i;
+            ++j;
+        } else if (nFactors.f[i] < mFactors.f[j])
+            ++i;
+        else
+            ++j;
+    }
+    return g;
 }
 
 
 
 static bool
-overlaps(rectangle const a,
-         rectangle const b) {
+overlaps(Rectangle const a,
+         Rectangle const b) {
 
     return
         (a.ul.x < lr(b).x && a.ul.y < lr(b).y) &&
@@ -194,8 +217,8 @@ overlaps(rectangle const a,
 
 
 static bool
-collides(rectangle         const test,
-         const rectangle * const fieldList,
+collides(Rectangle         const test,
+         const Rectangle * const fieldList,
          unsigned int      const n) {
 /*----------------------------------------------------------------------------
    Return true iff the rectangle 'test' overlaps any of the 'n' rectangles
@@ -203,19 +226,19 @@ collides(rectangle         const test,
 -----------------------------------------------------------------------------*/
     unsigned int i;
 
-    for (i = 0; i < n; ++i)
+    for (i = 0; i < n; ++i) {
         if (overlaps(fieldList[i], test))
             return true;
-
+    }
     return false;
 }
 
 
 
 static void 
-recursefindpack(rectangle *    const current,
-                coord          const currentsz,
-                coord *        const best,
+recursefindpack(Rectangle *    const current,
+                Coord          const currentsz,
+                Coord *        const best,
                 unsigned int   const minarea,
                 unsigned int * const maxareaP, 
                 unsigned int   const depth,
@@ -235,13 +258,13 @@ recursefindpack(rectangle *    const current,
     } else {
         unsigned int i;
 
-        rectangle * const newP = &current[depth];
+        Rectangle * const newP = &current[depth];
 
         for (i = 0; ; ++i) {
             for (newP->ul.x = 0, newP->ul.y = i * yinc;
                  newP->ul.y <= i * yinc;) {
 
-                coord c;
+                Coord c;
 
                 c.x = MAX(lr(*newP).x, currentsz.x);
                 c.y = MAX(lr(*newP).y, currentsz.y);
@@ -273,36 +296,35 @@ recursefindpack(rectangle *    const current,
 
 static void 
 findpack(struct pam * const imgs,
-         unsigned int const n,
-         coord *      const coords,
+         unsigned int const imgCt,
+         Coord **     const coordsP,
          unsigned int const quality,
          unsigned int const qfactor) {
 
-    int minarea;
-    int i;
-    int rdiv;
-    int cdiv;
-    int minx;
-    int miny;
-    rectangle * current;
+    Coord * coords;  /* malloc'ed array */
+    unsigned int minarea;
+    unsigned int i;
+    unsigned int rdiv;
+    unsigned int cdiv;
+    Rectangle * current;  /* malloc'ed array */
     unsigned int z;
-    coord c;
+    Coord c;
+
+    MALLOCARRAY(coords, imgCt);
+  
+    if (!coords)
+        pm_error("Out of memory allocating %u-element coords array", imgCt);
 
-    minx = -1; miny = -1;  /* initial value */
     z = UINT_MAX;  /* initial value */
     c.x = 0; c.y = 0;  /* initial value */
 
     if (quality > 1) {
         unsigned int realMinarea;
-        for (realMinarea = i = 0; i < n; ++i)
-            realMinarea += imgs[i].height * imgs[i].width,
-                minx = MAX(minx, imgs[i].width),
-                miny = MAX(miny, imgs[i].height);
-
+        for (realMinarea = i = 0; i < imgCt; ++i)
+            realMinarea += imgs[i].height * imgs[i].width;
         minarea = realMinarea * qfactor / 100;
-    } else {
-        minarea = INT_MAX - 1;
-    }
+    } else
+        minarea = UINT_MAX - 1;
 
     /* It's relatively easy to show that, if all the images
      * are multiples of a particular size, then a best
@@ -311,20 +333,24 @@ findpack(struct pam * const imgs,
      *
      * This speeds computation immensely.
      */
-    for (rdiv = imgs[0].height, i = 1; i < n; ++i)
-        rdiv = gcd(imgs[i].height, rdiv);
+    for (rdiv = imgs[0].height, i = 1; i < imgCt; ++i)
+        rdiv = gcf(imgs[i].height, rdiv);
 
-    for (cdiv = imgs[0].width, i = 1; i < n; ++i)
-        cdiv = gcd(imgs[i].width, cdiv);
+    for (cdiv = imgs[0].width, i = 1; i < imgCt; ++i)
+        cdiv = gcf(imgs[i].width, cdiv);
 
-    MALLOCARRAY(current, n);
+    MALLOCARRAY(current, imgCt);
 
-    for (i = 0; i < n; ++i) {
+    for (i = 0; i < imgCt; ++i) {
         current[i].size.x = imgs[i].width;
         current[i].size.y = imgs[i].height;
     }
-    recursefindpack(current, c, coords, minarea, &z, 0, n, cdiv, rdiv,
+    recursefindpack(current, c, coords, minarea, &z, 0, imgCt, cdiv, rdiv,
                     quality, qfactor);
+
+    free(current);
+
+    *coordsP = coords;
 }
 
 
@@ -333,8 +359,8 @@ static void
 adjustDepth(tuple *            const tuplerow,
             const struct pam * const inpamP,
             const struct pam * const outpamP,
-            coord              const coord) {
-
+            Coord              const coord) {
+    
     if (inpamP->depth < outpamP->depth) {
         unsigned int i;
         for (i = coord.x; i < coord.x + inpamP->width; ++i) {
@@ -351,12 +377,12 @@ static void
 adjustMaxval(tuple *            const tuplerow,
              const struct pam * const inpamP,
              const struct pam * const outpamP,
-             coord              const coord) {
+             Coord              const coord) {
 
     if (inpamP->maxval < outpamP->maxval) {
-        int i;
+        unsigned int i;
         for (i = coord.x; i < coord.x + inpamP->width; ++i) {
-            int j;
+            unsigned int j;
             for (j = 0; j < outpamP->depth; ++j)
                 tuplerow[i][j] *= outpamP->maxval / inpamP->maxval;
         }
@@ -382,29 +408,37 @@ makeRowBlack(struct pam * const pamP,
 
 static void
 writePam(struct pam *       const outpamP,
-         unsigned int       const nfiles,
-         const coord *      const coords,
+         unsigned int       const imgCt,
+         const Coord *      const coords,
          const struct pam * const imgs) {
-
-    tuple *tuplerow;
-    int i;
+/*----------------------------------------------------------------------------
+   Write the entire composite image.  There are 'imgCt' source images,
+   described by imgs[].  Their placement in the output is coords[].
+   Properties of the output image, including where to write it
+   and its dimensions are *outpamP.
+-----------------------------------------------------------------------------*/
+    tuple * tuplerow;
+    unsigned int row;  /* Row number in the output image */
   
     pnm_writepaminit(outpamP);
 
     tuplerow = pnm_allocpamrow(outpamP);
 
-    for (i = 0; i < outpamP->height; ++i) {
-        int j;
-
+    for (row = 0; row < outpamP->height; ++row) {
+        unsigned int imgIdx;
+        
         makeRowBlack(outpamP, tuplerow);  /* initial value */
 
-        for (j = 0; j < nfiles; ++j) {
-            if (coords[j].y <= i && i < coords[j].y + imgs[j].height) {
-                pnm_readpamrow(&imgs[j], &tuplerow[coords[j].x]);
-                adjustDepth(tuplerow, &imgs[j], outpamP, coords[j]);
+        for (imgIdx = 0; imgIdx < imgCt; ++imgIdx) {
+            const Coord *      const imgCoordP = &coords[imgIdx];
+            const struct pam * const imgPamP   = &imgs[imgIdx];
 
-                adjustMaxval(tuplerow, &imgs[j], outpamP, coords[j]);
+            if (imgCoordP->y <= row && row < imgCoordP->y + imgPamP->height) {
+                pnm_readpamrow(imgPamP, &tuplerow[imgCoordP->x]);
 
+                adjustDepth(tuplerow, imgPamP, outpamP, *imgCoordP);
+
+                adjustMaxval(tuplerow, imgPamP, outpamP, *imgCoordP);
             }
         }
         pnm_writepamrow(outpamP, tuplerow);
@@ -418,18 +452,18 @@ static void
 writeData(FILE *             const dataFileP,
           unsigned int       const width,
           unsigned int       const height,
-          unsigned int       const nfiles,
+          unsigned int       const imgCt,
           const char **      const names,
-          const coord *      const coords,
+          const Coord *      const coords,
           const struct pam * const imgs) {
 
-    unsigned int i;
+    unsigned int imgIdx;
 
     fprintf(dataFileP, ":0:0:%u:%u\n", width, height);
 
-    for (i = 0; i < nfiles; ++i) {
-        fprintf(dataFileP, "%s:%u:%u:%u:%u\n", names[i], coords[i].x,
-                coords[i].y, imgs[i].width, imgs[i].height);
+    for (imgIdx = 0; imgIdx < imgCt; ++imgIdx) {
+        fprintf(dataFileP, "%s:%u:%u:%u:%u\n", names[imgIdx], coords[imgIdx].x,
+                coords[imgIdx].y, imgs[imgIdx].width, imgs[imgIdx].height);
     }
 }
 
@@ -442,7 +476,7 @@ writeHeader(FILE * const headerFileP,
             unsigned int const height,
             unsigned int const nfiles,
             const char ** const names,
-            const coord * const coords,
+            const Coord * const coords,
             const struct pam * imgs) {
 
     unsigned int i;
@@ -455,7 +489,7 @@ writeHeader(FILE * const headerFileP,
 
     for (i = 0; i < nfiles; ++i) {
         char * const buffer = strdup(names[i]);
-        coord const coord = coords[i];
+        Coord const coord = coords[i];
         struct pam const img = imgs[i];
 
         unsigned int j;
@@ -554,11 +588,11 @@ computeOutputType(sample *           const maxvalP,
 
 
 static void
-computeOutputDimensions(int * const widthP,
-                        int * const heightP,
-                        unsigned int const nfiles,
+computeOutputDimensions(int *              const widthP,
+                        int *              const heightP,
+                        unsigned int       const nfiles,
                         const struct pam * const imgs,
-                        const coord * const coords) {
+                        const Coord *      const coords) {
 
     unsigned int widthGuess, heightGuess;
     unsigned int i;
@@ -577,100 +611,162 @@ computeOutputDimensions(int * const widthP,
 
 
 
-int 
-main(int argc, const char **argv) {
+static unsigned int
+qfactorFromQuality(unsigned int const quality,
+                   unsigned int const quality2) {
 
-    struct cmdlineInfo cmdline;
-    struct pam * imgs;
-    struct pam outimg;
-    unsigned int nfiles;
-    coord * coords;
-    FILE * header;
-    FILE * data;
-    const char ** names;
-    unsigned int i;
-    unsigned int qfactor;  /* In per cent */
+    unsigned int qfactor;
 
-    pm_proginit(&argc, argv);
-
-    parseCommandLine(argc, argv, &cmdline);
-
-    header = cmdline.header ? pm_openw(cmdline.header) : NULL;
-    data = cmdline.data ? pm_openw(cmdline.data) : NULL;
-
-    switch (cmdline.quality2) {
+    switch (quality2) {
     case 0: case 1:
-        qfactor = cmdline.quality;
+        qfactor = quality;
         break;
     case 2: case 3: case 4: case 5: case 6: 
-        qfactor = 100 * (8 - cmdline.quality2); 
+        qfactor = 100 * (8 - quality2); 
         break;
     case 7: qfactor = 150; break;
     case 8: qfactor = 125; break;
     case 9: qfactor = 100; break;
     default: pm_error("Internal error - impossible value of 'quality2': %u",
-                      cmdline.quality2);
+                      quality2);
     }
 
-    nfiles = cmdline.nFiles > 0 ? cmdline.nFiles : 1;
+    return qfactor;
+}
 
-    MALLOCARRAY(imgs, nfiles);
-    MALLOCARRAY(coords, nfiles);
-    MALLOCARRAY(names, nfiles);
-  
-    if (!imgs || !coords || !names)
+
+
+static void
+openFiles(struct CmdlineInfo const cmdline,
+          unsigned int *     const fileCtP,
+          struct pam **      const imgPamP,
+          const char ***     const namesP) {
+
+    unsigned int fileCt;
+    struct pam * imgPam;
+    const char ** names;
+
+    fileCt = cmdline.nFiles > 0 ? cmdline.nFiles : 1;
+
+    MALLOCARRAY(imgPam, fileCt);
+    MALLOCARRAY(names, fileCt);
+
+    if (!imgPam || !names)
         pm_error("out of memory");
 
     if (cmdline.nFiles > 0) {
         unsigned int i;
 
         for (i = 0; i < cmdline.nFiles; ++i) {
-            imgs[i].file = pm_openr(cmdline.inFileName[i]);
+            imgPam[i].file = pm_openr(cmdline.inFileName[i]);
             names[i] = strdup(cmdline.inFileName[i]);
         }
     } else {
-        imgs[0].file = stdin;
+        imgPam[0].file = stdin;
         names[0] = strdup("stdin");
     }
 
-    for (i = 0; i < nfiles; ++i)
-        pnm_readpaminit(imgs[i].file, &imgs[i], PAM_STRUCT_SIZE(tuple_type));
+    *fileCtP = fileCt;
+    *imgPamP = imgPam;
+    *namesP  = names;
+}
 
-    sortImagesByArea(nfiles, imgs, names);
 
-    findpack(imgs, nfiles, coords, cmdline.quality2, qfactor);
 
-    computeOutputType(&outimg.maxval, &outimg.format, outimg.tuple_type,
-                      &outimg.depth, nfiles, imgs);
+static void
+readFileHeaders(struct pam * const imgPam,
+                unsigned int const fileCt) {
 
-    computeOutputDimensions(&outimg.width, &outimg.height, nfiles,
-                            imgs, coords);
+    unsigned int i;
 
-    pnm_setminallocationdepth(&outimg, outimg.depth);
+    for (i = 0; i < fileCt; ++i)
+        pnm_readpaminit(imgPam[i].file, &imgPam[i],
+                        PAM_STRUCT_SIZE(tuple_type));
+}
+
+
+
+static void
+closeFiles(const struct pam * const imgPam,
+           unsigned int       const fileCt,
+           FILE *             const headerFileP,
+           FILE *             const dataFileP) {
+
+    unsigned int i;
+
+    for (i = 0; i < fileCt; ++i)
+        pm_close(imgPam[i].file);
+
+    pm_close(stdout);
+
+    if (headerFileP)
+        pm_close(headerFileP);
+
+    if (dataFileP)
+        pm_close(dataFileP);
+}
 
+
+
+int 
+main(int argc, const char **argv) {
+
+    struct CmdlineInfo cmdline;
+    struct pam * imgPam;  /* malloced */
+    struct pam outimg;
+    unsigned int fileCt;
+    Coord * coords;  /* malloced */
+    FILE * headerFileP;
+    FILE * dataFileP;
+    const char ** names; /* malloced */
+    unsigned int qfactor;  /* In per cent */
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    headerFileP = cmdline.header ? pm_openw(cmdline.header) : NULL;
+    dataFileP = cmdline.data ? pm_openw(cmdline.data) : NULL;
+
+    qfactor = qfactorFromQuality(cmdline.quality, cmdline.quality2);
+
+    openFiles(cmdline, &fileCt, &imgPam, &names);
+
+    readFileHeaders(imgPam, fileCt);
+
+    sortImagesByArea(fileCt, imgPam, names);
+
+    findpack(imgPam, fileCt, &coords, cmdline.quality2, qfactor);
+
+    computeOutputType(&outimg.maxval, &outimg.format, outimg.tuple_type,
+                      &outimg.depth, fileCt, imgPam);
+
+    computeOutputDimensions(&outimg.width, &outimg.height, fileCt,
+                            imgPam, coords);
     outimg.size = sizeof(outimg);
     outimg.len = PAM_STRUCT_SIZE(allocation_depth);
     pnm_setminallocationdepth(&outimg, outimg.depth);
     outimg.plainformat = false;
     outimg.file = stdout;
  
-    writePam(&outimg, nfiles, coords, imgs);
+    writePam(&outimg, fileCt, coords, imgPam);
 
-    if (data)
-        writeData(data, outimg.width, outimg.height,
-                  nfiles, names, coords, imgs);
+    if (dataFileP)
+        writeData(dataFileP, outimg.width, outimg.height,
+                  fileCt, names, coords, imgPam);
 
-    if (header)
-        writeHeader(header, cmdline.prefix, outimg.width, outimg.height,
-                    nfiles, names, coords, imgs);
+    if (headerFileP)
+        writeHeader(headerFileP, cmdline.prefix, outimg.width, outimg.height,
+                    fileCt, names, coords, imgPam);
 
-    for (i = 0; i < nfiles; ++i)
-        pm_close(imgs[i].file);
-    pm_close(stdout);
-    if (header)
-        pm_close(header);
-    if (data)
-        pm_close(data);
+    closeFiles(imgPam, fileCt, headerFileP, dataFileP);
+
+    free(coords);
+    free(imgPam);
+    free(names);
 
     return 0;
 }
+
+
+
diff --git a/editor/pnmnlfilt.c b/editor/pnmnlfilt.c
index bde0cd82..f55a67bd 100644
--- a/editor/pnmnlfilt.c
+++ b/editor/pnmnlfilt.c
@@ -990,7 +990,7 @@ main(int argc, char *argv[]) {
 
     FILE * ifP;
     struct cmdlineInfo cmdline;
-	bool eof;  /* We've hit the end of the input stream */
+	int eof;  /* We've hit the end of the input stream */
     unsigned int imageSeq;  /* Sequence number of image, starting from 0 */
 
     pnm_init(&argc, argv);
diff --git a/editor/pnmnorm.c b/editor/pnmnorm.c
index 27d51115..131b39d0 100644
--- a/editor/pnmnorm.c
+++ b/editor/pnmnorm.c
@@ -31,7 +31,9 @@
 
 #include "pm_c_util.h"
 #include "mallocvar.h"
+#include "nstring.h"
 #include "shhopt.h"
+#include "matrix.h"
 #include "pnm.h"
 
 enum brightMethod {BRIGHT_LUMINOSITY, BRIGHT_COLORVALUE, BRIGHT_SATURATION};
@@ -40,7 +42,7 @@ struct cmdlineInfo {
     /* All the information the user supplied in the command line,
        in a form easy for the program to use.
     */
-    const char *inputFilespec;  /* Filespec of input file */
+    const char * inputFileName;  /* Name of input file */
     unsigned int bvalueSpec;
     xelval bvalue;
     unsigned int bpercentSpec;
@@ -49,6 +51,11 @@ struct cmdlineInfo {
     xelval wvalue;
     unsigned int wpercentSpec;
     float wpercent;
+    unsigned int bsingle;
+    unsigned int wsingle;
+    float middle;
+    unsigned int midvalueSpec;
+    xelval midvalue;
     enum brightMethod brightMethod;
     unsigned int keephues;
     float maxExpansion;
@@ -56,6 +63,7 @@ struct cmdlineInfo {
            by per centile.  This is a factor, not a per cent increase.
            E.g. 50% increase means a factor of 1.50.
         */
+    unsigned int verbose;
 };
 
 
@@ -74,12 +82,12 @@ parseCommandLine(int argc, const char ** argv,
    was passed to us as the argv array.  We also trash *argv.
 -----------------------------------------------------------------------------*/
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
     unsigned int luminosity, colorvalue, saturation;
-    unsigned int maxexpandSpec;
+    unsigned int middleSpec, maxexpandSpec;
     float maxexpand;
     
     unsigned int option_def_index;
@@ -95,6 +103,14 @@ parseCommandLine(int argc, const char ** argv,
             &cmdlineP->bvalue,     &cmdlineP->bvalueSpec, 0);
     OPTENT3(0,   "wvalue",        OPT_UINT,   
             &cmdlineP->wvalue,     &cmdlineP->wvalueSpec, 0);
+    OPTENT3(0,   "bsingle",       OPT_FLAG,   
+            NULL,                 &cmdlineP->bsingle, 0);
+    OPTENT3(0,   "wsingle",       OPT_FLAG,   
+            NULL,                 &cmdlineP->wsingle, 0);
+    OPTENT3(0,   "middle",        OPT_FLOAT,   
+            &cmdlineP->middle,     &middleSpec, 0);
+    OPTENT3(0,   "midvalue",      OPT_UINT,   
+            &cmdlineP->midvalue,   &cmdlineP->midvalueSpec, 0);
     OPTENT3(0,   "maxexpand",     OPT_FLOAT,   
             &maxexpand,            &maxexpandSpec, 0);
     OPTENT3(0,   "keephues",      OPT_FLAG,   
@@ -107,6 +123,8 @@ parseCommandLine(int argc, const char ** argv,
             NULL,                  &saturation, 0);
     OPTENT3(0,   "brightmax",     OPT_FLAG,   
             NULL,                  &colorvalue, 0);
+    OPTENT3(0,   "verbose",       OPT_FLAG,   
+            NULL,                  &cmdlineP->verbose, 0);
 
     /* Note: -brightmax was documented and accepted long before it was
        actually implemented.  By the time we implemented it, we
@@ -117,7 +135,7 @@ parseCommandLine(int argc, const char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    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 *cmdline_p and others. */
 
     if (!cmdlineP->wpercentSpec)
@@ -138,6 +156,20 @@ parseCommandLine(int argc, const char ** argv,
         pm_error("You specified a per centage > 100 for bpercent: %f",
                  cmdlineP->bpercent);
 
+    if (cmdlineP->bsingle && (cmdlineP->bpercentSpec || cmdlineP->bvalueSpec))
+        pm_error("You cannot specify both -bsingle and -bpercent or -bvalue");
+
+    if (cmdlineP->wsingle && (cmdlineP->wpercentSpec || cmdlineP->wvalueSpec))
+        pm_error("You cannot specify both -wsingle and -wpercent or -wvalue");
+
+    if (middleSpec) {
+        if (cmdlineP->middle < 0.0 || cmdlineP->middle > 1.0)
+            pm_error("-middle is a normalized brightness value; it "
+                     "must be in the range 0..1.  You specified %f",
+                     cmdlineP->middle);
+    } else
+        cmdlineP->middle = 0.5;
+
     if (luminosity + colorvalue + saturation > 1)
         pm_error("You can specify only one of "
                  "-luminosity, -colorvalue, and -saturation");
@@ -163,9 +195,9 @@ parseCommandLine(int argc, const char ** argv,
                  "specification.  "
                  "You specified %d arguments.", argc-1);
     if (argc-1 < 1)
-        cmdlineP->inputFilespec = "-";
+        cmdlineP->inputFileName = "-";
     else
-        cmdlineP->inputFilespec = argv[1];
+        cmdlineP->inputFileName = argv[1];
 }
 
 
@@ -211,7 +243,7 @@ buildHistogram(FILE *   const ifp,
             if (PNM_FORMAT_TYPE(format) == PPM_TYPE) {
                 switch(brightMethod) {
                 case BRIGHT_LUMINOSITY:
-                    brightness = PPM_LUMIN(p);
+                    brightness = ppm_luminosity(p);
                     break;
                 case BRIGHT_COLORVALUE:
                     brightness = ppm_colorvalue(p);
@@ -230,6 +262,50 @@ buildHistogram(FILE *   const ifp,
 
 
 
+static xelval
+minimumValue(const unsigned int * const hist,
+             unsigned int         const highest) {
+
+    xelval i;
+    bool foundOne;
+
+    for (i = 0, foundOne = false; !foundOne; ) {
+        if (hist[i] > 0)
+            foundOne = true;
+        else {
+            if (i == highest)
+                pm_error("INTERNAL ERROR in '%s'.  No pixels", __FUNCTION__);
+            else
+                ++i;
+        }
+    }
+    return i;
+}
+
+
+
+static xelval
+maximumValue(const unsigned int * const hist,
+             unsigned int         const highest) {
+
+    xelval i;
+    bool foundOne;
+
+    for (i = highest, foundOne = false; !foundOne; ) {
+        if (hist[i] > 0)
+            foundOne = true;
+        else {
+            if (i == 0)
+                pm_error("INTERNAL ERROR in '%s'.  No pixels", __FUNCTION__);
+            else
+                --i;
+        }
+    }
+    return i;
+}
+
+
+
 static void
 computeBottomPercentile(unsigned int         hist[], 
                         unsigned int   const highest,
@@ -239,7 +315,7 @@ computeBottomPercentile(unsigned int         hist[],
 /*----------------------------------------------------------------------------
    Compute the lowest index of hist[] such that the sum of the hist[]
    values with that index and lower represent at least 'percent' per cent of
-   'n' (which is assumed to be the sum of all the values in hist[],
+   'total' (which is assumed to be the sum of all the values in hist[],
    given to us to save us the time of computing it).
 -----------------------------------------------------------------------------*/
     unsigned int cutoff = total * percent / 100.0;
@@ -271,7 +347,7 @@ computeTopPercentile(unsigned int         hist[],
 /*----------------------------------------------------------------------------
    Compute the highest index of hist[] such that the sum of the hist[]
    values with that index and higher represent 'percent' per cent of
-   'n' (which is assumed to be the sum of all the values in hist[],
+   'total' (which is assumed to be the sum of all the values in hist[],
    given to us to save us the time of computing it).
 -----------------------------------------------------------------------------*/
     unsigned int cutoff = total * percent / 100.0;
@@ -430,9 +506,9 @@ resolvePercentParams(FILE *             const ifP,
 /*----------------------------------------------------------------------------
    Figure out the endpoint of the stretch (the value that is to be stretched
    to black and the one that is to be stretched to white) as requested
-   by the -bvalue, -bpercent, -wvalue, and -wpercent options.
+   by the -{b,w}{value,percent,single} options.
 
-   These values may be invalid due to overlapping, and they may exceed
+   These values may be invalid because of overlapping, and they may exceed
    the maximum allowed stretch; Caller must deal with that.
 -----------------------------------------------------------------------------*/
     unsigned int * hist;  /* malloc'ed */
@@ -445,7 +521,9 @@ resolvePercentParams(FILE *             const ifP,
         buildHistogram(ifP, cols, rows, maxval, format, hist,
                        cmdline.brightMethod);
 
-        if (cmdline.bvalueSpec && !cmdline.bpercentSpec) {
+        if (cmdline.bsingle)
+            *bvalueP = minimumValue(hist, maxval);
+        else if (cmdline.bvalueSpec && !cmdline.bpercentSpec) {
             *bvalueP = cmdline.bvalue;
         } else {
             xelval percentBvalue;
@@ -457,7 +535,9 @@ resolvePercentParams(FILE *             const ifP,
                 *bvalueP = percentBvalue;
         }
 
-        if (cmdline.wvalueSpec && !cmdline.wpercentSpec) {
+        if (cmdline.wsingle)
+            *wvalueP = maximumValue(hist, maxval);
+        else if (cmdline.wvalueSpec && !cmdline.wpercentSpec) {
             *wvalueP = cmdline.wvalue;
         } else {
             xelval percentWvalue;
@@ -482,14 +562,25 @@ computeEndValues(FILE *             const ifP,
                  int                const format,
                  struct cmdlineInfo const cmdline,
                  xelval *           const bvalueP,
-                 xelval *           const wvalueP) {
+                 xelval *           const wvalueP,
+                 bool *             const quadraticP,
+                 xelval *           const midvalueP) {
 /*----------------------------------------------------------------------------
-   Figure out what original values will be translated to full white and
-   full black -- thus defining to what all the other values get translated.
-
-   This may involve looking at the image.  The image is in the file
-   'ifp', which is positioned just past the header (at the raster).
-   Leave it positioned arbitrarily.
+   Figure out what original values will be translated to full bright and full
+   dark and, if user requested, a middle brightness -- thus defining to what
+   all the other values get translated.
+
+   This may involve looking at the image.  The image is in the file 'ifP',
+   which is positioned just past the header (at the raster).  Leave it
+   positioned arbitrarily.
+
+   We return *quadraticP == true iff the normalization is to be via a
+   quadratic transfer function fixed at 3 points - full bright, full dark, and
+   something in between.  In that case, the original brightnesses of those
+   three points are *bvalueP, *midvalueP, and *wvalueP.  We return *quadraticP
+   == false iff the normalization is to be via a linear function fixed at 2
+   points - full bright and full dark.  In that case, *midvalueP is
+   meaningless.
 -----------------------------------------------------------------------------*/
     xelval reqBvalue, reqWvalue, nonOlapBvalue, nonOlapWvalue;
     unsigned int bLower, wRaise;
@@ -507,15 +598,187 @@ computeEndValues(FILE *             const ifP,
 
     *bvalueP = nonOlapBvalue - bLower;
     *wvalueP = nonOlapWvalue + wRaise;
+
+    if (cmdline.midvalueSpec) {
+        if (cmdline.midvalue > *bvalueP && cmdline.midvalue < *wvalueP) {
+            *quadraticP = true;
+            *midvalueP = cmdline.midvalue;
+        } else
+            *quadraticP = false;
+    } else
+        *quadraticP = false;
+}
+
+
+
+static void
+computeLinearTransfer(xelval   const bvalue,
+                      xelval   const wvalue,
+                      xelval   const maxval,
+                      xelval * const newBrightness) {
+/*----------------------------------------------------------------------------
+   Map the middle brightnesses (the ones that don't get clipped to full dark
+   or full bright, i.e. from 'bvalue' to 'wvalue') linearly onto 0..maxval.
+   Set this mapping in newBrightness[].
+-----------------------------------------------------------------------------*/
+    unsigned int const range = wvalue - bvalue;
+
+    xelval i;
+    unsigned int val;
+    /* The following for structure is a hand optimization of this one:
+       for (i = bvalue; i <= wvalue; ++i)
+       newBrightness[i] = (i-bvalue)*maxval/range);
+       (with proper rounding)
+    */
+    for (i = bvalue, val = range/2; 
+         i <= wvalue; 
+         ++i, val += maxval)
+        newBrightness[i] = MIN(val / range, maxval);
+
+    assert(newBrightness[bvalue] == 0);
+    assert(newBrightness[wvalue] == maxval);
+}
+
+
+
+typedef struct {
+/*----------------------------------------------------------------------------
+   A quadratic polynomial function
+-----------------------------------------------------------------------------*/
+    double a;  /* x^2 coefficient */
+    double b;  /* x^1 coefficient */
+    double c;  /* x^0 coefficient */
+} Quadfn;
+
+
+
+static void
+computeQuadraticFunction(xelval   const bvalue,
+                         xelval   const midvalue,
+                         xelval   const wvalue,
+                         xelval   const middle,
+                         xelval   const maxval,
+                         Quadfn * const functionP) {
+
+    /* The matrix equation we solve (for varMatrix) is
+
+       a * x = c
+    */
+    double ** a;
+    double c[3];
+    double x[3];
+    const char * error;
+
+    MALLOCARRAY2_NOFAIL(a, 3, 3);
+
+    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) {
+        pm_error("Cannot fit a quadratic function to the points "
+                 "(%u, %u), (%u, %u), and (%u, %u).  %s",
+                 bvalue, 0, midvalue, middle, wvalue, maxval, error);
+        pm_strfree(error);
+    } else {
+        functionP->a = x[0];
+        functionP->b = x[1];
+        functionP->c = x[2];
+    }
+
+    pm_freearray2((void **)a);
 }
 
 
 
 static void
-computeTransferFunction(xelval            const bvalue, 
-                        xelval            const wvalue,
-                        xelval            const maxval,
-                        xelval **         const newBrightnessP) {
+computeQuadraticTransfer(xelval   const bvalue,
+                         xelval   const midvalue,
+                         xelval   const wvalue,
+                         float    const middleNorm,
+                         xelval   const maxval,
+                         bool     const verbose,
+                         xelval * const newBrightness) {
+/*----------------------------------------------------------------------------
+   Map the middle brightnesses (the ones that don't get clipped to full dark
+   or full bright, i.e. from 'bvalue' to 'wvalue') quadratically onto
+   0..maxval, such that 'bvalue' maps to 0, 'wvalue' maps to 'maxval, and
+   'midvalue' maps to the normalized value 'middleNorm' (i.e. the actual
+   xelval middleNorm * maxval).
+
+   Set this mapping in newBrightness[].
+-----------------------------------------------------------------------------*/
+    xelval const middle = ROUNDU(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)
+
+       We do that by solving the system of three linear equations in
+       in 3 variables.  The 3 variables are the coefficients of the
+       quadratic function we're looking for -- A, B, and C in this:
+
+           NEWVAL = A * OLDVAL^2 + B * OLDVAL + C
+
+       The three equations of the system are:
+
+           0      = A * bvalue^2   + B * bvalue   + C
+           middle = A * midvalue^2 + B * midvalue + C
+           maxval = A * wvalue^2   + B * wvalue   + C
+
+       Expressed in matrix form:
+
+          [ bvalue^2   bvalue   1 ]   [ A ]   [ 0      ]
+          [ midvalue^2 midvalue 1 ] * [ B ] = [ middle ]
+          [ wvalue^2   wvalue   1 ]   [ C ]   [ maxval ]
+
+       So we solve that for A, B, and C.
+
+       With those coefficients, we have the quadratic function, and we
+       simple apply it to every old sample value I in the range to get the
+       new:
+
+           newBrightness[I] = A * I^2 + B * I + C
+    */
+
+    Quadfn xfer;
+
+    computeQuadraticFunction(bvalue, midvalue, wvalue, middle, maxval,
+                             &xfer);
+
+
+    if (verbose)
+        pm_message("Transfer function is %f * s^2 + %f * s + %f",
+                   xfer.a, xfer.b, xfer.c);
+
+    {
+        xelval i;
+        for (i = bvalue; i <= wvalue; ++i)
+            newBrightness[i] =
+                MIN(ROUNDU(xfer.a * SQR(i) + xfer.b * i + xfer.c), maxval);
+    }
+}
+
+
+
+static void
+computeTransferFunction(bool      const quadratic,
+                        xelval    const bvalue, 
+                        xelval    const midvalue, 
+                        xelval    const wvalue,
+                        float     const middle,
+                        xelval    const maxval,
+                        bool      const verbose,
+                        xelval ** const newBrightnessP) {
 /*----------------------------------------------------------------------------
    Compute the transfer function, i.e. the array *newBrightnessP such that
    (*newBrightnessP)[x] is the brightness of the xel that should replace a
@@ -524,9 +787,16 @@ computeTransferFunction(xelval            const bvalue,
 
    'bvalue' is the highest brightness that should map to zero brightness;
    'wvalue' is the lowest brightness that should map to full brightness.
-   brightnesses in between should be stretched linearly.  (That stretching
-   could conceivably result in more brightnesses mapping to zero and full
-   brightness, due to rounding).
+
+   If 'quadratic' is false, brightnesses in between should be stretched
+   linearly.  Otherwise, brightness 'midvalue' should map to brightness
+   'middle' (which is expressed on a 0..1 normalized scale) and brightnesses
+   should be stretched according to a quadratic polynomial that includes those
+   3 points.
+
+   This stretching could conceivably result in more brightnesses mapping to
+   zero and full brightness that 'bvalue' and 'wvalue' demand, because of
+   rounding.
 
    Define function only for values 0..maxval.
 -----------------------------------------------------------------------------*/
@@ -543,23 +813,13 @@ computeTransferFunction(xelval            const bvalue,
         for (i = 0; i < bvalue; ++i)
             newBrightness[i] = 0;
 
-    /* Map the middle brightnesses linearly onto 0..maxval */
-    {
-        unsigned int const range = wvalue - bvalue;
-        unsigned int val;
-        /* The following for loop is a hand optimization of this one:
-           for (i = bvalue; i <= wvalue; ++i)
-             newBrightness[i] = (i-bvalue)*maxval/range);
-           (with proper rounding)
-        */
-        for (i = bvalue, val = range/2; 
-             i <= wvalue; 
-             ++i, val += maxval)
-            newBrightness[i] = MIN(val / range, maxval);
-
-        assert(newBrightness[bvalue] == 0);
-        assert(newBrightness[wvalue] == maxval);
-    }
+    /* Map the middle brightnesses onto 0..maxval */
+    
+    if (quadratic)
+        computeQuadraticTransfer(bvalue, midvalue, wvalue, middle, maxval,
+                                 verbose, newBrightness);
+    else
+        computeLinearTransfer(bvalue, wvalue, maxval, newBrightness);
 
     /* Clip the highest brightnesses to maxval */
     for (i = wvalue+1; i <= maxval; ++i)
@@ -589,7 +849,7 @@ brightScaler(xel               const p,
              
     switch (brightMethod) {
     case BRIGHT_LUMINOSITY:
-        oldBrightness = PPM_LUMIN(p);
+        oldBrightness = ppm_luminosity(p);
         break;
     case BRIGHT_COLORVALUE:
         oldBrightness = ppm_colorvalue(p);
@@ -653,6 +913,25 @@ writeRowNormalized(xel *             const xelrow,
 
 
 
+static void
+reportTransferParm(bool   const quadratic,
+                   xelval const bvalue,
+                   xelval const midvalue,
+                   xelval const wvalue,
+                   xelval const maxval,
+                   float  const middle) {
+
+    if (quadratic)
+        pm_message("remapping %u..%u..%u to %u..%u..%u",
+                   bvalue, midvalue, wvalue,
+                   0, ROUNDU(maxval*middle), maxval);
+    else
+        pm_message("remapping %u..%u to %u..%u",
+                   bvalue, wvalue, 0, maxval);
+}
+
+
+
 int
 main(int argc, const char *argv[]) {
 
@@ -661,20 +940,21 @@ main(int argc, const char *argv[]) {
     pm_filepos imagePos;
     xelval maxval;
     int rows, cols, format;
-    xelval bvalue, wvalue;
+    bool quadratic;
+    xelval bvalue, midvalue, wvalue;
     
     pm_proginit(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
 
-    ifP = pm_openr_seekable(cmdline.inputFilespec);
+    ifP = pm_openr_seekable(cmdline.inputFileName);
 
     /* Rescale so that bvalue maps to 0, wvalue maps to maxval. */
     pnm_readpnminit(ifP, &cols, &rows, &maxval, &format);
     pm_tell2(ifP, &imagePos, sizeof(imagePos));
 
     computeEndValues(ifP, cols, rows, maxval, format, cmdline, 
-                     &bvalue, &wvalue);
+                     &bvalue, &wvalue, &quadratic, &midvalue);
     {
         xelval * newBrightness;
         int row;
@@ -685,9 +965,13 @@ main(int argc, const char *argv[]) {
 
         xelrow = pnm_allocrow(cols);
 
-        pm_message("remapping %u..%u to %u..%u", bvalue, wvalue, 0, maxval);
+        reportTransferParm(quadratic, bvalue, midvalue, wvalue, maxval,
+                           cmdline.middle);
 
-        computeTransferFunction(bvalue, wvalue, maxval, &newBrightness);
+        
+        computeTransferFunction(quadratic, bvalue, midvalue, wvalue,
+                                cmdline.middle, maxval, cmdline.verbose,
+                                &newBrightness);
 
         pm_seek2(ifP, &imagePos, sizeof(imagePos));
         pnm_writepnminit(stdout, cols, rows, maxval, format, 0);
@@ -704,6 +988,9 @@ main(int argc, const char *argv[]) {
         pnm_freerow(rowbuf);
         pnm_freerow(xelrow);
     } 
-   pm_close(ifP);
+    pm_close(ifP);
     return 0;
 }
+
+
+
diff --git a/editor/pnmpad.c b/editor/pnmpad.c
index 34672dc5..168b73e0 100644
--- a/editor/pnmpad.c
+++ b/editor/pnmpad.c
@@ -11,6 +11,9 @@
 #include "pnm.h"
 
 #define MAX_WIDTHHEIGHT INT_MAX-10
+    /* The maximum width or height value we can handle without risking
+       arithmetic overflow
+    */
 
 struct cmdlineInfo {
     /* All the information the user supplied in the command line,
@@ -31,6 +34,8 @@ struct cmdlineInfo {
     unsigned int bottomSpec;
     float xalign;
     float yalign;
+    unsigned int mwidth;
+    unsigned int mheight;
     unsigned int white;     /* >0: pad white; 0: pad black */
     unsigned int verbose;
 };
@@ -51,7 +56,7 @@ parseCommandLine(int argc, const char ** argv,
 
     unsigned int option_def_index;
     unsigned int blackOpt;
-    unsigned int xalignSpec, yalignSpec;
+    unsigned int xalignSpec, yalignSpec, mwidthSpec, mheightSpec;
 
     MALLOCARRAY_NOFAIL(option_def, 100);
 
@@ -82,6 +87,10 @@ parseCommandLine(int argc, const char ** argv,
             &yalignSpec,           0);
     OPTENT3(0,   "black",     OPT_FLAG,    NULL,
             &blackOpt,           0);
+    OPTENT3(0,   "mwidth",    OPT_UINT,    &cmdlineP->mwidth,
+            &mwidthSpec,         0);
+    OPTENT3(0,   "mheight",   OPT_UINT,    &cmdlineP->mheight,
+            &mheightSpec,        0);
     OPTENT3(0,   "white",     OPT_FLAG,    NULL,
             &cmdlineP->white,    0);
     OPTENT3(0,   "verbose",   OPT_FLAG,    NULL,
@@ -91,7 +100,7 @@ parseCommandLine(int argc, const char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    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 (blackOpt && cmdlineP->white)
@@ -147,6 +156,12 @@ parseCommandLine(int argc, const char ** argv,
     } else
         cmdlineP->yalign = 0.5;
 
+    if (!mwidthSpec)
+        cmdlineP->mwidth = 1;
+
+    if (!mheightSpec)
+        cmdlineP->mheight = 1;
+
     /* get optional input filename */
     if (argc-1 > 1)
         pm_error("This program takes at most 1 parameter.  You specified %d",
@@ -163,7 +178,7 @@ static void
 parseCommandLineOld(int argc, const char ** argv,
                     struct cmdlineInfo * const cmdlineP) {
 
-    /* This syntax was abandonned in February 2002. */
+    /* This syntax was abandoned in February 2002. */
     pm_message("Warning: old style options are deprecated!");
 
     cmdlineP->xsize = cmdlineP->ysize = 0;
@@ -229,13 +244,17 @@ parseCommandLineOld(int argc, const char ** argv,
 
 static void
 validateHorizontalSize(struct cmdlineInfo const cmdline,
-                       unsigned int const cols) {
-
-    unsigned int const xsize = cmdline.xsizeSpec ? cmdline.xsize : 0;
-    unsigned int const lpad  = cmdline.leftSpec  ? cmdline.left  : 0;
-    unsigned int const rpad  = cmdline.rightSpec ? cmdline.right : 0;
+                       unsigned int       const cols) {
+/*----------------------------------------------------------------------------
+   Abort the program if the padding parameters in 'cmdline', applied to
+   an image width 'cols', would result in numbers too large for us to
+   compute with easily.
+-----------------------------------------------------------------------------*/
+    unsigned int const lpad         = cmdline.leftSpec   ? cmdline.left   : 0;
+    unsigned int const rpad         = cmdline.rightSpec  ? cmdline.right  : 0;
+    unsigned int const mwidthMaxPad = cmdline.mwidth - 1;
 
-    if (xsize > MAX_WIDTHHEIGHT)
+    if (cmdline.xsizeSpec && cmdline.xsize > MAX_WIDTHHEIGHT)
         pm_error("The width value you specified is too large.");
 
     if (lpad > MAX_WIDTHHEIGHT)
@@ -244,66 +263,167 @@ validateHorizontalSize(struct cmdlineInfo const cmdline,
     if (rpad > MAX_WIDTHHEIGHT)
         pm_error("The right padding value you specified is too large.");
 
-    if ((double) cols + (double) lpad + (double) rpad > MAX_WIDTHHEIGHT)
-        pm_error("Given padding value(s) makes output width too large.");
+    if ((double) cols +
+        (double) lpad + 
+        (double) rpad +
+        (double) mwidthMaxPad > MAX_WIDTHHEIGHT)
+        pm_error("Given padding parameters make output width too large "
+                 "for this program to compute");
+
+    if (cmdline.xsizeSpec &&
+        (double) cmdline.xsize + (double) mwidthMaxPad> MAX_WIDTHHEIGHT)
+        pm_error("Given padding parameters make output width too large "
+                 "for this program to compute");
 }
 
 
 
 static void
-computeHorizontalPadSizes(struct cmdlineInfo const cmdline,
-                          unsigned int       const cols,
-                          unsigned int *     const lpadP,
-                          unsigned int *     const rpadP) {
-
-    validateHorizontalSize(cmdline, cols);
-
-    if (cmdline.xsizeSpec) {
-        if (cmdline.leftSpec && cmdline.rightSpec) {
-            if (cmdline.left + cols + cmdline.right < cmdline.xsize) {
-                pm_error("Left padding (%u), and right "
+computePadSizeBeforeMult(unsigned int   const unpaddedSize,
+                         bool           const sizeSpec,
+                         unsigned int   const sizeReq,
+                         bool           const begPadSpec,
+                         unsigned int   const begPadReq,
+                         bool           const endPadSpec,
+                         unsigned int   const endPadReq,
+                         double         const align,
+                         unsigned int * const begPadP,
+                         unsigned int * const endPadP) {
+/*----------------------------------------------------------------------------
+   Compute the padding on each end that would be required if user did not
+   request any "multiple" padding; i.e. he didn't say request e.g. that the
+   output width be a multiple of 10 pixels.
+-----------------------------------------------------------------------------*/
+    if (sizeSpec) {
+        if (begPadSpec && endPadSpec) {
+            if (begPadReq + unpaddedSize + endPadReq < unpaddedSize) {
+                pm_error("Beginning adding (%u), and end "
                          "padding (%u) are insufficient to bring the "
-                         "image width of %d up to %u.",
-                         cmdline.left, cmdline.right, cols, cmdline.xsize);
+                         "image size of %u up to %u.",
+                         begPadReq, endPadReq, unpaddedSize, sizeReq);
             } else {
-                *lpadP = cmdline.left;
-                *rpadP = cmdline.right;
+                *begPadP = begPadReq;
+                *endPadP = endPadReq;
             }
-        } else if (cmdline.leftSpec) {
-            *lpadP = cmdline.left;
-            *rpadP = MAX(cmdline.xsize, cmdline.left + cols) -
-                (cmdline.left + cols);
-        } else if (cmdline.rightSpec) {
-            *rpadP = cmdline.right;
-            *lpadP = MAX(cmdline.xsize, cols + cmdline.right) -
-                (cols + cmdline.right);
+        } else if (begPadSpec) {
+            *begPadP = begPadReq;
+            *endPadP = MAX(sizeReq, unpaddedSize + begPadReq) -
+                (begPadReq + unpaddedSize);
+        } else if (endPadReq) {
+            *endPadP = endPadReq;
+            *begPadP = MAX(sizeReq, unpaddedSize + endPadReq) -
+                (unpaddedSize + endPadReq);
         } else {
-            if (cmdline.xsize > cols) {
-                *lpadP = ROUNDU((cmdline.xsize - cols) * cmdline.xalign);
-                *rpadP = cmdline.xsize - cols - *lpadP;
+            if (sizeReq > unpaddedSize) {
+                *begPadP = ROUNDU((sizeReq - unpaddedSize) * align);
+                *endPadP = sizeReq - unpaddedSize - *begPadP;
             } else {
-                *lpadP = 0;
-                *rpadP = 0;
+                *begPadP = 0;
+                *endPadP = 0;
             }
         }
     } else {
-        *lpadP = cmdline.leftSpec  ? cmdline.left  : 0;
-        *rpadP = cmdline.rightSpec ? cmdline.right : 0;
+        *begPadP = begPadSpec ? begPadReq : 0;
+        *endPadP = endPadSpec ? endPadReq : 0;
+    }
+}
+
+
+
+static void
+computePadSizesOneDim(unsigned int   const unpaddedSize,
+                      bool           const sizeSpec,
+                      unsigned int   const sizeReq,
+                      bool           const begPadSpec,
+                      unsigned int   const begPadReq,
+                      bool           const endPadSpec,
+                      unsigned int   const endPadReq,
+                      double         const align,
+                      unsigned int   const multiple,
+                      unsigned int * const begPadP,
+                      unsigned int * const endPadP) {
+/*----------------------------------------------------------------------------
+   Compute the number of pixels of padding needed before and after a row or
+   column ("before" means on the left side of a row or the top side of a
+   column).  Return them as *padBegP and *padEndP, respectively.
+
+   'unpaddedSize' is the size (width/height) of the row or column before
+   any padding.
+
+   The rest of the inputs are the padding parameters, equivalent to the
+   program's corresponding command line options.
+-----------------------------------------------------------------------------*/
+    unsigned int begPadBeforeMult, endPadBeforeMult;
+        /* The padding we would apply if user did not request multiple
+           padding (such as "make the output a multiple of 10 pixels")
+        */
+
+    computePadSizeBeforeMult(unpaddedSize, sizeSpec, sizeReq,
+                             begPadSpec, begPadReq, endPadSpec, endPadReq,
+                             align,
+                             &begPadBeforeMult, &endPadBeforeMult);
+
+    {
+        unsigned int const sizeBeforeMpad =
+            unpaddedSize + begPadBeforeMult + endPadBeforeMult;
+        unsigned int const paddedSize =
+            ROUNDUP(sizeBeforeMpad, multiple);
+        unsigned int const morePadNeeded = paddedSize - sizeBeforeMpad;
+        unsigned int const totalPadBeforeMult =
+            begPadBeforeMult + endPadBeforeMult;
+        double const begFrac =
+            totalPadBeforeMult > 0 ? 
+            (double)begPadBeforeMult / totalPadBeforeMult :
+            0.0;
+        unsigned int const addlMsizeBegPad = ROUNDU(morePadNeeded * begFrac);
+            /* # of pixels we have to add to the beginning to satisfy
+               user's desire for the final size to be a multiple of something
+            */
+        unsigned int const addlMsizeEndPad = morePadNeeded - addlMsizeBegPad;
+            /* Analogous to 'addlMsizeBegPad' */
+
+        *begPadP = begPadBeforeMult + addlMsizeBegPad;
+        *endPadP = endPadBeforeMult + addlMsizeEndPad;
     }
 }
 
 
 
 static void
+computeHorizontalPadSizes(struct cmdlineInfo const cmdline,
+                          unsigned int       const cols,
+                          unsigned int *     const lpadP,
+                          unsigned int *     const rpadP) {
+
+    validateHorizontalSize(cmdline, cols);
+
+    computePadSizesOneDim(cols,
+                          cmdline.xsizeSpec > 0, cmdline.xsize,
+                          cmdline.leftSpec > 0, cmdline.left,
+                          cmdline.rightSpec > 0, cmdline.right,
+                          cmdline.xalign,
+                          cmdline.mwidth,
+                          lpadP, rpadP);
+}
+
+
+
+static void
 validateVerticalSize(struct cmdlineInfo const cmdline,
                      unsigned int       const rows) {
+/*----------------------------------------------------------------------------
+   Abort the program if the padding parameters in 'cmdline', applied to
+   an image width 'cols', would result in numbers too large for us to
+   compute with easily.
+-----------------------------------------------------------------------------*/
+    unsigned int const tpad          =
+        cmdline.topSpec    ?  cmdline.top     : 0;
+    unsigned int const bpad          =
+        cmdline.bottomSpec ?  cmdline.bottom  : 0;
+    unsigned int const mheightMaxPad = cmdline.mheight - 1;
 
-    unsigned int const ysize = cmdline.ysizeSpec  ? cmdline.ysize  : 0;
-    unsigned int const tpad  = cmdline.topSpec    ? cmdline.top    : 0;
-    unsigned int const bpad  = cmdline.bottomSpec ? cmdline.bottom : 0;
-
-    if (ysize > MAX_WIDTHHEIGHT)
-        pm_error("The height value you specified is too large.");
+    if (cmdline.ysizeSpec && cmdline.ysize > MAX_WIDTHHEIGHT)
+        pm_error("The width value you specified is too large.");
 
     if (tpad > MAX_WIDTHHEIGHT)
         pm_error("The top padding value you specified is too large.");
@@ -311,8 +431,17 @@ validateVerticalSize(struct cmdlineInfo const cmdline,
     if (bpad > MAX_WIDTHHEIGHT)
         pm_error("The bottom padding value you specified is too large.");
 
-    if ((double) rows + (double) tpad + (double) bpad > MAX_WIDTHHEIGHT)
-        pm_error("Given padding value(s) makes output height too large.");
+    if ((double) rows +
+        (double) tpad +
+        (double) bpad +
+        (double) mheightMaxPad > MAX_WIDTHHEIGHT)
+        pm_error("Given padding parameters make output height too large "
+            "for this program to compute");
+
+    if (cmdline.ysizeSpec &&
+        (double) cmdline.ysize && (double) mheightMaxPad > MAX_WIDTHHEIGHT)
+        pm_error("Given padding parameters make output height too large "
+            "for this program to compute");
 }
 
 
@@ -325,38 +454,13 @@ computeVerticalPadSizes(struct cmdlineInfo const cmdline,
 
     validateVerticalSize(cmdline, rows);
 
-    if (cmdline.ysizeSpec) {
-        if (cmdline.topSpec && cmdline.bottomSpec) {
-            if (cmdline.bottom + rows + cmdline.top < cmdline.ysize) {
-                pm_error("Top padding (%u), and bottom "
-                         "padding (%u) are insufficient to bring the "
-                         "image height of %d up to %u.",
-                         cmdline.top, cmdline.bottom, rows, cmdline.ysize);
-            } else {
-                *tpadP = cmdline.top;
-                *bpadP = cmdline.bottom;
-            }
-        } else if (cmdline.topSpec) {
-            *tpadP = cmdline.top;
-            *bpadP = MAX(cmdline.ysize, cmdline.top + rows) -
-                (cmdline.top + rows);
-        } else if (cmdline.bottomSpec) {
-            *bpadP = cmdline.bottom;
-            *tpadP = MAX(cmdline.ysize, rows + cmdline.bottom) -
-                (rows + cmdline.bottom);
-        } else {
-            if (cmdline.ysize > rows) {
-                *bpadP = ROUNDU((cmdline.ysize - rows) * cmdline.yalign);
-                *tpadP = cmdline.ysize - rows - *bpadP;
-            } else {
-                *bpadP = 0;
-                *tpadP = 0;
-            }
-        }
-    } else {
-        *bpadP = cmdline.bottomSpec ? cmdline.bottom : 0;
-        *tpadP = cmdline.topSpec    ? cmdline.top    : 0;
-    }
+    computePadSizesOneDim(rows,
+                          cmdline.ysizeSpec > 0, cmdline.ysize,
+                          cmdline.topSpec > 0, cmdline.top,
+                          cmdline.bottomSpec > 0, cmdline.bottom,
+                          cmdline.yalign,
+                          cmdline.mheight,
+                          tpadP, bpadP);
 }
 
 
diff --git a/editor/pnmpaste.c b/editor/pnmpaste.c
index 33834669..1e29d933 100644
--- a/editor/pnmpaste.c
+++ b/editor/pnmpaste.c
@@ -1,4 +1,4 @@
-/* pnmpaste.c - paste a rectangle into a portable anymap
+/* pnmpaste.c - paste a rectangle into a PNM image
 **
 ** Copyright (C) 1989 by Jef Poskanzer.
 **
@@ -21,7 +21,7 @@
 
 enum boolOp {REPLACE, AND, OR, XOR /*, NAND, NOR, NXOR */ };
 
-struct cmdlineInfo {
+struct CmdlineInfo {
     /* All the information the user supplied in the command line,
        in a form easy for the program to use.
     */
@@ -36,7 +36,7 @@ struct cmdlineInfo {
 
 static void
 parseCommandLine(int argc, const char ** argv,
-                 struct cmdlineInfo * const cmdlineP) {
+                 struct CmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
    Note that the file spec array we return is stored in the storage that
    was passed to us as the argv array.
@@ -65,7 +65,7 @@ parseCommandLine(int argc, const char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = TRUE;  /* We have parms that are negative numbers */
 
-    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 (replaceOpt + andOpt + orOpt + xorOpt > 1)
@@ -108,7 +108,7 @@ static unsigned char
 leftBits(unsigned char const x,
          unsigned int  const n){
 /*----------------------------------------------------------------------------
-  Clear rightmost (8-n) bits, retain leftmost (=high) n bits.
+  'x' with the leftmost (high) n bits retained and the rest cleared to zero.
 -----------------------------------------------------------------------------*/
     assert(n <= 8);
 
@@ -121,7 +121,7 @@ static unsigned char
 rightBits(unsigned char const x,
           unsigned int  const n){
 /*----------------------------------------------------------------------------
-  Return rightmost (=low) n bits of x. (Clear the rest).
+  The rightmost 'n' bits of 'x'.
 -----------------------------------------------------------------------------*/
     assert(n <= 8);
 
@@ -146,6 +146,12 @@ insertDirect(FILE *          const ifP,
    'buffer' is a scratch buffer for our use, at least wide enough to hold
    a packed PBM row of 'ifP'.
 -----------------------------------------------------------------------------*/
+    /* We use pbm_readpbmrow_packed() to read whole bytes rounded up and merge
+       those into 'destrow', which means we update more than we're supposed to
+       if the image is not a multiple of 8 columns.  In that case, we then fix
+       up the last byte by replacing the bits from the original image that we
+       messed up.
+    */
     unsigned int  const colBytes  = pbm_packed_bytes(cols);
     unsigned int  const last      = colBytes - 1;
     unsigned char const origRight = destrow[last];
@@ -172,6 +178,10 @@ insertDirect(FILE *          const ifP,
         }
     }
 
+    /* destrow[] now contains garbage in all but the cols % 8 leftmost bits of
+       the last byte we touched.  Those are supposed to be unchanged from the
+       input, so we restore them now.
+    */
     if (cols % 8 > 0)
         destrow[last] = leftBits(destrow[last], cols % 8)
             | rightBits(origRight, 8 - cols % 8);
@@ -194,10 +204,10 @@ insertShift(FILE *          const ifP,
    buffer[] is wide enough to hold a packed PBM row of *ifP plus one
    byte of margin.
 -----------------------------------------------------------------------------*/
-    unsigned int const shiftBytes = pbm_packed_bytes(cols + offset);
-    unsigned int const last = shiftBytes - 1;
-    unsigned char const origLeft  = destrow[0];
-    unsigned char const origRight = destrow[last];
+    unsigned int  const shiftByteCt = pbm_packed_bytes(cols + offset);
+    unsigned int  const last        = shiftByteCt - 1;
+    unsigned char const origLeft    = destrow[0];
+    unsigned char const origRight   = destrow[last];
 
     unsigned int const padOffset = (cols + offset) % 8;
 
@@ -209,7 +219,7 @@ insertShift(FILE *          const ifP,
 
     /* Note that buffer[0] is undefined. */
 
-    for (i = 0; i < shiftBytes; ++i) {
+    for (i = 0; i < shiftByteCt; ++i) {
         unsigned int  const rsh = offset;
         unsigned int  const lsh = 8-rsh;
         unsigned char const t = buffer[i] << lsh | buffer[i+1] >> rsh;
@@ -227,9 +237,9 @@ insertShift(FILE *          const ifP,
         }
     }
 
-    /* destrow[] now contains garbage in the 'offset' leftmost bits
-       and 8-offset rightmost bits.  Those are supposed to be unchanged
-       from the input, so we restore them now.
+    /* destrow[] now contains garbage in the 'offset' leftmost bits and
+       8-offset rightmost bits of the last byte we touched.  Those are
+       supposed to be unchanged from the input, so we restore them now.
     */
 
     destrow[0] = leftBits(origLeft, offset) |
@@ -257,11 +267,11 @@ pastePbm(FILE *       const fpInset,
 /*----------------------------------------------------------------------------
   Fast paste for PBM
 -----------------------------------------------------------------------------*/
-    unsigned char * const baserow = pbm_allocrow_packed(baseCols);
-    unsigned char * const buffer = pbm_allocrow_packed(insetCols+8);
-    int const shiftBytes = insertCol / 8;
-    unsigned int const shiftOffset = insertCol % 8;
-    int const baseColBytes = pbm_packed_bytes(baseCols);
+    unsigned char * const baserow       = pbm_allocrow_packed(baseCols);
+    unsigned char * const buffer        = pbm_allocrow_packed(insetCols+8);
+    unsigned int    const shiftByteCt   = insertCol / 8;
+    unsigned int    const shiftOffset   = insertCol % 8;
+    unsigned int    const baseColByteCt = pbm_packed_bytes(baseCols);
 
     unsigned int row;
 
@@ -272,16 +282,16 @@ pastePbm(FILE *       const fpInset,
         
         if (row >= insertRow && row < insertRow + insetRows) {
             if (shiftOffset == 0)
-                insertDirect(fpInset, &baserow[shiftBytes], insetCols,
+                insertDirect(fpInset, &baserow[shiftByteCt], insetCols,
                              insetFormat, operation, buffer);
             else
-                insertShift(fpInset, &baserow[shiftBytes], insetCols,
+                insertShift(fpInset, &baserow[shiftByteCt], insetCols,
                             insetFormat, shiftOffset, operation, buffer);
         }
 
         if (baseCols % 8 > 0)
-            baserow[baseColBytes-1]
-                = leftBits(baserow[baseColBytes-1] , baseCols % 8);
+            baserow[baseColByteCt-1]
+                = leftBits(baserow[baseColByteCt-1] , baseCols % 8);
 
         pbm_writepbmrow_packed(stdout, baserow, baseCols, 0);
     }
@@ -344,7 +354,7 @@ pasteNonPbm(FILE *       const fpInset,
 int
 main(int argc, const char ** argv) {
 
-    struct cmdlineInfo cmdline;
+    struct CmdlineInfo cmdline;
     FILE * fpInset;
     FILE * fpBase;
     xelval maxvalInset, maxvalBase;
diff --git a/editor/pnmquant b/editor/pnmquant
index 5edbe85e..93d452cd 100755
--- a/editor/pnmquant
+++ b/editor/pnmquant
@@ -1,6 +1,29 @@
-#!/usr/bin/perl -w
+#!/bin/sh
 
 ##############################################################################
+# This is essentially a Perl program.  We exec the Perl interpreter specifying
+# this same file as the Perl program and use the -x option to cause the Perl
+# interpreter to skip down to the Perl code.  The reason we do this instead of
+# just making /usr/bin/perl the script interpreter (instead of /bin/sh) is
+# that the user may have multiple Perl interpreters and the one he wants to
+# use is properly located in the PATH.  The user's choice of Perl interpreter
+# may be crucial, such as when the user also has a PERL5LIB environment
+# variable and it selects modules that work with only a certain main
+# interpreter program.
+#
+# An alternative some people use is to have /usr/bin/env as the script
+# interpreter.  We don't do that because we think the existence and
+# compatibility of /bin/sh is more reliable.
+#
+# Note that we aren't concerned about efficiency because the user who needs
+# high efficiency can use directly the programs that this program invokes.
+#
+##############################################################################
+
+exec perl -w -x -S -- "$0" "$@"
+
+#!/usr/bin/perl
+##############################################################################
 #                         pnmquant 
 ##############################################################################
 #  By Bryan Henderson, San Jose CA; December 2001.
@@ -11,7 +34,6 @@
 use strict;
 use English;
 use Getopt::Long;
-#use File::Temp "tempfile";  # not available before Perl 5.6.1
 use File::Spec;
 #use Fcntl ":seek";  # not available in Perl 5.00503
 use Fcntl;  # gets open flags
@@ -22,22 +44,26 @@ my ($SEEK_SET, $SEEK_CUR, $SEEK_END) = (0, 1, 2);
 
 sub tempFile($) {
 
-# Here's what we'd do if we could expect Perl 5.6.1 or later, instead
-# of calling this subroutine:
-#    my ($file, $filename) = tempfile("pnmquant_XXXX", 
-#                                     SUFFIX=>".pnm", 
-#                                     DIR=>File::Spec->tmpdir())
-#                                     UNLINK=>$TRUE);
-    my ($suffix) = @_;
-    my $fileName;
-    local *file;  # For some inexplicable reason, must be local, not my
-    my $i;
-    $i = 0;
-    do {
-        $fileName = File::Spec->tmpdir() . "/pnmquant_" . $i++ . $suffix;
-    } until sysopen(*file, $fileName, O_RDWR|O_CREAT|O_EXCL);
-
-    return(*file, $fileName);
+    # We trust Perl's File::Temp to do a better job of creating the temp
+    # file, but it doesn't exist before Perl 5.6.1.
+
+    if (eval { require File::Temp; 1 }) {
+        return File::Temp::tempfile("pnmquant_XXXX", 
+                                    SUFFIX=>".pnm", 
+                                    DIR=>File::Spec->tmpdir(),
+                                    UNLINK=>$TRUE);
+    } else {
+        my ($suffix) = @_;
+        my $fileName;
+        local *file;  # For some inexplicable reason, must be local, not my
+        my $i;
+        $i = 0;
+        do {
+            $fileName = File::Spec->tmpdir() . "/pnmquant_" . $i++ . $suffix;
+        } until sysopen(*file, $fileName, O_RDWR|O_CREAT|O_EXCL);
+
+        return(*file, $fileName);
+    }
 }
 
 
@@ -247,7 +273,7 @@ openSeekableAsStdin($cmdlineR->{infile});
 
 # Save Standard Output for our eventual output
 open(OLDOUT, ">&STDOUT");
-select(OLDOUT);  # avoids Perl bug where it says we never use STDOUT 
+select(OLDOUT);  # avoids Perl bug where it says we never use OLDOUT 
 
 
 my $mapfileSpec = makeColormap($cmdlineR->{ncolors}, 
diff --git a/editor/pnmquantall b/editor/pnmquantall
new file mode 100755
index 00000000..0890383e
--- /dev/null
+++ b/editor/pnmquantall
@@ -0,0 +1,208 @@
+#!/bin/sh
+
+##############################################################################
+# This is essentially a Perl program.  We exec the Perl interpreter specifying
+# this same file as the Perl program and use the -x option to cause the Perl
+# interpreter to skip down to the Perl code.  The reason we do this instead of
+# just making /usr/bin/perl the script interpreter (instead of /bin/sh) is
+# that the user may have multiple Perl interpreters and the one he wants to
+# use is properly located in the PATH.  The user's choice of Perl interpreter
+# may be crucial, such as when the user also has a PERL5LIB environment
+# variable and it selects modules that work with only a certain main
+# interpreter program.
+#
+# An alternative some people use is to have /usr/bin/env as the script
+# interpreter.  We don't do that because we think the existence and
+# compatibility of /bin/sh is more reliable.
+#
+# Note that we aren't concerned about efficiency because the user who needs
+# high efficiency can use directly the programs that this program invokes.
+#
+##############################################################################
+
+exec perl -w -x -S -- "$0" "$@"
+
+#!/usr/bin/perl
+##############################################################################
+#                                  pnmquantall  
+##############################################################################
+#
+# HISTORY:
+#
+# This was in the original 1989 Pbmplus as a C shell program.  In Netpbm 9.13
+# (April 2001), it was converted to Bash.  (Actually, it was thought to be
+# Bourne shell, but it used arrays).  In Netpbm 10.58 (March 2012), it was
+# converted to Perl for better portability.
+#
+# The 2012 Perl conversion also changed the name from Ppmquantall to
+# Pnmquantall.  It had already handled non-PPM input files for many years.
+#
+# The original program was more complex:  Because in those days Pnmcolormap
+# and Pnmremap did not exist, Ppmquantall concatenated all the input images
+# together and ran Ppmquant (later Pnmquant) on the combination.  It then
+# split the combination image apart to make one output image per input image.
+# Today, Pnmquant is just a combination of Pnmcolormap and Pnmremap, and
+# we are able to use them better separately in Ppmquantall: We still make
+# the combination image, but use it only to compute the colormap with
+# Pnmcolormap.  We then apply that colormap separately to each input image
+# to produce an output image.
+#
+# Bryan Henderson wrote the current version from scratch in March 2012
+# and contributed it to the public domain.
+#
+##############################################################################
+
+use strict;
+use warnings;
+use English;
+use Fcntl;  # gets open flags
+
+my $TRUE=1; my $FALSE = 0;
+
+
+
+sub parseArgs($$$$) {
+    my ($argvR, $extR, $newColorCtR, $fileNamesR) = @_;
+
+    my @argv = @{$argvR};
+
+    my $firstArgPos;
+
+    if (@argv > 0 && $argv[0] eq "-ext") {
+        if (@argv < 2) {
+            print STDERR ("-ext requires a value\n");
+        exit(100);
+        } else {
+            $$extR = $argv[1];
+            $firstArgPos = 2;
+        }
+    } else {
+        $$extR = "";
+        $firstArgPos = 0;
+    }
+
+    if (@argv < $firstArgPos + 2) {
+        print STDERR ("Not enough arguments.  You need at least the number " .
+                      "of colors and one file name\n");
+        exit(100);
+    }
+    
+    $$newColorCtR = $argv[$firstArgPos];
+
+    @{$fileNamesR} = @argv[$firstArgPos + 1 .. @argv-1];
+}
+
+
+
+sub tempFile($) {
+
+    # We trust Perl's File::Temp to do a better job of creating the temp
+    # file, but it doesn't exist before Perl 5.6.1.
+
+    if (eval { require File::Temp; 1 }) {
+        return File::Temp::tempfile("pnmquant_XXXX", 
+                                    SUFFIX=>".pnm", 
+                                    DIR=>File::Spec->tmpdir(),
+                                    UNLINK=>$TRUE);
+    } else {
+        my ($suffix) = @_;
+        my $fileName;
+        local *file;  # For some inexplicable reason, must be local, not my
+        my $i;
+        $i = 0;
+        do {
+            $fileName = File::Spec->tmpdir() . "/pnmquant_" . $i++ . $suffix;
+        } until sysopen(*file, $fileName, O_RDWR|O_CREAT|O_EXCL);
+
+        return(*file, $fileName);
+    }
+}
+
+
+
+sub makeColorMap($$$$) {
+    my ($fileNamesR, $newColorCt, $colorMapFileName, $errorR) = @_;
+
+    my $pnmcatCmd = "pnmcat -topbottom -white -jleft @{$fileNamesR}";
+
+    my $pnmcolormapCmd = "pnmcolormap $newColorCt";
+
+    my $makeMapCmd = "$pnmcatCmd | $pnmcolormapCmd >$colorMapFileName";
+
+    my $termStatus = system($makeMapCmd);
+
+    if ($termStatus != 0) {
+        $$errorR =
+            "Shell command to create the color map failed:  '$makeMapCmd'.";
+    }
+}
+
+
+ 
+sub remapFiles($$$$) {
+    my ($fileNamesR, $colorMapFileName, $ext, $errorR) = @_;
+
+    my ($outputFh, $outputFileName) = tempFile("pnm");
+    if (!defined($outputFh)) {
+        $$errorR = "Unable to create temporary file.  Errno=$ERRNO";
+    } else {
+        for (my $i = 0; $i < @{$fileNamesR} && !$$errorR; ++$i) {
+            my $inFileName = $fileNamesR->[$i];
+
+            my $pnmremapCmd =
+                "pnmremap '$inFileName' -mapfile=$colorMapFileName " .
+                ">$outputFileName";
+
+            my $pnmremapTermStatus = system($pnmremapCmd);
+
+            if ($pnmremapTermStatus != 0) {
+                $errorR =
+                    "Shell command to quantize '$inFileName'  failed:  " .
+                    "'$pnmremapCmd'";
+            } else {
+                my $newFileName = $inFileName . $ext;
+
+                unlink($newFileName);
+                rename($outputFileName, $newFileName)
+                    or $errorR = "Rename to '$newFileName' failed.";
+            }
+        }
+        unlink($outputFileName);  # In case something failed
+    }
+}
+
+
+
+###############################################################################
+#                             MAINLINE
+###############################################################################
+
+my $progError;
+
+parseArgs(\@ARGV, \my $ext, \my $newColorCt, \my @fileNames);
+
+my ($colorMapFh, $colorMapFileName) = tempFile("pnm");
+if (!defined($colorMapFh)) {
+    $progError = "Unable to create temporary file.  Errno=$ERRNO";
+}
+
+if (!$progError) {
+    makeColorMap(\@fileNames, $newColorCt, $colorMapFileName, \$progError);
+}
+print ("got color map\n");
+if (!$progError) {
+    remapFiles(\@fileNames, $colorMapFileName, $ext, \$progError);
+}
+
+my $exitStatus;
+
+if ($progError) {
+    print STDERR ("Failed.  $progError\n");
+    $exitStatus = 1;
+} else {
+    $exitStatus = 0;
+}
+
+unlink($colorMapFileName);
+
+exit($exitStatus);
diff --git a/editor/pnmremap.c b/editor/pnmremap.c
index db35e2c0..ed758aa3 100644
--- a/editor/pnmremap.c
+++ b/editor/pnmremap.c
@@ -30,17 +30,18 @@
 #include "nstring.h"
 #include "shhopt.h"
 #include "pam.h"
+#include "ppm.h"
 #include "pammap.h"
 
 #define MAXCOLORS 32767u
 
-enum missingMethod {
+enum MissingMethod {
     MISSING_FIRST,
     MISSING_SPECIFIED,
     MISSING_CLOSE
 };
 
-struct cmdlineInfo {
+struct CmdlineInfo {
     /* All the information the user supplied in the command line,
        in a form easy for the program to use.
     */
@@ -48,7 +49,7 @@ struct cmdlineInfo {
     const char * mapFilespec;    /* Filespec of colormap file */
     unsigned int floyd;   /* Boolean: -floyd/-fs option */
     unsigned int norandom;
-    enum missingMethod missingMethod;
+    enum MissingMethod missingMethod;
     char * missingcolor;      
         /* -missingcolor value.  Null if not specified */
     unsigned int verbose;
@@ -57,8 +58,8 @@ struct cmdlineInfo {
 
 
 static void
-parseCommandLine (int argc, char ** argv,
-                  struct cmdlineInfo *cmdlineP) {
+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.  
@@ -70,7 +71,7 @@ parseCommandLine (int argc, char ** argv,
    was passed to us as the argv array.  We also trash *argv.
 -----------------------------------------------------------------------------*/
     optEntry * option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -82,24 +83,24 @@ parseCommandLine (int argc, char ** argv,
     MALLOCARRAY_NOFAIL(option_def, 100);
     
     option_def_index = 0;   /* incremented by OPTENT3 */
-    OPTENT3(0,   "floyd",        OPT_FLAG,   
-            NULL,                       &cmdlineP->floyd, 0);
-    OPTENT3(0,   "fs",           OPT_FLAG,   
-            NULL,                       &cmdlineP->floyd, 0);
-    OPTENT3(0,   "nofloyd",      OPT_FLAG,   
-            NULL,                       &nofloyd, 0);
-    OPTENT3(0,   "nofs",         OPT_FLAG,   
-            NULL,                       &nofloyd, 0);
-    OPTENT3(0,   "norandom",     OPT_FLAG,   
+    OPTENT3(0,   "floyd",          OPT_FLAG,   
+            NULL,                       &cmdlineP->floyd,    0);
+    OPTENT3(0,   "fs",             OPT_FLAG,   
+            NULL,                       &cmdlineP->floyd,    0);
+    OPTENT3(0,   "nofloyd",        OPT_FLAG,   
+            NULL,                       &nofloyd,            0);
+    OPTENT3(0,   "nofs",           OPT_FLAG,   
+            NULL,                       &nofloyd,            0);
+    OPTENT3(0,   "norandom",       OPT_FLAG,   
             NULL,                       &cmdlineP->norandom, 0);
     OPTENT3(0,   "firstisdefault", OPT_FLAG,   
-            NULL,                       &firstisdefault, 0);
-    OPTENT3(0,   "mapfile",      OPT_STRING, 
-            &cmdlineP->mapFilespec,    &mapfileSpec, 0);
-    OPTENT3(0,   "missingcolor", OPT_STRING, 
-            &cmdlineP->missingcolor,   &missingSpec, 0);
-    OPTENT3(0, "verbose",        OPT_FLAG,   NULL,                  
-            &cmdlineP->verbose,        0 );
+            NULL,                       &firstisdefault,     0);
+    OPTENT3(0,   "mapfile",        OPT_STRING, 
+            &cmdlineP->mapFilespec,    &mapfileSpec,         0);
+    OPTENT3(0,   "missingcolor",   OPT_STRING, 
+            &cmdlineP->missingcolor,   &missingSpec,         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 */
@@ -107,7 +108,7 @@ parseCommandLine (int argc, char ** argv,
 
     cmdlineP->missingcolor = NULL;  /* default value */
     
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0 );
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdline_p and others. */
 
     if (cmdlineP->floyd && nofloyd)
@@ -134,6 +135,8 @@ parseCommandLine (int argc, char ** argv,
         cmdlineP->inputFilespec = "-";
     else
         cmdlineP->inputFilespec = argv[1];
+
+    free(option_def);
 }
 
 
@@ -267,7 +270,7 @@ selectDepthAdjustment(const struct pam * const pamP,
     if (newDepth == pamP->depth)
         *adjustmentP = ADJUST_NONE;
     else {
-        if (stripeq(pamP->tuple_type, "RGB")) {
+        if (pm_stripeq(pamP->tuple_type, "RGB")) {
             if (newDepth != 1) {
                 pm_error("Map image depth of %u differs from input image "
                          "depth of %u, and the tuple type is RGB.  "
@@ -276,8 +279,8 @@ selectDepthAdjustment(const struct pam * const pamP,
                          newDepth, pamP->depth);
             } else
                 *adjustmentP = ADJUST_RGBTO1;
-        } else if (stripeq(pamP->tuple_type, "GRAYSCALE") ||
-                   stripeq(pamP->tuple_type, "BLACKANDWHITE")) {
+        } else if (pm_stripeq(pamP->tuple_type, "GRAYSCALE") ||
+                   pm_stripeq(pamP->tuple_type, "BLACKANDWHITE")) {
             if (newDepth != 3) {
                 pm_error("Map image depth of %u differs from input image "
                          "depth of %u, and the tuple type is GRAYSCALE "
@@ -748,7 +751,7 @@ lookupThroughHash(struct pam *            const pamP,
         } else 
             searchColormapClose(pamP, tuple, colorFinderP, colormapIndexP);
         if (*usehashP) {
-            bool fits;
+            int fits;
             pnm_addtotuplehash(pamP, colorhash, tuple, *colormapIndexP, 
                                &fits);
             if (!fits) {
@@ -1062,7 +1065,7 @@ remap(FILE *             const ifP,
    same as that of the input even though the individual pixels have different
    colors.
 -----------------------------------------------------------------------------*/
-    bool eof;
+    int eof;
     eof = FALSE;
     while (!eof) {
         struct pam inpam, outpam;
@@ -1102,7 +1105,14 @@ processMapFile(const char *   const mapFileName,
                tupletable *   const colormapP,
                unsigned int * const colormapSizeP,
                tuple *        const firstColorP) {
+/*----------------------------------------------------------------------------
+   Read a color map from the file named 'mapFileName'.  It's a map that
+   associates each color in that file with a unique whole number.  Return the
+   map as *colormapP, with the number of entries in it as *colormapSizeP.
 
+   Also determine the first color (top left) in the map file and return that
+   as *firstColorP.
+-----------------------------------------------------------------------------*/
     FILE * mapfile;
     struct pam mappam;
     tuple ** maptuples;
@@ -1142,7 +1152,7 @@ getSpecifiedMissingColor(struct pam * const pamP,
             specColor[PAM_GRN_PLANE] = PPM_GETG(color);
             specColor[PAM_BLU_PLANE] = PPM_GETB(color);
         } else if (pamP->depth == 1) {
-            specColor[0] = PPM_LUMIN(color);
+            specColor[0] = ppm_luminosity(color);
         } else {
             pm_error("You may not use -missing with a colormap that is not "
                      "of depth 1 or 3.  Yours has depth %u",
@@ -1155,9 +1165,9 @@ getSpecifiedMissingColor(struct pam * const pamP,
 
 
 int
-main(int argc, char * argv[] ) {
+main(int argc, const char * argv[] ) {
 
-    struct cmdlineInfo cmdline;
+    struct CmdlineInfo cmdline;
     FILE * ifP;
     struct pam outpamCommon;
         /* Describes the output images.  Width and height fields are
@@ -1180,7 +1190,7 @@ main(int argc, char * argv[] ) {
            color (i.e. we'll choose an approximate match from the map).
         */
 
-    pnm_init(&argc, argv);
+    pm_proginit(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
 
@@ -1216,3 +1226,6 @@ main(int argc, char * argv[] ) {
 
     return 0;
 }
+
+
+
diff --git a/editor/pnmrotate.c b/editor/pnmrotate.c
index ba37f4e0..da5de3a8 100644
--- a/editor/pnmrotate.c
+++ b/editor/pnmrotate.c
@@ -80,7 +80,7 @@ parseCommandLine(int argc, char ** const argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = TRUE;  /* We may have parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (!backgroundSpec)
diff --git a/editor/pnmscalefixed.c b/editor/pnmscalefixed.c
index 8deb65d7..884ca315 100644
--- a/editor/pnmscalefixed.c
+++ b/editor/pnmscalefixed.c
@@ -22,8 +22,9 @@
 #include <math.h>
 
 #include "pm_c_util.h"
-#include "pnm.h"
+#include "mallocvar.h"
 #include "shhopt.h"
+#include "pnm.h"
 
 /* The pnm library allows us to code this program without branching cases
    for PGM and PPM, but we do the branch anyway to speed up processing of 
@@ -61,27 +62,29 @@ parse_command_line(int argc, char ** argv,
    Note that the file spec array we return is stored in the storage that
    was passed to us as the argv array.
 -----------------------------------------------------------------------------*/
-    optStruct *option_def = malloc(100*sizeof(optStruct));
-        /* Instructions to OptParseOptions2 on how to parse our options.
+    optEntry * option_def;
+        /* Instructions to OptParseOptions3 on how to parse our options.
          */
-    optStruct2 opt;
+    optStruct3 opt;
 
     unsigned int option_def_index;
     int xysize, xsize, ysize, pixels;
     int reduce;
     float xscale, yscale, scale_parm;
 
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
     option_def_index = 0;   /* incremented by OPTENTRY */
-    OPTENTRY(0,   "xsize",     OPT_UINT,    &xsize,         0);
-    OPTENTRY(0,   "width",     OPT_UINT,    &xsize,         0);
-    OPTENTRY(0,   "ysize",     OPT_UINT,    &ysize,         0);
-    OPTENTRY(0,   "height",    OPT_UINT,    &ysize,         0);
-    OPTENTRY(0,   "xscale",    OPT_FLOAT,   &xscale,        0);
-    OPTENTRY(0,   "yscale",    OPT_FLOAT,   &yscale,        0);
-    OPTENTRY(0,   "pixels",    OPT_UINT,    &pixels,        0);
-    OPTENTRY(0,   "xysize",    OPT_FLAG,    &xysize,        0);
-    OPTENTRY(0,   "verbose",   OPT_FLAG,    &cmdline_p->verbose,        0);
-    OPTENTRY(0,   "reduce",    OPT_UINT,    &reduce,        0);
+    OPTENT3(0,   "xsize",     OPT_UINT,    &xsize,              NULL, 0);
+    OPTENT3(0,   "width",     OPT_UINT,    &xsize,              NULL, 0);
+    OPTENT3(0,   "ysize",     OPT_UINT,    &ysize,              NULL, 0);
+    OPTENT3(0,   "height",    OPT_UINT,    &ysize,              NULL, 0);
+    OPTENT3(0,   "xscale",    OPT_FLOAT,   &xscale,             NULL, 0);
+    OPTENT3(0,   "yscale",    OPT_FLOAT,   &yscale,             NULL, 0);
+    OPTENT3(0,   "pixels",    OPT_UINT,    &pixels,             NULL, 0);
+    OPTENT3(0,   "xysize",    OPT_FLAG,    &xysize,             NULL, 0);
+    OPTENT3(0,   "verbose",   OPT_FLAG,    &cmdline_p->verbose, NULL, 0);
+    OPTENT3(0,   "reduce",    OPT_UINT,    &reduce,             NULL, 0);
 
     /* Set the defaults. -1 = unspecified */
     xsize = -1;
@@ -97,7 +100,7 @@ parse_command_line(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions2(&argc, argv, opt, 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdline_p and others. */
 
     if (xsize == 0)
@@ -253,7 +256,7 @@ compute_output_dimensions(const struct cmdline_info cmdline,
             *newrowsP = rows;
     }    
 
-    /* If the calculations above yielded (due to rounding) a zero 
+    /* If the calculations above yielded (because of rounding) a zero 
        dimension, we fudge it up to 1.  We do this rather than considering
        it a specification error (and dying) because it's friendlier to 
        automated processes that work on arbitrary input.  It saves them
@@ -280,7 +283,7 @@ horizontal_scale(const xel inputxelrow[], xel newxelrow[],
    output rows.
 
    *stretchP is the number of columns (could be fractional) on the right 
-   that we had to fill by stretching due to rounding problems.
+   that we had to fill by stretching because of rounding problems.
 -----------------------------------------------------------------------------*/
     long r, g, b;
     long fraccoltofill, fraccolleft;
@@ -571,7 +574,7 @@ main(int argc, char **argv ) {
             
             if (cmdline.verbose && row == 0 && stretch != 0)
                 pm_message("%d/%d = %f right columns filled by stretching "
-                           "due to arithmetic imprecision", 
+                           "because of arithmetic imprecision", 
                            stretch, SCALE, (float) stretch/SCALE);
             
             pnm_writepnmrow(stdout, newxelrow, newcols, 
diff --git a/editor/pnmshear.c b/editor/pnmshear.c
index 359df299..99fa3026 100644
--- a/editor/pnmshear.c
+++ b/editor/pnmshear.c
@@ -1,4 +1,4 @@
-/* pnmshear.c - read a portable anymap and shear it by some angle
+ /* pnmshear.c - read a portable anymap and shear it by some angle
 **
 ** Copyright (C) 1989, 1991 by Jef Poskanzer.
 **
@@ -17,6 +17,7 @@
 #include <string.h>
 
 #include "pm_c_util.h"
+#include "mallocvar.h"
 #include "ppm.h"
 #include "pnm.h"
 #include "shhopt.h"
@@ -24,11 +25,13 @@
 #define SCALE 4096
 #define HALFSCALE 2048
 
-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;  /* Filespec of input file */
+    const char * inputFileName;   /* Name of input file */
     double       angle;           /* requested shear angle, in radians */
     unsigned int noantialias;     /* -noantialias option */
     const char * background;      /* NULL if none */
@@ -37,15 +40,17 @@ struct cmdline_info {
 
 
 static void
-parseCommandLine(int argc, char ** argv,
-                 struct cmdline_info *cmdlineP) {
+parseCommandLine(int argc, const char ** argv,
+                 struct CmdlineInfo *cmdlineP) {
 
     optStruct3 opt;
     unsigned int option_def_index = 0;
-    optEntry *option_def = malloc(100*sizeof(optEntry));
+    optEntry * option_def;
 
     unsigned int backgroundSpec;
 
+    MALLOCARRAY(option_def, 100);
+
     OPTENT3(0, "noantialias",      OPT_FLAG,  NULL, &cmdlineP->noantialias, 0);
     OPTENT3(0, "background",       OPT_STRING, &cmdlineP->background,
             &backgroundSpec, 0);
@@ -54,7 +59,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;
     opt.allowNegNum = TRUE;
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
 
     if (!backgroundSpec)
         cmdlineP->background = NULL;
@@ -68,15 +73,16 @@ parseCommandLine(int argc, char ** argv,
             pm_error("Angle argument is not a valid floating point number: "
                      "'%s'", argv[1]);
         if (argc-1 < 2)
-            cmdlineP->input_filespec = "-";
+            cmdlineP->inputFileName = "-";
         else {
-            cmdlineP->input_filespec = argv[2];
+            cmdlineP->inputFileName = argv[2];
             if (argc-1 > 2)
                 pm_error("too many arguments (%d).  "
                          "The only arguments are shear angle and filespec.",
                          argc-1);
         }
     }
+    free(option_def);
 }
 
 
@@ -200,7 +206,7 @@ backgroundColor(const char * const backgroundColorName,
 
 
 int
-main(int argc, char * argv[]) {
+main(int argc, const char * argv[]) {
 
     FILE * ifP;
     xel * xelrow;
@@ -212,13 +218,13 @@ main(int argc, char * argv[]) {
     xelval maxval, newmaxval;
     double shearfac;
 
-    struct cmdline_info cmdline;
+    struct CmdlineInfo cmdline;
 
-    pnm_init(&argc, argv);
+    pm_proginit(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
 
-    ifP = pm_openr(cmdline.input_filespec);
+    ifP = pm_openr(cmdline.inputFileName);
 
     pnm_readpnminit(ifP, &cols, &rows, &maxval, &format);
     xelrow = pnm_allocrow(cols);
@@ -256,7 +262,7 @@ main(int argc, char * argv[]) {
             shearCols = (rows - row) * shearfac;
 
         shearRow(xelrow, cols, newxelrow, newcols, 
-                  shearCols, format, bgxel, !cmdline.noantialias);
+                 shearCols, format, bgxel, !cmdline.noantialias);
 
         pnm_writepnmrow(stdout, newxelrow, newcols, newmaxval, newformat, 0);
     }
@@ -266,3 +272,6 @@ main(int argc, char * argv[]) {
 
     return 0;
 }
+
+
+
diff --git a/editor/pnmsmooth.c b/editor/pnmsmooth.c
index 16d8ec33..a76bd42b 100644
--- a/editor/pnmsmooth.c
+++ b/editor/pnmsmooth.c
@@ -21,6 +21,7 @@
 
 
 #include <unistd.h>
+#include <assert.h>
 #include <string.h>
 #include <errno.h>
 
@@ -28,6 +29,7 @@
 #include "mallocvar.h"
 #include "shhopt.h"
 #include "nstring.h"
+#include "pm.h"   /* For pm_plain_output */
 #include "pm_system.h"
 #include "pnm.h"
 
@@ -39,13 +41,13 @@ struct cmdlineInfo {
     const char * inputFilespec;  /* Filespec of input file */
     unsigned int width;
     unsigned int height;
-    const char * dump;
+    unsigned int dump;
 };
 
 
 
 static void
-parseCommandLine (int argc, char ** argv,
+parseCommandLine (int argc, const char ** argv,
                   struct cmdlineInfo *cmdlineP) {
 /*----------------------------------------------------------------------------
    parse program command line described in Unix standard form by argc
@@ -58,19 +60,19 @@ parseCommandLine (int argc, char ** argv,
    was passed to us as the argv array.  We also trash *argv.
 -----------------------------------------------------------------------------*/
     optEntry * option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
     unsigned int option_def_index;
 
-    unsigned int widthSpec, heightSpec, dumpSpec, sizeSpec;
+    unsigned int widthSpec, heightSpec, sizeSpec;
 
     MALLOCARRAY_NOFAIL(option_def, 100);
     
     option_def_index = 0;   /* incremented by OPTENT3 */
-    OPTENT3(0,   "dump",          OPT_STRING,   
-            &cmdlineP->dump,            &dumpSpec, 0);
+    OPTENT3(0,   "dump",          OPT_FLAG,   
+            NULL,                       &cmdlineP->dump, 0);
     OPTENT3(0,   "width",         OPT_UINT,
             &cmdlineP->width,           &widthSpec, 0);
     OPTENT3(0,   "height",        OPT_UINT,
@@ -82,18 +84,17 @@ parseCommandLine (int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0 );
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdline_p and others. */
 
+    free(option_def);
+
     if (!widthSpec)
         cmdlineP->width = 3;
 
     if (!heightSpec)
         cmdlineP->height = 3;
 
-    if (!dumpSpec)
-        cmdlineP->dump = NULL;
-
     if (sizeSpec) {
         /* -size is strictly for backward compatibility.  This program
            used to use a different command line processor and had
@@ -142,69 +143,122 @@ parseCommandLine (int argc, char ** argv,
 }
 
 
-
 static void
-writeConvolutionImage(FILE *       const cofp,
-                      unsigned int const cols,
-                      unsigned int const rows,
-                      int          const format) {
-
-    xelval const convmaxval = rows * cols * 2;
-        /* normalizing factor for our convolution matrix */
-    xelval const g = rows * cols + 1;
-        /* weight of all pixels in our convolution matrix */
-    int row;
-    xel *outputrow;
-
-    if (convmaxval > PNM_OVERALLMAXVAL)
-        pm_error("The convolution matrix is too large.  "
-                 "Width x Height x 2\n"
-                 "must not exceed %d and it is %d.",
-                 PNM_OVERALLMAXVAL, convmaxval);
-
-    pnm_writepnminit(cofp, cols, rows, convmaxval, format, 0);
-    outputrow = pnm_allocrow(cols);
-
-    for (row = 0; row < rows; ++row) {
-        unsigned int col;
-        for (col = 0; col < cols; ++col)
-            PNM_ASSIGN1(outputrow[col], g);
-        pnm_writepnmrow(cofp, outputrow, cols, convmaxval, format, 0);
+validateComputableDimensions(unsigned int const cols,
+                             unsigned int const rows){
+/*----------------------------------------------------------------------------
+   Make sure that convolution matrix dimensions are small enough to
+   represent in a string.
+-----------------------------------------------------------------------------*/
+    unsigned int const maxStringLength = INT_MAX - 2 -6;
+
+    if (cols >  maxStringLength / rows / 2 )
+       pm_error("The convolution matrix size %u x %u is too large.",
+                cols, rows);
+}
+
+
+
+static const char *
+makeConvolutionKernel(unsigned int const cols,
+                      unsigned int const rows) {
+/*----------------------------------------------------------------------------
+  Return a value for a Pnmconvol '-matrix' option that specifies a
+  convolution kernel with with dimensions 'cols' by 'rows' with 1
+  for every weight.  Caller can use this with Pnmconvol -normalize.
+-----------------------------------------------------------------------------*/
+    unsigned int const maxOptSize = cols * rows * 2;
+
+    char * matrix;
+
+    MALLOCARRAY(matrix, maxOptSize);
+
+    if (matrix == NULL)
+        pm_error("Could not get memory for a %u x %u convolution matrix",
+                 rows, cols);
+    else {
+        unsigned int row;
+        unsigned int cursor;
+     
+        for (row = 0, cursor = 0; row < rows; ++row) {
+            unsigned int col;
+
+            if (row > 0)
+                matrix[cursor++] = ';';
+
+            for (col = 0; col < cols; ++col) {
+                if (col > 0)
+                    matrix[cursor++] = ',';
+
+                matrix[cursor++] = '1';
+            }
+        }
+        assert(cursor < maxOptSize);
+        matrix[cursor] = '\0';
     }
-    pnm_freerow(outputrow);
+
+    return matrix;
+}
+
+
+
+static void
+validateMatrixOptSize(unsigned int const rows,
+                      unsigned int const cols) {
+
+    /* If the user accidentally specifies absurdly large values for the
+       convolution matrix size, the failure mode can be a confusing message
+       resulting from the 'pnmconvol' arguments being too large.  To try
+       to be more polite in that case, we apply an arbitrary limit on the
+       size of the option here.
+    */
+
+    if (rows * cols > 5000)
+        pm_error("Convolution matrix dimensions %u x %u are too large "
+                 "to be useful, so we assume you made a mistake.  "
+                 "We refuse to use numbers this large because they might "
+                 "cause excessive resource use that would cause failures "
+                 "whose cause would not be obvious to you", cols, rows);
 }
 
 
 
 int
-main(int argc, char ** argv) {
+main(int argc, const char ** argv) {
 
     struct cmdlineInfo cmdline;
-    FILE * convFileP;
-    const char * tempfileName;
+    const char * matrixOptValue;
 
-    pnm_init(&argc, argv);
+    pm_proginit(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
 
-    if (cmdline.dump)
-        convFileP = pm_openw(cmdline.dump);
-    else
-        pm_make_tmpfile(&convFileP, &tempfileName);
-        
-    writeConvolutionImage(convFileP, cmdline.width, cmdline.height,
-                          PGM_FORMAT);
+    validateComputableDimensions(cmdline.width, cmdline.height);
+    validateMatrixOptSize(cmdline.width, cmdline.height);
 
-    pm_close(convFileP);
+    matrixOptValue = makeConvolutionKernel(cmdline.width, cmdline.height);
 
     if (cmdline.dump) {
-        /* We're done.  Convolution image is in user's file */
+        pm_error("-dump option no longer exists.  "
+                 "You don't need it because you can now do uniform "
+                 "convolution easily with the -matrix and -normalize "
+                 "options of 'pnmconvol'.");
     } else {
+        const char * const plainOpt = pm_plain_output ? "-plain" : NULL;
+
+        const char * matrixOpt;
+
+        pm_asprintf(&matrixOpt, "-matrix=%s", matrixOptValue);
+
+        pm_message("Running Pnmconvol -normalize %s", matrixOpt);
+
         pm_system_lp("pnmconvol", NULL, NULL, NULL, NULL,
-                     "pnmconvol", tempfileName, cmdline.inputFilespec, NULL);
+                     "pnmconvol", matrixOpt, cmdline.inputFilespec,
+                     "-normalize", "-quiet", plainOpt, NULL);
 
-        unlink(tempfileName);
-        strfree(tempfileName);
+        pm_strfree(matrixOpt);
     }
+    pm_strfree(matrixOptValue);
+
     return 0;
 }
diff --git a/editor/pnmstitch.c b/editor/pnmstitch.c
index 45aee2f4..849445fb 100644
--- a/editor/pnmstitch.c
+++ b/editor/pnmstitch.c
@@ -77,7 +77,7 @@
  *      - Add RotateCrop filter algorithm (in-memory copy of image,
  *        detect least loss horizontal crop on a rotated image).
  *      - pnmstitch should be generalized to handle transformation
- *        occuring on the left image, currently it blends assuming
+ *        occurring on the left image, currently it blends assuming
  *        that there is no transformation effects on the left image.
  *      - user selectable blending algorithms?
  */
@@ -233,7 +233,7 @@ parseCommandLine ( int argc, char ** argv,
    was passed to us as the argv array.  We also trash *argv.
 -----------------------------------------------------------------------------*/
     optEntry *option_def = malloc( 100*sizeof( optEntry ) );
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -265,7 +265,7 @@ parseCommandLine ( int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3( &argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (!widthSpec) {
@@ -370,7 +370,7 @@ static void
 free_image(Image * image)
 {
     if (image->name) {
-        strfree(image->name);
+        pm_strfree(image->name);
         image->name = NULL;     
     }
     if (image->tuple) {
@@ -405,14 +405,14 @@ openWithPossibleExtension(const char *  const baseName,
         
         const char * trialName;
         
-        asprintfN(&trialName, "%s%s", baseName, extlist[extIndex]);
+        pm_asprintf(&trialName, "%s%s", baseName, extlist[extIndex]);
         
         ifP = fopen(trialName, "rb");
         
         if (ifP)
             *filenameP = trialName;
         else
-            strfree(trialName);
+            pm_strfree(trialName);
     }
     if (!ifP) 
         pm_error ("Failed to open input file named '%s' "
@@ -470,7 +470,7 @@ writeinit(Image * image)
 {
     if (streq(image->name, "-")) {
         image->pam.file = stdout;
-        strfree(image->name);
+        pm_strfree(image->name);
         image->name = strdup("<stdout>");
     } else {
         image->pam.file = pm_openw(image->name);
@@ -893,7 +893,7 @@ stitchOneRow(Image *    const Left,
      *  We scale the overlap of the left and right images, we need to
      * discover and hold on to the left edge of the right image to
      * determine the rate at which we blend. Most (7/8) of the blending
-     * occurs in the first half of the overlap to reduce the occurences
+     * occurs in the first half of the overlap to reduce the occurrences
      * of blending artifacts. If there is no overlap, the image present
      * has no blending activity, this is determined by the black
      * background and is not through an alpha layer to help reduce
@@ -1393,7 +1393,7 @@ LinearConstrain(Stitcher * me, int x, int y, int width, int height)
  *  width sliver of the left hand side of the right image and compare
  *  the sample to the left hand image. Accuracy is honored over speed.
  *  The image overlap is expected between 7/16 to 1/16 in the horizontal
- *  position, and a minumum of 5/8 in the vertical dimension.
+ *  position, and a minimum of 5/8 in the vertical dimension.
  *
  *  Blind alleys:
  *      - reduced resolution can match in totally wrong regions,
@@ -2071,7 +2071,7 @@ BiLinearConstrain(Stitcher * me, int x, int y, int width, int height)
  *  width sliver of the left hand side of the right image and compare
  *  the sample to the left hand image. Accuracy is honored over speed.
  *  The image overlap is expected between 7/16 to 1/16 in the horizontal
- *  position, and a minumum of 5/8 in the vertical dimension.
+ *  position, and a minimum of 5/8 in the vertical dimension.
  *
  *  Blind alleys:
  *      - Tried a simpler constraint for right side to be `back'
diff --git a/editor/pnmtile.c b/editor/pnmtile.c
index 21512b36..5ec53415 100644
--- a/editor/pnmtile.c
+++ b/editor/pnmtile.c
@@ -52,7 +52,7 @@ parseCommandLine(int argc, const char ** argv,
 
     OPTENTINIT;
 
-    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 (argc-1 < 2)
diff --git a/editor/ppmbrighten.c b/editor/ppmbrighten.c
index 9bcf7bb5..6db3987b 100644
--- a/editor/ppmbrighten.c
+++ b/editor/ppmbrighten.c
@@ -44,7 +44,7 @@ parseCommandLine(int argc, char ** argv,
    was passed to us as the argv array.  We also trash *argv.
 -----------------------------------------------------------------------------*/
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -68,7 +68,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3( &argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
     
     if (saturationSpec) {
diff --git a/editor/ppmchange.c b/editor/ppmchange.c
index f0d2cc38..dea85a77 100644
--- a/editor/ppmchange.c
+++ b/editor/ppmchange.c
@@ -70,7 +70,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We may have parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (!remainderSpec)
diff --git a/editor/ppmcolormask.c b/editor/ppmcolormask.c
index 0d7a214c..31fbff2a 100644
--- a/editor/ppmcolormask.c
+++ b/editor/ppmcolormask.c
@@ -9,6 +9,7 @@
   Contributed to the public domain by its author.
 =========================================================================*/
 
+#define _XOPEN_SOURCE 500  /* Make sure strdup() is in string.h */
 #define _BSD_SOURCE  /* Make sure strdup() is in <string.h> */
 #include <assert.h>
 #include <string.h>
@@ -59,7 +60,7 @@ parseColorOpt(const char *         const colorOpt,
     colorCount = 0; /* initial value */
     while (!eol && colorCount < ARRAY_SIZE(cmdlineP->maskColor)) {
         const char * token;
-        token = strsepN(&cursor, ",");
+        token = pm_strsep(&cursor, ",");
         if (token) {
             if (strneq(token, "bk:", 3)) {
                 cmdlineP->maskColor[colorCount].matchType = MATCH_BK;
@@ -108,7 +109,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We may have parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and all of *cmdlineP. */
 
     if (colorSpec)
diff --git a/editor/ppmdist.c b/editor/ppmdist.c
index 90c2e3d3..e8f17bff 100644
--- a/editor/ppmdist.c
+++ b/editor/ppmdist.c
@@ -22,6 +22,7 @@ struct colorToGrayEntry {
 #define MAXCOLORS 255
 
 
+
 static gray
 newGrayValue(pixel *pix, struct colorToGrayEntry *colorToGrayMap, int colors) {
 
@@ -40,20 +41,34 @@ newGrayValue(pixel *pix, struct colorToGrayEntry *colorToGrayMap, int colors) {
 
 
 
+#ifndef LITERAL_FN_DEF_MATCH
+static qsort_comparison_fn cmpColorToGrayEntryByIntensity;
+#endif
+
 static int
-cmpColorToGrayEntryByIntensity(const void *entry1, const void *entry2) {
+cmpColorToGrayEntryByIntensity(const void * const a,
+                               const void * const b) {
+
+    const struct colorToGrayEntry * const entry1P = a;
+    const struct colorToGrayEntry * const entry2P = b;
 
-    return ((struct colorToGrayEntry *) entry1)->gray -
-        ((struct colorToGrayEntry *) entry2)->gray;
+    return entry1P->gray - entry2P->gray;
 }
 
 
 
+#ifndef LITERAL_FN_DEF_MATCH
+static qsort_comparison_fn cmpColorToGrayEntryByFrequency;
+#endif
+
 static int
-cmpColorToGrayEntryByFrequency(const void * entry1, const void * entry2) {
+cmpColorToGrayEntryByFrequency(const void * const a,
+                               const void * const b) {
+
+    const struct colorToGrayEntry * const entry1P = a;
+    const struct colorToGrayEntry * const entry2P = b;
 
-    return ((struct colorToGrayEntry *) entry1)->frequency -
-        ((struct colorToGrayEntry *) entry2)->frequency;
+    return entry1P->frequency - entry2P->frequency;
 }
 
 
@@ -125,7 +140,7 @@ main(int argc, char *argv[]) {
          * by frequency - but again, for a small number of colors
          * it's a small matter.
          */
-        colorToGrayMap[color].gray = PPM_LUMIN(hist[color].color);
+        colorToGrayMap[color].gray = ppm_luminosity(hist[color].color);
     }
 
     /*
diff --git a/editor/ppmdither.c b/editor/ppmdither.c
index beb45e2f..ec1b9771 100644
--- a/editor/ppmdither.c
+++ b/editor/ppmdither.c
@@ -1,60 +1,294 @@
-/* ppmdither.c - Ordered dithering of a color ppm file to a specified number
-**               of primary shades.
-**
-** Copyright (C) 1991 by Christos Zoulas.
-**
-** 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.
-*/
+/*=============================================================================
+                                 pamdither
+===============================================================================
+  By Bryan Henderson, July 2006.
+
+  Contributed to the public domain.
 
-#include "ppm.h"
+  This is meant to replace Ppmdither by Christos Zoulas, 1991.
+=============================================================================*/
+#include <assert.h>
+
+#include "pm_c_util.h"
 #include "mallocvar.h"
+#include "nstring.h"
+#include "shhopt.h"
+#include "pam.h"
 
 /* Besides having to have enough memory available, the limiting factor
    in the dithering matrix power is the size of the dithering value.
    We need 2*dith_power bits in an unsigned int.  We also reserve
    one bit to give headroom to do calculations with these numbers.
 */
-#define MAX_DITH_POWER ((sizeof(unsigned int)*8 - 1) / 2)
+#define MAX_DITH_POWER (((unsigned)sizeof(unsigned int)*8 - 1) / 2)
+
+
+struct colorResolution {
+    unsigned int c[3];
+        /* comp[PAM_RED_PLANE] is number of distinct red levels, etc. */
+};
+
+#define RED PAM_RED_PLANE
+#define GRN PAM_GRN_PLANE
+#define BLU PAM_BLU_PLANE
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFileName;  /* File name of input file */
+    unsigned int dim;
+    struct colorResolution colorRes;
+    unsigned int verbose;
+};
+
+
+
+static void
+parseCommandLine(int argc, const char ** const 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 dimSpec, redSpec, greenSpec, blueSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+    
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "dim",          OPT_UINT, 
+            &cmdlineP->dim,            &dimSpec,                  0);
+    OPTENT3(0, "red",          OPT_UINT, 
+            &cmdlineP->colorRes.c[RED],   &redSpec,       0);
+    OPTENT3(0, "green",        OPT_UINT, 
+            &cmdlineP->colorRes.c[GRN],   &greenSpec,     0);
+    OPTENT3(0, "blue",         OPT_UINT,
+            &cmdlineP->colorRes.c[BLU],   &blueSpec,      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 */
+    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 *cmdline_p and others. */
+
+    if (!dimSpec)
+        cmdlineP->dim = 4;
+
+    if (cmdlineP->dim > MAX_DITH_POWER)
+        pm_error("Dithering matrix power %u (-dim) is too large.  "
+                 "Must be <= %u",
+                 cmdlineP->dim, MAX_DITH_POWER);
+        
+    if (redSpec) {
+        if (cmdlineP->colorRes.c[RED] < 2)
+            pm_error("-red must be at least 2.  You specified %u",
+                     cmdlineP->colorRes.c[RED]);
+    } else
+        cmdlineP->colorRes.c[RED] = 5;
+
+    if (greenSpec) {
+        if (cmdlineP->colorRes.c[GRN] < 2)
+            pm_error("-green must be at least 2.  You specified %u",
+                     cmdlineP->colorRes.c[GRN]);
+    } else
+        cmdlineP->colorRes.c[GRN] = 9;
+
+    if (blueSpec) {
+        if (cmdlineP->colorRes.c[BLU] < 2)
+            pm_error("-blue must be at least 2.  You specified %u",
+                     cmdlineP->colorRes.c[BLU]);
+    } else
+        cmdlineP->colorRes.c[BLU] = 5;
+
+    if (argc-1 > 1)
+        pm_error("Program takes at most one argument: the input file "
+                 "specification.  "
+                 "You specified %d arguments.", argc-1);
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else
+        cmdlineP->inputFileName = argv[1];
+}
+
+
+
+typedef struct {
+/*----------------------------------------------------------------------------
+   A scaler object scales a red/green/blue triple, each component having its
+   own maxval, to a tuple having another maxval.  That maxval is the same for
+   all three components.  The input and output maxvals are characteristic of
+   the scaler.
+
+   Example: The scaler scales from a red value of 0-3, green value of
+   0-3, and blue value of 0-1 to a tuple with maxval 255.  So you can
+   ask it to scale (1,1,1) and it responds with (85, 85, 255).
+-----------------------------------------------------------------------------*/
+    struct colorResolution colorRes;
+        /* Number of values of each color component possible, i.e. maxval
+           plus 1
+        */
+    tuple * out;
+        /* Malloced array that provides the scaled output when indexed by a
+           certain function (see scaler_scale()) of the input red, green, and
+           blue values.
+        */
+} scaler;    
+
+
+
+static tuple *
+allocScalerMap(unsigned int const size) {
+    /* The tuple row data structure starts with 'size' pointers to
+       the tuples, immediately followed by the 'size' tuples
+       themselves.  Each tuple consists of 3 samples.  
+    */
+
+    unsigned int const depth = 3;
+    unsigned int const bytesPerTuple = depth * sizeof(sample);
+
+    tuple * map;
+
+    map = malloc(size * (sizeof(tuple *) + bytesPerTuple));
+                      
+    if (map != NULL) {
+        /* Now we initialize the pointers to the individual tuples
+           to make this a regulation C two dimensional array.  
+        */
+        char * p;
+        unsigned int i;
+        
+        p = (char*) (map + size);  /* location of Tuple 0 */
+        for (i = 0; i < size; ++i) {
+            map[i] = (tuple) p;
+            p += bytesPerTuple;
+        }
+    }
+    return map;
+}
+
+
+
+static void
+scaler_create(sample                 const outputMaxval,
+              struct colorResolution const colorRes,
+              scaler **              const scalerPP) {
+
+    scaler * scalerP;
+    unsigned int mapSize;
+    
+    if (UINT_MAX / colorRes.c[RED] / colorRes.c[GRN] / colorRes.c[BLU] < 1)
+        pm_error("red/green/blue dimensions %u/%u/%u is uncomputably large",
+                 colorRes.c[RED], colorRes.c[GRN], colorRes.c[BLU]);
+
+    {
+        unsigned int plane;
+        for (plane = 0, mapSize = 1; plane < 3; ++plane)
+            mapSize *= colorRes.c[plane];
+    }
+    MALLOCVAR_NOFAIL(scalerP);
+
+    scalerP->colorRes = colorRes;
+
+    scalerP->out = allocScalerMap(mapSize);
+
+    if (scalerP->out == NULL)
+        pm_error("Unable to allocate memory for %u colors "
+                 "(%u red x %u green x %u blue)",
+                 mapSize, colorRes.c[RED], colorRes.c[GRN], colorRes.c[BLU]);
+
+    {
+        unsigned int r;
+        for (r = 0; r < colorRes.c[RED]; ++r) {
+            unsigned int g;
+            for (g = 0; g < colorRes.c[GRN]; ++g) {
+                unsigned int b;
+                for (b = 0; b < colorRes.c[BLU]; ++b) {
+                    unsigned int const index =
+                        (r * colorRes.c[GRN] + g)
+                        * colorRes.c[BLU] + b;
+                    tuple const t = scalerP->out[index];
+                         
+                    t[PAM_RED_PLANE] =
+                        r * outputMaxval / (colorRes.c[RED] - 1);
+                    t[PAM_GRN_PLANE] = 
+                        g * outputMaxval / (colorRes.c[GRN] - 1);
+                    t[PAM_BLU_PLANE] =
+                        b * outputMaxval / (colorRes.c[BLU] - 1);
+                }
+            }
+        }
+    }
+    *scalerPP = scalerP;
+}
+
+
+
+static void
+scaler_destroy(scaler * const scalerP) {
+
+    free(scalerP->out);
+
+    free(scalerP);
+}
 
-typedef unsigned char ubyte;
 
-static unsigned int dith_power;     /* base 2 log of dither matrix dimension */
-static unsigned int dith_dim;      	/* dimension of the dither matrix	*/
-static unsigned int dith_dm2;      	/* dith_dim squared				*/
-static unsigned int **dith_mat; 	/* the dithering matrix			*/
-static int debug;
 
-/* COLOR():
- *	returns the index in the colormap for the
- *      r, g, b values specified.
- */
-#define COLOR(r,g,b) (((r) * dith_ng + (g)) * dith_nb + (b))
+static tuple
+scaler_scale(const scaler * const scalerP,
+             unsigned int   const red,
+             unsigned int   const grn,
+             unsigned int   const blu) {
+
+    unsigned int const index =
+        ((red * scalerP->colorRes.c[GRN]) + grn)
+        * scalerP->colorRes.c[BLU] + blu;
+
+    assert(red < scalerP->colorRes.c[RED]);
+    assert(grn < scalerP->colorRes.c[GRN]);
+    assert(blu < scalerP->colorRes.c[BLU]);
+
+    return scalerP->out[index];
+}
 
 
 
 static unsigned int
-dither(pixval const p,
-       pixval const maxval,
+dither(sample       const p,
+       sample       const maxval,
        unsigned int const d,
-       unsigned int const ditheredMaxval) {
+       unsigned int const ditheredMaxval,
+       unsigned int const ditherMatrixArea) {
 /*----------------------------------------------------------------------------
-  Return the dithered intensity for a component of a pixel whose real 
-  intensity for that component is 'p' based on a maxval of 'maxval'.
-  The returned intensity is based on a maxval of ditheredMaxval.
+  Return the dithered brightness for a component of a pixel whose real 
+  brightness for that component is 'p' based on a maxval of 'maxval'.
+  The returned brightness is based on a maxval of ditheredMaxval.
 
   'd' is the entry in the dithering matrix for the position of this pixel
   within the dithered square.
+
+  'ditherMatrixArea' is the area (number of pixels in) the dithered square.
 -----------------------------------------------------------------------------*/
-    unsigned int const ditherSquareMaxval = ditheredMaxval * dith_dm2;
+    unsigned int const ditherSquareMaxval = ditheredMaxval * ditherMatrixArea;
         /* This is the maxval for an intensity that an entire dithered
            square can represent.
         */
-    pixval const pScaled = ditherSquareMaxval * p / maxval;
+    unsigned int const pScaled = ditherSquareMaxval * p / maxval;
         /* This is the input intensity P expressed with a maxval of
            'ditherSquareMaxval'
         */
@@ -64,246 +298,254 @@ dither(pixval const p,
        in the dithered square, as determined by 'd'
     */
 
-    return (pScaled + d) / dith_dm2;
+    return (pScaled + d) / ditherMatrixArea;
 }
 
 
-/* 
- *	Return the value of a dither matrix which is 2**dith_power elements
- *  square at Row x, Column y.
- *	[graphics gems, p. 714]
- */
-static unsigned int
-dith_value(unsigned int y, unsigned int x, const unsigned int dith_power) { 
 
+static unsigned int
+dithValue(unsigned int const yArg,
+          unsigned int const xArg,
+          unsigned int const dithPower) { 
+/*----------------------------------------------------------------------------
+  Return the value of a dither matrix which is 2 ** dithPower elements
+  square at Row x, Column y.
+  [graphics gems, p. 714]
+-----------------------------------------------------------------------------*/
     unsigned int d;
+        /*
+          Think of d as the density. At every iteration, d is shifted
+          left one and a new bit is put in the low bit based on x and y.
+          If x is odd and y is even, or visa versa, then a bit is shifted in.
+          This generates the checkerboard pattern seen in dithering.
+          This quantity is shifted again and the low bit of y is added in.
+          This whole thing interleaves a checkerboard pattern and y's bits
+          which is what you want.
+        */
+    unsigned int x, y;
+    unsigned int i;
 
-    /*
-     * Think of d as the density. At every iteration, d is shifted
-     * left one and a new bit is put in the low bit based on x and y.
-     * If x is odd and y is even, or visa versa, then a bit is shifted in.
-     * This generates the checkerboard pattern seen in dithering.
-     * This quantity is shifted again and the low bit of y is added in.
-     * This whole thing interleaves a checkerboard pattern and y's bits
-     * which is what you want.
-     */
-    int i;
-    for (i = 0, d = 0; i < dith_power; i++, x >>= 1, y >>= 1)
+    for (i = 0, d = 0, x = xArg, y = yArg;
+         i < dithPower;
+         ++i, x >>= 1, y >>= 1)
         d = (d << 2) | (((x & 1) ^ (y & 1)) << 1) | (y & 1);
-    return(d);
-} /* end dith_value */
+
+    return d;
+}
 
 
 
 static unsigned int **
-dith_matrix(unsigned int const dith_dim) {
+dithMatrix(unsigned int const dithPower) {
 /*----------------------------------------------------------------------------
-   Create the dithering matrix for dimension 'dith_dim'.
+   Create the dithering matrix for dimension 'dithDim'.
 
    Return it in newly malloc'ed storage.
 
-   Note that we assume 'dith_dim' is small enough that the dith_mat_sz
+   Note that we assume 'dithPower' is small enough that the 'dithMatSize'
    computed within fits in an int.  Otherwise, results are undefined.
 -----------------------------------------------------------------------------*/
-    unsigned int ** dith_mat;
-    {
-        unsigned int const dith_mat_sz = 
-            (dith_dim * sizeof(int *)) + /* pointers */
-            (dith_dim * dith_dim * sizeof(int)); /* data */
+    unsigned int const dithDim = 1 << dithPower;
+
+    unsigned int ** dithMat;
 
-        dith_mat = (unsigned int **) malloc(dith_mat_sz);
+    assert(dithPower < sizeof(unsigned int) * 8);
 
-        if (dith_mat == NULL) 
+    {
+        unsigned int const dithMatSize = 
+            (dithDim * sizeof(*dithMat)) + /* pointers */
+            (dithDim * dithDim * sizeof(**dithMat)); /* data */
+        
+        dithMat = malloc(dithMatSize);
+        
+        if (dithMat == NULL) 
             pm_error("Out of memory.  "
-                     "Cannot allocate %d bytes for dithering matrix.",
-                     dith_mat_sz);
+                     "Cannot allocate %u bytes for dithering matrix.",
+                     dithMatSize);
     }
     {
-        unsigned int * const dat = (unsigned int *) &dith_mat[dith_dim];
+        unsigned int * const rowStorage = (unsigned int *)&dithMat[dithDim];
         unsigned int y;
-        for (y = 0; y < dith_dim; y++)
-            dith_mat[y] = &dat[y * dith_dim];
+        for (y = 0; y < dithDim; ++y)
+            dithMat[y] = &rowStorage[y * dithDim];
     }
     {
         unsigned int y;
-        for (y = 0; y < dith_dim; y++) {
+        for (y = 0; y < dithDim; ++y) {
             unsigned int x;
-            for (x = 0; x < dith_dim; x++) {
-                dith_mat[y][x] = dith_value(y, x, dith_power);
-                if (debug)
-                    (void) fprintf(stderr, "%4d ", dith_mat[y][x]);
-            }
-            if (debug)
-                (void) fprintf(stderr, "\n");
+            for (x = 0; x < dithDim; ++x)
+                dithMat[y][x] = dithValue(y, x, dithPower);
         }
     }
-    return dith_mat;
+    return dithMat;
 }
 
-    
+
 
 static void
-dith_setup(const unsigned int dith_power, 
-           const unsigned int dith_nr, 
-           const unsigned int dith_ng, 
-           const unsigned int dith_nb, 
-           const pixval output_maxval,
-           pixel ** const colormapP) {
+validateNoDitherOverflow(unsigned int           const ditherMatrixArea,
+                         struct pam *           const inpamP,
+                         struct colorResolution const colorRes) {
 /*----------------------------------------------------------------------------
-   Set up the dithering parameters, color map (lookup table) and
-   dithering matrix.
-
-   Return the colormap in newly malloc'ed storage and return its address
-   as *colormapP.
+   Validate that we'll be able to do the dithering calculations based on
+   the parameters above without busting out of an integer.
 -----------------------------------------------------------------------------*/
-    unsigned int r, g, b;
-
-    if (dith_nr < 2) 
-        pm_error("too few shades for red, minimum of 2");
-    if (dith_ng < 2) 
-        pm_error("too few shades for green, minimum of 2");
-    if (dith_nb < 2) 
-        pm_error("too few shades for blue, minimum of 2");
-
-    MALLOCARRAY(*colormapP, dith_nr * dith_ng * dith_nb);
-    if (*colormapP == NULL) 
-        pm_error("Unable to allocate space for the color lookup table "
-                 "(%d by %d by %d pixels).", dith_nr, dith_ng, dith_nb);
-    
-    for (r = 0; r < dith_nr; r++) 
-        for (g = 0; g < dith_ng; g++) 
-            for (b = 0; b < dith_nb; b++) {
-                PPM_ASSIGN((*colormapP)[COLOR(r,g,b)], 
-                           (r * output_maxval / (dith_nr - 1)),
-                           (g * output_maxval / (dith_ng - 1)),
-                           (b * output_maxval / (dith_nb - 1)));
-            }
-    
-    if (dith_power > MAX_DITH_POWER) {
-        pm_error("Dithering matrix power %d is too large.  Must be <= %d",
-                 dith_power, MAX_DITH_POWER);
-    } else {
-        dith_dim = (1 << dith_power);
-        dith_dm2 = dith_dim * dith_dim;
+    unsigned int maxDitherMaxval;
+    unsigned int plane;
+
+    for (plane = 0, maxDitherMaxval = 1; plane < 0; ++plane) {
+        assert(colorRes.c[plane] >= 2);
+        maxDitherMaxval = MAX(maxDitherMaxval, colorRes.c[plane]-1);
     }
 
-    dith_mat = dith_matrix(dith_dim);
-} /* end dith_setup */
+    if (UINT_MAX / ditherMatrixArea / inpamP->maxval / maxDitherMaxval < 1)
+        pm_error("Numbers are too large to compute.  You must reduce "
+                 "the dither power, the input maxval, or the number of "
+                 "component levels in the output");
+}
+
 
 
-/* 
- *  Dither whole image
- */
 static void
-dith_dither(const unsigned int width, const unsigned int height, 
-            const pixval maxval,
-            const pixel * const colormap, 
-            pixel ** const input, pixel ** const output,
-            const unsigned int dith_nr,
-            const unsigned int dith_ng,
-            const unsigned int dith_nb, 
-            const pixval output_maxval
-            ) {
-
-    const unsigned int dm = (dith_dim - 1);  /* A mask */
-    unsigned int row, col; 
-
-    for (row = 0; row < height; row++)
-        for (col = 0; col < width; col++) {
-            unsigned int const d = dith_mat[row & dm][(width-col-1) & dm];
-            pixel const input_pixel = input[row][col];
-            unsigned int const dithered_r = 
-                dither(PPM_GETR(input_pixel), maxval, d, dith_nr-1);
-            unsigned int const dithered_g = 
-                dither(PPM_GETG(input_pixel), maxval, d, dith_ng-1);
-            unsigned int const dithered_b = 
-                dither(PPM_GETB(input_pixel), maxval, d, dith_nb-1);
-            output[row][col] = 
-                colormap[COLOR(dithered_r, dithered_g, dithered_b)];
-        }
+ditherRow(struct pam *           const inpamP,
+          const tuple *          const inrow,
+          const scaler *         const scalerP,
+          unsigned int **        const ditherMatrix,
+          unsigned int           const ditherMatrixArea,
+          struct colorResolution const colorRes,
+          unsigned int           const row,
+          unsigned int           const modMask,
+          struct pam *           const outpamP,
+          tuple *                const outrow) {
+
+    unsigned int col;
+
+    for (col = 0; col < inpamP->width; ++col) {
+        unsigned int const d =
+            ditherMatrix[row & modMask][(inpamP->width-col-1) & modMask];
+
+        unsigned int dithered[3];
+        unsigned int plane;
+
+        assert(inpamP->depth >= 3);
+
+        for (plane = 0; plane < 3; ++plane)
+            dithered[plane] =
+                dither(inrow[col][plane], inpamP->maxval, d,
+                       colorRes.c[plane]-1, ditherMatrixArea);
+
+        pnm_assigntuple(outpamP,
+                        outrow[col],
+                        scaler_scale(scalerP,
+                                     dithered[PAM_RED_PLANE],
+                                     dithered[PAM_GRN_PLANE],
+                                     dithered[PAM_BLU_PLANE]));
+    }
 }
 
 
+
+static void
+ditherImage(struct pam *           const inpamP,
+            const scaler *         const scalerP,
+            unsigned int           const dithPower,
+            struct colorResolution const colorRes,
+            struct pam *           const outpamP,
+            tuple ***              const outTuplesP) {
+
+    unsigned int const dithDim = 1 << dithPower;
+    unsigned int const ditherMatrixArea = SQR(dithDim);
+
+    unsigned int const modMask = (dithDim - 1);
+       /* And this into N to compute N % dithDim cheaply, since we
+          know (though the compiler doesn't) that dithDim is a power of 2
+       */
+    unsigned int ** const ditherMatrix = dithMatrix(dithPower);
+
+    tuple * inrow;
+    tuple ** outTuples;
+    unsigned int row; 
+    struct pam ditherPam;
+        /* Describes the tuples that ditherRow() sees */
+
+    assert(dithPower < sizeof(unsigned int) * 8);
+    assert(UINT_MAX / dithDim >= dithDim);
+    
+    validateNoDitherOverflow(ditherMatrixArea, inpamP, colorRes);
+
+    inrow = pnm_allocpamrow(inpamP);
+
+    outTuples = pnm_allocpamarray(outpamP);
+
+    /* We will modify the input to promote it to depth 3 */
+    ditherPam = *inpamP;
+    ditherPam.depth = 3;
+
+    for (row = 0; row < inpamP->height; ++row) {
+        pnm_readpamrow(inpamP, inrow);
+
+        pnm_makerowrgb(inpamP, inrow);
+
+        ditherRow(&ditherPam, inrow, scalerP, ditherMatrix, ditherMatrixArea,
+                  colorRes, row, modMask,
+                  outpamP, outTuples[row]);
+    }
+    free(ditherMatrix);
+    pnm_freepamrow(inrow);
+    *outTuplesP = outTuples;
+}
+
+
+
 int
-main( argc, argv )
-    int argc;
-    char* argv[];
-    {
-    FILE* ifp;
-    pixel *colormap;    /* malloc'd */
-    pixel **ipixels;        /* Input image */
-    pixel **opixels;        /* Output image */
-    int cols, rows;
-    pixval maxval;  /* Maxval of the input image */
-    pixval output_maxval;  /* Maxval in the dithered output image */
-    unsigned int argn;
-    const char* const usage = 
-	"[-dim <num>] [-red <num>] [-green <num>] [-blue <num>] [ppmfile]";
-    unsigned int dith_nr; /* number of red shades in output */
-    unsigned int dith_ng; /* number of green shades	in output */
-    unsigned int dith_nb; /* number of blue shades in output */
-
-
-    ppm_init( &argc, argv );
-
-    dith_nr = 5;  /* default */
-    dith_ng = 9;  /* default */
-    dith_nb = 5;  /* default */
-
-    dith_power = 4;  /* default */
-    debug = 0; /* default */
-    argn = 1;
-
-    while ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' )
-	{
-	if ( pm_keymatch( argv[argn], "-dim", 1) &&  argn + 1 < argc ) {
-	    argn++;
-	    if (sscanf(argv[argn], "%u", &dith_power) != 1)
-		pm_usage( usage );
-	}
-	else if ( pm_keymatch( argv[argn], "-red", 1 ) && argn + 1 < argc ) {
-	    argn++;
-	    if (sscanf(argv[argn], "%u", &dith_nr) != 1)
-		pm_usage( usage );
-	}
-	else if ( pm_keymatch( argv[argn], "-green", 1 ) && argn + 1 < argc ) {
-	    argn++;
-	    if (sscanf(argv[argn], "%u", &dith_ng) != 1)
-		pm_usage( usage );
-	}
-	else if ( pm_keymatch( argv[argn], "-blue", 1 ) && argn + 1 < argc ) {
-	    argn++;
-	    if (sscanf(argv[argn], "%u", &dith_nb) != 1)
-		pm_usage( usage );
-	}
-	else if ( pm_keymatch( argv[argn], "-debug", 6 )) {
-        debug = 1;
-	}
-	else
-	    pm_usage( usage );
-	++argn;
-	}
-
-    if ( argn != argc )
-	{
-	ifp = pm_openr( argv[argn] );
-	++argn;
-	}
-    else
-	ifp = stdin;
-
-    if ( argn != argc )
-	pm_usage( usage );
-
-    ipixels = ppm_readppm( ifp, &cols, &rows, &maxval );
-    pm_close( ifp );
-    opixels = ppm_allocarray(cols, rows);
-    output_maxval = pm_lcm(dith_nr-1, dith_ng-1, dith_nb-1, PPM_MAXMAXVAL);
-    dith_setup(dith_power, dith_nr, dith_ng, dith_nb, output_maxval, 
-               &colormap);
-    dith_dither(cols, rows, maxval, colormap, ipixels, opixels,
-                dith_nr, dith_ng, dith_nb, output_maxval);
-    ppm_writeppm(stdout, opixels, cols, rows, output_maxval, 0);
-    pm_close(stdout);
-    exit(0);
+main(int           argc,
+     const char ** argv) {
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    tuple ** outTuples;        /* Output image */
+    scaler * scalerP;
+    struct pam inpam;
+    struct pam outpam;
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(allocation_depth));
+
+    pnm_setminallocationdepth(&inpam, 3);
+    
+    outpam.size               = sizeof(outpam);
+    outpam.len                = PAM_STRUCT_SIZE(tuple_type);
+    outpam.file               = stdout;
+    outpam.width              = inpam.width;
+    outpam.height             = inpam.height;
+    outpam.depth              = 3;
+    outpam.maxval             =
+        pm_lcm(cmdline.colorRes.c[RED]-1,
+               cmdline.colorRes.c[GRN]-1,
+               cmdline.colorRes.c[BLU]-1,
+               PPM_MAXMAXVAL);
+    outpam.bytes_per_sample   = inpam.bytes_per_sample;
+    STRSCPY(outpam.tuple_type, "RGB");
+    outpam.format             = RPPM_FORMAT;
+    outpam.plainformat        = false;
+
+    scaler_create(outpam.maxval, cmdline.colorRes, &scalerP);
+
+    ditherImage(&inpam, scalerP, cmdline.dim, cmdline.colorRes,
+                &outpam, &outTuples);
+
+    pnm_writepam(&outpam, outTuples);
+
+    scaler_destroy(scalerP);
+
+    pnm_freepamarray(outTuples, &outpam);
+
+    pm_close(ifP);
+
+    return 0;
 }
diff --git a/editor/ppmdraw.c b/editor/ppmdraw.c
index 3bd271a6..63d781ec 100644
--- a/editor/ppmdraw.c
+++ b/editor/ppmdraw.c
@@ -1,5 +1,6 @@
-#define _XOPEN_SOURCE    /* Make sure M_PI is in math.h */
-#define _BSD_SOURCE      /* Make sure strdup is in string.h */
+#define _XOPEN_SOURCE 500 
+   /* Make sure M_PI is in math.h, strdup is in string.h */
+#define _BSD_SOURCE      /* Make sure strdup is in string.h (alternate) */
 
 #include <string.h>
 #include <ctype.h>
@@ -44,7 +45,7 @@ struct cmdlineInfo {
 
 
 static void
-parseCommandLine (int argc, char ** argv,
+parseCommandLine (int argc, const char ** argv,
                   struct cmdlineInfo * const cmdlineP) {
 /*----------------------------------------------------------------------------
    parse program command line described in Unix standard form by argc
@@ -57,7 +58,7 @@ parseCommandLine (int argc, char ** argv,
    was passed to us as the argv array.  We also trash *argv.
 -----------------------------------------------------------------------------*/
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -80,7 +81,7 @@ parseCommandLine (int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
     
     if (!scriptSpec && !scriptfileSpec)
@@ -189,6 +190,7 @@ enum drawVerb {
     VERB_LINE_HERE,
     VERB_SPLINE3,
     VERB_CIRCLE,
+    VERB_FILLEDCIRCLE,
     VERB_FILLEDRECTANGLE,
     VERB_TEXT,
     VERB_TEXT_HERE
@@ -288,10 +290,10 @@ freeDrawCommand(const struct drawCommand * const commandP) {
     case VERB_SETLINECLIP:
         break;
     case VERB_SETCOLOR:
-        strfree(commandP->u.setcolorArg.colorName);
+        pm_strfree(commandP->u.setcolorArg.colorName);
         break;
     case VERB_SETFONT:
-        strfree(commandP->u.setfontArg.fontFileName);
+        pm_strfree(commandP->u.setfontArg.fontFileName);
         break;
     case VERB_LINE:
         break;
@@ -301,11 +303,13 @@ freeDrawCommand(const struct drawCommand * const commandP) {
         break;
     case VERB_CIRCLE:
         break;
+    case VERB_FILLEDCIRCLE:
+        break;
     case VERB_FILLEDRECTANGLE:
         break;
     case VERB_TEXT:
     case VERB_TEXT_HERE:
-        strfree(commandP->u.textArg.text);
+        pm_strfree(commandP->u.textArg.text);
         break;
     }
     
@@ -346,6 +350,35 @@ freeScript(struct script * const scriptP) {
 
 
 static void
+doFilledCircle(pixel **                   const pixels,
+               unsigned int               const cols,
+               unsigned int               const rows,
+               pixval                     const maxval,
+               const struct drawCommand * const commandP,
+               const struct drawState *   const drawStateP) {
+
+    struct fillobj * fhP;
+
+    fhP = ppmd_fill_create();
+            
+    ppmd_circle(pixels, cols, rows, maxval,
+                commandP->u.circleArg.cx,
+                commandP->u.circleArg.cy,
+                commandP->u.circleArg.radius,
+                ppmd_fill_drawproc,
+                fhP);
+            
+    ppmd_fill(pixels, cols, rows, maxval,
+              fhP,
+              PPMD_NULLDRAWPROC,
+              &drawStateP->color);
+
+    ppmd_fill_destroy(fhP);
+} 
+
+
+
+static void
 doTextHere(pixel **                   const pixels,
            unsigned int               const cols,
            unsigned int               const rows,
@@ -463,6 +496,9 @@ executeScript(struct script * const scriptP,
                         PPMD_NULLDRAWPROC,
                         &drawState.color);
             break;
+        case VERB_FILLEDCIRCLE:
+            doFilledCircle(pixels, cols, rows, maxval, commandP, &drawState);
+            break;
         case VERB_FILLEDRECTANGLE:
             ppmd_filledrectangle(pixels, cols, rows, maxval,
                                  commandP->u.filledrectangleArg.x,
@@ -611,6 +647,17 @@ parseDrawCommand(struct tokenSet             const commandTokens,
                 argP->cy     = atoi(commandTokens.token[2]);
                 argP->radius = atoi(commandTokens.token[3]);
             } 
+        } else if (streq(verb, "filledcircle")) {
+            drawCommandP->verb = VERB_FILLEDCIRCLE;
+            if (commandTokens.count < 4)
+                pm_error("Not enough tokens for a 'filledcircle' command.  "
+                         "Need %u.  Got %u", 4, commandTokens.count);
+            else {
+                struct circleArg * const argP = &drawCommandP->u.circleArg;
+                argP->cx     = atoi(commandTokens.token[1]);
+                argP->cy     = atoi(commandTokens.token[2]);
+                argP->radius = atoi(commandTokens.token[3]);
+            } 
         } else if (streq(verb, "filledrectangle")) {
             drawCommandP->verb = VERB_FILLEDRECTANGLE;
             if (commandTokens.count < 5)
@@ -677,7 +724,7 @@ disposeOfCommandTokens(struct tokenSet * const tokenSetP,
     {
         unsigned int i;
         for (i = 0; i < tokenSetP->count; ++i)
-            strfree(tokenSetP->token[i]);
+            pm_strfree(tokenSetP->token[i]);
         tokenSetP->count = 0;
     }
     /* Put the list element for this command at the tail of the list */
@@ -828,7 +875,7 @@ getScript(struct cmdlineInfo const cmdline,
 
     parseScript(scriptText, scriptPP);
 
-    strfree(scriptText);
+    pm_strfree(scriptText);
 }
 
           
@@ -853,14 +900,14 @@ doOneImage(FILE *          const ifP,
 
 
 int
-main(int argc, char * argv[]) {
+main(int argc, const char * argv[]) {
 
     struct cmdlineInfo cmdline;
     FILE * ifP;
     struct script * scriptP;
-    bool eof;
+    int eof;
 
-    ppm_init(&argc, argv);
+    pm_proginit(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
 
diff --git a/editor/ppmfade b/editor/ppmfade
index fbc62968..027fc793 100755
--- a/editor/ppmfade
+++ b/editor/ppmfade
@@ -1,5 +1,31 @@
-#!/usr/bin/perl -w
-#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+#!/bin/sh
+
+##############################################################################
+# This is essentially a Perl program.  We exec the Perl interpreter specifying
+# this same file as the Perl program and use the -x option to cause the Perl
+# interpreter to skip down to the Perl code.  The reason we do this instead of
+# just making /usr/bin/perl the script interpreter (instead of /bin/sh) is
+# that the user may have multiple Perl interpreters and the one he wants to
+# use is properly located in the PATH.  The user's choice of Perl interpreter
+# may be crucial, such as when the user also has a PERL5LIB environment
+# variable and it selects modules that work with only a certain main
+# interpreter program.
+#
+# An alternative some people use is to have /usr/bin/env as the script
+# interpreter.  We don't do that because we think the existence and
+# compatibility of /bin/sh is more reliable.
+#
+# Note that we aren't concerned about efficiency because the user who needs
+# high efficiency can use directly the programs that this program invokes.
+#
+##############################################################################
+
+exec perl -w -x -S -- "$0" "$@"
+
+#!/usr/bin/perl
+##############################################################################
+#                                  ppmfade
+##############################################################################
 #
 #  This program creates a fade (a sequence of frames) between two images.
 #
@@ -12,7 +38,7 @@
 #  much the same thing, but handles non-Netpbm formats too, and is 
 #  implemented in a more primitive language.
 #
-#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+##############################################################################
 use strict;
 
 my $SPREAD =  1;
diff --git a/editor/ppmmix.c b/editor/ppmmix.c
index 5306d1cf..f4678a4b 100644
--- a/editor/ppmmix.c
+++ b/editor/ppmmix.c
@@ -1,131 +1,96 @@
-
 /*********************************************************************/
 /* ppmmix -  mix together two pictures like with a fader             */
 /* Frank Neumann, October 1993                                       */
 /* V1.2 16.11.1993                                                   */
 /*                                                                   */
-/* version history:                                                  */
-/* V1.0 Aug   1993    first version                                  */
-/* V1.1 12.10.1993    uses ppm libs&headers, integer math, cleanups  */
-/* V1.2 16.11.1993    Rewritten to be NetPBM.programming conforming  */
 /*********************************************************************/
 
 #include "ppm.h"
 
-/* global variables */
-#ifdef AMIGA
-static char *version = "$VER: ppmmix 1.2 (16.11.93)"; /* Amiga version identification */
-#endif
-
-/**************************/
-/* start of main function */
-/**************************/
-int main(argc, argv)
-int argc;
-char *argv[];
-{
-	FILE *ifp1, *ifp2;
-	int argn, rows, cols, format, i = 0, j = 0;
-	int rows2, cols2, format2;
-	pixel *srcrow1, *srcrow2, *destrow;
-	pixel *pP1, *pP2, *pP3;
-	pixval maxval, maxval2;
-	pixval r1, r2, r3, g1, g2, g3, b1, b2, b3;
-	double fadefactor;
-	long longfactor;
-	const char * const usage = "fadefactor ppmfile1 ppmfile2\n        fadefactor: 0.0 = only ppmfile1, 1.0 = only ppmfile2\n";
-
-	/* parse in 'default' parameters */
-	ppm_init(&argc, argv);
-
-	argn = 1;
-
-	/* parse in dim factor */
-	if (argn == argc)
-		pm_usage(usage);
-	if (sscanf(argv[argn], "%lf", &fadefactor) != 1)
-		pm_usage(usage);
-	if (fadefactor < 0.0 || fadefactor > 1.0)
-		pm_error("fade factor must be in the range from 0.0 to 1.0 ");
-	++argn;
-
-	/* parse in filenames and open files (cannot be stdin-filters, sorry..) */
-	if (argn == argc-2)
-	{
-		ifp1 = pm_openr(argv[argn]);
-		++argn;
-		ifp2 = pm_openr(argv[argn]);
-	}
-	else
-		pm_usage(usage);
-
-	/* read first data from both files and compare sizes etc. */
-	ppm_readppminit(ifp1, &cols, &rows, &maxval, &format);
-	ppm_readppminit(ifp2, &cols2, &rows2, &maxval2, &format2);
-
-    if ( (cols != cols2) || (rows != rows2) )
+int main(int argc, const char ** argv) {
+
+    FILE * if1P;
+    FILE * if2P;
+    int argn;
+    int rows1, cols1, format1;
+    unsigned int row;
+    int rows2, cols2, format2;
+    pixel *srcrow1, *srcrow2, *destrow;
+    pixval maxval1, maxval2;
+    double fadefactor;
+    long longfactor;
+    const char * const usage = "fadefactor ppmfile1 ppmfile2\n        fadefactor: 0.0 = only ppmfile1, 1.0 = only ppmfile2\n";
+
+    /* parse in 'default' parameters */
+    pm_proginit(&argc, argv);
+
+    argn = 1;
+
+    /* parse in dim factor */
+    if (argn == argc)
+        pm_usage(usage);
+    if (sscanf(argv[argn], "%lf", &fadefactor) != 1)
+        pm_usage(usage);
+    if (fadefactor < 0.0 || fadefactor > 1.0)
+        pm_error("fade factor must be in the range from 0.0 to 1.0 ");
+    ++argn;
+
+    if (argn == argc-2) {
+        if1P = pm_openr(argv[argn]);
+        ++argn;
+        if2P = pm_openr(argv[argn]);
+    } else
+        pm_usage(usage);
+
+    ppm_readppminit(if1P, &cols1, &rows1, &maxval1, &format1);
+    ppm_readppminit(if2P, &cols2, &rows2, &maxval2, &format2);
+
+    if ((cols1 != cols2) || (rows1 != rows2))
         pm_error("image sizes are different!");
 
-    if ( maxval != maxval2)
-		pm_error("images have different maxvalues");
-
-	if (format != format2)
-	{
-		pm_error("images have different PxM types");
-	}
-
-	/* no error checking required here, ppmlib does it all for us */
-	srcrow1 = ppm_allocrow(cols);
-	srcrow2 = ppm_allocrow(cols);
+    if (maxval1 != maxval2)
+        pm_error("images have different maxvalues");
 
-	longfactor = (long)(fadefactor * 65536);
+    srcrow1 = ppm_allocrow(cols1);
+    srcrow2 = ppm_allocrow(cols2);
 
-	/* allocate a row of pixel data for the new pixels */
-	destrow = ppm_allocrow(cols);
+    longfactor = (long)(fadefactor * 65536);
 
-	ppm_writeppminit(stdout, cols, rows, maxval, 0);
+    destrow = ppm_allocrow(cols1);
 
-	for (i = 0; i < rows; i++)
-	{
-		ppm_readppmrow(ifp1, srcrow1, cols, maxval, format);
-		ppm_readppmrow(ifp2, srcrow2, cols, maxval, format);
+    ppm_writeppminit(stdout, cols1, rows1, maxval1, 0);
 
-		pP1 = srcrow1;
-		pP2 = srcrow2;
-        pP3 = destrow;
+    for (row = 0; row < rows1; ++row) {
+        unsigned int col;
+        ppm_readppmrow(if1P, srcrow1, cols1, maxval1, format1);
+        ppm_readppmrow(if2P, srcrow2, cols2, maxval2, format2);
 
-		for (j = 0; j < cols; j++)
-		{
-			r1 = PPM_GETR(*pP1);
-			g1 = PPM_GETG(*pP1);
-			b1 = PPM_GETB(*pP1);
+        for (col = 0; col < cols1; ++col) {
+            pixel const p1 = srcrow1[col];
+            pixval const r1 = PPM_GETR(p1);
+            pixval const g1 = PPM_GETG(p1);
+            pixval const b1 = PPM_GETB(p1);
 
-			r2 = PPM_GETR(*pP2);
-			g2 = PPM_GETG(*pP2);
-			b2 = PPM_GETB(*pP2);
+            pixel const p2 = srcrow2[col];
+            pixval const r2 = PPM_GETR(p2);
+            pixval const g2 = PPM_GETG(p2);
+            pixval const b2 = PPM_GETB(p2);
 
-			r3 = r1 + (((r2 - r1) * longfactor) >> 16);
-			g3 = g1 + (((g2 - g1) * longfactor) >> 16);
-			b3 = b1 + (((b2 - b1) * longfactor) >> 16);
+            pixval const r = r1 + (((r2 - r1) * longfactor) >> 16);
+            pixval const g = g1 + (((g2 - g1) * longfactor) >> 16);
+            pixval const b = b1 + (((b2 - b1) * longfactor) >> 16);
 
+            PPM_ASSIGN(destrow[col], r, g, b);
+        }
 
-			PPM_ASSIGN(*pP3, r3, g3, b3);
+        ppm_writeppmrow(stdout, destrow, cols1, maxval1, 0);
+    }
 
-			pP1++;
-			pP2++;
-			pP3++;
-		}
+    pm_close(if1P);
+    pm_close(if2P);
+    ppm_freerow(srcrow1);
+    ppm_freerow(srcrow2);
+    ppm_freerow(destrow);
 
-		/* write out one line of graphic data */
-		ppm_writeppmrow(stdout, destrow, cols, maxval, 0);
-	}
-
-	pm_close(ifp1);
-	pm_close(ifp2);
-	ppm_freerow(srcrow1);
-	ppm_freerow(srcrow2);
-	ppm_freerow(destrow);
-
-	exit(0);
+    return 0;
 }
-
diff --git a/editor/ppmquant b/editor/ppmquant
index 11bce6d2..57963982 100755
--- a/editor/ppmquant
+++ b/editor/ppmquant
@@ -1,4 +1,28 @@
-#!/usr/bin/perl -w
+#!/bin/sh
+
+##############################################################################
+# This is essentially a Perl program.  We exec the Perl interpreter specifying
+# this same file as the Perl program and use the -x option to cause the Perl
+# interpreter to skip down to the Perl code.  The reason we do this instead of
+# just making /usr/bin/perl the script interpreter (instead of /bin/sh) is
+# that the user may have multiple Perl interpreters and the one he wants to
+# use is properly located in the PATH.  The user's choice of Perl interpreter
+# may be crucial, such as when the user also has a PERL5LIB environment
+# variable and it selects modules that work with only a certain main
+# interpreter program.
+#
+# An alternative some people use is to have /usr/bin/env as the script
+# interpreter.  We don't do that because we think the existence and
+# compatibility of /bin/sh is more reliable.
+#
+# Note that we aren't concerned about efficiency because the user who needs
+# high efficiency can use directly the programs that this program invokes.
+#
+##############################################################################
+
+exec perl -w -x -S -- "$0" "$@"
+
+#!/usr/bin/perl
 ##############################################################################
 #  This is nothing but a compatibility interface for Pnmquant.
 #  An old program coded to call Ppmquant will continue working because
@@ -13,8 +37,6 @@ use strict;
 
 use Getopt::Long;
 
-my $TRUE=1; my $FALSE = 0;
-
 my @ppmquantArgv = @ARGV;
 
 Getopt::Long::Configure('pass_through');
diff --git a/editor/ppmquantall b/editor/ppmquantall
deleted file mode 100755
index 4807de8c..00000000
--- a/editor/ppmquantall
+++ /dev/null
@@ -1,96 +0,0 @@
-#!/bin/sh
-#
-# ppmquantall - run ppmquant on a bunch of files all at once, so they share
-#               a common colormap
-#
-# WARNING: overwrites the source files with the results!!!
-#
-# Verbose explanation: Let's say you've got a dozen pixmaps that you want
-# to display on the screen all at the same time.  Your screen can only
-# display 256 different colors, but the pixmaps have a total of a thousand
-# or so different colors.  For a single pixmap you solve this problem with
-# pnmquant; this script solves it for multiple pixmaps.  All it does is
-# concatenate them together into one big pixmap, run pnmquant on that, and
-# then split it up into little pixmaps again.
-#
-# IMPLEMENTATION NOTE:  Now that Pnmcolormap can compute a single colormap
-# for a whole stream of images, this program could be implemented more
-# simply.  Today, it concatenates a bunch of images into one image, uses
-# Pnmquant to quantize that, then splits the result back into multiple
-# images.  It could instead just run Pnmcolormap over all the images,
-# then run Pnmremap on each input image using the one colormap for all.
-
-usage()
-{
-    echo "usage: $0 [-ext extension] <newcolors> <ppmfile> ..."
-    exit 1
-}
-
-ext=
-
-while :; do
-
-    case "$1" in
-    -ext*)
-        if [ $# -lt 2 ]; then
-            usage
-        fi
-        ext=".$2"
-        shift
-        shift
-    ;;
-
-    *)  
-        break
-    ;;
-
-    esac
-done
-
-if [ $# -lt 2 ]; then
-    usage
-fi
-
-newcolors=$1
-shift
-nfiles=$#
-files=($@)
-
-# Extract the width and height of each of the images.
-# Here, we make the assumption that the width and height are on the
-# second line, even though the PPM format doesn't require that.
-# To be robust, we need to use Pnmfile to get that information, or 
-# Put this program in C and use ppm_readppminit().
-
-widths=()
-heights=()
-
-for i in ${files[@]}; do
-    widths=(${widths[*]} `grep -v '^#' $i | sed '1d; s/ .*//; 2q'`)
-    heights=(${heights[*]} `grep -v '^#' $i | sed '1d; s/.* //; 2q'`)
-done
-
-tempdir="${TMPDIR-/tmp}/ppmquantall.$$"
-mkdir -m 0700 $tempdir || \
-  { echo "Could not create temporary file. Exiting."; exit 1;}
-trap 'rm -rf $tempdir' 0 1 3 15
-
-all=$tempdir/pqa.all.$$
-
-pnmcat -topbottom -jleft -white ${files[@]} | pnmquant $newcolors > $all
-if [ $? != 0 ]; then
-    exit $?
-fi
-
-y=0
-i=0
-
-while [ $i -lt $nfiles ]; do
-    pamcut -left 0 -top $y -width ${widths[$i]} -height ${heights[$i]} $all \
-        > ${files[$i]}$ext
-    if [ $? != 0 ]; then
-        exit $?
-    fi
-    y=$(($y + ${heights[$i]}))
-    i=$(($i + 1))
-done
diff --git a/editor/ppmquantall.csh b/editor/ppmquantall.csh
deleted file mode 100644
index 9a89bca0..00000000
--- a/editor/ppmquantall.csh
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/bin/csh -f
-#
-# ppmquantall - run ppmquant on a bunch of files all at once, so they share
-#               a common colormap
-#
-# WARNING: overwrites the source files with the results!!!
-#
-# Verbose explanation: Let's say you've got a dozen pixmaps that you want
-# to display on the screen all at the same time.  Your screen can only
-# display 256 different colors, but the pixmaps have a total of a thousand
-# or so different colors.  For a single pixmap you solve this problem with
-# ppmquant; this script solves it for multiple pixmaps.  All it does is
-# concatenate them together into one big pixmap, run ppmquant on that, and
-# then split it up into little pixmaps again.
-
-if ( $#argv < 3 ) then
-    echo "usage:  ppmquantall <newcolors> <ppmfile> <ppmfile> ..."
-    exit 1
-endif
-
-set newcolors=$argv[1]
-set files=( $argv[2-] )
-
-# Extract the width and height of each of the images.
-# Here, we make the assumption that the width and height are on the
-# second line, even though the PPM format doesn't require that.
-# To be robust, we need to use Pnmfile to get that information, or 
-# Put this program in C and use ppm_readppminit().
-
-set widths=()
-set heights=()
-foreach i ( $files )
-    set widths=( $widths `sed '1d; s/ .*//; 2q' $i` )
-    set heights=( $heights `sed '1d; s/.* //; 2q' $i` )
-end
-
-set all=/tmp/pqa.all.$$
-rm -f $all
-pnmcat -topbottom -jleft -white $files | ppmquant -quiet $newcolors > $all
-if ( $status != 0 ) exit $status
-
-@ y = 0
-@ i = 1
-while ( $i <= $#files )
-    pnmcut -left 0 -top $y -width $widths[$i] -height $heights[$i] $all \
-       > $files[$i]
-    if ( $status != 0 ) exit $status
-    @ y = $y + $heights[$i]
-    @ i++
-end
-
-rm -f $all
-
-
-
-
-
diff --git a/editor/ppmshadow b/editor/ppmshadow
index 2a32fca0..62cdf8b8 100755
--- a/editor/ppmshadow
+++ b/editor/ppmshadow
@@ -1,15 +1,40 @@
-#!/usr/bin/perl -w
+#!/bin/sh
 
-#                         P P M S H A D O W
+##############################################################################
+# This is essentially a Perl program.  We exec the Perl interpreter specifying
+# this same file as the Perl program and use the -x option to cause the Perl
+# interpreter to skip down to the Perl code.  The reason we do this instead of
+# just making /usr/bin/perl the script interpreter (instead of /bin/sh) is
+# that the user may have multiple Perl interpreters and the one he wants to
+# use is properly located in the PATH.  The user's choice of Perl interpreter
+# may be crucial, such as when the user also has a PERL5LIB environment
+# variable and it selects modules that work with only a certain main
+# interpreter program.
+#
+# An alternative some people use is to have /usr/bin/env as the script
+# interpreter.  We don't do that because we think the existence and
+# compatibility of /bin/sh is more reliable.
+#
+# Note that we aren't concerned about efficiency because the user who needs
+# high efficiency can use directly the programs that this program invokes.
+#
+##############################################################################
+
+exec perl -w -x -S -- "$0" "$@"
 
+#!/usr/bin/perl
+##############################################################################
+#                              ppmshadow
+##############################################################################
+#
 #            by John Walker  --  http://www.fourmilab.ch/
 #                          version = 1.2;
 #   --> with minor changes by Bryan Henderson to adapt to Netbpm.  
 #   See above web site for the real John Walker work, named pnmshadow.
-
+#
 #   Bryan Henderson later made some major style changes (use strict, etc) and
 #   eliminated most use of shells.  See Netbpm HISTORY file.
-
+#
 #   Pnmshadow is a brutal sledgehammer implemented in Perl which
 #   adds attractive shadows to images, as often seen in titles
 #   of World-Wide Web pages.  This program does not actually
@@ -20,7 +45,7 @@
 #
 #               This program is in the public domain.
 #
-#
+##############################################################################
 
 use strict;
 require 5.0;
diff --git a/editor/specialty/Makefile b/editor/specialty/Makefile
index 76befbb4..427c2c8f 100644
--- a/editor/specialty/Makefile
+++ b/editor/specialty/Makefile
@@ -11,11 +11,13 @@ PORTBINARIES = pamdeinterlace \
 	       pammixinterlace \
 	       pamoil \
 	       pampop9 \
+	       pampaintspill \
 	       pbmlife \
 	       pgmabel \
 	       pgmbentley \
 	       pgmmorphconv \
 	       pnmindex \
+	       pnmmercator \
 	       ppm3d \
 	       ppmglobe \
 	       ppmntsc \
diff --git a/editor/specialty/pamdeinterlace.c b/editor/specialty/pamdeinterlace.c
index 7c6b123c..d6f6aee1 100644
--- a/editor/specialty/pamdeinterlace.c
+++ b/editor/specialty/pamdeinterlace.c
@@ -47,7 +47,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     free(option_def);
diff --git a/editor/specialty/pammixinterlace.c b/editor/specialty/pammixinterlace.c
index 9f98b406..7410a8f1 100644
--- a/editor/specialty/pammixinterlace.c
+++ b/editor/specialty/pammixinterlace.c
@@ -200,7 +200,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
     /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (!filterSpec)
diff --git a/editor/specialty/pampaintspill.c b/editor/specialty/pampaintspill.c
new file mode 100644
index 00000000..745c9b68
--- /dev/null
+++ b/editor/specialty/pampaintspill.c
@@ -0,0 +1,500 @@
+/* ----------------------------------------------------------------------
+ *
+ * Bleed colors from non-background colors into the background
+ *
+ * By Scott Pakin <scott+pbm@pakin.org>
+ *
+ * ----------------------------------------------------------------------
+ *
+ * Copyright (C) 2010 Scott Pakin <scott+pbm@pakin.org>
+ *
+ * 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 3 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, see http://www.gnu.org/licenses/.
+ *
+ * ----------------------------------------------------------------------
+ */
+
+/*
+  This program contains code to work with Openmp, so that it can process
+  multiple columns at once, using multiple threads on multiple CPU cores,
+  and thus take less elapsed time to run.
+
+  But that code is dead in a normal Netpbm build, as it does not use the
+  required compiler options or link with the required library in any
+  conventional environment we know of.  One can exploit this code with a
+  modified build, e.g. with CADD and LADD make variables.
+
+  10.04.14
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <math.h>
+#include <time.h>
+
+#include "mallocvar.h"
+#include "nstring.h"
+#include "shhopt.h"
+#include "pam.h"
+#include "pammap.h"
+
+
+static time_t const timeUpdateDelta = 30;
+    /* Seconds between progress updates */
+static int const    minUpdates = 4;
+    /* Minimum number of progress updates to output */
+
+
+struct cmdlineInfo {
+    /* This structure represents all of the information the user
+       supplied in the command line but in a form that's easy for the
+       program to use.
+    */
+    const char * inputFilename;  /* '-' if stdin */
+    const char * bgcolor;
+    unsigned int wrap;
+    unsigned int all;
+    float        power;
+    unsigned int downsample;
+};
+
+struct coords {
+    /* This structure represents an (x,y) coordinate within an image and
+       the color at that coordinate. */
+    unsigned int x;
+    unsigned int y;
+    tuple        color;
+};
+
+typedef double distFunc_t(const struct coords * const p0,
+                          const struct coords * const p1,
+                          unsigned int          const width,
+                          unsigned int          const height);
+    /* Distance function */
+
+
+
+static void
+parseCommandLine(int argc, const char ** const argv,
+                 struct cmdlineInfo * const cmdlineP ) {
+
+    optEntry     * option_def;
+        /* Instructions to OptParseOptions3 on how to parse our options */
+    optStruct3     opt;
+    unsigned int   option_def_index;
+    unsigned int bgcolorSpec, powerSpec, downsampleSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+    option_def_index = 0;          /* Incremented by OPTENTRY */
+
+    OPTENT3(0, "bgcolor",    OPT_STRING, &cmdlineP->bgcolor,    
+            &bgcolorSpec, 0);
+    OPTENT3(0, "wrap",       OPT_FLAG,   NULL,
+            &cmdlineP->wrap,       0);
+    OPTENT3(0, "all",        OPT_FLAG,   NULL,
+            &cmdlineP->all,        0);
+    OPTENT3(0, "power",      OPT_FLOAT,  &cmdlineP->power,      
+            &powerSpec, 0);
+    OPTENT3(0, "downsample", OPT_UINT,   &cmdlineP->downsample, 
+            &downsampleSpec, 0);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = 0;
+    opt.allowNegNum = 1;
+
+    pm_optParseOptions3( &argc, (char **)argv, opt, sizeof(opt), 0 );
+
+    if (!bgcolorSpec)
+        cmdlineP->bgcolor = NULL;
+
+    if (!powerSpec)
+        cmdlineP->power = -2.0;
+
+    if (!downsampleSpec)
+        cmdlineP->downsample = 0;
+
+    if (argc-1 < 1)
+        cmdlineP->inputFilename = "-";
+    else {
+        cmdlineP->inputFilename = argv[1];
+        if (argc-1 > 1)
+            pm_error("Too many arguments: %u.  The only argument is the "
+                     "optional input file name", argc-1);
+    }
+}
+
+
+static bool
+tupleEqualColor(const struct pam * const pamP,
+                tuple              const comparand,
+                tuple              const comparator) {
+/*----------------------------------------------------------------------------
+  Report whether two tuples have equal color, regardless of alpha.
+----------------------------------------------------------------------------*/
+    unsigned int const nColorPlanes = pamP->depth >= 3 ? 3 : 1;
+
+    unsigned int plane;
+
+    for (plane = 0; plane < nColorPlanes; ++plane)
+        if (comparand[plane] != comparator[plane])
+            return false;
+
+    return true;
+}
+
+
+
+struct paintSourceSet {
+    struct coords * list;  /* malloc'ed */
+        /* The list of places in the image from which paint comes */
+    unsigned int size;
+        /* Number of entries in sources[] */
+    unsigned int alloc;
+        /* Number of slots for entries of allocated memory */
+};
+
+
+
+static void
+setPaintSourceColors(struct pam *          const pamP,
+                     tuple **              const tuples,
+                     struct paintSourceSet const paintSources) {
+/*----------------------------------------------------------------------------
+   Set the 'color' member of each source in 'paintSources'.
+
+   Set it to the color of the source pixel in tuples[][], indicated by
+   'paintSources'.
+
+   Malloc memory to store these colors -- a contiguous block of memory for all
+   of them.
+-----------------------------------------------------------------------------*/
+    struct pam pamPaint;
+        /* numPaintSources-wide PAM for use by pnm_allocpamrow() */
+    tuple * paintColor;
+        /* Points to storage for the color tuples */
+    unsigned int    i;
+
+    pamPaint = *pamP;
+    pamPaint.width = paintSources.size;
+    paintColor = pnm_allocpamrow(&pamPaint);
+
+    for (i = 0; i < paintSources.size; ++i) {
+        struct coords * const thisSourceP = &paintSources.list[i];
+
+        thisSourceP->color = paintColor[i];
+        pnm_assigntuple(pamP, thisSourceP->color,
+                        tuples[thisSourceP->y][thisSourceP->x]);
+    }
+}
+
+
+
+static void
+addPaintSource(unsigned int            const row,
+               unsigned int            const col,
+               struct paintSourceSet * const paintSourcesP) {
+
+    if (paintSourcesP->size == paintSourcesP->alloc) {
+        paintSourcesP->alloc += 1024;
+        REALLOCARRAY(paintSourcesP->list, paintSourcesP->alloc);
+        if (!paintSourcesP->list)
+            pm_error("Out of memory");
+    }
+    paintSourcesP->list[paintSourcesP->size].x = col;
+    paintSourcesP->list[paintSourcesP->size].y = row;
+    ++paintSourcesP->size;
+}
+
+
+
+static void
+locatePaintSources(struct pam *            const pamP,
+                   tuple **                const tuples,
+                   tuple                   const bgColor,
+                   unsigned int            const downsample,
+                   struct paintSourceSet * const paintSourcesP) {
+/*--------------------------------------------------------------------
+  Construct a list of all pixel coordinates in the input image that
+  represent a non-background color.
+  ----------------------------------------------------------------------*/
+    struct paintSourceSet paintSources;
+    int row;  /* signed so it works with Openmp */
+
+    paintSources.list  = NULL;
+    paintSources.size  = 0;
+    paintSources.alloc = 0;
+
+    #pragma omp parallel for
+    for (row = 0; row < pamP->height; ++row) {
+        unsigned int col;
+        for (col = 0; col < pamP->width; ++col) {
+            if (!tupleEqualColor(pamP, tuples[row][col], bgColor))
+                #pragma omp critical (addPaintSource)
+                addPaintSource(row, col, &paintSources);
+        }
+    }
+
+    pm_message("Image contains %u background + %u non-background pixels",
+               pamP->width * pamP->height - paintSources.size,
+               paintSources.size);
+    
+    /* Reduce the number of paint sources to reduce execution time. */
+    if (downsample > 0 && downsample < paintSources.size) {
+        unsigned int i;
+
+        srand(time(NULL));
+
+        for (i = 0; i < downsample; ++i) {
+            unsigned int const swapIdx =
+                i + rand() % (paintSources.size - i);
+            struct coords const swapVal = paintSources.list[i];
+
+            paintSources.list[i] = paintSources.list[swapIdx];
+            paintSources.list[swapIdx] = swapVal;
+        }
+        paintSources.size = downsample;
+    }
+
+    setPaintSourceColors(pamP, tuples, paintSources);
+
+    *paintSourcesP    = paintSources;
+}
+
+
+
+static distFunc_t euclideanDistanceSqr;
+
+static double
+euclideanDistanceSqr(const struct coords * const p0,
+                     const struct coords * const p1,
+                     unsigned int          const width,
+                     unsigned int          const height) {
+/*----------------------------------------------------------------------------
+   Return the square of the Euclidian distance between p0 and p1.
+-----------------------------------------------------------------------------*/
+    double const deltax = (double) (int) (p1->x - p0->x);
+    double const deltay = (double) (int) (p1->y - p0->y);
+
+    return SQR(deltax) + SQR(deltay);
+}
+
+
+
+static distFunc_t euclideanDistanceTorusSqr;
+
+static double
+euclideanDistanceTorusSqr(const struct coords * const p0,
+                          const struct coords * const p1,
+                          unsigned int          const width,
+                          unsigned int          const height) {
+/*----------------------------------------------------------------------------
+   Return the square of the Euclidian distance between p0 and p1, assuming
+   it's a toroidal surface on which the top row curves around to meet the
+   bottom and the left column to the right.
+-----------------------------------------------------------------------------*/
+    struct coords p0Adj, p1Adj;
+
+    if (p1->x >= p0->x + width / 2) {
+        p0Adj.x = p0->x + width;
+        p1Adj.x = p1->x;
+    } else if (p0->x >= p1->x + width / 2) {
+        p0Adj.x = p0->x;
+        p1Adj.x = p1->x + width;
+    } else {
+        p0Adj.x = p0->x;
+        p1Adj.x = p1->x;
+    }
+    if (p1->y >= p0->y + height / 2) {
+        p0Adj.y = p0->y + height;
+        p1Adj.y = p1->y;
+    } else if (p0->y >= p1->y + height / 2) {
+        p0Adj.y = p0->y;
+        p1Adj.y = p1->y + height;
+    } else {
+        p0Adj.y = p0->y;
+        p1Adj.y = p1->y;
+    }
+
+    return euclideanDistanceSqr(&p0Adj, &p1Adj, 0, 0);
+}
+
+
+
+static void
+reportProgress(unsigned int const rowsComplete,
+               unsigned int const height) {
+
+    static time_t prevOutputTime = 0;
+    time_t        now;                  /* Current time in seconds */
+
+    if (prevOutputTime == 0)
+        prevOutputTime = time(NULL);
+
+    /* Output our progress only every timeUpdateDelta seconds. */
+    now = time(NULL);
+    if (prevOutputTime) {
+        if (now - prevOutputTime >= timeUpdateDelta
+            || rowsComplete % (height/minUpdates) == 0) {
+            pm_message("%5.1f%% complete",
+                       rowsComplete * 100.0 / height);
+            prevOutputTime = now;
+        }
+    } else
+        prevOutputTime = now;
+}
+
+
+
+static void
+spillOnePixel(struct pam *          const pamP,
+              struct coords         const target,
+              struct paintSourceSet const paintSources,
+              distFunc_t *          const distFunc,
+              double                const distPower,
+              tuple                 const outTuple,
+              double *              const newColor) {
+
+    unsigned int plane;
+    unsigned int ps;
+    double       totalWeight;
+
+    for (plane = 0; plane < pamP->depth; ++plane)
+        newColor[plane] = 0.0;
+    totalWeight = 0.0;
+    for (ps = 0; ps < paintSources.size; ++ps) {
+        struct coords const source = paintSources.list[ps];
+        double const distSqr =
+            (*distFunc)(&target, &source,
+                        pamP->width, pamP->height);
+
+        if (distSqr > 0.0) {
+            /* We do special cases for some common cases with code
+               that is much faster than pow().
+            */
+            double const weight =
+                distPower == -2.0 ? 1.0 / distSqr :
+                distPower == -1.0 ? 1.0 / sqrt(distSqr):
+                pow(distSqr, distPower/2);
+
+            unsigned int plane;
+
+            for (plane = 0; plane < pamP->depth; ++plane)
+                newColor[plane] += weight * source.color[plane];
+
+            totalWeight += weight;
+        }
+    }
+    for (plane = 0; plane < pamP->depth; ++plane)
+        outTuple[plane] = (sample) (newColor[plane] / totalWeight);
+}
+
+
+
+static void
+produceOutputImage(struct pam *          const pamP,
+                   tuple **              const intuples,
+                   tuple                 const bgColor,
+                   struct paintSourceSet const paintSources,
+                   distFunc_t *          const distFunc,
+                   double                const distPower,
+                   bool                  const all,
+                   tuple ***             const outtuplesP) {
+/*--------------------------------------------------------------------
+  Color each background pixel (or, if allPixels is 1, all pixels)
+  using a fraction of each paint source as determined by its distance
+  to the background pixel.
+----------------------------------------------------------------------*/
+    int row;   /* signed so it works with Openmp */
+    unsigned int rowsComplete;
+    tuple ** outtuples;
+
+    outtuples = pnm_allocpamarray(pamP);
+
+    rowsComplete = 0;
+    #pragma omp parallel for
+    for (row = 0; row < pamP->height; ++row) {
+        struct coords   target;
+        double        * newColor;
+        
+        MALLOCARRAY(newColor, pamP->depth);
+
+        target.y = row;
+        for (target.x = 0; target.x < pamP->width; ++target.x) {
+            tuple const targetTuple = intuples[target.y][target.x];
+            tuple const outputTuple = outtuples[target.y][target.x];
+
+            if (all || tupleEqualColor(pamP, targetTuple, bgColor))
+                spillOnePixel(pamP, target, paintSources, distFunc, distPower,
+                              outputTuple, newColor);
+            else
+                pnm_assigntuple(pamP,  outputTuple, targetTuple);
+        }
+        #pragma omp critical (rowTally)
+        reportProgress(++rowsComplete, pamP->height);
+
+        free(newColor);
+    }
+    *outtuplesP = outtuples;
+}
+
+
+
+int
+main(int argc, const char *argv[]) {
+    FILE *             ifP;
+    struct cmdlineInfo cmdline;          /* Command-line parameters */
+    tuple              bgColor;          /* Input image's background color */
+    struct paintSourceSet paintSources;
+        /* The set of paint-source indexes into 'tuples' */
+    distFunc_t *       distFunc;         /* The distance function */
+    struct pam inPam;
+    struct pam outPam;
+    tuple ** inTuples;
+    tuple ** outTuples;
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilename);
+
+    inTuples = pnm_readpam(ifP, &inPam, PAM_STRUCT_SIZE(allocation_depth));
+
+    pm_close(ifP);
+
+    distFunc = cmdline.wrap ? euclideanDistanceTorusSqr : euclideanDistanceSqr;
+
+    if (cmdline.bgcolor)
+        bgColor = pnm_parsecolor(cmdline.bgcolor, inPam.maxval) ;
+    else
+        bgColor = pnm_backgroundtuple(&inPam, inTuples);
+
+    pm_message("Treating %s as the background color",
+               pnm_colorname(&inPam, bgColor, PAM_COLORNAME_HEXOK));
+
+    locatePaintSources(&inPam, inTuples, bgColor, cmdline.downsample,
+                       &paintSources);
+
+    produceOutputImage(&inPam, inTuples, bgColor, paintSources, distFunc,
+                       cmdline.power, cmdline.all, &outTuples);
+
+    outPam = inPam;
+    outPam.file = stdout;
+    pnm_writepam(&outPam, outTuples);
+
+    pnm_freepamarray(outTuples, &inPam);
+    pnm_freepamarray(inTuples, &outPam);
+
+    return 0;
+}
diff --git a/editor/specialty/pgmabel.c b/editor/specialty/pgmabel.c
index 4914c4be..1764c5d7 100644
--- a/editor/specialty/pgmabel.c
+++ b/editor/specialty/pgmabel.c
@@ -38,15 +38,15 @@ static const char* const version="$VER: pgmabel 1.009 (24 Jan 2002)";
 
 #include <math.h>
 #include <stdlib.h>   /* for calloc */
-#include "pgm.h"
+
+#include "pm_c_util.h"
 #include "mallocvar.h"
+#include "pgm.h"
 
 #ifndef PID2          /*  PI/2 (on AMIGA always defined) */
 #define PID2    1.57079632679489661923  
 #endif
 
-#define TRUE 1
-#define FALSE 0
 
 /* some global variables */
 static double *aldl, *ardl;                /* pointer for weighting factors */
diff --git a/editor/specialty/pgmmorphconv.c b/editor/specialty/pgmmorphconv.c
index abc4e718..2ba2d62d 100644
--- a/editor/specialty/pgmmorphconv.c
+++ b/editor/specialty/pgmmorphconv.c
@@ -17,237 +17,403 @@
 */
 
 #include "pm_c_util.h"
+#include "shhopt.h"
+#include "mallocvar.h"
 #include "pgm.h"
 
 
-/************************************************************
- * Dilate 
- ************************************************************/
 
-static int 
-dilate( bit** template, int trowso2, int tcolso2, 
-        gray** in_image, gray** out_image, 
-        int rows, int cols ){
+enum Operation { ERODE, DILATE, OPEN, CLOSE, GRADIENT };
+
+
+
+struct CmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFileName;  /* File name of input file */
+    const char * templateFileName;  /* File name of template file */
 
-  int c, r, tc, tr;
-  int templatecount;
-  gray source;
+    enum Operation operation;
+};
 
-  for( c=0; c<cols; ++c)
-    for( r=0; r<rows; ++r )
-      out_image[r][c] = 0;   /* only difference with erode is here and below */
-  
-  /* 
-   *  for each non-black pixel of the template
-   *  add in to out
-   */
 
-  templatecount=0;
 
-  for( tr=-trowso2; tr<=trowso2; ++tr ){
-    for( tc=-tcolso2; tc<=tcolso2; ++tc ){
+static void
+parseCommandLine(int argc, const char ** const argv,
+                 struct CmdlineInfo * const cmdlineP) {
+/*----------------------------------------------------------------------------
+   Note that the file spec array we return is stored in the storage that
+   was passed to us as the argv array.
+-----------------------------------------------------------------------------*/
+    optEntry * option_def;
+        /* Instructions to OptParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+    unsigned int option_def_index;
+    unsigned int erode, dilate, open, close, gradient;
 
-      if( template[trowso2+tr][tcolso2+tc] == PBM_BLACK ) continue;
+    MALLOCARRAY_NOFAIL(option_def, 100);
 
-      ++templatecount;
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0,   "erode",        OPT_FLAG,   NULL, &erode,           0);
+    OPTENT3(0,   "dilate",       OPT_FLAG,   NULL, &dilate,          0);
+    OPTENT3(0,   "open",         OPT_FLAG,   NULL, &open,            0);
+    OPTENT3(0,   "close",        OPT_FLAG,   NULL, &close,           0);
+    OPTENT3(0,   "gradient",     OPT_FLAG,   NULL, &gradient,        0);
 
-      for( r= ((tr>0)?0:-tr) ; r< ((tr>0)?(rows-tr):rows) ; ++r ){
-        for( c= ((tc>0)?0:-tc) ; c< ((tc>0)?(cols-tc):cols) ; ++c ){
-          source = in_image[r+tr][c+tc];
-          out_image[r][c] = MAX(source, out_image[r][c]);
-        } /* for c */
-      } /* for r */
-    } /* for tr */
-  } /* for tc */
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = TRUE;  /* We may have parms that are negative numbers */
 
-  return templatecount;
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
-} /* dilate */
+    if (erode + dilate + open + close + gradient > 1)
+        pm_error("You may specify at most one of -erode, -dilate, "
+                 "-open, -close, or -gradient");
 
+    if (erode)
+        cmdlineP->operation = ERODE;
+    else if (dilate)
+        cmdlineP->operation = DILATE;
+    else if (open)
+        cmdlineP->operation = OPEN;
+    else if (close)
+        cmdlineP->operation = CLOSE;
+    else if (gradient)
+        cmdlineP->operation = GRADIENT;
+    else
+        cmdlineP->operation = DILATE;
+
+    if (argc-1 < 1)
+        pm_error("You must specify the template file name as an argument");
+    else {
+        cmdlineP->templateFileName = argv[1];
+
+        if (argc-1 < 2)
+            cmdlineP->inputFileName = "-";
+        else {
+            cmdlineP->inputFileName = argv[2];
+            
+            if (argc-1 > 2)
+                pm_error("Too many arguments: %u.  "
+                         "The only possible arguments "
+                         "are the template file name and the input file name",
+                         argc-1);
+        }
+    }
+}
+
+
+
+static void
+readTemplateMatrix(const char *   const fileName,
+                   bit ***        const templateP,
+                   unsigned int * const rowsP,
+                   unsigned int * const colsP) {
+/*----------------------------------------------------------------------------
+  Read in the template matrix.
+-----------------------------------------------------------------------------*/
+    FILE * templateFileP;
+    int cols, rows;
+
+    templateFileP = pm_openr(fileName);
+
+    *templateP = pbm_readpbm(templateFileP, &cols, &rows);
+
+    pm_close(templateFileP);
+
+    if (cols % 2 != 1 || rows % 2 != 1)
+        pm_error("the template matrix must have an odd number of "
+                 "rows and columns" );
+
+        /* the reason is that we want the middle pixel to be the origin */
+    *rowsP = rows;
+    *colsP = cols;
+}
+
+
+
+static void
+setAllPixel(gray **      const image,
+            unsigned int const rows,
+            unsigned int const cols,
+            gray         const value) {
+
+    unsigned int col;
+
+    for (col = 0; col < cols; ++col) {
+        unsigned int row;
+        for (row = 0; row < rows; ++row)
+            image[row][col] = value; 
+    }
+}
+
+
+
+static void
+dilate(bit **         const template,
+       int            const trowso2,
+       int            const tcolso2, 
+       gray **        const inImage,
+       gray **        const outImage, 
+       unsigned int   const rows,
+       unsigned int   const cols,
+       unsigned int * const templateCountP) {
+
+    unsigned int templateCount;
+    int tr;
+
+    setAllPixel(outImage, rows, cols, 0);
+
+    /* for each non-black pixel of the template add in to out */
+
+    for (tr = -trowso2, templateCount = 0; tr <= trowso2; ++tr) {
+        int tc;
+        for (tc = -tcolso2; tc <= tcolso2; ++tc) {
+            int r;
+            if (template[trowso2+tr][tcolso2+tc] != PBM_BLACK) {
+                ++templateCount;
+
+                for (r = ((tr > 0) ? 0 : -tr);
+                     r < ((tr > 0) ? (rows-tr) : rows);
+                     ++r) {
+                    int c;
+                    for (c = ((tc > 0) ? 0 : -tc);
+                         c < ((tc > 0) ? (cols-tc) : cols);
+                         ++c) {
+                        gray const source = inImage[r+tr][c+tc];
+                        outImage[r][c] = MAX(source, outImage[r][c]);
+                    }
+                }
+            }
+        }
+    }
+    *templateCountP = templateCount;
+}
+
+
+
+static void
+erode(bit **         const template,
+      int            const trowso2,
+      int            const tcolso2, 
+      gray **        const inImage,
+      gray **        const outImage, 
+      unsigned int   const rows,
+      unsigned int   const cols,
+      unsigned int * const templateCountP) {
+
+    unsigned int templateCount;
+    int tr;
+
+    setAllPixel(outImage, rows, cols, PGM_MAXMAXVAL);
+
+    /* For each non-black pixel of the template add in to out */
+
+    for (tr = -trowso2, templateCount = 0; tr <= trowso2; ++tr) {
+        int tc;
+        for (tc = -tcolso2; tc <= tcolso2; ++tc) {
+            if (template[trowso2+tr][tcolso2+tc] != PBM_BLACK) {
+                int r;
+                ++templateCount;
+
+                for (r = ((tr > 0) ? 0 : -tr);
+                     r < ((tr > 0) ? (rows-tr) : rows);
+                     ++r){
+                    int c;
+
+                    for (c = ((tc > 0) ? 0 : -tc);
+                         c < ((tc > 0) ? (cols-tc) : cols);
+                         ++c) {
+                        
+                        gray const source = inImage[r+tr][c+tc];
+                        outImage[r][c] = MIN(source, outImage[r][c]);
+      
+                    }
+                }
+            }
+        }
+    }
+    *templateCountP = templateCount;
+}
 
 
-/************************************************************
- * Erode: same as dilate except !!!!
- ************************************************************/
 
-static int 
-erode( bit** template, int trowso2, int tcolso2, 
-       gray** in_image, gray** out_image, 
-       int rows, int cols ){
+static void
+openMorph(bit **         const template,
+          int            const trowso2,
+          int            const tcolso2, 
+          gray **        const inputImage,
+          gray **        const outputImage, 
+          unsigned int   const rows,
+          unsigned int   const cols,
+          unsigned int * const templateCountP) {
 
-  int c, r, tc, tr;
-  int templatecount;
-  gray source;
+    gray ** erodedImage;
+    unsigned int erodedTemplateCount;
 
-  for( c=0; c<cols; ++c)
-    for( r=0; r<rows; ++r )
-      out_image[r][c] = PGM_MAXMAXVAL; /* !!!! */
-  
-  /* 
-   *  for each non-black pixel of the template
-   *  add in to out
-   */
+    erodedImage = pgm_allocarray(cols, rows);
+    
+    erode(template, trowso2, tcolso2, 
+          inputImage, erodedImage, rows, cols, &erodedTemplateCount);
 
-  templatecount=0;
+    dilate(template, trowso2, tcolso2, 
+           erodedImage, outputImage, rows, cols, templateCountP);
 
-  for( tr=-trowso2; tr<=trowso2; ++tr ){
-    for( tc=-tcolso2; tc<=tcolso2; ++tc ){
+    pgm_freearray(erodedImage, rows);
+}
 
-      if( template[trowso2+tr][tcolso2+tc] == PBM_BLACK ) continue;
 
-      ++templatecount;
 
-      for( r= ((tr>0)?0:-tr) ; r< ((tr>0)?(rows-tr):rows) ; ++r ){
-    for( c= ((tc>0)?0:-tc) ; c< ((tc>0)?(cols-tc):cols) ; ++c ){
+static void
+closeMorph(bit **         const template,
+           int            const trowso2,
+           int            const tcolso2, 
+           gray **        const inputImage,
+           gray **        const outputImage, 
+           unsigned int   const rows,
+           unsigned int   const cols,
+           unsigned int * const templateCountP) {
 
-      source = in_image[r+tr][c+tc];
-      out_image[r][c] = MIN(source, out_image[r][c]);
-      
-    } /* for c */
-      } /* for r */
+    gray ** dilatedImage;
+    unsigned int dilatedTemplateCount;
 
+    dilatedImage = pgm_allocarray(cols, rows);
 
+    dilate(template, trowso2, tcolso2, 
+           inputImage, dilatedImage, rows, cols, &dilatedTemplateCount);
 
-    } /* for tr */
-  } /* for tc */
+    erode(template, trowso2, tcolso2, 
+          dilatedImage, outputImage, rows, cols, templateCountP);
 
-  return templatecount;
+    pgm_freearray(dilatedImage, rows);
+}
 
-} /* erode */
 
 
+static void
+subtract(gray **      const subtrahendImage,
+         gray **      const subtractorImage,
+         gray **      const outImage, 
+         unsigned int const rows,
+         unsigned int const cols ) {
 
-/************************************************************
- *  Main
- ************************************************************/
+    /* (I call the minuend the subtrahend and the subtrahend the subtractor,
+       to be consistent with other arithmetic terminology).
+    */
 
+    unsigned int c;
 
-int main( int argc, char* argv[] ){
+    for (c = 0; c < cols; ++c) {
+        unsigned int r;
+        for (r = 0; r < rows; ++r)
+            outImage[r][c] = subtrahendImage[r][c] - subtractorImage[r][c];
+    }
+}
 
-  int argn;
-  char operation;
-  const char* usage = "-dilate|-erode|-open|-close <templatefile> [pgmfile]";
 
-  FILE* tifp;   /* template */
-  int tcols, trows;
-  int tcolso2, trowso2;
-  bit** template;
 
+static void
+gradient(bit **         const template,
+         int            const trowso2,
+         int            const tcolso2, 
+         gray **        const inputImage,
+         gray **        const outputImage, 
+         unsigned int   const rows,
+         unsigned int   const cols,
+         unsigned int * const templateCountP) {
 
-  FILE*  ifp;   /* input image */
-  int cols, rows;
-  gray maxval;
+    gray ** dilatedImage;
+    gray ** erodedImage;
+    unsigned int dilatedTemplateCount;
+    
+    dilatedImage = pgm_allocarray(cols, rows);
+    erodedImage = pgm_allocarray(cols, rows);
 
-  gray** in_image;
-  gray** out_image;
+    dilate(template, trowso2, tcolso2, 
+           inputImage, dilatedImage, rows, cols, &dilatedTemplateCount);
 
-  int templatecount=0;
+    erode(template, trowso2, tcolso2, 
+          inputImage, erodedImage, rows, cols, templateCountP);
 
-  pgm_init( &argc, argv );
+    subtract(dilatedImage, erodedImage, outputImage, rows, cols);
 
-  /*
-   *  parse arguments
-   */ 
-  
-  ifp = stdin;
-  operation = 'd';
+    pgm_freearray(erodedImage, rows );
+    pgm_freearray(dilatedImage, rows );
+}
 
-  argn=1;
-  
-  if( argn == argc ) pm_usage( usage );
-  
-  if( pm_keymatch( argv[argn], "-erode", 2  )) { operation='e'; argn++; }
-  else
-  if( pm_keymatch( argv[argn], "-dilate", 2 )) { operation='d'; argn++; }
-  else
-  if( pm_keymatch( argv[argn], "-open", 2   )) { operation='o'; argn++; }
-  else
-  if( pm_keymatch( argv[argn], "-close", 2  )) { operation='c'; argn++; }
-  
-  if( argn == argc ) pm_usage( usage );
-  
-  tifp = pm_openr( argv[argn++] );
-  
-  if( argn != argc ) ifp = pm_openr( argv[argn++] );
 
-  if( argn != argc ) pm_usage( usage );
 
-  
-  /* 
-   * Read in the template matrix.
-   */
+int
+main(int argc, const char ** argv) {
 
-  template = pbm_readpbm( tifp, &tcols, &trows );
-  pm_close( tifp );
+    struct CmdlineInfo cmdline;
+    FILE * ifP;
+    bit ** template;
+    unsigned int templateCols, templateRows;
+    int cols, rows;
+    gray maxval;
+    gray ** inputImage;
+    gray ** outputImage;
+    unsigned int templateCount;
 
-  if( tcols % 2 != 1 || trows % 2 != 1 )
-    pm_error("the template matrix must have an odd number of "
-             "rows and columns" );
+    pm_proginit(&argc, argv);
 
-  /* the reason is that we want the middle pixel to be the origin */
-  tcolso2 = tcols / 2; /* template coords run from -tcols/2 .. 0 .. +tcols/2 */
-  trowso2 = trows / 2;
+    parseCommandLine(argc, argv, &cmdline);
 
-#if 0
-  fprintf(stderr, "template: %d  x %d\n", trows, tcols);
-  fprintf(stderr, "half: %d  x %d\n", trowso2, tcolso2);
-#endif
+    ifP = pm_openr(cmdline.inputFileName);
 
-  /*
-   * Read in the image
-   */
-  
-  in_image = pgm_readpgm( ifp, &cols, &rows, &maxval);
+    readTemplateMatrix(cmdline.templateFileName,
+                       &template, &templateRows, &templateCols);
 
-  if( cols < tcols || rows < trows )
-    pm_error("the image is smaller than the convolution matrix" );
+    /* Template coords run from -templateCols/2 .. 0 .. + templateCols/2 */
   
-#if 0
-  fprintf(stderr, "image: %d  x %d (%d)\n", rows, cols, maxval);
-#endif
-
-  /* 
-   * Allocate  output buffer and initialize with min or max value 
-   */
+    inputImage = pgm_readpgm(ifP, &cols, &rows, &maxval);
 
-  out_image = pgm_allocarray( cols, rows );
+    if (cols < templateCols || rows < templateRows)
+        pm_error("the image is smaller than the convolution matrix" );
   
-  if( operation == 'd' ){
-    templatecount = dilate(template, trowso2, tcolso2, 
-               in_image, out_image, rows, cols);
-  } 
-  else if( operation == 'e' ){
-    templatecount = erode(template, trowso2, tcolso2, 
-              in_image, out_image, rows, cols);
-  }
-  else if( operation == 'o' ){
-    gray ** eroded_image;
-    eroded_image = pgm_allocarray( cols, rows );
-    templatecount = erode(template, trowso2, tcolso2, 
-                          in_image, eroded_image, rows, cols);
-    templatecount = dilate(template, trowso2, tcolso2, 
-                           eroded_image, out_image, rows, cols);
-    pgm_freearray( eroded_image, rows );
-  }
-  else if( operation == 'c' ){
-    gray ** dilated_image;
-    dilated_image = pgm_allocarray( cols, rows );
-    templatecount = dilate(template, trowso2, tcolso2, 
-                           in_image, dilated_image, rows, cols);
-    templatecount = erode(template, trowso2, tcolso2, 
-                          dilated_image, out_image, rows, cols);
-    pgm_freearray( dilated_image, rows );
-  }
+    outputImage = pgm_allocarray(cols, rows);
   
-  if(templatecount == 0 ) pm_error( "The template was empty!" );
-
-  pgm_writepgm( stdout, out_image, cols, rows, maxval, 1 );
-
-  pgm_freearray( out_image, rows );
-  pgm_freearray( in_image, rows );
-  pm_close( ifp );
-
-  exit( 0 );
-
-} /* main */
+    switch (cmdline.operation) {
+    case DILATE:
+        dilate(template, templateRows/2, templateCols/2,
+               inputImage, outputImage, rows, cols,
+               &templateCount);
+        break;
+    case ERODE:
+        erode(template, templateRows/2, templateCols/2,
+              inputImage, outputImage, rows, cols,
+              &templateCount);
+        break;
+    case OPEN:
+        openMorph(template, templateRows/2, templateCols/2,
+              inputImage, outputImage, rows, cols,
+              &templateCount);
+        break;
+    case CLOSE:
+        closeMorph(template, templateRows/2, templateCols/2,
+                   inputImage, outputImage, rows, cols,
+                   &templateCount);
+        break;
+    case GRADIENT:
+        gradient(template, templateRows/2, templateCols/2,
+                 inputImage, outputImage, rows, cols,
+                 &templateCount);
+        break;
+    }
+
+    if (templateCount == 0)
+        pm_error( "The template was empty!" );
+
+    pgm_writepgm(stdout, outputImage, cols, rows, maxval, 0);
+
+    pgm_freearray(outputImage, rows);
+    pgm_freearray(inputImage, rows);
+    pm_close(ifP);
+
+    return 0;
+}
 
diff --git a/editor/specialty/pnmindex.c b/editor/specialty/pnmindex.c
index ca1da18c..4ec9edaa 100644
--- a/editor/specialty/pnmindex.c
+++ b/editor/specialty/pnmindex.c
@@ -14,6 +14,7 @@
 
 ============================================================================*/
 
+#define _XOPEN_SOURCE 500  /* Make sure strdup() is in string.h */
 #define _BSD_SOURCE   /* Make sure strdup is in string.h */
 
 #include <assert.h>
@@ -59,7 +60,7 @@ systemf(const char * const fmt,
     
     va_start(varargs, fmt);
     
-    vsnprintfN(NULL, 0, fmt, varargs, &dryRunLen);
+    pm_vsnprintf(NULL, 0, fmt, varargs, &dryRunLen);
 
     va_end(varargs);
 
@@ -72,7 +73,7 @@ systemf(const char * const fmt,
         shellCommand = malloc(allocSize);
         if (shellCommand == NULL)
             pm_error("Can't get storage for %u-character command",
-                     allocSize);
+                     (unsigned)allocSize);
         else {
             va_list varargs;
             size_t realLen;
@@ -80,7 +81,7 @@ systemf(const char * const fmt,
 
             va_start(varargs, fmt);
 
-            vsnprintfN(shellCommand, allocSize, fmt, varargs, &realLen);
+            pm_vsnprintf(shellCommand, allocSize, fmt, varargs, &realLen);
                 
             assert(realLen == dryRunLen);
             va_end(varargs);
@@ -93,7 +94,7 @@ systemf(const char * const fmt,
                 pm_error("shell command '%s' failed.  rc %d",
                          shellCommand, rc);
             
-            strfree(shellCommand);
+            pm_strfree(shellCommand);
         }
     }
 }
@@ -106,7 +107,7 @@ parseCommandLine(int argc, char ** argv,
 
     unsigned int option_def_index;
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -137,7 +138,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE; 
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdline_p and others. */
 
     if (quant && cmdlineP->noquant)
@@ -184,7 +185,7 @@ freeCmdline(struct cmdlineInfo const cmdline) {
     unsigned int i;
 
     for (i = 0; i < cmdline.inputFileCount; ++i)
-        strfree(cmdline.inputFileName[i]);
+        pm_strfree(cmdline.inputFileName[i]);
 
     free(cmdline.inputFileName);
 
@@ -200,7 +201,7 @@ makeTempDir(const char ** const tempDirP) {
     const char * mytmpdir;
     int rc;
 
-    asprintfN(&mytmpdir, "%s/pnmindex_%d", tmpdir, getpid());
+    pm_asprintf(&mytmpdir, "%s/pnmindex_%d", tmpdir, getpid());
 
     rc = pm_mkdir(mytmpdir, 0700);
     if (rc != 0)
@@ -232,7 +233,7 @@ rowFileName(const char * const dirName,
 
     const char * fileName;
     
-    asprintfN(&fileName, "%s/pi.%u", dirName, row);
+    pm_asprintf(&fileName, "%s/pi.%u", dirName, row);
     
     return fileName;
 }
@@ -259,7 +260,7 @@ makeTitle(const char * const title,
             "> %s", 
             title, invertStage, fileName);
 
-    strfree(fileName);
+    pm_strfree(fileName);
 }
 
 
@@ -285,28 +286,28 @@ copyScaleQuantImage(const char * const inputFileName,
 
     switch (PNM_FORMAT_TYPE(format)) {
     case PBM_TYPE:
-        asprintfN(&scaleCommand, 
-                  "pamscale -quiet -xysize %u %u %s "
-                  "| pgmtopbm > %s",
-                  size, size, inputFileName, outputFileName);
+        pm_asprintf(&scaleCommand, 
+                    "pamscale -quiet -xysize %u %u %s "
+                    "| pgmtopbm > %s",
+                    size, size, inputFileName, outputFileName);
         break;
         
     case PGM_TYPE:
-        asprintfN(&scaleCommand, 
-                  "pamscale -quiet -xysize %u %u %s >%s",
-                  size, size, inputFileName, outputFileName);
+        pm_asprintf(&scaleCommand, 
+                    "pamscale -quiet -xysize %u %u %s >%s",
+                    size, size, inputFileName, outputFileName);
         break;
         
     case PPM_TYPE:
         if (quant)
-            asprintfN(&scaleCommand, 
-                      "pamscale -quiet -xysize %u %u %s "
-                      "| pnmquant -quiet %u > %s",
-                      size, size, inputFileName, colors, outputFileName);
+            pm_asprintf(&scaleCommand, 
+                        "pamscale -quiet -xysize %u %u %s "
+                        "| pnmquant -quiet %u > %s",
+                        size, size, inputFileName, colors, outputFileName);
         else
-            asprintfN(&scaleCommand, 
-                      "pamscale -quiet -xysize %u %u %s >%s",
-                      size, size, inputFileName, outputFileName);
+            pm_asprintf(&scaleCommand, 
+                        "pamscale -quiet -xysize %u %u %s >%s",
+                        size, size, inputFileName, outputFileName);
         break;
     default:
         pm_error("Unrecognized Netpbm format: %d", format);
@@ -314,7 +315,7 @@ copyScaleQuantImage(const char * const inputFileName,
 
     systemf("%s", scaleCommand);
 
-    strfree(scaleCommand);
+    pm_strfree(scaleCommand);
 }
 
 
@@ -340,7 +341,7 @@ thumbnailFileName(const char * const dirName,
 
     const char * fileName;
     
-    asprintfN(&fileName, "%s/pi.%u.%u", dirName, row, col);
+    pm_asprintf(&fileName, "%s/pi.%u.%u", dirName, row, col);
     
     return fileName;
 }
@@ -372,7 +373,7 @@ thumbnailFileList(const char * const dirName,
             strcat(list, " ");
             strcat(list, fileName);
         }
-        strfree(fileName);
+        pm_strfree(fileName);
     }
 
     return list;
@@ -420,7 +421,7 @@ makeThumbnail(const char *  const inputFileName,
     pnm_readpnminit(ifP, &imageCols, &imageRows, &maxval, &format);
     pm_close(ifP);
     
-    asprintfN(&tmpfile, "%s/pi.tmp", tempDir);
+    pm_asprintf(&tmpfile, "%s/pi.tmp", tempDir);
 
     if (imageCols < size && imageRows < size)
         copyImage(inputFileName, tmpfile);
@@ -434,8 +435,8 @@ makeThumbnail(const char *  const inputFileName,
 
     unlink(tmpfile);
 
-    strfree(fileName);
-    strfree(tmpfile);
+    pm_strfree(fileName);
+    pm_strfree(tmpfile);
 
     *formatP = format;
 }
@@ -454,7 +455,7 @@ unlinkThumbnailFiles(const char * const dirName,
 
         unlink(fileName);
 
-        strfree(fileName);
+        pm_strfree(fileName);
     }
 }
 
@@ -471,7 +472,7 @@ unlinkRowFiles(const char * const dirName,
 
         unlink(fileName);
 
-        strfree(fileName);
+        pm_strfree(fileName);
     }
 }
 
@@ -497,7 +498,7 @@ combineIntoRowAndDelete(unsigned int const row,
     unlink(fileName);
 
     if (maxFormatType == PPM_TYPE && quant)
-        asprintfN(&quantStage, "| pnmquant -quiet %u ", colors);
+        pm_asprintf(&quantStage, "| pnmquant -quiet %u ", colors);
     else
         quantStage = strdup("");
 
@@ -508,9 +509,9 @@ combineIntoRowAndDelete(unsigned int const row,
             ">%s",
             blackWhiteOpt, fileList, quantStage, fileName);
 
-    strfree(fileList);
-    strfree(quantStage);
-    strfree(fileName);
+    pm_strfree(fileList);
+    pm_strfree(quantStage);
+    pm_strfree(fileName);
 
     unlinkThumbnailFiles(tempDir, row, cols);
 }
@@ -542,7 +543,7 @@ rowFileList(const char * const dirName,
             strcat(list, " ");
             strcat(list, fileName);
         }
-        strfree(fileName);
+        pm_strfree(fileName);
     }
 
     return list;
@@ -564,7 +565,7 @@ writeRowsAndDelete(unsigned int const rows,
     const char * fileList;
     
     if (maxFormatType == PPM_TYPE && quant)
-        asprintfN(&quantStage, "| pnmquant -quiet %u ", colors);
+        pm_asprintf(&quantStage, "| pnmquant -quiet %u ", colors);
     else
         quantStage = strdup("");
 
@@ -573,8 +574,8 @@ writeRowsAndDelete(unsigned int const rows,
     systemf("pnmcat %s -topbottom %s %s",
             blackWhiteOpt, fileList, quantStage);
 
-    strfree(fileList);
-    strfree(quantStage);
+    pm_strfree(fileList);
+    pm_strfree(quantStage);
 
     unlinkRowFiles(tempDir, rows);
 }
diff --git a/editor/specialty/pnmmercator.c b/editor/specialty/pnmmercator.c
new file mode 100644
index 00000000..cd9ff19b
--- /dev/null
+++ b/editor/specialty/pnmmercator.c
@@ -0,0 +1,430 @@
+/* pammercator.c - convert a map in PNM image format from 360x180 degrees
+**                 to Mercator projection or vice versa
+**
+** This program borrowed a lot of code from PnmScale which again was derived
+** from PpmScale.
+**
+** Copyright (C) 2009 by Willem van Schaik <willem@schaik.com>
+** 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.
+**
+*/
+
+#define _XOPEN_SOURCE  /* Make sure M_PI is in <math.h> */
+#include <math.h>
+#include <string.h>
+
+#include "pm_c_util.h"
+#include "mallocvar.h"
+#include "shhopt.h"
+#include "pnm.h"
+
+/* The pnm library allows us to code this program without branching cases for
+   PGM and PPM, but we do the branch anyway to speed up processing of PGM
+   images. 
+*/
+
+
+
+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 */
+    const char * inputFileName;  /* Filespec of input file */
+    unsigned int inverse; /* from Mercator to Degrees */
+    unsigned int nomix;
+    unsigned int verbose;
+    unsigned int vverbose;
+};
+
+
+
+static void
+parseCommandLine(int                        argc, 
+                 const char **              argv,
+                 struct cmdlineInfo * const cmdlineP ) {
+
+    optEntry * option_def;
+    optStruct3 opt;
+        /* Instructions to pm_optParseOptions3 on how to parse our options. */
+    
+    unsigned int option_def_index;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "inverse",   OPT_FLAG,    NULL,       &cmdlineP->inverse,   0);
+    OPTENT3(0, "nomix",     OPT_FLAG,    NULL,       &cmdlineP->nomix,     0);
+    OPTENT3(0, "verbose",   OPT_FLAG,    NULL,       &cmdlineP->verbose,   0);
+    OPTENT3(0, "vverbose",  OPT_FLAG,    NULL,       &cmdlineP->vverbose,  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 */
+
+    /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+    pm_optParseOptions3( &argc, (char **)argv, opt, sizeof(opt), 0 );
+
+    /* Only parameter allowed is optional filespec */
+    if (argc-1 < 1)
+        cmdlineP->input_filespec = "-";
+    else
+        cmdlineP->input_filespec = argv[1];
+}
+
+
+
+static void
+computeOutputDimensions(const struct cmdlineInfo cmdline, 
+                        const int rows, const int cols,
+                        int * newrowsP, int * newcolsP) 
+{
+    *newcolsP = cols;
+    if (!cmdline.inverse)
+        *newrowsP = 2 * rows;
+    else
+        *newrowsP = rows / 2;
+
+    if (*newcolsP < 1) *newcolsP = 1;
+    if (*newrowsP < 1) *newrowsP = 1;
+
+    if (cmdline.verbose) {
+        if (!cmdline.inverse)
+            pm_message("Creating Mercator map, new size is %dx%d",
+                       *newcolsP, *newrowsP);
+        else
+            pm_message("Creating Degrees map, new size is %dx%d",
+                       *newcolsP, *newrowsP);
+    }
+}        
+
+
+
+static void
+transformWithMixing(FILE * const ifP,
+                    int const cols, int const rows,
+                    xelval const maxval, int const format,
+                    int const newcols, int const newrows,
+                    xelval const newmaxval, int const newformat,
+                    bool const inverse,
+                    bool const verbose, bool const vverbose) 
+{
+    /* 
+    Transform the map image on input file 'ifP' (which is described by 
+    'cols', 'rows', 'format', and 'maxval') to a Mercator projection
+    and write the result to standard output as format 'newformat' and 
+    with maxval 'newmaxval'.
+
+    We'll blend colors from subsequent rows in the output pixels. 
+    */
+
+    xel* oddxelrow;  /* an input row */
+    xel* evenxelrow;
+    xel* newxelrow;  /* the output row */
+
+    int row;
+    double fRow;
+    int rowInXelrow;
+    int inputRow;
+    int col;
+
+    double fFract;
+    double fLatRad;
+    double fLatMerc;
+
+    oddxelrow = pnm_allocrow(cols); 
+    evenxelrow = pnm_allocrow(cols); 
+    rowInXelrow = 0;
+
+    newxelrow = pnm_allocrow(newcols);
+
+    for (row = 0; row < newrows; ++row) {
+        
+        fRow = (double)row + 0.5; /* center on half the pixel */
+        if (!inverse) {
+            /* the result is mercator, calculate back to degrees */
+            fLatMerc = 2.0 * M_PI * (fRow / (double)newrows - 0.5);
+                /* merc goes from -pi to +pi */
+            fLatRad = 2.0 * (atan(exp(fLatMerc)) - M_PI / 4.0);
+            fRow = ((fLatRad / M_PI) + 0.5) * (double)rows;
+                /* lat goes from -pi/2 to +pi/2 */
+        } else {
+            /* the result is in degrees, calculate back to mercator */
+            fLatRad = M_PI * (fRow / (double)newrows - 0.5);
+                /* lat goes from -pi/2 to +pi/2 */
+            fLatMerc = log(tan((M_PI / 4.0 + fLatRad / 2.0)));
+            fRow = ((fLatMerc / M_PI / 2.0) + 0.5) * (double)rows;
+                /* merc goes from -pi to +pi */
+        }
+        fRow -= 0.5;
+
+        inputRow = (int) floor(fRow);
+        if (inputRow < 0)
+            inputRow = 0;
+        if (inputRow > rows - 1)
+            inputRow = rows - 1;
+
+        /* calculate the fraction */
+        fFract = 1.0 - ((fRow) - (double) inputRow);
+
+        if (vverbose) {
+            if (!inverse) {
+#ifdef DBG_EDGES
+                if ((row < 10) || (row > (newrows - 10)))
+#else
+  #ifdef DBG_MIDDLE
+                if ((row > (newrows/2 - 10)) && (row < (newrows/2 + 10)))
+  #else
+                if (row % 20 == 0)
+  #endif
+#endif
+                    pm_message("outputRow=%d latMerc=%f latRad=%f inputRow=%d "
+                               "fRow=%f fFract=%f",
+                               row, fLatMerc, fLatRad, inputRow, fRow, fFract);
+            } else {
+#ifdef DBG_EDGES
+                if ((row < 10) || (row > (newrows - 10)))
+#else
+  #ifdef DBG_MIDDLE
+                if ((row > (newrows/2 - 10)) && (row < (newrows/2 + 10)))
+  #else
+                if (row % 10 == 0)
+  #endif
+#endif
+                    pm_message("outputRow=%d latRad=%f latMerc=%f inputRow=%d"
+                               "fRow=%f fFract=%f",
+                               row, fLatRad, fLatMerc, inputRow, fRow, fFract);
+            }
+        }
+
+        while ((rowInXelrow <= inputRow + 1) && 
+               (rowInXelrow < rows)) {
+            /* we need to read one row ahead */
+            if (rowInXelrow % 2 == 0) {
+#ifdef DBG_EDGES
+                if ((row < 10) || (row > (newrows - 10)))
+                    pm_message("read even row - rowInXelrow=%d inputRow=%d",
+                               rowInXelrow, inputRow);
+#endif
+                pnm_readpnmrow(ifP, evenxelrow, cols, newmaxval, format);
+            } else {
+#ifdef DBG_EDGES
+                if ((row < 10) || (row > (newrows - 10)))
+                    pm_message("read odd row - rowInXelrow=%d inputRow=%d",
+                               rowInXelrow, inputRow);
+#endif
+                pnm_readpnmrow(ifP, oddxelrow, cols, newmaxval, format);
+            }
+            ++rowInXelrow;
+        }
+
+        for (col = 0; col < newcols; ++col) {
+            if (inputRow == 0)
+                newxelrow[col] = evenxelrow[col];
+            else if (inputRow == rows - 1)
+                newxelrow[col] = oddxelrow[col];
+            else if (inputRow % 2 == 0) {
+                /* the even row is the low one, the odd the high one */
+                newxelrow[col].r = fFract * evenxelrow[col].r +
+                    (1.0 - fFract) * oddxelrow[col].r;
+                newxelrow[col].g = fFract * evenxelrow[col].g +
+                    (1.0 - fFract) * oddxelrow[col].g;
+                newxelrow[col].b = fFract * evenxelrow[col].b +
+                    (1.0 - fFract) * oddxelrow[col].b;
+            } else {
+                newxelrow[col].r = fFract * oddxelrow[col].r +
+                    (1.0 - fFract) * evenxelrow[col].r;
+                newxelrow[col].g = fFract * oddxelrow[col].g +
+                    (1.0 - fFract) * evenxelrow[col].g;
+                newxelrow[col].b = fFract * oddxelrow[col].b +
+                    (1.0 - fFract) * evenxelrow[col].b;
+            }
+        }
+
+        pnm_writepnmrow(stdout, newxelrow, newcols, newmaxval, newformat, 0 );
+    }
+
+    pnm_freerow(oddxelrow);
+    pnm_freerow(evenxelrow);
+    pnm_freerow(newxelrow);
+}
+
+
+
+static void
+transformNoneMixing(FILE * const ifP,
+                   int const cols, int const rows,
+                   xelval const maxval, int const format,
+                   int const newcols, int const newrows,
+                   xelval const newmaxval, int const newformat,
+                   bool const inverse,
+                   bool const verbose, bool const vverbose) 
+{
+    /*
+    Transform the map image on input file 'ifP' (which is described by 
+    'cols', 'rows', 'format', and 'maxval') to a Mercator projection and
+    write the result to standard output as format 'newformat' and with 
+    maxval 'newmaxval'.
+
+    Don't mix colors from different input pixels together in the output
+    pixels.  Each output pixel is an exact copy of some corresponding 
+    input pixel.
+    */
+
+    xel* xelrow;          /* an input row */
+    xel* newxelrow;         /* the output row */
+
+    int row;
+    double fRow;
+    int rowInXelrow;
+    int inputRow;
+    int col;
+
+    double fLatRad;
+    double fLatMerc;
+
+    xelrow = pnm_allocrow(cols); 
+    rowInXelrow = 0;
+
+    newxelrow = pnm_allocrow(newcols);
+
+    for (row = 0; row < newrows; ++row) {
+        
+        fRow = (double)row + 0.5; /* center on half the pixel */
+        if (!inverse) {
+            /* the result is mercator, calculate back to degrees */
+            fLatMerc = 2.0 * M_PI * (fRow / (double)newrows - 0.5);
+                /* merc goes from -pi to +pi */
+            fLatRad = 2.0 * (atan(exp(fLatMerc)) - M_PI / 4.0);
+            fRow = ((fLatRad / M_PI) + 0.5) * (double)rows;
+                /* lat goes from -pi/2 to +pi/2 */
+        } else {
+            /* the result is in degrees, calculate back to mercator */
+            fLatRad = M_PI * (fRow / (double)newrows - 0.5);
+                /* lat goes from -pi/2 to +pi/2 */
+            fLatMerc = log(tan((M_PI / 4.0 + fLatRad / 2.0)));
+            fRow = ((fLatMerc / M_PI / 2.0) + 0.5) * (double)rows;
+                /* merc goes from -pi to +pi */
+        }
+        /* fRow -= 0.5; */        /* it's weird that this isn't needed */
+
+        inputRow = (int) floor(fRow);
+        if (inputRow < 0)
+            inputRow = 0;
+        if (inputRow > rows - 1)
+            inputRow = rows - 1;
+
+        if (vverbose) {
+            if (!inverse) {
+#ifdef DBG_EDGES
+                if ((row < 10) || (row > (newrows - 10)))
+#else
+  #ifdef DBG_MIDDLE
+                if ((row > (newrows/2 - 10)) && (row < (newrows/2 + 10)))
+  #else
+                if (row % 20 == 0)
+  #endif
+#endif
+                    pm_message("outputRow=%d latMerc=%f latRad=%f inputRow=%d"
+                               "fRow=%f",
+                               row, fLatMerc, fLatRad, inputRow, fRow);
+            } else {
+#ifdef DBG_EDGES
+                if ((row < 10) || (row > (newrows - 10)))
+#else
+  #ifdef DBG_MIDDLE
+                if ((row > (newrows/2 - 10)) && (row < (newrows/2 + 10)))
+  #else
+                if (row % 10 == 0)
+  #endif
+#endif
+                    pm_message("outputRow=%d latRad=%f latMerc=%f inputRow=%d"
+                               "fRow=%f",
+                               row, fLatRad, fLatMerc, inputRow, fRow);
+            }
+        }
+
+        while ((rowInXelrow <= inputRow) && (rowInXelrow < rows)) {
+#ifdef DBG_EDGES
+            if ((row < 10) || (row > (newrows - 10)))
+                pm_message("read row - rowInXelrow=%d inputRow=%d",
+                           rowInXelrow, inputRow);
+#endif
+            pnm_readpnmrow(ifP, xelrow, cols, newmaxval, format);
+            ++rowInXelrow;
+        }
+        for (col = 0; col < newcols; ++col) {
+            newxelrow[col] = xelrow[col];
+        }
+
+        pnm_writepnmrow(stdout, newxelrow, newcols, newmaxval, newformat, 0 );
+    }
+
+    pnm_freerow(xelrow);
+    pnm_freerow(newxelrow);
+}
+
+
+
+int
+main(int argc, const char ** argv ) 
+{
+    struct cmdlineInfo cmdline;
+    FILE* ifP;
+    int rows, cols, format, newformat, newrows, newcols;
+    xelval maxval, newmaxval;
+    bool verbose;
+
+    pm_proginit( &argc, argv );
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    verbose = cmdline.verbose || cmdline.vverbose;
+
+    ifP = pm_openr(cmdline.input_filespec);
+
+    pnm_readpnminit( ifP, &cols, &rows, &maxval, &format );
+
+    /* Promote PBM file to PGM. */
+    if ( PNM_FORMAT_TYPE(format) == PBM_TYPE ) {
+        newformat = PGM_TYPE;
+        newmaxval = PGM_MAXMAXVAL;
+        pm_message( "promoting from PBM to PGM" );
+    } else {
+        newformat = format;
+        newmaxval = maxval;
+    }
+
+    computeOutputDimensions(cmdline, rows, cols, &newrows, &newcols);
+
+    pnm_writepnminit(stdout, newcols, newrows, newmaxval, newformat, 0);
+
+    if (cmdline.nomix) {
+        if (verbose)
+            pm_message("Transforming map without mixing/blending colors");
+        transformNoneMixing(ifP, cols, rows, maxval, format,
+                            newcols, newrows, newmaxval, newformat, 
+                            cmdline.inverse, verbose, cmdline.vverbose);
+    } else {
+        if (verbose)
+            pm_message("Transforming map while using intermediate colors");
+        transformWithMixing(ifP, cols, rows, maxval, format,
+                            newcols, newrows, newmaxval, newformat, 
+                            cmdline.inverse, verbose, cmdline.vverbose);
+    }
+
+    pm_close(ifP);
+    pm_close(stdout);
+    
+    return 0;
+}
diff --git a/editor/specialty/ppm3d.c b/editor/specialty/ppm3d.c
index d9ada365..a6faa341 100644
--- a/editor/specialty/ppm3d.c
+++ b/editor/specialty/ppm3d.c
@@ -42,7 +42,7 @@ parseCommandLine(int argc, char ** argv,
    was passed to us as the argv array.  We also trash *argv.
 -----------------------------------------------------------------------------*/
     optEntry * option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -62,7 +62,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3( &argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3( &argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
     
     if (argc-1 < 2)
diff --git a/editor/specialty/ppmglobe.c b/editor/specialty/ppmglobe.c
index 82fae5fb..92e22746 100644
--- a/editor/specialty/ppmglobe.c
+++ b/editor/specialty/ppmglobe.c
@@ -42,7 +42,7 @@ parseCommandLine(int argc, char ** argv,
    was passed to us as the argv array.
 -----------------------------------------------------------------------------*/
     optEntry *option_def;
-        /* Instructions to optParseOptions3 on how to parse our options.
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
          */
     optStruct3 opt;
 
@@ -62,7 +62,7 @@ parseCommandLine(int argc, char ** argv,
     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
 
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
         /* Uses and sets argc, argv, and some of *cmdlineP and others. */
 
     if (!backgroundSpec)
diff --git a/editor/specialty/ppmntsc.c b/editor/specialty/ppmntsc.c
index ae3bcfe9..a721b891 100644
--- a/editor/specialty/ppmntsc.c
+++ b/editor/specialty/ppmntsc.c
@@ -51,9 +51,6 @@
 #include "mallocvar.h"
 #include "shhopt.h"
 
-#define TRUE 1
-#define FALSE 0
-
 enum legalize {RAISE_SAT, LOWER_SAT, ALREADY_LEGAL};
    /* The actions that make a legal pixel */
 
@@ -70,6 +67,61 @@ struct cmdlineInfo {
 
 
 
+static void
+parseCommandLine(int argc, const char ** argv,
+                 struct cmdlineInfo * const cmdlineP) {
+/*----------------------------------------------------------------------------
+   Note that many of the strings that this function returns in the
+   *cmdlineP structure are actually in the supplied argv array.  And
+   sometimes, one of these strings is actually just a suffix of an entry
+   in argv!
+-----------------------------------------------------------------------------*/
+    optStruct3 opt;
+    optEntry *option_def;
+        /* Instructions to OptParseOptions on how to parse our options.
+         */
+    unsigned int option_def_index;
+    unsigned int legalonly, illegalonly, correctedonly;
+
+    MALLOCARRAY(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    OPTENT3('v', "verbose",        OPT_FLAG, NULL,  &cmdlineP->verbose,  0);
+    OPTENT3('V', "debug",          OPT_FLAG, NULL,  &cmdlineP->debug,    0);
+    OPTENT3('p', "pal",            OPT_FLAG, NULL,  &cmdlineP->pal,      0);
+    OPTENT3('l', "legalonly",      OPT_FLAG, NULL,  &legalonly,           0);
+    OPTENT3('i', "illegalonly",    OPT_FLAG, NULL,  &illegalonly,         0);
+    OPTENT3('c', "correctedonly",  OPT_FLAG, NULL,  &correctedonly,       0);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = true;
+    opt.allowNegNum = false;
+
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+
+    if (argc - 1 == 0)
+        cmdlineP->inputFilename = "-";  /* he wants stdin */
+    else if (argc - 1 == 1)
+        cmdlineP->inputFilename = argv[1];
+    else 
+        pm_error("Too many arguments.  The only arguments accepted "
+                 "are the mask color and optional input file specification");
+
+    if (legalonly + illegalonly + correctedonly > 1)
+        pm_error("--legalonly, --illegalonly, and --correctedonly are "
+                 "conflicting options.  Specify at most one of these.");
+        
+    if (legalonly) 
+        cmdlineP->output = LEGAL_ONLY;
+    else if (illegalonly) 
+        cmdlineP->output = ILLEGAL_ONLY;
+    else if (correctedonly) 
+        cmdlineP->output = CORRECTED_ONLY;
+    else 
+        cmdlineP->output = ALL;
+}
+
+
 
 static void 
 rgbtoyiq(const int r, const int g, const int b, 
@@ -118,39 +170,39 @@ yuvtorgb(const double y, const double u, const double v,
 
 
 static void
-make_legal_yiq(const double y, const double i, const double q, 
-               double * const y_new_p, 
-               double * const i_new_p, 
-               double * const q_new_p,
-               enum legalize * const action_p
-    ) {
+makeLegalYiq(double          const y,
+             double          const i,
+             double          const q, 
+             double *        const yNewP, 
+             double *        const iNewP, 
+             double *        const qNewP,
+             enum legalize * const actionP) {
     
-    double sat_old, sat_new;
+    double satOld, satNew;
     /*
      * I and Q are legs of a right triangle.  Saturation is the hypotenuse.
      */
-    sat_old = sqrt(i*i + q*q);
-    if (y+sat_old > 1.0) {
-        const double diff = 0.5*((y+sat_old) - 1.0);
-        *y_new_p = y - diff;
-        sat_new = 1.0 - *y_new_p;
-        *i_new_p = i*(sat_new/sat_old);
-        *q_new_p = q*(sat_new/sat_old);
-        *action_p = LOWER_SAT;
-    } else if (y-sat_old <= -0.251) {
-        const double diff = 0.5*((sat_old-y) - 0.251);
-        *y_new_p = y + diff;
-        sat_new = 0.250 + *y_new_p;
-        *i_new_p = i*(sat_new/sat_old);
-        *q_new_p = q*(sat_new/sat_old);
-        *action_p = RAISE_SAT;
+    satOld = sqrt(SQR(i) + SQR(q));
+    if (y+satOld > 1.0) {
+        const double diff = 0.5*((y + satOld) - 1.0);
+        *yNewP = y - diff;
+        satNew = 1.0 - *yNewP;
+        *iNewP = i * (satNew/satOld);
+        *qNewP = q * (satNew/satOld);
+        *actionP = LOWER_SAT;
+    } else if (y - satOld <= -0.251) {
+        const double diff = 0.5*((satOld - y) - 0.251);
+        *yNewP = y + diff;
+        satNew = 0.250 + *yNewP;
+        *iNewP = i * (satNew/satOld);
+        *qNewP = q * (satNew/satOld);
+        *actionP = RAISE_SAT;
     } else {
-        *y_new_p = y;
-        *i_new_p = i;
-        *q_new_p = q;
-        *action_p = ALREADY_LEGAL;
+        *yNewP = y;
+        *iNewP = i;
+        *qNewP = q;
+        *actionP = ALREADY_LEGAL;
     }
-    return;
 }
 
 
@@ -206,7 +258,7 @@ make_legal_yiq_i(const int r_in, const int g_in, const int b_in,
      * Convert to YIQ and compute the new saturation.
      */
     rgbtoyiq(r_in, g_in, b_in, &y, &i, &q);
-    make_legal_yiq(y, i, q, &y_new, &i_new, &q_new, action_p);
+    makeLegalYiq(y, i, q, &y_new, &i_new, &q_new, action_p);
     if (*action_p != ALREADY_LEGAL)
         /*
          * Given the new I and Q, compute new RGB values.
@@ -295,204 +347,155 @@ make_legal_yuv_b(const pixel input,
 
 
 static void 
-report_mapping(const pixel old_pixel, const pixel new_pixel) {
+reportMapping(pixel const oldPixel,
+              pixel const newPixel) {
 /*----------------------------------------------------------------------------
-  Assuming old_pixel and new_pixel are input and output pixels,
+  Assuming oldPixel and newPixel are input and output pixels,
   tell the user that we changed a pixel to make it legal, if in fact we
   did and it isn't the same change that we just reported.
 -----------------------------------------------------------------------------*/
-    static pixel last_changed_pixel;
-    static int first_time = TRUE;
-
-    if (!PPM_EQUAL(old_pixel, new_pixel) && 
-        (first_time || PPM_EQUAL(old_pixel, last_changed_pixel))) {
-        pm_message("Mapping %d %d %d -> %d %d %d\n",
-                   PPM_GETR(old_pixel),
-                   PPM_GETG(old_pixel),
-                   PPM_GETB(old_pixel),
-                   PPM_GETR(new_pixel),
-                   PPM_GETG(new_pixel),
-                   PPM_GETB(new_pixel)
+    static pixel lastChangedPixel;
+    static bool firstTime = true;
+
+    if (!PPM_EQUAL(oldPixel, newPixel) && 
+        (firstTime || PPM_EQUAL(oldPixel, lastChangedPixel))) {
+        pm_message("Mapping %u %u %u -> %u %u %u\n",
+                   PPM_GETR(oldPixel),
+                   PPM_GETG(oldPixel),
+                   PPM_GETB(oldPixel),
+                   PPM_GETR(newPixel),
+                   PPM_GETG(newPixel),
+                   PPM_GETB(newPixel)
             );
 
-        last_changed_pixel = old_pixel;
-        first_time = FALSE;
+        lastChangedPixel = oldPixel;
+        firstTime = false;
     }    
 }
 
 
 
 static void
-convert_one_image(FILE * const ifp, struct cmdlineInfo const cmdline, 
-                  bool * const eofP, 
-                  int * const hicountP, int * const locountP) {
+convertOneImage(FILE *             const ifP,
+                struct cmdlineInfo const cmdline, 
+                unsigned int *     const hiCountP,
+                unsigned int *     const loCountP) {
 
     /* Parameters of input image: */
     int rows, cols;
     pixval maxval;
     int format;
 
-    ppm_readppminit(ifp, &cols, &rows, &maxval, &format);
-    ppm_writeppminit(stdout, cols, rows, maxval, FALSE);
+    ppm_readppminit(ifP, &cols, &rows, &maxval, &format);
+    ppm_writeppminit(stdout, cols, rows, maxval, 0);
     {
-        pixel* const input_row = ppm_allocrow(cols);
-        pixel* const output_row = ppm_allocrow(cols);
-        pixel last_illegal_pixel;
-        /* Value of the illegal pixel we most recently processed */
+        pixel * const inputRow = ppm_allocrow(cols);
+        pixel * const outputRow = ppm_allocrow(cols);
+
+        pixel lastIllegalPixel;
+            /* Value of the illegal pixel we most recently processed */
         pixel black;
-        /* A constant - black pixel */
+            /* A constant - black pixel */
 
         PPM_ASSIGN(black, 0, 0, 0);
 
-        PPM_ASSIGN(last_illegal_pixel, 0, 0, 0);  /* initial value */
+        PPM_ASSIGN(lastIllegalPixel, 0, 0, 0);  /* initial value */
         {
-            int row;
+            unsigned int row;
 
-            *hicountP = 0; *locountP = 0;  /* initial values */
+            *hiCountP = 0; *loCountP = 0;  /* initial values */
 
             for (row = 0; row < rows; ++row) {
-                int col;
-                ppm_readppmrow(ifp, input_row, cols, maxval, format);
+                unsigned int col;
+                ppm_readppmrow(ifP, inputRow, cols, maxval, format);
                 for (col = 0; col < cols; ++col) {
                     pixel corrected;
-                    /* Corrected or would-be corrected value for pixel */
+                        /* Corrected or would-be corrected value for pixel */
                     enum legalize action;
-                    /* What action was used to make pixel legal */
+                        /* What action was used to make pixel legal */
                     if (cmdline.pal)
-                        make_legal_yuv_b(input_row[col],
+                        make_legal_yuv_b(inputRow[col],
                                          &corrected,
                                          &action);
                     else
-                        make_legal_yiq_b(input_row[col],
+                        make_legal_yiq_b(inputRow[col],
                                          &corrected,
                                          &action);
                         
                     if (action == LOWER_SAT) 
-                        (*hicountP)++;
+                        ++*hiCountP;
                     if (action == RAISE_SAT)
-                        (*locountP)++;
-                    if (cmdline.debug) report_mapping(input_row[col],
-                                                      corrected);
+                        ++*loCountP;
+                    if (cmdline.debug)
+                        reportMapping(inputRow[col], corrected);
                     switch (cmdline.output) {
                     case ALL:
-                        output_row[col] = corrected;
+                        outputRow[col] = corrected;
                         break;
                     case LEGAL_ONLY:
-                        output_row[col] = (action == ALREADY_LEGAL) ?
-                            input_row[col] : black;
+                        outputRow[col] = (action == ALREADY_LEGAL) ?
+                            inputRow[col] : black;
                         break;
                     case ILLEGAL_ONLY:
-                        output_row[col] = (action != ALREADY_LEGAL) ?
-                            input_row[col] : black;
+                        outputRow[col] = (action != ALREADY_LEGAL) ?
+                            inputRow[col] : black;
                         break;
                     case CORRECTED_ONLY:
-                        output_row[col] = (action != ALREADY_LEGAL) ?
+                        outputRow[col] = (action != ALREADY_LEGAL) ?
                             corrected : black;
                         break;
                     }
                 }
-                ppm_writeppmrow(stdout, output_row, cols, maxval, FALSE);
+                ppm_writeppmrow(stdout, outputRow, cols, maxval, 0);
             }
         }
-        ppm_freerow(output_row);
-        ppm_freerow(input_row);
+        ppm_freerow(outputRow);
+        ppm_freerow(inputRow);
     }
 }
 
 
-static void
-parseCommandLine(int argc, char ** argv,
-                 struct cmdlineInfo * const cmdlineP) {
-/*----------------------------------------------------------------------------
-   Note that many of the strings that this function returns in the
-   *cmdlineP structure are actually in the supplied argv array.  And
-   sometimes, one of these strings is actually just a suffix of an entry
-   in argv!
------------------------------------------------------------------------------*/
-    optStruct3 opt;
-    optEntry *option_def;
-        /* Instructions to OptParseOptions on how to parse our options.
-         */
-    unsigned int option_def_index;
-    unsigned int legalonly, illegalonly, correctedonly;
-
-    MALLOCARRAY(option_def, 100);
-
-    option_def_index = 0;   /* incremented by OPTENTRY */
-    OPTENT3('v', "verbose",        OPT_FLAG, NULL,  &cmdlineP->verbose,  0);
-    OPTENT3('V', "debug",          OPT_FLAG, NULL,  &cmdlineP->debug,    0);
-    OPTENT3('p', "pal",            OPT_FLAG, NULL,  &cmdlineP->pal,      0);
-    OPTENT3('l', "legalonly",      OPT_FLAG, NULL,  &legalonly,           0);
-    OPTENT3('i', "illegalonly",    OPT_FLAG, NULL,  &illegalonly,         0);
-    OPTENT3('c', "correctedonly",  OPT_FLAG, NULL,  &correctedonly,       0);
-
-    opt.opt_table = option_def;
-    opt.short_allowed = TRUE;
-    opt.allowNegNum = FALSE;
-
-    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
-
-    if (argc - 1 == 0)
-        cmdlineP->inputFilename = "-";  /* he wants stdin */
-    else if (argc - 1 == 1)
-        cmdlineP->inputFilename = argv[1];
-    else 
-        pm_error("Too many arguments.  The only arguments accepted "
-                 "are the mask color and optional input file specification");
-
-    if (legalonly + illegalonly + correctedonly > 1)
-        pm_error("--legalonly, --illegalonly, and --correctedonly are "
-                 "conflicting options.  Specify at most one of these.");
-        
-    if (legalonly) 
-        cmdlineP->output = LEGAL_ONLY;
-    else if (illegalonly) 
-        cmdlineP->output = ILLEGAL_ONLY;
-    else if (correctedonly) 
-        cmdlineP->output = CORRECTED_ONLY;
-    else 
-        cmdlineP->output = ALL;
-}
-
-
 
 int
-main(int argc, char **argv) {
+main(int argc, const char **argv) {
     
     struct cmdlineInfo cmdline;
     FILE * ifP;
-    int total_hicount, total_locount;
-    int image_count;
+    unsigned int totalHiCount, totalLoCount;
+    unsigned int imageCount;
 
-    bool eof;
+    int eof;
 
-    ppm_init(&argc, argv);
+    pm_proginit(&argc, argv);
 
     parseCommandLine(argc, argv, &cmdline);
 
     ifP = pm_openr(cmdline.inputFilename);
+    
+    imageCount = 0;    /* initial value */
+    totalHiCount = 0;  /* initial value */
+    totalLoCount = 0;  /* initial value */
 
-    image_count = 0;    /* initial value */
-    total_hicount = 0;  /* initial value */
-    total_locount = 0;  /* initial value */
-
-    eof = FALSE;
+    eof = false;
     while (!eof) {
-        int hicount, locount;
-        convert_one_image(ifP, cmdline, &eof, &hicount, &locount);
-        image_count++;
-        total_hicount += hicount;
-        total_locount += locount;
+        unsigned int hiCount, loCount;
+
+        convertOneImage(ifP, cmdline, &hiCount, &loCount);
+
+        ++imageCount;
+        totalHiCount += hiCount;
+        totalLoCount += loCount;
+
         ppm_nextimage(ifP, &eof);
     }
 
 
 	if (cmdline.verbose) {
-        pm_message("%d images processed.", image_count);
-        pm_message("%d pixels were above the saturation limit.", 
-                   total_hicount);
-        pm_message("%d pixels were below the saturation limit.", 
-                   total_locount);
+        pm_message("%u images processed.", imageCount);
+        pm_message("%u pixels were above the saturation limit.", 
+                   totalHiCount);
+        pm_message("%u pixels were below the saturation limit.", 
+                   totalLoCount);
     }
     
     pm_close(ifP);
diff --git a/editor/specialty/ppmrelief.c b/editor/specialty/ppmrelief.c
index 1c408aec..14a6d0a8 100644
--- a/editor/specialty/ppmrelief.c
+++ b/editor/specialty/ppmrelief.c
@@ -1,4 +1,4 @@
-/* ppmrelief.c - generate a relief map of a portable pixmap
+/* ppmrelief.c - generate a relief map of a PPM image
 **
 ** Copyright (C) 1990 by Wilson H. Bent, Jr.
 **
@@ -11,86 +11,104 @@
 */
 
 #include <stdio.h>
+
 #include "pm_c_util.h"
 #include "ppm.h"
 
+
+
+static pixval
+clip(int    const p,
+     pixval const maxval) {
+
+    return MAX(0, MIN(maxval, p));
+}
+
+
+
 int
-main(int argc, char * argv[]) {
-
-    FILE* ifp;
-    pixel** inputbuf;
-    pixel* outputrow;
-    int argn, rows, cols, format, row;
-    register int col;
-    pixval maxval, mv2;
+main(int argc, const char * argv[]) {
+
+    FILE * ifP;
+    pixel ** inputbuf;
+    pixel * outputrow;
+    int argn, format, rows, cols;
+    unsigned int row, col;
+    pixval maxval;
     const char* const usage = "[ppmfile]";
 
-    ppm_init( &argc, argv );
+    pm_proginit(&argc, argv);
 
     argn = 1;
 
     if ( argn != argc ) {
-        ifp = pm_openr( argv[argn] );
+        ifP = pm_openr( argv[argn] );
         ++argn;
     } else
-        ifp = stdin;
+        ifP = stdin;
 
     if ( argn != argc )
         pm_usage( usage );
-    
-    ppm_readppminit( ifp, &cols, &rows, &maxval, &format );
+
+    ppm_readppminit(ifP, &cols, &rows, &maxval, &format );
 
     if (cols < 3 || rows < 3 )
         pm_error("Input image too small: %u x %u.  Must be at least 3x3",
                   cols, rows);
 
-    mv2 = maxval / 2;
-
     /* Allocate space for 3 input rows, plus an output row. */
-    inputbuf = ppm_allocarray( cols, 3 );
-    outputrow = ppm_allocrow( cols );
+    inputbuf  = ppm_allocarray(cols, 3);
+    outputrow = ppm_allocrow(cols);
 
-    ppm_writeppminit( stdout, cols, rows, maxval, 0 );
+    ppm_writeppminit(stdout, cols, rows, maxval, 0);
 
     /* Read in the first two rows. */
-    ppm_readppmrow( ifp, inputbuf[0], cols, maxval, format );
-    ppm_readppmrow( ifp, inputbuf[1], cols, maxval, format );
+    ppm_readppmrow(ifP, inputbuf[0], cols, maxval, format);
+    ppm_readppmrow(ifP, inputbuf[1], cols, maxval, format);
 
     /* Write out the first row, all zeros. */
-    for ( col = 0; col < cols; ++col )
+    for (col = 0; col < cols; ++col)
         PPM_ASSIGN( outputrow[col], 0, 0, 0 );
-    ppm_writeppmrow( stdout, outputrow, cols, maxval, 0 );
+
+    ppm_writeppmrow(stdout, outputrow, cols, maxval, 0);
 
     /* Now the rest of the image - read in the 3rd row of inputbuf,
-    ** and convolve with the first row into the output buffer.
+       and convolve with the first row into the output buffer.
     */
-    for ( row = 2 ; row < rows; ++row ) {
-        pixval r, g, b;
-        int rowa, rowb;
-
-        rowa = row % 3;
-        rowb = (row + 2) % 3;
-        ppm_readppmrow( ifp, inputbuf[rowa], cols, maxval, format );
-        
-        for ( col = 0; col < cols - 2; ++col ) {
-            r = MAX(0, MIN(maxval, PPM_GETR( inputbuf[rowa][col] ) +
-                           ( mv2 - PPM_GETR( inputbuf[rowb][col + 2] ) )));
-            g = MAX(0, MIN(maxval, PPM_GETG( inputbuf[rowa][col] ) +
-                           ( mv2 - PPM_GETG( inputbuf[rowb][col + 2] ) )));
-            b = MAX(0, MIN(maxval, PPM_GETB( inputbuf[rowa][col] ) +
-                           ( mv2 - PPM_GETB( inputbuf[rowb][col + 2] ) )));
-            PPM_ASSIGN( outputrow[col + 1], r, g, b );
+    for (row = 2 ; row < rows; ++row) {
+        pixval       const mv2 = maxval / 2;
+        unsigned int const rowa = row % 3;
+        unsigned int const rowb = (rowa + 2) % 3;
+
+        ppm_readppmrow(ifP, inputbuf[rowa], cols, maxval, format);
+
+        for (col = 0; col < cols - 2; ++col) {
+            pixel const inputA = inputbuf[rowa][col];
+            pixel const inputB = inputbuf[rowb][col + 2];
+            
+            pixval const r =
+                clip(PPM_GETR(inputA) + (mv2 - PPM_GETR(inputB)), maxval);
+            pixval const g =
+                clip(PPM_GETG(inputA) + (mv2 - PPM_GETG(inputB)), maxval);
+            pixval const b =
+                clip(PPM_GETB(inputA) + (mv2 - PPM_GETB(inputB)), maxval);
+
+            PPM_ASSIGN(outputrow[col + 1], r, g, b);
         }
-        ppm_writeppmrow( stdout, outputrow, cols, maxval, 0 );
+        ppm_writeppmrow(stdout, outputrow, cols, maxval, 0);
     }
 
     /* And write the last row, zeros again. */
-    for ( col = 0; col < cols; ++col )
-        PPM_ASSIGN( outputrow[col], 0, 0, 0 );
-    ppm_writeppmrow( stdout, outputrow, cols, maxval, 0 );
+    for (col = 0; col < cols; ++col)
+        PPM_ASSIGN(outputrow[col], 0, 0, 0);
+
+    ppm_writeppmrow(stdout, outputrow, cols, maxval, 0);
+
+    ppm_freerow(outputrow);
+    ppm_freearray(inputbuf, 3);
 
-    pm_close( ifp );
-    pm_close( stdout );
+    pm_close(ifP);
+    pm_close(stdout);
 
-    exit( 0 );
+    return 0;
 }