about summary refs log tree commit diff
path: root/editor
diff options
context:
space:
mode:
authorgiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2006-08-19 03:12:28 +0000
committergiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2006-08-19 03:12:28 +0000
commit1fd361a1ea06e44286c213ca1f814f49306fdc43 (patch)
tree64c8c96cf54d8718847339a403e5e67b922e8c3f /editor
downloadnetpbm-mirror-1fd361a1ea06e44286c213ca1f814f49306fdc43.tar.gz
netpbm-mirror-1fd361a1ea06e44286c213ca1f814f49306fdc43.tar.xz
netpbm-mirror-1fd361a1ea06e44286c213ca1f814f49306fdc43.zip
Create Subversion repository
git-svn-id: http://svn.code.sf.net/p/netpbm/code/trunk@1 9d0c8265-081b-0410-96cb-a4ca84ce46f8
Diffstat (limited to 'editor')
-rw-r--r--editor/Makefile86
-rw-r--r--editor/dithers.h87
-rw-r--r--editor/pamaddnoise.c486
-rw-r--r--editor/pamcomp.c619
-rw-r--r--editor/pamcut.c573
-rw-r--r--editor/pamcut.test10
-rw-r--r--editor/pamdeinterlace.c132
-rw-r--r--editor/pamdice.c494
-rw-r--r--editor/pamdither.c319
-rw-r--r--editor/pamditherbw.c743
-rw-r--r--editor/pamedge.c203
-rw-r--r--editor/pamenlarge.c117
-rw-r--r--editor/pamenlarge.test8
-rw-r--r--editor/pamflip.c910
-rw-r--r--editor/pamflip.test12
-rw-r--r--editor/pamfunc.c221
-rw-r--r--editor/pammasksharpen.c192
-rw-r--r--editor/pammixinterlace.c173
-rw-r--r--editor/pamoil.c137
-rw-r--r--editor/pamperspective.c1331
-rw-r--r--editor/pampop9.c108
-rw-r--r--editor/pamscale.c2149
-rwxr-xr-xeditor/pamstretch-gen80
-rw-r--r--editor/pamstretch.c408
-rw-r--r--editor/pamthreshold.c623
-rw-r--r--editor/pbmclean.c239
-rw-r--r--editor/pbmlife.c114
-rw-r--r--editor/pbmmask.c222
-rw-r--r--editor/pbmpscale.c199
-rw-r--r--editor/pbmreduce.c208
-rw-r--r--editor/pgmabel.c316
-rw-r--r--editor/pgmbentley.c64
-rw-r--r--editor/pgmdeshadow.c343
-rw-r--r--editor/pgmenhance.c112
-rw-r--r--editor/pgmmedian.c462
-rw-r--r--editor/pgmmorphconv.c253
-rw-r--r--editor/pnmalias.c250
-rw-r--r--editor/pnmcat.c427
-rw-r--r--editor/pnmcomp.c459
-rw-r--r--editor/pnmconvol.c1989
-rw-r--r--editor/pnmcrop.c613
-rw-r--r--editor/pnmcut.c427
-rwxr-xr-xeditor/pnmflip80
-rw-r--r--editor/pnmgamma.c753
-rw-r--r--editor/pnmhisteq.c416
-rw-r--r--editor/pnmindex.c638
-rwxr-xr-xeditor/pnmindex.csh189
-rwxr-xr-xeditor/pnmindex.sh215
-rw-r--r--editor/pnminvert.c115
-rw-r--r--editor/pnminvert.test15
-rwxr-xr-xeditor/pnmmargin88
-rw-r--r--editor/pnmmontage.c439
-rw-r--r--editor/pnmnlfilt.c1028
-rw-r--r--editor/pnmnorm.c620
-rw-r--r--editor/pnmpad.c387
-rw-r--r--editor/pnmpaste.c185
-rwxr-xr-xeditor/pnmquant275
-rw-r--r--editor/pnmremap.c872
-rw-r--r--editor/pnmrotate.c808
-rw-r--r--editor/pnmscale.c748
-rw-r--r--editor/pnmscalefixed.c590
-rw-r--r--editor/pnmshear.c227
-rw-r--r--editor/pnmsmooth.README21
-rw-r--r--editor/pnmsmooth.c241
-rw-r--r--editor/pnmstitch.c2408
-rw-r--r--editor/pnmtile.c63
-rw-r--r--editor/ppm3d.c138
-rw-r--r--editor/ppmbrighten.c337
-rw-r--r--editor/ppmchange.c232
-rw-r--r--editor/ppmcolormask.c245
-rw-r--r--editor/ppmdim.c112
-rw-r--r--editor/ppmdist.c170
-rw-r--r--editor/ppmdither.c309
-rw-r--r--editor/ppmdraw.c885
-rwxr-xr-xeditor/ppmfade309
-rw-r--r--editor/ppmflash.c114
-rw-r--r--editor/ppmglobe.c172
-rw-r--r--editor/ppmlabel.c212
-rw-r--r--editor/ppmmix.c131
-rw-r--r--editor/ppmntsc.c499
-rwxr-xr-xeditor/ppmquant30
-rwxr-xr-xeditor/ppmquantall97
-rw-r--r--editor/ppmquantall.csh57
-rw-r--r--editor/ppmrelief.c90
-rwxr-xr-xeditor/ppmshadow273
-rw-r--r--editor/ppmshadow.doc627
-rw-r--r--editor/ppmshift.c137
-rw-r--r--editor/ppmspread.c127
-rw-r--r--editor/ppmtv.c105
89 files changed, 33747 insertions, 0 deletions
diff --git a/editor/Makefile b/editor/Makefile
new file mode 100644
index 00000000..18165666
--- /dev/null
+++ b/editor/Makefile
@@ -0,0 +1,86 @@
+ifeq ($(SRCDIR)x,x)
+  SRCDIR = $(CURDIR)/..
+  BUILDDIR = $(SRCDIR)
+endif
+SUBDIR = editor
+VPATH=.:$(SRCDIR)/$(SUBDIR)
+
+include $(BUILDDIR)/Makefile.config
+
+# 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.
+# That way, if there is a problem with a dependency, we can still
+# successfully build all the stuff that doesn't depend upon it.
+# This package is so big, it's useful even when some parts won't 
+# build.
+
+PORTBINARIES = pamaddnoise pamcomp pamcut \
+	       pamdeinterlace pamdice pamditherbw pamedge \
+	       pamenlarge \
+	       pamflip pamfunc pammasksharpen pammixinterlace \
+	       pamoil pamperspective pampop9 \
+	       pamscale pamstretch pamthreshold \
+	       pbmclean pbmlife pbmmask pbmpscale pbmreduce \
+	       pgmabel pgmbentley pgmdeshadow pgmenhance \
+	       pgmmedian pgmmorphconv \
+	       pnmalias pnmcat pnmcomp pnmconvol pnmcrop pnmcut \
+	       pnmgamma \
+	       pnmhisteq pnmindex pnminvert pnmmontage \
+	       pnmnlfilt pnmnorm pnmpad pnmpaste \
+	       pnmremap pnmrotate \
+	       pnmscale pnmscalefixed pnmshear pnmsmooth pnmstitch pnmtile \
+	       ppm3d ppmbrighten ppmchange ppmcolormask \
+	       ppmdim ppmdist ppmdither ppmdraw \
+	       ppmflash ppmglobe ppmlabel ppmmix \
+	       ppmntsc ppmrelief ppmshift ppmspread ppmtv
+
+# We don't include programs that have special library dependencies in the
+# merge scheme, because we don't want those dependencies to prevent us
+# from building all the other programs.
+
+NOMERGEBINARIES = 
+MERGEBINARIES = $(PORTBINARIES)
+
+
+BINARIES = $(MERGEBINARIES) $(NOMERGEBINARIES)
+SCRIPTS = pnmflip ppmfade ppmquant ppmquantall ppmshadow \
+	  pamstretch-gen pnmmargin pnmquant 
+
+OBJECTS = $(BINARIES:%=%.o)
+
+MERGE_OBJECTS = $(MERGEBINARIES:%=%.o2)
+
+.PHONY: all
+all: $(BINARIES)
+
+include $(SRCDIR)/Makefile.common
+
+install.bin: install.bin.local
+
+.PHONY: install.bin.local
+install.bin.local: $(PKGDIR)/bin
+# Remember that $(SYMLINK) might just be a copy command.
+# backward compatibility: program used to be named pnmnoraw
+# backward compatibility: program used to be pnminterp
+	cd $(PKGDIR)/bin ; \
+	rm -f pnminterp; \
+	$(SYMLINK) pamstretch$(EXE) pnminterp
+# pamoil replaced pgmoil in June 2001.
+	cd $(PKGDIR)/bin ; \
+	rm -f pgmoil ; \
+	$(SYMLINK) pamoil$(EXE) pgmoil
+# In March 2002, pnmnorm replaced ppmnorm and pgmnorm
+	cd $(PKGDIR)/bin ; \
+	rm -f ppmnorm ; \
+	$(SYMLINK) pnmnorm$(EXE) ppmnorm 
+	cd $(PKGDIR)/bin ; \
+	rm -f pgmnorm ; \
+	$(SYMLINK) pnmnorm$(EXE) pgmnorm
+# In March 2003, pamedge replaced pgmedge
+	cd $(PKGDIR)/bin ; \
+	rm -f pgmedge ; \
+	$(SYMLINK) pamedge$(EXE) pgmedge
+# In October 2004, pamenlarge replaced pnmenlarge
+	cd $(PKGDIR)/bin ; \
+	rm -f pnmenlarge ; \
+	$(SYMLINK) pamenlarge$(EXE) pnmenlarge
diff --git a/editor/dithers.h b/editor/dithers.h
new file mode 100644
index 00000000..24a9fb39
--- /dev/null
+++ b/editor/dithers.h
@@ -0,0 +1,87 @@
+/*
+** dithers.h
+**
+** Here are some dithering matrices.  They are all taken from "Digital
+** Halftoning" by Robert Ulichney, MIT Press, ISBN 0-262-21009-6.
+*/
+
+
+#if 0
+/*
+** Order-6 ordered dithering matrix.  Note that smaller ordered dithers
+** have no advantage over larger ones, so use dither8 instead.
+*/
+static int const dither6[8][8] = {
+  {  1, 59, 15, 55,  2, 56, 12, 52 },
+  { 33, 17, 47, 31, 34, 18, 44, 28 },
+  {  9, 49,  5, 63, 10, 50,  6, 60 },
+  { 41, 25, 37, 21, 42, 26, 38, 22 },
+  {  3, 57, 13, 53,  0, 58, 14, 54 },
+  { 35, 19, 45, 29, 32, 16, 46, 30 },
+  { 11, 51,  7, 61,  8, 48,  4, 62 },
+  { 43, 27, 39, 23, 40, 24, 36, 20 }
+  };
+#endif
+
+/* Order-8 ordered dithering matrix. */
+static int const dither8[16][16] = {
+  {   1,235, 59,219, 15,231, 55,215,  2,232, 56,216, 12,228, 52,212},
+  { 129, 65,187,123,143, 79,183,119,130, 66,184,120,140, 76,180,116},
+  {  33,193, 17,251, 47,207, 31,247, 34,194, 18,248, 44,204, 28,244},
+  { 161, 97,145, 81,175,111,159, 95,162, 98,146, 82,172,108,156, 92},
+  {   9,225, 49,209,  5,239, 63,223, 10,226, 50,210,  6,236, 60,220},
+  { 137, 73,177,113,133, 69,191,127,138, 74,178,114,134, 70,188,124},
+  {  41,201, 25,241, 37,197, 21,255, 42,202, 26,242, 38,198, 22,252},
+  { 169,105,153, 89,165,101,149, 85,170,106,154, 90,166,102,150, 86},
+  {   3,233, 57,217, 13,229, 53,213,  0,234, 58,218, 14,230, 54,214},
+  { 131, 67,185,121,141, 77,181,117,128, 64,186,122,142, 78,182,118},
+  {  35,195, 19,249, 45,205, 29,245, 32,192, 16,250, 46,206, 30,246},
+  { 163, 99,147, 83,173,109,157, 93,160, 96,144, 80,174,110,158, 94},
+  {  11,227, 51,211,  7,237, 61,221,  8,224, 48,208,  4,238, 62,222},
+  { 139, 75,179,115,135, 71,189,125,136, 72,176,112,132, 68,190,126},
+  {  43,203, 27,243, 39,199, 23,253, 40,200, 24,240, 36,196, 20,254},
+  { 171,107,155, 91,167,103,151, 87,168,104,152, 88,164,100,148, 84} 
+};
+
+/* Order-3 clustered dithering matrix. */
+static int const cluster3[6][6] = {
+  {  9,11,10, 8, 6, 7},
+  { 12,17,16, 5, 0, 1},
+  { 13,14,15, 4, 3, 2},
+  {  8, 6, 7, 9,11,10},
+  {  5, 0, 1,12,17,16},
+  {  4, 3, 2,13,14,15}
+};
+
+/* Order-4 clustered dithering matrix. */
+static int const cluster4[8][8] = {
+  { 18,20,19,16,13,11,12,15},
+  { 27,28,29,22, 4, 3, 2, 9},
+  { 26,31,30,21, 5, 0, 1,10},
+  { 23,25,24,17, 8, 6, 7,14},
+  { 13,11,12,15,18,20,19,16},
+  {  4, 3, 2, 9,27,28,29,22},
+  {  5, 0, 1,10,26,31,30,21},
+  {  8, 6, 7,14,23,25,24,17}
+};
+
+/* Order-8 clustered dithering matrix. */
+static int const cluster8[16][16] = {
+   { 64, 69, 77, 87, 86, 76, 68, 67, 63, 58, 50, 40, 41, 51, 59, 60},
+   { 70, 94,100,109,108, 99, 93, 75, 57, 33, 27, 18, 19, 28, 34, 52},
+   { 78,101,114,116,115,112, 98, 83, 49, 26, 13, 11, 12, 15, 29, 44},
+   { 88,110,123,124,125,118,107, 85, 39, 17,  4,  3,  2,  9, 20, 42},
+   { 89,111,122,127,126,117,106, 84, 38, 16,  5,  0,  1, 10, 21, 43},
+   { 79,102,119,121,120,113, 97, 82, 48, 25,  8,  6,  7, 14, 30, 45},
+   { 71, 95,103,104,105, 96, 92, 74, 56, 32, 24, 23, 22, 31, 35, 53},
+   { 65, 72, 80, 90, 91, 81, 73, 66, 62, 55, 47, 37, 36, 46, 54, 61},
+   { 63, 58, 50, 40, 41, 51, 59, 60, 64, 69, 77, 87, 86, 76, 68, 67},
+   { 57, 33, 27, 18, 19, 28, 34, 52, 70, 94,100,109,108, 99, 93, 75},
+   { 49, 26, 13, 11, 12, 15, 29, 44, 78,101,114,116,115,112, 98, 83},
+   { 39, 17,  4,  3,  2,  9, 20, 42, 88,110,123,124,125,118,107, 85},
+   { 38, 16,  5,  0,  1, 10, 21, 43, 89,111,122,127,126,117,106, 84},
+   { 48, 25,  8,  6,  7, 14, 30, 45, 79,102,119,121,120,113, 97, 82},
+   { 56, 32, 24, 23, 22, 31, 35, 53, 71, 95,103,104,105, 96, 92, 74},
+   { 62, 55, 47, 37, 36, 46, 54, 61, 65, 72, 80, 90, 91, 81, 73, 66}
+};
+
diff --git a/editor/pamaddnoise.c b/editor/pamaddnoise.c
new file mode 100644
index 00000000..9c2d12f7
--- /dev/null
+++ b/editor/pamaddnoise.c
@@ -0,0 +1,486 @@
+/*
+** 
+** Add gaussian, multiplicative gaussian, impulse, laplacian or 
+** poisson noise to a portable anymap.
+** 
+** Version 1.0  November 1995
+**
+** Copyright (C) 1995 by Mike Burns (burns@cac.psu.edu)
+**
+** Adapted to Netpbm 2005.08.09, by Bryan Henderson
+**
+** 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.
+*/
+
+/* References
+** ----------
+** "Adaptive Image Restoration in Signal-Dependent Noise" by R. Kasturi
+** Institute for Electronic Science, Texas Tech University  1982
+**
+** "Digital Image Processing Algorithms" by Ioannis Pitas
+** Prentice Hall, 1993  ISBN 0-13-145814-0
+*/
+
+#define _XOPEN_SOURCE   /* get M_PI in math.h */
+
+#include <math.h>
+
+#include "pm_c_util.h"
+#include "pam.h"
+
+#define RANDOM_MASK 0x7FFF  /* only compare lower 15 bits.  Stupid PCs. */
+
+static double const EPSILON = 1.0e-5;
+static double const arand = 32767.0;      /* 2^15-1 in case stoopid computer */
+
+enum noiseType {
+    GAUSSIAN,
+    IMPULSE,  /* aka salt and pepper noise */
+    LAPLACIAN,
+    MULTIPLICATIVE_GAUSSIAN,
+    POISSON,
+    MAX_NOISE_TYPES
+};
+
+
+
+static void
+gaussian_noise(sample   const maxval,
+               sample   const origSample,
+               sample * const newSampleP,
+               float    const sigma1,
+               float    const sigma2) {
+/*----------------------------------------------------------------------------
+   Add Gaussian noise.
+
+   Based on Kasturi/Algorithms of the ACM
+-----------------------------------------------------------------------------*/
+
+    double x1, x2, xn, yn;
+    double rawNewSample;
+
+    x1 = (rand() & RANDOM_MASK) / arand; 
+
+    if (x1 == 0.0)
+        x1 = 1.0;
+    x2 = (rand() & RANDOM_MASK) / arand;
+    xn = sqrt(-2.0 * log(x1)) * cos(2.0 * M_PI * x2);
+    yn = sqrt(-2.0 * log(x1)) * sin(2.0 * M_PI * x2);
+    
+    rawNewSample =
+        origSample + (sqrt((double) origSample) * sigma1 * xn) + (sigma2 * yn);
+
+    *newSampleP = MAX(MIN((int)rawNewSample, maxval), 0);
+}
+
+
+
+static void
+impulse_noise(sample   const maxval,
+              sample   const origSample,
+              sample * const newSampleP,
+              float    const tolerance) {
+/*----------------------------------------------------------------------------
+   Add impulse (salt and pepper) noise
+-----------------------------------------------------------------------------*/
+
+    double const low_tol  = tolerance / 2.0;
+    double const high_tol = 1.0 - (tolerance / 2.0);
+    double const sap = (rand() & RANDOM_MASK) / arand; 
+
+    if (sap < low_tol) 
+        *newSampleP = 0;
+    else if ( sap >= high_tol )
+        *newSampleP = maxval;
+}
+
+
+
+static void
+laplacian_noise(sample   const maxval,
+                double   const infinity,
+                sample   const origSample,
+                sample * const newSampleP,
+                float    const lsigma) {
+/*----------------------------------------------------------------------------
+   Add Laplacian noise
+
+   From Pitas' book.
+-----------------------------------------------------------------------------*/
+    double const u = (rand() & RANDOM_MASK) / arand; 
+                
+    double rawNewSample;
+
+    if (u <= 0.5) {
+        if (u <= EPSILON)
+            rawNewSample = origSample - infinity;
+        else
+            rawNewSample = origSample + lsigma * log(2.0 * u);
+    } else {
+        double const u1 = 1.0 - u;
+        if (u1 <= 0.5 * EPSILON)
+            rawNewSample = origSample + infinity;
+        else
+            rawNewSample = origSample - lsigma * log(2.0 * u1);
+    }
+    *newSampleP = MIN(MAX((int)rawNewSample, 0), maxval);
+}
+
+
+
+static void
+multiplicative_gaussian_noise(sample   const maxval,
+                              double   const infinity,
+                              sample   const origSample,
+                              sample * const newSampleP,
+                              float    const mgsigma) {
+/*----------------------------------------------------------------------------
+   Add multiplicative Gaussian noise
+
+   From Pitas' book.
+-----------------------------------------------------------------------------*/
+    double rayleigh, gauss;
+    double rawNewSample;
+
+    {
+        double const uniform = (rand() & RANDOM_MASK) / arand; 
+        if (uniform <= EPSILON)
+            rayleigh = infinity;
+        else
+            rayleigh = sqrt(-2.0 * log( uniform));
+    }
+    {
+        double const uniform = (rand() & RANDOM_MASK) / arand; 
+        gauss = rayleigh * cos(2.0 * M_PI * uniform);
+    }
+    rawNewSample = origSample + (origSample * mgsigma * gauss);
+
+    *newSampleP = MIN(MAX((int)rawNewSample, 0), maxval);
+}
+
+
+
+static void
+poisson_noise(sample   const maxval,
+              sample   const origSample,
+              sample * const newSampleP,
+              float    const lambda) {
+/*----------------------------------------------------------------------------
+   Add Poisson noise
+-----------------------------------------------------------------------------*/
+    double const x  = lambda * origSample;
+    double const x1 = exp(-x);
+
+    double rawNewSample;
+    float rr;
+    unsigned int k;
+
+    rr = 1.0;  /* initial value */
+    k = 0;     /* initial value */
+    rr = rr * ((rand() & RANDOM_MASK) / arand);
+    while (rr > x1) {
+        ++k;
+        rr = rr * ((rand() & RANDOM_MASK) / arand);
+    }
+    rawNewSample = k / lambda;
+
+    *newSampleP = MIN(MAX((int)rawNewSample, 0), maxval);
+}
+
+
+
+int 
+main(int argc, char * argv[]) {
+
+    FILE * ifP;
+    struct pam inpam;
+    struct pam outpam;
+    tuple * tuplerow;
+    const tuple * newtuplerow;
+    unsigned int row;
+    double infinity;
+
+    int argn;
+    const char * inputFilename;
+    int noise_type;
+    int seed;
+    int i;
+    const char * const usage = "[-type noise_type] [-lsigma x] [-mgsigma x] "
+        "[-sigma1 x] [-sigma2 x] [-lambda x] [-seed n] "
+        "[-tolerance ratio] [pgmfile]";
+
+    const char * const noise_name[] = { 
+        "gaussian",
+        "impulse",
+        "laplacian",
+        "multiplicative_gaussian",
+        "poisson"
+    };
+    int const noise_id[] = { 
+        GAUSSIAN,
+        IMPULSE,
+        LAPLACIAN,
+        MULTIPLICATIVE_GAUSSIAN,
+        POISSON
+    };
+    /* minimum number of characters to match noise name for pm_keymatch() */
+    int const noise_compare[] = {
+        1,
+        1,
+        1,
+        1,
+        1
+    };
+
+    /* define default values for configurable options */
+    float lambda = 0.05;        
+    float lsigma = 10.0;
+    float mgsigma = 0.5;
+    float sigma1 = 4.0;
+    float sigma2 = 20.0;
+    float tolerance = 0.10;
+
+    pnm_init(&argc, argv);
+
+    seed = time(NULL) ^ getpid();
+    noise_type = GAUSSIAN;
+
+    argn = 1;
+    while ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' )
+    {
+        if ( pm_keymatch( argv[argn], "-lambda", 3 ) )
+        {
+            ++argn;
+            if ( argn >= argc )
+            {
+                pm_message( 
+                    "incorrect number of arguments for -lambda option" );
+                pm_usage( usage );
+            }
+            else if ( argv[argn][0] == '-' )
+            {
+                pm_message( "invalid argument to -lambda option: %s", 
+                            argv[argn] );
+                pm_usage( usage );
+            }
+            lambda = atof( argv[argn] );
+        }
+        else if ( pm_keymatch( argv[argn], "-lsigma", 3 ) )
+        {
+            ++argn;
+            if ( argn >= argc )
+            {
+                pm_message( 
+                    "incorrect number of arguments for -lsigma option" );
+                pm_usage( usage );
+            }
+            else if ( argv[argn][0] == '-' )
+            {
+                pm_message( "invalid argument to -lsigma option: %s", 
+                            argv[argn] );
+                pm_usage( usage );
+            }
+            lsigma = atof( argv[argn] );
+        }
+        else if ( pm_keymatch( argv[argn], "-mgsigma", 2 ) )
+        {
+            ++argn;
+            if ( argn >= argc )
+            {
+                pm_message( 
+                    "incorrect number of arguments for -mgsigma option" );
+                pm_usage( usage );
+            }
+            else if ( argv[argn][0] == '-' )
+            {
+                pm_message( "invalid argument to -mgsigma option: %s", 
+                            argv[argn] );
+                pm_usage( usage );
+            }
+            mgsigma = atof( argv[argn] );
+        }
+        else if ( pm_keymatch( argv[argn], "-seed", 3 ) )
+        {
+            ++argn;
+            if ( argn >= argc )
+            {
+                pm_message( "incorrect number of arguments for -seed option" );
+                pm_usage( usage );
+            }
+            else if ( argv[argn][0] == '-' )
+            {
+                pm_message( "invalid argument to -seed option: %s", 
+                            argv[argn] );
+                pm_usage( usage );
+            }
+            seed = atoi(argv[argn]);
+        }
+        else if ( pm_keymatch( argv[argn], "-sigma1", 7 ) ||
+                  pm_keymatch( argv[argn], "-s1", 3 ) )
+        {
+            ++argn;
+            if ( argn >= argc )
+            {
+                pm_message( 
+                    "incorrect number of arguments for -sigma1 option" );
+                pm_usage( usage );
+            }
+            else if ( argv[argn][0] == '-' )
+            {
+                pm_message( "invalid argument to -sigma1 option: %s", 
+                            argv[argn] );
+                pm_usage( usage );
+            }
+            sigma1 = atof( argv[argn] );
+        }
+        else if ( pm_keymatch( argv[argn], "-sigma2", 7 ) ||
+                  pm_keymatch( argv[argn], "-s2", 3 ) )
+        {
+            ++argn;
+            if ( argn >= argc )
+            {
+                pm_message( 
+                    "incorrect number of arguments for -sigma2 option" );
+                pm_usage( usage );
+            }
+            else if ( argv[argn][0] == '-' )
+            {
+                pm_message( "invalid argument to -sigma2 option: %s", 
+                            argv[argn] );
+                pm_usage( usage );
+            }
+            sigma2 = atof( argv[argn] );
+        }
+        else if ( pm_keymatch( argv[argn], "-tolerance", 3 ) )
+        {
+            ++argn;
+            if ( argn >= argc )
+            {
+                pm_message( 
+                    "incorrect number of arguments for -tolerance option" );
+                pm_usage( usage );
+            }
+            else if ( argv[argn][0] == '-' )
+            {
+                pm_message( "invalid argument to -tolerance option: %s", 
+                            argv[argn] );
+                pm_usage( usage );
+            }
+            tolerance = atof( argv[argn] );
+        }
+        else if ( pm_keymatch( argv[argn], "-type", 3 ) )
+        {
+            ++argn;
+            if ( argn >= argc )
+            {
+                pm_message( "incorrect number of arguments for -type option" );
+                pm_usage( usage );
+            }
+            else if ( argv[argn][0] == '-' )
+            {
+                pm_message( "invalid argument to -type option: %s", 
+                            argv[argn] );
+                pm_usage( usage );
+            }
+            /* search through list of valid noise types and compare */
+            i = 0;
+            while ( ( i < MAX_NOISE_TYPES ) && 
+                    !pm_keymatch( argv[argn], 
+                                  noise_name[i], noise_compare[i] ) )
+                ++i;
+            if ( i >= MAX_NOISE_TYPES )
+            {
+                pm_message( "invalid argument to -type option: %s", 
+                            argv[argn] );
+                pm_usage( usage );
+            }
+            noise_type = noise_id[i];
+        }
+        else
+            pm_usage( usage );
+        ++argn;
+    }
+
+    if ( argn < argc )
+    {
+        inputFilename = argv[argn];
+        argn++;
+    }
+    else
+        inputFilename = "-";
+
+    if ( argn != argc )
+        pm_usage( usage );
+
+    srand(seed);
+
+    ifP = pm_openr(inputFilename);
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    outpam = inpam;
+    outpam.file = stdout;
+
+    pnm_writepaminit(&outpam);
+
+    tuplerow = pnm_allocpamrow(&inpam);
+    newtuplerow = pnm_allocpamrow(&inpam);
+    infinity = (double) inpam.maxval;
+    
+    for (row = 0; row < inpam.height; ++row) {
+        unsigned int col;
+        pnm_readpamrow(&inpam, tuplerow);
+        for (col = 0; col < inpam.width; ++col) {
+            unsigned int plane;
+            for (plane = 0; plane < inpam.depth; ++plane) {
+                switch (noise_type) {
+                case GAUSSIAN:
+                    gaussian_noise(inpam.maxval,
+                                   tuplerow[col][plane],
+                                   &newtuplerow[col][plane],
+                                   sigma1, sigma2);
+                    break;
+                    
+                case IMPULSE:
+                    impulse_noise(inpam.maxval,
+                                  tuplerow[col][plane],
+                                  &newtuplerow[col][plane],
+                                  tolerance);
+                   break;
+                    
+                case LAPLACIAN:
+                    laplacian_noise(inpam.maxval, infinity,
+                                    tuplerow[col][plane],
+                                    &newtuplerow[col][plane],
+                                    lsigma);
+                    break;
+                    
+                case MULTIPLICATIVE_GAUSSIAN:
+                    multiplicative_gaussian_noise(inpam.maxval, infinity,
+                                                  tuplerow[col][plane],
+                                                  &newtuplerow[col][plane],
+                                                  mgsigma);
+                    break;
+                    
+                case POISSON:
+                    poisson_noise(inpam.maxval,
+                                  tuplerow[col][plane],
+                                  &newtuplerow[col][plane],
+                                  lambda);
+                    break;
+
+                }
+            }
+        }
+        pnm_writepamrow(&outpam, newtuplerow);
+    }
+    pnm_freepamrow(newtuplerow);
+    pnm_freepamrow(tuplerow);
+
+    return 0;
+}
diff --git a/editor/pamcomp.c b/editor/pamcomp.c
new file mode 100644
index 00000000..871267b2
--- /dev/null
+++ b/editor/pamcomp.c
@@ -0,0 +1,619 @@
+/*----------------------------------------------------------------------------
+                              pamcomp
+-----------------------------------------------------------------------------
+   This program composes two images together, with optional translucence.
+
+   This program is derived from (and replaces) Pnmcomp, whose origin is
+   as follows:
+
+       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.          
+
+   No code from the original remains in the present version.  The
+   January 2004 version was coded entirely by Bryan Henderson.
+   Bryan has contributed his work to the public domain.
+
+   The current version is derived from the January 2004 version, with
+   additional work by multiple authors.
+-----------------------------------------------------------------------------*/
+
+#define _BSD_SOURCE    /* Make sure strcasecmp() is in string.h */
+#include <string.h>
+#include <math.h>
+
+#include "pam.h"
+#include "pm_gamma.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+enum horizPos {BEYONDLEFT, LEFT, CENTER, RIGHT, BEYONDRIGHT};
+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 scale has no meaning if the PAM is not a visual image.  
+*/
+
+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;
+    unsigned int linear;
+};
+
+
+
+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);
+    OPTENT3(0, "linear",     OPT_FLAG,    NULL,       
+            &cmdlineP->linear,            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 int
+commonFormat(int const formatA,
+             int const formatB) {
+/*----------------------------------------------------------------------------
+   Return a viable format for the result of composing the two formats
+   'formatA' and 'formatB'.
+-----------------------------------------------------------------------------*/
+    int retval;
+
+    int const typeA = PAM_FORMAT_TYPE(formatA);
+    int const typeB = PAM_FORMAT_TYPE(formatB);
+    
+    if (typeA == PAM_TYPE || typeB == PAM_TYPE)
+        retval = PAM_FORMAT;
+    else if (typeA == PPM_TYPE || typeB == PPM_TYPE)
+        retval = PPM_FORMAT;
+    else if (typeA == PGM_TYPE || typeB == PGM_TYPE)
+        retval = PGM_FORMAT;
+    else if (typeA == PBM_TYPE || typeB == PBM_TYPE)
+        retval = PBM_FORMAT;
+    else {
+        /* Results are undefined for this case, so we do a hail Mary. */
+        retval = formatA;
+    }
+    return retval;
+}
+
+
+
+static void
+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);
+    else
+        /* Results are undefined for this case, so we do a hail Mary. */
+        strncpy(tupletypeOut, tupletypeA, size);
+}
+
+
+
+static void
+determineOutputType(struct pam * const composedPamP,
+                    struct pam * const underlayPamP,
+                    struct pam * const overlayPamP) {
+
+    composedPamP->height = underlayPamP->height;
+    composedPamP->width = underlayPamP->width;
+
+    composedPamP->format = commonFormat(underlayPamP->format, 
+                                        overlayPamP->format);
+    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);
+}
+
+
+
+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(int                const underCols, 
+                       int                const underRows,
+                       int                const overCols, 
+                       int                const overRows,
+                       struct cmdlineInfo const 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 sample
+composeComponents(sample           const compA, 
+                  sample           const compB,
+                  float            const distrib,
+                  sample           const maxval,
+                  enum sampleScale const sampleScale) {
+/*----------------------------------------------------------------------------
+  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'.
+  
+  The inputs and result are based on a maxval of 'maxval'.
+  
+  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 sample value.
+-----------------------------------------------------------------------------*/
+    sample retval;
+
+    if (fabs(1.0-distrib) < .001)
+        /* Fast path for common case */
+        retval = compA;
+    else {
+        if (sampleScale == INTENSITY_SAMPLE) {
+            sample const mix = 
+                ROUNDU(compA * 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 mix = 
+                compALinear * distrib + compBLinear * (1.0 - distrib);
+            sample const sampleValue = ROUNDU(pm_gamma709(mix) * maxval);
+            retval = MIN(maxval, MAX(0, sampleValue));
+        }
+    }
+    return retval;
+}
+
+
+
+static void
+overlayPixel(tuple            const overlayTuple,
+             struct pam *     const overlayPamP,
+             tuple            const underlayTuple,
+             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) {
+
+    float overlayWeight;
+
+    overlayWeight = masterOpacity;  /* initial value */
+    
+    if (overlayHasOpacity)
+        overlayWeight *= (float)
+            overlayTuple[opacityPlane] / overlayPamP->maxval;
+    
+    if (alphaTuplen) {
+        float const alphaval = 
+            invertAlpha ? (1.0 - alphaTuplen[0]) : alphaTuplen[0];
+        overlayWeight *= alphaval;
+    }
+
+    {
+        unsigned int plane;
+        
+        for (plane = 0; plane < composedPamP->depth; ++plane)
+            composedTuple[plane] = 
+                composeComponents(overlayTuple[plane], underlayTuple[plane], 
+                                  overlayWeight,
+                                  composedPamP->maxval,
+                                  sampleScale);
+    }
+}
+
+
+
+static void
+adaptRowToOutputFormat(struct pam * const inpamP,
+                       tuple *      const tuplerow,
+                       struct pam * const outpamP) {
+/*----------------------------------------------------------------------------
+   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.
+-----------------------------------------------------------------------------*/
+    pnm_scaletuplerow(inpamP, tuplerow, tuplerow, outpamP->maxval);
+
+    if (strncmp(outpamP->tuple_type, "RGB", 3) == 0)
+        pnm_makerowrgb(inpamP, tuplerow);
+}
+
+
+
+static void
+composite(int          const originleft, 
+          int          const origintop, 
+          struct pam * const underlayPamP,
+          struct pam * const overlayPamP,
+          struct pam * const alphaPamP,
+          bool         const invertAlpha,
+          float        const masterOpacity,
+          struct pam * const composedPamP,
+          bool         const assumeLinear) {
+/*----------------------------------------------------------------------------
+   Overlay the overlay image in the array 'overlayImage', described by
+   *overlayPamP, onto the underlying image from the input image file
+   as described by *underlayPamP, output the composite to the image
+   file as described by *composedPamP.
+
+   Apply the overlay image with transparency described by the array
+   'alpha' and *alphaPamP.
+
+   The underlying image is positioned after its header.
+
+   '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.
+-----------------------------------------------------------------------------*/
+    enum sampleScale const sampleScale = 
+        assumeLinear ? INTENSITY_SAMPLE : GAMMA_SAMPLE;
+
+    int underlayRow;  /* NB may be negative */
+    int overlayRow;   /* NB may be negative */
+    tuple * composedTuplerow;
+    tuple * underlayTuplerow;
+    tuple * overlayTuplerow;
+    tuplen * alphaTuplerown;
+    bool overlayHasOpacity;
+    unsigned int opacityPlane;
+
+    pnm_getopacity(overlayPamP, &overlayHasOpacity, &opacityPlane);
+
+    composedTuplerow = pnm_allocpamrow(composedPamP);
+    underlayTuplerow = pnm_allocpamrow(underlayPamP);
+    overlayTuplerow  = pnm_allocpamrow(overlayPamP);
+    if (alphaPamP)
+        alphaTuplerown = pnm_allocpamrown(alphaPamP);
+
+    pnm_writepaminit(composedPamP);
+
+    for (underlayRow = MIN(0, origintop), overlayRow = MIN(0, -origintop);
+         underlayRow < MAX(underlayPamP->height, 
+                           origintop + overlayPamP->height);
+         ++underlayRow, ++overlayRow) {
+
+        if (overlayRow >= 0 && overlayRow < overlayPamP->height) {
+            pnm_readpamrow(overlayPamP, overlayTuplerow);
+            adaptRowToOutputFormat(overlayPamP, overlayTuplerow, composedPamP);
+            if (alphaPamP)
+                pnm_readpamrown(alphaPamP, alphaTuplerown);
+        }
+        if (underlayRow >= 0 && underlayRow < underlayPamP->height) {
+            pnm_readpamrow(underlayPamP, underlayTuplerow);
+            adaptRowToOutputFormat(underlayPamP, underlayTuplerow, 
+                                   composedPamP);
+
+            if (underlayRow < origintop || 
+                underlayRow >= origintop + overlayPamP->height) {
+            
+                /* Overlay image does not touch this underlay row. */
+
+                pnm_writepamrow(composedPamP, underlayTuplerow);
+            } else {
+                unsigned int col;
+                for (col = 0; col < composedPamP->width; ++col) {
+                    int const ovlcol = col - originleft;
+
+                    if (ovlcol >= 0 && ovlcol < overlayPamP->width) {
+                        tuplen const alphaTuplen = 
+                            alphaPamP ? alphaTuplerown[ovlcol] : NULL;
+
+                        overlayPixel(overlayTuplerow[ovlcol], overlayPamP,
+                                     underlayTuplerow[col], underlayPamP,
+                                     alphaTuplen, invertAlpha,
+                                     overlayHasOpacity, opacityPlane,
+                                     composedTuplerow[col], composedPamP,
+                                     masterOpacity, sampleScale);
+                    } else
+                        /* Overlay image does not touch this column. */
+                        pnm_assigntuple(composedPamP, composedTuplerow[col],
+                                        underlayTuplerow[col]);
+                }
+                pnm_writepamrow(composedPamP, composedTuplerow);
+            }
+        }
+    }
+    pnm_freepamrow(composedTuplerow);
+    pnm_freepamrow(underlayTuplerow);
+    pnm_freepamrow(overlayTuplerow);
+    if (alphaPamP)
+        pnm_freepamrown(alphaTuplerown);
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE * underlayFileP;
+    FILE * overlayFileP;
+    FILE * alphaFileP;
+    struct pam underlayPam;
+    struct pam overlayPam;
+    struct pam alphaPam;
+    struct pam composedPam;
+    int originLeft, originTop;
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    overlayFileP = pm_openr(cmdline.overlayFilespec);
+    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));
+
+        if (overlayPam.width != alphaPam.width || 
+            overlayPam.height != overlayPam.height)
+            pm_error("Opacity map and overlay image are not the same size");
+    } else
+        alphaFileP = NULL;
+
+    underlayFileP = pm_openr(cmdline.underlyingFilespec);
+
+    pnm_readpaminit(underlayFileP, &underlayPam, 
+                    PAM_STRUCT_SIZE(allocation_depth));
+
+    computeOverlayPosition(underlayPam.width, underlayPam.height, 
+                           overlayPam.width,  overlayPam.height,
+                           cmdline, &originLeft, &originTop);
+
+    composedPam.size             = sizeof(composedPam);
+    composedPam.len              = PAM_STRUCT_SIZE(allocation_depth);
+    composedPam.allocation_depth = 0;
+    composedPam.file             = pm_openw(cmdline.outputFilespec);
+
+    determineOutputType(&composedPam, &underlayPam, &overlayPam);
+
+    pnm_setminallocationdepth(&underlayPam, composedPam.depth);
+    pnm_setminallocationdepth(&overlayPam,  composedPam.depth);
+    
+    composite(originLeft, originTop,
+              &underlayPam, &overlayPam, alphaFileP ? &alphaPam : NULL,
+              cmdline.alphaInvert, cmdline.opacity,
+              &composedPam, cmdline.linear);
+
+    if (alphaFileP)
+        pm_close(alphaFileP);
+    pm_close(overlayFileP);
+    pm_close(underlayFileP);
+    pm_close(composedPam.file);
+
+    /* If the program failed, it previously aborted with nonzero completion
+       code, via various function calls.
+    */
+    return 0;
+}
diff --git a/editor/pamcut.c b/editor/pamcut.c
new file mode 100644
index 00000000..d5de45fb
--- /dev/null
+++ b/editor/pamcut.c
@@ -0,0 +1,573 @@
+/*============================================================================ 
+                                pamcut
+==============================================================================
+  Cut a rectangle out of a Netpbm image
+
+  This is inspired by and intended as a replacement for Pnmcut by 
+  Jef Poskanzer, 1989.
+
+  By Bryan Henderson, San Jose CA.  Contributed to the public domain
+  by its author.
+============================================================================*/
+
+#include <limits.h>
+#include <assert.h>
+#include "pam.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+#define UNSPEC INT_MAX
+    /* UNSPEC is the value we use for an argument that is not specified
+       by the user.  Theoretically, the user could specify this value,
+       but we hope not.
+       */
+
+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 */
+
+    /* The following describe the rectangle the user wants to cut out. 
+       the value UNSPEC for any of them indicates that value was not
+       specified.  A negative value means relative to the far edge.
+       'width' and 'height' are not negative.  These specifications 
+       do not necessarily describe a valid rectangle; they are just
+       what the user said.
+       */
+    int left;
+    int right;
+    int top;
+    int bottom;
+    int width;
+    int height;
+    unsigned int pad;
+
+    unsigned int verbose;
+};
+
+
+
+static void
+parseCommandLine(int argc, 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 */
+    OPTENT3(0,   "left",       OPT_INT,    &cmdlineP->left,     NULL,      0);
+    OPTENT3(0,   "right",      OPT_INT,    &cmdlineP->right,    NULL,      0);
+    OPTENT3(0,   "top",        OPT_INT,    &cmdlineP->top,      NULL,      0);
+    OPTENT3(0,   "bottom",     OPT_INT,    &cmdlineP->bottom,   NULL,      0);
+    OPTENT3(0,   "width",      OPT_INT,    &cmdlineP->width,    NULL,      0);
+    OPTENT3(0,   "height",     OPT_INT,    &cmdlineP->height,   NULL,      0);
+    OPTENT3(0,   "pad",        OPT_FLAG,   NULL, &cmdlineP->pad,           0);
+    OPTENT3(0,   "verbose",    OPT_FLAG,   NULL, &cmdlineP->verbose,       0);
+
+    /* Set the defaults */
+    cmdlineP->left = UNSPEC;
+    cmdlineP->right = UNSPEC;
+    cmdlineP->top = UNSPEC;
+    cmdlineP->bottom = UNSPEC;
+    cmdlineP->width = UNSPEC;
+    cmdlineP->height = UNSPEC;
+
+    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 */
+
+    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (cmdlineP->width < 0)
+        pm_error("-width may not be negative.");
+    if (cmdlineP->height < 0)
+        pm_error("-height may not be negative.");
+
+    if ((argc-1) != 0 && (argc-1) != 1 && (argc-1) != 4 && (argc-1) != 5)
+        pm_error("Wrong number of arguments.  "
+                 "Must be 0, 1, 4, or 5 arguments.");
+
+    switch (argc-1) {
+    case 0:
+        cmdlineP->inputFilespec = "-";
+        break;
+    case 1:
+        cmdlineP->inputFilespec = argv[1];
+        break;
+    case 4:
+    case 5: {
+        int warg, harg;  /* The "width" and "height" command line arguments */
+
+        if (sscanf(argv[1], "%d", &cmdlineP->left) != 1)
+            pm_error("Invalid number for left column argument");
+        if (sscanf(argv[2], "%d", &cmdlineP->top) != 1)
+            pm_error("Invalid number for right column argument");
+        if (sscanf(argv[3], "%d", &warg) != 1)
+            pm_error("Invalid number for width argument");
+        if (sscanf(argv[4], "%d", &harg) != 1)
+            pm_error("Invalid number for height argument");
+
+        if (warg > 0) {
+            cmdlineP->width = warg;
+            cmdlineP->right = UNSPEC;
+        } else {
+            cmdlineP->width = UNSPEC;
+            cmdlineP->right = warg -1;
+        }
+        if (harg > 0) {
+            cmdlineP->height = harg;
+            cmdlineP->bottom = UNSPEC;
+        } else {
+            cmdlineP->height = UNSPEC;
+            cmdlineP->bottom = harg - 1;
+        }
+
+        if (argc-1 == 4)
+            cmdlineP->inputFilespec = "-";
+        else
+            cmdlineP->inputFilespec = argv[5];
+        break;
+    }
+    }
+}
+
+
+
+static void
+computeCutBounds(const int cols, const int rows,
+                 const int leftarg, const int rightarg, 
+                 const int toparg, const int bottomarg,
+                 const int widtharg, const int heightarg,
+                 int * const leftcolP, int * const rightcolP,
+                 int * const toprowP, int * const bottomrowP) {
+/*----------------------------------------------------------------------------
+   From the values given on the command line 'leftarg', 'rightarg',
+   'toparg', 'bottomarg', 'widtharg', and 'heightarg', determine what
+   rectangle the user wants cut out.
+
+   Any of these arguments may be UNSPEC to indicate "not specified".
+   Any except 'widtharg' and 'heightarg' may be negative to indicate
+   relative to the far edge.  'widtharg' and 'heightarg' are positive.
+
+   Return the location of the rectangle as *leftcolP, *rightcolP,
+   *toprowP, and *bottomrowP.  
+-----------------------------------------------------------------------------*/
+
+    int leftcol, rightcol, toprow, bottomrow;
+        /* The left and right column numbers and top and bottom row numbers
+           specified by the user, except with negative values translated
+           into the actual values.
+
+           Note that these may very well be negative themselves, such
+           as when the user says "column -10" and there are only 5 columns
+           in the image.
+           */
+
+    /* Translate negative column and row into real column and row */
+    /* Exploit the fact that UNSPEC is a positive number */
+
+    if (leftarg >= 0)
+        leftcol = leftarg;
+    else
+        leftcol = cols + leftarg;
+    if (rightarg >= 0)
+        rightcol = rightarg;
+    else
+        rightcol = cols + rightarg;
+    if (toparg >= 0)
+        toprow = toparg;
+    else
+        toprow = rows + toparg;
+    if (bottomarg >= 0)
+        bottomrow = bottomarg;
+    else
+        bottomrow = rows + bottomarg;
+
+    /* Sort out left, right, and width specifications */
+
+    if (leftcol == UNSPEC && rightcol == UNSPEC && widtharg == UNSPEC) {
+        *leftcolP = 0;
+        *rightcolP = cols - 1;
+    }
+    if (leftcol == UNSPEC && rightcol == UNSPEC && widtharg != UNSPEC) {
+        *leftcolP = 0;
+        *rightcolP = 0 + widtharg - 1;
+    }
+    if (leftcol == UNSPEC && rightcol != UNSPEC && widtharg == UNSPEC) {
+        *leftcolP = 0;
+        *rightcolP = rightcol;
+    }
+    if (leftcol == UNSPEC && rightcol != UNSPEC && widtharg != UNSPEC) {
+        *leftcolP = rightcol - widtharg + 1;
+        *rightcolP = rightcol;
+    }
+    if (leftcol != UNSPEC && rightcol == UNSPEC && widtharg == UNSPEC) {
+        *leftcolP = leftcol;
+        *rightcolP = cols - 1;
+    }
+    if (leftcol != UNSPEC && rightcol == UNSPEC && widtharg != UNSPEC) {
+        *leftcolP = leftcol;
+        *rightcolP = leftcol + widtharg - 1;
+    }
+    if (leftcol != UNSPEC && rightcol != UNSPEC && widtharg == UNSPEC) {
+        *leftcolP = leftcol;
+        *rightcolP = rightcol;
+    }
+    if (leftcol != UNSPEC && rightcol != UNSPEC && widtharg != UNSPEC) {
+        pm_error("You may not specify left, right, and width.\n"
+                 "Choose at most two of these.");
+    }
+
+
+    /* Sort out top, bottom, and height specifications */
+
+    if (toprow == UNSPEC && bottomrow == UNSPEC && heightarg == UNSPEC) {
+        *toprowP = 0;
+        *bottomrowP = rows - 1;
+    }
+    if (toprow == UNSPEC && bottomrow == UNSPEC && heightarg != UNSPEC) {
+        *toprowP = 0;
+        *bottomrowP = 0 + heightarg - 1;
+    }
+    if (toprow == UNSPEC && bottomrow != UNSPEC && heightarg == UNSPEC) {
+        *toprowP = 0;
+        *bottomrowP = bottomrow;
+    }
+    if (toprow == UNSPEC && bottomrow != UNSPEC && heightarg != UNSPEC) {
+        *toprowP = bottomrow - heightarg + 1;
+        *bottomrowP = bottomrow;
+    }
+    if (toprow != UNSPEC && bottomrow == UNSPEC && heightarg == UNSPEC) {
+        *toprowP = toprow;
+        *bottomrowP = rows - 1;
+    }
+    if (toprow != UNSPEC && bottomrow == UNSPEC && heightarg != UNSPEC) {
+        *toprowP = toprow;
+        *bottomrowP = toprow + heightarg - 1;
+    }
+    if (toprow != UNSPEC && bottomrow != UNSPEC && heightarg == UNSPEC) {
+        *toprowP = toprow;
+        *bottomrowP = bottomrow;
+    }
+    if (toprow != UNSPEC && bottomrow != UNSPEC && heightarg != UNSPEC) {
+        pm_error("You may not specify top, bottom, and height.\n"
+                 "Choose at most two of these.");
+    }
+
+}
+
+
+
+static void
+rejectOutOfBounds(const int cols, const int rows, 
+                  const int leftcol, const int rightcol, 
+                  const int toprow, const int bottomrow) {
+
+    /* Reject coordinates off the edge */
+
+    if (leftcol < 0)
+        pm_error("You have specified a left edge (%d) that is beyond\n"
+                 "the left edge of the image (0)", leftcol);
+    if (leftcol > cols-1)
+        pm_error("You have specified a left edge (%d) that is beyond\n"
+                 "the right edge of the image (%d)", leftcol, cols-1);
+    if (rightcol < 0)
+        pm_error("You have specified a right edge (%d) that is beyond\n"
+                 "the left edge of the image (0)", rightcol);
+    if (rightcol > cols-1)
+        pm_error("You have specified a right edge (%d) that is beyond\n"
+                 "the right edge of the image (%d)", rightcol, cols-1);
+    if (leftcol > rightcol) 
+        pm_error("You have specified a left edge (%d) that is to the right\n"
+                 "of the right edge you specified (%d)", 
+                 leftcol, rightcol);
+    
+    if (toprow < 0)
+        pm_error("You have specified a top edge (%d) that is above the top "
+                 "edge of the image (0)", toprow);
+    if (toprow > rows-1)
+        pm_error("You have specified a top edge (%d) that is below the\n"
+                 "bottom edge of the image (%d)", toprow, rows-1);
+    if (bottomrow < 0)
+        pm_error("You have specified a bottom edge (%d) that is above the\n"
+                 "top edge of the image (0)", bottomrow);
+    if (bottomrow > rows-1)
+        pm_error("You have specified a bottom edge (%d) that is below the\n"
+                 "bottom edge of the image (%d)", bottomrow, rows-1);
+    if (toprow > bottomrow) 
+        pm_error("You have specified a top edge (%d) that is below\n"
+                 "the bottom edge you specified (%d)", 
+                 toprow, bottomrow);
+}
+
+
+
+static void
+writeBlackRows(const struct pam * const outpamP, 
+               int                const rows) {
+/*----------------------------------------------------------------------------
+   Write out 'rows' rows of black tuples of the image described by *outpamP.
+
+   Unless our input image is PBM, PGM, or PPM, or PAM equivalent, we
+   don't really know what "black" means, so this is just something
+   arbitrary in that case.
+-----------------------------------------------------------------------------*/
+    tuple blackTuple;
+    tuple * blackRow;
+    int col;
+    
+    pnm_createBlackTuple(outpamP, &blackTuple);
+
+    MALLOCARRAY_NOFAIL(blackRow, outpamP->width);
+    
+    for (col = 0; col < outpamP->width; ++col)
+        blackRow[col] = blackTuple;
+
+    pnm_writepamrowmult(outpamP, blackRow, rows);
+
+    free(blackRow);
+
+    pnm_freepamtuple(blackTuple);
+}
+
+
+
+struct rowCutter {
+/*----------------------------------------------------------------------------
+   This is an object that gives you pointers you can use to effect the
+   horizontal cutting and padding of a row just by doing one
+   pnm_readpamrow() and one pnm_writepamrow().  It works like this:
+
+   The array inputPointers[] contains an element for each pixel in an input
+   row.  If it's a pixel that gets discarded in the cutting process, 
+   inputPointers[] points to a special "discard" tuple.  All thrown away
+   pixels have the same discard tuple to save CPU cache space.  If it's
+   a pixel that gets copied to the output, inputPointers[] points to some
+   tuple to which outputPointers[] also points.
+
+   The array outputPointers[] contains an element for each pixel in an
+   output row.  If the pixel is one that gets copied from the input, 
+   outputPointers[] points to some tuple to which inputPointers[] also
+   points.  If it's a pixel that gets padded with black, outputPointers[]
+   points to a constant black tuple.  All padded pixels have the same
+   constant black tuple to save CPU cache space.
+
+   For example, if you have a three pixel input row and are cutting
+   off the right two pixels, inputPointers[0] points to copyTuples[0]
+   and inputPointers[1] and inputPointers[2] point to discardTuple.
+   outputPointers[0] points to copyTuples[0].
+
+   We arrange to have the padded parts of the output row filled with
+   black tuples.  Unless the input image is PBM, PGM, or PPM, or PAM
+   equivalent, we don't really know what "black" means, so we fill with
+   something arbitrary in that case.
+-----------------------------------------------------------------------------*/
+    tuple * inputPointers;
+    tuple * outputPointers;
+
+    unsigned int inputWidth;
+    unsigned int outputWidth;
+
+    /* The following are the tuples to which inputPointers[] and
+       outputPointers[] may point.
+    */
+    tuple * copyTuples;
+    tuple blackTuple;
+    tuple discardTuple;
+};
+
+
+
+/* In a typical multi-image stream, all the images have the same
+   dimensions, so this program creates and destroys identical row
+   cutters for each image in the stream.  If that turns out to take a
+   significant amount of resource to do, we should create a cache:
+   keep the last row cutter made, tagged by the parameters used to
+   create it.  If the parameters are the same for the next image, we
+   just use that cached row cutter; otherwise, we discard it and
+   create a new one then.
+*/
+
+static void
+createRowCutter(struct pam *        const inpamP,
+                struct pam *        const outpamP,
+                int                 const leftcol,
+                int                 const rightcol,
+                struct rowCutter ** const rowCutterPP) {
+    
+    struct rowCutter * rowCutterP;
+    tuple * inputPointers;
+    tuple * outputPointers;
+    tuple * copyTuples;
+    tuple blackTuple;
+    tuple discardTuple;
+    int col;
+    
+    assert(inpamP->depth >= outpamP->depth);
+        /* Entry condition.  If this weren't true, we could not simply
+           treat an input tuple as an output tuple.
+        */
+
+    copyTuples   = pnm_allocpamrow(outpamP);
+    discardTuple = pnm_allocpamtuple(inpamP);
+    pnm_createBlackTuple(outpamP, &blackTuple);
+
+    MALLOCARRAY_NOFAIL(inputPointers,  inpamP->width);
+    MALLOCARRAY_NOFAIL(outputPointers, outpamP->width);
+
+    /* Put in left padding */
+    for (col = leftcol; col < 0; ++col)
+        outputPointers[col-leftcol] = blackTuple;
+ 
+    /* Put in extracted columns */
+    for (col = MAX(leftcol, 0); 
+         col <= MIN(rightcol, inpamP->width-1); 
+         ++col) {
+        int const outcol = col - leftcol;
+
+        inputPointers[col] = outputPointers[outcol] = copyTuples[outcol];
+    }
+
+    /* Put in right padding */
+    for (col = MIN(rightcol, inpamP->width-1) + 1; col <= rightcol; ++col)
+        outputPointers[col-leftcol] = blackTuple;
+    
+    /* Direct input pixels that are getting cut off to the discard tuple */
+
+    for (col = 0; col < leftcol; ++col)
+        inputPointers[col] = discardTuple;
+
+    for (col = rightcol + 1; col < inpamP->width; ++col)
+        inputPointers[col] = discardTuple;
+
+    MALLOCVAR_NOFAIL(rowCutterP);
+
+    rowCutterP->inputWidth     = inpamP->width;
+    rowCutterP->outputWidth    = outpamP->width;
+    rowCutterP->inputPointers  = inputPointers;
+    rowCutterP->outputPointers = outputPointers;
+    rowCutterP->copyTuples     = copyTuples;
+    rowCutterP->discardTuple   = discardTuple;
+    rowCutterP->blackTuple     = blackTuple;
+
+    *rowCutterPP = rowCutterP;
+}
+
+
+
+static void
+destroyRowCutter(struct rowCutter * const rowCutterP) {
+
+    pnm_freepamrow(rowCutterP->copyTuples);
+    pnm_freepamtuple(rowCutterP->blackTuple);
+    pnm_freepamtuple(rowCutterP->discardTuple);
+    free(rowCutterP->inputPointers);
+    free(rowCutterP->outputPointers);
+    
+    free(rowCutterP);
+}
+
+
+
+static void
+cutOneImage(FILE *             const ifP,
+            struct cmdlineInfo const cmdline,
+            FILE *             const ofP) {
+
+    int row;
+    int leftcol, rightcol, toprow, bottomrow;
+    struct pam inpam;   /* Input PAM image */
+    struct pam outpam;  /* Output PAM image */
+    struct rowCutter * rowCutterP;
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+    
+    computeCutBounds(inpam.width, inpam.height, 
+                     cmdline.left, cmdline.right, 
+                     cmdline.top, cmdline.bottom, 
+                     cmdline.width, cmdline.height, 
+                     &leftcol, &rightcol, &toprow, &bottomrow);
+
+    if (!cmdline.pad)
+        rejectOutOfBounds(inpam.width, inpam.height, leftcol, rightcol, 
+                          toprow, bottomrow);
+
+    if (cmdline.verbose) {
+        pm_message("Image goes from Row 0, Column 0 through Row %d, Column %d",
+                   inpam.height-1, inpam.width-1);
+        pm_message("Cutting from Row %d, Column %d through Row %d Column %d",
+                   toprow, leftcol, bottomrow, rightcol);
+    }
+
+    outpam = inpam;    /* Initial value -- most fields should be same */
+    outpam.file   = ofP;
+    outpam.width  = rightcol-leftcol+1;
+    outpam.height = bottomrow-toprow+1;
+
+    pnm_writepaminit(&outpam);
+
+    /* Write out top padding */
+    if (0 - toprow > 0)
+        writeBlackRows(&outpam, 0 - toprow);
+
+    createRowCutter(&inpam, &outpam, leftcol, rightcol, &rowCutterP);
+
+    /* Read input and write out rows extracted from it */
+    for (row = 0; row < inpam.height; ++row) {
+        if (row >= toprow && row <= bottomrow){
+            pnm_readpamrow(&inpam, rowCutterP->inputPointers);
+            pnm_writepamrow(&outpam, rowCutterP->outputPointers);
+        } else  /* row < toprow || row > bottomrow */
+            pnm_readpamrow(&inpam, NULL);
+        
+        /* Note that we may be tempted just to quit after reaching the bottom
+           of the extracted image, but that would cause a broken pipe problem
+           for the process that's feeding us the image.
+        */
+    }
+
+    destroyRowCutter(rowCutterP);
+    
+    /* Write out bottom padding */
+    if ((bottomrow - (inpam.height-1)) > 0)
+        writeBlackRows(&outpam, bottomrow - (inpam.height-1));
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    FILE * const ofP = stdout;
+
+    struct cmdlineInfo cmdline;
+    FILE* ifP;
+    bool eof;
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilespec);
+
+    eof = FALSE;
+    while (!eof) {
+        cutOneImage(ifP, cmdline, ofP);
+        pnm_nextimage(ifP, &eof);
+    }
+
+    pm_close(ifP);
+    pm_close(ofP);
+    
+    return 0;
+}
diff --git a/editor/pamcut.test b/editor/pamcut.test
new file mode 100644
index 00000000..be70f1fd
--- /dev/null
+++ b/editor/pamcut.test
@@ -0,0 +1,10 @@
+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/pamdeinterlace.c b/editor/pamdeinterlace.c
new file mode 100644
index 00000000..9ed1d8eb
--- /dev/null
+++ b/editor/pamdeinterlace.c
@@ -0,0 +1,132 @@
+/******************************************************************************
+                             pamdeinterlace
+*******************************************************************************
+  De-interlace an image, i.e. select every 2nd row.
+   
+  By Bryan Henderson, San Jose, CA 2001.11.11.
+
+  Contributed to the public domain.
+******************************************************************************/
+
+#include "pam.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+enum evenodd {EVEN, ODD};
+
+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 */
+    enum evenodd rowsToTake;
+};
+
+
+static void
+parseCommandLine(int argc, 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;
+    unsigned int option_def_index;
+
+    unsigned int takeeven, takeodd;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0,   "takeeven", OPT_FLAG, NULL, &takeeven, 0);
+    OPTENT3(0,   "takeodd",  OPT_FLAG, NULL, &takeodd,  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 (takeeven && takeodd)
+        pm_error("You cannot specify both -takeeven and -takeodd options.");
+
+    if (takeodd)
+        cmdlineP->rowsToTake = ODD;
+    else
+        cmdlineP->rowsToTake = EVEN;
+
+    if (argc-1 < 1)
+        cmdlineP->inputFilespec = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFilespec = argv[1];
+    else
+        pm_error("You specified too many arguments (%d).  The only "
+                 "argument is the optional input file specification.",
+                 argc-1);
+}
+
+
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    FILE * ifP;
+    tuple * tuplerow;   /* Row from input image */
+    unsigned int row;
+    struct cmdlineInfo cmdline;
+    struct pam inpam;  
+    struct pam outpam;
+
+    pnm_init( &argc, argv );
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilespec);
+    
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    tuplerow = pnm_allocpamrow(&inpam);
+
+    outpam = inpam;    /* Initial value -- most fields should be same */
+    outpam.file = stdout;
+    if (inpam.height % 2 == 0)
+        outpam.height = inpam.height / 2;
+    else {
+        if (cmdline.rowsToTake == ODD)
+            outpam.height = inpam.height / 2;
+        else
+            outpam.height = inpam.height / 2 + 1;
+    }
+
+    pnm_writepaminit(&outpam);
+
+    {
+        unsigned int modulusToTake;
+            /* The row number mod 2 of the rows that are supposed to go into
+               the output.
+            */
+
+        switch (cmdline.rowsToTake) {
+        case EVEN: modulusToTake = 0; break;
+        case ODD:  modulusToTake = 1; break;
+        default: pm_error("INTERNAL ERROR: invalid rowsToTake");
+        }
+
+        /* Read input and write out rows extracted from it */
+        for (row = 0; row < inpam.height; row++) {
+            pnm_readpamrow(&inpam, tuplerow);
+            if (row % 2 == modulusToTake)
+                pnm_writepamrow(&outpam, tuplerow);
+        }
+    }
+    pnm_freepamrow(tuplerow);
+    pm_close(inpam.file);
+    pm_close(outpam.file);
+    
+    return 0;
+}
+
diff --git a/editor/pamdice.c b/editor/pamdice.c
new file mode 100644
index 00000000..062e05e3
--- /dev/null
+++ b/editor/pamdice.c
@@ -0,0 +1,494 @@
+/*****************************************************************************
+                                  pamdice
+******************************************************************************
+  Slice a Netpbm image vertically and/or horizontally into multiple images.
+
+  By Bryan Henderson, San Jose CA 2001.01.31
+
+  Contributed to the public domain.
+
+******************************************************************************/
+
+#include <string.h>
+
+#include "pam.h"
+#include "shhopt.h"
+#include "nstring.h"
+#include "mallocvar.h"
+
+#define MAXFILENAMELEN 80
+    /* Maximum number of characters we accept in filenames */
+
+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 */
+    char * outstem; 
+        /* null-terminated string, max MAXFILENAMELEN-10 characters */
+    unsigned int sliceVertically;    /* boolean */
+    unsigned int sliceHorizontally;  /* boolean */
+    unsigned int width;    /* Meaningless if !sliceVertically */
+    unsigned int height;   /* Meaningless if !sliceHorizontally */
+    unsigned int hoverlap; 
+        /* Meaningless if !sliceVertically.  Guaranteed < width */
+    unsigned int voverlap; 
+        /* Meaningless if !sliceHorizontally.  Guaranteed < height */
+    unsigned int verbose;
+};
+
+
+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 outstemSpec, hoverlapSpec, voverlapSpec;
+    unsigned int option_def_index;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "width",       OPT_UINT,    &cmdlineP->width,       
+            &cmdlineP->sliceVertically,       0 );
+    OPTENT3(0, "height",      OPT_UINT,    &cmdlineP->height,
+            &cmdlineP->sliceHorizontally,     0 );
+    OPTENT3(0, "hoverlap",    OPT_UINT,    &cmdlineP->hoverlap,
+            &hoverlapSpec,                    0 );
+    OPTENT3(0, "voverlap",    OPT_UINT,    &cmdlineP->voverlap,
+            &voverlapSpec,                    0 );
+    OPTENT3(0, "outstem",     OPT_STRING,  &cmdlineP->outstem,
+            &outstemSpec,                     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 (cmdlineP->sliceVertically) {
+        if (hoverlapSpec) {
+            if (cmdlineP->hoverlap > cmdlineP->width - 1)
+                pm_error("-hoverlap value must be less than -width (%u).  "
+                         "You specified %u.",
+                         cmdlineP->width, cmdlineP->hoverlap);
+        } else
+            cmdlineP->hoverlap = 0;
+    }
+    if (cmdlineP->sliceHorizontally) {
+        if (voverlapSpec) {
+            if (cmdlineP->voverlap > cmdlineP->height - 1)
+                pm_error("-voverlap value must be less than -height (%u).  "
+                         "You specified %u.",
+                         cmdlineP->height, cmdlineP->voverlap);
+        } else
+            cmdlineP->voverlap = 0;
+    }
+
+    if (!outstemSpec)
+        pm_error("You must specify the -outstem option to indicate where to "
+                 "put the output images.");
+    if (argc-1 < 1)
+        cmdlineP->inputFilespec = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFilespec = argv[1];
+    else 
+        pm_error("Progam takes at most 1 parameter: the file specification.  "
+                 "You specified %d", argc-1);
+}
+
+
+
+static unsigned int
+divup(unsigned int const dividend,
+      unsigned int const divisor) {
+/*----------------------------------------------------------------------------
+   Divide 'dividend' by 'divisor' and round up to the next whole number.
+-----------------------------------------------------------------------------*/
+    return (dividend + divisor - 1) / divisor;
+}
+
+
+
+static void
+computeSliceGeometry(struct cmdlineInfo const cmdline,
+                     struct pam         const inpam,
+                     bool               const verbose,
+                     unsigned int *     const nHorizSliceP,
+                     unsigned int *     const sliceHeightP,
+                     unsigned int *     const bottomSliceHeightP,
+                     unsigned int *     const nVertSliceP,
+                     unsigned int *     const sliceWidthP,
+                     unsigned int *     const rightSliceWidthP
+                     ) {
+/*----------------------------------------------------------------------------
+   Compute the geometry of the slices, both common slices and possibly
+   smaller remainder slices at the top and right.
+-----------------------------------------------------------------------------*/
+    if (cmdline.sliceHorizontally) {
+        if (cmdline.height >= inpam.height)
+            *nHorizSliceP = 1;
+        else
+            *nHorizSliceP = 1 + divup(inpam.height - cmdline.height, 
+                                      cmdline.height - cmdline.voverlap);
+        *sliceHeightP = cmdline.height;
+    } else {
+        *nHorizSliceP = 1;
+        *sliceHeightP = inpam.height;
+    }
+
+    *bottomSliceHeightP = 
+        inpam.height - (*nHorizSliceP-1) * (cmdline.height - cmdline.voverlap);
+
+    if (cmdline.sliceVertically) {
+        if (cmdline.width >= inpam.width)
+            *nVertSliceP = 1;
+        else
+            *nVertSliceP = 1 + divup(inpam.width - cmdline.width, 
+                                     cmdline.width - cmdline.hoverlap);
+        *sliceWidthP = cmdline.width;
+    } else {
+        *nVertSliceP = 1;
+        *sliceWidthP = inpam.width;
+    }
+
+    *rightSliceWidthP = 
+        inpam.width - (*nVertSliceP-1) * (cmdline.width - cmdline.hoverlap);
+
+    if (verbose) {
+        pm_message("Creating %u images, %u across by %u down; "
+                   "each %u w x %u h",
+                   *nVertSliceP * *nHorizSliceP,
+                   *nVertSliceP, *nHorizSliceP,
+                   *sliceWidthP, *sliceHeightP);
+        if (*rightSliceWidthP != *sliceWidthP)
+            pm_message("Right vertical slice is only %u wide", 
+                       *rightSliceWidthP);
+        if (*bottomSliceHeightP != *sliceHeightP)
+            pm_message("Bottom horizontal slice is only %u high",
+                       *bottomSliceHeightP);
+    }
+}
+
+
+
+static unsigned int
+ndigits(unsigned int const arg) {
+/*----------------------------------------------------------------------------
+   Return the minimum number of digits it takes to represent the number
+   'arg' in decimal.
+-----------------------------------------------------------------------------*/
+    unsigned int leftover;
+    unsigned int i;
+
+    for (leftover = arg, i = 0; leftover > 0; leftover /= 10, ++i);
+
+    return MAX(1, i);
+}
+
+
+
+static void
+computeOutputFilenameFormat(int           const format, 
+                            char          const outstem[],
+                            unsigned int  const nHorizSlice,
+                            unsigned int  const nVertSlice,
+                            const char ** const filenameFormatP) {
+
+    const char * filenameSuffix;
+
+    switch(PNM_FORMAT_TYPE(format)) {
+    case PPM_TYPE: filenameSuffix = "ppm"; break;
+    case PGM_TYPE: filenameSuffix = "pgm"; break;
+    case PBM_TYPE: filenameSuffix = "pbm"; break;
+    case PAM_TYPE: filenameSuffix = "pam"; break;
+    default:       filenameSuffix = "";    break;
+    }
+    
+    asprintfN(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");
+}
+
+
+
+static void
+openOutStreams(struct pam   const inpam, 
+               struct pam         outpam[],
+               unsigned int const horizSlice, 
+               unsigned int const nHorizSlice,
+               unsigned int const nVertSlice,
+               unsigned int const sliceHeight, 
+               unsigned int const sliceWidth,
+               unsigned int const rightSliceWidth,
+               unsigned int const hOverlap,
+               char         const outstem[]) {
+/*----------------------------------------------------------------------------
+   Open the output files for a single horizontal slice (there's one file
+   for each vertical slice) and write the Netpbm headers to them.  Also
+   compute the pam structures to control each.
+-----------------------------------------------------------------------------*/
+    const char * filenameFormat;
+    unsigned int vertSlice;
+
+    computeOutputFilenameFormat(inpam.format, outstem, nHorizSlice, nVertSlice,
+                                &filenameFormat);
+
+    for (vertSlice = 0; vertSlice < nVertSlice; ++vertSlice) {
+        const char * filename;
+
+        asprintfN(&filename, filenameFormat, horizSlice, vertSlice);
+
+        if (filename == NULL)
+            pm_error("Unable to allocate memory for output filename");
+        else {
+            outpam[vertSlice] = inpam;
+            outpam[vertSlice].file = pm_openw(filename);
+            
+            outpam[vertSlice].width = 
+                vertSlice < nVertSlice-1 ? sliceWidth : rightSliceWidth;
+            
+            outpam[vertSlice].height = sliceHeight;
+            
+            pnm_writepaminit(&outpam[vertSlice]);
+
+            strfree(filename);
+        }
+    }        
+    strfree(filenameFormat);
+}
+
+
+
+static void
+closeOutFiles(struct pam pam[], unsigned int const nVertSlice) {
+
+    unsigned int vertSlice;
+    
+    for (vertSlice = 0; vertSlice < nVertSlice; ++vertSlice)
+        pm_close(pam[vertSlice].file);
+}
+
+static void
+sliceRow(tuple              inputRow[], 
+         struct pam         outpam[], 
+         unsigned int const nVertSlice,
+         unsigned int const hOverlap) {
+/*----------------------------------------------------------------------------
+   Distribute the row inputRow[] across the 'nVerticalSlice' output
+   files described by outpam[].  Each outpam[x] tells how many columns
+   of inputRow[] to take and what their composition is.
+
+   'hOverlap', which is meaningful only when nVertSlice is greater than 1,
+   is the amount by which slices overlap each other.
+-----------------------------------------------------------------------------*/
+    tuple * outputRow;
+    unsigned int vertSlice;
+    unsigned int const sliceWidth = outpam[0].width;
+    unsigned int const stride = 
+        nVertSlice > 1 ? sliceWidth - hOverlap : sliceWidth;
+
+    for (vertSlice = 0, outputRow = inputRow; 
+         vertSlice < nVertSlice; 
+         outputRow += stride, ++vertSlice) {
+        pnm_writepamrow(&outpam[vertSlice], outputRow);
+    }
+}
+
+
+/*----------------------------------------------------------------------------
+   The input reader.  This just reads the input image row by row, except
+   that it lets us back up up to a predefined amount (the window size).
+   When we're overlapping horizontal slices, that's useful.  It's not as
+   simple as just reading the entire image into memory at once, but uses
+   a lot less memory.
+-----------------------------------------------------------------------------*/
+
+struct inputWindow {
+    unsigned int windowSize;
+    unsigned int firstRowInWindow;
+    struct pam pam;
+    tuple ** rows;
+};
+
+static void
+initInput(struct inputWindow * const inputWindowP,
+          struct pam *         const pamP,
+          unsigned int         const windowSize) {
+    
+    struct pam allocPam;  /* Just for allocating the window array */
+    unsigned int i;
+
+    inputWindowP->pam = *pamP;
+    inputWindowP->windowSize = windowSize;
+
+    allocPam = *pamP;
+    allocPam.height = windowSize;
+    
+    inputWindowP->rows = pnm_allocpamarray(&allocPam);
+
+    inputWindowP->firstRowInWindow = 0;
+
+    /* Fill the window with the beginning of the image */
+    for (i = 0; i < windowSize && i < pamP->height; ++i)
+        pnm_readpamrow(&inputWindowP->pam, inputWindowP->rows[i]);
+}
+
+static void
+termInputWindow(struct inputWindow * const inputWindowP) {
+
+    struct pam freePam;  /* Just for freeing window array */
+
+    freePam = inputWindowP->pam;
+    freePam.height = inputWindowP->windowSize;
+
+    pnm_freepamarray(inputWindowP->rows, &freePam);
+}
+
+static tuple *
+getInputRow(struct inputWindow * const inputWindowP,
+            unsigned int         const row) {
+
+    if (row < inputWindowP->firstRowInWindow)
+        pm_error("INTERNAL ERROR: attempt to back up too far with "
+                 "getInputRow() (row %u)", row);
+    if (row >= inputWindowP->pam.height)
+        pm_error("INTERNAL ERROR: attempt to read beyond bottom of "
+                 "input image (row %u)", row);
+
+    while (row >= inputWindowP->firstRowInWindow + inputWindowP->windowSize) {
+        tuple * const oldRow0 = inputWindowP->rows[0];
+        unsigned int i;
+        /* Slide the window down one row */
+        for (i = 0; i < inputWindowP->windowSize - 1; ++i)
+            inputWindowP->rows[i] = inputWindowP->rows[i+1];
+        ++inputWindowP->firstRowInWindow;
+
+        /* Read in the new last row in the window */
+        inputWindowP->rows[i] = oldRow0;  /* Reuse the memory */
+        pnm_readpamrow(&inputWindowP->pam, inputWindowP->rows[i]);
+    }        
+
+    return inputWindowP->rows[row - inputWindowP->firstRowInWindow];
+}
+
+/*-----  end of input reader ----------------------------------------------*/
+
+
+
+static void
+allocOutpam(unsigned int  const nVertSlice,
+            struct pam ** const outpamArrayP) {
+
+    struct pam * outpamArray;
+
+    MALLOCARRAY(outpamArray, nVertSlice);
+
+    if (outpamArray == NULL)
+        pm_error("Unable to allocate array for %u output pam structures.",
+                 nVertSlice);
+
+    *outpamArrayP = outpamArray;
+}
+
+
+
+int
+main(int argc, char ** argv) {
+
+    struct cmdlineInfo cmdline;
+    FILE    *ifP;
+    struct pam inpam;
+    unsigned int horizSlice;
+        /* Number of the current horizontal slice.  Slices are numbered
+           sequentially starting at 0.
+        */
+    unsigned int sliceWidth;
+        /* Width in pam columns of each vertical slice, except
+           the rightmost slice, which may be narrower.  If we aren't slicing
+           vertically, that means one slice, i.e. the slice width
+           is the image width.  
+        */
+    unsigned int rightSliceWidth;
+        /* Width in pam columns of the rightmost vertical slice. */
+    unsigned int sliceHeight;
+        /* Height in pam rows of each horizontal slice, except
+           the bottom slice, which may be shorter.  If we aren't slicing
+           horizontally, that means one slice, i.e. the slice height
+           is the image height.  
+        */
+    unsigned int bottomSliceHeight;
+        /* Height in pam rows of the bottom horizontal slice. */
+    unsigned int nHorizSlice;
+    unsigned int nVertSlice;
+    struct inputWindow inputWindow;
+    
+    struct pam * outpam;
+        /* malloc'ed.  outpam[x] is the pam structure that controls
+           the current horizontal slice of vertical slice x.
+        */
+
+    pnm_init(&argc, argv);
+    
+    parseCommandLine(argc, argv, &cmdline);
+        
+    ifP = pm_openr(cmdline.inputFilespec);
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    computeSliceGeometry(cmdline, inpam, !!cmdline.verbose,
+                         &nHorizSlice, &sliceHeight, &bottomSliceHeight,
+                         &nVertSlice, &sliceWidth, &rightSliceWidth);
+
+    allocOutpam(nVertSlice, &outpam);
+    
+    initInput(&inputWindow, &inpam, 
+              nHorizSlice > 1 ? cmdline.voverlap + 1 : 1);
+
+    for (horizSlice = 0; horizSlice < nHorizSlice; ++horizSlice) {
+        unsigned int const thisSliceFirstRow = 
+            horizSlice * (sliceHeight - cmdline.voverlap);
+        unsigned int const thisSliceHeight = 
+            horizSlice < nHorizSlice-1 ? sliceHeight : bottomSliceHeight;
+
+        unsigned int row;
+
+        openOutStreams(inpam, outpam, horizSlice, nHorizSlice, nVertSlice, 
+                       thisSliceHeight, sliceWidth, rightSliceWidth,
+                       cmdline.hoverlap, cmdline.outstem);
+
+        for (row = 0; row < thisSliceHeight; ++row) {
+            tuple * const inputRow =
+                getInputRow(&inputWindow, thisSliceFirstRow + row);
+            sliceRow(inputRow, outpam, nVertSlice, cmdline.hoverlap);
+        }
+        closeOutFiles(outpam, nVertSlice);
+    }
+
+    termInputWindow(&inputWindow);
+
+    free(outpam);
+
+    pm_close(ifP);
+
+    return 0;
+}
diff --git a/editor/pamdither.c b/editor/pamdither.c
new file mode 100644
index 00000000..5eb931a6
--- /dev/null
+++ b/editor/pamdither.c
@@ -0,0 +1,319 @@
+/*=============================================================================
+                                 pamdither
+===============================================================================
+  By Bryan Henderson, July 2006.
+
+  Contributed to the public domain.
+
+  This is meant to replace Ppmdither by Christos Zoulas, 1991.
+=============================================================================*/
+
+#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
new file mode 100644
index 00000000..61c23103
--- /dev/null
+++ b/editor/pamditherbw.c
@@ -0,0 +1,743 @@
+/*=============================================================================
+                           pamditherbw
+===============================================================================
+   Dither a grayscale PAM to a black and white PAM.
+
+   By Bryan Henderson, San Jose CA.  June 2004.
+
+   Contributed to the public domain by its author.
+
+   Based on ideas from Pgmtopbm by Jef Poskanzer, 1989.
+=============================================================================*/
+
+#include <assert.h>
+#include <string.h>
+
+#include "pam.h"
+#include "dithers.h"
+#include "mallocvar.h"
+#include "shhopt.h"
+#include "pm_gamma.h"
+
+enum halftone {QT_FS, QT_THRESH, QT_DITHER8, QT_CLUSTER, QT_HILBERT};
+
+enum ditherType {DT_REGULAR, DT_CLUSTER};
+
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char *  inputFilespec;
+    enum halftone halftone;
+    unsigned int  clumpSize;
+        /* Defined only for halftone == QT_HILBERT */
+    unsigned int  clusterRadius;  
+        /* Defined only for halftone == QT_CLUSTER */
+    float         threshval;
+};
+
+
+
+
+static void
+parseCommandLine(int argc, 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.
+-----------------------------------------------------------------------------*/
+    optEntry *option_def;
+        /* Instructions to optParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+    unsigned int floydOpt, hilbertOpt, thresholdOpt, dither8Opt,
+        cluster3Opt, cluster4Opt, cluster8Opt;
+    unsigned int valueSpec, clumpSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    OPTENT3(0, "floyd",     OPT_FLAG,  NULL, &floydOpt,     0);
+    OPTENT3(0, "fs",        OPT_FLAG,  NULL, &floydOpt,     0);
+    OPTENT3(0, "threshold", OPT_FLAG,  NULL, &thresholdOpt, 0);
+    OPTENT3(0, "hilbert",   OPT_FLAG,  NULL, &hilbertOpt,   0);
+    OPTENT3(0, "dither8",   OPT_FLAG,  NULL, &dither8Opt,   0);
+    OPTENT3(0, "d8",        OPT_FLAG,  NULL, &dither8Opt,   0);
+    OPTENT3(0, "cluster3",  OPT_FLAG,  NULL, &cluster3Opt,  0);
+    OPTENT3(0, "c3",        OPT_FLAG,  NULL, &cluster3Opt,  0);
+    OPTENT3(0, "cluster4",  OPT_FLAG,  NULL, &cluster4Opt,  0);
+    OPTENT3(0, "c4",        OPT_FLAG,  NULL, &cluster4Opt,  0);
+    OPTENT3(0, "cluster8",  OPT_FLAG,  NULL, &cluster8Opt,  0);
+    OPTENT3(0, "c8",        OPT_FLAG,  NULL, &cluster8Opt,  0);
+    OPTENT3(0, "value",     OPT_FLOAT, &cmdlineP->threshval, 
+            &valueSpec, 0);
+    OPTENT3(0, "clump",     OPT_UINT,  &cmdlineP->clumpSize, 
+            &clumpSpec, 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);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (floydOpt + thresholdOpt + hilbertOpt + dither8Opt + 
+        cluster3Opt + cluster4Opt + cluster8Opt == 0)
+        cmdlineP->halftone = QT_FS;
+    else if (floydOpt + thresholdOpt + dither8Opt + 
+        cluster3Opt + cluster4Opt + cluster8Opt > 1)
+        pm_error("No cannot specify more than one halftoning type");
+    else {
+        if (floydOpt)
+            cmdlineP->halftone = QT_FS;
+        else if (thresholdOpt)
+            cmdlineP->halftone = QT_THRESH;
+        else if (hilbertOpt) {
+            cmdlineP->halftone = QT_HILBERT;
+
+            if (!clumpSpec)
+                cmdlineP->clumpSize = 5;
+            else {
+                if (cmdlineP->clumpSize < 2)
+                    pm_error("-clump must be at least 2.  You specified %u",
+                             cmdlineP->clumpSize);
+            }
+        } else if (dither8Opt)
+            cmdlineP->halftone = QT_DITHER8;
+        else if (cluster3Opt) {
+            cmdlineP->halftone = QT_CLUSTER;
+            cmdlineP->clusterRadius = 3;
+        } else if (cluster4Opt) {
+            cmdlineP->halftone = QT_CLUSTER;
+            cmdlineP->clusterRadius = 4;
+        } else if (cluster8Opt) {
+            cmdlineP->halftone = QT_CLUSTER;
+            cmdlineP->clusterRadius = 8;
+        } else 
+            pm_error("INTERNAL ERROR.  No halftone option");
+    }
+
+    if (!valueSpec)
+        cmdlineP->threshval = 0.5;
+    else {
+        if (cmdlineP->threshval < 0.0)
+            pm_error("-value cannot be negative.  You specified %f",
+                     cmdlineP->threshval);
+        if (cmdlineP->threshval > 1.0)
+            pm_error("-value cannot be greater than one.  You specified %f",
+                     cmdlineP->threshval);
+    }
+
+    if (clumpSpec && cmdlineP->halftone != QT_HILBERT)
+        pm_error("-clump is not valid without -hilbert");
+
+    if (argc-1 > 1)
+        pm_error("Too many arguments (%d).  There is at most one "
+                 "non-option argument:  the file name",
+                 argc-1);
+    else if (argc-1 == 1)
+        cmdlineP->inputFilespec = argv[1];
+    else
+        cmdlineP->inputFilespec = "-";
+}
+
+
+static struct pam
+makeOutputPam(unsigned int const width,
+              unsigned int const height) {
+
+    struct pam outpam;
+
+    outpam.size = sizeof(outpam);
+    outpam.len = PAM_STRUCT_SIZE(tuple_type);
+    outpam.file = stdout;
+    outpam.format = PAM_FORMAT;
+    outpam.plainformat = 0;
+    outpam.height = height;
+    outpam.width = width;
+    outpam.depth = 1;
+    outpam.maxval = 1;
+    outpam.bytes_per_sample = 1;
+    strcpy(outpam.tuple_type, "BLACKANDWHITE");
+
+    return outpam;
+}
+
+
+
+/* Hilbert curve tracer */
+
+#define MAXORD 18
+
+static int hil_order,hil_ord;
+static int hil_turn;
+static int hil_dx,hil_dy;
+static int hil_x,hil_y;
+static int hil_stage[MAXORD];
+static int hil_width,hil_height;
+
+static void 
+initHilbert(int const w, 
+            int const h) {
+/*----------------------------------------------------------------------------
+  Initialize the Hilbert curve tracer 
+-----------------------------------------------------------------------------*/
+    int big,ber;
+    hil_width = w;
+    hil_height = h;
+    big = w > h ? w : h;
+    for (ber = 2, hil_order = 1; ber < big; ber <<= 1, hil_order++);
+    if (hil_order > MAXORD)
+        pm_error("Sorry, hilbert order is too large");
+    hil_ord = hil_order;
+    hil_order--;
+}
+
+
+
+static int 
+hilbert(int * const px, int * const py) {
+/*----------------------------------------------------------------------------
+  Return non-zero if got another point
+-----------------------------------------------------------------------------*/
+    int temp;
+    if (hil_ord > hil_order) {
+        /* have to do first point */
+
+        hil_ord--;
+        hil_stage[hil_ord] = 0;
+        hil_turn = -1;
+        hil_dy = 1;
+        hil_dx = hil_x = hil_y = 0;
+        *px = *py = 0;
+        return 1;
+    }
+
+    /* Operate the state machine */
+    for(;;)  {
+        switch (hil_stage[hil_ord]) {
+        case 0:
+            hil_turn = -hil_turn;
+            temp = hil_dy;
+            hil_dy = -hil_turn * hil_dx;
+            hil_dx = hil_turn * temp;
+            if (hil_ord > 0) {
+                hil_stage[hil_ord] = 1;
+                hil_ord--;
+                hil_stage[hil_ord]=0;
+                continue;
+            }
+        case 1:
+            hil_x += hil_dx;
+            hil_y += hil_dy;
+            if (hil_x < hil_width && hil_y < hil_height) {
+                hil_stage[hil_ord] = 2;
+                *px = hil_x;
+                *py = hil_y;
+                return 1;
+            }
+        case 2:
+            hil_turn = -hil_turn;
+            temp = hil_dy;
+            hil_dy = -hil_turn * hil_dx;
+            hil_dx = hil_turn * temp;
+            if (hil_ord > 0) { 
+                /* recurse */
+
+                hil_stage[hil_ord] = 3;
+                hil_ord--;
+                hil_stage[hil_ord]=0;
+                continue;
+            }
+        case 3:
+            hil_x += hil_dx;
+            hil_y += hil_dy;
+            if (hil_x < hil_width && hil_y < hil_height) {
+                hil_stage[hil_ord] = 4;
+                *px = hil_x;
+                *py = hil_y;
+                return 1;
+            }
+        case 4:
+            if (hil_ord > 0) {
+                /* recurse */
+                hil_stage[hil_ord] = 5;
+                hil_ord--;
+                hil_stage[hil_ord]=0;
+                continue;
+            }
+        case 5:
+            temp = hil_dy;
+            hil_dy = -hil_turn * hil_dx;
+            hil_dx = hil_turn * temp;
+            hil_turn = -hil_turn;
+            hil_x += hil_dx;
+            hil_y += hil_dy;
+            if (hil_x < hil_width && hil_y < hil_height) {
+                hil_stage[hil_ord] = 6;
+                *px = hil_x;
+                *py = hil_y;
+                return 1;
+            }
+        case 6:
+            if (hil_ord > 0) {
+                /* recurse */
+                hil_stage[hil_ord] = 7;
+                hil_ord--;
+                hil_stage[hil_ord]=0;
+                continue;
+            }
+        case 7:
+            temp = hil_dy;
+            hil_dy = -hil_turn * hil_dx;
+            hil_dx = hil_turn * temp;
+            hil_turn = -hil_turn;
+            /* Return from a recursion */
+            if (hil_ord < hil_order)
+                hil_ord++;
+            else
+                return 0;
+        }
+    }
+}
+
+
+
+static void 
+doHilbert(FILE *       const ifP,
+          unsigned int const clumpSize) {
+/*----------------------------------------------------------------------------
+  Use hilbert space filling curve dithering
+-----------------------------------------------------------------------------*/
+    /*
+     * This is taken from the article "Digital Halftoning with
+     * Space Filling Curves" by Luiz Velho, proceedings of
+     * SIGRAPH '91, page 81.
+     *
+     * This is not a terribly efficient or quick version of
+     * this algorithm, but it seems to work. - Graeme Gill.
+     * graeme@labtam.labtam.OZ.AU
+     *
+     */
+    struct pam graypam;
+    struct pam bitpam;
+    tuple ** grays;
+    tuple ** bits;
+
+    int end;
+    int *x,*y;
+    int sum;
+
+    grays = pnm_readpam(ifP, &graypam, sizeof(graypam));
+
+    bitpam = makeOutputPam(graypam.width, graypam.height);
+
+    bits = pnm_allocpamarray(&bitpam);
+
+    MALLOCARRAY(x, clumpSize);
+    MALLOCARRAY(y, clumpSize);
+    if (x == NULL  || y == NULL)
+        pm_error("out of memory");
+    initHilbert(graypam.width, graypam.height);
+
+    sum = 0;
+    end = clumpSize;
+
+    while (end == clumpSize) {
+        unsigned int i;
+        /* compute the next cluster co-ordinates along hilbert path */
+        for (i = 0; i < end; i++) {
+            if (hilbert(&x[i],&y[i])==0)
+                end = i;    /* we reached the end */
+        }
+        /* sum levels */
+        for (i = 0; i < end; i++)
+            sum += grays[y[i]][x[i]][0];
+        /* dither half and half along path */
+        for (i = 0; i < end; i++) {
+            unsigned int const row = y[i];
+            unsigned int const col = x[i];
+            if (sum >= graypam.maxval) {
+                bits[row][col][0] = 1;
+                sum -= graypam.maxval;
+            } else
+                bits[row][col][0] = 0;
+        }
+    }
+    pnm_writepam(&bitpam, bits);
+
+    pnm_freepamarray(bits, &bitpam);
+    pnm_freepamarray(grays, &graypam);
+}
+
+
+
+struct converter {
+    void (*convertRow)(struct converter * const converterP,
+                       unsigned int       const row,
+                       tuplen                   grayrow[], 
+                       tuple                    bitrow[]);
+    void (*destroy)(struct converter * const converterP);
+    unsigned int cols;
+    void * stateP;
+};
+
+
+
+struct fsState {
+    float * thiserr;
+    float * nexterr;
+    bool fs_forward;
+    samplen threshval;
+        /* The power value we consider to be half white */
+};
+
+
+static void
+fsConvertRow(struct converter * const converterP,
+             unsigned int       const row,
+             tuplen                   grayrow[],
+             tuple                    bitrow[]) {
+
+    struct fsState * const stateP = converterP->stateP;
+
+    samplen * const thiserr = stateP->thiserr;
+    samplen * const nexterr = stateP->nexterr;
+
+    unsigned int limitcol;
+    unsigned int col;
+    
+    for (col = 0; col < converterP->cols + 2; ++col)
+        nexterr[col] = 0.0;
+
+    if (stateP->fs_forward) {
+        col = 0;
+        limitcol = converterP->cols;
+    } else {
+        col = converterP->cols - 1;
+        limitcol = -1;
+    }
+
+    do {
+        samplen sum;
+
+        sum = pm_ungamma709(grayrow[col][0]) + thiserr[col + 1];
+        if (sum >= stateP->threshval) {
+            /* We've accumulated enough light to justify a white output
+               pixel.
+            */
+            bitrow[col][0] = PAM_BW_WHITE;
+            /* Remove from sum the power of the white output pixel */
+            sum -= 2*stateP->threshval;
+        } else
+            bitrow[col][0] = PAM_BLACK;
+        
+        /* Forward the power from current input pixel and the power
+           forwarded from previous input pixels to the current pixel,
+           to future output pixels, but subtract out any power we put
+           into the current output pixel.  
+        */
+        if (stateP->fs_forward) {
+            thiserr[col + 2] += (sum * 7) / 16;
+            nexterr[col    ] += (sum * 3) / 16;
+            nexterr[col + 1] += (sum * 5) / 16;
+            nexterr[col + 2] += (sum    ) / 16;
+            
+            ++col;
+        } else {
+            thiserr[col    ] += (sum * 7) / 16;
+            nexterr[col + 2] += (sum * 3) / 16;
+            nexterr[col + 1] += (sum * 5) / 16;
+            nexterr[col    ] += (sum    ) / 16;
+            
+            --col;
+        }
+    } while (col != limitcol);
+    
+    stateP->thiserr = nexterr;
+    stateP->nexterr = thiserr;
+    stateP->fs_forward = ! stateP->fs_forward;
+}
+
+
+
+static void
+fsDestroy(struct converter * const converterP) {
+    free(converterP->stateP);
+}
+
+
+
+static struct converter
+createFsConverter(struct pam * const graypamP,
+                  float        const threshFraction) {
+
+    struct fsState * stateP;
+    struct converter converter;
+
+    converter.cols       = graypamP->width;
+    converter.convertRow = &fsConvertRow;
+    converter.destroy    = &fsDestroy;
+
+    MALLOCVAR_NOFAIL(stateP);
+
+    /* Initialize Floyd-Steinberg error vectors. */
+    MALLOCARRAY_NOFAIL(stateP->thiserr, graypamP->width + 2);
+    MALLOCARRAY_NOFAIL(stateP->nexterr, graypamP->width + 2);
+    srand((int)(time(NULL) ^ getpid()));
+
+    {
+        /* (random errors in [-1/8 .. 1/8]) */
+        unsigned int col;
+        for (col = 0; col < graypamP->width + 2; ++col)
+            stateP->thiserr[col] = ((float)rand()/RAND_MAX - 0.5) / 4;
+    }
+
+    stateP->threshval  = threshFraction;
+
+    stateP->fs_forward = TRUE;
+
+    converter.stateP = stateP;
+
+    return converter;
+}
+
+
+
+struct threshState {
+    samplen threshval;
+};
+
+
+static void
+threshConvertRow(struct converter * const converterP,
+                 unsigned int       const row,
+                 tuplen                   grayrow[],
+                 tuple                    bitrow[]) {
+    
+    struct threshState * const stateP = converterP->stateP;
+
+    unsigned int col;
+    for (col = 0; col < converterP->cols; ++col)
+        bitrow[col][0] =
+            grayrow[col][0] >= stateP->threshval ? PAM_BW_WHITE : PAM_BLACK;
+}
+
+
+
+static void
+threshDestroy(struct converter * const converterP) {
+    free(converterP->stateP);
+}
+
+
+
+static struct converter
+createThreshConverter(struct pam * const graypamP,
+                      float        const threshFraction) {
+
+    struct threshState * stateP;
+    struct converter converter;
+
+    MALLOCVAR_NOFAIL(stateP);
+
+    converter.cols       = graypamP->width;
+    converter.convertRow = &threshConvertRow;
+    converter.destroy    = &threshDestroy;
+    
+    stateP->threshval    = threshFraction;
+    converter.stateP     = stateP;
+
+    return converter;
+}
+
+
+
+struct clusterState {
+    unsigned int radius;
+    float ** clusterMatrix;
+};
+
+
+
+static void
+clusterConvertRow(struct converter * const converterP,
+                  unsigned int       const row,
+                  tuplen                   grayrow[],
+                  tuple                    bitrow[]) {
+
+    struct clusterState * const stateP = converterP->stateP;
+    unsigned int const diameter = 2 * stateP->radius;
+
+    unsigned int col;
+
+    for (col = 0; col < converterP->cols; ++col) {
+        float const threshold = 
+            stateP->clusterMatrix[row % diameter][col % diameter];
+        bitrow[col][0] = 
+            grayrow[col][0] > threshold ? PAM_BW_WHITE : PAM_BLACK;
+    }
+}
+
+
+
+static void
+clusterDestroy(struct converter * const converterP) {
+
+    struct clusterState * const stateP = converterP->stateP;
+    unsigned int const diameter = 2 * stateP->radius;
+
+    unsigned int row;
+
+    for (row = 0; row < diameter; ++row)
+        free(stateP->clusterMatrix[row]);
+
+    free(stateP->clusterMatrix);
+    
+    free(stateP);
+}
+
+
+
+static struct converter
+createClusterConverter(struct pam *    const graypamP,
+                       enum ditherType const ditherType,
+                       unsigned int    const radius) {
+    
+    /* TODO: We create a floating point normalized, gamma-adjusted
+       dither matrix from the old integer dither matrices that were 
+       developed for use with integer arithmetic.  We really should
+       just change the literal values in dither.h instead of computing
+       the matrix from the integer literal values here.
+    */
+    
+    int const clusterNormalizer = radius * radius * 2;
+    unsigned int const diameter = 2 * radius;
+
+    struct converter converter;
+    struct clusterState * stateP;
+    unsigned int row;
+
+    converter.cols       = graypamP->width;
+    converter.convertRow = &clusterConvertRow;
+    converter.destroy    = &clusterDestroy;
+
+    MALLOCVAR_NOFAIL(stateP);
+
+    stateP->radius = radius;
+
+    MALLOCARRAY_NOFAIL(stateP->clusterMatrix, diameter);
+    for (row = 0; row < diameter; ++row) {
+        unsigned int col;
+
+        MALLOCARRAY_NOFAIL(stateP->clusterMatrix[row], diameter);
+        
+        for (col = 0; col < diameter; ++col) {
+            switch (ditherType) {
+            case DT_REGULAR: 
+                switch (radius) {
+                case 8: 
+                    stateP->clusterMatrix[row][col] = 
+                        pm_gamma709((float)dither8[row][col] / 256);
+                    break;
+                default: 
+                    pm_error("INTERNAL ERROR: invalid radius");
+                }
+                break;
+            case DT_CLUSTER: {
+                int val;
+                switch (radius) {
+                case 3: val = cluster3[row][col]; break;
+                case 4: val = cluster4[row][col]; break;
+                case 8: val = cluster8[row][col]; break;
+                default:
+                    pm_error("INTERNAL ERROR: invalid radius");
+                }
+                stateP->clusterMatrix[row][col] = 
+                    pm_gamma709((float)val / clusterNormalizer);
+            }
+            break;
+            }
+        }
+    }            
+
+    converter.stateP = stateP;
+
+    return converter;
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE* ifP;
+
+    pgm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilespec);
+
+    if (cmdline.halftone == QT_HILBERT)
+        doHilbert(ifP, cmdline.clumpSize);
+    else {
+        struct converter converter;
+        struct pam graypam;
+        struct pam bitpam;
+        tuplen * grayrow;
+        tuple * bitrow;
+        int row;
+
+        pnm_readpaminit(ifP, &graypam, sizeof(graypam));
+
+        bitpam = makeOutputPam(graypam.width, graypam.height);
+        
+        pnm_writepaminit(&bitpam);
+
+        switch (cmdline.halftone) {
+        case QT_FS:
+            converter = createFsConverter(&graypam, cmdline.threshval);
+            break;
+        case QT_THRESH:
+            converter = createThreshConverter(&graypam, cmdline.threshval);
+            break;
+        case QT_DITHER8: 
+            converter = createClusterConverter(&graypam, DT_REGULAR, 8); 
+            break;
+        case QT_CLUSTER: 
+            converter = createClusterConverter(&graypam, 
+                                               DT_CLUSTER, 
+                                               cmdline.clusterRadius);
+            break;
+        case QT_HILBERT: 
+                pm_error("INTERNAL ERROR: halftone is QT_HILBERT where it "
+                         "shouldn't be.");
+                break;
+        }
+
+        grayrow = pnm_allocpamrown(&graypam);
+        bitrow  = pnm_allocpamrow(&bitpam);
+
+        for (row = 0; row < graypam.height; ++row) {
+            pnm_readpamrown(&graypam, grayrow);
+
+            converter.convertRow(&converter, row, grayrow, bitrow);
+            
+            pnm_writepamrow(&bitpam, bitrow);
+        }
+        pnm_freepamrow(bitrow);
+        pnm_freepamrow(grayrow);
+
+        if (converter.destroy)
+            converter.destroy(&converter);
+    }
+
+    pm_close(ifP);
+
+    return 0;
+}
diff --git a/editor/pamedge.c b/editor/pamedge.c
new file mode 100644
index 00000000..e73c9d17
--- /dev/null
+++ b/editor/pamedge.c
@@ -0,0 +1,203 @@
+/* pnmedge.c - edge-detection
+**
+** Copyright (C) 1989 by Jef Poskanzer.
+**   modified for pnm by Peter Kirchgessner, 1995.
+**
+** 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 <math.h>
+
+#include "pm_c_util.h"
+#include "pam.h"
+
+
+
+static void
+writeBlackRow(struct pam * const pamP) {
+
+    tuple * const tuplerow = pnm_allocpamrow(pamP);
+
+    unsigned int col;
+    for (col = 0; col < pamP->width; ++col) {
+        unsigned int plane;
+        for (plane = 0; plane < pamP->depth; ++plane)
+            tuplerow[col][plane] = 0;
+    }
+    pnm_writepamrow(pamP, tuplerow);
+} 
+
+
+
+static void
+rotateRows(tuple ** const row0P,
+           tuple ** const row1P,
+           tuple ** const row2P) {
+    /* Rotate rows. */
+    tuple * const formerRow0 = *row0P;
+    *row0P = *row1P;
+    *row1P = *row2P;
+    *row2P = formerRow0;
+}
+
+
+
+static long
+horizGradient(tuple *      const tuplerow,
+              unsigned int const col,
+              unsigned int const plane) {
+
+    return (long)tuplerow[col+1][plane] - (long)tuplerow[col-1][plane];
+}
+
+
+
+static long
+horizAvg(tuple *      const tuplerow,
+         unsigned int const col,
+         unsigned int const plane) {
+
+    return
+        1 * (long)tuplerow[col-1][plane] +
+        2 * (long)tuplerow[col  ][plane] +
+        1 * (long)tuplerow[col+1][plane];
+
+}
+
+
+
+static void
+computeOneRow(struct pam * const inpamP,
+              struct pam * const outpamP,
+              tuple *      const row0,
+              tuple *      const row1,
+              tuple *      const row2,
+              tuple *      const orow) {
+/*----------------------------------------------------------------------------
+   Compute an output row from 3 input rows.
+
+   The input rows must have the same maxval as the output row.
+-----------------------------------------------------------------------------*/
+    unsigned int plane;
+
+    for (plane = 0; plane < inpamP->depth; ++plane) {
+        unsigned int col;
+
+        /* Left column is black */
+        orow[0][plane] = 0;
+
+        for (col = 1; col < inpamP->width - 1; ++col) {
+            double const grad1 = 
+                1 * horizGradient(row0, col, plane) +
+                2 * horizGradient(row1, col, plane) +
+                1 * horizGradient(row2, col, plane);
+
+            double const grad2 = 
+                horizAvg(row2, col, plane) - horizAvg(row0, col, plane);
+
+            double const gradient = sqrt(SQR(grad1) + SQR(grad2));
+
+            /* apply arbitrary scaling factor and maxval clipping */
+            orow[col][plane] = MIN(outpamP->maxval, (long)(gradient / 1.8));
+
+            /* Right column is black */
+            orow[inpamP->width - 1][plane] = 0;
+        }
+    }
+}
+
+
+
+static void
+writeMiddleRows(struct pam * const inpamP,
+                struct pam * const outpamP) {
+
+    tuple *row0, *row1, *row2;
+    tuple *orow, *irow;
+    unsigned int row;
+
+    irow = pnm_allocpamrow(inpamP);
+    orow = pnm_allocpamrow(outpamP);
+    row0 = pnm_allocpamrow(outpamP);
+    row1 = pnm_allocpamrow(outpamP);
+    row2 = pnm_allocpamrow(outpamP);
+
+    /* Read in the first two rows. */
+    pnm_readpamrow(inpamP, irow);
+    pnm_scaletuplerow(inpamP, row0, irow, outpamP->maxval);
+    pnm_readpamrow(inpamP, irow);
+    pnm_scaletuplerow(inpamP, row1, irow, outpamP->maxval);
+
+    pm_message("row1[0][0]=%lu", row1[0][0]);
+
+    for (row = 1; row < inpamP->height - 1; ++row) {
+        /* Read in the next row and write out the current row.  */
+
+        pnm_readpamrow(inpamP, irow);
+        pnm_scaletuplerow(inpamP, row2, irow, outpamP->maxval);
+
+        computeOneRow(inpamP, outpamP, row0, row1, row2, orow);
+
+        pnm_writepamrow(outpamP, orow);
+
+        rotateRows(&row0, &row1, &row2);
+    }
+    pnm_freepamrow(orow);
+    pnm_freepamrow(row2);
+    pnm_freepamrow(row1);
+    pnm_freepamrow(row0);
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+    FILE *ifP;
+    struct pam inpam, outpam;
+
+    pnm_init( &argc, argv );
+
+    if (argc-1 == 1) 
+        ifP = pm_openr(argv[1]);
+    else if (argc-1 == 0)
+        ifP = stdin;
+    else
+        pm_error("Too many arguments.  Program takes at most 1 argument: "
+                 "input file name");
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+    if (inpam.width < 3)
+        pm_error("Image is %u columns wide.  It must be at least 3.",
+                 inpam.width);
+    if (inpam.height < 3)
+        pm_error("Image is %u rows high.  It must be at least 3.",
+                 inpam.height);
+
+    outpam = inpam;
+    outpam.file = stdout;
+    if (PAM_FORMAT_TYPE(inpam.format) == PBM_TYPE) {
+        outpam.format = PGM_FORMAT;
+        outpam.maxval = 255;
+    }
+
+    pnm_writepaminit(&outpam);
+
+    /* First row is black: */
+    writeBlackRow(&outpam      );
+
+    writeMiddleRows(&inpam, &outpam);
+
+    pm_close(ifP);
+
+    /* Last row is black: */
+    writeBlackRow(&outpam);
+
+    pm_close(stdout);
+
+    return 0;
+}
diff --git a/editor/pamenlarge.c b/editor/pamenlarge.c
new file mode 100644
index 00000000..15b91b4f
--- /dev/null
+++ b/editor/pamenlarge.c
@@ -0,0 +1,117 @@
+/*=============================================================================
+                             pamenlarge
+===============================================================================
+  By Bryan Henderson 2004.09.26.  Contributed to the public domain by its
+  author.
+=============================================================================*/
+
+#include "pam.h"
+#include "mallocvar.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;  
+    unsigned int scaleFactor;
+};
+
+
+
+static void
+parseCommandLine(int argc, 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.
+-----------------------------------------------------------------------------*/
+    if (argc-1 < 1)
+        pm_error("You must specify at least one argument:  The scale factor");
+    else {
+        cmdlineP->scaleFactor = atoi(argv[1]);
+        
+        if (cmdlineP->scaleFactor < 1)
+            pm_error("Scale factor must be an integer at least 1.  "
+                     "You specified '%s'", argv[1]);
+
+        if (argc-1 >= 2)
+            cmdlineP->inputFilespec = argv[2];
+        else
+            cmdlineP->inputFilespec = "-";
+    }
+}        
+
+
+
+static void
+makeOutputRowMap(tuple **     const outTupleRowP,
+                 struct pam * const outpamP,
+                 struct pam * const inpamP,
+                 tuple *      const inTuplerow) {
+/*----------------------------------------------------------------------------
+   Create a tuple *outTupleRowP which is actually a row of pointers into
+   inTupleRow[], so as to map input pixels to output pixels by stretching.
+-----------------------------------------------------------------------------*/
+    tuple * newtuplerow;
+    int col;
+
+    MALLOCARRAY_NOFAIL(newtuplerow, outpamP->width);
+
+    for (col = 0 ; col < inpamP->width; ++col) {
+        unsigned int const scaleFactor = outpamP->width / inpamP->width;
+        unsigned int subcol;
+
+        for (subcol = 0; subcol < scaleFactor; ++subcol)
+            newtuplerow[col * scaleFactor + subcol] = inTuplerow[col];
+    }
+    *outTupleRowP = newtuplerow;
+}
+
+
+
+int
+main(int    argc, 
+     char * argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    struct pam inpam;
+    struct pam outpam; 
+    tuple * tuplerow;
+    tuple * newtuplerow;
+    int row;
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilespec);
+ 
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    outpam = inpam; 
+    outpam.file   = stdout;
+    outpam.width  = inpam.width * cmdline.scaleFactor;
+    outpam.height = inpam.height * cmdline.scaleFactor; 
+
+    pnm_writepaminit(&outpam);
+
+    tuplerow = pnm_allocpamrow(&inpam);
+
+    makeOutputRowMap(&newtuplerow, &outpam, &inpam, tuplerow);
+
+    for (row = 0; row < inpam.height; ++row) {
+        pnm_readpamrow(&inpam, tuplerow);
+        pnm_writepamrowmult(&outpam, newtuplerow, cmdline.scaleFactor);
+    }
+
+    free(newtuplerow);
+
+    pnm_freepamrow(tuplerow);
+
+    pm_close(ifP);
+    pm_close(stdout);
+
+    return 0;
+}
+
diff --git a/editor/pamenlarge.test b/editor/pamenlarge.test
new file mode 100644
index 00000000..a2221d4d
--- /dev/null
+++ b/editor/pamenlarge.test
@@ -0,0 +1,8 @@
+echo Test 1.  Should print 3424505894 913236
+./pamenlarge 3 ../testimg.ppm | cksum
+echo Test 2.  Should print 2940246561 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.c b/editor/pamflip.c
new file mode 100644
index 00000000..59b60b56
--- /dev/null
+++ b/editor/pamflip.c
@@ -0,0 +1,910 @@
+/* pamflip.c - perform one or more flip operations on a Netpbm image
+**
+** Copyright (C) 1989 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.
+*/
+
+/*
+   transformGen() is the general transformation function.
+   
+   The following are enhancements for specific cases:
+   
+     transformRowByRowPbm()
+     transformRowsBottomTopPbm()
+     transformRowByRowNonPbm()
+     transformRowsBottomTopNonPbm()
+     transformPbm()
+   
+   Although we use transformGen() only when none of the enhancement
+   functions apply, it is capable of handling all cases.  (Only that it
+   is slow, and uses more memory.)  In the same manner, transformPbm() is
+   capable of handling all pbm transformations and transformRowByRowNonPbm()
+   transformRowsBottomTomNonPbm() are capable of handling pbm.
+
+
+   There is some fancy virtual memory management in transformGen() to avoid
+   page thrashing when you flip a large image in a columns-for-rows
+   way (e.g. -transpose).
+   
+   The page thrashing we're trying to avoid could happen because the
+   output of the transformation is stored in an array of tuples in
+   virtual memory.  A tuple array is stored in column-first order,
+   meaning that all the columns of particular row are contiguous, the
+   next row is next to that, etc.  If you fill up that array by
+   filling in Column 0 sequentially in every row from top to bottom,
+   you will touch a lot of different virtual memory pages, and every
+   one has to be paged in as you touch it.
+
+   If the number of virtual memory pages you touch exceeds the amount
+   of real memory the process can get, then by the time you hit the bottom
+   of the tuple array, the pages that hold the top are already paged out.
+   So if you go back and do Column 1 from top to bottom, you will again
+   touch lots of pages and have to page in every one of them.  Do this 
+   for 100 columns, and you might page in every page in the array 100 times
+   each, putting a few bytes in the page each time.
+
+   That is very expensive.  Instead, you'd like to keep the same pages in
+   real memory as long as possible and fill them up as much as you can 
+   before paging them out and working on a new set of pages.  You can do
+   that by doing Column 0 from top to say Row 10, then Column 1 from top
+   to Row 10, etc. all the way across the image.  Assuming 10 rows fits
+   in real memory, you will keep the virtual memory for the first 10 rows
+   of the tuple array in real memory until you've filled them in completely.
+   Now you go back and do Column 0 from Row 11 to Row 20, then Column 1
+   from Row 11 to Row 20, and so on.
+
+   So why are we even trying to fill in column by column instead of just
+   filling in row by row?  Because we're reading an input image row by row
+   and transforming it in such a way that a row of the input becomes
+   a column of the output.  In order to fill in a whole row of the output,
+   we'd have to read a whole column of the input, and then we have the same
+   page thrashing problem in the input.
+
+   So the optimal procedure is to do N output rows in each pass, where
+   N is the largest number of rows we can fit in real memory.  In each
+   pass, we read certain columns of every row of the input and output
+   every column of certain rows of the output.  The output area for
+   the rows in the pass gets paged in once during the pass and then
+   never again.  Note that some pages of every row of the input get
+   paged in once in each pass too.  As each input page is referenced
+   only in one burst, input pages do not compete with output pages for
+   real memory -- the working set is the output pages, which get referenced
+   cyclically.
+
+   This all worked when we used the pnm xel format, but now that we
+   use the pam tuple format, there's an extra memory reference that
+   could be causing trouble.  Because tuples have varying depth, a pam
+   row is an array of pointers to the tuples.  To access a tuple, we
+   access the tuple pointer, then the tuple.  We could probably do better,
+   because the samples are normally in the memory immediately following
+   the tuple pointers, so we could compute where a tuple's samples live
+   without actually loading the tuple address from memory.  I.e. the 
+   address of the tuple for Column 5 of Row 9 of a 3-deep 100-wide
+   image is (void*)tuples[9] + 100 * sizeof(tuple*) + 5*(3*sizeof(sample)).
+*/
+
+#define _BSD_SOURCE 1      /* Make sure strdup() is in string.h */
+#define _XOPEN_SOURCE 500  /* Make sure strdup() is in string.h */
+
+#include <limits.h>
+#include <string.h>
+
+#include "pam.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+#include "nstring.h"
+#include "bitreverse.h"
+
+enum xformType {LEFTRIGHT, TOPBOTTOM, TRANSPOSE};
+
+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 */
+    unsigned int xformCount;
+        /* Number of transforms in the 'xformType' array */
+    enum xformType xformList[10];
+        /* Array of transforms to be applied, in order */
+    unsigned int availableMemory;
+    unsigned int pageSize;
+    unsigned int verbose;
+};
+
+
+
+static void
+parseXformOpt(const char *     const xformOpt,
+              unsigned int  *  const xformCountP,
+              enum xformType * const xformList) {
+/*----------------------------------------------------------------------------
+   Translate the -xform option string into an array of transform types.
+
+   Return the array as xformList[], which is preallocated for at least
+   10 elements.
+-----------------------------------------------------------------------------*/
+    unsigned int xformCount;
+    char * xformOptWork;
+    char * cursor;
+    bool eol;
+    
+    xformOptWork = strdup(xformOpt);
+    cursor = &xformOptWork[0];
+    
+    eol = FALSE;    /* initial value */
+    xformCount = 0; /* initial value */
+    while (!eol && xformCount < 10) {
+        const char * token;
+        token = strsepN(&cursor, ",");
+        if (token) {
+            if (streq(token, "leftright"))
+                xformList[xformCount++] = LEFTRIGHT;
+            else if (streq(token, "topbottom"))
+                xformList[xformCount++] = TOPBOTTOM;
+            else if (streq(token, "transpose"))
+                xformList[xformCount++] = TRANSPOSE;
+            else if (streq(token, ""))
+            { /* ignore it */}
+            else
+                pm_error("Invalid transform type in -xform option: '%s'",
+                         token );
+        } else
+            eol = TRUE;
+    }
+    free(xformOptWork);
+
+    *xformCountP = xformCount;
+}
+
+
+
+static void
+parseCommandLine(int argc, 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 = malloc(100*sizeof(optEntry));
+        /* Instructions to OptParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+
+    unsigned int lr, tb, xy, r90, r270, r180, null;
+    unsigned int memsizeSpec, pagesizeSpec, xformSpec;
+    unsigned int memsizeOpt;
+    const char *xformOpt;
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    OPTENT3(0, "lr",        OPT_FLAG,    NULL, &lr,      0);
+    OPTENT3(0, "leftright", OPT_FLAG,    NULL, &lr,      0);
+    OPTENT3(0, "tb",        OPT_FLAG,    NULL, &tb,      0);
+    OPTENT3(0, "topbottom", OPT_FLAG,    NULL, &tb,      0);
+    OPTENT3(0, "xy",        OPT_FLAG,    NULL, &xy,      0);
+    OPTENT3(0, "transpose", OPT_FLAG,    NULL, &xy,      0);
+    OPTENT3(0, "r90",       OPT_FLAG,    NULL, &r90,     0);
+    OPTENT3(0, "rotate90",  OPT_FLAG,    NULL, &r90,     0);
+    OPTENT3(0, "ccw",       OPT_FLAG,    NULL, &r90,     0);
+    OPTENT3(0, "r180",      OPT_FLAG,    NULL, &r180,    0);
+    OPTENT3(0, "rotate180", OPT_FLAG,    NULL, &r180,    0);
+    OPTENT3(0, "r270",      OPT_FLAG,    NULL, &r270,    0);
+    OPTENT3(0, "rotate270", OPT_FLAG,    NULL, &r270,    0);
+    OPTENT3(0, "cw",        OPT_FLAG,    NULL, &r270,    0);
+    OPTENT3(0, "null",      OPT_FLAG,    NULL, &null,    0);
+    OPTENT3(0, "verbose",   OPT_FLAG,    NULL, &cmdlineP->verbose,       0);
+    OPTENT3(0, "memsize",   OPT_UINT,    &memsizeOpt, 
+            &memsizeSpec,       0);
+    OPTENT3(0, "pagesize",  OPT_UINT,    &cmdlineP->pageSize,
+            &pagesizeSpec,      0);
+    OPTENT3(0, "xform",     OPT_STRING,  &xformOpt, 
+            &xformSpec, 0);
+
+    opt.opt_table = option_def;
+    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);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (lr + tb + xy + r90 + r180 + r270 + null > 1)
+        pm_error("You may specify only one type of flip.");
+    if (lr + tb + xy + r90 + r180 + r270 + null == 1) {
+        if (lr) {
+            cmdlineP->xformCount = 1;
+            cmdlineP->xformList[0] = LEFTRIGHT;
+        } else if (tb) {
+            cmdlineP->xformCount = 1;
+            cmdlineP->xformList[0] = TOPBOTTOM;
+        } else if (xy) {
+            cmdlineP->xformCount = 1;
+            cmdlineP->xformList[0] = TRANSPOSE;
+        } else if (r90) {
+            cmdlineP->xformCount = 2;
+            cmdlineP->xformList[0] = TRANSPOSE;
+            cmdlineP->xformList[1] = TOPBOTTOM;
+        } else if (r180) {
+            cmdlineP->xformCount = 2;
+            cmdlineP->xformList[0] = LEFTRIGHT;
+            cmdlineP->xformList[1] = TOPBOTTOM;
+        } else if (r270) {
+            cmdlineP->xformCount = 2;
+            cmdlineP->xformList[0] = TRANSPOSE;
+            cmdlineP->xformList[1] = LEFTRIGHT;
+        } else if (null) {
+            cmdlineP->xformCount = 0;
+        }
+    } else if (xformSpec) 
+        parseXformOpt(xformOpt, &cmdlineP->xformCount, cmdlineP->xformList);
+    else
+        pm_error("You must specify an option such as -topbottom to indicate "
+                 "what kind of flip you want.");
+
+    if (memsizeSpec) {
+        if (memsizeOpt > UINT_MAX / 1024 / 1024)
+            pm_error("-memsize value too large: %u MiB.  Maximum this program "
+                     "can handle is %u MiB", 
+                     memsizeOpt, UINT_MAX / 1024 / 1024);
+        cmdlineP->availableMemory = memsizeOpt * 1024 *1024;
+    } else
+        cmdlineP->availableMemory = UINT_MAX;
+
+    if (!pagesizeSpec)
+        cmdlineP->pageSize = 4*1024;         
+
+    if (argc-1 == 0) 
+        cmdlineP->inputFilespec = "-";
+    else if (argc-1 != 1)
+        pm_error("Program takes zero or one argument (filename).  You "
+                 "specified %d", argc-1);
+    else
+        cmdlineP->inputFilespec = argv[1];
+}
+
+
+
+struct xformMatrix {
+    int a;
+    int b;
+    int c;
+    int d;
+    int e;
+    int f;
+};
+
+
+
+static void
+leftright(struct xformMatrix * const xformP) {
+    xformP->a = - xformP->a;
+    xformP->c = - xformP->c;
+    xformP->e = - xformP->e + 1;
+}
+
+
+
+static void
+topbottom(struct xformMatrix * const xformP) {
+    xformP->b = - xformP->b;
+    xformP->d = - xformP->d;
+    xformP->f = - xformP->f + 1;
+}
+
+
+
+static void
+swap(int * const xP, int * const yP) {
+
+    int const t = *xP;
+
+    *xP = *yP;
+    *yP = t;
+}
+
+
+
+static void
+transpose(struct xformMatrix * const xformP) {
+    swap(&xformP->a, &xformP->b);
+    swap(&xformP->c, &xformP->d);
+    swap(&xformP->e, &xformP->f);
+}
+
+
+
+static void
+computeXformMatrix(struct xformMatrix * const xformP, 
+                   unsigned int         const xformCount,
+                   enum xformType             xformType[]) {
+
+    struct xformMatrix const nullTransform = {1, 0, 0, 1, 0, 0};
+
+    unsigned int i;
+
+    *xformP = nullTransform;   /* initial value */
+
+    for (i = 0; i < xformCount; ++i) {
+        switch (xformType[i]) {
+        case LEFTRIGHT: 
+            leftright(xformP);
+            break;
+        case TOPBOTTOM:
+            topbottom(xformP);
+            break;
+        case TRANSPOSE:
+            transpose(xformP);
+            break;
+        }
+    }
+}
+
+
+
+static void
+bitOrderReverse(unsigned char * const bitrow, 
+                unsigned int    const cols) {
+/*----------------------------------------------------------------------------
+  Reverse the bits in a packed pbm row (1 bit per pixel).  I.e. the leftmost
+  bit becomes the rightmost, etc.
+-----------------------------------------------------------------------------*/
+    unsigned int const lastfullByteIdx = cols/8 - 1;
+
+    if (cols == 0 || bitrow == NULL )
+        pm_error("Invalid arguments passed to bitOrderReverse");
+
+    if (cols <= 8)
+        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;
+        }
+    } else {
+        unsigned int const m = cols % 8; 
+
+        unsigned int i, j;
+            /* Cursors into bitrow[].  i moves from left to center;
+               j moves from right to center as bits of bitrow[] are exchanged.
+            */
+        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 (i == j) 
+            /* 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);
+    }
+}
+
+
+
+static void
+transformRowByRowPbm(struct pam * const inpamP, 
+                     struct pam * const outpamP,
+                     bool         const reverse) {
+/*----------------------------------------------------------------------------
+  Transform a PBM image either by flipping it left for right, or just leaving
+  it alone, as indicated by 'reverse'.
+
+  Process the image one row at a time and use fast packed PBM bit
+  reverse algorithm (where required).
+-----------------------------------------------------------------------------*/
+    unsigned char * bitrow;
+    unsigned int row;
+
+    bitrow = pbm_allocrow_packed(outpamP->width); 
+
+    pbm_writepbminit(outpamP->file, outpamP->width, outpamP->height, 0);
+
+    for (row = 0; row < inpamP->height; ++row) {
+        pbm_readpbmrow_packed(inpamP->file,  bitrow, inpamP->width,
+                              inpamP->format);
+
+        if (reverse)
+            bitOrderReverse(bitrow, inpamP->width);
+
+        pbm_writepbmrow_packed(outpamP->file, bitrow, outpamP->width, 0);
+    }
+    pbm_freerow_packed(bitrow);
+}
+
+
+
+static void
+transformRowByRowNonPbm(struct pam * const inpamP, 
+                        struct pam * const outpamP,
+                        bool         const reverse) {
+/*----------------------------------------------------------------------------
+  Flip an image left for right or leave it alone.
+
+  Process one row at a time.
+
+  This works on any image, but is slower and uses more memory than the
+  PBM-only transformRowByRowPbm().
+-----------------------------------------------------------------------------*/
+    tuple * tuplerow;
+    tuple * newtuplerow;
+        /* This is not a full tuple row.  It is either an array of pointers
+           to the tuples in 'tuplerow' (in reverse order) or just 'tuplerow'
+           itself.
+        */
+    tuple * scratchTuplerow;
+    
+    unsigned int row;
+    
+    tuplerow = pnm_allocpamrow(inpamP);
+    
+    if (reverse) {
+        /* Set up newtuplerow[] to point to the tuples of tuplerow[] in
+           reverse order.
+        */
+        unsigned int col;
+        
+        MALLOCARRAY_NOFAIL(scratchTuplerow, inpamP->width);
+
+        for (col = 0; col < inpamP->width; ++col) 
+            scratchTuplerow[col] = tuplerow[inpamP->width - col - 1];
+        newtuplerow = scratchTuplerow;
+    } else {
+        scratchTuplerow = NULL;
+        newtuplerow = tuplerow;
+    }
+    pnm_writepaminit(outpamP);
+
+    for (row = 0; row < inpamP->height ; ++row) {
+        pnm_readpamrow(inpamP, tuplerow);
+        pnm_writepamrow(outpamP, newtuplerow);
+    }
+    
+    if (scratchTuplerow)
+        free(scratchTuplerow);
+    pnm_freepamrow(tuplerow);
+}
+
+
+
+static void
+transformRowByRow(struct pam * const inpamP,
+                  struct pam * const outpamP,
+                  bool         const reverse,
+                  bool         const verbose) {
+
+    if (verbose)
+        pm_message("Transforming row by row, top to bottom");
+
+    switch (PNM_FORMAT_TYPE(inpamP->format)) {
+    case PBM_TYPE:
+        transformRowByRowPbm(inpamP, outpamP, reverse);
+        break;
+    default:
+        transformRowByRowNonPbm(inpamP, outpamP, reverse);
+        break;
+    }
+} 
+
+
+
+static void
+transformRowsBottomTopPbm(struct pam * const inpamP,
+                          struct pam * const outpamP,
+                          bool         const reverse) { 
+/*----------------------------------------------------------------------------
+  Flip a PBM image top for bottom.  Iff 'reverse', also flip it left for right.
+
+  Read complete image into memory in packed PBM format; Use fast
+  packed PBM bit reverse algorithm (where required).
+-----------------------------------------------------------------------------*/
+    unsigned int const rows=inpamP->height;
+
+    unsigned char ** bitrow;
+    int row;
+        
+    bitrow = pbm_allocarray_packed(outpamP->width, outpamP->height);
+        
+    for (row = 0; row < rows; ++row)
+        pbm_readpbmrow_packed(inpamP->file, bitrow[row], 
+                              inpamP->width, inpamP->format);
+
+    pbm_writepbminit(outpamP->file, outpamP->width, outpamP->height, 0);
+
+    for (row = 0; row < rows; ++row) {
+        if (reverse) 
+            bitOrderReverse(bitrow[rows-row-1], inpamP->width);
+
+        pbm_writepbmrow_packed(outpamP->file, bitrow[rows - row - 1],
+                               outpamP->width, 0);
+    }
+    pbm_freearray_packed(bitrow, outpamP->height);
+}
+
+
+
+static void
+transformRowsBottomTopNonPbm(struct pam * const inpamP, 
+                             struct pam * const outpamP,
+                             bool         const reverse) {
+/*----------------------------------------------------------------------------
+  Read complete image into memory as a tuple array.
+
+  This can do any transformation except a column-for-row transformation,
+  on any type of image, but is slower and uses more memory than the 
+  PBM-only transformRowsBottomTopPbm().
+-----------------------------------------------------------------------------*/
+    tuple** tuplerows;
+    tuple * scratchTuplerow;
+        /* This is not a full tuple row -- just an array of pointers to
+           the tuples in 'tuplerows'.
+        */
+    unsigned int row;
+
+    if (reverse)
+        MALLOCARRAY_NOFAIL(scratchTuplerow, inpamP->width);
+    else
+        scratchTuplerow = NULL;
+  
+    tuplerows = pnm_allocpamarray(outpamP);
+
+    for (row = 0; row < inpamP->height ; ++row)
+        pnm_readpamrow(inpamP, tuplerows[row]);
+
+    pnm_writepaminit(outpamP);
+
+    for (row = 0; row < inpamP->height ; ++row) {
+        tuple * newtuplerow;
+        tuple * const tuplerow = tuplerows[inpamP->height - row - 1];
+        if (reverse) {
+            unsigned int col;
+            newtuplerow = scratchTuplerow;
+            for (col = 0; col < inpamP->width; ++col) 
+                newtuplerow[col] = tuplerow[inpamP->width - col - 1];
+        } else
+            newtuplerow = tuplerow;
+        pnm_writepamrow(outpamP, newtuplerow);
+    }
+
+    if (scratchTuplerow)
+        free(scratchTuplerow);
+    
+    pnm_freepamarray(tuplerows, outpamP);
+}
+
+
+
+static void
+transformRowsBottomTop(struct pam * const inpamP,
+                       struct pam * const outpamP,
+                       bool         const reverse,
+                       bool         const verbose) {
+
+    if (PNM_FORMAT_TYPE(inpamP->format) == PBM_TYPE) {
+        if (verbose)
+            pm_message("Transforming PBM row by row, bottom to top");
+        transformRowsBottomTopPbm(inpamP, outpamP, reverse);
+    } else {
+        if (verbose)
+            pm_message("Transforming non-PBM row by row, bottom to top");
+        transformRowsBottomTopNonPbm(inpamP, outpamP, reverse);
+    }
+}
+
+
+
+static void __inline__
+transformPoint(int                const col, 
+               int                const newcols,
+               int                const row, 
+               int                const newrows, 
+               struct xformMatrix const xform, 
+               int *              const newcolP, 
+               int *              const newrowP ) {
+/*----------------------------------------------------------------------------
+   Compute the location in the output of a pixel that is at row 'row',
+   column 'col' in the input.  Assume the output image is 'newcols' by
+   'newrows' and the transformation is as described by 'xform'.
+
+   Return the output image location of the pixel as *newcolP and *newrowP.
+-----------------------------------------------------------------------------*/
+    /* The transformation is:
+     
+                 [ a b 0 ]
+       [ x y 1 ] [ c d 0 ] = [ x2 y2 1 ]
+                 [ e f 1 ]
+    */
+    *newcolP = xform.a * col + xform.c * row + xform.e * (newcols - 1);
+    *newrowP = xform.b * col + xform.d * row + xform.f * (newrows - 1);
+}
+
+
+
+static void
+transformPbm(struct pam *       const inpamP,
+             struct pam *       const outpamP,
+             struct xformMatrix const xform) { 
+/*----------------------------------------------------------------------------
+   This is the same as transformGen, except that it uses less 
+   memory, since the PBM buffer format uses one bit per pixel instead
+   of twelve bytes + pointer space
+
+   This can do any PBM transformation, but is slower and uses more
+   memory than the more restricted transformRowByRowPbm() and
+   transformRowsBottomTopPbm().
+-----------------------------------------------------------------------------*/
+    bit* bitrow;
+    bit** newbits;
+    int row;
+            
+    bitrow = pbm_allocrow(inpamP->width);
+    newbits = pbm_allocarray(pbm_packed_bytes(outpamP->width), 
+                             outpamP->height);
+            
+    /* Initialize entire array to zeroes.  One bits will be or'ed in later */
+    for (row = 0; row < outpamP->height; ++row) {
+        int col;
+        for (col = 0; col < pbm_packed_bytes(outpamP->width); ++col) 
+             newbits[row][col] = 0; 
+    }
+    
+    for (row = 0; row < inpamP->height; ++row) {
+        int col;
+        pbm_readpbmrow(inpamP->file, bitrow, inpamP->width, inpamP->format);
+        for (col = 0; col < inpamP->width; ++col) {
+            int newcol, newrow;
+            transformPoint(col, outpamP->width, row, outpamP->height, xform,
+                           &newcol, &newrow);
+            newbits[newrow][newcol/8] |= bitrow[col] << (7 - newcol % 8);
+                /* Use of "|=" patterned after pbm_readpbmrow_packed. */
+         }
+    }
+    
+    pbm_writepbminit(outpamP->file, outpamP->width, outpamP->height, 0);
+    for (row = 0; row < outpamP->height; ++row)
+        pbm_writepbmrow_packed(outpamP->file, newbits[row], outpamP->width, 
+                               0);
+    
+    pbm_freearray(newbits, outpamP->height);
+    pbm_freerow(bitrow);
+}
+
+
+
+static unsigned int 
+optimalSegmentSize(struct xformMatrix  const xform, 
+                   struct pam *        const pamP,
+                   unsigned int        const availableMemory,
+                   unsigned int        const pageSize) {
+/*----------------------------------------------------------------------------
+   Compute the maximum number of columns that can be transformed, one row
+   at a time, without causing page thrashing.
+
+   See comments at the top of this file for an explanation of the kind
+   of page thrashing using segments avoids.
+
+   'availableMemory' is the amount of real memory in bytes that this
+   process should expect to be able to use.
+
+   'pageSize' is the size of a page in bytes.  A page means the unit that
+   is paged in or out.
+   
+   'pamP' describes the storage required to represent a row of the
+   output array.
+-----------------------------------------------------------------------------*/
+    unsigned int segmentSize;
+
+    if (xform.b == 0)
+        segmentSize = pamP->width;
+    else {
+        unsigned int const otherNeeds = 200*1024;
+            /* A wild guess at how much real memory is needed by the program
+               for purposes other than the output tuple array.
+            */
+        if (otherNeeds > availableMemory)
+            segmentSize = pamP->width;  /* Can't prevent thrashing */
+        else {
+            unsigned int const availablePages = 
+                (availableMemory - otherNeeds) / pageSize;
+            if (availablePages <= 1)
+                segmentSize = pamP->width; /* Can't prevent thrashing */
+            else {
+                unsigned int const bytesPerRow = 
+                    pamP->width * pamP->depth * pamP->bytes_per_sample;
+                unsigned int rowsPerPage = 
+                    MAX(1, (pageSize + (pageSize/2)) / bytesPerRow);
+                    /* This is how many consecutive rows we can touch
+                       on average while staying within the same page.  
+                    */
+                segmentSize = availablePages * rowsPerPage;
+            }
+        }
+    }    
+    return segmentSize;
+}
+
+
+
+static void
+transformNonPbm(struct pam *       const inpamP,
+                struct pam *       const outpamP,
+                struct xformMatrix const xform,
+                unsigned int       const segmentSize,
+                bool               const verbose) {
+/*----------------------------------------------------------------------------
+  Do the transform using "pam" library functions, as opposed to "pbm"
+  ones.
+
+  Assume input file is positioned to the raster (just after the
+  header).
+
+  'segmentSize' is the number of columns we are to process in each
+  pass.  We do each segment going from left to right.  For each
+  segment, we do each row, going from top to bottom.  For each row of
+  the segment, we do each column, going from left to right.  (The
+  reason Caller wants it done by segments is to improve virtual memory
+  reference locality.  See comments at the top of this file).
+
+  if 'segmentSize' is less than the whole image, ifP must be a seekable
+  file.
+
+  This can do any transformation, but is slower and uses more memory
+  than the PBM-only transformPbm().
+-----------------------------------------------------------------------------*/
+    pm_filepos imagepos;
+        /* The input file position of the raster.  But defined only if
+           segment size is less than whole image.
+        */
+    tuple* tuplerow;
+    tuple** newtuples;
+    unsigned int startCol;
+
+    tuplerow = pnm_allocpamrow(inpamP);
+    newtuples = pnm_allocpamarray(outpamP);
+    
+    if (segmentSize < inpamP->width)
+        pm_tell2(inpamP->file, &imagepos, sizeof(imagepos));
+
+    for (startCol = 0; startCol < inpamP->width; startCol += segmentSize) {
+        /* Do one set of columns which is small enough not to cause
+           page thrashing.
+        */
+        unsigned int const endCol = MIN(inpamP->width, startCol + segmentSize);
+        unsigned int row;
+
+        if (verbose)
+            pm_message("Transforming Columns %u up to %u", 
+                       startCol, endCol);
+
+        if (startCol > 0)
+            /* Go back to read from Row 0 again */
+            pm_seek2(inpamP->file, &imagepos, sizeof(imagepos));
+
+        for (row = 0; row < inpamP->height; ++row) {
+            unsigned int col;
+            pnm_readpamrow(inpamP, tuplerow);
+
+            for (col = startCol; col < endCol; ++col) {
+                int newcol, newrow;
+                transformPoint(col, outpamP->width, row, outpamP->height, 
+                               xform,
+                               &newcol, &newrow);
+                pnm_assigntuple(inpamP, newtuples[newrow][newcol],
+                                tuplerow[col]);
+            }
+        }
+    }
+    
+    pnm_writepam(outpamP, newtuples);
+    
+    pnm_freepamarray(newtuples, outpamP);
+    pnm_freepamrow(tuplerow);
+}
+
+
+
+static void
+transformGen(struct pam *       const inpamP,
+             struct pam *       const outpamP,
+             struct xformMatrix const xform,
+             unsigned int       const availableMemory,
+             unsigned int       const pageSize,
+             bool               const verbose) {
+/*----------------------------------------------------------------------------
+  Produce the transformed output on Standard Output.
+
+  Assume input file is positioned to the raster (just after the
+  header).
+
+  This can transform any image in any way, but is slower and uses more
+  memory than the more restricted transformRowByRow() and
+  transformRowsBottomTop().
+-----------------------------------------------------------------------------*/
+    unsigned int const segmentSize = 
+        optimalSegmentSize(xform, outpamP, availableMemory, pageSize);
+    
+    switch (PNM_FORMAT_TYPE(inpamP->format)) {
+    case PBM_TYPE: 
+        transformPbm(inpamP, outpamP, xform);
+        break;
+    default:
+        if (segmentSize < outpamP->width) {
+            if (verbose && xform.b !=0)
+                pm_message("Transforming %u columns of %u total at a time", 
+                           segmentSize, outpamP->width);
+            else
+                pm_message("Transforming entire image at once");
+        }
+        transformNonPbm(inpamP, outpamP, xform, segmentSize, verbose);
+        break;
+    }
+}
+
+
+
+int
+main(int argc, char * argv[]) {
+    struct cmdlineInfo cmdline;
+    struct pam inpam;
+    struct pam outpam;
+    FILE* ifP;
+    struct xformMatrix xform;
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    if (cmdline.availableMemory < UINT_MAX)
+        ifP = pm_openr_seekable(cmdline.inputFilespec);
+    else
+        ifP = pm_openr(cmdline.inputFilespec);
+    
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    computeXformMatrix(&xform, cmdline.xformCount, cmdline.xformList);
+
+    outpam = inpam;  /* initial value */
+
+    outpam.file = stdout;
+    outpam.width  = abs(xform.a) * inpam.width + abs(xform.c) * inpam.height;
+    outpam.height = abs(xform.b) * inpam.width + abs(xform.d) * inpam.height;
+    
+    if (xform.b == 0 && xform.d == 1 && xform.f == 0)
+        /* In this case Row N of the output is based only on Row N of
+           the input, so we can transform row by row and avoid
+           in-memory buffering altogether.  
+        */
+        transformRowByRow(&inpam, &outpam, xform.a == -1, cmdline.verbose);
+    else if (xform.b == 0 && xform.c == 0) 
+        /* In this case, Row N of the output is based only on Row ~N of the
+           input.  We need all the rows in memory, but have to pass
+           through them only twice, so there is no page thrashing concern.
+        */
+        transformRowsBottomTop(&inpam, &outpam, xform.a == -1, 
+                               cmdline.verbose);
+    else
+        /* This is a colum-for-row type of transformation, which requires
+           complex traversal of an in-memory image.
+        */
+        transformGen(&inpam, &outpam, xform,
+                     cmdline.availableMemory, cmdline.pageSize, 
+                     cmdline.verbose);
+
+    pm_close(inpam.file);
+    pm_close(outpam.file);
+    
+    return 0;
+}
diff --git a/editor/pamflip.test b/editor/pamflip.test
new file mode 100644
index 00000000..96e889ea
--- /dev/null
+++ b/editor/pamflip.test
@@ -0,0 +1,12 @@
+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/pamfunc.c b/editor/pamfunc.c
new file mode 100644
index 00000000..dbb1ca70
--- /dev/null
+++ b/editor/pamfunc.c
@@ -0,0 +1,221 @@
+/******************************************************************************
+                               pamfunc
+*******************************************************************************
+  Apply one of various functions to each sample in a PAM image
+
+  By Bryan Henderson, San Jose CA 2002.06.16.
+
+  Contributed to the public domain
+
+  ENHANCEMENT IDEAS:
+
+  1) speed up by doing integer arithmetic instead of floating point for
+  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.
+
+******************************************************************************/
+
+#include "pam.h"
+#include "shhopt.h"
+
+enum function {FN_MULTIPLY, FN_DIVIDE, FN_ADD, FN_SUBTRACT, FN_MIN, FN_MAX};
+
+/* Note that when the user specifies a minimum, that means he's requesting
+   a "max" function.
+*/
+
+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;
+    union {
+        float multiplier;
+        float divisor;
+        int adder;
+        int subtractor;
+        unsigned int max;
+        unsigned int min;
+    } u;
+    unsigned int verbose;
+};
+
+
+static void
+parseCommandLine(int argc, 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 = malloc(100*sizeof(optEntry));
+        /* Instructions to OptParseOptions2 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+
+    unsigned int multiplierSpec, divisorSpec, adderSpec, subtractorSpec;
+    unsigned int maxSpec, minSpec;
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    OPTENT3(0,   "multiplier", OPT_FLOAT,  &cmdlineP->u.multiplier, 
+            &multiplierSpec, 0);
+    OPTENT3(0,   "divisor",    OPT_FLOAT,  &cmdlineP->u.divisor,
+            &divisorSpec, 0);
+    OPTENT3(0,   "adder",      OPT_INT,    &cmdlineP->u.adder,
+            &adderSpec, 0);
+    OPTENT3(0,   "subtractor", OPT_INT,    &cmdlineP->u.subtractor,
+            &subtractorSpec, 0);
+    OPTENT3(0,   "min",        OPT_UINT,   &cmdlineP->u.min,
+            &minSpec, 0);
+    OPTENT3(0,   "max",        OPT_UINT,   &cmdlineP->u.max,
+            &maxSpec, 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 *cmdlineP and others. */
+
+    if (multiplierSpec + divisorSpec + adderSpec + subtractorSpec +
+        minSpec + maxSpec > 1)
+        pm_error("You may specify at most one of -multiplier, -divisor,"
+                 "-adder, -subtractor, -min, and -max");
+
+    if (multiplierSpec) {
+        cmdlineP->function = FN_MULTIPLY;
+        if (cmdlineP->u.multiplier < 0)
+            pm_error("Multiplier must be nonnegative.  You specified %f", 
+                     cmdlineP->u.multiplier);
+    } else if (divisorSpec) {
+        cmdlineP->function = FN_DIVIDE;
+        if (cmdlineP->u.divisor < 0)
+            pm_error("Divisor must be nonnegative.  You specified %f", 
+                     cmdlineP->u.divisor);
+    } else if (adderSpec) {
+        cmdlineP->function = FN_ADD;
+    } else if (subtractorSpec) {
+        cmdlineP->function = FN_SUBTRACT;
+    } else if (minSpec) {
+        cmdlineP->function = FN_MAX;
+    } else if (maxSpec) {
+        cmdlineP->function = FN_MIN;
+    } else 
+        pm_error("You must specify one of -multiplier, -divisor, "
+                 "-adder, -subtractor, -min, or -max");
+        
+    if (argc-1 > 1)
+        pm_error("Too many arguments (%d).  File spec is the only argument.",
+                 argc-1);
+
+    if (argc-1 < 1)
+        cmdlineP->inputFilespec = "-";
+    else 
+        cmdlineP->inputFilespec = argv[1];
+    
+}
+
+
+
+static void
+applyFunction(struct cmdlineInfo const cmdline,
+              struct pam         const inpam,
+              struct pam         const outpam,
+              tuple *            const inputRow,
+              tuple *            const outputRow) {
+
+    float const oneOverDivisor = 1/cmdline.u.divisor;
+        /* In my experiments, the compiler couldn't figure out that
+           1/cmdline.u.divisor is a constant and instead recomputed it
+           for each and every pixel.  division is slower than
+           multiplication, so we want to multiply by
+           1/cmdline.u.divisor instead of divide by cmdline.u.divisor,
+           so we compute that here.  Note that if the function isn't
+           divide, both cmdline.u.divisor and oneOverDivisor are
+           meaningless.  
+        */
+    int col;
+
+    for (col = 0; col < inpam.width; ++col) {
+        int plane;
+        for (plane = 0; plane < inpam.depth; ++plane) {
+            sample const inSample = inputRow[col][plane];
+            sample outSample;  /* Could be > maxval  */
+
+            switch (cmdline.function) {
+            case FN_MULTIPLY:
+                outSample = ROUNDU(inSample * cmdline.u.multiplier);
+                break;
+            case FN_DIVIDE:
+                outSample = ROUNDU(inSample * oneOverDivisor);
+                break;
+            case FN_ADD:
+                outSample = MAX(0, (long)inSample + cmdline.u.adder);
+                break;
+            case FN_SUBTRACT:
+                outSample = MAX(0, (long)inSample - cmdline.u.subtractor);
+                break;
+            case FN_MAX:
+                outSample = MAX(inSample, cmdline.u.min);
+                break;
+            case FN_MIN:
+                outSample = MIN(inSample, cmdline.u.max);
+                break;
+            }
+            outputRow[col][plane] = MIN(outpam.maxval, outSample);
+        }
+    }
+}                
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    FILE* ifP;
+    tuple* inputRow;   /* Row from input image */
+    tuple* outputRow;  /* Row of output image */
+    int row;
+    struct cmdlineInfo cmdline;
+    struct pam inpam;   /* Input PAM image */
+    struct pam outpam;  /* Output PAM image */
+
+    pnm_init( &argc, argv );
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilespec);
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    inputRow = pnm_allocpamrow(&inpam);
+
+    outpam = inpam;    /* Initial value -- most fields should be same */
+    outpam.file = stdout;
+
+    pnm_writepaminit(&outpam);
+
+    outputRow = pnm_allocpamrow(&outpam);
+
+    for (row = 0; row < inpam.height; row++) {
+        pnm_readpamrow(&inpam, inputRow);
+
+        applyFunction(cmdline, inpam, outpam, inputRow, outputRow);
+
+        pnm_writepamrow(&outpam, outputRow);
+    }
+    pnm_freepamrow(outputRow);
+    pnm_freepamrow(inputRow);
+    pm_close(inpam.file);
+    pm_close(outpam.file);
+    
+    exit(0);
+}
+
diff --git a/editor/pammasksharpen.c b/editor/pammasksharpen.c
new file mode 100644
index 00000000..87b928be
--- /dev/null
+++ b/editor/pammasksharpen.c
@@ -0,0 +1,192 @@
+#include "pam.h"
+#include "shhopt.h"
+#include "mallocvar.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;  
+    const char * maskFilespec;  
+    unsigned int verbose;
+    float        sharpness;
+    float        threshold;
+};
+
+
+
+static void
+parseCommandLine(int argc, 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 OptParseOptions2 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+
+    unsigned int sharpSpec, thresholdSpec;
+    
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    OPTENT3(0, "sharpness",       OPT_FLOAT,  &cmdlineP->sharpness,
+            &sharpSpec,           0);
+    OPTENT3(0, "threshold",       OPT_FLOAT,  &cmdlineP->threshold,
+            &thresholdSpec,       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 *cmdlineP and others. */
+
+    if (sharpSpec) {
+        if (cmdlineP->sharpness < 0)
+            pm_error("-sharpness less than zero doesn't make sense.  "
+                     "You specified %f", cmdlineP->sharpness);
+    } else
+        cmdlineP->sharpness = 1.0;
+
+    if (thresholdSpec) {
+        if (cmdlineP->threshold < 0)
+            pm_error("-threshold less than zero doesn't make sense.  "
+                     "You specified %f", cmdlineP->threshold);
+        if (cmdlineP->threshold > 1.0)
+            pm_error("-threshold greater than unity doesn't make sense.  "
+                     "You specified %f", cmdlineP->threshold);
+        
+    } else
+        cmdlineP->threshold = 0.0;
+
+    if (argc-1 < 1)
+        pm_error("You must specify at least one argument:  The name "
+                 "of the mask image file");
+    else {
+        cmdlineP->maskFilespec = argv[1];
+        if (argc-1 < 2)
+            cmdlineP->inputFilespec = "-";
+        else {
+            cmdlineP->inputFilespec = argv[2];
+        
+            if (argc-1 > 2)
+                pm_error("There are at most two arguments:  mask file name "
+                         "and input file name.  You specified %d", argc-1);
+        }
+    }
+}        
+
+
+
+static sample
+sharpened(sample const inputSample,
+          sample const maskSample,
+          float  const sharpness,
+          sample const threshold,
+          sample const maxval) {
+
+    int const edgeness = inputSample - maskSample;
+
+    sample retval;
+
+    if (abs(edgeness) > threshold) {
+        float const rawResult = inputSample + edgeness * sharpness;
+        
+        retval = MIN(maxval, (unsigned)MAX(0, (int)(rawResult+0.5)));
+    } else
+        retval = inputSample;
+
+    return retval;
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    struct cmdlineInfo cmdline;
+    struct pam inpam;
+    struct pam maskpam;
+    struct pam outpam;
+    FILE * ifP;
+    FILE * maskfP;
+    tuple * inputTuplerow;
+    tuple * maskTuplerow;
+    tuple * outputTuplerow;
+    unsigned int row;
+    sample threshold;
+        /* Magnitude of difference between image and unsharp mask below
+           which they will be considered identical.
+        */
+    
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilespec);
+    maskfP = pm_openr(cmdline.maskFilespec);
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+    pnm_readpaminit(maskfP, &maskpam, PAM_STRUCT_SIZE(tuple_type));
+
+    if (inpam.width  != maskpam.width || 
+        inpam.height != maskpam.height ||
+        inpam.depth  != maskpam.depth)
+        pm_error("The mask image must be the same dimensions as the "
+                 "input image.  The mask is %dx%dx%d, but the input is "
+                 "%dx%dx%d.",
+                 maskpam.width, maskpam.height, maskpam.depth,
+                 inpam.width,   inpam.height,   inpam.depth);
+    if (inpam.maxval != maskpam.maxval)
+        pm_error("The mask image must have the same maxval as the "
+                 "input image.  The input image has maxval %u, "
+                 "but the mask image has maxval %u",
+                 (unsigned)inpam.maxval, (unsigned)maskpam.maxval);
+
+    threshold = (float)cmdline.threshold / inpam.maxval;
+
+    outpam = inpam;
+    outpam.file = stdout;
+
+    inputTuplerow  = pnm_allocpamrow(&inpam);
+    maskTuplerow   = pnm_allocpamrow(&maskpam);
+    outputTuplerow = pnm_allocpamrow(&outpam);
+
+    pnm_writepaminit(&outpam);
+
+    for (row = 0; row < outpam.height; ++row) {
+        unsigned int col;
+        pnm_readpamrow(&inpam,   inputTuplerow);
+        pnm_readpamrow(&maskpam, maskTuplerow);
+        
+        for (col = 0; col < outpam.width; ++col) {
+            unsigned int plane;
+            
+            for (plane = 0; plane < outpam.depth; ++plane) {
+                outputTuplerow[col][plane] =
+                    sharpened(inputTuplerow[col][plane],
+                              maskTuplerow[col][plane],
+                              cmdline.sharpness,
+                              threshold,
+                              outpam.maxval);
+            }
+        }
+        pnm_writepamrow(&outpam, outputTuplerow);
+    }
+
+    pm_close(ifP);
+    pm_close(maskfP);
+
+    pnm_freepamrow(inputTuplerow);
+    pnm_freepamrow(maskTuplerow);
+    pnm_freepamrow(outputTuplerow);
+    
+    return 0;
+}
diff --git a/editor/pammixinterlace.c b/editor/pammixinterlace.c
new file mode 100644
index 00000000..1421c7a2
--- /dev/null
+++ b/editor/pammixinterlace.c
@@ -0,0 +1,173 @@
+/******************************************************************************
+                             pammixinterlace
+*******************************************************************************
+  De-interlace an image by merging adjacent rows.
+   
+  Copyright (C) 2005 Bruce Guenter, FutureQuest, Inc.
+
+  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 "pam.h"
+#include "shhopt.h"
+#include "mallocvar.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 */
+};
+
+
+static void
+parseCommandLine(int argc, 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;
+    unsigned int option_def_index;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+
+    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 (argc-1 < 1)
+        cmdlineP->inputFilespec = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFilespec = argv[1];
+    else
+        pm_error("You specified too many arguments (%d).  The only "
+                 "argument is the optional input file specification.",
+                 argc-1);
+}
+
+
+
+static void
+allocateRowWindowBuffer(struct pam * const pamP,
+                        tuple **     const tuplerow) {
+
+    unsigned int row;
+
+    for (row = 0; row < 3; ++row)
+        tuplerow[row] = pnm_allocpamrow(pamP);
+}
+
+
+
+static void
+freeRowWindowBuffer(tuple ** const tuplerow) {
+
+    unsigned int row;
+
+    for (row = 0; row < 3; ++row)
+        pnm_freepamrow(tuplerow[row]);
+
+}
+
+
+
+static void
+slideWindowDown(tuple ** const tuplerow) {
+/*----------------------------------------------------------------------------
+  Slide the 3-line tuple row window tuplerow[] down one row by moving
+  pointers.
+
+  tuplerow[2] ends up an uninitialized buffer.
+-----------------------------------------------------------------------------*/
+    tuple * const oldrow0 = tuplerow[0];
+    tuplerow[0] = tuplerow[1];
+    tuplerow[1] = tuplerow[2];
+    tuplerow[2] = oldrow0;
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    FILE * ifP;
+    struct cmdlineInfo cmdline;
+    struct pam inpam;  
+    struct pam outpam;
+    tuple * tuplerow[3];
+    tuple * outputrow;
+    
+    pnm_init( &argc, argv );
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilespec);
+    
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    outpam = inpam;    /* Initial value -- most fields should be same */
+    outpam.file = stdout;
+
+    pnm_writepaminit(&outpam);
+
+    allocateRowWindowBuffer(&inpam, tuplerow);
+    outputrow = pnm_allocpamrow(&outpam);
+
+    if (inpam.height < 3) {
+        unsigned int row;
+        pm_message("WARNING: Image height less than 3.  No mixing done.");
+        for (row = 0; row < inpam.height; ++row) {
+            pnm_readpamrow(&inpam, tuplerow[0]);
+            pnm_writepamrow(&outpam, tuplerow[0]);
+        }
+    } else {
+        unsigned int row;
+
+        pnm_readpamrow(&inpam, tuplerow[0]);
+        pnm_readpamrow(&inpam, tuplerow[1]);
+
+        /* Pass through first row */
+        pnm_writepamrow(&outpam, tuplerow[0]);
+
+        for (row = 2; row < inpam.height; ++row) {
+            unsigned int col;
+            pnm_readpamrow(&inpam, tuplerow[2]);
+            for (col = 0; col < inpam.width; ++col) {
+                unsigned int plane;
+
+                for (plane = 0; plane < inpam.depth; ++plane) {
+                    outputrow[col][plane] =
+                        (tuplerow[0][col][plane]
+                         + tuplerow[1][col][plane] * 2
+                         + tuplerow[2][col][plane]) / 4;
+                }
+            }
+            pnm_writepamrow(&outpam, outputrow);
+            
+            slideWindowDown(tuplerow);
+        }
+
+        /* Pass through last row */
+        pnm_writepamrow(&outpam, tuplerow[1]);
+    }
+
+    freeRowWindowBuffer(tuplerow);
+    pnm_freepamrow(outputrow);
+    pm_close(inpam.file);
+    pm_close(outpam.file);
+    
+    return 0;
+}
diff --git a/editor/pamoil.c b/editor/pamoil.c
new file mode 100644
index 00000000..6cb8d3ac
--- /dev/null
+++ b/editor/pamoil.c
@@ -0,0 +1,137 @@
+/* pgmoil.c - read a portable pixmap and turn into an oil painting
+**
+** Copyright (C) 1990 by Wilson Bent (whb@hoh-2.att.com)
+** Shamelessly butchered into a color version by Chris Sheppard
+** 2001
+**
+** 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 "pam.h"
+#include "mallocvar.h"
+
+static void 
+convertRow(struct pam const inpam, tuple ** const tuples,
+           tuple * const tuplerow, int const row, int const smearFactor,
+           int * const hist) {
+
+    int sample;
+    for (sample = 0; sample < inpam.depth; sample++) {
+        int col;
+        for (col = 0; col < inpam.width; ++col)  {
+            int i;
+            int drow;
+            int modalval;
+                /* The sample value that occurs most often in the neighborhood
+                   of the pixel being examined
+                */
+
+            /* Compute hist[] - frequencies, in the neighborhood, of each 
+               sample value
+            */
+            for (i = 0; i <= inpam.maxval; ++i) hist[i] = 0;
+
+            for (drow = row - smearFactor; drow <= row + smearFactor; ++drow) {
+                if (drow >= 0 && drow < inpam.height) {
+                    int dcol;
+                    for (dcol = col - smearFactor; 
+                         dcol <= col + smearFactor; 
+                         ++dcol) {
+                        if ( dcol >= 0 && dcol < inpam.width )
+                            ++hist[tuples[drow][dcol][sample]];
+                    }
+                }
+            }
+            {
+                /* Compute modalval */
+                int sampleval;
+                int maxfreq;
+
+                maxfreq = 0;
+                modalval = 0;
+
+                for (sampleval = 0; sampleval <= inpam.maxval; ++sampleval) {
+                    if (hist[sampleval] > maxfreq) {
+                        maxfreq = hist[sampleval];
+                        modalval = sampleval;
+                    }
+                }
+            }
+            tuplerow[col][sample] = modalval;
+        }
+    }
+}
+
+
+
+int
+main(int argc, char *argv[] ) {
+    struct pam inpam, outpam;
+    FILE* ifp;
+    tuple ** tuples;
+    tuple * tuplerow;
+    int * hist;
+        /* A buffer for the convertRow subroutine to use */
+    int argn;
+    int row;
+    int smearFactor;
+    const char* const usage = "[-n <n>] [ppmfile]";
+
+    ppm_init( &argc, argv );
+
+    argn = 1;
+    smearFactor = 3;       /* DEFAULT VALUE */
+
+    /* Check for options. */
+    if ( argn < argc && argv[argn][0] == '-' ) {
+        if ( argv[argn][1] == 'n' ) {
+            ++argn;
+            if ( argn == argc || sscanf(argv[argn], "%d", &smearFactor) != 1 )
+                pm_usage( usage );
+        } else
+            pm_usage( usage );
+        ++argn;
+    }
+    if ( argn < argc ) {
+        ifp = pm_openr( argv[argn] );
+        ++argn;
+    } else
+        ifp = stdin;
+
+    if ( argn != argc )
+        pm_usage( usage );
+
+    tuples = pnm_readpam(ifp, &inpam, PAM_STRUCT_SIZE(tuple_type));
+    pm_close(ifp);
+
+    MALLOCARRAY(hist, inpam.maxval + 1);
+    if (hist == NULL)
+        pm_error("Unable to allocate memory for histogram.");
+
+    outpam = inpam; outpam.file = stdout;
+
+    pnm_writepaminit(&outpam);
+
+    tuplerow = pnm_allocpamrow(&inpam);
+
+    for (row = 0; row < inpam.height; ++row) {
+        convertRow(inpam, tuples, tuplerow, row, smearFactor, hist);
+        pnm_writepamrow(&outpam, tuplerow);
+    }
+
+    pnm_freepamrow(tuplerow);
+    free(hist);
+    pnm_freepamarray(tuples, &inpam);
+
+    pm_close(stdout);
+    exit(0);
+}
+
diff --git a/editor/pamperspective.c b/editor/pamperspective.c
new file mode 100644
index 00000000..fdf446c7
--- /dev/null
+++ b/editor/pamperspective.c
@@ -0,0 +1,1331 @@
+/*
+    pamperspective -- a reverse scanline renderer
+
+    Copyright (C) 2004 by Mark Weyer
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+*/
+
+#define _BSD_SOURCE   /* Make sure strdup is int string.h */
+
+#include <math.h>
+#include <string.h>
+
+#include "pam.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+typedef double number;
+
+/* There was no reason for exactly this value of eps.
+   For compatibility it should only be decreased in future versions.
+*/
+
+#define eps 0.0001
+
+
+/* Multiple choice types for the command line */
+
+typedef enum {image, pixel_u} unit;
+const char *const unit_token[3] = {"image", "pixel", NULL};
+
+typedef enum {lattice, pixel_s} coord_system;
+const char *const system_token[3] = {"lattice", "pixel", NULL};
+
+typedef enum {nearest, linear} interpolation;
+const char *const interpolation_token[3] = {"nearest", "linear", NULL};
+
+typedef enum {free_, fixed} proportion;
+const char *const proportion_token[3] = {"free", "fixed", NULL};
+
+const char *const bool_token[7] = {"yes", "true", "on",
+                                    "no", "false", "off", NULL};
+#define first_false_bool_token 3
+
+/* All command line options that have float (actually number) values.
+   We use our own parsing technique for these, to handle width/height
+   ratios like 4/3
+*/
+
+#define num_float_options 15
+const char *const float_option_name[num_float_options][3] = {
+  {"upper left x", "upper_left_x", "ulx"},
+  {"upper left y", "upper_left_y", "uly"},
+  {"upper right x", "upper_right_x", "urx"},
+  {"upper right y", "upper_right_y", "ury"},
+  {"lower left x", "lower_left_x", "llx"},
+  {"lower left y", "lower_left_y", "lly"},
+  {"lower right x", "lower_right_x", "lrx"},
+  {"lower right y", "lower_right_y", "lry"},
+  {NULL, "detail", NULL},
+  {NULL, "ratio", NULL},
+  {NULL, "margin", NULL},
+  {NULL, "top_margin", "tmargin"},
+  {NULL, "bottom_margin", "bmargin"},
+  {NULL, "left_margin", "lmargin"},
+  {NULL, "right_margin", "rmargin"}
+};
+
+/* All command line options that have multiple choice values (except bools). */
+
+#define num_enum_options 5
+const char *const enum_option_name[num_enum_options] = {
+  "input_system", "output_system", "input_unit", "interpolation", "proportion"
+};
+const char *const *const enum_option_type[num_enum_options] = {
+  system_token, system_token, unit_token, interpolation_token, proportion_token
+};
+
+/* All command line options that have bool values */
+
+#define num_bool_options 1
+const char *const bool_option_name[num_bool_options] = {
+  "frame_include"
+};
+
+
+/* A linked list node for --include points */
+
+typedef struct include_point_tag {
+
+  /* How the point is given on the command line (for error messages) */
+
+  const char* specification;
+
+  /* coordinates */
+
+  number xi,yi;
+
+  /* link */
+
+  struct include_point_tag* next;
+
+} include_point;
+
+
+/* The collection of command line options */
+
+typedef struct {
+
+  /* float options */
+
+  number floats[num_float_options];
+
+  /* enum options */
+
+  int enums[num_enum_options];
+
+  /* bool options */
+
+  bool bools[num_bool_options];
+
+  /* flags */
+
+  unsigned int width_spec, height_spec,
+    top_margin_spec, left_margin_spec, right_margin_spec, bottom_margin_spec;
+
+  /* Other stuff */
+
+  int width, height;
+  const char* infilename;
+  include_point* include_points;
+
+} option;
+
+
+/* The collection of properties that correspond to the four specified
+   vertices 
+*/
+
+typedef struct {
+
+  /* 2d (image) coordinates of the 4 vertices */
+
+  number xi_ul, yi_ul,  xi_ur, yi_ur,  xi_ll, yi_ll,  xi_lr, yi_lr;
+
+  /* 3d (world) coordinates of the 4 vertices */
+
+  number xw_ul, yw_ul, zw_ul,  xw_ur, yw_ur, zw_ur,
+         xw_ll, yw_ll, zw_ll,  xw_lr, yw_lr, zw_lr;
+
+  /* Originally I planned to include the possibility to move the
+     centre of projection, that is the pixel the camera "looks at".  It
+     turned out, maybe surprisingly, that this does not have any
+     effect. So now this centre is moved to (0,0).
+     
+     Another original plan was to correct the output parameters
+     depending on the lengths of the paralellograms sides or its
+     angles.  This is, however, not possible without knowing something
+     like the camera angle or focal length (in pixels).
+  */
+  
+  /* The coefficients for the map from output to world coordinates.
+
+     The actual mapping is
+     u,v -> (ax+bx*u+cx*v, ay+by*u+cy*v, az+bz*u+cz*v)
+  */     
+  number ax,bx,cx, ay,by,cy, az,bz,cz;
+
+} world_data;
+
+
+/*
+  Internal infile buffer
+
+  This is a cyclic in random access out buffer, just large enough
+  to store all input lines that are still in use.
+*/
+
+typedef struct {
+
+  int num_rows, last_physical, last_logical;
+  tuple** rows;
+  const struct pam* inpam;
+
+} buffer;
+
+
+
+/*
+  The following are like MALLOCARRAY_NOFAIL and MALLOCVAR_NOFAIL,
+  but issue an error message instead of aborting.
+*/
+
+#define MALLOCARRAY_SAFE(handle,length) \
+{ \
+  MALLOCARRAY(handle,length); \
+  if (handle==NULL) \
+    pm_error ("Out of memory."); \
+}
+
+#define MALLOCVAR_SAFE(handle) \
+{ \
+  MALLOCVAR(handle); \
+  if (handle==NULL) \
+    pm_error ("Out of memory."); \
+}
+
+
+
+static void set_command_line_defaults (option *const options)
+{
+  options->infilename = "-";
+  options->include_points = NULL;
+  options->floats[8] = 1.0;             /* --detail               */
+  options->floats[9] = 1.0;             /* --ratio                */
+  options->floats[10] = 0.0;            /* --margin               */
+  options->floats[11] = 0.0;            /* --top_margin           */
+  options->floats[12] = 0.0;            /* --bottom_margin        */
+  options->floats[13] = 0.0;            /* --left_margin          */
+  options->floats[14] = 0.0;            /* --right_margin         */
+  options->enums[0] = lattice;          /* --input_system         */
+  options->enums[1] = lattice;          /* --output_system        */
+  options->enums[2] = pixel_u;          /* --input_unit           */
+  options->enums[3] = nearest;          /* --interpolation        */
+  options->enums[4] = free_;            /* --proportion           */
+  options->bools[0] = TRUE;             /* --frame_include        */
+}
+
+
+
+static int parse_enum (const char *const text,
+                       const char *const *const tokens, const char *const name)
+/*----------------------------------------------------------------------------
+  Parse an argument given to a multiple choice command line option
+-----------------------------------------------------------------------------*/
+{
+  bool found;
+  const char *const * cur_token;
+  char* tokenlist;
+  int tokenlistlen;
+  int value;
+  int num_spaces;
+  int i;
+
+  /* We find out, whether ^text occurs in ^tokens */
+
+  found = FALSE;
+  value = 0;
+  while (tokens[value] && !found) {
+    if (strcmp (text, tokens[value]))
+      value++;
+    else
+      found = TRUE;
+  };
+
+  /* otherwise issue an error */
+
+  if (!found) {
+    /* For the error message we want to list the allowed tokens.
+       First we have to determine, how much memory we need for that.
+    */
+    num_spaces = 2;
+
+    tokenlistlen = 0;
+    cur_token = tokens;
+    while (*cur_token) {
+      tokenlistlen += (strlen(*cur_token) + num_spaces);
+      cur_token++;
+    };
+    /* Then we create that list */
+    MALLOCARRAY_SAFE(tokenlist, tokenlistlen);
+    *tokenlist = 0;
+    cur_token = tokens;
+    while (*cur_token) {
+      for (i=0; i<num_spaces; i++)
+    strcat (tokenlist, " ");
+      strcat (tokenlist, *cur_token);
+      cur_token++;
+    };
+    /* Finally we issue the error */
+    pm_error ("'%s' is not a valid value for --%s.  "
+              "Valid values are:  %s", text, name, tokenlist);
+    /* pm_error() aborts the program, so there is no memory freeing here. */
+  };
+
+  /* If all went well, we return the value associated with the token,
+     which happens to be the index where we found the token
+  */
+  return value;
+}
+
+
+
+static number parse_float (char *const text)
+/*----------------------------------------------------------------------------
+  Parse an argument given to a float command line option.  We cannot
+  just call strtod, because we want to parse fractions like "5/3"
+-----------------------------------------------------------------------------*/
+{
+  bool error;
+  char* end;
+  char* denstart;
+  number num,den;
+
+  error = FALSE;
+  num = strtod (text, &end);    /* try strtod anyway */
+  switch (*end) {
+  case 0:           /* It is a plain number */
+    break;
+  case '/':         /* It might be a fraction */
+    /* (Try to) parse the numerator */
+    *end = 0;
+    num = strtod (text, &end);
+    error = (*end) != 0;
+    if (!error) {
+      /* Undo the above change */
+      *end = '/';
+      /* (Try to) parse the denominator */
+      denstart = end+1;
+      den = strtod (denstart, &end);
+      error = (fabs(den)<eps) || ((*end) != 0);
+      if (!error)
+    num /= den;
+    };
+    break;
+  default:          /* It is no number format we know */
+    error = TRUE;
+  };
+  if (error)
+    pm_error ("Invalid number format: %s", text);
+
+  return num;
+}
+
+
+
+static void parse_include_point(char * specification,
+                                include_point ** const include_pointsP)
+/*----------------------------------------------------------------------------
+  Add one point to the front of the linked list of include points
+  headed by include_pointsP.
+
+  The point is described by the asciiz string at 'specification'.
+----------------------------------------------------------------------------*/
+{
+  include_point* new_point;
+  char* comma_seek;
+
+  MALLOCVAR_SAFE(new_point);
+  new_point->specification = specification;
+  new_point->next = *include_pointsP;
+  *include_pointsP = new_point;
+
+  /* Now we parse the specification */
+
+  for (comma_seek = specification; (*comma_seek != ',') && (*comma_seek != 0);
+       comma_seek++);
+  if (*comma_seek == 0)
+    pm_error ("Invalid format for --include point: '%s'", specification);
+  *comma_seek = 0;      /* separate the two parts for parsing purposes */
+  new_point->xi = (number) parse_float(specification);
+  new_point->yi = (number) parse_float(comma_seek+1);
+  *comma_seek = ',';
+}
+
+
+static void parse_include_points(const char * const include_opt,
+                                 include_point ** const include_pointsP)
+/*----------------------------------------------------------------------------
+  Process the --include option value include_opt by making a linked list
+  of the points it describes (in reverse order).
+
+  Return a pointer to the first element of that linked list as
+  *include_pointsP.
+----------------------------------------------------------------------------*/
+{
+    char * cursor;
+    char * optWork;
+        /* Same as include_opt, except we replace delimiters with nulls
+           as we work.
+        */
+
+    optWork = strdup(include_opt);
+    if (optWork == NULL)
+        pm_error("out of memory");
+
+    cursor = &optWork[0];
+    while (*cursor != '\0') {
+        bool hit_end;
+        char * sem_seek;
+
+        for (sem_seek = cursor;
+             (*sem_seek != ';') && (*sem_seek != 0);
+             sem_seek++);
+
+        hit_end = (*sem_seek == '\0');
+            
+        *sem_seek = '\0';
+        parse_include_point(cursor, include_pointsP);
+
+        if (hit_end)
+            cursor = sem_seek;
+        else
+            cursor = sem_seek+1;
+    }
+    free(optWork);
+}
+
+
+static void parse_command_line (int argc, char* argv[], option *const options)
+{
+  char* float_text[num_float_options];
+  unsigned int float_spec[num_float_options];
+  char* enum_text[num_enum_options];
+  unsigned int enum_spec[num_enum_options];
+  char* bool_text[num_bool_options];
+  unsigned int bool_spec[num_bool_options];
+  char * include_opt;
+  unsigned int include_spec;
+  int i,j;
+  optStruct3 opt;
+  unsigned int option_def_index;
+  optEntry* option_def;
+
+  /* Let shhopt try its best */
+
+  option_def_index = 0;
+  MALLOCARRAY_SAFE(option_def,
+    (2*num_float_options + num_enum_options + num_bool_options + 3));
+  for (i=0; i<num_float_options; i++)
+    for (j=1; j<3; j++)
+      if (float_option_name[i][j])
+        OPTENT3(0, float_option_name[i][j], OPT_STRING,
+                &(float_text[i]), &(float_spec[i]), 0);
+  for (i=0; i<num_enum_options; i++)
+    OPTENT3(0, enum_option_name[i], OPT_STRING,
+            &(enum_text[i]), &(enum_spec[i]), 0);
+  for (i=0; i<num_bool_options; i++)
+    OPTENT3(0, bool_option_name[i], OPT_STRING,
+            &(bool_text[i]), &(bool_spec[i]), 0);
+  OPTENT3(0, "width", OPT_INT, &(options->width), &(options->width_spec), 0);
+  OPTENT3(0, "height", OPT_INT, &(options->height), &(options->height_spec),
+          0);
+  OPTENT3(0, "include", OPT_STRING, &include_opt, &include_spec, 0);
+  opt.opt_table = option_def;
+  opt.short_allowed = FALSE;
+  opt.allowNegNum = TRUE;
+  optParseOptions3 (&argc, argv, opt, sizeof(opt), 0);
+
+  /* The non-option arguments are optionally all eight coordinates
+     and optionally the input filename
+  */
+
+  switch (argc-1) {
+  case 1:
+    options->infilename = argv[1];
+  case 0:
+    for (i=0; i<8; i++)
+      if (!float_spec[i])
+        pm_error ("The %s-coordinate was not specified",
+                  float_option_name[i][0]);
+    break;
+  case 9:
+    options->infilename = argv[9];
+  case 8:
+    for (i=0; i<8; i++) {
+      float_text[i] = argv[i+1];
+      float_spec[i] = 1;
+    };
+    break;
+  default: pm_error ("Wrong (number of) command line arguments");
+  };
+
+  if (include_spec)
+      parse_include_points(include_opt, &options->include_points);
+
+  /* Parse float options -- shhopt retrieved them as strings */
+
+  for (i=0; i<num_float_options; i++)
+    if (float_spec[i])
+      options->floats[i] = parse_float (float_text[i]);
+
+  /* Parse enum options -- shhopt retrieved them as strings */
+
+  for (i=0; i<num_enum_options; i++)
+    if (enum_spec[i])
+      options->enums[i] = parse_enum (enum_text[i],enum_option_type[i],
+                                      enum_option_name[i]);
+
+  /* Parse bool options -- shhopt retrieved them as strings */
+
+  for (i=0; i<num_bool_options; i++)
+    if (bool_spec[i])
+      options->bools[i] = (first_false_bool_token >
+                           parse_enum (bool_text[i], bool_token,
+                                       bool_option_name[i]));
+
+  /* Propagate values where neccessary */
+
+  if (float_spec[10])           /* --margin */
+    for (i=11; i<15; i++)       /* --top_margin through --right_margin */
+      if (!(float_spec[i])) {
+        options->floats[i] = options->floats[10];
+        float_spec[i]=1;
+      };
+  options->top_margin_spec = float_spec[11];
+  options->bottom_margin_spec = float_spec[12];
+  options->left_margin_spec = float_spec[13];
+  options->right_margin_spec = float_spec[14];
+
+  /* Clean up */
+
+  free(option_def);
+}
+
+
+
+static void free_option (option *const options)
+{
+  include_point* current;
+  include_point* dispose;
+
+  current = options->include_points;
+  while (current != NULL) {
+    dispose = current;
+    current = current->next;
+    free(dispose);
+  };
+}
+
+
+
+static void init_world (option *const options,
+                        const struct pam *const inpam, world_data *const world)
+{
+  /* constructs xi_ul,...,yi_lr
+
+     Internally we use a pixel coordinate system with pixel units
+
+     This also translates the --include points' coordinates
+     into the internal system
+  */
+
+  number mult_x, mult_y, add_after;
+  int add_before;
+  include_point* current_include;
+
+  switch (options->enums[0]) {  /* --input_system */
+  case lattice:
+    add_after = -0.5;
+    add_before = 0;
+    break;
+  case pixel_s:
+    add_after = 0.0;
+    add_before = -1;
+    break;
+  };
+  switch (options->enums[2]) {  /* --input_unit */
+  case image:
+    mult_x = (number)((inpam->width) + add_before);
+    mult_y = (number)((inpam->height) + add_before);
+    break;
+  case pixel_u:
+    mult_x = 1.0;
+    mult_y = 1.0;
+    break;
+  };
+
+  world->xi_ul = ((number) options->floats[0]) * mult_x + add_after;
+  world->yi_ul = ((number) options->floats[1]) * mult_y + add_after;
+  world->xi_ur = ((number) options->floats[2]) * mult_x + add_after;
+  world->yi_ur = ((number) options->floats[3]) * mult_y + add_after;
+  world->xi_ll = ((number) options->floats[4]) * mult_x + add_after;
+  world->yi_ll = ((number) options->floats[5]) * mult_y + add_after;
+  world->xi_lr = ((number) options->floats[6]) * mult_x + add_after;
+  world->yi_lr = ((number) options->floats[7]) * mult_y + add_after;
+
+  for (current_include = options->include_points; current_include != NULL;
+       current_include = current_include->next) {
+    current_include->xi = current_include->xi * mult_x + add_after;
+    current_include->yi = current_include->yi * mult_y + add_after;
+  };
+}
+
+
+
+static bool solve_3_linear_equations (number* x1, number* x2, number* x3,
+                                      number const a11, number const a12,
+                                      number const a13, number const b1,
+                                      number const a21, number const a22,
+                                      number const a23, number const b2,
+                                      number const a31, number const a32,
+                                      number const a33, number const b3)
+/*----------------------------------------------------------------------------
+  The three equations are
+    a11*x1 + a12*x2 + a13*x3 = b1
+    a21*x1 + a22*x2 + a23*x3 = b2
+    a31*x1 + a32*x2 + a33*x3 = b3
+  The return value is wether the system is solvable
+----------------------------------------------------------------------------*/
+{
+  number c11,c12,d1,c21,c22,d2,e,f;
+  int pivot;
+
+  /* We do Gaussian elimination.
+     Whenever we find the system to be unsolvable, we just return FALSE.
+     In this specific case it makes the code clearer.
+  */
+
+  if (fabs(a11)<fabs(a21))
+    if (fabs(a21)<fabs(a31))
+      pivot=3;
+    else
+      pivot=2;
+  else
+    if (fabs(a11)<fabs(a31))
+      pivot=3;
+    else
+      pivot=1;
+
+  switch (pivot) {
+  case 1:
+    if (fabs(a11)<eps) return FALSE;
+    c11 = a22-a12*a21/a11;
+    c12 = a23-a13*a21/a11;
+    d1 =   b2- b1*a21/a11;
+    c21 = a32-a12*a31/a11;
+    c22 = a33-a13*a31/a11;
+    d2 =   b3- b1*a31/a11;
+    break;
+  case 2:
+    if (fabs(a21)<eps) return FALSE;
+    c11 = a12-a22*a11/a21;
+    c12 = a13-a23*a11/a21;
+    d1 =   b1- b2*a11/a21;
+    c21 = a32-a22*a31/a21;
+    c22 = a33-a23*a31/a21;
+    d2 =   b3- b2*a31/a21;
+    break;
+  case 3:
+    if (fabs(a31)<eps) return FALSE;
+    c11 = a12-a32*a11/a31;
+    c12 = a13-a33*a11/a31;
+    d1 =   b1- b3*a11/a31;
+    c21 = a22-a32*a21/a31;
+    c22 = a23-a33*a21/a31;
+    d2 =   b2- b3*a21/a31;
+    break;
+  }
+
+  /* Now we have a subsystem:
+       c11*x2 + c12*x3 = d1
+       c21*x2 + c22*x3 = d2
+  */
+
+  if (fabs(c11)>fabs(c21)) {
+    if (fabs(c11)<eps) return FALSE;
+    e = c22-c12*c21/c11;
+    f =  d2- d1*c21/c11;
+    /* Now we have a single equation e*x3=f */
+    if (fabs(e)<eps) return FALSE;
+    *x3 = f/e;
+    *x2 = (d1-c12*(*x3))/c11;
+  }
+  else {
+    if (fabs(c21)<eps) return FALSE;
+    e = c12-c22*c11/c21;
+    f =  d1- d2*c11/c21;
+    /* Now we have a single equation e*x3=f */
+    if (fabs(e)<eps) return FALSE;
+    *x3 = f/e;
+    *x2 = (d2-c22*(*x3))/c21;
+  };
+
+  switch (pivot) {
+  case 1:
+    *x1 = (b1-a13*(*x3)-a12*(*x2))/a11;
+    break;
+  case 2:
+    *x1 = (b2-a23*(*x3)-a22*(*x2))/a21;
+    break;
+  case 3:
+    *x1 = (b3-a33*(*x3)-a32*(*x2))/a31;
+    break;
+  };
+
+  return TRUE;
+}
+
+
+static void determine_world_parallelogram (world_data *const world,
+                                           const option *const options)
+/*----------------------------------------------------------------------------
+  constructs xw_ul,...,zw_lr from xi_ul,...,yi_lr
+     
+  Actually this is a solution of a linear equation system.
+  
+  We first solve 4 variables (the 4 z-coordinates) against 4
+  equations: Each z-coordinate determines the corresponding x- and
+  y-coordinates in a linear fashion, where the coefficients are taken
+  from the image coordinates. This corresponds to the fact that a
+  point of an image determines a line in the world.
+  
+  3 equations state that the 4 points form a parallelogram.  The 4th
+  equation is for normalization and states, that the centre of the
+  parallelogram has a z-coordinate of 1.
+-----------------------------------------------------------------------------*/
+{
+  number dx1,dx2,dx3,dx4,dx5, dy1,dy2,dy3,dy4,dy5;
+  number det;
+  number xw_ul,yw_ul,zw_ul, xw_ur,yw_ur,zw_ur,
+         xw_ll,yw_ll,zw_ll, xw_lr,yw_lr,zw_lr;
+  number top_margin, left_margin, right_margin, bottom_margin;
+  include_point* current_include;
+  number include_xo, include_yo, include_zw;
+  bool solvable, margin_spec;
+
+  dx1 = world->xi_lr - world->xi_ul;  /* d1 is the image diagonal ul -> lr */
+  dy1 = world->yi_lr - world->yi_ul;
+  dx2 = world->xi_ur - world->xi_ll;  /* d2 is the image diagonal ll -> ur */
+  dy2 = world->yi_ur - world->yi_ll;
+  dx3 = world->xi_ur - world->xi_ul;  /* d3 is the image side ul -> ur */
+  dy3 = world->yi_ur - world->yi_ul;
+  dx4 = world->xi_ul - world->xi_ll;  /* d4 is the image side ll -> ul */
+  dy4 = world->yi_ul - world->yi_ll;
+  dx5 = world->xi_ur - world->xi_lr;  /* d5 is the image side lr -> ur */
+  dy5 = world->yi_ur - world->yi_lr;
+
+  det = dx2*dy1 - dx1*dy2;
+
+  /* A determinant of 0 is really bad: It means that that diagonals in the
+     image are parallel (or of zero length)
+  */
+
+  if ((-eps<det) && (det<eps))
+    pm_error ("The specified vertices are degenerated.  "
+              "Maybe they were given in the wrong order?");
+
+  zw_ul = 2.0*(dx5*dy2-dx2*dy5)/det;
+  zw_ur = 2.0*(dx4*dy1-dx1*dy4)/det;
+  zw_ll = 2.0*(dx3*dy1-dx1*dy3)/det;
+  zw_lr = 2.0*(dx2*dy3-dx3*dy2)/det;
+
+  /* A zero or negative value for some z means that three of the points
+     lie on a line in the image or that the four points do not define
+     a convex shape.  We have to forbid this in order to prevent divisions
+     by zero later on
+  */
+
+  if ((zw_ul<eps) || (zw_ur<eps) || (zw_ll<eps) || (zw_lr<eps))
+    pm_error ("The specified vertices are degenerated.  "
+              "Maybe they were given in the wrong order?");
+
+  xw_ul = world->xi_ul * zw_ul;
+  yw_ul = world->yi_ul * zw_ul;
+  xw_ur = world->xi_ur * zw_ur;
+  yw_ur = world->yi_ur * zw_ur;
+  xw_ll = world->xi_ll * zw_ll;
+  yw_ll = world->yi_ll * zw_ll;
+  xw_lr = world->xi_lr * zw_lr;
+  yw_lr = world->yi_lr * zw_lr;
+
+  /* Now we introduce the margin. There are several ways the margin can be
+     defined. margin_spec keeps track of wether one of them has yet been
+     used. As long as margin_spec==FALSE, the variables top_margin to
+     bottom_margin are not initialized! */
+
+  if (options->bools[0]) {      /* --frame_include */
+    top_margin = 0.0;
+    left_margin = 0.0;
+    right_margin = 0.0;
+    bottom_margin = 0.0;
+    margin_spec = TRUE;
+  } else
+    margin_spec = FALSE;
+
+  for (current_include = options->include_points; current_include != NULL;
+       current_include = current_include->next) {
+    solvable = solve_3_linear_equations(&include_xo, &include_yo, &include_zw,
+      xw_ul-xw_ur, xw_ul-xw_ll, current_include->xi, xw_ul,
+      yw_ul-yw_ur, yw_ul-yw_ll, current_include->yi, yw_ul,
+      zw_ul-zw_ur, zw_ul-zw_ll, 1.0, zw_ul);
+    if (!solvable)
+      pm_error ("The --include point %s lies on the horizon.",
+                current_include->specification);
+    if (include_zw<0.0)
+      pm_error ("The --include point %s lies beyond the horizon.",
+                current_include->specification);
+    if (margin_spec) {
+      top_margin = MAX(top_margin, -include_yo);
+      left_margin = MAX(left_margin, -include_xo);
+      right_margin = MAX(right_margin, include_xo-1.0);
+      bottom_margin = MAX(bottom_margin, include_yo-1.0);
+    } else {
+      top_margin = -include_yo;
+      left_margin = -include_xo;
+      right_margin = include_xo-1.0;
+      bottom_margin = include_yo-1.0;
+      margin_spec = TRUE;
+    };
+  }
+
+  if (margin_spec) {    /* the margin is there. --top_margin and such can
+                           still enlarge it */
+    if (options->top_margin_spec)
+      top_margin = MAX(top_margin, options->floats[11]);
+    if (options->left_margin_spec)
+      left_margin = MAX(left_margin, options->floats[13]);
+    if (options->right_margin_spec)
+      right_margin = MAX(right_margin, options->floats[14]);
+    if (options->bottom_margin_spec)
+      bottom_margin = MAX(bottom_margin, options->floats[12]);
+  } else                /* the margin is not yet there. --top_margin and
+                           such can remedy this only if all of them are
+                           given */
+    if ((options->top_margin_spec) && (options->left_margin_spec) &&
+        (options->right_margin_spec) && (options->bottom_margin_spec)) {
+      top_margin = options->floats[11];
+      left_margin = options->floats[13];
+      right_margin = options->floats[14];
+      bottom_margin = options->floats[12];
+    } else              /* the margin finally is not there */
+      pm_error ("No frame specified. "
+                "Use --frame_include=yes or --include or --margin.");
+
+  world->xw_ul = xw_ul
+    - top_margin * (xw_ll-xw_ul)
+    - left_margin * (xw_ur-xw_ul);
+  world->yw_ul = yw_ul
+    - top_margin * (yw_ll-yw_ul)
+    - left_margin * (yw_ur-yw_ul);
+  world->zw_ul = zw_ul
+    - top_margin * (zw_ll-zw_ul)
+    - left_margin * (zw_ur-zw_ul);
+  world->xw_ur = xw_ur
+    - top_margin * (xw_lr-xw_ur)
+    - right_margin * (xw_ul-xw_ur);
+  world->yw_ur = yw_ur
+    - top_margin * (yw_lr-yw_ur)
+    - right_margin * (yw_ul-yw_ur);
+  world->zw_ur = zw_ur
+    - top_margin * (zw_lr-zw_ur)
+    - right_margin * (zw_ul-zw_ur);
+  world->xw_ll = xw_ll
+    - bottom_margin * (xw_ul-xw_ll)
+    - left_margin * (xw_lr-xw_ll);
+  world->yw_ll = yw_ll
+    - bottom_margin * (yw_ul-yw_ll)
+    - left_margin * (yw_lr-yw_ll);
+  world->zw_ll = zw_ll
+    - bottom_margin * (zw_ul-zw_ll)
+    - left_margin * (zw_lr-zw_ll);
+  world->xw_lr = xw_lr
+    - bottom_margin * (xw_ur-xw_lr)
+    - right_margin * (xw_ll-xw_lr);
+  world->yw_lr = yw_lr
+    - bottom_margin * (yw_ur-yw_lr)
+    - right_margin * (yw_ll-yw_lr);
+  world->zw_lr = zw_lr
+    - bottom_margin * (zw_ur-zw_lr)
+    - right_margin * (zw_ll-zw_lr);
+
+  /* Again we have to forbid nonpositive z */
+
+  if ((world->zw_ul<eps) || (world->zw_ur<eps) ||
+       (world->zw_ll<eps) || (world->zw_lr<eps))
+    pm_error ("The specified margin is too large.");
+
+}
+
+
+
+static int diff (int const a, int const b)
+{
+  return MAX (b-a, a-b);
+}
+
+
+
+static number norm_vector (number const x1, number const y1, number const z1,
+                           number const x2, number const y2, number const z2)
+/*----------------------------------------------------------------------------
+  Two 3D vertices p1 and p2 are given by their coordinates.
+  A linear movement from p1 to p2, parameterized by the interval [0,1],
+  is projected to the input image. The function returns the norm of
+  the derivative of this overall movement at time 0, that is at p1.
+  The norm uses the max metric.
+-----------------------------------------------------------------------------*/
+{
+  number dx,dy;
+
+  dx = (x2-x1)/z1 - (z2-z1)*x1/(z1*z1);
+  dy = (y2-y1)/z1 - (z2-z1)*y1/(z1*z1);
+
+  return MAX (fabs(dx), fabs(dy));
+}
+
+
+
+static number norm_side (number const x1, number const y1, number const z1,
+                         number const x2, number const y2, number const z2)
+/*----------------------------------------------------------------------------
+  This is similar to norm_vector. But now the norm of the derivative
+  is computed at both endpoints of the movement and the maximum is
+  returned.
+  
+  Why do we do this? The return value n is in fact the maximum of the
+  norm of the derivative ALONG the movement. So we know that if we
+  divide the movement into at least n steps, we will encounter every
+  x- and every y-coordinate of the input image between the two points.
+  This is our notion of losslessness with --detail=1.
+-----------------------------------------------------------------------------*/
+{
+  return MAX (norm_vector(x1,y1,z1,x2,y2,z2),
+              norm_vector(x2,y2,z2,x1,y1,z1));
+}
+
+
+
+static void determine_output_width_and_height (const world_data *const world,
+                                               option *const options)
+{
+  number du,dv;
+  int xsteps,ysteps,width,height;
+
+  /* Determine the number of steps for losslessness */
+
+  du = MAX (norm_side(world->xw_ul, world->yw_ul, world->zw_ul,
+                      world->xw_ur, world->yw_ur, world->zw_ur),
+            norm_side(world->xw_ll, world->yw_ll, world->zw_ll,
+                      world->xw_lr, world->yw_lr, world->zw_lr));
+  dv = MAX (norm_side(world->xw_ul, world->yw_ul, world->zw_ul,
+                      world->xw_ll, world->yw_ll, world->zw_ll),
+            norm_side(world->xw_ur, world->yw_ur, world->zw_ur,
+                      world->xw_lr, world->yw_lr, world->zw_lr));
+  xsteps = ceil(du*options->floats[8]);  /* option->floats[8] is --detail */
+  ysteps = ceil(dv*options->floats[8]);
+
+  /* Turn the numbers of steps into width and height */
+
+  switch (options->enums[1]) {  /* --output_system */
+  case lattice:
+    width = xsteps;
+    height = ysteps;
+    break;
+  case pixel_s:
+    width = xsteps+1;
+    height = ysteps+1;
+    break;
+  };
+
+  /* Correct the proportion of width and height by increasing one of them */
+
+  switch (options->enums[4]) {  /* --proportion */
+  case free_:   /* no correction at all */
+    break;
+  case fixed:   /* correction now */
+    /* options->floats[9] is --ratio */
+    width = MAX (floor(0.5 + ((number)height) * options->floats[9]),
+                 width);
+    height = MAX (floor(0.5 + ((number)width) / options->floats[9]),
+                  height);
+    break;
+  };
+
+  /* Override anything we have by the specified width and height */
+
+  if (!(options->width_spec))
+    options->width=width;
+  if (!(options->height_spec))
+    options->height=height;
+}
+
+
+
+static void determine_coefficients_lattice (world_data *const world,
+                                            const option *const options)
+/*----------------------------------------------------------------------------
+  Constructs ax,...,cz from xw_ul,...,zw_lr
+     
+  The calculations assume lattice coordinates, that is the point ul
+  corresponds to the upper left corner of the pixel (0,0) and the
+  point lr corresponds to the lower left corner of the pixel
+  (width-1,height-1)
+-----------------------------------------------------------------------------*/
+{
+  number width,height;
+
+  width = (number) options->width;
+  height = (number) options->height;
+
+  world->bx = (world->xw_ur - world->xw_ul)/width;
+  world->cx = (world->xw_ll - world->xw_ul)/height;
+  world->by = (world->yw_ur - world->yw_ul)/width;
+  world->cy = (world->yw_ll - world->yw_ul)/height;
+  world->bz = (world->zw_ur - world->zw_ul)/width;
+  world->cz = (world->zw_ll - world->zw_ul)/height;
+
+  world->ax = world->xw_ul + world->bx/2.0 + world->cx/2.0;
+  world->ay = world->yw_ul + world->by/2.0 + world->cy/2.0;
+  world->az = world->zw_ul + world->bz/2.0 + world->cz/2.0;
+}
+
+
+
+static void determine_coefficients_pixel (world_data *const world,
+                                          const option *const options)
+/*----------------------------------------------------------------------------
+  Constructs ax,...,cz from xw_ul,...,zw_lr
+     
+  The calculations assume pixel coordinates, that is the point ul
+  corresponds to the centre of the pixel (0,0) and the point lr
+  corresponds to the centre of the pixel (width-1,height-1)
+-----------------------------------------------------------------------------*/
+{
+  number width,height;
+
+  if (options->width == 1)
+    pm_error ("You specified 'pixel' as output coordinate model "
+              "and a width of 1.  These things don't mix.");
+  if (options->height == 1)
+    pm_error ("You specified 'pixel' as output coordinate model "
+              "and a height of 1.  These things don't mix.");
+
+  width = (number) (options->width-1);
+  height = (number) (options->height-1);
+
+  world->bx = (world->xw_ur - world->xw_ul)/width;
+  world->cx = (world->xw_ll - world->xw_ul)/height;
+  world->by = (world->yw_ur - world->yw_ul)/width;
+  world->cy = (world->yw_ll - world->yw_ul)/height;
+  world->bz = (world->zw_ur - world->zw_ul)/width;
+  world->cz = (world->zw_ll - world->zw_ul)/height;
+
+  world->ax = world->xw_ul;
+  world->ay = world->yw_ul;
+  world->az = world->zw_ul;
+}
+
+
+
+static void outpixel_to_inpixel (int const xo, int const yo, 
+                                 number* const xi, number* const yi,
+                                 const world_data *const world)
+{
+  number xof,yof,xw,yw,zw;
+
+  xof = (number) xo;
+  yof = (number) yo;
+  xw = world->ax + world->bx*xof + world->cx*yof;
+  yw = world->ay + world->by*xof + world->cy*yof;
+  zw = world->az + world->bz*xof + world->cz*yof;
+  *xi = xw/zw;
+  *yi = yw/zw;
+}
+
+static int outpixel_to_iny (int xo, int yo, const world_data *const world)
+{
+  number xi,yi;
+
+  outpixel_to_inpixel (xo,yo,&xi,&yi,world);
+
+  return (int) yi;
+}
+
+static int clean_y (int const y,  const struct pam *const outpam)
+{
+  return MIN(MAX(0, y), outpam->height-1);
+}
+
+static void init_buffer (buffer *const b, const world_data *const world,
+                         const option *const options,
+                         const struct pam *const inpam,
+                         const struct pam *const outpam)
+{
+  int yul, yur, yll, ylr, y_min;
+  int i, num_rows;
+
+  yul = outpixel_to_iny (0,0,world);
+  yur = outpixel_to_iny (outpam->width-1,0,world);
+  yll = outpixel_to_iny (0,outpam->height-1,world);
+  ylr = outpixel_to_iny (outpam->width-1,outpam->height-1,world);
+
+  y_min = MIN (MIN (yul,yur), MIN (yll,ylr));
+  num_rows = MAX (MAX (diff (yul, yur),
+                       diff (yll, ylr)),
+                  MAX (diff (clean_y(yul,outpam), clean_y(y_min,outpam)),
+                       diff (clean_y(yur,outpam), clean_y(y_min,outpam))))
+    + 2;
+  switch (options->enums[3]) {  /* --interpolation */
+  case nearest:
+    break;
+  case linear:
+    num_rows += 1;
+    break;
+  };
+  if (num_rows > inpam->height)
+    num_rows = inpam->height;
+
+  b->num_rows = num_rows;
+  MALLOCARRAY_SAFE (b->rows, num_rows);
+  for (i=0; i<num_rows; i++) {
+    b->rows[i] = pnm_allocpamrow (inpam);
+    pnm_readpamrow (inpam, b->rows[i]);
+  };
+  b->last_physical = num_rows-1;
+  b->last_logical = num_rows-1;
+  b->inpam = inpam;
+}
+
+static tuple* read_buffer (buffer *const b, int const logical_y)
+{
+  int y;
+
+  while (logical_y > b->last_logical) {
+    b->last_physical++;
+    if (b->last_physical == b->num_rows)
+      b->last_physical = 0;
+    pnm_readpamrow (b->inpam, b->rows[b->last_physical]);
+    b->last_logical++;
+  }
+
+  y = logical_y - b->last_logical + b->last_physical;
+  if (y<0)
+    y += b->num_rows;
+
+  return b->rows[y];
+}
+
+static void free_buffer (buffer *const b)
+{
+  int i;
+
+  for (i=0; i<b->num_rows; i++)
+    pnm_freepamrow (b->rows[i]);
+  free (b->rows);
+}
+
+
+
+
+/* The following variables are global for speed reasons.
+   In this way they do not have to be passed to each call of the
+   interpolation functions
+
+   Think of this as Sch&ouml;nfinkeling (aka Currying).
+*/
+
+static tuple background;
+static buffer* indata;
+static int width,height,depth;
+
+static void init_interpolation_global_vars (buffer* const inbuffer,
+                                            const struct pam *const inpam,
+                                            const struct pam *const outpam)
+{
+  pnm_createBlackTuple (outpam, &background);
+  indata = inbuffer;
+  width = inpam->width;
+  height = inpam->height;
+  depth = outpam->depth;
+}
+
+
+
+static void clean_interpolation_global_vars (void)
+{
+  free (background);
+}
+
+
+
+/* These functions perform the interpolation */
+
+static tuple attempt_read (int const x, int const y)
+{
+  if ((x<0) || (x>=width) || (y<0) || (y>=height))
+    return background;
+  else
+    return read_buffer(indata, y)[x];
+}
+
+
+
+static void take_nearest (tuple const dest, number const x, number const y)
+{
+  int xx,yy,entry;
+  tuple p;
+
+  xx = (int)floor(x+0.5);
+  yy = (int)floor(y+0.5);
+  p = attempt_read (xx, yy);
+  for (entry=0; entry<depth; entry++) {
+    dest[entry]=p[entry];
+  }
+}
+
+
+
+static void linear_interpolation (tuple const dest, 
+                                  number const x, number const y)
+{
+  int xx,yy,entry;
+  number xf,yf,a,b,c,d;
+  tuple p1,p2,p3,p4;
+
+  xx = (int)floor(x);
+  yy = (int)floor(y);
+  xf = x-(number)xx;
+  yf = y-(number)yy;
+  p1 = attempt_read (xx, yy);
+  p2 = attempt_read (xx+1, yy);
+  p3 = attempt_read (xx, yy+1);
+  p4 = attempt_read (xx+1, yy+1);
+  a = (1.0-xf)*(1.0-yf);
+  b = xf*(1.0-yf);
+  c = (1.0-xf)*yf;
+  d = xf*yf;
+  for (entry=0; entry<depth; entry++) {
+    dest[entry]=(sample) floor(
+      a*((number) p1[entry]) +
+      b*((number) p2[entry]) +
+      c*((number) p3[entry]) +
+      d*((number) p4[entry]) +
+      0.5);
+  }
+}
+
+
+
+int main (int argc, char* argv[])
+{
+  FILE* infp;
+  struct pam inpam;
+  buffer inbuffer;
+  FILE* outfp;
+  struct pam outpam;
+  tuple* outrow;
+  option options;
+  world_data world;
+  int row,col;
+  number xi,yi;
+  void (*interpolate) (tuple, number, number);
+
+  /* The usual initializations */
+
+  pnm_init (&argc, argv);
+  set_command_line_defaults (&options);
+  parse_command_line (argc, argv, &options);
+  infp = pm_openr (options.infilename);
+  pnm_readpaminit (infp, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+  /* Our own initializations */
+
+  init_world (&options, &inpam, &world);
+  determine_world_parallelogram (&world, &options);
+  determine_output_width_and_height (&world, &options);
+  switch (options.enums[1]) {   /* --output_system */
+  case lattice:
+    determine_coefficients_lattice (&world, &options);
+    break;
+  case pixel_s:
+    determine_coefficients_pixel (&world, &options);
+    break;
+  };
+
+  /* Initialize outpam */
+
+  outfp = pm_openw ("-");
+  outpam.size = sizeof (outpam);
+  outpam.len = PAM_STRUCT_SIZE(bytes_per_sample);
+  outpam.file = outfp;
+  outpam.format = inpam.format;
+  outpam.plainformat = inpam.plainformat;
+  outpam.height = options.height;
+  outpam.width = options.width;
+  outpam.depth = inpam.depth;
+  outpam.maxval = inpam.maxval;
+  outpam.bytes_per_sample = inpam.bytes_per_sample;
+  pnm_writepaminit (&outpam);
+
+  /* Initialize the actual calculation */
+
+  init_buffer (&inbuffer, &world, &options, &inpam, &outpam);
+  outrow = pnm_allocpamrow (&outpam);
+  init_interpolation_global_vars (&inbuffer,&inpam,&outpam);
+  switch (options.enums[3]) {   /* --interpolation */
+  case nearest:
+    interpolate = take_nearest;
+    break;
+  case linear:
+    interpolate = linear_interpolation;
+    break;
+  };
+
+  /* Perform the actual calculation */
+
+  for (row=0; row<outpam.height; row++) {
+    for (col=0; col<outpam.width; col++) {
+      outpixel_to_inpixel (col,row,&xi,&yi,&world);
+      interpolate(outrow[col],xi,yi);
+    }
+    pnm_writepamrow (&outpam, outrow);
+  }
+
+  /* Close everything down nicely */
+
+  clean_interpolation_global_vars ();
+  free_buffer (&inbuffer);
+  pnm_freepamrow (outrow);
+  free_option (&options);
+  pm_close (infp);
+  pm_close (outfp);
+  return 0;
+}
+
+
+
+
diff --git a/editor/pampop9.c b/editor/pampop9.c
new file mode 100644
index 00000000..d6c61e4f
--- /dev/null
+++ b/editor/pampop9.c
@@ -0,0 +1,108 @@
+/* 
+ *
+ * (c) Robert Tinsley, 2003 (http://www.thepoacher.net/contact)
+ *
+ * Released under the GPL (http://www.fsf.org/licenses/gpl.txt)
+ *
+ *
+ * CHANGES
+ *
+ * v1.00 2003-02-28 Original version
+ *
+ * v1.10 2003-03-02
+ *  + changed to use pam_* routines rather than ppm_*
+ *  + changed to use pm_* rather than fopen()/fclose()
+ *  + renamed from ppmgrid to pampup9
+ *  + wrote a man-page (actually, html)
+ *
+ * Changes by Bryan Henderson for inclusion in the Netpbm package (fully
+ * exploiting Netpbm library).  Renamed to Pampop9.  March 2003.
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h> /* atoi() */
+
+#include "pam.h"
+
+static const char * const copyright = 
+  "(c) Robert Tinsley 2003 (http://www.thepoacher.net/contact)";
+
+static const char *usagestr = "pnmfile|- xtiles ytiles xdelta ydelta";
+
+int main(int argc, char *argv[])
+{
+    const char *filename = "-";
+    int xtiles, ytiles, xdelta, ydelta;
+    FILE *fp;
+
+    struct pam spam, dpam;
+    tuple **spix, *dpix;
+    int xtilesize, ytilesize, sx, sy, dx, dy, p;
+
+
+
+    pnm_init(&argc, argv);
+
+    if (argc-1 != 5) {
+        pm_error("Wrong number of arguments.  Program requires 5 arguments; "
+                 "you supplied %d.  Usage: %s", argc-1, usagestr);
+    }
+
+    filename = argv[1];
+    xtiles = atoi(argv[2]);
+    ytiles = atoi(argv[3]);
+    xdelta = atoi(argv[4]);
+    ydelta = atoi(argv[5]);
+
+    if (filename == NULL || *filename == '\0'
+        || xtiles <= 0 || ytiles <= 0 || xdelta < 0 || ydelta < 0) 
+        pm_error("invalid argument");
+
+    /* read src pam */
+
+    fp = pm_openr(filename);
+
+    spix = pnm_readpam(fp, &spam, PAM_STRUCT_SIZE(tuple_type));
+
+    pm_close(fp);
+
+    /* init dst pam */
+
+    xtilesize = spam.width  - (xtiles - 1) * xdelta;
+    ytilesize = spam.height - (ytiles - 1) * ydelta;
+
+    if (xtilesize <= 0)
+        pm_error("xtilesize must be positive.  You specified %d", xtilesize);
+    if (ytilesize <= 0)
+        pm_error("ytilesize must be positive.  You specified %d", ytilesize);
+
+    dpam = spam;
+    dpam.file = stdout;
+    dpam.width  = xtiles * xtilesize;
+    dpam.height = ytiles * ytilesize;
+
+    dpix = pnm_allocpamrow(&dpam);
+
+    pnm_writepaminit(&dpam);
+
+    /* generate dst pam */
+
+    for (dy = 0; dy < dpam.height; dy++) {
+        sy = ((int) (dy / ytilesize)) * ydelta + (dy % ytilesize);
+        for (dx = 0; dx < dpam.width; dx++) {
+            sx = ((int) (dx / xtilesize)) * xdelta + (dx % xtilesize);
+                for (p = 0; p < spam.depth; ++p) {
+                    dpix[dx][p] = spix[sy][sx][p];
+            }
+        }
+        pnm_writepamrow(&dpam, dpix);
+    }
+
+    /* all done */
+
+    pnm_freepamarray(spix, &spam);
+    pnm_freepamrow(dpix);
+
+    exit(EXIT_SUCCESS);
+}
diff --git a/editor/pamscale.c b/editor/pamscale.c
new file mode 100644
index 00000000..229fce42
--- /dev/null
+++ b/editor/pamscale.c
@@ -0,0 +1,2149 @@
+/* pamscale.c - rescale (resample) a PNM image
+
+   This program evolved out of Jef Poskanzer's program Pnmscale from
+   his Pbmplus package (which was derived from Poskanzer's 1989
+   Ppmscale).  The resampling logic was taken from Michael Reinelt's
+   program Pnmresample, somewhat recoded to follow Netpbm conventions.
+   Michael submitted that for inclusion in Netpbm in December 2003.
+   The frame of the program is by Bryan Henderson, and the old scaling
+   algorithm is based on that in Jef Poskanzer's Pnmscale, but
+   completely rewritten by Bryan Henderson ca. 2000.  Plenty of other
+   people contributed code changes over the years.
+
+   Copyright (C) 2003 by Michael Reinelt <reinelt@eunet.at>
+  
+   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   /* get M_PI in math.h */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <assert.h>
+
+#include "pam.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+
+/****************************/
+/****************************/
+/********* filters **********/
+/****************************/
+/****************************/
+
+/* Most of the filters are FIR (finite impulse respone), but some
+** (sinc, bessel) are IIR (infinite impulse respone).
+** They should be windowed with hanning, hamming, blackman or
+** kaiser window.
+** For sinc and bessel the blackman window will be used per default.
+*/
+
+#define EPSILON 1e-7
+
+
+/* x^2 and x^3 helper functions */
+static __inline__ double 
+pow2 (double x)
+{
+  return x*x;
+}
+
+static __inline__ double 
+pow3 (double x) 
+{
+  return x*x*x;
+}
+
+
+/* box, pulse, Fourier window, */
+/* box function also know as rectangle function */
+/* 1st order (constant) b-spline */
+
+#define radius_point (0.0)
+#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;
+}
+
+
+/* triangle, Bartlett window, */
+/* triangle function also known as lambda function */
+/* 2nd order (linear) b-spline */
+
+#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;
+}
+
+
+/* 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;
+}
+
+
+/* 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;
+}
+
+
+/* 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;
+}
+
+
+/* Mitchell & Netravali's two-param cubic */
+/* see Mitchell&Netravali,  */
+/* "Reconstruction Filters in Computer Graphics", SIGGRAPH 88 */
+
+#define radius_mitchell (2.0)
+
+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;
+}
+
+
+/* Gaussian filter (infinite) */
+
+#define radius_gauss (1.25)
+
+static double 
+filter_gauss(double 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)
+{
+    /* 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);
+}
+
+
+/* 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);
+}
+
+
+/* Hanning window (infinite) */
+
+#define radius_hanning (1.0)
+
+static double 
+filter_hanning(double 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;
+}
+
+
+/* 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;
+}
+
+
+/* parameterized Kaiser window (infinite) */
+/* from Oppenheim & Schafer, Hamming */
+
+#define radius_kaiser (1.0)
+
+/* modified zeroth order Bessel function of the first kind. */
+static double 
+bessel_i0(double x)
+{
+  
+    int i;
+    double sum, y, t;
+  
+    sum = 1.0;
+    y = pow2(x)/4.0;
+    t = y;
+    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 */
+    /* param a trades off main lobe width (sharpness) */
+    /* for side lobe amplitude (ringing) */
+  
+    double a   = 6.5;
+    double i0a = 1.0/bessel_i0(a);
+  
+    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)
+{
+    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)
+{
+    /* 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;
+}
+
+
+/* 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);
+}
+
+
+
+typedef struct {
+    const char *name;
+    double (*function)(double);
+    double radius;
+        /* This is how far from the Y axis (on either side) the
+           function has significant value.  (You can use this to limit
+           how much of your domain you bother to compute the function
+           over).  
+        */
+    bool windowed;
+} filter;
+
+
+static filter Filters[] = {
+    { "point",     filter_box,       radius_point,     FALSE },
+    { "box",       filter_box,       radius_box,       FALSE },
+    { "triangle",  filter_triangle,  radius_triangle,  FALSE },
+    { "quadratic", filter_quadratic, radius_quadratic, FALSE },
+    { "cubic",     filter_cubic,     radius_cubic,     FALSE },
+    { "catrom",    filter_catrom,    radius_catrom,    FALSE },
+    { "mitchell",  filter_mitchell,  radius_mitchell,  FALSE },
+    { "gauss",     filter_gauss,     radius_gauss,     FALSE },
+    { "sinc",      filter_sinc,      radius_sinc,      TRUE  },
+    { "bessel",    filter_bessel,    radius_bessel,    TRUE  },
+    { "hanning",   filter_hanning,   radius_hanning,   FALSE },
+    { "hamming",   filter_hamming,   radius_hamming,   FALSE },
+    { "blackman",  filter_blackman,  radius_blackman,  FALSE },
+    { "kaiser",    filter_kaiser,    radius_kaiser,    FALSE },
+    { "normal",    filter_normal,    radius_normal,    FALSE },
+    { "hermite",   filter_hermite,   radius_hermite,   FALSE },
+    { "lanczos",   filter_lanczos,   radius_lanczos,   FALSE },
+   { NULL },
+};
+
+
+typedef double (*basicFunction_t)(double);
+
+
+/****************************/
+/****************************/
+/****** end of filters ******/
+/****************************/
+/****************************/
+
+
+
+enum scaleType {SCALE_SEPARATE, SCALE_BOXFIT, SCALE_BOXFILL, SCALE_PIXELMAX};
+    /* This is a way of specifying the output dimensions.
+
+       SCALE_SEPARATE means specify the horizontal and vertical scaling
+       separately.  One or both may be unspecified.
+
+       SCALE_BOXFIT means specify height and width of a box, and the
+       image must be scaled, preserving aspect ratio, to the largest
+       size that will fit in the box.  Some of the box may be empty.
+
+       SCALE_BOXFILL means specify height and width of a box, and the
+       image must be scaled, preserving aspect ratio, to the smallest
+       size that completely fills the box.  Some of the image may be
+       outside the box.
+
+       SCALE_PIXELMAX means specify the maximum number of pixels the result
+       should have and scale preserving aspect ratio and maximizing image
+       size.
+    */
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+     * in a form easy for the program to use.
+     */
+    const char * inputFileName;  /* Filespec of input file */
+    unsigned int nomix;
+    basicFunction_t filterFunction; /* NULL if not using resample method */
+    basicFunction_t windowFunction;
+        /* Meaningful only when filterFunction != NULL */
+    double filterRadius;           
+        /* Meaningful only when filterFunction != NULL */
+    enum scaleType scaleType;
+    /* 'xsize' and 'ysize' are numbers of pixels.  Their meaning depends upon
+       'scaleType'.  for SCALE_BOXFIT and SCALE_BOXFILL, they are the box 
+       dimensions.  For SCALE_SEPARATE, they are the separate dimensions, or
+       zero to indicate unspecified.  For SCALE_PIXELMAX, they are
+       meaningless.
+    */
+    unsigned int xsize;
+    unsigned int ysize;
+    /* 'xscale' and 'yscale' are meaningful only for scaleType == 
+       SCALE_SEPARATE and only where the corresponding xsize/ysize is
+       unspecified.  0.0 means unspecified.
+    */
+    float xscale;
+    float yscale;
+    /* 'pixels' is meaningful only for scaleType == SCALE_PIXELMAX */
+    unsigned int pixels; 
+    unsigned int linear;
+    unsigned int verbose;
+};
+
+
+
+static void
+lookupFilterByName(const char * const filtername,
+                   filter *     const filterP) {
+
+    unsigned int i;
+    bool found;
+
+    found = FALSE;  /* initial assumption */
+
+    for (i=0; Filters[i].name; ++i) {
+        if (strcmp(filtername, Filters[i].name) == 0) {
+            *filterP = Filters[i];
+            found = TRUE;
+        }
+    }
+    if (!found) {
+        unsigned int i;
+        char known_filters[1024];
+        strcpy(known_filters, "");
+        for (i = 0; Filters[i].name; ++i) {
+            const char * const name = Filters[i].name;
+            if (strlen(known_filters) + strlen(name) + 1 + 1 < 
+                sizeof(known_filters)) {
+                strcat(known_filters, name);
+                strcat(known_filters, " ");
+            }
+        }
+        pm_error("No such filter as '%s'.  Known filter names are: %s",
+                 filtername, known_filters);
+    }
+}
+
+
+
+static void
+processFilterOptions(unsigned int const         filterSpec,
+                     const char                 filterOpt[],
+                     unsigned int const         windowSpec,
+                     const char                 windowOpt[],
+                     struct cmdlineInfo * const cmdlineP) {
+
+    if (filterSpec) {
+        filter baseFilter;
+        lookupFilterByName(filterOpt, &baseFilter);
+        cmdlineP->filterFunction = baseFilter.function; 
+        cmdlineP->filterRadius   = baseFilter.radius;
+
+        if (windowSpec) {
+            filter windowFilter;
+            lookupFilterByName(windowOpt, &windowFilter);
+            
+            if (cmdlineP->windowFunction == filter_box)
+                cmdlineP->windowFunction = NULL;
+            else
+                cmdlineP->windowFunction = windowFilter.function;
+        } else {
+            /* Default for most filters is no window.  Those that _require_
+               a window function get Blackman.
+               */
+            if (baseFilter.windowed)
+                cmdlineP->windowFunction = filter_blackman;
+            else
+                cmdlineP->windowFunction = NULL;
+        }
+    } else
+        cmdlineP->filterFunction = NULL;
+}
+
+
+
+static void
+parseXyParms(int                  const argc, 
+             char **              const argv,
+             struct cmdlineInfo * const cmdlineP) {
+
+    /* parameters are box width (columns), box height (rows), and
+       optional filespec 
+    */
+    if (argc-1 < 2)
+        pm_error("You must supply at least two parameters with "
+                 "-xyfit/xyfill/xysize: "
+                 "x and y dimensions of the bounding box.");
+    else if (argc-1 > 3)
+        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);
+        
+        if (argc-1 < 3)
+            cmdlineP->inputFileName = "-";
+        else
+            cmdlineP->inputFileName = argv[3];
+    }
+}
+
+
+
+static void
+parseScaleParms(int                   const argc, 
+                char **               const argv,
+                struct cmdlineInfo  * const cmdlineP) {
+
+    /* parameters are scale factor and optional filespec */
+    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)
+            pm_error("The scale parameter %s is not a positive number.",
+                     argv[1]);
+        else {
+            if (argc-1 < 2)
+                cmdlineP->inputFileName = "-";
+            else
+                cmdlineP->inputFileName = argv[2];
+        }
+    }
+}
+
+
+
+static void
+parseFilespecOnlyParms(int                   const argc, 
+                       char **               const argv,
+                       struct cmdlineInfo  * const cmdlineP) {
+
+    /* Only parameter allowed is optional filespec */
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else
+        cmdlineP->inputFileName = argv[1];
+}
+
+
+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;
+    unsigned int xyfit, xyfill;
+    int xsize, ysize, pixels;
+    int reduce;
+    float xscale, yscale;
+    const char *filterOpt, *window;
+    unsigned int filterSpec, windowSpec;
+
+    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, "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, "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);
+  
+    /* 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 );
+    /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (cmdlineP->nomix && filterSpec) 
+        pm_error("You cannot specify both -nomix and -filter.");
+
+    processFilterOptions(filterSpec, filterOpt, windowSpec, window,
+                         cmdlineP);
+
+    if (xsize == 0)
+        pm_error("-xsize/width must be greater than zero.");
+    if (ysize == 0)
+        pm_error("-ysize/height must be greater than zero.");
+    if (xscale != -1.0 && xscale <= 0.0)
+        pm_error("-xscale must be greater than zero.");
+    if (yscale != -1.0 && yscale <= 0.0)
+        pm_error("-yscale must be greater than zero.");
+    if (reduce <= 0 && reduce != -1)
+        pm_error("-reduce must be greater than zero.");
+
+    if (xsize != -1 && xscale != -1)
+        pm_error("Cannot specify both -xsize/width and -xscale.");
+    if (ysize != -1 && yscale != -1)
+        pm_error("Cannot specify both -ysize/height and -yscale.");
+    
+    if ((xyfit || xyfill) &&
+        (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 || 
+         reduce != -1 || pixels != -1) )
+        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) )
+        pm_error("Cannot specify -pixels with other dimension options.");
+    if (reduce != -1 && 
+        (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1) )
+        pm_error("Cannot specify -reduce with other dimension options.");
+
+    if (pixels == 0)
+        pm_error("-pixels must be greater than zero");
+
+    /* Get the program parameters */
+
+    if (xyfit || xyfill) {
+        cmdlineP->scaleType = xyfit ? SCALE_BOXFIT : SCALE_BOXFILL;
+        parseXyParms(argc, argv, cmdlineP);
+    } else if (reduce != -1) {
+        cmdlineP->scaleType = SCALE_SEPARATE;
+        parseFilespecOnlyParms(argc, argv, cmdlineP);
+        cmdlineP->xscale = cmdlineP->yscale = 
+            ((double) 1.0) / ((double) reduce);
+        pm_message("reducing by %d gives scale factor of %f.", 
+                   reduce, cmdlineP->xscale);
+    } else if (pixels != -1) {
+        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) {
+        cmdlineP->scaleType = SCALE_SEPARATE;
+        parseScaleParms(argc, argv, cmdlineP);
+        cmdlineP->xsize = cmdlineP->ysize = 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;
+    }
+}
+
+
+
+static void 
+computeOutputDimensions(struct cmdlineInfo  const cmdline, 
+                        int                 const rows, 
+                        int                 const cols, 
+                        int *               const newrowsP, 
+                        int *               const newcolsP) {
+
+    switch(cmdline.scaleType) {
+    case SCALE_PIXELMAX: {
+        if (rows * cols <= cmdline.pixels) {
+            *newrowsP = rows;
+            *newcolsP = cols;
+        } else {
+            const double scale =
+                sqrt( (float) cmdline.pixels / ((float) cols * (float) rows));
+            *newrowsP = rows * scale;
+            *newcolsP = cols * scale;
+        }
+    } break;
+    case SCALE_BOXFIT:
+    case SCALE_BOXFILL: {
+        double const aspect_ratio = (float) cols / (float) rows;
+        double const box_aspect_ratio = 
+            (float) cmdline.xsize / (float) cmdline.ysize;
+        
+        if ((box_aspect_ratio > aspect_ratio && 
+             cmdline.scaleType == SCALE_BOXFIT) ||
+            (box_aspect_ratio < aspect_ratio &&
+             cmdline.scaleType == SCALE_BOXFILL)) {
+            *newrowsP = cmdline.ysize;
+            *newcolsP = *newrowsP * aspect_ratio + 0.5;
+        } else {
+            *newcolsP = cmdline.xsize;
+            *newrowsP = *newcolsP / aspect_ratio + 0.5;
+        }
+    } break;
+    case SCALE_SEPARATE: {
+        if (cmdline.xsize)
+            *newcolsP = cmdline.xsize;
+        else if (cmdline.xscale)
+            *newcolsP = cmdline.xscale * cols + .5;
+        else if (cmdline.ysize)
+            *newcolsP = cols * ((float) cmdline.ysize/rows) +.5;
+        else
+            *newcolsP = cols;
+
+        if (cmdline.ysize)
+            *newrowsP = cmdline.ysize;
+        else if (cmdline.yscale)
+            *newrowsP = cmdline.yscale * rows +.5;
+        else if (cmdline.xsize)
+            *newrowsP = rows * ((float) cmdline.xsize/cols) +.5;
+        else
+            *newrowsP = 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;
+}
+
+
+
+
+/****************************/
+/****************************/
+/******* resampling *********/
+/****************************/
+/****************************/
+
+/* The resample code was inspired by Paul Heckbert's zoom program.
+** http://www.cs.cmu.edu/~ph/zoom
+*/
+
+struct filterFunction {
+/*----------------------------------------------------------------------------
+   A function to convolve with the samples.
+-----------------------------------------------------------------------------*/
+    basicFunction_t basicFunction;
+        /* The basic shape of the function.  Its horizontal scale is
+           designed to filter out frequencies greater than 1.
+        */
+    basicFunction_t windowFunction;
+        /* A function to multiply by basicFunction().  NULL if none. */
+    double windowScaler;
+        /* Factor by which to compress windowFunction() horizontally */
+    double horizontalScaler;
+        /* Factor by which to compress basicFunction() *
+           windowFunction horizontally.  Note that compressing
+           horizontally in the sample domain is equivalent to
+           expanding horizontally (and shrinking vertically) in the
+           frequency domain.  I.e. values less than unity have the
+           effect of chopping out high frequencies.
+        */
+    double radius;
+        /* A final filter.  filterFunction(x) is zero for |x| > radius
+           regardless of what the rest of the members say.
+
+           Implementation note:  This is important because windowFunction(),
+           out of laziness, doesn't do the whole job of windowing.  It is
+           not zero beyond the cutoff points as it should be.  If not for
+           that, radius would only be a hint to describe what the other
+           members already do, so the convolver knows where to stop.
+        */
+};
+
+typedef struct {
+    /* A term of the linear combination of input rows that makes up an
+       output row.  I.e. an input row and its weight.
+
+       Alternatively, the analogous thing for a column.
+    */
+    int    position;    /* Row/column number in the input image */
+    double weight;      /* Weight to be given to that row/col.  In [0, 1]. */
+} WEIGHT;
+
+typedef struct {
+    /* A description of the linear combination of input rows that
+       generates a particular output row.  An output row is a weighted
+       average of some input rows.  E.g. Row 2 of the output might be
+       composed of 50% of Row 2 of the input and 50% of Row 3 of the
+       input.
+
+       Alternatively, the analogous thing for columns.
+    */
+    unsigned int nWeight;
+        /* Number of elements in 'Weight'.  They're consecutive, starting
+           at index 0.
+        */
+    unsigned int allocWeight;
+        /* Number of allocated frames in 'Weight' */ 
+    WEIGHT *Weight;
+        /* The terms of the linear combination.  Has 'nWeight' elements. 
+           The coefficients (weights) of each add up to unity.
+        */
+} WLIST;
+
+typedef struct {
+    /* This identifies a row of the input image. */
+    int rowNumber;
+        /* The row number in the input image of the row.
+           -1 means no row.
+        */
+    tuple *tuplerow;  
+        /* The tuples of the row.
+           If rowNumber = -1, these are arbitrary, but allocated, tuples.
+        */
+} SCANLINE;
+
+typedef struct {
+    /* A vertical window of a raster */
+    int width;    /* Width of the window, in columns */
+    int height;   /* Height of the window, in rows */
+    SCANLINE *line;
+        /* An array of 'height' elements, malloced.
+           This identifies the lines of the input image that compose the
+           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
+           line window with topmost row at index 3 might be:
+
+              line[0] = Row 24
+              line[1] = Row 25
+              line[2] = Row 26
+              line[3] = Row 22
+              line[4] = Row 23
+        */
+} SCAN;
+
+
+
+static int
+appendWeight(WLIST * const WList,
+             int     const index,
+             double  const weight) {
+/*----------------------------------------------------------------------------
+   Add a weighting of 'weight' for index 'index' to the weight list
+   'WList'.
+-----------------------------------------------------------------------------*/
+    if (weight == 0.0) {
+        /* A weight of 0 in the list is redundant, so we don't add it.
+           A weight entry says "Add W fraction of the pixel at index I,"
+           so where W is 0, it's the same as not having the entry at all.
+        */
+    } else {
+        unsigned int const n = WList->nWeight;
+
+        assert(WList->allocWeight >= n+1);
+        
+        WList->Weight[n].position = index;
+        WList->Weight[n].weight   = weight;
+        ++WList->nWeight;
+    }
+    return 0;
+}
+
+
+
+static sample
+floatToSample(double const value,
+              sample const maxval) {
+
+    /* Take care here, the conversion of any floating point value <=
+       -1.0 to an unsigned type is _undefined_.  See ISO 9899:1999
+       section 6.3.1.4.  Not only is it undefined it also does the
+       wrong thing in actual practice, EG on Darwin PowerPC (my iBook
+       running OS X) negative values clamp to maxval.  We get negative
+       values because some of the filters (EG catrom) have negative
+       weights.  
+    */
+
+    return MIN(maxval, (sample)(MAX(0.0, (value + 0.5))));
+}
+
+
+
+static void
+initWeightList(WLIST *      const weightListP,
+               unsigned int const maxWeights) {
+
+    weightListP->nWeight = 0;
+    weightListP->allocWeight = maxWeights;
+    MALLOCARRAY(weightListP->Weight, maxWeights);
+    if (weightListP->Weight == NULL)
+        pm_error("Out of memory allocating a %u-element weight list.",
+                 maxWeights);
+}
+
+
+
+static void
+createWeightList(unsigned int          const targetPos,
+                 unsigned int          const sourceSize,
+                 double                const scale,
+                 struct filterFunction       filter,
+                 WLIST *               const weightListP) {
+/*----------------------------------------------------------------------------
+   Create a weight list for computing target pixel number 'targetPos' from
+   a set of source pixels.  These pixels are a line of pixels either 
+   horizontally or vertically.  The weight list is a list of weights to give
+   each source pixel in the set.
+   
+   The source pixel set is a window of source pixels centered on some
+   point.  The weights are defined by the function 'filter' of
+   the position within the window, and normalized to add up to 1.0.
+   Technically, the window is infinite, but we know that the filter
+   function is zero beyond a certain distance from the center of the
+   window.
+
+   For example, assume 'targetPos' is 5.  That means we're computing weights
+   for either Column 5 or Row 5 of the target image.  Assume it's Column 5.
+   Assume 'radius' is 1.  That means a window of two pixels' worth of a
+   source row determines the color of the Column 5 pixel of a target
+   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.
+
+   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
+   height of the filter function over each source pixel region.  But 
+   that's too hard.  So we approximate by assuming that the filter function
+   is constant within each region, at the value the function has at the
+   _center_ of the region.
+
+   So for the Column 1 region, which goes from 1.75 to 2.00, centered
+   -.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
+   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
+
+   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
+
+-----------------------------------------------------------------------------*/
+    /* 'windowCenter', is the continous 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
+       pixels at the edges of that window.  Note that if we're
+       doing vertical weights, "left" and "right" mean top and
+       bottom.  
+    */
+    double const windowCenter = ((double)targetPos + 0.5) / scale;
+    double left = MAX(0.0, windowCenter - filter.radius - EPSILON);
+    unsigned int const leftPixel = floor(left);
+    double right = MIN((double)sourceSize - EPSILON, 
+                       windowCenter + filter.radius + EPSILON);
+    unsigned int const rightPixel = floor(right);
+
+    double norm;
+    unsigned int j;
+
+    initWeightList(weightListP, rightPixel - leftPixel + 1);
+
+    /* calculate weights */
+    norm = 0.0;  /* initial value */
+
+    for (j = leftPixel; j <= rightPixel; ++j) {
+        /* Calculate the weight that source pixel 'j' will have in the 
+           value of target pixel 'targetPos'.
+        */
+        double const regionLeft   = MAX(left, (double)j);
+        double const regionRight  = MIN(right, (double)(j + 1));
+        double const regionWidth  = regionRight - regionLeft;
+        double const regionCenter = (regionRight + regionLeft) / 2;
+        double const dist         = regionCenter - windowCenter;
+        double weight;
+
+        weight = filter.basicFunction(filter.horizontalScaler * dist);
+        if (filter.windowFunction)
+            weight *= filter.windowFunction(
+                filter.horizontalScaler * filter.windowScaler * dist);
+
+        assert(regionWidth <= 1.0);
+        weight *= regionWidth;
+        norm += weight;
+        appendWeight(weightListP, j, weight);
+    }
+
+    if (norm == 0.0)
+        pm_error("INTERNAL ERROR: No source pixels contribute to target "
+                 "pixel %u", targetPos);
+
+    /* normalize the weights so they add up to 1.0 */
+    if (norm != 1.0) {
+        unsigned int n;
+        for (n = 0; n < weightListP->nWeight; ++n) {
+            weightListP->Weight[n].weight /= norm;
+        }
+    }
+}
+
+
+
+static void
+createWeightListSet(unsigned int          const sourceSize,
+                    unsigned int          const targetSize,
+                    struct filterFunction const filterFunction,
+                    WLIST **              const weightListSetP) {
+/*----------------------------------------------------------------------------
+   Create the set of weight lists that will effect the resample.
+
+   This is where the actual work of resampling gets done.
+
+   The weight list set is a bunch of factors one can multiply by the
+   pixels in a region to effect a resampling.  Multiplying by these
+   factors effects all of the following transformations on the
+   original pixels:
+   
+   1) Filter out any frequencies that are artifacts of the
+      original sampling.  We assume a perfect sampling was done,
+      which means the original continuous dataset had a maximum
+      frequency of 1/2 of the original sample rate and anything
+      above that is an artifact of the sampling.  So we filter out
+      anything above 1/2 of the original sample rate (sample rate
+      == pixel resolution).
+      
+   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.
+      
+   3) Sample the result at the new sample rate.
+
+   We do all three of these steps in a single convolution of the
+   original pixels.  Steps (1) and (2) can be combined into a
+   single frequency domain rectangle function.  A frequency domain
+   rectangle function is a pixel domain sinc function, which is
+   what we assume 'filterFunction' is.  We get Step 3 by computing
+   the convolution only at the new sample points.
+   
+   I don't know what any of this means when 'filterFunction' is
+   not sinc.  Maybe it just means some approximation or additional
+   filtering steps are happening.
+-----------------------------------------------------------------------------*/
+    double const scale = (double)targetSize / sourceSize;
+    WLIST *weightListSet;  /* malloc'ed */
+    unsigned int targetPos;
+
+    MALLOCARRAY_NOFAIL(weightListSet, targetSize);
+    
+    for (targetPos = 0; targetPos < targetSize; ++targetPos)
+        createWeightList(targetPos, sourceSize, scale, filterFunction,
+                         &weightListSet[targetPos]);
+
+    *weightListSetP = weightListSet;
+}
+
+
+
+static struct filterFunction
+makeFilterFunction(double          const scale,
+                   basicFunction_t       basicFunction,
+                   double          const basicRadius,
+                   basicFunction_t       windowFunction) {
+/*----------------------------------------------------------------------------
+   Create a function to convolve with the samples (so it isn't actually
+   a filter function, but the Fourier transform of a filter function.
+   A filter function is something you multiply by in the frequency domain)
+   to create a function from which one can resample.
+
+   Convolving with this function will achieve two goals:
+
+   1) filter out high frequencies that are artifacts of the original
+      sampling (i.e. the turning of a continuous function into a staircase
+      function);
+   2) filter out frequencies higher than half the resample rate, so that
+      the resample will be a perfect sampling of it, and not have aliasing.
+
+
+   To make the calculation even more efficient, we take advantage
+   of the fact that the weight list doesn't depend on the
+   particular old and new sample rates at all except -- all that's
+   important is their ratio (which is 'scale').  So we assume the
+   original sample rate is 1 and the new sample rate is 'scale'.
+
+-----------------------------------------------------------------------------*/
+    double const freqLimit = MIN(1.0, scale);
+        /* We're going to cut out any frequencies above this, to accomplish
+           Steps (1) and (2) above.
+        */
+
+    struct filterFunction retval;
+
+    retval.basicFunction = basicFunction;
+    retval.windowFunction = windowFunction;
+    
+    retval.horizontalScaler = freqLimit;
+
+    /* Our 'windowFunction' argument is a function normalized to the
+       domain (-1, 1).  We need to scale it horizontally to fit the
+       basic filter function.  We assume the radius of the filter
+       function is the area to which the window should fit (i.e. zero
+       beyond the radius, nonzero inside the radius).  But that's
+       really a misuse of radius, because radius is supposed to be
+       just the distance beyond which we can assume for convenience
+       that the filter function is zero, possibly giving up some
+       precision.
+
+       But note that 'windowFunction' isn't zero outside (-1, 1), even
+       though the actual window function is supposed to be.  Hence,
+       scaling the window function exactly to the radius stops our
+       calculations from noticing the wrong values outside (-1, 1) --
+       we'll never use them.
+    */
+    retval.windowScaler = 1/basicRadius;
+
+    retval.radius = basicRadius / retval.horizontalScaler;
+
+    return retval;
+}
+                   
+
+                   
+static void
+destroyWeightListSet(WLIST *      const weightListSet, 
+                     unsigned int const size) {
+
+    unsigned int i;
+
+    for (i = 0; i < size; ++i)
+        free(weightListSet[i].Weight);
+
+    free(weightListSet);
+}
+
+
+
+static void
+createScanBuf(struct pam * const pamP,
+              double       const maxRowWeights,
+              bool         const verbose,
+              SCAN *       const scanbufP) {
+
+    SCAN scanbuf;
+    unsigned int lineNumber;
+
+    scanbuf.width = pamP->width;
+    scanbuf.height = maxRowWeights;
+    MALLOCARRAY_NOFAIL(scanbuf.line, scanbuf.height);
+  
+    for (lineNumber = 0; lineNumber < scanbuf.height; ++lineNumber) {
+        scanbuf.line[lineNumber].rowNumber = -1;
+        scanbuf.line[lineNumber].tuplerow = pnm_allocpamrow(pamP);
+    }
+  
+    if (verbose)
+        pm_message("scanline buffer: %d lines of %d pixels", 
+                   scanbuf.height, scanbuf.width);
+
+    *scanbufP = scanbuf;
+}
+
+
+
+static void
+destroyScanbuf(SCAN const scanbuf) {
+
+    unsigned int lineNumber;
+
+    for (lineNumber = 0; lineNumber < scanbuf.height; ++lineNumber)
+        pnm_freepamrow(scanbuf.line[lineNumber].tuplerow);
+
+    free(scanbuf.line);
+}
+
+
+
+static void
+resampleDimensionMessage(struct pam * const inpamP,
+                         struct pam * const outpamP) {
+
+    pm_message ("resampling from %d*%d to %d*%d (%f, %f)", 
+                inpamP->width, inpamP->height, 
+                outpamP->width, outpamP->height,
+                (double)outpamP->width/inpamP->width, 
+                (double)outpamP->height/inpamP->height);
+}
+
+
+
+static void
+addInPixel(const struct pam * const pamP,
+           tuple              const tuple,
+           float              const weight,
+           bool               const haveOpacity,
+           unsigned int       const opacityPlane,
+           double *           const accum) {
+/*----------------------------------------------------------------------------
+  Add into *accum the values from the tuple 'tuple', weighted by
+  'weight'.
+
+  Iff 'haveOpacity', Plane 'opacityPlane' of the tuple is an opacity
+  (alpha, transparency) plane.
+-----------------------------------------------------------------------------*/
+    unsigned int plane;
+
+    for (plane = 0; plane < pamP->depth; ++plane) {
+        sample adjustedForOpacity;
+        
+        if (haveOpacity && plane != opacityPlane) {
+            float const opacity = (float)tuple[opacityPlane]/pamP->maxval;
+            float const unadjusted = (float)tuple[plane]/pamP->maxval;
+
+            adjustedForOpacity = 
+                floatToSample(unadjusted * opacity, pamP->maxval);
+        } else
+            adjustedForOpacity = tuple[plane];
+        
+        accum[plane] += (double)adjustedForOpacity * weight;
+    }
+}
+
+
+
+static void
+generateOutputTuple(const struct pam * const pamP,
+                    double             const accum[],
+                    bool               const haveOpacity, 
+                    unsigned int       const opacityPlane,
+                    tuple *            const tupleP) {
+/*----------------------------------------------------------------------------
+  Convert the values accum[] accumulated for a pixel by
+  outputOneResampledRow() to a bona fide PAM tuple as *tupleP,
+  as described by *pamP.
+-----------------------------------------------------------------------------*/
+    unsigned int plane;
+
+    for (plane = 0; plane < pamP->depth; ++plane) {
+        float opacityAdjustedSample;
+
+        if (haveOpacity && plane != opacityPlane) {
+            if (accum[opacityPlane] < EPSILON) {
+                assert(accum[plane] < EPSILON);
+                opacityAdjustedSample = 0.0;
+            } else 
+                opacityAdjustedSample = accum[plane] / accum[opacityPlane];
+        } else
+            opacityAdjustedSample = accum[plane];
+
+        (*tupleP)[plane] = floatToSample(opacityAdjustedSample, pamP->maxval);
+    }
+}
+
+
+
+static void
+outputOneResampledRow(const struct pam * const outpamP,
+                      SCAN               const scanbuf,
+                      WLIST              const YW,
+                      const WLIST *      const XWeight,
+                      tuple *            const line,
+                      double *           const accum) {
+/*----------------------------------------------------------------------------
+   From the data in 'scanbuf' and weights in 'YW' and 'XWeight', 
+   generate one output row for the image described by *outpamP and
+   output it.
+
+   An output pixel is a weighted average of the pixels in a certain
+   rectangle of the input.  'YW' and 'XWeight' describe those weights
+   for each column of the row we are to output.
+
+   'line' and 'accum' are just working space that Caller provides us
+   with to save us the time of allocating it.  'line' is at least big
+   enough to hold an output row; 'weight' is at least outpamP->depth
+   big.
+-----------------------------------------------------------------------------*/
+    unsigned int col;
+
+    bool haveOpacity;           /* There is an opacity plane */
+    unsigned int opacityPlane;  /* Plane number of opacity plane, if any */
+
+    pnm_getopacity(outpamP, &haveOpacity, &opacityPlane);
+
+    for (col = 0; col < outpamP->width; ++col) {
+        WLIST const XW = XWeight[col];
+
+        unsigned int i;
+        {
+            unsigned int plane;
+            for (plane = 0; plane < outpamP->depth; ++plane)
+                accum[plane] = 0.0;
+        }
+        
+        for (i = 0; i < YW.nWeight; ++i) {
+            int   const yp   = YW.Weight[i].position;
+            float const yw   = YW.Weight[i].weight;
+            int   const slot = yp % scanbuf.height;
+
+            unsigned int j;
+            
+            for (j = 0; j < XW.nWeight; ++j) {
+                int   const xp    = XW.Weight[j].position;
+                tuple const tuple = scanbuf.line[slot].tuplerow[xp];
+                
+                addInPixel(outpamP, tuple, yw * XW.Weight[j].weight, 
+                           haveOpacity, opacityPlane,
+                           accum);
+            }
+        }
+        generateOutputTuple(outpamP, accum, haveOpacity, opacityPlane, 
+                            &line[col]);
+    }
+    pnm_writepamrow(outpamP, line);
+}
+
+
+
+static bool
+scanbufContainsTheRows(SCAN  const scanbuf,
+                       WLIST const rowWeights) {
+/*----------------------------------------------------------------------------
+   Return TRUE iff scanbuf 'scanbuf' contains every row mentioned in
+   'rowWeights'.
+
+   It might contain additional rows besides.
+-----------------------------------------------------------------------------*/
+    bool missingRow;
+    unsigned int i;
+    
+    for (i = 0, missingRow = FALSE;
+         i < rowWeights.nWeight && !missingRow;
+        ++i) {
+        unsigned int const inputRow = rowWeights.Weight[i].position;
+        unsigned int const slot = inputRow % scanbuf.height;
+            /* This is the number of the slot in the scanbuf that would
+               have the input row in question if the scanbuf has the
+               row at all.
+            */
+        if (scanbuf.line[slot].rowNumber != inputRow) {
+            /* Nope, this slot has some other row or no row at all.
+               So the row we're looking for isn't in the scanbuf.
+            */
+            missingRow = TRUE;
+        }
+    }
+    return !missingRow;
+}
+
+
+
+static void
+createWeightLists(struct pam *     const inpamP,
+                  struct pam *     const outpamP,
+                  basicFunction_t  const filterFunction,
+                  double           const filterRadius,
+                  basicFunction_t  const windowFunction,
+                  WLIST **         const horizWeightP,
+                  WLIST **         const vertWeightP,
+                  unsigned int *   const maxRowWeightsP) {
+/*----------------------------------------------------------------------------
+   This is the function that actually does the resampling.  Note that it
+   does it without ever looking at the source or target pixels!  It produces
+   a simple set of numbers that Caller can blindly apply to the source 
+   pixels to get target pixels.
+-----------------------------------------------------------------------------*/
+    struct filterFunction horizFilter, vertFilter;
+
+    horizFilter = makeFilterFunction(
+        (double)outpamP->width/inpamP->width,
+        filterFunction, filterRadius, windowFunction);
+
+    createWeightListSet(inpamP->width, outpamP->width, horizFilter, 
+                        horizWeightP);
+    
+    vertFilter = makeFilterFunction(
+        (double)outpamP->height/inpamP->height,
+        filterFunction, filterRadius, windowFunction);
+
+    createWeightListSet(inpamP->height, outpamP->height, vertFilter, 
+                        vertWeightP);
+
+    *maxRowWeightsP = ceil(2.0*(vertFilter.radius+EPSILON) + 1 + EPSILON);
+}
+
+
+
+static void
+resample(struct pam *     const inpamP,
+         struct pam *     const outpamP,
+         basicFunction_t  const filterFunction,
+         double           const filterRadius,
+         basicFunction_t  const windowFunction,
+         bool             const verbose,
+         bool             const linear) {
+/*---------------------------------------------------------------------------
+  Resample the image in the input file, described by *inpamP,
+  so as to create the image in the output file, described by *outpamP.
+  
+  Input and output differ by height, width, and maxval only.
+
+  Use the resampling filter function 'filterFunction', applied over
+  radius 'filterRadius'.
+  
+  The input file is positioned past the header, to the beginning of the
+  raster.  The output file is too.
+---------------------------------------------------------------------------*/
+    int inputRow, outputRow;
+    WLIST * horizWeight;
+    WLIST * vertWeight;
+    SCAN scanbuf;
+    unsigned int maxRowWeights;
+
+    tuple * line;
+        /* This is just work space for outputOneSampleRow() */
+    double * weight;
+        /* This is just work space for outputOneSampleRow() */
+
+    if (linear)
+        pm_error("You cannot use the resampling scaling method on "
+                 "linear input.");
+  
+    createWeightLists(inpamP, outpamP, filterFunction, filterRadius,
+                      windowFunction, &horizWeight, &vertWeight,
+                      &maxRowWeights);
+
+    createScanBuf(inpamP, maxRowWeights, verbose, &scanbuf);
+
+    if (verbose)
+        resampleDimensionMessage(inpamP, outpamP);
+
+    line = pnm_allocpamrow(outpamP);
+    MALLOCARRAY_NOFAIL(weight, outpamP->depth);
+
+    outputRow = 0;
+    for (inputRow = 0; inputRow < inpamP->height; ++inputRow) {
+        bool needMoreInput;
+            /* We've output as much as we can using the rows that are in
+               the scanbuf; it's time to move the window.  Or fill it in
+               the first place.
+            */
+        unsigned int scanbufSlot;
+
+        /* Read source row; add it to the scanbuf */
+        scanbufSlot = inputRow % scanbuf.height;
+        scanbuf.line[scanbufSlot].rowNumber = inputRow;
+        pnm_readpamrow(inpamP, scanbuf.line[scanbufSlot].tuplerow);
+
+        /* Output all the rows we can make out of the current contents of
+           the scanbuf.  Might be none.
+        */
+        needMoreInput = FALSE;  /* initial assumption */
+        while (outputRow < outpamP->height && !needMoreInput) {
+            WLIST const rowWeights = vertWeight[outputRow];
+                /* The description of what makes up our current output row;
+                   i.e. what fractions of which input rows combine to create
+                   this output row.
+                */
+            assert(rowWeights.nWeight <= scanbuf.height); 
+
+            if (scanbufContainsTheRows(scanbuf, rowWeights)) {
+                outputOneResampledRow(outpamP, scanbuf, rowWeights, 
+                                      horizWeight, line, weight);
+                ++outputRow;
+            } else
+                needMoreInput = TRUE;
+        }
+    }
+
+    if (outputRow != outpamP->height)
+        pm_error("INTERNAL ERROR: assembled only %u of the required %u "
+                 "output rows.", outputRow, outpamP->height);
+
+    pnm_freepamrow(line);
+    destroyScanbuf(scanbuf);
+    destroyWeightListSet(horizWeight, outpamP->width);
+    destroyWeightListSet(vertWeight, outpamP->height);
+}
+
+
+/****************************/
+/****************************/
+/**** end of resampling *****/
+/****************************/
+/****************************/
+
+
+
+static void
+zeroNewRow(struct pam * const pamP,
+           tuplen *     const tuplenrow) {
+    
+    unsigned int col;
+
+    for (col = 0; col < pamP->width; ++col) {
+        unsigned int plane;
+
+        for (plane = 0; plane < pamP->depth; ++plane)
+            tuplenrow[col][plane] = 0.0;
+    }
+}
+
+
+
+static void
+accumOutputCol(struct pam * const pamP,
+               tuplen       const intuplen,
+               float        const fraction, 
+               tuplen       const accumulator) {
+/*----------------------------------------------------------------------------
+   Add fraction 'fraction' of the pixel indicated by 'intuplen' to the
+   pixel accumulator 'accumulator'.
+
+   'intuplen' and 'accumulator' are not a standard libnetpbm tuplen.
+   It is proportional to light intensity.  The foreground color
+   component samples are proportional to light intensity, and have
+   opacity factored in.
+-----------------------------------------------------------------------------*/
+    unsigned int plane;
+
+    for (plane = 0; plane < pamP->depth; ++plane)
+        accumulator[plane] += fraction * intuplen[plane];
+}
+
+
+
+static void
+horizontalScale(tuplen *     const inputtuplenrow, 
+                tuplen *     const newtuplenrow,
+                struct pam * const inpamP,
+                struct pam * const outpamP,
+                float        const xscale,
+                float *      const stretchP) {
+/*----------------------------------------------------------------------------
+  Take the input row 'inputtuplenrow', decribed 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
+  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.
+
+  Assume maxval and depth of input and output are the same.
+-----------------------------------------------------------------------------*/
+    float fraccoltofill, fraccolleft;
+    unsigned int col;
+    unsigned int newcol;
+
+    newcol = 0;
+    fraccoltofill = 1.0;  /* Output column is "empty" now */
+
+    zeroNewRow(outpamP, newtuplenrow);
+
+    for (col = 0; col < inpamP->width; ++col) {
+        /* Process one tuple from input ('inputtuplenrow') */
+        fraccolleft = xscale;
+        /* Output all columns, if any, that can be filled using information
+           from this input column, in addition to what's already in the output
+           column.
+        */
+        while (fraccolleft >= fraccoltofill) {
+            /* Generate one output pixel in 'newtuplerow'.  It will
+               consist of anything accumulated from prior input pixels
+               in accumulator[], plus a fraction of the current input
+               pixel.  
+            */
+            assert(newcol < outpamP->width);
+            accumOutputCol(inpamP, inputtuplenrow[col], fraccoltofill,
+                           newtuplenrow[newcol]);
+
+            fraccolleft -= fraccoltofill;
+
+            /* Set up to start filling next output column */
+            ++newcol;
+            fraccoltofill = 1.0;
+        }
+        /* 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
+           have a tiny bit of pixel left and have run out of output pixels.
+           In that case, we throw away what's left.
+        */
+        if (fraccolleft > 0.0 && newcol < outpamP->width) {
+            accumOutputCol(inpamP, inputtuplenrow[col], fraccolleft,
+                           newtuplenrow[newcol]);
+            fraccoltofill -= fraccolleft;
+        }
+    }
+
+    if (newcol < outpamP->width-1 || newcol > outpamP->width)
+        pm_error("Internal error: last column filled is %d, but %d "
+                 "is the rightmost output column.",
+                 newcol, outpamP->width-1);
+
+    if (newcol < outpamP->width) {
+        /* We were still working on the last output column when we 
+           ran out of input columns.  This would be because of rounding
+           down, and we should be missing only a tiny fraction of that
+           last output column.  Just fill in the missing color with the
+           color of the rightmost input pixel.
+        */
+        accumOutputCol(inpamP, inputtuplenrow[inpamP->width-1], 
+                       fraccoltofill, newtuplenrow[newcol]);
+        
+        *stretchP = fraccoltofill;
+    } else 
+        *stretchP = 0.0;
+}
+
+
+
+static void
+zeroAccum(struct pam * const pamP,
+          tuplen *     const accumulator) {
+
+    unsigned int plane;
+
+    for (plane = 0; plane < pamP->depth; ++plane) {
+        unsigned int col;
+        for (col = 0; col < pamP->width; ++col)
+            accumulator[col][plane] = 0.0;
+    }
+}
+
+
+
+static void
+accumOutputRow(struct pam * const pamP,
+               tuplen *     const tuplenrow, 
+               float        const fraction, 
+               tuplen *     const accumulator) {
+/*----------------------------------------------------------------------------
+   Take 'fraction' times the samples in row 'tuplenrow' and add it to 
+   'accumulator' in the same way as accumOutputCol().
+
+   'fraction' is less than 1.0.
+-----------------------------------------------------------------------------*/
+    unsigned int plane;
+
+    for (plane = 0; plane < pamP->depth; ++plane) {
+        unsigned int col;
+        for (col = 0; col < pamP->width; ++col)
+            accumulator[col][plane] += fraction * tuplenrow[col][plane];
+    }
+}
+
+
+
+static void
+readARow(struct pam *             const pamP,
+         tuplen *                 const tuplenRow,
+         const pnm_transformMap * const transform) {
+/*----------------------------------------------------------------------------
+  Read a row from the input file described by *pamP, as values (for the
+  foreground color) proportional to light intensity, with opacity
+  included.
+
+  By contrast, a simple libnetpbm read would give the same numbers you
+  find in a PAM: gamma-adjusted values for the foreground color
+  component and scaled as if opaque.  The latter means that full red
+  would have a red intensity of 1.0 even if the pixel is only 75%
+  opaque.  We, on the other hand, would return red intensity of .75 in
+  that case.
+
+  The opacity plane we return is the same as a simple libnetpbm read
+  would return.
+
+  We ASSUME that the transform 'transform' is that necessary to effect
+  the conversion to intensity-linear values and normalize.  If it is
+  NULL, we ASSUME that they already are intensity-proportional and just
+  need to be normalized.
+-----------------------------------------------------------------------------*/
+    tuple * tupleRow;
+
+    tupleRow = pnm_allocpamrow(pamP);
+
+    pnm_readpamrow(pamP, tupleRow);
+
+    pnm_normalizeRow(pamP, tupleRow, transform, tuplenRow);
+
+    pnm_applyopacityrown(pamP, tuplenRow);
+
+    pnm_freepamrow(tupleRow);
+}
+
+
+
+static void
+writeARow(struct pam *             const pamP,
+          tuplen *                 const tuplenRow,
+          const pnm_transformMap * const transform) {
+/*----------------------------------------------------------------------------
+  Write a row to the output file described by *pamP, from values
+  proportional to light intensity with opacity included (i.e. the same
+  kind of number you would get form readARow()).
+
+  We ASSUME that the transform 'transform' is that necessary to effect
+  the conversion to brightness-linear unnormalized values.  If it is
+  NULL, we ASSUME that they already are brightness-proportional and just
+  need to be unnormalized.
+
+  We destroy *tuplenRow in the process.
+-----------------------------------------------------------------------------*/
+    tuple * tupleRow;
+
+    tupleRow = pnm_allocpamrow(pamP);
+
+    pnm_unapplyopacityrown(pamP, tuplenRow);
+
+    pnm_unnormalizeRow(pamP, tuplenRow, transform, tupleRow);
+
+    pnm_writepamrow(pamP, tupleRow);
+
+    pnm_freepamrow(tupleRow);
+}
+
+
+
+static void
+issueStretchWarning(bool   const verbose, 
+                    double const fracrowtofill) {
+
+    /* We need another input row to fill up this
+       output row, but there aren't any more.
+       That's because of rounding down on our
+       scaling arithmetic.  So we go ahead with
+       the data from the last row we read, which
+       amounts to stretching out the last output
+       row.  
+    */
+    if (verbose)
+        pm_message("%f of bottom row stretched due to "
+                   "arithmetic imprecision", 
+                   fracrowtofill);
+}
+
+
+
+static void
+scaleHorizontallyAndOutputRow(struct pam *             const inpamP,
+                              struct pam *             const outpamP,
+                              tuplen *                 const rowAccumulator,
+                              const pnm_transformMap * const transform,
+                              tuplen *                 const newtuplenrow,
+                              float                    const xscale,
+                              unsigned int             const row,
+                              bool                     const verbose) {
+/*----------------------------------------------------------------------------
+   Scale the row in 'rowAccumulator' horizontally by factor 'xscale'
+   and output it.
+
+   'newtuplenrow' is work space Caller provides us.  It is at least
+   wide enough to hold one output row.
+-----------------------------------------------------------------------------*/
+    if (outpamP->width == inpamP->width)    
+        /* shortcut X scaling */
+        writeARow(outpamP, rowAccumulator, transform);
+            /* This destroys 'rowAccumulator' */
+    else {
+        float stretch;
+            
+        horizontalScale(rowAccumulator, newtuplenrow, inpamP, outpamP,
+                        xscale, &stretch);
+            
+        if (verbose && row == 0)
+            pm_message("%f of right column stretched due to "
+                       "arithmetic imprecision", 
+                       stretch);
+            
+        writeARow(outpamP, newtuplenrow, transform);
+            /* This destroys 'newtuplenrow' */
+    }
+}
+
+
+
+static void
+createTransforms(struct pam *              const inpamP,
+                 struct pam *              const outpamP,
+                 bool                      const assumeLinear,
+                 const pnm_transformMap ** const inputTransformP,
+                 const pnm_transformMap ** const outputTransformP) {
+
+    if (assumeLinear) {
+        *inputTransformP  = NULL;
+        *outputTransformP = NULL;
+    } else {
+        *inputTransformP  = pnm_createungammatransform(inpamP);
+        *outputTransformP = pnm_creategammatransform(outpamP);
+    }
+}
+
+
+
+static void
+destroyTransforms(const pnm_transformMap * const inputTransform,
+                  const pnm_transformMap * const outputTransform) {
+
+    if (inputTransform)
+        free((void*)inputTransform);
+    
+    if (outputTransform)
+        free((void*)outputTransform);
+}
+
+
+
+static void
+scaleWithMixing(struct pam * const inpamP,
+                struct pam * const outpamP,
+                float        const xscale, 
+                float        const yscale,
+                bool         const assumeLinear,
+                bool         const verbose) {
+/*----------------------------------------------------------------------------
+  Scale the image described by *inpamP by xscale horizontally and
+  yscale vertically and write the result as the image described by
+  *outpamP.
+
+  The input file is positioned past the header, to the beginning of the
+  raster.  The output file is too.
+
+  Mix colors from input rows together in the output rows.
+
+  'assumeLinear' means to assume that the sample values in the input
+  image vary from standard PAM in that they are proportional to
+  intensity, (This makes the computation a lot faster, so you might
+  use this even if the samples are actually standard PAM, to get
+  approximate but fast results).
+
+-----------------------------------------------------------------------------*/
+    /* Here's how we think of the color mixing scaling operation:  
+       
+       First, I'll describe scaling in one dimension.  Assume we have
+       a one row image.  A raster row is ordinarily a sequence of
+       discrete pixels which have no width and no distance between
+       them -- only a sequence.  Instead, think of the raster row as a
+       bunch of pixels 1 unit wide adjacent to each other.  For
+       example, we are going to scale a 100 pixel row to a 150 pixel
+       row.  Imagine placing the input row right above the output row
+       and stretching it so it is the same size as the output row.  It
+       still contains 100 pixels, but they are 1.5 units wide each.
+       Our goal is to make the output row look as much as possible
+       like the stretched input row, while observing that a pixel can
+       be only one color.
+
+       Output Pixel 0 is completely covered by Input Pixel 0, so we
+       make Output Pixel 0 the same color as Input Pixel 0.  Output
+       Pixel 1 is covered half by Input Pixel 0 and half by Input
+       Pixel 1.  So we make Output Pixel 1 a 50/50 mix of Input Pixels
+       0 and 1.  If you stand back far enough, input and output will
+       look the same.
+
+       This works for all scale factors, both scaling up and scaling down.
+       
+       For images with an opacity plane, imagine Input Pixel 0's
+       foreground is fully opaque red (1,0,0,1), and Input Pixel 1 is
+       fully transparent (foreground irrelevant) (0,0,0,0).  We make
+       Output Pixel 0's foreground fully opaque red as before.  Output
+       Pixel 1 is covered half by Input Pixel 0 and half by Input
+       Pixel 1, so it is 50% opaque; but its foreground color is still
+       red: (1,0,0,0.5).  The output foreground color is the opacity
+       and coverage weighted average of the input foreground colors,
+       and the output opacity is the coverage weighted average of the
+       input opacities.
+
+       This program always stretches or squeezes the input row to be the
+       same length as the output row; The output row's pixels are always
+       1 unit wide.
+
+       The same thing works in the vertical direction.  We think of
+       rows as stacked strips of 1 unit height.  We conceptually
+       stretch the image vertically first (same process as above, but
+       in place of a single-color pixels, we have a vector of colors).
+       Then we take each row this vertical stretching generates and
+       stretch it horizontally.  
+    */
+
+    tuplen * tuplenrow;     /* An input row */
+    tuplen * newtuplenrow;  /* Working space */
+    float rowsleft;
+    /* The number of rows of output that need to be formed from the
+       current input row (the one in tuplerow[]), less the number that 
+       have already been formed (either in accumulator[]
+       or output to the file).  This can be fractional because of the
+       way we define rows as having height.
+    */
+    float fracrowtofill;
+        /* The fraction of the current output row (the one in vertScaledRow[])
+           that hasn't yet been filled in from an input row.
+        */
+    tuplen * rowAccumulator;
+        /* The red, green, and blue color intensities so far accumulated
+           from input rows for the current output row.  The ultimate value
+           of this is an output row after vertical scaling, but before
+           horizontal scaling.
+        */
+    int rowsread;
+        /* Number of rows of the input file that have been read */
+    int row;
+    const pnm_transformMap * inputTransform;
+    const pnm_transformMap * outputTransform;
+    
+    tuplenrow = pnm_allocpamrown(inpamP); 
+    rowAccumulator = pnm_allocpamrown(inpamP);
+
+    rowsread = 0;
+    rowsleft = 0.0;
+    fracrowtofill = 1.0;
+
+    newtuplenrow = pnm_allocpamrown(outpamP);
+
+    createTransforms(inpamP, outpamP, assumeLinear,
+                     &inputTransform, &outputTransform);
+
+    for (row = 0; row < outpamP->height; ++row) {
+        /* First scale Y from tuplerow[] into rowAccumulator[]. */
+
+        zeroAccum(inpamP, rowAccumulator);
+
+        if (outpamP->height == inpamP->height) {
+            /* shortcut Y scaling */
+            readARow(inpamP, rowAccumulator, inputTransform);
+        } else {
+            while (fracrowtofill > 0) {
+                if (rowsleft <= 0.0) {
+                    if (rowsread < inpamP->height) {
+                        readARow(inpamP, tuplenrow, inputTransform);
+                        ++rowsread;
+                    } else
+                        issueStretchWarning(verbose, fracrowtofill);
+                    rowsleft = yscale;
+                }
+                if (rowsleft < fracrowtofill) {
+                    accumOutputRow(inpamP, tuplenrow, rowsleft,
+                                   rowAccumulator);
+                    fracrowtofill -= rowsleft;
+                    rowsleft = 0.0;
+                } else {
+                    accumOutputRow(inpamP, tuplenrow, fracrowtofill,
+                                   rowAccumulator);
+                    rowsleft = rowsleft - fracrowtofill;
+                    fracrowtofill = 0.0;
+                }
+            }
+            fracrowtofill = 1.0;
+        }
+        /* 'rowAccumulator' now contains the contents of a single
+           output row, but not yet horizontally scaled.  Scale it now
+           horizontally and write it out.
+        */
+        scaleHorizontallyAndOutputRow(inpamP, outpamP, rowAccumulator,
+                                      outputTransform, newtuplenrow, xscale,
+                                      row, verbose);
+            /* Destroys rowAccumulator */
+
+    }
+    destroyTransforms(inputTransform, outputTransform);
+    pnm_freepamrown(rowAccumulator);
+    pnm_freepamrown(newtuplenrow);
+    pnm_freepamrown(tuplenrow);
+}
+
+
+
+static void
+scaleWithoutMixing(const struct pam * const inpamP,
+                   const struct pam * const outpamP,
+                   float              const xscale, 
+                   float              const yscale) {
+/*----------------------------------------------------------------------------
+  Scale the image described by *inpamP by xscale horizontally and
+  yscale vertically and write the result as the image described by
+  *outpamP.
+
+  The input file is positioned past the header, to the beginning of the
+  raster.  The output file is too.
+  
+  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.
+-----------------------------------------------------------------------------*/
+    tuple * tuplerow;  /* An input row */
+    tuple * newtuplerow;
+    int row;
+    int rowInInput;
+
+    tuplerow = pnm_allocpamrow(inpamP); 
+    rowInInput = -1;
+
+    newtuplerow = pnm_allocpamrow(outpamP);
+
+    for (row = 0; row < outpamP->height; ++row) {
+        int col;
+        
+        int const inputRow = (int) (row / yscale);
+
+        for (; rowInInput < inputRow; ++rowInInput) 
+            pnm_readpamrow(inpamP, tuplerow);
+        
+        for (col = 0; col < outpamP->width; ++col) {
+            int const inputCol = (int) (col / xscale);
+            
+            pnm_assigntuple(inpamP, newtuplerow[col], tuplerow[inputCol]);
+        }
+
+        pnm_writepamrow(outpamP, newtuplerow);
+    }
+    pnm_freepamrow(tuplerow);
+    pnm_freepamrow(newtuplerow);
+}
+
+
+
+int
+main(int argc, char **argv ) {
+
+    struct cmdlineInfo cmdline;
+    FILE* ifP;
+    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;
+
+    if (PNM_FORMAT_TYPE(inpam.format) == PBM_TYPE) {
+        outpam.format = PGM_TYPE;
+        outpam.maxval = PGM_MAXMAXVAL;
+        pm_message("promoting from PBM to PGM");
+    } else {
+        outpam.format = inpam.format;
+        outpam.maxval = inpam.maxval;
+    }
+
+    computeOutputDimensions(cmdline, inpam.height, inpam.width,
+                            &outpam.height, &outpam.width);
+
+    xscale = (float) outpam.width / inpam.width;
+    yscale = (float) outpam.height / inpam.height;
+
+    if (cmdline.verbose) {
+        pm_message("Scaling by %f horizontally to %d columns.", 
+                   xscale, outpam.width);
+        pm_message("Scaling by %f vertically to %d rows.", 
+                   yscale, outpam.height);
+    }
+
+    if (xscale * inpam.width < outpam.width - 1 ||
+        yscale * inpam.height < outpam.height - 1) 
+        pm_error("Arithmetic precision of this program is inadequate to "
+                 "do the specified scaling.  Use a smaller input image "
+                 "or a slightly different scale factor.");
+
+    pnm_writepaminit(&outpam);
+
+    if (cmdline.nomix) {
+        if (cmdline.verbose)
+            pm_message("Using nomix method");
+        scaleWithoutMixing(&inpam, &outpam, xscale, yscale);
+    } else if (!cmdline.filterFunction) {
+        if (cmdline.verbose)
+            pm_message("Using regular rescaling method");
+        scaleWithMixing(&inpam, &outpam, xscale, yscale, 
+                        cmdline.linear, cmdline.verbose);
+    } else {
+        if (cmdline.verbose)
+            pm_message("Using general filter method");
+        resample(&inpam, &outpam,
+                 cmdline.filterFunction, cmdline.filterRadius,
+                 cmdline.windowFunction, cmdline.verbose,
+                 cmdline.linear);
+    }
+    pm_close(ifP);
+    pm_close(stdout);
+    
+    return 0;
+}
diff --git a/editor/pamstretch-gen b/editor/pamstretch-gen
new file mode 100755
index 00000000..cd59a36b
--- /dev/null
+++ b/editor/pamstretch-gen
@@ -0,0 +1,80 @@
+#!/bin/sh
+#
+# pamstretch-gen - a shell script which acts a little like a general
+# form of pamstretch, by scaling up with pamstretch then scaling
+# down with pamscale.
+#
+# it also copes with N<1, but then it just uses pamscale. :-)
+#
+# Formerly named 'pnminterp-gen' and 'pnmstretch-gen'.
+#
+# Copyright (C) 1998,2000 Russell Marks.
+# 
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or (at
+# your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# 
+
+
+if [ "$1" = "" ]; then
+  echo 'usage: pamstretch-gen N [pnmfile]'
+  exit 1
+fi
+
+tempdir="${TMPDIR-/tmp}/pamstretch-gen.$$"
+mkdir $tempdir || { echo "Could not create temporary file. Exiting."; exit 1;}
+chmod 700 $tempdir
+tempfile=$tempdir/pnmig
+
+trap 'rm -rf $tempdir' 0 1 3 15
+
+if ! cat $2 >$tempfile 2>/dev/null; then
+  echo 'pamstretch-gen: error reading file' 1>&2
+  exit 1
+fi
+
+if ! pnmfile $tempfile 1>/dev/null 2>/dev/null; then
+  echo 'Not valid pnm input'
+  exit 1
+fi
+
+# we use the width as indication of how much to scale; width and
+# height are being scaled equally, so this should be ok.
+width=`pnmfile $tempfile 2>/dev/null|cut -d " " -f 3`
+
+if [ "$width" = "" ]; then
+  echo 'pamstretch-gen: not a PNM file' 1>&2
+  exit 1
+fi
+
+# should really use dc for maths, but awk is less painful :-)
+target_width=`awk 'BEGIN{printf("%d",'0.5+"$width"*"$1"')}'`
+
+# work out how far we have to scale it up with pamstretch so that the
+# new width is >= the target width.
+int_scale=`awk '
+BEGIN {
+int_scale=1;int_width='"$width"'
+while(int_width<'"$target_width"')
+  {
+  int_scale++
+  int_width+='"$width"'
+  }
+print int_scale
+}'`
+
+if [ "$int_scale" -eq 1 ]; then
+  pamscale "$1" $tempfile
+else
+  pamstretch "$int_scale" $tempfile | pnmscale -xsi "$target_width"
+fi
diff --git a/editor/pamstretch.c b/editor/pamstretch.c
new file mode 100644
index 00000000..0e9e6abf
--- /dev/null
+++ b/editor/pamstretch.c
@@ -0,0 +1,408 @@
+/* pamstretch - scale up portable anymap by interpolating between pixels.
+ * 
+ * This program is based on 'pnminterp' by Russell Marks, rename
+ * pnmstretch for inclusion in Netpbm, then rewritten and renamed to
+ * pamstretch by Bryan Henderson in December 2001.
+ *
+ * Copyright (C) 1998,2000 Russell Marks.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include "pam.h"
+#include "shhopt.h"
+
+enum an_edge_mode {
+    EDGE_DROP,
+        /* drop one (source) pixel at right/bottom edges. */
+    EDGE_INTERP_TO_BLACK,
+        /* interpolate right/bottom edge pixels to black. */
+    EDGE_NON_INTERP
+        /* don't interpolate right/bottom edge pixels 
+           (default, and what zgv does). */
+};
+
+
+struct cmdline_info {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char *input_filespec;  /* Filespecs of input files */
+    enum an_edge_mode edge_mode;
+    unsigned int xscale;
+    unsigned int yscale;
+};
+
+
+
+tuple blackTuple;  
+   /* A "black" tuple.  Unless our input image is PBM, PGM, or PPM, we
+      don't really know what "black" means, so this is just something
+      arbitrary in that case.
+      */
+
+
+static void
+parse_command_line(int argc, char ** argv,
+                   struct cmdline_info *cmdline_p) {
+/*----------------------------------------------------------------------------
+   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));
+    unsigned int option_def_index;
+
+    unsigned int blackedge;
+    unsigned int dropedge;
+    unsigned int xscale_spec;
+    unsigned int yscale_spec;
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    OPTENT3('b', "blackedge",    OPT_FLAG, NULL, &blackedge,            0);
+    OPTENT3('d', "dropedge",     OPT_FLAG, NULL, &dropedge,             0);
+    OPTENT3(0,   "xscale",       OPT_UINT, 
+            &cmdline_p->xscale, &xscale_spec, 0);
+    OPTENT3(0,   "yscale",       OPT_UINT, 
+            &cmdline_p->yscale, &yscale_spec, 0);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE; /* We have some short (old-fashioned) options */
+    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
+
+    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdline_p and others. */
+
+    if (blackedge && dropedge) 
+        pm_error("Can't specify both -blackedge and -dropedge options.");
+    else if (blackedge)
+        cmdline_p->edge_mode = EDGE_INTERP_TO_BLACK;
+    else if (dropedge)
+        cmdline_p->edge_mode = EDGE_DROP;
+    else
+        cmdline_p->edge_mode = EDGE_NON_INTERP;
+
+    if (xscale_spec && cmdline_p->xscale == 0)
+        pm_error("You specified zero for the X scale factor.");
+    if (yscale_spec && cmdline_p->yscale == 0)
+        pm_error("You specified zero for the Y scale factor.");
+
+    if (xscale_spec && !yscale_spec)
+        cmdline_p->yscale = 1;
+    if (yscale_spec && !xscale_spec)
+        cmdline_p->xscale = 1;
+
+    if (!(xscale_spec || yscale_spec)) {
+        /* scale must be specified in an argument */
+        if ((argc-1) != 1 && (argc-1) != 2)
+            pm_error("Wrong number of arguments (%d).  Without scale options, "
+                     "you must supply 1 or 2 arguments:  scale and "
+                     "optional file specification", argc-1);
+        
+        {
+            char *endptr;   /* ptr to 1st invalid character in scale arg */
+            unsigned int scale;
+            
+            scale = strtol(argv[1], &endptr, 10);
+            if (*argv[1] == '\0') 
+                pm_error("Scale argument is a null string.  "
+                         "Must be a number.");
+            else if (*endptr != '\0')
+                pm_error("Scale argument contains non-numeric character '%c'.",
+                         *endptr);
+            else if (scale < 2)
+                pm_error("Scale argument must be at least 2.  "
+                         "You specified %d", scale);
+            cmdline_p->xscale = scale;
+            cmdline_p->yscale = scale;
+        }
+        if (argc-1 > 1) 
+            cmdline_p->input_filespec = argv[2];
+        else
+            cmdline_p->input_filespec = "-";
+    } else {
+        /* No scale argument allowed */
+        if ((argc-1) > 1)
+            pm_error("Too many arguments (%d).  With a scale option, "
+                     "the only argument is the "
+                     "optional file specification", argc-1);
+        if (argc-1 > 0) 
+            cmdline_p->input_filespec = argv[1];
+        else
+            cmdline_p->input_filespec = "-";
+    }
+}
+
+
+
+static void
+stretch_line(struct pam * const inpamP, 
+             const tuple * const line, const tuple * const line_stretched, 
+             unsigned int const scale, enum an_edge_mode const edge_mode) {
+/*----------------------------------------------------------------------------
+   Stretch the line of tuples 'line' into the output buffer 'line_stretched',
+   by factor 'scale'.
+-----------------------------------------------------------------------------*/
+    int scaleincr;
+    int sisize;   
+        /* normalizing factor to make fractions representable as integers.
+           E.g. if sisize = 100, one half is represented as 50.
+        */
+    unsigned int col;
+    unsigned int outcol;
+    
+    sisize=0;
+    while (sisize<256) 
+        sisize += scale;
+    scaleincr = sisize/scale;  /* (1/scale, normalized) */
+
+    outcol = 0;  /* initial value */
+
+    for (col = 0; col < inpamP->width; ++col) {
+        unsigned int pos;
+            /* The fraction of the way we are from curline to nextline,
+               normalized by sisize.
+            */
+        if (col >= inpamP->width-1) {
+            /* We're at the edge.  There is no column to the right with which
+               to interpolate.
+            */
+            switch(edge_mode) {
+            case EDGE_DROP:
+                /* No output column needed for this input column */
+                break;
+            case EDGE_INTERP_TO_BLACK: {
+                unsigned int pos;
+                for (pos = 0; pos < sisize; pos += scaleincr) {
+                    unsigned int plane;
+                    for (plane = 0; plane < inpamP->depth; ++plane)
+                        line_stretched[outcol][plane] = 
+                            (line[col][plane] * (sisize-pos)) / sisize;
+                    ++outcol;
+                }
+            }
+            break;
+            case EDGE_NON_INTERP: {
+                unsigned int pos;
+                for (pos = 0; pos < sisize; pos += scaleincr) {
+                    unsigned int plane;
+                    for (plane = 0; plane < inpamP->depth; ++plane)
+                        line_stretched[outcol][plane] = line[col][plane];
+                    ++outcol;
+                }
+            }
+            break;
+            default: 
+                pm_error("INTERNAL ERROR: invalid value for edge_mode");
+            }
+        } else {
+            /* Interpolate with the next input column to the right */
+            for (pos = 0; pos < sisize; pos += scaleincr) {
+                unsigned int plane;
+                for (plane = 0; plane < inpamP->depth; ++plane)
+                    line_stretched[outcol][plane] = 
+                        (line[col][plane] * (sisize-pos) 
+                         +  line[col+1][plane] * pos) / sisize;
+                ++outcol;
+            }
+        }
+    }
+}
+
+
+
+static void 
+write_interp_rows(struct pam *      const outpamP,
+                  const tuple *     const curline,
+                  const tuple *     const nextline, 
+                  tuple *           const outbuf,
+                  int               const scale) {
+/*----------------------------------------------------------------------------
+   Write out 'scale' rows, being 'curline' followed by rows that are 
+   interpolated between 'curline' and 'nextline'.
+-----------------------------------------------------------------------------*/
+    unsigned int scaleincr;
+    unsigned int sisize;
+    unsigned int pos;
+
+    sisize=0;
+    while(sisize<256) sisize+=scale;
+    scaleincr=sisize/scale;
+
+    for (pos = 0; pos < sisize; pos += scaleincr) {
+        unsigned int col;
+        for (col = 0; col < outpamP->width; ++col) {
+            unsigned int plane;
+            for (plane = 0; plane < outpamP->depth; ++plane) 
+                outbuf[col][plane] = (curline[col][plane] * (sisize-pos)
+                    + nextline[col][plane] * pos) / sisize;
+        }
+        pnm_writepamrow(outpamP, outbuf);
+    }
+}
+
+
+
+static void
+swap_buffers(tuple ** const buffer1P, tuple ** const buffer2P) {
+    /* Advance "next" line to "current" line by switching
+       line buffers 
+    */
+    tuple *tmp;
+
+    tmp = *buffer1P;
+    *buffer1P = *buffer2P;
+    *buffer2P = tmp;
+}
+
+
+static void 
+stretch(struct pam * const inpamP, struct pam * const outpamP,
+        int const xscale, int const yscale,
+        enum an_edge_mode const edge_mode) {
+
+    tuple *linebuf1, *linebuf2;  /* Input buffers for two rows at a time */
+    tuple *curline, *nextline;   /* Pointers to one of the two above buffers */
+    /* And the stretched versions: */
+    tuple *stretched_linebuf1, *stretched_linebuf2;
+    tuple *curline_stretched, *nextline_stretched;
+
+    tuple *outbuf;   /* One-row output buffer */
+    unsigned int row;
+    unsigned int rowsToStretch;
+    
+    linebuf1 =           pnm_allocpamrow(inpamP);
+    linebuf2 =           pnm_allocpamrow(inpamP);
+    stretched_linebuf1 = pnm_allocpamrow(outpamP);
+    stretched_linebuf2 = pnm_allocpamrow(outpamP);
+    outbuf =             pnm_allocpamrow(outpamP);
+
+    curline = linebuf1;
+    curline_stretched = stretched_linebuf1;
+    nextline = linebuf2;
+    nextline_stretched = stretched_linebuf2;
+
+    pnm_readpamrow(inpamP, curline);
+    stretch_line(inpamP, curline, curline_stretched, xscale, edge_mode);
+
+    if (edge_mode == EDGE_DROP) 
+        rowsToStretch = inpamP->height - 1;
+    else
+        rowsToStretch = inpamP->height;
+    
+    for (row = 0; row < rowsToStretch; row++) {
+        if (row == inpamP->height-1) {
+            /* last line is about to be output. there is no further
+             * `next line'.  if EDGE_DROP, we stop here, with output
+             * of rows-1 rows.  if EDGE_INTERP_TO_BLACK we make next
+             * line black.  if EDGE_NON_INTERP (default) we make it a
+             * copy of the current line.  
+             */
+            switch (edge_mode) {
+            case EDGE_INTERP_TO_BLACK: {
+                int col;
+                for (col = 0; col < outpamP->width; col++)
+                    nextline_stretched[col] = blackTuple;
+            } 
+            break;
+            case EDGE_NON_INTERP: {
+                /* EDGE_NON_INTERP */
+                int col;
+                for (col = 0; col < outpamP->width; col++)
+                    nextline_stretched[col] = curline_stretched[col];
+            }
+            break;
+            case EDGE_DROP: 
+                pm_error("INTERNAL ERROR: processing last row, but "
+                         "edge_mode is EDGE_DROP.");
+            }
+        } else {
+            pnm_readpamrow(inpamP, nextline);
+            stretch_line(inpamP, nextline, nextline_stretched, xscale,
+                         edge_mode);
+        }
+        
+        /* interpolate curline towards nextline into outbuf */
+        write_interp_rows(outpamP, curline_stretched, nextline_stretched,
+                          outbuf, yscale);
+
+        swap_buffers(&curline, &nextline);
+        swap_buffers(&curline_stretched, &nextline_stretched);
+    }
+    pnm_freerow(outbuf);
+    pnm_freerow(stretched_linebuf2);
+    pnm_freerow(stretched_linebuf1);
+    pnm_freerow(linebuf2);
+    pnm_freerow(linebuf1);
+}
+
+
+
+int 
+main(int argc,char *argv[]) {
+
+    FILE *ifp;
+
+    struct cmdline_info cmdline; 
+    struct pam inpam, outpam;
+    
+    pnm_init(&argc, argv);
+
+    parse_command_line(argc, argv, &cmdline);
+
+    ifp = pm_openr(cmdline.input_filespec);
+
+    pnm_readpaminit(ifp, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    if (inpam.width < 2)
+        pm_error("Image is too narrow.  Must be at least 2 columns.");
+    if (inpam.height < 2)
+        pm_error("Image is too short.  Must be at least 2 lines.");
+
+
+    outpam = inpam;  /* initial value */
+    outpam.file = stdout;
+
+    if (PNM_FORMAT_TYPE(inpam.format) == PBM_TYPE) {
+        outpam.format = PGM_TYPE;
+        /* usual filter message when reading PBM but writing PGM: */
+        pm_message("promoting from PBM to PGM");
+    } else {
+        outpam.format = inpam.format;
+    }
+    {
+        unsigned int const dropped = cmdline.edge_mode == EDGE_DROP ? 1 : 0;
+
+        outpam.width = (inpam.width - dropped) * cmdline.xscale;
+        outpam.height = (inpam.height - dropped) * cmdline.yscale;
+
+        pnm_writepaminit(&outpam);
+    }
+
+    pnm_createBlackTuple(&outpam, &blackTuple);
+
+    stretch(&inpam, &outpam, 
+            cmdline.xscale, cmdline.yscale, cmdline.edge_mode);
+
+    pm_close(ifp);
+
+    exit(0);
+}
+
+
+
diff --git a/editor/pamthreshold.c b/editor/pamthreshold.c
new file mode 100644
index 00000000..40260e79
--- /dev/null
+++ b/editor/pamthreshold.c
@@ -0,0 +1,623 @@
+/* pamthreshold - convert a Netpbm image to black and white by thresholding  */
+
+/* This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+/* Copyright (C) 2006 Erik Auerswald
+ * auerswal@unix-ag.uni-kl.de */
+
+#include <assert.h>
+#include <math.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "mallocvar.h"
+#include "nstring.h"
+#include "shhopt.h"
+#include "pam.h"
+
+#define MAX_ITERATIONS 100             /* stop after at most 100 iterations */
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFileName;
+    unsigned int simple;
+    float        threshold;
+    bool         local;
+    bool         dual;
+    float        contrast;
+    unsigned int width, height;
+        /* geometry of local subimage.  Defined only if 'local' or 'dual'
+           is true.
+        */
+};
+
+
+
+struct range {
+    /* A range of sample values, normalized to [0, 1] */
+    samplen min;
+    samplen max;
+};
+
+
+
+static void
+initRange(struct range * const rangeP) {
+
+    /* Initialize to "undefined" state */
+    rangeP->min = 1.0;
+    rangeP->max = 0.0;
+}
+
+          
+
+static void
+addToRange(struct range * const rangeP,
+           samplen        const newSample) {
+
+    rangeP->min = MIN(newSample, rangeP->min);
+    rangeP->max = MAX(newSample, rangeP->max);
+}
+
+
+
+static float
+spread(struct range const range) {
+
+    assert(range.max >= range.min);
+    
+    return range.max - range.min;
+}
+
+
+
+static void
+parseGeometry(const char *   const wxl,
+              unsigned int * const widthP,
+              unsigned int * const heightP,
+              const char **  const errorP) {
+
+    char * const xPos = strchr(wxl, 'x');
+    if (!xPos)
+        asprintfN(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.");
+        else if (*heightP == 0)
+            asprintfN(errorP, "Height is zero.");
+        else
+            *errorP = NULL;
+    }
+}
+
+
+
+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.
+-----------------------------------------------------------------------------*/
+    /* vars for the option parser */
+    optEntry * option_def;
+    optStruct3 opt;
+    unsigned int option_def_index = 0;   /* incremented by OPTENT3 */
+
+    unsigned int thresholdSpec, localSpec, dualSpec, contrastSpec;
+    const char * localOpt;
+    const char * dualOpt;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    /* define the options */
+    OPTENT3(0, "simple",    OPT_FLAG,   NULL,               
+            &cmdlineP->simple,      0);
+    OPTENT3(0, "local",     OPT_STRING, &localOpt,
+            &localSpec,             0);
+    OPTENT3(0, "dual",      OPT_STRING, &dualOpt,
+            &dualSpec,              0);
+    OPTENT3(0, "threshold", OPT_FLOAT,  &cmdlineP->threshold,
+            &thresholdSpec,         0);
+    OPTENT3(0, "contrast",  OPT_FLOAT,  &cmdlineP->contrast,
+            &contrastSpec,          0);
+
+    /* set the defaults */
+    cmdlineP->width = cmdlineP->height = 0U;
+
+    /* set the option description for 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);
+
+    if (cmdlineP->simple + localSpec + dualSpec > 1)
+        pm_error("You may specify only one of -simple, -local, and -dual");
+
+    if (!thresholdSpec)
+        cmdlineP->threshold = 0.5;
+
+    /* 0 <= threshold <= 1 */
+    if ((cmdlineP->threshold < 0.0) || (cmdlineP->threshold > 1.0))
+        pm_error("threshold must be in [0,1]");
+
+    if (!contrastSpec)
+        cmdlineP->contrast = 0.05;
+
+    /* 0 <= contrast <= 1 */
+    if ((cmdlineP->contrast < 0.0) || (cmdlineP->contrast > 1.0))
+        pm_error("contrast must be in [0,1]");
+
+    if (localSpec) {
+        const char * error;
+        cmdlineP->local = TRUE;
+
+        parseGeometry(localOpt, &cmdlineP->width, &cmdlineP->height, &error);
+
+        if (error) {
+            pm_error("Invalid -local value '%s'.  %s", localOpt, error);
+            strfree(error);
+        }
+    } else
+        cmdlineP->local = FALSE;
+
+    if (dualSpec) {
+        const char * error;
+        cmdlineP->dual = TRUE;
+
+        parseGeometry(dualOpt, &cmdlineP->width, &cmdlineP->height, &error);
+
+        if (error) {
+            pm_error("Invalid -dual value '%s'.  %s", dualOpt, error);
+            strfree(error);
+        }
+    } else
+        cmdlineP->dual = FALSE;
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFileName = argv[1];
+    else 
+        pm_error("Progam takes at most 1 parameter: the file name.  "
+                 "You specified %d", argc-1);
+}
+
+
+
+/* simple thresholding (the same as in pamditherbw) */
+
+static void
+thresholdSimple(struct pam * const inpamP,
+                struct pam * const outpamP,
+                float        const threshold) {
+
+    tuplen * inrow;    /* normalized input row */
+    tuple * outrow;    /* raw output row */
+    unsigned int row;  /* number of the current row */
+
+    inrow  = pnm_allocpamrown(inpamP);
+    outrow = pnm_allocpamrow(outpamP);
+
+    /* do the simple thresholding */
+    for (row = 0; row < inpamP->height; ++row) {
+        unsigned int col;
+        pnm_readpamrown(inpamP, inrow);
+        for (col = 0; col < inpamP->width; ++col)
+            outrow[col][0] =
+                inrow[col][0] >= threshold ? PAM_BW_WHITE : PAM_BLACK;
+        pnm_writepamrow(outpamP, outrow);
+    }
+
+    pnm_freepamrow(inrow);
+    pnm_freepamrow(outrow);
+}
+
+
+
+static void
+analyzeDistribution(struct pam *          const inpamP,
+                    const unsigned int ** const histogramP,
+                    struct range *        const rangeP) {
+/*----------------------------------------------------------------------------
+   Find the distribution of the sample values -- minimum, maximum, and
+   how many of each value -- in input image *inpamP, whose file is
+   positioned to the raster.
+
+   Return the minimum and maximum as *rangeP and the frequency
+   distribution as *histogramP, an array such that histogram[i] is the
+   number of pixels that have sample value i.
+
+   Leave the file positioned to the raster.
+-----------------------------------------------------------------------------*/
+    unsigned int row;
+    tuple * inrow;
+    tuplen * inrown;
+    unsigned int * histogram;  /* malloced array */
+    unsigned int i;
+
+    pm_filepos rasterPos;      /* Position in input file of the raster */
+
+    pm_tell2(inpamP->file, &rasterPos, sizeof(rasterPos));
+
+    inrow = pnm_allocpamrow(inpamP);
+    inrown = pnm_allocpamrown(inpamP);
+    MALLOCARRAY(histogram, inpamP->maxval+1);
+    if (histogram == NULL)
+        pm_error("Unable to allocate space for %lu-entry histogram",
+                 inpamP->maxval+1);
+
+    /* Initialize histogram -- zero occurences of everything */
+    for (i = 0; i <= inpamP->maxval; ++i)
+        histogram[i] = 0;
+
+    initRange(rangeP);
+
+    for (row = 0; row < inpamP->height; ++row) {
+        unsigned int col;
+        pnm_readpamrow(inpamP, inrow);
+        pnm_normalizeRow(inpamP, inrow, NULL, inrown);
+        for (col = 0; col < inpamP->width; ++col) {
+            ++histogram[inrow[col][0]];
+            addToRange(rangeP, inrown[col][0]);
+        }
+    }
+    *histogramP = histogram;
+
+    pnm_freepamrow(inrow);
+    pnm_freepamrown(inrown);
+
+    pm_seek2(inpamP->file, &rasterPos, sizeof(rasterPos));
+}
+
+
+
+static void
+getLocalThreshold(tuplen **    const inrows,
+                  unsigned int const windowWidth,
+                  unsigned int const x,
+                  unsigned int const localWidth,
+                  unsigned int const localHeight,
+                  float        const darkness,
+                  float        const minSpread,
+                  samplen      const defaultThreshold,
+                  samplen *    const thresholdP) {
+/*----------------------------------------------------------------------------
+  Find a suitable threshold in local area around one pixel.
+
+  inrows[][] is a an array of 'windowWidth' pixels by 'localHeight'.
+
+  'x' is a column number within the window.
+
+  We look at the rectangle consisting of the 'localWidth' columns
+  surrounding x, all rows.  If x is near the left or right edge, we truncate
+  the window as needed.
+
+  We base the threshold on the local spread (difference between minimum
+  and maximum sample values in the local areas) and the 'darkness'
+  factor.  A higher 'darkness' gets a higher threshold.
+
+  If the spread is less than 'minSpread', we return 'defaultThreshold' and
+  'darkness' is irrelevant.
+
+  'localWidth' must be odd.
+-----------------------------------------------------------------------------*/
+    unsigned int const startCol = x >= localWidth/2 ? x - localWidth/2 : 0;
+
+    unsigned int col;
+    struct range localRange;
+
+    assert(localWidth % 2 == 1);  /* entry condition */
+
+    initRange(&localRange);
+
+    for (col = startCol; col <= x + localWidth/2 && col < windowWidth; ++col) {
+        unsigned int row;
+
+        for (row = 0; row < localHeight; ++row)
+            addToRange(&localRange, inrows[row][col][0]);
+    }
+
+    if (spread(localRange) < minSpread)
+        *thresholdP = defaultThreshold;
+    else
+        *thresholdP = localRange.min + darkness * spread(localRange);
+}
+
+
+
+static void
+thresholdLocalRow(struct pam *       const inpamP,
+                  tuplen **          const inrows,
+                  unsigned int       const localWidth,
+                  unsigned int       const windowHeight,
+                  unsigned int       const row,
+                  struct cmdlineInfo const cmdline,
+                  struct range       const globalRange,
+                  samplen            const globalThreshold,
+                  tuple *            const outrow) {
+
+    tuplen * const inrow = inrows[row % windowHeight];
+
+    float minSpread;
+    unsigned int col;
+
+    if (cmdline.dual)
+        minSpread = cmdline.contrast * spread(globalRange);
+    else
+        minSpread = 0.0;
+
+    for (col = 0; col < inpamP->width; ++col) {
+        samplen threshold;
+
+        getLocalThreshold(inrows, inpamP->width, col, localWidth, windowHeight,
+                          cmdline.threshold, minSpread, globalThreshold,
+                          &threshold);
+        
+        outrow[col][0] = inrow[col][0] >= threshold ? PAM_BW_WHITE : PAM_BLACK;
+    }
+}
+
+
+
+static void
+computeGlobalThreshold(struct pam *         const inpamP,
+                       const unsigned int * const histogram,
+                       struct range         const globalRange,
+                       float *              const thresholdP) {
+/*----------------------------------------------------------------------------
+   Compute the proper threshold to use for the image described by
+   *inpamP, whose file is positioned to the raster.
+
+   For our convenience:
+
+     'histogram' describes the frequency of occurence of the various sample
+     values in the image.
+
+     'globalRange' describes the range (minimum, maximum) of sample values
+     in the image.
+
+   Return the threshold (scaled to [0, 1]) as *thresholdP.
+
+   Leave the file positioned to the raster.
+-----------------------------------------------------------------------------*/
+    /* Found this algo in the wikipedia article "Thresholding (image
+       processing)"
+    */
+
+    float threshold;           /* threshold is iteratively determined */
+    float oldthreshold;        /* stop if oldthreshold==threshold */
+    unsigned int iter;         /* count of done iterations */
+
+    /* Use middle value (halfway between min and max) as initial threshold */
+    threshold = (globalRange.min + globalRange.max) / 2.0;
+
+    oldthreshold = -1.0;  /* initial value */
+    iter = 0; /* initial value */
+
+    /* adjust threshold to image */
+    while (fabs(oldthreshold - threshold) > 0.01 && iter < MAX_ITERATIONS) {
+        unsigned long white, black;   /* number of white, black pixels */
+        unsigned int row;
+
+        ++iter;
+
+        /* count black and white pixels */
+
+        for (row = 0, white = 0, black = 0; row < inpamP->height; ++row) {
+            unsigned int col;
+
+            for(col = 0; col < threshold * inpamP->maxval; ++col)
+                black += histogram[col];
+            for(; col <= inpamP->maxval; ++col)
+                white += histogram[col];
+        }
+
+        oldthreshold = threshold;
+
+        /* Use the weighted average of black and white pixels to calculate new
+           threshold
+        */
+        threshold =
+            (black * (globalRange.min + threshold) / 2.0 +
+             white * (threshold + globalRange.max) / 2.0) /
+            (black + white);
+    }
+
+    *thresholdP = threshold;
+}
+
+
+
+static void
+thresholdLocal(struct pam *       const inpamP,
+               struct pam *       const outpamP,
+               struct cmdlineInfo const cmdline) {
+/*----------------------------------------------------------------------------
+  Threshold the image described by *inpamP, whose file is positioned to the
+  raster, and output the resulting raster to the image described by
+  *outpamP.
+
+  Use local adaptive thresholding aka dynamic thresholding or dual
+  thresholding (global for low contrast areas, LAT otherwise)
+-----------------------------------------------------------------------------*/
+    struct range globalRange; /* Range of sample values in entire image */
+    tuplen ** inrows;
+        /* vertical window of image containing the local area.  This is
+           a ring of 'windowHeight' rows.  Row R of the image, when it is
+           in the window, is inrows[R % windowHeight].
+        */
+    unsigned int windowHeight;  /* size of 'inrows' window */
+    unsigned int nextRowToRead;
+        /* Number of the next row to be read from the file into the inrows[]
+           buffer.
+        */
+    tuple * outrow;           /* raw output row */
+    unsigned int row;
+        /* Number of the current row.  The current row is normally the
+           one in the center of the inrows[] buffer (which has an actual
+           center row because it is of odd height), but when near the top
+           and bottom edge of the image, it is not.
+        */
+    const unsigned int * histogram;
+    samplen globalThreshold;
+        /* This is a threshold based on the entire image, to use in areas
+           where the contrast is too small to use a locally-derived threshold.
+        */
+    unsigned int oddLocalWidth;
+    unsigned int oddLocalHeight;
+    unsigned int i;
+    
+    /* use a subimage with odd width and height to have a middle pixel */
+
+    if (cmdline.width % 2 == 0)
+        oddLocalWidth = cmdline.width + 1;
+    else 
+        oddLocalWidth = cmdline.width;
+    if (cmdline.height % 2 == 0)
+        oddLocalHeight = cmdline.height + 1;
+    else
+        oddLocalHeight = cmdline.height;
+
+    windowHeight = MIN(oddLocalHeight, inpamP->height);
+
+    analyzeDistribution(inpamP, &histogram, &globalRange);
+
+    computeGlobalThreshold(inpamP, histogram, globalRange, &globalThreshold);
+
+    outrow = pnm_allocpamrow(outpamP);
+
+    MALLOCARRAY(inrows, windowHeight);
+
+    if (inrows == NULL)
+        pm_error("Unable to allocate memory for a %u-row array", windowHeight);
+
+    for (i = 0; i < windowHeight; ++i)
+        inrows[i] = pnm_allocpamrown(inpamP);
+
+    /* Fill the vertical window buffer */
+    nextRowToRead = 0;
+
+    while (nextRowToRead < windowHeight)
+        pnm_readpamrown(inpamP, inrows[nextRowToRead++ % windowHeight]);
+
+    for (row = 0; row < inpamP->height; ++row) {
+        thresholdLocalRow(inpamP, inrows, oddLocalWidth, windowHeight, row,
+                          cmdline, globalRange, globalThreshold, outrow);
+
+        pnm_writepamrow(outpamP, outrow);
+        
+        /* read next image line if available and necessary */
+        if (row + windowHeight / 2 >= nextRowToRead &&
+            nextRowToRead < inpamP->height)
+            pnm_readpamrown(inpamP, inrows[nextRowToRead++ % windowHeight]);
+    }
+
+    free((void*)histogram);
+    for (i = 0; i < windowHeight; ++i)
+        pnm_freepamrow(inrows[i]);
+    free(inrows);
+    pnm_freepamrow(outrow);
+}
+
+
+
+static void
+thresholdIterative(struct pam * const inpamP,
+                   struct pam * const outpamP) {
+
+    const unsigned int * histogram;
+    struct range globalRange;
+    samplen threshold;
+
+    analyzeDistribution(inpamP, &histogram, &globalRange);
+
+    computeGlobalThreshold(inpamP, histogram, globalRange, &threshold);
+
+    pm_message("using global threshold %4.2f", threshold);
+
+    thresholdSimple(inpamP, outpamP, threshold);
+}
+
+
+
+int
+main(int argc, char **argv) {
+
+    FILE * ifP; 
+    struct cmdlineInfo cmdline;
+    struct pam inpam, outpam;
+    bool eof;  /* No more images in input stream */
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    if (cmdline.simple)
+        ifP = pm_openr(cmdline.inputFileName);
+    else
+        ifP = pm_openr_seekable(cmdline.inputFileName);
+
+    /* threshold each image in the PAM file */
+    eof = FALSE;
+    while (!eof) {
+        pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+        /* set output image parameters for a bilevel image */
+        outpam.size        = sizeof(outpam);
+        outpam.len         = PAM_STRUCT_SIZE(tuple_type);
+        outpam.file        = stdout;
+        outpam.format      = PAM_FORMAT;
+        outpam.plainformat = 0;
+        outpam.height      = inpam.height;
+        outpam.width       = inpam.width;
+        outpam.depth       = 1;
+        outpam.maxval      = 1;
+        outpam.bytes_per_sample = 1;
+        strcpy(outpam.tuple_type, "BLACKANDWHITE");
+
+        pnm_writepaminit(&outpam);
+
+        /* do the thresholding */
+
+        if (cmdline.simple)
+            thresholdSimple(&inpam, &outpam, cmdline.threshold);
+        else if (cmdline.local || cmdline.dual)
+            thresholdLocal(&inpam, &outpam, cmdline);
+        else
+            thresholdIterative(&inpam, &outpam);
+
+        pnm_nextimage(ifP, &eof);
+    }
+
+    pm_close(ifP);
+
+    return 0;
+}
diff --git a/editor/pbmclean.c b/editor/pbmclean.c
new file mode 100644
index 00000000..3ae3acfc
--- /dev/null
+++ b/editor/pbmclean.c
@@ -0,0 +1,239 @@
+/* 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
+ */
+
+#include <stdio.h>
+#include "pbm.h"
+#include "shhopt.h"
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char *inputFilespec;  /* Filespecs of input files */
+    bool flipWhite;
+    bool flipBlack;
+    unsigned int connect;
+    unsigned int verbose;
+};
+
+#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,
+                 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));
+    unsigned int option_def_index;
+
+    unsigned int black, white;
+    unsigned int minneighborsSpec;
+
+    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, 
+            &minneighborsSpec, 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);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    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
+           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 */
+        foundNegative = FALSE;
+
+        for (i = 1; i < argc; ++i) {
+            if (foundNegative)
+                argv[i-1] = argv[i];
+            else {
+                if (atoi(argv[i]) < 0) {
+                    cmdlineP->connect = - atoi(argv[i]);
+                    foundNegative = TRUE;
+                }
+            }
+        }
+        if (foundNegative)
+            --argc;
+    }
+
+    if (argc-1 < 1) 
+        cmdlineP->inputFilespec = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFilespec = argv[1];
+    else
+        pm_error("You specified too many arguments (%d).  The only "
+                 "argument is the optional input file specification.",
+                 argc-1);
+}
+
+
+
+
+
+static void 
+nextrow(FILE * const ifd,
+        int    const row,
+        int    const cols,
+        int    const rows,
+        int    const format) {
+/*----------------------------------------------------------------------------
+   Advance one row in the input.
+
+   'row' is the row number that will be the current row.
+-----------------------------------------------------------------------------*/
+    bit * shuffle;
+
+    /* 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; 
+        }
+    }
+}
+
+
+
+static unsigned int
+likeNeighbors(bit *        const inrow[3], 
+              unsigned int const col, 
+              unsigned int const cols) {
+    
+    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++ ;
+    }
+    return joined;
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    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 */
+
+    pbm_init( &argc, argv );
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifp = pm_openr(cmdline.inputFilespec);
+
+    inrow[0] = inrow[1] = inrow[2] = NULL;
+    pbm_readpbminit(ifp, &cols, &rows, &format);
+
+    outrow = pbm_allocrow(cols);
+
+    pbm_writepbminit(stdout, cols, rows, 0) ;
+
+    nFlipped = 0;  /* No pixels flipped yet */
+    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;
+        }
+        pbm_writepbmrow(stdout, outrow, cols, 0) ;
+    }
+    pbm_freerow(outrow);
+    pm_close(ifp);
+
+    if (cmdline.verbose)
+        pm_message("%d pixels flipped", nFlipped);
+
+    return 0;
+}
diff --git a/editor/pbmlife.c b/editor/pbmlife.c
new file mode 100644
index 00000000..be34cc69
--- /dev/null
+++ b/editor/pbmlife.c
@@ -0,0 +1,114 @@
+/* pbmlife.c - read a portable bitmap and apply Conway's rules of Life to it
+**
+** Copyright (C) 1988,1 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.
+*/
+
+#include "pbm.h"
+
+int
+main( argc, argv )
+int argc;
+char* argv[];
+    {
+    FILE* ifp;
+    bit* prevrow;
+    bit* thisrow;
+    bit* nextrow;
+    bit* temprow;
+    register bit* newrow;
+    int rows, cols, row;
+    register int col, count;
+    int format;
+
+
+    pbm_init( &argc, argv );
+
+    if ( argc > 2 )
+	pm_usage( "[pbmfile]" );
+
+    if ( argc == 2 )
+	ifp = pm_openr( argv[1] );
+    else
+	ifp = stdin;
+
+    pbm_readpbminit( ifp, &cols, &rows, &format );
+    prevrow = pbm_allocrow( cols );
+    thisrow = pbm_allocrow( cols );
+    nextrow = pbm_allocrow( cols );
+
+    pbm_writepbminit( stdout, cols, rows, 0 );
+    newrow = pbm_allocrow( cols );
+
+    pbm_readpbmrow( ifp, nextrow, cols, format );
+
+    for ( row = 0; row < rows; ++row )
+	{
+	temprow = prevrow;
+	prevrow = thisrow;
+	thisrow = nextrow;
+	nextrow = temprow;
+	if ( row < rows - 1 )
+	    pbm_readpbmrow( ifp, nextrow, cols, format );
+
+        for ( col = 0; col < cols; ++col )
+	    {
+	    /* Check the neighborhood, with an unrolled double loop. */
+	    count = 0;
+	    if ( row > 0 )
+		{
+		/* upper left */
+		if ( col > 0 && prevrow[col - 1] == PBM_WHITE )
+		    ++count;
+		/* upper center */
+		if ( prevrow[col] == PBM_WHITE )
+		    ++count;
+		/* upper right */
+		if ( col < cols - 1 && prevrow[col + 1] == PBM_WHITE )
+		    ++count;
+		}
+	    /* left */
+	    if ( col > 0 && thisrow[col - 1] == PBM_WHITE )
+		++count;
+	    /* right */
+	    if ( col < cols - 1 && thisrow[col + 1] == PBM_WHITE )
+		++count;
+	    if ( row < rows - 1 )
+		{
+		/* lower left */
+		if ( col > 0 && nextrow[col - 1] == PBM_WHITE )
+		    ++count;
+		/* lower center */
+		if ( nextrow[col] == PBM_WHITE )
+		    ++count;
+		/* lower right */
+		if ( col < cols - 1 && nextrow[col + 1] == PBM_WHITE )
+		    ++count;
+		}
+
+	    /* And compute the new value. */
+	    if ( thisrow[col] == PBM_WHITE )
+		if ( count == 2 || count == 3 )
+		    newrow[col] = PBM_WHITE;
+		else
+		    newrow[col] = PBM_BLACK;
+	    else
+		if ( count == 3 )
+		    newrow[col] = PBM_WHITE;
+		else
+		    newrow[col] = PBM_BLACK;
+	    }
+	pbm_writepbmrow( stdout, newrow, cols, 0 );
+	}
+
+    pm_close( ifp );
+    pm_close( stdout );
+
+    exit( 0 );
+    }
diff --git a/editor/pbmmask.c b/editor/pbmmask.c
new file mode 100644
index 00000000..21ada6b9
--- /dev/null
+++ b/editor/pbmmask.c
@@ -0,0 +1,222 @@
+/* pbmmask.c - create a mask bitmap from a portable bitmap
+**
+** 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.
+*/
+
+#include "pbm.h"
+#include "mallocvar.h"
+
+static bit ** bits;
+static bit ** mask;
+static bit backcolor;
+static int rows, cols;
+
+
+
+static short * fcols;
+static short * frows;
+static int fstacksize = 0;
+static int fstackp = 0;
+
+
+
+static void
+addflood(int const col,
+         int const row) {
+
+    if ( bits[row][col] == backcolor && mask[row][col] == PBM_BLACK ) {
+        if ( fstackp >= fstacksize ) {
+            if ( fstacksize == 0 ) {
+                fstacksize = 1000;
+                MALLOCARRAY(fcols, fstacksize);
+                MALLOCARRAY(frows, fstacksize);
+                if ( fcols == NULL || frows == NULL )
+                    pm_error( "out of memory" );
+            } else {
+                fstacksize *= 2;
+                fcols = (short*) realloc(
+                    (char*) fcols, fstacksize * sizeof(short) );
+                frows = (short*) realloc(
+                    (char*) frows, fstacksize * sizeof(short) );
+                if ( fcols == (short*) 0 || frows == (short*) 0 )
+                    pm_error( "out of memory" );
+            }
+        }
+        fcols[fstackp] = col;
+        frows[fstackp] = row;
+        ++fstackp;
+    }
+}
+
+
+
+static void
+flood(void) {
+
+    while ( fstackp > 0 ) {
+        int col, row;
+        --fstackp;
+        col = fcols[fstackp];
+        row = frows[fstackp];
+        if ( bits[row][col] == backcolor && mask[row][col] == PBM_BLACK ) {
+            int c;
+            mask[row][col] = PBM_WHITE;
+            if ( row - 1 >= 0 )
+                addflood( col, row - 1 );
+            if ( row + 1 < rows )
+                addflood( col, row + 1 );
+            for ( c = col + 1; c < cols; ++c ) {
+                if ( bits[row][c] == backcolor && mask[row][c] == PBM_BLACK ) {
+                    mask[row][c] = PBM_WHITE;
+                    if ( row - 1 >= 0 && 
+                         ( bits[row - 1][c - 1] != backcolor || 
+                           mask[row - 1][c - 1] != PBM_BLACK ) )
+                        addflood( c, row - 1 );
+                    if ( row + 1 < rows && 
+                         ( bits[row + 1][c - 1] != backcolor || 
+                           mask[row + 1][c - 1] != PBM_BLACK ) )
+                        addflood( c, row + 1 );
+                }
+                else
+                    break;
+            }
+            for ( c = col - 1; c >= 0; --c ) {
+                if ( bits[row][c] == backcolor && mask[row][c] == PBM_BLACK ) {
+                    mask[row][c] = PBM_WHITE;
+                    if ( row - 1 >= 0 && 
+                         ( bits[row - 1][c + 1] != backcolor || 
+                           mask[row - 1][c + 1] != PBM_BLACK ) )
+                        addflood( c, row - 1 );
+                    if ( row + 1 < rows && 
+                         ( bits[row + 1][c + 1] != backcolor || 
+                           mask[row + 1][c + 1] != PBM_BLACK ) )
+                        addflood( c, row + 1 );
+                } else
+                    break;
+            }
+        }
+    }
+}
+
+
+
+int
+main(int argc, char * argv[]) {
+
+    FILE* ifp;
+    int argn, expand, wcount;
+    register int row, col;
+    const char* const usage = "[-expand] [pbmfile]";
+
+    pbm_init( &argc, argv );
+
+    argn = 1;
+    expand = 0;
+
+    if ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' )
+    {
+        if ( pm_keymatch( argv[argn], "-expand", 2 ) )
+            expand = 1;
+        else if ( pm_keymatch( argv[argn], "-noexpand", 2 ) )
+            expand = 0;
+        else
+            pm_usage( usage );
+        ++argn;
+    }
+
+    if ( argn == argc )
+        ifp = stdin;
+    else
+    {
+        ifp = pm_openr( argv[argn] );
+        ++argn;
+    }
+
+    if ( argn != argc )
+        pm_usage( usage );
+
+    bits = pbm_readpbm( ifp, &cols, &rows );
+    pm_close( ifp );
+    mask = pbm_allocarray( cols, rows );
+
+    /* Clear out the mask. */
+    for ( row = 0; row < rows; ++row )
+        for ( col = 0; col < cols; ++col )
+            mask[row][col] = PBM_BLACK;
+
+    /* Figure out the background color, by counting along the edge. */
+    wcount = 0;
+    for ( row = 0; row < rows; ++row ) {
+        if ( bits[row][0] == PBM_WHITE )
+            ++wcount;
+        if ( bits[row][cols - 1] == PBM_WHITE )
+            ++wcount;
+    }
+    for ( col = 1; col < cols - 1; ++col ) {
+        if ( bits[0][col] == PBM_WHITE )
+            ++wcount;
+        if ( bits[rows - 1][col] == PBM_WHITE )
+            ++wcount;
+    }
+    if ( wcount >= rows + cols - 2 )
+        backcolor = PBM_WHITE;
+    else
+        backcolor = PBM_BLACK;
+
+    /* Flood the entire edge.  Probably the first call will be enough, but
+       might as well be sure.
+    */
+    for ( col = cols - 3; col >= 2; col -= 2 ) {
+        addflood( col, rows - 1 );
+        addflood( col, 0 );
+    }
+    for ( row = rows - 1; row >= 0; row -= 2 ) {
+        addflood( cols - 1, row );
+        addflood( 0, row );
+    }
+    flood( );
+
+    if ( ! expand )
+        /* Done. */
+        pbm_writepbm( stdout, mask, cols, rows, 0 );
+    else {
+        /* Expand by one pixel. */
+        int srow, scol;
+        unsigned int row;
+        bit ** emask;
+
+        emask = pbm_allocarray( cols, rows );
+
+        for ( row = 0; row < rows; ++row ) {
+            unsigned int col;
+            for ( col = 0; col < cols; ++col )
+                if ( mask[row][col] == PBM_BLACK )
+                    emask[row][col] = PBM_BLACK;
+                else {
+                    emask[row][col] = PBM_WHITE;
+                    for ( srow = row - 1; srow <= row + 1; ++srow )
+                        for ( scol = col - 1; scol <= col + 1; ++scol )
+                            if ( srow >= 0 && srow < rows &&
+                                 scol >= 0 && scol < cols &&
+                                 mask[srow][scol] == PBM_BLACK ) {
+
+                                emask[row][col] = PBM_BLACK;
+                                break;
+                            }
+                }
+        }
+        pbm_writepbm( stdout, emask, cols, rows, 0 );
+    }
+
+    pm_close( stdout );
+
+    return 0;
+}
+
diff --git a/editor/pbmpscale.c b/editor/pbmpscale.c
new file mode 100644
index 00000000..63f203ed
--- /dev/null
+++ b/editor/pbmpscale.c
@@ -0,0 +1,199 @@
+/* pbmpscale.c - pixel scaling with jagged edge smoothing.
+ * AJCD 13/8/90
+ */
+
+#include <stdio.h>
+#include "pbm.h"
+#include "mallocvar.h"
+
+/* prototypes */
+void nextrow_pscale ARGS((FILE *ifd, int row));
+int corner ARGS((int pat));
+
+/* input bitmap size and storage */
+int rows, columns, format ;
+bit *inrow[3] ;
+
+#define thisrow (1)
+
+/* 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)
+#define SE(f) (((f) >> 2) & 3)
+#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
+ */
+
+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;
+}
+
+/* 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 */
+
+}
+
+int
+main(argc, argv)
+     int argc;
+     char *argv[];
+{
+   FILE *ifd;
+   register bit *outrow;
+   register int row, col, i, k;
+   int scale, cutoff, ucutoff ;
+   unsigned char *flags;
+
+   pbm_init( &argc, argv );
+
+   if (argc < 2)
+      pm_usage("scale [pbmfile]");
+
+   scale = atoi(argv[1]);
+   if (scale < 1)
+      pm_perror("bad scale (< 1)");
+
+   if (argc == 3)
+      ifd = pm_openr(argv[2]);
+   else
+      ifd = stdin ;
+
+   inrow[0] = inrow[1] = inrow[2] = NULL;
+   pbm_readpbminit(ifd, &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) ;
+
+   cutoff = scale / 2;
+   ucutoff = scale - 1 - cutoff;
+   nextrow_pscale(ifd, 0);
+   for (row = 0; row < rows; row++) {
+      nextrow_pscale(ifd, row+1);
+      for (col = 0; col < columns; col++) {
+         flags[col] = 0 ;
+         for (i = 0; i != 8; i += 2) {
+            int vec = inrow[thisrow][col] != PBM_WHITE;
+            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 ;
+         }
+      }
+      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 ;
+
+            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 */
+              }
+               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 */
+               }
+               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++)  /* centre part */
+               *ptr++ = pix ;
+            for (k = 0; k < cutr; k++) /* right part */
+               *ptr++ = !pix ;
+         }
+         pbm_writepbmrow(stdout, outrow, scale*columns, 0) ;
+      }
+   }
+   pm_close(ifd);
+   exit(0);
+}
diff --git a/editor/pbmreduce.c b/editor/pbmreduce.c
new file mode 100644
index 00000000..15ec2a1b
--- /dev/null
+++ b/editor/pbmreduce.c
@@ -0,0 +1,208 @@
+/* pbmreduce.c - read a portable bitmap and reduce it N times
+**
+** Copyright (C) 1989 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.
+*/
+
+#include "pbm.h"
+#include "mallocvar.h"
+
+int
+main( argc, argv )
+    int argc;
+    char* argv[];
+    {
+    FILE* ifp;
+    register bit** bitslice;
+    register bit* newbitrow;
+    register bit* nbP;
+    int argn, n, rows, cols, format, newrows, newcols;
+    int row, col, limitcol, subrow, subcol, count, direction;
+    const char* const usage = "[-floyd|-fs | -threshold] [-value <val>] N [pbmfile]";
+    int halftone;
+#define QT_FS 1
+#define QT_THRESH 2
+#define SCALE 1024
+#define HALFSCALE 512
+    long threshval, sum;
+    long* thiserr;  /* used for Floyd-Steinberg stuff */
+    long* nexterr;  /* used for Floyd-Steinberg stuff */
+    long* temperr;
+
+
+    pbm_init( &argc, argv );
+
+    argn = 1;
+    halftone = QT_FS;
+    threshval = HALFSCALE;
+
+    while ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' )
+	{
+	if ( pm_keymatch( argv[argn], "-fs", 2 ) ||
+	     pm_keymatch( argv[argn], "-floyd", 2 ) )
+	    halftone = QT_FS;
+	else if ( pm_keymatch( argv[argn], "-threshold", 2 ) )
+	    halftone = QT_THRESH;
+	else if ( pm_keymatch( argv[argn], "-value", 2 ) )
+	    {
+	    float f;
+
+	    ++argn;
+	    if ( argn == argc || sscanf( argv[argn], "%f", &f ) != 1 ||
+		 f < 0.0 || f > 1.0 )
+		pm_usage( usage );
+	    threshval = f * SCALE;
+	    }
+	else
+	    pm_usage( usage );
+	++argn;
+	}
+
+    if ( argn == argc )
+	pm_usage( usage );
+    if ( sscanf( argv[argn], "%d", &n ) != 1 )
+	pm_usage( usage );
+    if ( n < 2 )
+	pm_error( "N must be greater than 1" );
+    ++argn;
+
+    if ( argn == argc )
+	ifp = stdin;
+    else
+	{
+	ifp = pm_openr( argv[argn] );
+	++argn;
+	}
+
+    if ( argn != argc )
+	pm_usage( usage );
+
+    pbm_readpbminit( ifp, &cols, &rows, &format );
+    bitslice = pbm_allocarray( cols, n );
+
+    newrows = rows / n;
+    newcols = cols / n;
+    pbm_writepbminit( stdout, newcols, newrows, 0 );
+    newbitrow = pbm_allocrow( newcols );
+
+    if ( halftone == QT_FS ) {
+        /* Initialize Floyd-Steinberg. */
+        MALLOCARRAY(thiserr, newcols + 2);
+        MALLOCARRAY(nexterr, newcols + 2);
+        if ( thiserr == NULL || nexterr == NULL )
+          pm_error( "out of memory" );
+
+        srand( (int) ( time( 0 ) ^ getpid( ) ) );
+        for ( col = 0; col < newcols + 2; ++col )
+          thiserr[col] = ( rand( ) % SCALE - HALFSCALE ) / 4;
+	    /* (random errors in [-SCALE/8 .. SCALE/8]) */
+	} else {
+        /* These variables are meaningless in this case, and the values
+           should never be used.
+           */
+        thiserr = NULL;
+        nexterr = NULL;
+    }
+    direction = 1;
+
+    for ( row = 0; row < newrows; ++row )
+	{
+	for ( subrow = 0; subrow < n; ++subrow )
+	    pbm_readpbmrow( ifp, bitslice[subrow], cols, format );
+
+	if ( halftone == QT_FS )
+	    for ( col = 0; col < newcols + 2; ++col )
+		nexterr[col] = 0;
+	if ( direction )
+	    {
+	    col = 0;
+	    limitcol = newcols;
+	    nbP = newbitrow;
+	    }
+	else
+	    {
+	    col = newcols - 1;
+	    limitcol = -1;
+	    nbP = &(newbitrow[col]);
+	    }
+
+	do
+	    {
+	    sum = 0;
+	    count = 0;
+	    for ( subrow = 0; subrow < n; ++subrow )
+		for ( subcol = 0; subcol < n; ++subcol )
+		    if ( row * n + subrow < rows && col * n + subcol < cols )
+			{
+			count += 1;
+			if ( bitslice[subrow][col * n + subcol] == PBM_WHITE )
+			    sum += 1;
+			}
+	    sum = ( sum * SCALE ) / count;
+
+	    if ( halftone == QT_FS )
+		sum += thiserr[col + 1];
+
+	    if ( sum >= threshval )
+		{
+		*nbP = PBM_WHITE;
+		if ( halftone == QT_FS )
+		    sum = sum - threshval - HALFSCALE;
+		}
+	    else
+		*nbP = PBM_BLACK;
+
+	    if ( halftone == QT_FS )
+		{
+		if ( direction )
+		    {
+		    thiserr[col + 2] += ( sum * 7 ) / 16;
+		    nexterr[col    ] += ( sum * 3 ) / 16;
+		    nexterr[col + 1] += ( sum * 5 ) / 16;
+		    nexterr[col + 2] += ( sum     ) / 16;
+		    }
+		else
+		    {
+		    thiserr[col    ] += ( sum * 7 ) / 16;
+		    nexterr[col + 2] += ( sum * 3 ) / 16;
+		    nexterr[col + 1] += ( sum * 5 ) / 16;
+		    nexterr[col    ] += ( sum     ) / 16;
+		    }
+		}
+	    if ( direction )
+		{
+		++col;
+		++nbP;
+		}
+	    else
+		{
+		--col;
+		--nbP;
+		}
+	    }
+	while ( col != limitcol );
+
+	pbm_writepbmrow( stdout, newbitrow, newcols, 0 );
+
+	if ( halftone == QT_FS )
+	    {
+	    temperr = thiserr;
+	    thiserr = nexterr;
+	    nexterr = temperr;
+	    direction = ! direction;
+	    }
+	}
+
+    pm_close( ifp );
+    pm_close( stdout );
+
+    exit( 0 );
+    }
+
+
diff --git a/editor/pgmabel.c b/editor/pgmabel.c
new file mode 100644
index 00000000..4914c4be
--- /dev/null
+++ b/editor/pgmabel.c
@@ -0,0 +1,316 @@
+/* pgmabel.c - read a portable graymap and making the deconvolution
+**
+**      Deconvolution of an axial-symmetric image of an rotation symmetrical
+**      process by solving the linear equation system with y-Axis as
+**      symmetry-line
+**
+** Copyright (C) 1997-2006 by German Aerospace Research establishment
+**
+** Author: Volker Schmidt
+**         lefti@voyager.boerde.de
+**
+** 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.
+**
+** $HISTORY:
+**
+** 24 Jan 2002 : 001.009 :  some optimzization
+** 22 Jan 2002 : 001.008 :  some stupid calculations changed
+** 08 Aug 2001 : 001.007 :  new usage (netpbm-conform)
+** 27 Jul 1998 : 001.006 :  First try of error correction
+** 26 Mar 1998 : 001.005 :  Calculating the dl's before transformation
+** 06 Feb 1998 : 001.004 :  Include of a logo in the upper left edge
+** 26 Nov 1997 : 001.003 :  Some bug fixes and reading only lines
+** 25 Nov 1997 : 001.002 :  include of pixsize for getting scale invariant
+** 03 Sep 1997 : 001.001 :  only define for PID2
+** 03 Sep 1997 : 001.000 :  First public release
+** 21 Aug 1997 : 000.909 :  Recalculate the streching-factor
+** 20 Aug 1997 : 000.908 :  -left and -right for calculating only one side
+** 20 Aug 1997 : 000.906 :  correction of divisor, include of -factor
+** 15 Aug 1997 : 000.905 :  Include of -help and -axis
+*/
+
+static const char* const version="$VER: pgmabel 1.009 (24 Jan 2002)";
+
+#include <math.h>
+#include <stdlib.h>   /* for calloc */
+#include "pgm.h"
+#include "mallocvar.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 */
+
+/* ----------------------------------------------------------------------------
+** procedure for calculating the sum of the calculated surfaces with the
+** weight of the surface
+**      n     <-  index of end point of the summation
+**      N     <-  width of the calculated row
+**      xr    <-  array of the calculated elements of the row
+**      adl   <-  pre-calculated surface coefficient for each segment
+*/
+static double 
+Sum ( int n, double *xr, int N, double *adl)
+{
+    int k;
+    double result=0.0;
+
+    if (n==0) return(0.0);             /* outer ring  is 0 per definition    */
+    for (k=0 ; k<=(n-1) ; k++)
+    {
+         result += xr[k] * ( adl[k*N+n] - adl[(k+1)*N+n]);
+/*       result += xr[k] * ( dr(k,n+0.5,N) - dr(k+1,n+0.5,N));   */
+    }
+    return(result);
+}
+
+/* ----------------------------------------------------------------------------
+** procedure for calculating the surface coefficient for the Integration
+**      R, N  <-  indizes of the coefficient
+**      r     <-  radial position of the center of the surface
+*/
+static double 
+dr ( int R, double r,  int N)
+{
+    double a;
+    double b;
+    a=(double) N-R ;
+    b=(double) N-r ;
+    return(sqrt(a*a-b*b));
+}
+
+/* ----------------------------------------------------------------------------
+** procedure for making the Abel integration for deconvolution of the image
+**        y    <-> array with values for deconvolution and results
+**        N    <-  width of the array
+**        adl  <-  array with pre-calculated weighting factors
+*/
+static void 
+abel ( float *y, int N, double *adl)
+{
+    register int n;
+    double *rho, *rhop;       /* results and new index                       */
+    float  *yp;               /* new indizes for the y-array                 */
+
+    MALLOCARRAY(rho, N);
+    if( !rho )
+        pm_error( "out of memory" );
+    rhop = rho;
+    yp  = y;
+
+    for (n=0 ; n<N ; n++)
+    {
+        *(rhop++) = ((*yp++) - Sum(n,rho,N,adl))/(adl[n*N+n]);
+/*    *(rhop++) = ((*yp++) - Sum(n,rho,N))/(dr(n,n+0.5,N));  old version */
+        if ( *rhop < 0.0 ) *rhop = 0.0;         /*  error correction !       */
+/*   if (n > 2) rhop[n-1] = (rho[n-2]+rho[n-1]+rho[n])/3.0;  stabilization*/
+    }
+    for (n=0 ; n<N ; n++)
+        {
+            if (( n>=1 )&&( n<N-1 ))
+	       (*y++) = ((rho[n-1]*0.5+rho[n]+rho[n+1]*0.5)/2.0);/*1D median filter*/
+            else (*y++) = rho[n];
+        }
+    free(rho);
+}
+
+/* ----------------------------------------------------------------------------
+** printing a help message if Option -h(elp) is chosen
+*/
+static void 
+help()
+{
+    pm_message("-----------------------------------------------------------------");
+    pm_message("| pgmabel                                                       |");
+    pm_message("| make a deconvolution with vertical axis as symmetry-line      |");
+    pm_message("| usage:                                                        |");
+    pm_message("| pgmabel [-help] [-axis N] [-factor N] [-left|-right]          |");
+    pm_message("|         [-pixsize] [-verbose] [pgmfile]                       |");
+    pm_message("|   axis    : horizontal position of the axis                   |");
+    pm_message("|   factor  : user defines stretch-factor for the gray levels   |");
+    pm_message("|   pixsize : size of one pixel in mm (default = 0.1)           |");
+    pm_message("|   left    : calculating only the left (or right) side         |");
+    pm_message("|   verbose : output of useful data                             |");
+    pm_message("|   pgmfile : Name of a pgmfile (optional)                      |");
+    pm_message("|                                                               |");
+    pm_message("| for further information please contact the manpage            |"); 
+    pm_message("-----------------------------------------------------------------");
+    pm_message("%s",version);     /* telling the version      */
+    exit(-1);                     /* retur-code for no result */
+}
+
+
+
+
+
+/* ----------------------------------------------------------------------------
+** main program
+*/
+int main( argc, argv )
+    int    argc;
+    char*  argv[];
+{
+    FILE*  ifp;
+    gray maxval;                            /* maximum gray-level            */
+    gray* grayorig;
+    gray* grayrow;                          /* one line in the image         */
+    int argn, rows, cols, row, format;
+    int col, midcol=0, temp, tc;
+    float *trow;                          /* temporary row for deconvolution */
+    float l_div, r_div, fac=1.0, cfac=4.0;  /* factor for scaling gray-level */
+    float pixsize=0.1;
+    /* no verbose, calculating both sides                                */
+    int verb = FALSE, left = TRUE, right = TRUE;
+    int nologo = FALSE;
+    const char* const usage = "[-help] [-axis N] [-factor N] [-pixsize N] [-left|-right] [-verbose] [pgmfile]";
+
+    pgm_init( &argc, argv );
+    argn = 1;
+
+    /* Check for flags. */
+    while ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' )
+        {
+        if ( pm_keymatch( argv[argn], "-help", 1 ) ) help();
+        else if ( pm_keymatch( argv[argn], "-axis", 1 ) )
+            {
+            ++argn;
+            if ( argn == argc || sscanf( argv[argn], "%i", &midcol ) !=1 )
+                pm_usage( usage );
+            }
+        else if ( pm_keymatch( argv[argn], "-factor", 1 ) )
+            {
+            ++argn;
+            if ( argn == argc || sscanf( argv[argn], "%f", &fac ) !=1 )
+                pm_usage( usage );
+            }
+        else if ( pm_keymatch( argv[argn], "-pixsize", 1 ) )
+            {
+            ++argn;
+            if ( argn == argc || sscanf( argv[argn], "%f", &pixsize ) !=1 )
+                pm_usage( usage );
+            }
+        else if ( pm_keymatch( argv[argn], "-verbose", 1 ) )
+            {
+                verb = TRUE;
+            }
+        else if ( pm_keymatch( argv[argn], "-left", 1 ) )
+            {
+                if ( left ) right = FALSE;
+                else pm_usage( usage );
+            }
+        else if ( pm_keymatch( argv[argn], "-right", 1 ) )
+           {
+                if ( right ) left = FALSE;
+                else pm_usage( usage );
+            }
+        else if ( pm_keymatch( argv[argn], "-nologo", 4 ) )
+            {
+                nologo = TRUE;
+            }
+        else
+            pm_usage( usage );
+        ++ argn;
+        }
+    if ( argn < argc )
+        {
+        ifp = pm_openr( argv[argn] );                    /* open the picture */
+        ++argn;
+        }
+    else
+        ifp = stdin;                                /* or reading from STDIN */
+    if ( argn != argc )
+        pm_usage( usage );
+
+    pgm_readpgminit( ifp, &cols, &rows, &maxval, &format );  /* read picture  */
+    pgm_writepgminit( stdout, cols, rows, maxval, 0 );  /* write the header  */
+    grayorig = pgm_allocrow(cols);
+    grayrow = pgm_allocrow( cols );                     /* allocate a row    */
+
+    if (midcol == 0) midcol = cols/2;     /* if no axis set take the center */
+    if (left ) l_div = (float)(PID2*pixsize)/(cfac*fac);
+    else l_div=1.0;                              /* weighting the left side  */
+    if (right) r_div = (float)(PID2*pixsize)/(cfac*fac);
+    else r_div=1.0;                              /* weighting the right side */
+
+    if (verb)
+    {
+        pm_message("%s",version);
+        pm_message("Calculating a portable graymap with %i rows and %i cols",rows,cols);
+        pm_message("  resuming a pixelsize of %f mm",pixsize);
+        if ( !right ) pm_message("     only the left side!");
+        if ( !left ) pm_message("     only the right side!");
+        pm_message("  axis = %i, stretching factor = %f",midcol,cfac*fac);
+        if ( left ) pm_message("  left side weighting = %f",l_div);
+        if ( right ) pm_message(" right side weighting = %f",r_div);
+    }
+
+    /* allocating the memory for the arrays aldl and ardl                    */
+    aldl = calloc ( midcol*midcol, sizeof(double));
+    if( !aldl )
+        pm_error( "out of memory" );
+    ardl = calloc ( (cols-midcol)*(cols-midcol), sizeof(double));
+    if( !ardl )
+        pm_error( "out of memory" );
+
+    MALLOCARRAY(trow, cols);
+    if( !trow )
+        pm_error( "out of memory" );
+
+    /* now precalculating the weighting-factors for the abel-transformation  */
+    for (col = 0; col < midcol; ++col)             /* factors for left side  */
+    {
+        for (tc = 0; tc < midcol; ++tc) aldl[col*midcol+tc] = dr(col,tc+0.5,midcol);
+    }
+    for (col = 0; col < (cols-midcol); ++col)      /* factors for right side */
+    {
+        for (tc = 0; tc < (cols-midcol); ++tc) 
+            ardl[col*(cols-midcol)+tc] = dr(col,tc+0.5,cols-midcol);
+    }
+
+    /* abel-transformation for each row splitted in right and left side      */
+    for ( row = 0; row < rows ; ++row )
+    {
+        pgm_readpgmrow( ifp, grayorig, cols, maxval, format );
+        for ( col = 0; col < midcol; ++col)          /* left side            */
+        {
+            trow[col] = (float) (grayorig[col]);
+        }
+        if (left ) abel(trow, midcol, aldl);         /* deconvolution        */
+        for ( col = 0; col < midcol; ++col)          /* writing left side    */
+        {
+            temp = (int)(trow[col]/l_div);
+            grayrow[col] = (temp>0?temp:0);
+        }
+        for ( col = midcol; col < cols; ++col )      /* right side           */
+        {
+            trow[cols-col-1] = (float) (grayorig[col]);
+        }
+        if ( right ) abel(trow,(cols-midcol),ardl);  /* deconvolution        */
+        for ( col = midcol; col < cols; ++col)       /* writing right side   */
+        {
+            temp = (int)(trow[cols-col-1]/r_div);
+            temp = (temp>0?temp:0);
+            grayrow[col] = temp;
+        }
+        pgm_writepgmrow( stdout, grayrow, cols, maxval, 0 );  /* saving row  */
+    }
+    pm_close( ifp );
+    pm_close( stdout );               /* closing output                      */
+    free( trow );                     /* deconvolution is done, clear memory */
+    pgm_freerow( grayorig );
+    pgm_freerow( grayrow );
+    free(aldl);
+    free(ardl);                      /* all used memory freed (i hope)       */
+    exit( 0 );                       /* end of procedure                     */
+}
+
diff --git a/editor/pgmbentley.c b/editor/pgmbentley.c
new file mode 100644
index 00000000..9cc86a91
--- /dev/null
+++ b/editor/pgmbentley.c
@@ -0,0 +1,64 @@
+/* pgmbentley.c - read a portable graymap and smear it according to brightness
+**
+** Copyright (C) 1990 by Wilson Bent (whb@hoh-2.att.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 "pgm.h"
+
+int
+main( argc, argv )
+    int argc;
+    char* argv[];
+    {
+    FILE* ifp;
+    gray maxval;
+    gray** gin;
+    gray** gout;
+    int argn, rows, cols, row;
+    register int brow, col;
+    const char* const usage = "[pgmfile]";
+
+
+    pgm_init( &argc, argv );
+
+    argn = 1;
+
+    if ( argn < argc )
+	{
+	ifp = pm_openr( argv[argn] );
+	++argn;
+	}
+    else
+	ifp = stdin;
+
+    if ( argn != argc )
+	pm_usage( usage );
+
+    gin = pgm_readpgm( ifp, &cols, &rows, &maxval );
+    pm_close( ifp );
+    gout = pgm_allocarray( cols, rows );
+
+#define N 4
+    for ( row = 0; row < rows; ++row )
+	for ( col = 0; col < cols; ++col )
+	    {
+	    brow = row + (int) (gin[row][col]) / N;
+	    if ( brow >= rows )
+		brow = rows - 1;
+	    gout[brow][col] = gin[row][col];
+	    }
+
+    pgm_writepgm( stdout, gout, cols, rows, maxval, 0 );
+    pm_close( stdout );
+    pgm_freearray( gout, rows );
+
+    exit( 0 );
+    }
diff --git a/editor/pgmdeshadow.c b/editor/pgmdeshadow.c
new file mode 100644
index 00000000..482c6661
--- /dev/null
+++ b/editor/pgmdeshadow.c
@@ -0,0 +1,343 @@
+/*============================================================================
+                        pgmdeshadow
+==============================================================================
+   Read PGM containing scanned black/white text, deshadow, write PGM.
+============================================================================*/
+/*
+    This code is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This code 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 code; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+*/
+
+/*
+ * Algorithm reference: Luc Vincent, "Morphological Grayscale Reruction
+ * in Image Analysis: Applications and Efficient Algorithms," IEEE
+ * Transactions on Image Processing, vol. 2, no. 2, April 1993, pp. 176-201.
+ *
+ * The algorithm used here is "fast hybrid grayscale reruction,"
+ * described as follows on pp. 198-199:
+ *
+ * I: mask image (binary or grayscale)
+ * J: marker image, defined on domain D_I, J <= I.
+ *    Reruction is determined directly in J.
+ *
+ * Scan D_I in raster order:
+ *   Let p be the current pixel;
+ *   J(p) <- (max{J(q),q member_of N_G_plus(p) union {p}}) ^ I(p)
+ *       [Note that ^ here refers to "pointwise minimum.]
+ *
+ * Scan D_I in antiraster order:
+ *   Let p be the current pixel;
+ *   J(p) <- (max{J(q),q member_of N_G_minus(p) union {p}}) ^ I(p)
+ *       [Note that ^ here refers to "pointwise minimum.]
+ *   If there exists q member_of N_G_minus(p) such that J(q) < J(p) and
+ *       J(q) < I(q), then fifo_add(p)
+ *
+ * Propagation step:
+ *   While fifo_empty() is false
+ *   p <- fifo_first()
+ *   For every pixel q member_of N_G(p):
+ *     If J(q) < J(p) and I(q) ~= J(q), then
+ *       J(q) <- min{J(p),I(q)}
+ *       fifo_add(q)
+ */
+
+#include <stdio.h>
+
+#include "pm_c_util.h"
+#include "mallocvar.h"
+#include "shhopt.h"
+#include "pgm.h"
+
+
+struct cmdlineInfo {
+    const char * inputFileName;
+};
+
+
+
+static void
+parseCommandLine(int argc, char ** 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 */
+
+    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);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else {
+        cmdlineP->inputFileName = argv[1];
+        if (argc-1 > 1)
+            pm_error ("Too many arguments.  The only argument is "
+                      "the optional input file name");
+    }
+}
+
+
+
+static void
+initializeDeshadowMarker(gray **      const inputPixels,
+                         gray **      const markerPixels,
+                         unsigned int const cols,
+                         unsigned int const rows,
+                         gray         const maxval) {
+/*----------------------------------------------------------------------------
+  Fill the image with maxval and then copy 1-pixel-wide borders
+-----------------------------------------------------------------------------*/
+    { /* Make middle white */
+        unsigned int row;
+        
+        for (row = 1; row < rows-1; ++row) {
+            unsigned int col;
+            for (col = 1; col < cols-1; ++col)
+                markerPixels[row][col] = maxval;
+        }
+    }
+    { /* Copy top edge */
+        unsigned int col;
+        for (col = 0; col < cols; ++col)
+            markerPixels[0][col] = inputPixels[0][col];
+    }
+    { /* Copy bottom edge */
+        unsigned int col;
+        for (col = 0; col < cols; ++col)
+            markerPixels[rows-1][col] = inputPixels[rows-1][col];
+    }
+    { /* Copy left edge */
+        unsigned int row;
+        for (row = 0; row < rows; ++row)
+            markerPixels[row][0] = inputPixels[row][0];
+    }
+    { /* Copy right edge */
+        unsigned int row;
+        for (row = 0; row < rows; ++row)
+            markerPixels[row][cols-1] = inputPixels[row][cols-1];
+    }
+}
+
+
+
+static gray
+min5(gray const a,
+     gray const b,
+     gray const c,
+     gray const d,
+     gray const e) {
+    
+    return MIN(a,MIN(b,MIN(c,MIN(d,e))));
+}
+
+
+
+static gray
+minNortheastPixel(gray **      const pixels,
+                  unsigned int const col,
+                  unsigned int const row) {
+/*----------------------------------------------------------------------------
+  Return the minimum pixel value from among the immediate north-east
+  neighbors of (col, row) in pixels[][].
+-----------------------------------------------------------------------------*/
+    return min5(pixels[row][col],
+                pixels[row][col-1],
+                pixels[row-1][col-1],
+                pixels[row-1][col],
+                pixels[row-1][col+1]);
+}
+
+
+
+static gray
+minSouthwestPixel(gray **      const pixels,
+                  unsigned int const col,
+                  unsigned int const row) {
+/*----------------------------------------------------------------------------
+  Return the minimum pixel value from among the immediate south-west
+  neighbors of (col, row) in pixels[][].
+-----------------------------------------------------------------------------*/
+    return min5(pixels[row][col],
+                pixels[row][col+1],
+                pixels[row+1][col-1],
+                pixels[row+1][col],
+                pixels[row+1][col+1]);
+}
+
+
+
+static void
+estimateBackground(gray **      const inputPixels,
+                   gray **      const markerPixels,
+                   unsigned int const cols,
+                   unsigned int const rows,
+                   gray         const maxval) {
+/*----------------------------------------------------------------------------
+   Update markerPixels[].
+-----------------------------------------------------------------------------*/
+    unsigned int const passes = 2;
+        /* make only two passes since the image is not really complicated
+           (otherwise could go up to 10)
+        */
+
+    unsigned int pass;
+    bool stable;
+
+    for (pass = 0, stable = FALSE; pass < passes && !stable; ++pass) {
+        int row;
+
+        stable = TRUE;  /* initial assumption */
+
+        /* scan in raster order */
+
+        for (row = 1; row < rows; ++row) {
+            unsigned int col;
+            for (col = 1; col < cols-1; ++col) {
+                gray const minpixel =
+                    minNortheastPixel(markerPixels, col, row);
+
+                if (minpixel > inputPixels[row][col]) {
+                    markerPixels[row][col] = minpixel;
+                    stable = FALSE;
+                } else
+                    markerPixels[row][col] = inputPixels[row][col];
+            }       
+        }
+        /* scan in anti-raster order */
+        
+        for (row = rows-2; row >= 0; --row) {
+            int col;
+            for (col = cols-2; col > 0; --col) {
+                gray const minpixel =
+                    minSouthwestPixel(markerPixels, col, row);
+                
+                if (minpixel > inputPixels[row][col]) {
+                    markerPixels[row][col] = minpixel;
+                    stable = FALSE;
+                } else
+                    markerPixels[row][col] = inputPixels[row][col];
+            }
+        }
+    }
+}
+
+
+
+static void
+divide(gray **      const dividendPixels,
+       gray **      const divisorPixels,
+       unsigned int const cols,
+       unsigned int const rows,
+       gray         const maxval) {
+/*----------------------------------------------------------------------------
+   Divide each pixel of dividendPixels[][] by the corresponding pixel
+   in divisorPixels[], replacing the dividendPixels[][] pixel with the
+   quotient.
+
+   But leave a one-pixel border around dividendPixels[][] unmodified.
+
+   Make sure the results are reasonable and not larger than maxval.
+-----------------------------------------------------------------------------*/
+    unsigned int row;
+
+    for (row = 1; row < rows-1; ++row) {
+        unsigned int col;
+        for (col = 1; col < cols-1; ++col) {
+            gray const divisor  = divisorPixels[row][col];
+            gray const dividend = dividendPixels[row][col];
+
+            gray quotient;
+
+            if (divisor == 0)
+                quotient = maxval;
+            else {
+                if (25 * divisor < 3 * maxval && 25 * dividend < 3 * maxval)
+                    quotient = maxval;
+                else
+                    quotient =
+                        MIN(maxval,
+                            maxval * (dividend + dividend/2) / divisor);
+            }        
+            dividendPixels[row][col] = quotient;
+        }
+    }
+}
+
+
+
+static void
+deshadow(gray **      const inputPixels,
+         unsigned int const cols,
+         unsigned int const rows,
+         gray         const maxval) {
+/*----------------------------------------------------------------------------
+   Deshadow the image described by inputPixels[], 'cols', 'rows', and
+   'maxval'.  (Modify inputPixels[][]).
+-----------------------------------------------------------------------------*/
+    gray ** markerPixels;
+
+    markerPixels = pgm_allocarray(cols, rows);
+
+    initializeDeshadowMarker(inputPixels, markerPixels, cols, rows, maxval);
+    
+    estimateBackground(inputPixels, markerPixels, cols, rows, maxval);
+    
+    divide(inputPixels, markerPixels, cols, rows, maxval);
+
+    pgm_freearray(markerPixels, rows);
+}
+
+
+
+int
+main(int argc, char* argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    gray maxval;
+    gray ** pixels;
+    int cols, rows;
+
+    pgm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+    
+    pixels = pgm_readpgm(ifP, &cols, &rows, &maxval);
+    pm_close(ifP);
+    
+    deshadow(pixels, cols, rows, maxval);
+    
+    pgm_writepgm(stdout, pixels, cols, rows, maxval, 0);
+
+    pgm_freearray(pixels, rows);
+
+    return 0;
+}
diff --git a/editor/pgmenhance.c b/editor/pgmenhance.c
new file mode 100644
index 00000000..83670568
--- /dev/null
+++ b/editor/pgmenhance.c
@@ -0,0 +1,112 @@
+/* pgmenhance.c - edge-enhance a portable graymap
+**
+** 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.
+*/
+
+#include "pgm.h"
+
+int
+main(int argc, char * argv[] ) {
+    FILE* ifp;
+    gray* prevrow;
+    gray* thisrow;
+    gray* nextrow;
+    gray* temprow;
+    gray* newrow;
+    int argn, n, rows, cols, row, col;
+    float phi, omphi;
+    gray maxval;
+    int format;
+    const char* const usage = "[-N] [pgmfile]  ( 1 <= N <= 9, default = 9 )";
+
+    pgm_init( &argc, argv );
+
+    argn = 1;
+    n = 9;
+
+    if ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' ) {
+        if ( sscanf( &(argv[argn][1]), "%d", &n ) != 1 )
+            pm_usage( usage );
+        if ( n < 1 || n > 9 )
+            pm_usage( usage );
+        ++argn;
+    }
+
+    if ( argn != argc ) {
+        ifp = pm_openr( argv[argn] );
+        ++argn;
+    } else
+        ifp = stdin;
+
+    if ( argn != argc )
+        pm_usage( usage );
+
+    pgm_readpgminit( ifp, &cols, &rows, &maxval, &format );
+    prevrow = pgm_allocrow( cols );
+    thisrow = pgm_allocrow( cols );
+    nextrow = pgm_allocrow( cols );
+
+    pgm_writepgminit( stdout, cols, rows, maxval, 0 );
+    newrow = pgm_allocrow( cols );
+
+    /* The edge enhancing technique is taken from Philip R. Thompson's "xim"
+    ** program, which in turn took it from section 6 of "Digital Halftones by
+    ** Dot Diffusion", D. E. Knuth, ACM Transaction on Graphics Vol. 6, No. 4,
+    ** October 1987, which in turn got it from two 1976 papers by J. F. Jarvis
+    ** et. al.
+    */
+    phi = n / 10.0;
+    omphi = 1.0 - phi;
+
+    /* First row. */
+    pgm_readpgmrow( ifp, thisrow, cols, maxval, format );
+    pgm_writepgmrow( stdout, thisrow, cols, maxval, 0 );
+    pgm_readpgmrow( ifp, nextrow, cols, maxval, format );
+
+    /* Other rows. */
+    for ( row = 1; row < rows - 1; row++ ) {
+        temprow = prevrow;
+        prevrow = thisrow;
+        thisrow = nextrow;
+        nextrow = temprow;
+        pgm_readpgmrow( ifp, nextrow, cols, maxval, format );
+        
+        newrow[0] = thisrow[0];
+        for (col = 1; col < cols - 1; col++) {
+            /* Compute the sum of the neighborhood. */
+            long sum, newval;
+            sum =
+                (long) prevrow[col-1] + (long) prevrow[col] +
+                (long) prevrow[col+1] +
+                (long) thisrow[col-1] + (long) thisrow[col] +
+                (long) thisrow[col+1] +
+                (long) nextrow[col-1] + (long) nextrow[col] +
+                (long) nextrow[col+1];
+            /* Now figure new value. */
+            newval = ( ( thisrow[col] - phi * sum / 9 ) / omphi + 0.5 );
+            if ( newval < 0 )
+                newrow[col] = 0;
+            else if ( newval > maxval )
+                newrow[col] = maxval;
+            else
+                newrow[col] = newval;
+        }
+        newrow[cols - 1] = thisrow[cols - 1];
+        pgm_writepgmrow( stdout, newrow, cols, maxval, 0 );
+    }
+    pm_close( ifp );
+    
+    /* Last row. */
+    pgm_writepgmrow( stdout, nextrow, cols, maxval, 0 );
+
+    pm_close( stdout );
+
+    exit( 0 );
+}
diff --git a/editor/pgmmedian.c b/editor/pgmmedian.c
new file mode 100644
index 00000000..5878b1e7
--- /dev/null
+++ b/editor/pgmmedian.c
@@ -0,0 +1,462 @@
+/* 
+** Version 1.0  September 28, 1996
+**
+** Copyright (C) 1996 by Mike Burns <burns@cac.psu.edu>
+**
+** Adapted to Netpbm 2005.08.10 by Bryan Henderson
+**
+** 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.
+*/
+
+/* References
+** ----------
+** The select k'th value implementation is based on Algorithm 489 by 
+** Robert W. Floyd from the "Collected Algorithms from ACM" Volume II.
+**
+** The histogram sort is based is described in the paper "A Fast Two-
+** Dimensional Median Filtering Algorithm" in "IEEE Transactions on 
+** Acoustics, Speech, and Signal Processing" Vol. ASSP-27, No. 1, February
+** 1979.  The algorithm I more closely followed is found in "Digital
+** Image Processing Algorithms" by Ioannis Pitas.
+*/
+
+
+#include "pgm.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+#include "nstring.h"
+
+enum medianMethod {MEDIAN_UNSPECIFIED, SELECT_MEDIAN, HISTOGRAM_SORT_MEDIAN};
+#define MAX_MEDIAN_TYPES      2
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFileName;
+    unsigned int width;
+    unsigned int height;
+    unsigned int cutoff;
+    enum medianMethod type;
+};
+
+
+/* Global variables common to each median sort routine. */
+static int const forceplain = 0;
+static int format;
+static gray maxval;
+static gray **grays;
+static gray *grayrow;
+static gray **rowptr;
+static int ccolso2, crowso2;
+static int row;
+
+
+
+static void
+parseCommandLine(int argc, char ** 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 widthSpec, heightSpec, cutoffSpec, typeSpec;
+    const char * type;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "width",     OPT_UINT, &cmdlineP->width,
+            &widthSpec, 0);
+    OPTENT3(0, "height",    OPT_UINT, &cmdlineP->height,
+            &heightSpec, 0);
+    OPTENT3(0, "cutoff",    OPT_UINT, &cmdlineP->cutoff,
+            &cutoffSpec, 0);
+    OPTENT3(0, "type",    OPT_STRING, &type,
+            &typeSpec, 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);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (!widthSpec)
+        cmdlineP->width = 3;
+    if (!heightSpec)
+        cmdlineP->height = 3;
+    if (!cutoffSpec)
+        cmdlineP->cutoff = 250;
+
+    if (typeSpec) {
+        if (STREQ(type, "histogram_sort"))
+            cmdlineP->type = HISTOGRAM_SORT_MEDIAN;
+        else if (STREQ(type, "select"))
+            cmdlineP->type = SELECT_MEDIAN;
+        else
+            pm_error("Invalid value '%s' for -type.  Valid values are "
+                     "'histogram_sort' and 'select'", type);
+    } else
+        cmdlineP->type = MEDIAN_UNSPECIFIED;
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else {
+        cmdlineP->inputFileName = argv[1];
+        if (argc-1 > 1)
+            pm_error ("Too many arguments.  The only argument is "
+                      "the optional input file name");
+    }
+}
+
+
+
+static void
+select_489(gray * const a,
+           int *  const parray,
+           int    const n,
+           int    const k) {
+
+    gray t;
+    int i, j, l, r;
+    int ptmp, ttmp;
+
+    l = 0;
+    r = n - 1;
+    while ( r > l ) {
+        t = a[parray[k]];
+        ttmp = parray[k];
+        i = l;
+        j = r;
+        ptmp = parray[l];
+        parray[l] = parray[k];
+        parray[k] = ptmp;
+        if ( a[parray[r]] > t ) {
+            ptmp = parray[r];
+            parray[r] = parray[l];
+            parray[l] = ptmp;
+        }
+        while ( i < j ) {
+            ptmp = parray[i];
+            parray[i] = parray[j];
+            parray[j] = ptmp;
+            ++i;
+            --j;
+            while ( a[parray[i]] < t )
+                ++i;
+            while ( a[parray[j]] > t )
+                --j;
+        }
+        if ( a[parray[l]] == t ) {
+            ptmp = parray[l];
+            parray[l] = parray[j];
+            parray[j] = ptmp;
+        } else {
+            ++j;
+            ptmp = parray[j];
+            parray[j] = parray[r];
+            parray[r] = ptmp;
+        }
+        if ( j <= k )
+            l = j + 1;
+        if ( k <= j )
+            r = j - 1;
+    }
+}
+
+
+
+static void
+select_median(FILE * const ifp,
+              int    const ccols,
+              int    const crows,
+              int    const cols,
+              int    const rows,
+              int    const median) {
+
+    int ccol, col;
+    int crow;
+    int rownum, irow, temprow;
+    gray *temprptr;
+    int i, leftcol;
+    int num_values;
+    gray *garray;
+
+    int *parray;
+    int addcol;
+    int *subcol;
+    int tsum;
+
+    /* Allocate storage for array of the current gray values. */
+    garray = pgm_allocrow( crows * ccols );
+
+    num_values = crows * ccols;
+
+    parray = (int *) pm_allocrow( crows * ccols, sizeof(int) );
+    subcol = (int *) pm_allocrow( cols, sizeof(int) );
+
+    for ( i = 0; i < cols; ++i )
+        subcol[i] = ( i - (ccolso2 + 1) ) % ccols;
+
+    /* Apply median to main part of image. */
+    for ( ; row < rows; ++row ) {
+        temprow = row % crows;
+        pgm_readpgmrow( ifp, grays[temprow], cols, maxval, format );
+
+        /* Rotate pointers to rows, so rows can be accessed in order. */
+        temprow = ( row + 1 ) % crows;
+        rownum = 0;
+        for ( irow = temprow; irow < crows; ++rownum, ++irow )
+            rowptr[rownum] = grays[irow];
+        for ( irow = 0; irow < temprow; ++rownum, ++irow )
+            rowptr[rownum] = grays[irow];
+
+        for ( col = 0; col < cols; ++col ) {
+            if ( col < ccolso2 || col >= cols - ccolso2 ) {
+                grayrow[col] = rowptr[crowso2][col];
+            } else if ( col == ccolso2 ) {
+                leftcol = col - ccolso2;
+                i = 0;
+                for ( crow = 0; crow < crows; ++crow ) {
+                    temprptr = rowptr[crow] + leftcol;
+                    for ( ccol = 0; ccol < ccols; ++ccol ) {
+                        garray[i] = *( temprptr + ccol );
+                        parray[i] = i;
+                        ++i;
+                    }
+                }
+                select_489( garray, parray, num_values, median );
+                grayrow[col] = garray[parray[median]];
+            } else {
+                addcol = col + ccolso2;
+                for (crow = 0, tsum = 0; crow < crows; ++crow, tsum += ccols)
+                    garray[tsum + subcol[col]] = *(rowptr[crow] + addcol );
+                select_489( garray, parray, num_values, median );
+                grayrow[col] = garray[parray[median]];
+            }
+        }
+        pgm_writepgmrow( stdout, grayrow, cols, maxval, forceplain );
+    }
+
+    /* Write out remaining unchanged rows. */
+    for ( irow = crowso2 + 1; irow < crows; ++irow )
+        pgm_writepgmrow( stdout, rowptr[irow], cols, maxval, forceplain );
+
+    pgm_freerow( garray );
+    pm_freerow( (char *) parray );
+    pm_freerow( (char *) subcol );
+}
+
+
+
+static void
+histogram_sort_median(FILE * const ifp,
+                      int    const ccols,
+                      int    const crows,
+                      int    const cols,
+                      int    const rows,
+                      int    const median) {
+
+    int const histmax = maxval + 1;
+
+    int *hist;
+    int mdn, ltmdn;
+    gray *left_col, *right_col;
+
+    hist = (int *) pm_allocrow( histmax, sizeof( int ) );
+    left_col = pgm_allocrow( crows );
+    right_col = pgm_allocrow( crows );
+
+    /* Apply median to main part of image. */
+    for ( ; row < rows; ++row ) {
+        int col;
+        int temprow;
+        int rownum;
+        int irow;
+        int i;
+        /* initialize hist[] */
+        for ( i = 0; i < histmax; ++i )
+            hist[i] = 0;
+
+        temprow = row % crows;
+        pgm_readpgmrow( ifp, grays[temprow], cols, maxval, format );
+
+        /* Rotate pointers to rows, so rows can be accessed in order. */
+        temprow = ( row + 1 ) % crows;
+        rownum = 0;
+        for ( irow = temprow; irow < crows; ++rownum, ++irow )
+            rowptr[rownum] = grays[irow];
+        for ( irow = 0; irow < temprow; ++rownum, ++irow )
+            rowptr[rownum] = grays[irow];
+
+        for ( col = 0; col < cols; ++col ) {
+            if ( col < ccolso2 || col >= cols - ccolso2 )
+                grayrow[col] = rowptr[crowso2][col];
+            else if ( col == ccolso2 ) {
+                int crow;
+                int const leftcol = col - ccolso2;
+                i = 0;
+                for ( crow = 0; crow < crows; ++crow ) {
+                    int ccol;
+                    gray * const temprptr = rowptr[crow] + leftcol;
+                    for ( ccol = 0; ccol < ccols; ++ccol ) {
+                        gray const g = *( temprptr + ccol );
+                        ++hist[g];
+                        ++i;
+                    }
+                }
+                ltmdn = 0;
+                for ( mdn = 0; ltmdn <= median; ++mdn )
+                    ltmdn += hist[mdn];
+                mdn--;
+                if ( ltmdn > median ) 
+                    ltmdn -= hist[mdn];
+
+                grayrow[col] = mdn;
+            } else {
+                int crow;
+                int const subcol = col - ( ccolso2 + 1 );
+                int const addcol = col + ccolso2;
+                for ( crow = 0; crow < crows; ++crow ) {
+                    left_col[crow] = *( rowptr[crow] + subcol );
+                    right_col[crow] = *( rowptr[crow] + addcol );
+                }
+                for ( crow = 0; crow < crows; ++crow ) {
+                    {
+                        gray const g = left_col[crow];
+                        hist[(int) g]--;
+                        if ( (int) g < mdn )
+                            ltmdn--;
+                    }
+                    {
+                        gray const g = right_col[crow];
+                        hist[(int) g]++;
+                        if ( (int) g < mdn )
+                            ltmdn++;
+                    }
+                }
+                if ( ltmdn > median )
+                    do {
+                        mdn--;
+                        ltmdn -= hist[mdn];
+                    } while ( ltmdn > median );
+                else {
+                    /* This one change from Pitas algorithm can reduce run
+                    ** time by up to 10%.
+                    */
+                    while ( ltmdn <= median ) {
+                        ltmdn += hist[mdn];
+                        mdn++;
+                    }
+                    mdn--;
+                    if ( ltmdn > median ) 
+                        ltmdn -= hist[mdn];
+                }
+                grayrow[col] = mdn;
+            }
+        }
+        pgm_writepgmrow( stdout, grayrow, cols, maxval, forceplain );
+    }
+
+    {
+        /* Write out remaining unchanged rows. */
+        int irow;
+        for ( irow = crowso2 + 1; irow < crows; ++irow )
+            pgm_writepgmrow( stdout, rowptr[irow], cols, maxval, forceplain );
+    }
+    pm_freerow( (char *) hist );
+    pgm_freerow( left_col );
+    pgm_freerow( right_col );
+}
+
+
+
+int
+main(int    argc,
+     char * argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    int cols, rows;
+    int median;
+    enum medianMethod medianMethod;
+
+    pgm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+    
+    ifP = pm_openr(cmdline.inputFileName);
+
+    ccolso2 = cmdline.width / 2;
+    crowso2 = cmdline.height / 2;
+
+    pgm_readpgminit(ifP, &cols, &rows, &maxval, &format);
+    pgm_writepgminit(stdout, cols, rows, maxval, forceplain);
+
+    /* Allocate space for number of rows in mask size. */
+    grays = pgm_allocarray(cols, cmdline.height);
+    grayrow = pgm_allocrow(cols);
+
+    /* Allocate pointers to mask row buffer. */
+    rowptr = pgm_allocarray(1, cmdline.height);
+
+    /* Read in and write out initial rows that won't get changed. */
+    for (row = 0; row < cmdline.height - 1; ++row) {
+        pgm_readpgmrow(ifP, grays[row], cols, maxval, format);
+        /* Write out the unchanged row. */
+        if (row < crowso2)
+            pgm_writepgmrow(stdout, grays[row], cols, maxval, forceplain);
+    }
+
+    median = (cmdline.height * cmdline.width) / 2;
+
+    /* Choose which sort to run. */
+    if (cmdline.type == MEDIAN_UNSPECIFIED) {
+        if ((maxval / ((cmdline.width * cmdline.height) - 1)) < cmdline.cutoff)
+            medianMethod = HISTOGRAM_SORT_MEDIAN;
+        else
+            medianMethod = SELECT_MEDIAN;
+    } else
+        medianMethod = cmdline.type;
+
+    switch (medianMethod) {
+    case SELECT_MEDIAN:
+        select_median(ifP, cmdline.width, cmdline.height, cols, rows, median);
+        break;
+        
+    case HISTOGRAM_SORT_MEDIAN:
+        histogram_sort_median(ifP, cmdline.width, cmdline.height,
+                              cols, rows, median);
+        break;
+    case MEDIAN_UNSPECIFIED:
+        pm_error("INTERNAL ERROR: median unspecified");
+    }
+    
+    pm_close(ifP);
+    pm_close(stdout);
+
+    pgm_freearray(grays, cmdline.height);
+    pgm_freerow(grayrow);
+    pgm_freearray(rowptr, cmdline.height);
+
+    return 0;
+}
+
+
+
+
+
+
diff --git a/editor/pgmmorphconv.c b/editor/pgmmorphconv.c
new file mode 100644
index 00000000..abc4e718
--- /dev/null
+++ b/editor/pgmmorphconv.c
@@ -0,0 +1,253 @@
+/* pgmmorphconv.c - morphological convolutions on a graymap: dilation and 
+** erosion
+**
+** Copyright (C) 2000 by Luuk van Dijk/Mind over Matter
+**
+** Based on 
+** pnmconvol.c - general MxN convolution on a portable anymap
+**
+** 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.
+*/
+
+#include "pm_c_util.h"
+#include "pgm.h"
+
+
+/************************************************************
+ * Dilate 
+ ************************************************************/
+
+static int 
+dilate( bit** template, int trowso2, int tcolso2, 
+        gray** in_image, gray** out_image, 
+        int rows, int cols ){
+
+  int c, r, tc, tr;
+  int templatecount;
+  gray source;
+
+  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 ){
+
+      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 ){
+          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 */
+
+  return templatecount;
+
+} /* dilate */
+
+
+
+/************************************************************
+ * Erode: same as dilate except !!!!
+ ************************************************************/
+
+static int 
+erode( bit** template, int trowso2, int tcolso2, 
+       gray** in_image, gray** out_image, 
+       int rows, int cols ){
+
+  int c, r, tc, tr;
+  int templatecount;
+  gray source;
+
+  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
+   */
+
+  templatecount=0;
+
+  for( tr=-trowso2; tr<=trowso2; ++tr ){
+    for( tc=-tcolso2; tc<=tcolso2; ++tc ){
+
+      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 ){
+
+      source = in_image[r+tr][c+tc];
+      out_image[r][c] = MIN(source, out_image[r][c]);
+      
+    } /* for c */
+      } /* for r */
+
+
+
+    } /* for tr */
+  } /* for tc */
+
+  return templatecount;
+
+} /* erode */
+
+
+
+/************************************************************
+ *  Main
+ ************************************************************/
+
+
+int main( int argc, char* argv[] ){
+
+  int argn;
+  char operation;
+  const char* usage = "-dilate|-erode|-open|-close <templatefile> [pgmfile]";
+
+  FILE* tifp;   /* template */
+  int tcols, trows;
+  int tcolso2, trowso2;
+  bit** template;
+
+
+  FILE*  ifp;   /* input image */
+  int cols, rows;
+  gray maxval;
+
+  gray** in_image;
+  gray** out_image;
+
+  int templatecount=0;
+
+  pgm_init( &argc, argv );
+
+  /*
+   *  parse arguments
+   */ 
+  
+  ifp = stdin;
+  operation = 'd';
+
+  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.
+   */
+
+  template = pbm_readpbm( tifp, &tcols, &trows );
+  pm_close( tifp );
+
+  if( tcols % 2 != 1 || trows % 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 */
+  tcolso2 = tcols / 2; /* template coords run from -tcols/2 .. 0 .. +tcols/2 */
+  trowso2 = trows / 2;
+
+#if 0
+  fprintf(stderr, "template: %d  x %d\n", trows, tcols);
+  fprintf(stderr, "half: %d  x %d\n", trowso2, tcolso2);
+#endif
+
+  /*
+   * Read in the image
+   */
+  
+  in_image = pgm_readpgm( ifp, &cols, &rows, &maxval);
+
+  if( cols < tcols || rows < trows )
+    pm_error("the image is smaller than the convolution matrix" );
+  
+#if 0
+  fprintf(stderr, "image: %d  x %d (%d)\n", rows, cols, maxval);
+#endif
+
+  /* 
+   * Allocate  output buffer and initialize with min or max value 
+   */
+
+  out_image = pgm_allocarray( cols, rows );
+  
+  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 );
+  }
+  
+  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 */
+
diff --git a/editor/pnmalias.c b/editor/pnmalias.c
new file mode 100644
index 00000000..36b41ce4
--- /dev/null
+++ b/editor/pnmalias.c
@@ -0,0 +1,250 @@
+/* pnmmalias.c - antialias a portable anymap.
+**
+** Copyright (C) 1992 by Alberto Accomazzi, Smithsonian Astrophysical
+** Observatory.
+**
+** 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 "pnm.h"
+
+int
+main(int argc, char * argv[] ) {
+    FILE* ifp;
+    xel* xelrow[3];
+    xel* newxelrow;
+    pixel bgcolorppm, fgcolorppm;
+    register xel* xpP;
+    register xel* xP;
+    register xel* xnP;
+    register xel* nxP;
+    xel bgcolor, fgcolor;
+    int argn, rows, cols, format, newformat, bgonly, fgonly;
+    int bgalias, fgalias;
+    int row;
+    double fmask[9], weight;
+    xelval maxval;
+    xelval newmaxval;
+    const char* const usage = "[-bgcolor <color>] [-fgcolor <color>] [-bonly] [-fonly] [-balias] [-falias] [-weight <w>] [pnmfile]";
+
+    pnm_init( &argc, argv );
+
+    bgonly = fgonly = 0;
+    bgalias = fgalias = 0;
+    weight = 1./3.;
+    argn = 1;
+    PPM_ASSIGN( bgcolorppm, 0, 0, 0);
+    PPM_ASSIGN( fgcolorppm, 0, 0, 0);
+
+    while ( argn < argc && argv[argn][0] == '-' )
+        {
+        if ( pm_keymatch( argv[argn], "-fgcolor", 3 ) ) 
+        {
+        if ( ++argn >= argc ) 
+        pm_usage( usage );
+        else 
+        fgcolorppm = ppm_parsecolor( argv[argn], PPM_MAXMAXVAL );
+        }
+        else if ( pm_keymatch( argv[argn], "-bgcolor", 3 ) ) 
+        {
+        if ( ++argn >= argc ) 
+        pm_usage( usage );
+        else 
+        bgcolorppm = ppm_parsecolor( argv[argn], PPM_MAXMAXVAL );
+        }
+        else if ( pm_keymatch( argv[argn], "-weight", 2 ) ) 
+        {
+        if ( ++argn >= argc ) 
+        pm_usage( usage );
+        else if ( sscanf( argv[argn], "%lf", &weight ) != 1 )
+            pm_usage( usage );
+        else if ( weight >= 1. || weight <= 0. )
+        {
+        pm_message( "weight factor w must be 0.0 < w < 1.0" );
+        pm_usage( usage );
+        }
+        }
+    else if ( pm_keymatch( argv[argn], "-bonly", 3 ) )
+        bgonly = 1;
+    else if ( pm_keymatch( argv[argn], "-fonly", 3 ) )
+        fgonly = 1;
+    else if ( pm_keymatch( argv[argn], "-balias", 3 ) )
+        bgalias = 1;
+    else if ( pm_keymatch( argv[argn], "-falias", 3 ) )
+        fgalias = 1;
+    else if ( pm_keymatch( argv[argn], "-bfalias", 3 ) )
+        bgalias = fgalias = 0;
+    else if ( pm_keymatch( argv[argn], "-fbalias", 3 ) )
+        bgalias = fgalias = 0;
+        else
+            pm_usage( usage );
+        ++argn;
+        }
+
+    if ( argn != argc )
+    {
+    ifp = pm_openr( argv[argn] );
+    ++argn;
+    }
+    else
+    ifp = stdin;
+
+    if ( argn != argc )
+    pm_usage( usage );
+
+    /* normalize mask elements */
+    fmask[4] = weight;
+    fmask[0] = fmask[1] = fmask[2] = fmask[3] = ( 1.0 - weight ) / 8.0;
+    fmask[5] = fmask[6] = fmask[7] = fmask[8] = ( 1.0 - weight ) / 8.0;
+
+    pnm_readpnminit( ifp, &cols, &rows, &maxval, &format );
+   
+    xelrow[0] = pnm_allocrow( cols );
+    xelrow[1] = pnm_allocrow( cols );
+    xelrow[2] = pnm_allocrow( cols );
+    newxelrow = pnm_allocrow( cols );
+
+    /* Promote PBM files 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;
+    }
+
+    /* Figure out foreground pixel value if none was given */
+    if (PPM_GETR(fgcolorppm) == 0 && PPM_GETG(fgcolorppm) == 0 && 
+        PPM_GETB(fgcolorppm) == 0 ) {
+        if ( PNM_FORMAT_TYPE(newformat) == PGM_TYPE )
+            PNM_ASSIGN1( fgcolor, newmaxval );
+        else 
+            PPM_ASSIGN( fgcolor, newmaxval, newmaxval, newmaxval );
+    } else {
+        if ( PNM_FORMAT_TYPE(newformat) == PGM_TYPE )
+            PNM_ASSIGN1( fgcolor, PPM_GETR( fgcolorppm ) );
+        else 
+            fgcolor = fgcolorppm;
+    }
+
+    if (PPM_GETR(bgcolorppm) != 0 || PPM_GETG(bgcolorppm) != 0 || 
+        PPM_GETB(bgcolorppm) != 0 ) {
+        if ( PNM_FORMAT_TYPE(newformat) == PGM_TYPE )
+            PNM_ASSIGN1( bgcolor, PPM_GETR( bgcolorppm) );
+        else 
+            bgcolor = bgcolorppm;
+    } else {
+        if ( PNM_FORMAT_TYPE(newformat) == PGM_TYPE )
+            PNM_ASSIGN1( bgcolor, 0 );
+        else 
+            PPM_ASSIGN( bgcolor, 0, 0, 0 );
+    }
+
+
+    pnm_readpnmrow( ifp, xelrow[0], cols, newmaxval, format );
+    pnm_readpnmrow( ifp, xelrow[1], cols, newmaxval, format );
+    pnm_writepnminit( stdout, cols, rows, newmaxval, newformat, 0 );
+    pnm_writepnmrow( stdout, xelrow[0], cols, newmaxval, newformat, 0 );
+
+    for ( row = 1; row < rows - 1; ++row ) {
+        int col;
+        int value, valuer, valueg, valueb;
+
+        pnm_readpnmrow( ifp, xelrow[(row+1)%3], cols, newmaxval, format );
+        newxelrow[0] = xelrow[row%3][0];
+        
+        for ( col = 1, xpP = (xelrow[(row-1)%3] + 1), xP = (xelrow[row%3] + 1),
+                  xnP = (xelrow[(row+1)%3] + 1), nxP = (newxelrow+1); 
+              col < cols - 1; ++col, ++xpP, ++xP, ++xnP, ++nxP ) {
+
+            int fgflag, bgflag;
+
+            /* Reset flags if anti-aliasing is to be done on foreground
+             * or background pixels only */
+            if ( ( bgonly && PNM_EQUAL( *xP, fgcolor ) ) ||
+                 ( fgonly && PNM_EQUAL( *xP, bgcolor ) ) ) 
+                bgflag = fgflag = 0;
+            else {
+                /* Do anti-aliasing here: see if pixel is at the border of a
+                 * background or foreground stepwise side */
+                bgflag = 
+                    (PNM_EQUAL(*xpP,bgcolor) && PNM_EQUAL(*(xP+1),bgcolor)) ||
+                    (PNM_EQUAL(*(xP+1),bgcolor) && PNM_EQUAL(*xnP,bgcolor)) ||
+                    (PNM_EQUAL(*xnP,bgcolor) && PNM_EQUAL(*(xP-1),bgcolor)) ||
+                    (PNM_EQUAL(*(xP-1),bgcolor) && PNM_EQUAL(*xpP,bgcolor));
+                fgflag = 
+                    (PNM_EQUAL(*xpP,fgcolor) && PNM_EQUAL(*(xP+1),fgcolor)) ||
+                    (PNM_EQUAL(*(xP+1),fgcolor) && PNM_EQUAL(*xnP,fgcolor)) ||
+                    (PNM_EQUAL(*xnP,fgcolor) && PNM_EQUAL(*(xP-1),fgcolor)) ||
+                    (PNM_EQUAL(*(xP-1),fgcolor) && PNM_EQUAL(*xpP,fgcolor)); 
+            }
+            if ( ( bgflag && bgalias ) || ( fgflag && fgalias ) || 
+                 ( bgflag && fgflag ) )
+                switch( PNM_FORMAT_TYPE( newformat ) ) {   
+                case PGM_TYPE:
+                    value = PNM_GET1(*(xpP-1)) * fmask[0] +
+                        PNM_GET1(*(xpP  )) * fmask[1] + 
+                        PNM_GET1(*(xpP+1)) * fmask[2] +
+                        PNM_GET1(*(xP -1)) * fmask[3] +
+                        PNM_GET1(*(xP   )) * fmask[4] +
+                        PNM_GET1(*(xP +1)) * fmask[5] +
+                        PNM_GET1(*(xnP-1)) * fmask[6] +
+                        PNM_GET1(*(xnP  )) * fmask[7] +
+                        PNM_GET1(*(xnP+1)) * fmask[8] +
+                        0.5;
+                    PNM_ASSIGN1( *nxP, value );
+                    break;
+                default:
+                    valuer= PPM_GETR(*(xpP-1)) * fmask[0] +
+                        PPM_GETR(*(xpP  )) * fmask[1] + 
+                        PPM_GETR(*(xpP+1)) * fmask[2] +
+                        PPM_GETR(*(xP -1)) * fmask[3] +
+                        PPM_GETR(*(xP   )) * fmask[4] +
+                        PPM_GETR(*(xP +1)) * fmask[5] +
+                        PPM_GETR(*(xnP-1)) * fmask[6] +
+                        PPM_GETR(*(xnP  )) * fmask[7] +
+                        PPM_GETR(*(xnP+1)) * fmask[8] +
+                        0.5;
+                    valueg= PPM_GETG(*(xpP-1)) * fmask[0] +
+                        PPM_GETG(*(xpP  )) * fmask[1] + 
+                        PPM_GETG(*(xpP+1)) * fmask[2] +
+                        PPM_GETG(*(xP -1)) * fmask[3] +
+                        PPM_GETG(*(xP   )) * fmask[4] +
+                        PPM_GETG(*(xP +1)) * fmask[5] +
+                        PPM_GETG(*(xnP-1)) * fmask[6] +
+                        PPM_GETG(*(xnP  )) * fmask[7] +
+                        PPM_GETG(*(xnP+1)) * fmask[8] +
+                        0.5;
+                    valueb= PPM_GETB(*(xpP-1)) * fmask[0] +
+                        PPM_GETB(*(xpP  )) * fmask[1] + 
+                        PPM_GETB(*(xpP+1)) * fmask[2] +
+                        PPM_GETB(*(xP -1)) * fmask[3] +
+                        PPM_GETB(*(xP   )) * fmask[4] +
+                        PPM_GETB(*(xP +1)) * fmask[5] +
+                        PPM_GETB(*(xnP-1)) * fmask[6] +
+                        PPM_GETB(*(xnP  )) * fmask[7] +
+                        PPM_GETB(*(xnP+1)) * fmask[8] +
+                        0.5;
+                    PPM_ASSIGN( *nxP, valuer, valueg, valueb );
+                    break;
+                }
+            else
+                *nxP = *xP;
+        }
+
+        newxelrow[cols-1] = xelrow[row%3][cols-1];
+        pnm_writepnmrow( stdout, newxelrow, cols, newmaxval, newformat, 0 );
+    }
+        
+    pnm_writepnmrow( stdout, xelrow[row%3], cols, newmaxval, newformat, 0 );
+    
+    pm_close( ifp );
+    exit ( 0 );
+}
+
diff --git a/editor/pnmcat.c b/editor/pnmcat.c
new file mode 100644
index 00000000..20dbf34d
--- /dev/null
+++ b/editor/pnmcat.c
@@ -0,0 +1,427 @@
+/* pnmcat.c - concatenate portable anymaps
+**
+** 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.
+*/
+
+#include "pnm.h"
+#include "mallocvar.h"
+#include "shhopt.h"
+
+
+enum backcolor {BACK_BLACK, BACK_WHITE, BACK_AUTO};
+
+enum orientation {TOPBOTTOM, LEFTRIGHT};
+
+enum justification {JUST_CENTER, JUST_MIN, JUST_MAX};
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char **inputFilespec;  
+    unsigned int nfiles;
+    enum backcolor backcolor;
+    enum orientation orientation;
+    enum justification justification;
+};
+
+
+
+static void
+parseCommandLine(int argc, 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 = malloc(100*sizeof(optEntry));
+        /* Instructions to OptParseOptions2 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+    
+    unsigned int leftright, topbottom, black, white, jtop, jbottom,
+        jleft, jright, jcenter;
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    OPTENT3(0, "leftright",  OPT_FLAG,   NULL, &leftright,   0);
+    OPTENT3(0, "lr",         OPT_FLAG,   NULL, &leftright,   0);
+    OPTENT3(0, "topbottom",  OPT_FLAG,   NULL, &topbottom,   0);
+    OPTENT3(0, "tb",         OPT_FLAG,   NULL, &topbottom,   0);
+    OPTENT3(0, "black",      OPT_FLAG,   NULL, &black,       0);
+    OPTENT3(0, "white",      OPT_FLAG,   NULL, &white,       0);
+    OPTENT3(0, "jtop",       OPT_FLAG,   NULL, &jtop,        0);
+    OPTENT3(0, "jbottom",    OPT_FLAG,   NULL, &jbottom,     0);
+    OPTENT3(0, "jleft",      OPT_FLAG,   NULL, &jleft,       0);
+    OPTENT3(0, "jright",     OPT_FLAG,   NULL, &jright,      0);
+    OPTENT3(0, "jcenter",    OPT_FLAG,   NULL, &jcenter,     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 (leftright + topbottom > 1)
+        pm_error("You may specify only one of -topbottom (-tb) and "
+                 "-leftright (-lr)");
+    else if (leftright)
+        cmdlineP->orientation = LEFTRIGHT;
+    else if (topbottom)
+        cmdlineP->orientation = TOPBOTTOM;
+    else
+        pm_error("You must specify either -leftright or -topbottom");
+
+    if (black + white > 1)
+        pm_error("You may specify only one of -black and -white");
+    else if (black)
+        cmdlineP->backcolor = BACK_BLACK;
+    else if (white)
+        cmdlineP->backcolor = BACK_WHITE;
+    else
+        cmdlineP->backcolor = BACK_AUTO;
+
+    if (jtop + jbottom + jleft + jright + jcenter > 1)
+        pm_error("You may specify onlyone of -jtop, -jbottom, "
+                 "-jleft, and -jright");
+    else {
+        switch (cmdlineP->orientation) {
+        case LEFTRIGHT:
+            if (jleft)
+                pm_error("-jleft is invalid with -leftright");
+            if (jright)
+                pm_error("-jright is invalid with -leftright");
+            if (jtop)
+                cmdlineP->justification = JUST_MIN;
+            else if (jbottom)
+                cmdlineP->justification = JUST_MAX;
+            else if (jcenter)
+                cmdlineP->justification = JUST_CENTER;
+            else
+                cmdlineP->justification = JUST_CENTER;
+            break;
+        case TOPBOTTOM:
+            if (jtop)
+                pm_error("-jtop is invalid with -topbottom");
+            if (jbottom)
+                pm_error("-jbottom is invalid with -topbottom");
+            if (jleft)
+                cmdlineP->justification = JUST_MIN;
+            else if (jright)
+                cmdlineP->justification = JUST_MAX;
+            else if (jcenter)
+                cmdlineP->justification = JUST_CENTER;
+            else
+                cmdlineP->justification = JUST_CENTER;
+            break;
+        }
+    }
+
+    if (argc-1 < 1) {
+        MALLOCARRAY_NOFAIL(cmdlineP->inputFilespec, 1);
+        cmdlineP->inputFilespec[0] = "-";
+        cmdlineP->nfiles = 1;
+    } else {
+        unsigned int i;
+
+        MALLOCARRAY_NOFAIL(cmdlineP->inputFilespec, argc-1);
+        
+        for (i = 0; i < argc-1; ++i)
+            cmdlineP->inputFilespec[i] = argv[1+i];
+        cmdlineP->nfiles = argc-1;
+    }
+}        
+
+
+
+static void
+computeOutputParms(unsigned int     const nfiles,
+                   enum orientation const orientation,
+                   int                    cols[], 
+                   int                    rows[],
+                   xelval                 maxval[],
+                   int                    format[],
+                   int *            const newcolsP,
+                   int *            const newrowsP,
+                   xelval *         const newmaxvalP,
+                   int *            const newformatP) {
+
+    int newcols, newrows;
+    int newformat;
+    xelval newmaxval;
+
+    unsigned int i;
+
+    newcols = 0;
+    newrows = 0;
+
+    for (i = 0; i < nfiles; ++i)	{
+        if (i == 0) {
+            newmaxval = maxval[i];
+            newformat = format[i];
+	    } else {
+            if (PNM_FORMAT_TYPE(format[i]) > PNM_FORMAT_TYPE(newformat))
+                newformat = format[i];
+            if (maxval[i] > newmaxval)
+                newmaxval = maxval[i];
+	    }
+        switch (orientation) {
+        case LEFTRIGHT:
+            newcols += cols[i];
+            if (rows[i] > newrows)
+                newrows = rows[i];
+            break;
+        case TOPBOTTOM:
+            newrows += rows[i];
+            if (cols[i] > newcols)
+                newcols = cols[i];
+            break;
+	    }
+	}
+    *newrowsP   = newrows;
+    *newcolsP   = newcols;
+    *newmaxvalP = newmaxval;
+    *newformatP = newformat;
+}
+
+
+
+static void
+concatenateLeftRight(FILE *             const ofp,
+                     unsigned int       const nfiles,
+                     int                const newcols,
+                     int                const newrows,
+                     xelval             const newmaxval,
+                     int                const newformat,
+                     enum justification const justification,
+                     FILE *                   ifp[],
+                     int                      cols[],
+                     int                      rows[],
+                     xelval                   maxval[],
+                     int                      format[],
+                     xel *                    xelrow[],
+                     xel                      background[]) {
+
+    unsigned int row;
+    
+    xel * const newxelrow = pnm_allocrow(newcols);
+
+    for (row = 0; row < newrows; ++row) {
+        unsigned int new;
+        unsigned int i;
+
+        new = 0;
+        for (i = 0; i < nfiles; ++i) {
+            int padtop;
+
+            switch (justification) {
+            case JUST_MIN:
+                padtop = 0;
+                break;
+            case JUST_MAX:
+                padtop = newrows - rows[i];
+                break;
+            case JUST_CENTER:
+                padtop = ( newrows - rows[i] ) / 2;
+                break;
+            }
+            if (row < padtop || row >= padtop + rows[i]) {
+                unsigned int col;
+                for (col = 0; col < cols[i]; ++col)
+                    newxelrow[new+col] = background[i];
+            } else {
+                if (row != padtop) {
+                    /* first row already read */
+                    pnm_readpnmrow(
+                        ifp[i], xelrow[i], cols[i], maxval[i], format[i] );
+                    pnm_promoteformatrow(
+                        xelrow[i], cols[i], maxval[i], format[i],
+                        newmaxval, newformat );
+                }
+                {
+                    unsigned int col;
+                    for (col = 0; col < cols[i]; ++col)
+                        newxelrow[new+col] = xelrow[i][col];
+                }
+            }
+            new += cols[i];
+        }
+        pnm_writepnmrow(ofp, newxelrow, newcols, newmaxval, newformat, 0);
+    }
+}
+
+
+
+static void
+concatenateTopBottom(FILE *             const ofp,
+                     unsigned int       const nfiles,
+                     int                const newcols,
+                     int                const newrows,
+                     xelval             const newmaxval,
+                     int                const newformat,
+                     enum justification const justification,
+                     FILE *                   ifp[],
+                     int                      cols[],
+                     int                      rows[],
+                     xelval                   maxval[],
+                     int                      format[],
+                     xel *                    xelrow[],
+                     xel                      background[]) {
+
+    int new;
+    xel * const newxelrow = pnm_allocrow(newcols);
+    int padleft;
+    unsigned int i;
+    unsigned int row;
+    
+    i = 0;
+    switch (justification) {
+    case JUST_MIN:
+        padleft = 0;
+        break;
+    case JUST_MAX:
+        padleft = newcols - cols[i];
+        break;
+    case JUST_CENTER:
+        padleft = (newcols - cols[i]) / 2;
+        break;
+    }
+
+    new = 0;
+
+    for (row = 0; row < newrows; ++row) {
+        if (row - new >= rows[i]) {
+            new += rows[i];
+            ++i;
+            if (i >= nfiles)
+                pm_error("INTERNAL ERROR: i > nfiles");
+            switch (justification) {
+            case JUST_MIN:
+                padleft = 0;
+                break;
+            case JUST_MAX:
+                padleft = newcols - cols[i];
+                break;
+            case JUST_CENTER:
+                padleft = (newcols - cols[i]) / 2;
+                break;
+            }
+        }
+        if (row - new > 0) {
+            pnm_readpnmrow(
+                ifp[i], xelrow[i], cols[i], maxval[i], format[i]);
+            pnm_promoteformatrow(
+                xelrow[i], cols[i], maxval[i], format[i],
+                newmaxval, newformat);
+        }
+        {
+            unsigned int col;
+
+            for (col = 0; col < padleft; ++col)
+                newxelrow[col] = background[i];
+            for (col = 0; col < cols[i]; ++col)
+                newxelrow[padleft+col] = xelrow[i][col];
+            for (col = padleft + cols[i]; col < newcols; ++col)
+                newxelrow[col] = background[i];
+        }
+        pnm_writepnmrow(ofp,
+                        newxelrow, newcols, newmaxval, newformat, 0);
+	}
+}
+
+
+
+int
+main(int argc, char ** argv) {
+
+    struct cmdlineInfo cmdline;
+    FILE** ifp;
+    xel** xelrow;
+    xel* background;
+    xelval* maxval;
+    xelval newmaxval;
+    int* rows;
+    int* cols;
+    int* format;
+    int newformat;
+    unsigned int i;
+    int newrows, newcols;
+
+    pnm_init( &argc, argv );
+
+    parseCommandLine(argc, argv, &cmdline);
+    
+    MALLOCARRAY_NOFAIL(ifp,        cmdline.nfiles);
+    MALLOCARRAY_NOFAIL(xelrow,     cmdline.nfiles);
+    MALLOCARRAY_NOFAIL(background, cmdline.nfiles);
+    MALLOCARRAY_NOFAIL(maxval,     cmdline.nfiles);
+    MALLOCARRAY_NOFAIL(rows,       cmdline.nfiles);
+    MALLOCARRAY_NOFAIL(cols,       cmdline.nfiles);
+    MALLOCARRAY_NOFAIL(format,     cmdline.nfiles);
+
+    for (i = 0; i < cmdline.nfiles; ++i) {
+        ifp[i] = pm_openr(cmdline.inputFilespec[i]);
+        pnm_readpnminit(ifp[i], &cols[i], &rows[i], &maxval[i], &format[i]);
+        xelrow[i] = pnm_allocrow(cols[i]);
+    }
+
+    computeOutputParms(cmdline.nfiles, cmdline.orientation,
+                       cols, rows, maxval, format,
+                       &newcols, &newrows, &newmaxval, &newformat);
+
+    for (i = 0; i < cmdline.nfiles; ++i) {
+        /* Read first row just to get a good guess at the background. */
+        pnm_readpnmrow(ifp[i], xelrow[i], cols[i], maxval[i], format[i]);
+        pnm_promoteformatrow(
+            xelrow[i], cols[i], maxval[i], format[i], newmaxval, newformat);
+        switch (cmdline.backcolor) {
+        case BACK_AUTO:
+            background[i] =
+                pnm_backgroundxelrow(
+                    xelrow[i], cols[i], newmaxval, newformat);
+            break;
+        case BACK_BLACK:
+            background[i] = pnm_blackxel(newmaxval, newformat);
+            break;
+        case BACK_WHITE:
+            background[i] = pnm_whitexel(newmaxval, newformat);
+            break;
+        }
+	}
+
+    pnm_writepnminit(stdout, newcols, newrows, newmaxval, newformat, 0);
+
+    switch (cmdline.orientation) {
+    case LEFTRIGHT:
+        concatenateLeftRight(stdout, cmdline.nfiles,
+                             newcols, newrows, newmaxval, newformat,
+                             cmdline.justification,
+                             ifp, cols, rows, maxval, format, xelrow,
+                             background);
+        break;
+    case TOPBOTTOM:
+        concatenateTopBottom(stdout, cmdline.nfiles,
+                             newcols, newrows, newmaxval, newformat,
+                             cmdline.justification,
+                             ifp, cols, rows, maxval, format, xelrow,
+                             background);
+        break;
+    }
+    free(cmdline.inputFilespec);
+
+    for (i = 0; i < cmdline.nfiles; ++i)
+        pm_close(ifp[i]);
+
+    pm_close(stdout);
+
+    return 0;
+}
diff --git a/editor/pnmcomp.c b/editor/pnmcomp.c
new file mode 100644
index 00000000..5a8b1d55
--- /dev/null
+++ b/editor/pnmcomp.c
@@ -0,0 +1,459 @@
+/* +-------------------------------------------------------------------+ */
+/* | 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 "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
new file mode 100644
index 00000000..0bf44ce3
--- /dev/null
+++ b/editor/pnmconvol.c
@@ -0,0 +1,1989 @@
+/* pnmconvol.c - general MxN convolution on a PNM image
+**
+** Version 2.0.1 January 30, 1995
+**
+** Major rewriting by Mike Burns
+** Copyright (C) 1994, 1995 by Mike Burns (burns@chem.psu.edu)
+**
+** 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.
+*/
+
+/* A change history is at the bottom */
+
+#include "pnm.h"
+#include "shhopt.h"
+#include "mallocvar.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;  /* '-' if stdin */
+    const char *kernelFilespec;
+    unsigned int nooffset;
+};
+
+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;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "nooffset",     OPT_FLAG,   NULL,                  
+            &cmdlineP->nooffset,       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 (argc-1 < 1)
+        pm_error("Need at least one argument: file specification of the "
+                 "convolution kernel image.");
+
+    cmdlineP->kernelFilespec = argv[1];
+
+    if (argc-1 >= 2)
+        cmdlineP->inputFilespec = argv[2];
+    else
+        cmdlineP->inputFilespec = "-";
+
+    if (argc-1 > 2)
+        pm_error("Too many arguments.  Only acceptable arguments are: "
+                 "convolution 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;
+
+#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;
+
+struct convolveType {
+    void (*ppmConvolver)(const float ** const rweights,
+                         const float ** const gweights,
+                         const float ** const bweights);
+    void (*pgmConvolver)(const float ** const weights);
+};
+
+static FILE* ifp;
+static int crows, ccols, ccolso2, crowso2;
+static int cols, rows;
+static xelval maxval;
+static int format, newformat;
+
+
+
+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) {
+/*----------------------------------------------------------------------------
+   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
+   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;
+
+    float** rweights;
+    float** gweights;
+    float** bweights;
+
+    float rsum, gsum, bsum;
+
+    unsigned int crow;
+
+    /* 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));
+
+    rsum = gsum = bsum = 0.0;  /* initial value */
+    
+    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;
+            }
+        }
+    }
+    *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);
+
+            if (rsum < 0 && gsum < 0 && bsum < 0)
+                pm_message("Maybe you want the -nooffset option?");
+        }
+        break;
+
+    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;
+    }
+}
+
+
+
+/* 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);
+    }
+
+    /* 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 );
+            }
+        }
+        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 );
+}
+
+
+
+/* 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
+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 );
+    }
+
+    /* 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];
+
+    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 );
+
+    ++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 );
+        }
+        }
+    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 );
+
+    }
+
+
+/* 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 );
+    }
+
+    /* 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 );
+
+    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 ( 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 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 );
+
+    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];
+        }
+
+    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 );
+        }
+        }
+    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 );
+
+    }
+
+
+/* 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 );
+    }
+
+    /* 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 */
+
+    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 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 );
+        }
+    }
+    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];
+            }
+        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 );
+        }
+        }
+    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 );
+
+    }
+
+
+
+
+/* PPM General Convolution Algorithm
+**
+** No redundancy in convolution matrix.  Just use brute force.
+** See pgm_general_convolve() for more details.
+*/
+
+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 );
+    }
+
+    /* 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];
+            }
+            }
+            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 );
+    }
+
+    /* 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 );
+
+    }
+
+
+/* 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;
+    }
+
+    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 );
+    }
+
+    /* 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 ( 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 );
+        }
+    }
+    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.
+    */
+    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 );
+    }
+
+    /* 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 );
+
+    }
+
+
+/* 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 );
+    }
+
+    /* 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 );
+
+    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 ( crow = 0; crow < crows; ++crow )
+    {
+    rrowsumptr[crow] = rrowsum[crow];
+    growsumptr[crow] = growsum[crow];
+    browsumptr[crow] = browsum[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;
+        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) );
+            }
+        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 );
+
+    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];
+        }
+
+    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];
+            }
+        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];
+            }
+        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 );
+    }
+
+    /* 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 );
+
+    }
+
+
+/* PPM Vertical Convolution
+**
+** Same as pgm_vertical_convolve()
+**
+*/
+
+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;
+    }
+
+    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);
+    }
+
+    /* 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 */
+
+    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 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 (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];
+                }
+                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];
+                }
+                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);
+    }
+
+    /* 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
+determineConvolveType(xel * const *         const cxels,
+                      struct convolveType * const typeP) {
+/*----------------------------------------------------------------------------
+   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.
+-----------------------------------------------------------------------------*/
+    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;
+        }
+
+        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;
+            }
+            ++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;
+            }
+            ++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;
+            }
+            ++ccol;
+        }
+        break;
+    }
+    
+    /* 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 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);
+    }
+}
+
+
+
+int
+main(int argc, char * argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE* cifp;
+    xel** cxels;
+    int cformat;
+    xelval cmaxval;
+    struct convolveType convolveType;
+    float ** rweights;
+    float ** gweights;
+    float ** bweights;
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    cifp = pm_openr(cmdline.kernelFilespec);
+
+    /* Read in the convolution matrix. */
+    cxels = pnm_readpnm(cifp, &ccols, &crows, &cmaxval, &cformat);
+    pm_close(cifp);
+
+    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;
+        }
+    }
+
+    computeWeights(cxels, ccols, crows, newformat, cmaxval, !cmdline.nooffset,
+                   &rweights, &gweights, &bweights);
+
+    /* Handle certain special cases when runtime can be improved. */
+
+    determineConvolveType(cxels, &convolveType);
+
+    convolveIt(format, convolveType, 
+               (const float **)rweights, 
+               (const float **)gweights, 
+               (const float **)bweights);
+
+    pm_close(stdout);
+    pm_close(ifp);
+    return 0;
+}
+
+
+
+/******************************************************************************
+                            SOME CHANGE HISTORY
+*******************************************************************************
+
+ Version 2.0.1 Changes
+ ---------------------
+ Fixed four lines that were improperly allocated as sizeof( float ) when they
+ should have been sizeof( long ).
+
+ Version 2.0 Changes
+ -------------------
+
+ Version 2.0 was written by Mike Burns (derived from Jef Poskanzer's
+ original) in January 1995.
+
+ Reduce run time by general optimizations and handling special cases of
+ convolution matrices.  Program automatically determines if convolution 
+ matrix is one of the types it can make use of so no extra command line
+ arguments are necessary.
+
+ Examples of convolution matrices for the special cases are
+
+    Mean       Horizontal    Vertical
+    x x x        x x x        x y z
+    x x x        y y y        x y z
+    x x x        z z z        x y z
+
+ I don't know if the horizontal and vertical ones are of much use, but
+ after working on the mean convolution, it gave me ideas for the other two.
+
+ Some other compiler dependent optimizations
+ -------------------------------------------
+ Created separate functions as code was getting too large to put keep both
+ PGM and PPM cases in same function and also because SWITCH statement in
+ inner loop can take progressively more time the larger the size of the 
+ convolution matrix.  GCC is affected this way.
+
+ Removed use of MOD (%) operator from innermost loop by modifying manner in
+ which the current xelbuf[] is chosen.
+
+ This is from the file pnmconvol.README, dated August 1995, extracted in
+ April 2000, which was in the March 1994 Netpbm release:
+
+ ----------------------------------------------------------------------------- 
+ This is a faster version of the pnmconvol.c program that comes with netpbm.
+ There are no changes to the command line arguments, so this program can be
+ dropped in without affecting the way you currently run it.  An updated man
+ page is also included.
+ 
+ My original intention was to improve the running time of applying a
+ neighborhood averaging convolution matrix to an image by using a different
+ algorithm, but I also improved the run time of performing the general
+ convolution by optimizing that code.  The general convolution runs in 1/4 to
+ 1/2 of the original time and neighborhood averaging runs in near constant
+ time for the convolution masks I tested (3x3, 5x5, 7x7, 9x9).
+ 
+ Sample times for two computers are below.  Times are in seconds as reported
+ by /bin/time for a 512x512 pgm image.
+ 
+ Matrix                  IBM RS6000      SUN IPC
+ Size & Type                220
+ 
+ 3x3
+ original pnmconvol         6.3            18.4
+ new general case           3.1             6.0
+ new average case           1.8             2.6
+ 
+ 5x5
+ original pnmconvol        11.9            44.4
+ new general case           5.6            11.9
+ new average case           1.8             2.6
+ 
+ 7x7
+ original pnmconvol        20.3            82.9
+ new general case           9.4            20.7
+ new average case           1.8             2.6
+ 
+ 9x9
+ original pnmconvol        30.9           132.4
+ new general case          14.4            31.8
+ new average case           1.8             2.6
+ 
+ 
+ Send all questions/comments/bugs to me at burns@chem.psu.edu.
+ 
+ - Mike
+ 
+ ----------------------------------------------------------------------------
+ Mike Burns                                              System Administrator
+ burns@chem.psu.edu                                   Department of Chemistry
+ (814) 863-2123                             The Pennsylvania State University
+ ----------------------------------------------------------------------------
+
+*/
diff --git a/editor/pnmcrop.c b/editor/pnmcrop.c
new file mode 100644
index 00000000..5fafbaac
--- /dev/null
+++ b/editor/pnmcrop.c
@@ -0,0 +1,613 @@
+/* pnmcrop.c - crop a portable anymap
+**
+** Copyright (C) 1988 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.
+*/
+
+/* IDEA FOR EFFICIENCY IMPROVEMENT:
+
+   If we have to read the input into a regular file because it is not
+   seekable (a pipe), find the borders as we do the copy, so that we
+   do 2 passes through the file instead of 3.  Also find the background
+   color in that pass to save yet another pass with -sides.
+*/
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <assert.h>
+
+#include "pnm.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+enum bg_choice {BG_BLACK, BG_WHITE, BG_DEFAULT, BG_SIDES};
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFilespec;
+    enum bg_choice background;
+    unsigned int left, right, top, bottom;
+    unsigned int verbose;
+    unsigned int margin;
+    const char * borderfile;  /* NULL if none */
+};
+
+
+
+static void
+parseCommandLine(int argc, 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.
+-----------------------------------------------------------------------------*/
+    optEntry * option_def;
+        /* Instructions to OptParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int blackOpt, whiteOpt, sidesOpt;
+    unsigned int marginSpec, borderfileSpec;
+    
+    unsigned int option_def_index;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "black",      OPT_FLAG, NULL, &blackOpt,            0);
+    OPTENT3(0, "white",      OPT_FLAG, NULL, &whiteOpt,            0);
+    OPTENT3(0, "sides",      OPT_FLAG, NULL, &sidesOpt,            0);
+    OPTENT3(0, "left",       OPT_FLAG, NULL, &cmdlineP->left,      0);
+    OPTENT3(0, "right",      OPT_FLAG, NULL, &cmdlineP->right,     0);
+    OPTENT3(0, "top",        OPT_FLAG, NULL, &cmdlineP->top,       0);
+    OPTENT3(0, "bottom",     OPT_FLAG, NULL, &cmdlineP->bottom,    0);
+    OPTENT3(0, "verbose",    OPT_FLAG, NULL, &cmdlineP->verbose,   0);
+    OPTENT3(0, "margin",     OPT_UINT,   &cmdlineP->margin,    
+            &marginSpec,     0);
+    OPTENT3(0, "borderfile", OPT_STRING, &cmdlineP->borderfile,
+            &borderfileSpec, 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 (argc-1 == 0)
+        cmdlineP->inputFilespec = "-";  /* stdin */
+    else if (argc-1 == 1)
+        cmdlineP->inputFilespec = argv[1];
+    else 
+        pm_error("Too many arguments (%d).  "
+                 "Only need one: the input filespec", argc-1);
+
+    if (blackOpt && whiteOpt)
+        pm_error("You cannot specify both -black and -white");
+    else if (sidesOpt &&( blackOpt || whiteOpt ))
+        pm_error("You cannot specify both -sides and either -black or -white");
+    else if (blackOpt)
+        cmdlineP->background = BG_BLACK;
+    else if (whiteOpt)
+        cmdlineP->background = BG_WHITE;
+    else if (sidesOpt)
+        cmdlineP->background = BG_SIDES;
+    else
+        cmdlineP->background = BG_DEFAULT;
+
+    if (!cmdlineP->left && !cmdlineP->right && !cmdlineP->top
+        && !cmdlineP->bottom) {
+        cmdlineP->left = cmdlineP->right = cmdlineP->top 
+            = cmdlineP->bottom = TRUE;
+    }
+
+    if (!marginSpec)
+        cmdlineP->margin = 0;
+
+    if (!borderfileSpec)
+        cmdlineP->borderfile = NULL;
+}
+
+
+
+static xel
+background3Corners(FILE * const ifP,
+                   int    const rows,
+                   int    const cols,
+                   pixval const maxval,
+                   int    const format) {
+/*----------------------------------------------------------------------------
+  Read in the whole image, and check all the corners to determine the
+  background color.  This is a quite reliable way to determine the
+  background color.
+
+  Expect the file to be positioned to the start of the raster, and leave
+  it positioned arbitrarily.
+----------------------------------------------------------------------------*/
+    int row;
+    xel ** xels;
+    xel background;   /* our return value */
+
+    xels = pnm_allocarray(cols, rows);
+
+    for (row = 0; row < rows; ++row)
+        pnm_readpnmrow( ifP, xels[row], cols, maxval, format );
+
+    background = pnm_backgroundxel(xels, cols, rows, maxval, format);
+
+    pnm_freearray(xels, rows);
+
+    return background;
+}
+
+
+
+static xel
+background2Corners(FILE * const ifP,
+                   int    const cols,
+                   pixval const maxval,
+                   int    const format) {
+/*----------------------------------------------------------------------------
+  Look at just the top row of pixels and determine the background
+  color from the top corners; often this is enough to accurately
+  determine the background color.
+
+  Expect the file to be positioned to the start of the raster, and leave
+  it positioned arbitrarily.
+----------------------------------------------------------------------------*/
+    xel *xelrow;
+    xel background;   /* our return value */
+    
+    xelrow = pnm_allocrow(cols);
+
+    pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
+
+    background = pnm_backgroundxelrow(xelrow, cols, maxval, format);
+
+    pnm_freerow(xelrow);
+
+    return background;
+}
+
+
+
+static xel
+computeBackground(FILE *         const ifP,
+                  int            const cols,
+                  int            const rows,
+                  xelval         const maxval,
+                  int            const format,
+                  enum bg_choice const backgroundChoice,
+                  int            const verbose) {
+/*----------------------------------------------------------------------------
+   Determine what color is the background color of the image in file
+   *ifP, which is described by 'cols', 'rows', 'maxval', and 'format'.
+
+   'backgroundChoice' is the method we are to use in determining the
+   background color.
+   
+   Expect the file to be positioned to the start of the raster, and leave
+   it positioned arbitrarily.
+-----------------------------------------------------------------------------*/
+    xel background;  /* Our return value */
+    
+    switch (backgroundChoice) {
+    case BG_WHITE:
+	    background = pnm_whitexel(maxval, format);
+        break;
+    case BG_BLACK:
+	    background = pnm_blackxel(maxval, format);
+        break;
+    case BG_SIDES: 
+        background = 
+            background3Corners(ifP, rows, cols, maxval, format);
+        break;
+    case BG_DEFAULT: 
+        background = 
+            background2Corners(ifP, cols, maxval, format);
+        break;
+    }
+
+    if (verbose)
+        pm_message("Background color is %s", 
+                   ppm_colorname(&background, maxval, TRUE /*hexok*/));
+
+    return(background);
+}
+
+
+
+static void
+findBordersInImage(FILE *         const ifP,
+                   unsigned int   const cols,
+                   unsigned int   const rows,
+                   xelval         const maxval,
+                   int            const format,
+                   xel            const backgroundColor,
+                   bool           const verbose, 
+                   bool *         const hasBordersP,
+                   unsigned int * const leftP,
+                   unsigned int * const rightP, 
+                   unsigned int * const topP,
+                   unsigned int * const bottomP) {
+/*----------------------------------------------------------------------------
+   Find the left, right, top, and bottom borders in the image 'ifP'.
+   Return their sizes in pixels as *leftP, *rightP, *topP, and *bottomP.
+   
+   Iff the image is all background, *hasBordersP == FALSE.
+
+   Expect the input file to be positioned to the beginning of the
+   image raster and leave it positioned arbitrarily.
+-----------------------------------------------------------------------------*/
+    xel* xelrow;        /* A row of the input image */
+    int row;
+    bool gottop;
+    int left, right, bottom, top;
+        /* leftmost, etc. nonbackground pixel found so far; -1 for none */
+
+    xelrow = pnm_allocrow(cols);
+    
+    left   = cols;  /* initial value */
+    right  = -1;   /* initial value */
+    top    = rows;   /* initial value */
+    bottom = -1;  /* initial value */
+
+    gottop = FALSE;
+    for (row = 0; row < rows; ++row) {
+        int col;
+        int thisRowLeft;
+        int thisRowRight;
+
+        pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
+        
+        col = 0;
+        while (PNM_EQUAL(xelrow[col], backgroundColor) && col < cols)
+            ++col;
+        thisRowLeft = col;
+
+        col = cols-1;
+        while (col >= thisRowLeft && PNM_EQUAL(xelrow[col], backgroundColor))
+            --col;
+        thisRowRight = col + 1;
+
+        if (thisRowLeft < cols) {
+            /* This row is not entirely background */
+            
+            left  = MIN(thisRowLeft,  left);
+            right = MAX(thisRowRight, right);
+
+            if (!gottop) {
+                gottop = TRUE;
+                top = row;
+            }
+            bottom = row + 1;   /* New candidate */
+        }
+    }
+
+    if (right == -1)
+        *hasBordersP = FALSE;
+    else {
+        *hasBordersP = TRUE;
+        assert(right <= cols); assert(bottom <= rows);
+        *leftP       = left - 0;
+        *rightP      = cols - right;
+        *topP        = top - 0;
+        *bottomP     = rows - bottom;
+    }
+}
+
+
+
+static void
+findBordersInFile(const char *   const borderFileName,
+                  xel            const backgroundColor,
+                  bool           const verbose, 
+                  bool *         const hasBordersP,
+                  unsigned int * const leftP,
+                  unsigned int * const rightP, 
+                  unsigned int * const topP,
+                  unsigned int * const bottomP) {
+
+    FILE * borderFileP;
+    int cols;
+    int rows;
+    xelval maxval;
+    int format;
+    
+    borderFileP = pm_openr(borderFileName);
+    
+    pnm_readpnminit(borderFileP, &cols, &rows, &maxval, &format);
+    
+    findBordersInImage(borderFileP, cols, rows, maxval, format, 
+                       backgroundColor, verbose, hasBordersP,
+                       leftP, rightP, topP, bottomP);
+
+    pm_close(borderFileP);
+} 
+
+
+
+static void
+reportOneEdge(unsigned int const oldBorderSize,
+              unsigned int const newBorderSize,
+              const char * const place) {
+
+#define ending(n) (((n) > 1) ? "s" : "")
+
+    if (newBorderSize > oldBorderSize)
+        pm_message("Adding %u pixel%s to the %u-pixel %s border",
+                   newBorderSize - oldBorderSize,
+                   ending(newBorderSize - oldBorderSize),
+                   oldBorderSize, place);
+    else if (newBorderSize < oldBorderSize)
+        pm_message("Cropping %u pixel%s from the %u-pixel %s border",
+                   oldBorderSize - newBorderSize,
+                   ending(oldBorderSize - newBorderSize),
+                   oldBorderSize, place);
+    else
+        pm_message("Leaving %s border unchanged at %u pixel%s",
+                   place, oldBorderSize, ending(oldBorderSize));
+}        
+
+
+
+static void
+reportCroppingParameters(unsigned int const oldLeftBorderSize,
+                         unsigned int const oldRightBorderSize,
+                         unsigned int const oldTopBorderSize,
+                         unsigned int const oldBottomBorderSize,
+                         unsigned int const newLeftBorderSize,
+                         unsigned int const newRightBorderSize,
+                         unsigned int const newTopBorderSize,
+                         unsigned int const newBottomBorderSize) {
+
+    if (oldLeftBorderSize == 0 && oldRightBorderSize == 0 &&
+        oldTopBorderSize == 0 && oldBottomBorderSize == 0)
+        pm_message("No Border found.");
+
+    reportOneEdge(oldLeftBorderSize,   newLeftBorderSize,   "left"   );
+    reportOneEdge(oldRightBorderSize,  newRightBorderSize,  "right"  );
+    reportOneEdge(oldTopBorderSize,    newTopBorderSize,    "top"    );
+    reportOneEdge(oldBottomBorderSize, newBottomBorderSize, "bottom" );
+}
+
+
+
+
+static void
+fillRow(xel *        const xelrow,
+        unsigned int const cols,
+        xel          const color) {
+
+    unsigned int col;
+
+    for (col = 0; col < cols; ++col)
+        xelrow[col] = color;
+}
+
+
+
+static void
+writeCropped(FILE *       const ifP,
+             unsigned int const cols,
+             unsigned int const rows,
+             xelval       const maxval,
+             int          const format,
+             unsigned int const oldLeftBorder,
+             unsigned int const oldRightBorder,
+             unsigned int const oldTopBorder,
+             unsigned int const oldBottomBorder,
+             unsigned int const newLeftBorder,
+             unsigned int const newRightBorder,
+             unsigned int const newTopBorder,
+             unsigned int const newBottomBorder,
+             xel          const backgroundColor,
+             FILE *       const ofP) {
+
+    /* In order to do cropping, padding or both at the same time, we have
+       a rather complicated row buffer:
+
+       xelrow[] is both the input and the output buffer.  So it contains
+       the foreground pixels, the original border pixels, and the new
+       border pixels.
+
+       The foreground pixels are in the center of the
+       buffer, starting at Column 'foregroundLeft' and going to
+       'foregroundRight'.
+
+       There is space to the left of that for the larger of the input
+       left border and the output left border.
+
+       Similarly, there is space to the right of the foreground pixels
+       for the larger of the input right border and the output right
+       border.
+
+       We have to read an entire row, including the pixels we'll be
+       leaving out of the output, so we pick a starting location in
+       the buffer that lines up the first foreground pixel at
+       'foregroundLeft'.
+
+       When we output the row, we pick a starting location in the 
+       buffer that includes the proper number of left border pixels
+       before 'foregroundLeft'.
+
+       That's for the middle rows.  For the top and bottom, we just use
+       the left portion of xelrow[], starting at 0.
+    */
+
+    unsigned int const foregroundCols =
+        cols - oldLeftBorder - oldRightBorder;
+    unsigned int const outputCols     = 
+        foregroundCols + newLeftBorder + newRightBorder;
+    unsigned int const foregroundRows =
+        rows - oldTopBorder - oldBottomBorder;
+    unsigned int const outputRows     =
+        foregroundRows + newTopBorder + newBottomBorder;
+
+    unsigned int const foregroundLeft  = MAX(oldLeftBorder, newLeftBorder);
+        /* Index into xelrow[] of leftmost pixel of foreground */
+    unsigned int const foregroundRight = foregroundLeft + foregroundCols;
+        /* Index into xelrow[] just past rightmost pixel of foreground */
+
+    unsigned int const allocCols =
+        foregroundRight + MAX(oldRightBorder, newRightBorder);
+
+    xel *xelrow;
+    unsigned int i;
+
+    assert(outputCols == newLeftBorder + foregroundCols + newRightBorder);
+    assert(outputRows == newTopBorder + foregroundRows + newBottomBorder);
+    
+    pnm_writepnminit(ofP, outputCols, outputRows, maxval, format, 0);
+
+    xelrow = pnm_allocrow(allocCols);
+
+    /* Read off existing top border */
+    for (i = 0; i < oldTopBorder; ++i)
+        pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
+
+
+    /* Output new top border */
+    fillRow(xelrow, outputCols, backgroundColor);
+    for (i = 0; i < newTopBorder; ++i)
+        pnm_writepnmrow(ofP, xelrow, outputCols, maxval, format, 0);
+
+
+    /* Read and output foreground rows */
+    for (i = 0; i < foregroundRows; ++i) {
+        /* Set left border pixels */
+        fillRow(&xelrow[foregroundLeft - newLeftBorder], newLeftBorder,
+                backgroundColor);
+
+        /* Read foreground pixels */
+        pnm_readpnmrow(ifP, &(xelrow[foregroundLeft - oldLeftBorder]), cols,
+                       maxval, format);
+
+        /* Set right border pixels */
+        fillRow(&xelrow[foregroundRight], newRightBorder, backgroundColor);
+        
+        pnm_writepnmrow(ofP,
+                        &(xelrow[foregroundLeft - newLeftBorder]), outputCols,
+                        maxval, format, 0);
+    }
+
+    /* Read off existing bottom border */
+    for (i = 0; i < oldBottomBorder; ++i)
+        pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
+
+    /* Output new bottom border */
+    fillRow(xelrow, outputCols, backgroundColor);
+    for (i = 0; i < newBottomBorder; ++i)
+        pnm_writepnmrow(ofP, xelrow, outputCols, maxval, format, 0);
+
+    pnm_freerow(xelrow);
+}
+
+
+
+static void
+determineNewBorders(struct cmdlineInfo const cmdline,
+                    unsigned int       const leftBorderSize,
+                    unsigned int       const rightBorderSize,
+                    unsigned int       const topBorderSize,
+                    unsigned int       const bottomBorderSize,
+                    unsigned int *     const newLeftSizeP,
+                    unsigned int *     const newRightSizeP,
+                    unsigned int *     const newTopSizeP,
+                    unsigned int *     const newBottomSizeP) {
+
+    *newLeftSizeP   = cmdline.left   ? cmdline.margin : leftBorderSize   ;
+    *newRightSizeP  = cmdline.right  ? cmdline.margin : rightBorderSize  ;
+    *newTopSizeP    = cmdline.top    ? cmdline.margin : topBorderSize    ;
+    *newBottomSizeP = cmdline.bottom ? cmdline.margin : bottomBorderSize ;
+}
+        
+
+
+int
+main(int argc, char *argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;   
+        /* The program's regular input file.  Could be a seekable copy of it
+           in a temporary file.
+        */
+
+    xelval maxval;
+    int format;
+    int rows, cols;   /* dimensions of input image */
+    bool hasBorders;
+    unsigned int oldLeftBorder, oldRightBorder, oldTopBorder, oldBottomBorder;
+        /* The sizes of the borders in the input image */
+    unsigned int newLeftBorder, newRightBorder, newTopBorder, newBottomBorder;
+        /* The sizes of the borders in the output image */
+    xel background;
+    pm_filepos rasterpos;
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr_seekable(cmdline.inputFilespec);
+
+    pnm_readpnminit(ifP, &cols, &rows, &maxval, &format);
+
+    pm_tell2(ifP, &rasterpos, sizeof(rasterpos));
+
+    background = computeBackground(ifP, cols, rows, maxval, format,
+                                   cmdline.background, cmdline.verbose);
+
+    if (cmdline.borderfile) {
+        findBordersInFile(cmdline.borderfile,
+                          background, cmdline.verbose, &hasBorders,
+                          &oldLeftBorder, &oldRightBorder,
+                          &oldTopBorder,  &oldBottomBorder);
+    } else {
+        pm_seek2(ifP, &rasterpos, sizeof(rasterpos));
+
+        findBordersInImage(ifP, cols, rows, maxval, format, 
+                           background, cmdline.verbose, &hasBorders,
+                           &oldLeftBorder, &oldRightBorder,
+                           &oldTopBorder,  &oldBottomBorder);
+    }
+    if (!hasBorders)
+        pm_error("The image is entirely background; "
+                 "there is nothing to crop.");
+
+    determineNewBorders(cmdline, 
+                        oldLeftBorder, oldRightBorder,
+                        oldTopBorder,  oldBottomBorder,
+                        &newLeftBorder, &newRightBorder,
+                        &newTopBorder,  &newBottomBorder);
+
+    if (cmdline.verbose) 
+        reportCroppingParameters(oldLeftBorder, oldRightBorder,
+                                 oldTopBorder,  oldBottomBorder,
+                                 newLeftBorder, newRightBorder,
+                                 newTopBorder,  newBottomBorder);
+
+    pm_seek2(ifP, &rasterpos, sizeof(rasterpos));
+
+    writeCropped(ifP, cols, rows, maxval, format,
+                 oldLeftBorder, oldRightBorder,
+                 oldTopBorder,  oldBottomBorder,
+                 newLeftBorder, newRightBorder,
+                 newTopBorder,  newBottomBorder,
+                 background, stdout);
+
+    pm_close(stdout);
+    pm_close(ifP);
+    
+    return 0;
+}
diff --git a/editor/pnmcut.c b/editor/pnmcut.c
new file mode 100644
index 00000000..a21fcffb
--- /dev/null
+++ b/editor/pnmcut.c
@@ -0,0 +1,427 @@
+ /* pnmcut.c - cut a rectangle out of a portable anymap
+**
+** Copyright (C) 1989 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.
+*/
+
+#include <limits.h>
+#include "pnm.h"
+#include "shhopt.h"
+
+#define UNSPEC INT_MAX
+    /* UNSPEC is the value we use for an argument that is not specified
+       by the user.  Theoretically, the user could specify this value,
+       but we hope not.
+       */
+
+struct cmdline_info {
+    /* 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 */
+
+    /* The following describe the rectangle the user wants to cut out. 
+       the value UNSPEC for any of them indicates that value was not
+       specified.  A negative value means relative to the far edge.
+       'width' and 'height' are not negative.  These specifications 
+       do not necessarily describe a valid rectangle; they are just
+       what the user said.
+       */
+    int left;
+    int right;
+    int top;
+    int bottom;
+    int width;
+    int height;
+    int pad;
+
+    int verbose;
+};
+
+
+
+static xel black_xel;  /* A black xel */
+
+
+static void
+parse_command_line(int argc, char ** argv,
+                   struct cmdline_info *cmdline_p) {
+/*----------------------------------------------------------------------------
+   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.
+         */
+    optStruct2 opt;
+
+    unsigned int option_def_index;
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    OPTENTRY(0,   "left",       OPT_INT,    &cmdline_p->left,           0);
+    OPTENTRY(0,   "right",      OPT_INT,    &cmdline_p->right,          0);
+    OPTENTRY(0,   "top",        OPT_INT,    &cmdline_p->top,            0);
+    OPTENTRY(0,   "bottom",     OPT_INT,    &cmdline_p->bottom,         0);
+    OPTENTRY(0,   "width",      OPT_INT,    &cmdline_p->width,          0);
+    OPTENTRY(0,   "height",     OPT_INT,    &cmdline_p->height,         0);
+    OPTENTRY(0,   "pad",        OPT_FLAG,   &cmdline_p->pad,            0);
+    OPTENTRY(0,   "verbose",    OPT_FLAG,   &cmdline_p->verbose,        0);
+
+    /* Set the defaults */
+    cmdline_p->left = UNSPEC;
+    cmdline_p->right = UNSPEC;
+    cmdline_p->top = UNSPEC;
+    cmdline_p->bottom = UNSPEC;
+    cmdline_p->width = UNSPEC;
+    cmdline_p->height = UNSPEC;
+    cmdline_p->pad = FALSE;
+    cmdline_p->verbose = FALSE;
+
+    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 */
+
+    optParseOptions2(&argc, argv, opt, 0);
+        /* Uses and sets argc, argv, and some of *cmdline_p and others. */
+
+    if (cmdline_p->width < 0)
+        pm_error("-width may not be negative.");
+    if (cmdline_p->height < 0)
+        pm_error("-height may not be negative.");
+
+    if ((argc-1) != 0 && (argc-1) != 1 && (argc-1) != 4 && (argc-1) != 5)
+        pm_error("Wrong number of arguments.  "
+                 "Must be 0, 1, 4, or 5 arguments.");
+
+    switch (argc-1) {
+    case 0:
+        cmdline_p->input_filespec = "-";
+        break;
+    case 1:
+        cmdline_p->input_filespec = argv[1];
+        break;
+    case 4:
+    case 5: {
+        int warg, harg;  /* The "width" and "height" command line arguments */
+
+        if (sscanf(argv[1], "%d", &cmdline_p->left) != 1)
+            pm_error("Invalid number for left column argument");
+        if (sscanf(argv[2], "%d", &cmdline_p->top) != 1)
+            pm_error("Invalid number for top row argument");
+        if (sscanf(argv[3], "%d", &warg) != 1)
+            pm_error("Invalid number for width argument");
+        if (sscanf(argv[4], "%d", &harg) != 1)
+            pm_error("Invalid number for height argument");
+
+        if (warg > 0) {
+            cmdline_p->width = warg;
+            cmdline_p->right = UNSPEC;
+        } else {
+            cmdline_p->width = UNSPEC;
+            cmdline_p->right = warg -1;
+        }
+        if (harg > 0) {
+            cmdline_p->height = harg;
+            cmdline_p->bottom = UNSPEC;
+        } else {
+            cmdline_p->height = UNSPEC;
+            cmdline_p->bottom = harg - 1;
+        }
+
+        if (argc-1 == 4)
+            cmdline_p->input_filespec = "-";
+        else
+            cmdline_p->input_filespec = argv[5];
+        break;
+    }
+    }
+}
+
+
+
+static void
+compute_cut_bounds(const int cols, const int rows,
+                   const int leftarg, const int rightarg, 
+                   const int toparg, const int bottomarg,
+                   const int widtharg, const int heightarg,
+                   int * const leftcol_p, int * const rightcol_p,
+                   int * const toprow_p, int * const bottomrow_p) {
+/*----------------------------------------------------------------------------
+   From the values given on the command line 'leftarg', 'rightarg',
+   'toparg', 'bottomarg', 'widtharg', and 'heightarg', determine what
+   rectangle the user wants cut out.
+
+   Any of these arguments may be UNSPEC to indicate "not specified".
+   Any except 'widtharg' and 'heightarg' may be negative to indicate
+   relative to the far edge.  'widtharg' and 'heightarg' are positive.
+
+   Return the location of the rectangle as *leftcol_p, *rightcol_p,
+   *toprow_p, and *bottomrow_p.  
+-----------------------------------------------------------------------------*/
+
+    int leftcol, rightcol, toprow, bottomrow;
+        /* The left and right column numbers and top and bottom row numbers
+           specified by the user, except with negative values translated
+           into the actual values.
+
+           Note that these may very well be negative themselves, such
+           as when the user says "column -10" and there are only 5 columns
+           in the image.
+           */
+
+    /* Translate negative column and row into real column and row */
+    /* Exploit the fact that UNSPEC is a positive number */
+
+    if (leftarg >= 0)
+        leftcol = leftarg;
+    else
+        leftcol = cols + leftarg;
+    if (rightarg >= 0)
+        rightcol = rightarg;
+    else
+        rightcol = cols + rightarg;
+    if (toparg >= 0)
+        toprow = toparg;
+    else
+        toprow = rows + toparg;
+    if (bottomarg >= 0)
+        bottomrow = bottomarg;
+    else
+        bottomrow = rows + bottomarg;
+
+    /* Sort out left, right, and width specifications */
+
+    if (leftcol == UNSPEC && rightcol == UNSPEC && widtharg == UNSPEC) {
+        *leftcol_p = 0;
+        *rightcol_p = cols - 1;
+    }
+    if (leftcol == UNSPEC && rightcol == UNSPEC && widtharg != UNSPEC) {
+        *leftcol_p = 0;
+        *rightcol_p = 0 + widtharg - 1;
+    }
+    if (leftcol == UNSPEC && rightcol != UNSPEC && widtharg == UNSPEC) {
+        *leftcol_p = 0;
+        *rightcol_p = rightcol;
+    }
+    if (leftcol == UNSPEC && rightcol != UNSPEC && widtharg != UNSPEC) {
+        *leftcol_p = rightcol - widtharg + 1;
+        *rightcol_p = rightcol;
+    }
+    if (leftcol != UNSPEC && rightcol == UNSPEC && widtharg == UNSPEC) {
+        *leftcol_p = leftcol;
+        *rightcol_p = cols - 1;
+    }
+    if (leftcol != UNSPEC && rightcol == UNSPEC && widtharg != UNSPEC) {
+        *leftcol_p = leftcol;
+        *rightcol_p = leftcol + widtharg - 1;
+    }
+    if (leftcol != UNSPEC && rightcol != UNSPEC && widtharg == UNSPEC) {
+        *leftcol_p = leftcol;
+        *rightcol_p = rightcol;
+    }
+    if (leftcol != UNSPEC && rightcol != UNSPEC && widtharg != UNSPEC) {
+        pm_error("You may not specify left, right, and width.\n"
+                 "Choose at most two of these.");
+    }
+
+
+    /* Sort out top, bottom, and height specifications */
+
+    if (toprow == UNSPEC && bottomrow == UNSPEC && heightarg == UNSPEC) {
+        *toprow_p = 0;
+        *bottomrow_p = rows - 1;
+    }
+    if (toprow == UNSPEC && bottomrow == UNSPEC && heightarg != UNSPEC) {
+        *toprow_p = 0;
+        *bottomrow_p = 0 + heightarg - 1;
+    }
+    if (toprow == UNSPEC && bottomrow != UNSPEC && heightarg == UNSPEC) {
+        *toprow_p = 0;
+        *bottomrow_p = bottomrow;
+    }
+    if (toprow == UNSPEC && bottomrow != UNSPEC && heightarg != UNSPEC) {
+        *toprow_p = bottomrow - heightarg + 1;
+        *bottomrow_p = bottomrow;
+    }
+    if (toprow != UNSPEC && bottomrow == UNSPEC && heightarg == UNSPEC) {
+        *toprow_p = toprow;
+        *bottomrow_p = rows - 1;
+    }
+    if (toprow != UNSPEC && bottomrow == UNSPEC && heightarg != UNSPEC) {
+        *toprow_p = toprow;
+        *bottomrow_p = toprow + heightarg - 1;
+    }
+    if (toprow != UNSPEC && bottomrow != UNSPEC && heightarg == UNSPEC) {
+        *toprow_p = toprow;
+        *bottomrow_p = bottomrow;
+    }
+    if (toprow != UNSPEC && bottomrow != UNSPEC && heightarg != UNSPEC) {
+        pm_error("You may not specify top, bottom, and height.\n"
+                 "Choose at most two of these.");
+    }
+
+}
+
+
+
+static void
+reject_out_of_bounds(const int cols, const int rows, 
+                     const int leftcol, const int rightcol, 
+                     const int toprow, const int bottomrow) {
+
+    /* Reject coordinates off the edge */
+
+    if (leftcol < 0)
+        pm_error("You have specified a left edge (%d) that is beyond\n"
+                 "the left edge of the image (0)", leftcol);
+    if (rightcol > cols-1)
+        pm_error("You have specified a right edge (%d) that is beyond\n"
+                 "the right edge of the image (%d)", rightcol, cols-1);
+    if (rightcol < 0)
+        pm_error("You have specified a right edge (%d) that is beyond\n"
+                 "the left edge of the image (0)", rightcol);
+    if (rightcol > cols-1)
+        pm_error("You have specified a right edge (%d) that is beyond\n"
+                 "the right edge of the image (%d)", rightcol, cols-1);
+    if (leftcol > rightcol) 
+        pm_error("You have specified a left edge (%d) that is to the right\n"
+                 "of the right edge you specified (%d)", 
+                 leftcol, rightcol);
+    
+    if (toprow < 0)
+        pm_error("You have specified a top edge (%d) that is above the top "
+                 "edge of the image (0)", toprow);
+    if (bottomrow > rows-1)
+        pm_error("You have specified a bottom edge (%d) that is below the\n"
+                 "bottom edge of the image (%d)", bottomrow, rows-1);
+    if (bottomrow < 0)
+        pm_error("You have specified a bottom edge (%d) that is above the\n"
+                 "top edge of the image (0)", bottomrow);
+    if (bottomrow > rows-1)
+        pm_error("You have specified a bottom edge (%d) that is below the\n"
+                 "bottom edge of the image (%d)", bottomrow, rows-1);
+    if (toprow > bottomrow) 
+        pm_error("You have specified a top edge (%d) that is below\n"
+                 "the bottom edge you specified (%d)", 
+                 toprow, bottomrow);
+}
+
+
+
+static void
+write_black_rows(FILE *outfile, const int rows, const int cols, 
+                 xel * const output_row,
+                 const pixval maxval, const int format) {
+/*----------------------------------------------------------------------------
+   Write out to file 'outfile' 'rows' rows of 'cols' black xels each,
+   part of an image of format 'format' with maxval 'maxval'.  
+
+   Use *output_row as a buffer.  It is at least 'cols' xels wide.
+-----------------------------------------------------------------------------*/
+    int row;
+    for (row = 0; row < rows; row++) {
+        int col;
+        for (col = 0; col < cols; col++) output_row[col] = black_xel;
+        pnm_writepnmrow(outfile, output_row, cols, maxval, format, 0);
+    }
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    FILE* ifp;
+    xel* xelrow;  /* Row from input image */
+    xel* output_row;  /* Row of output image */
+    xelval maxval;
+    int rows, cols, format, row;
+    int leftcol, rightcol, toprow, bottomrow;
+    int output_cols;  /* Width of output image */
+    struct cmdline_info cmdline;
+
+    pnm_init( &argc, argv );
+
+    parse_command_line(argc, argv, &cmdline);
+
+    ifp = pm_openr(cmdline.input_filespec);
+
+    pnm_readpnminit(ifp, &cols, &rows, &maxval, &format);
+    xelrow = pnm_allocrow(cols);
+
+    black_xel = pnm_blackxel(maxval, format);
+
+    compute_cut_bounds(cols, rows, 
+                       cmdline.left, cmdline.right, 
+                       cmdline.top, cmdline.bottom, 
+                       cmdline.width, cmdline.height, 
+                       &leftcol, &rightcol, &toprow, &bottomrow);
+
+    if (!cmdline.pad)
+        reject_out_of_bounds(cols, rows, leftcol, rightcol, toprow, bottomrow);
+
+    if (cmdline.verbose) {
+        pm_message("Image goes from Row 0, Column 0 through Row %d, Column %d",
+                   rows-1, cols-1);
+        pm_message("Cutting from Row %d, Column %d through Row %d Column %d",
+                   toprow, leftcol, bottomrow, rightcol);
+    }
+
+    output_cols = rightcol-leftcol+1;
+    output_row = pnm_allocrow(output_cols);
+    
+    pnm_writepnminit(stdout, output_cols, bottomrow-toprow+1, 
+                     maxval, format, 0 );
+
+    /* Implementation note:  If speed is ever an issue, we can probably
+       speed up significantly the non-padding case by writing a special
+       case loop here for the case cmdline.pad == FALSE.
+       */
+
+    /* Write out top padding */
+    write_black_rows(stdout, 0 - toprow, output_cols, output_row, 
+                     maxval, format);
+    
+    /* Read input and write out rows extracted from it */
+    for (row = 0; row < rows; row++) {
+        pnm_readpnmrow(ifp, xelrow, cols, maxval, format);
+        if (row >= toprow && row <= bottomrow) {
+            int col;
+            /* Put in left padding */
+            for (col = leftcol; col < 0; col++) { 
+                output_row[col-leftcol] = black_xel;
+            }
+            /* Put in extracted columns */
+            for (col = MAX(leftcol, 0); col <= MIN(rightcol, cols-1); col++) {
+                output_row[col-leftcol] = xelrow[col];
+            }
+            /* Put in right padding */
+            for (col = MAX(cols, leftcol); col <= rightcol; col++) {
+                output_row[col-leftcol] = black_xel;
+            }
+            pnm_writepnmrow(stdout, output_row, output_cols, 
+                            maxval, format, 0);
+        }
+    }
+    /* Note that we may be tempted just to quit after reaching the bottom
+       of the extracted image, but that would cause a broken pipe problem
+       for the process that's feeding us the image.
+       */
+    /* Write out bottom padding */
+    write_black_rows(stdout, bottomrow - (rows-1), output_cols, output_row, 
+                     maxval, format);
+
+    pnm_freerow(output_row);
+    pnm_freerow(xelrow);
+    pm_close(ifp);
+    pm_close(stdout);
+    
+    exit( 0 );
+}
+
diff --git a/editor/pnmflip b/editor/pnmflip
new file mode 100755
index 00000000..6149aaa2
--- /dev/null
+++ b/editor/pnmflip
@@ -0,0 +1,80 @@
+#!/usr/bin/perl -w
+
+#============================================================================
+#  This is a compatibility interface to Pamflip.
+#
+#  It exists so existing programs and procedures that rely on Pnmflip
+#  syntax continue to work.  You should not make new use of Pnmflip and
+#  if you modify an old use, you should upgrade it to use Pamflip.
+#
+#  The one way that Pamflip is not backward compatible with Pnmflip is
+#  that with Pnmflip, you can do this:
+#
+#     pnmflip -xy -tb
+#
+#  and that causes pnmflip to do both transformations (i.e. the same thing
+#  as -r270).  With Pamflip, you can't specify multiple (or zero) flip
+#  type options.  Instead, you would use the -xform option:
+#
+#    pamflip -xform=transpose,topbottom
+#
+#============================================================================
+
+use strict;
+use File::Basename;
+use Cwd 'abs_path';
+
+my $xformOpt;
+my @miscOptions;
+my $infile;
+
+$xformOpt = '-xform=';  # initial value 
+
+@miscOptions = ();
+
+foreach (@ARGV) {
+    if (/^-/) {
+        # It's an option
+        if (/^-lr$/ || /^-le.*$/) {
+            $xformOpt .= "leftright,";
+        } elsif (/^-tb$/ || /^-to.*$/) {
+            $xformOpt .= "topbottom,";
+        } elsif (/^-x.*$/ || /^-tr.*$/) {
+            $xformOpt .= "transpose,";
+        } elsif (/^-r9.*$/ || /^-rotate9.*$/ || /^-cc.*$/) {
+            $xformOpt .= "transpose,topbottom,";
+        } elsif (/^-r1.*$/ || /^-rotate1.*$/) {
+            $xformOpt .= "leftright,topbottom,";
+        } elsif (/^-r2.*$/ || /^-rotate2.*$/ || /^-cw$/) {
+            $xformOpt .= "transpose,leftright,";
+        } else {
+            # It's not a transformation option; could be a Netpbm common
+            # option.
+            push(@miscOptions, $_);
+        }
+    } else {
+        # It's a parameter
+        if (defined($infile)) {
+            print(STDERR
+                  "You may specify at most one non-option parameter.\n");
+        } else {
+            $infile = $_;
+        }
+    }
+}
+
+# Finish off the -xform option by removing any trailing comma
+
+$/ = ',';
+chomp($xformOpt);
+
+my $infileParm = defined($infile) ? $infile : "-";
+
+# We want to get Pamflip from the same directory we came from if
+# it's there.  Frequently, the directory containing Netpbm programs is
+# not in the PATH and we were invoked by absolute path.
+
+my $my_directory = abs_path(dirname($0));
+$ENV{"PATH"} = $my_directory . ":" . $ENV{"PATH"};
+
+exec("pamflip", @miscOptions, $xformOpt, $infileParm);
diff --git a/editor/pnmgamma.c b/editor/pnmgamma.c
new file mode 100644
index 00000000..e13075ff
--- /dev/null
+++ b/editor/pnmgamma.c
@@ -0,0 +1,753 @@
+/* pnmgamma.c - perform gamma correction on a PNM image
+**
+** Copyright (C) 1991 by Bill Davidson and 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.
+*/
+
+#include <math.h>
+#include <ctype.h>
+
+#include "shhopt.h"
+#include "mallocvar.h"
+#include "pnm.h"
+
+enum transferFunction {
+    XF_EXP,
+    XF_EXP_INVERSE,
+    XF_BT709RAMP,
+    XF_BT709RAMP_INVERSE,
+    XF_SRGBRAMP,
+    XF_SRGBRAMP_INVERSE,
+    XF_BT709_TO_SRGB,
+    XF_SRGB_TO_BT709
+};
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char *filespec;  /* '-' if stdin */
+    enum transferFunction transferFunction;
+    float rgamma, ggamma, bgamma;
+    unsigned int maxval;
+    unsigned int makeNewMaxval;
+};
+
+
+
+static void
+interpretOldArguments(int                  const argc,
+                      char **              const argv,
+                      float                const defaultGamma,
+                      struct cmdlineInfo * const cmdlineP) {
+
+    /* Use the old syntax wherein the gamma values come from arguments.
+       If there is one argument, it's a gamma value for all three
+       components.  If 3 arguments, it's separate gamma values.  If
+       2, it's a single gamma value plus a file name.  If 4, it's
+       separate gamma values plus a file name.
+    */
+    if (argc-1 == 0) {
+        cmdlineP->rgamma = defaultGamma;
+        cmdlineP->ggamma = defaultGamma;
+        cmdlineP->bgamma = defaultGamma;
+        cmdlineP->filespec = "-";
+    } else if (argc-1 == 1) {
+        cmdlineP->rgamma = atof(argv[1]);
+        cmdlineP->ggamma = atof(argv[1]);
+        cmdlineP->bgamma = atof(argv[1]);
+        cmdlineP->filespec = "-";
+    } else if (argc-1 == 2) {
+        cmdlineP->rgamma = atof(argv[1]);
+        cmdlineP->ggamma = atof(argv[1]);
+        cmdlineP->bgamma = atof(argv[1]);
+        cmdlineP->filespec = argv[2];
+    } else if (argc-1 == 3) {
+        cmdlineP->rgamma = atof(argv[1]);
+        cmdlineP->ggamma = atof(argv[2]);
+        cmdlineP->bgamma = atof(argv[3]);
+        cmdlineP->filespec = "-";
+    } else if (argc-1 == 4) {
+        cmdlineP->rgamma = atof(argv[1]);
+        cmdlineP->ggamma = atof(argv[2]);
+        cmdlineP->bgamma = atof(argv[3]);
+        cmdlineP->filespec = argv[4];
+    } else 
+        pm_error("Wrong number of arguments.  "
+                 "You may have 0, 1, or 3 gamma values "
+                 "plus zero or one filename");
+        
+    if (cmdlineP->rgamma <= 0.0 || 
+        cmdlineP->ggamma <= 0.0 || 
+        cmdlineP->bgamma <= 0.0 )
+        pm_error("Invalid gamma value.  Must be positive floating point "
+                 "number.");
+}
+
+
+
+static void
+getGammaFromOpts(struct cmdlineInfo * const cmdlineP,
+                 bool                 const gammaSpec,
+                 float                const gammaOpt,
+                 bool                 const rgammaSpec,
+                 bool                 const ggammaSpec,
+                 bool                 const bgammaSpec,
+                 float                const defaultGamma) {
+
+    if (gammaSpec)
+        if (gammaOpt < 0.0)
+            pm_error("Invalid gamma value.  "
+                         "Must be positive floating point number.");
+    
+    if (rgammaSpec) {
+        if (cmdlineP->rgamma < 0.0)
+            pm_error("Invalid gamma value.  "
+                     "Must be positive floating point number.");
+    } else {
+        if (gammaSpec)
+            cmdlineP->rgamma = gammaOpt;
+        else 
+            cmdlineP->rgamma = defaultGamma;
+    }
+    if (ggammaSpec) {
+        if (cmdlineP->ggamma < 0.0) 
+            pm_error("Invalid gamma value.  "
+                     "Must be positive floating point number.");
+    } else {
+        if (gammaSpec)
+            cmdlineP->ggamma = gammaOpt;
+        else 
+            cmdlineP->ggamma = defaultGamma;
+    }
+    if (bgammaSpec) {
+        if (cmdlineP->bgamma < 0.0)
+            pm_error("Invalid gamma value.  "
+                     "Must be positive floating point number.");
+    } else {
+        if (gammaSpec)
+            cmdlineP->bgamma = gammaOpt;
+        else
+            cmdlineP->bgamma = defaultGamma;
+    }
+}
+
+
+
+static void
+parseCommandLine(int argc, char ** argv, 
+                 struct cmdlineInfo * const cmdlineP) {
+
+    optEntry *option_def;
+        /* Instructions to optParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int bt709ramp, srgbramp, ungamma, bt709tosrgb, srgbtobt709;
+    unsigned int bt709tolinear, lineartobt709;
+    unsigned int gammaSpec, rgammaSpec, ggammaSpec, bgammaSpec;
+    float gammaOpt;
+    float defaultGamma;
+    unsigned int option_def_index;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "ungamma",       OPT_FLAG,   NULL,
+            &ungamma,                 0);
+    OPTENT3(0, "bt709tolinear", OPT_FLAG,   NULL,
+            &bt709tolinear,           0);
+    OPTENT3(0, "lineartobt709", OPT_FLAG,   NULL,
+            &lineartobt709,           0);
+    OPTENT3(0, "bt709ramp",     OPT_FLAG,   NULL,
+            &bt709ramp,               0);
+    OPTENT3(0, "cieramp",       OPT_FLAG,   NULL,
+            &bt709ramp,               0);
+    OPTENT3(0, "srgbramp",      OPT_FLAG,   NULL,
+            &srgbramp,                0);
+    OPTENT3(0, "bt709tosrgb",   OPT_FLAG,   NULL,
+            &bt709tosrgb,             0);
+    OPTENT3(0, "srgbtobt709",   OPT_FLAG,   NULL,
+            &srgbtobt709,             0);
+    OPTENT3(0, "maxval",        OPT_UINT,   &cmdlineP->maxval,
+            &cmdlineP->makeNewMaxval, 0);
+    OPTENT3(0, "gamma",         OPT_FLOAT,  &gammaOpt,
+            &gammaSpec,               0);
+    OPTENT3(0, "rgamma",        OPT_FLOAT,  &cmdlineP->rgamma,
+            &rgammaSpec,              0);
+    OPTENT3(0, "ggamma",        OPT_FLOAT,  &cmdlineP->ggamma,
+            &ggammaSpec,              0);
+    OPTENT3(0, "bgamma",        OPT_FLOAT,  &cmdlineP->bgamma,
+            &bgammaSpec,              0);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = TRUE; 
+
+    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdline_p and others. */
+
+    if (bt709tolinear + lineartobt709 + bt709ramp + srgbramp +
+        bt709tosrgb + srgbtobt709 > 1)
+        pm_error("You may specify only one function option");
+    else {
+        if (bt709tolinear) {
+            if (ungamma)
+                pm_error("You cannot specify -ungamma with -bt709tolinear");
+            else
+                cmdlineP->transferFunction = XF_BT709RAMP_INVERSE;
+        } else if (lineartobt709) {
+            if (ungamma)
+                pm_error("You cannot specify -ungamma with -lineartobt709");
+            else
+                cmdlineP->transferFunction = XF_BT709RAMP;
+        } else if (bt709tosrgb) {
+            if (ungamma)
+                pm_error("You cannot specify -ungamma with -bt709tosrgb");
+            else
+                cmdlineP->transferFunction = XF_BT709_TO_SRGB;
+        } else if (srgbtobt709) {
+            if (ungamma)
+                pm_error("You cannot specify -ungamma with -srgbtobt709");
+            else
+                cmdlineP->transferFunction = XF_SRGB_TO_BT709;
+        } else if (bt709ramp) {
+            if (ungamma)
+                cmdlineP->transferFunction = XF_BT709RAMP_INVERSE;
+            else
+                cmdlineP->transferFunction = XF_BT709RAMP;
+        } else if (srgbramp) {
+            if (ungamma)
+                cmdlineP->transferFunction = XF_SRGBRAMP_INVERSE;
+            else
+                cmdlineP->transferFunction = XF_SRGBRAMP;
+        } else {
+            if (ungamma)
+                cmdlineP->transferFunction = XF_EXP_INVERSE;
+            else
+                cmdlineP->transferFunction = XF_EXP;
+        }
+    }
+
+    if (cmdlineP->makeNewMaxval) {
+        if (cmdlineP->maxval > PNM_OVERALLMAXVAL)
+            pm_error("Largest possible maxval is %u.  You specified %u",
+                     PNM_OVERALLMAXVAL, cmdlineP->maxval);
+    }
+
+    switch (cmdlineP->transferFunction) {
+    case XF_BT709RAMP:
+    case XF_BT709RAMP_INVERSE:
+    case XF_SRGB_TO_BT709:
+        defaultGamma = 1.0/0.45;
+        break;
+    case XF_SRGBRAMP:
+    case XF_SRGBRAMP_INVERSE:
+    case XF_BT709_TO_SRGB:
+        /* The whole function is often approximated with
+           exponent 2.2 and no linear piece.  We do the linear
+           piece, so we use the real exponent of 2.4.
+        */
+        defaultGamma = 2.4;
+        break;
+    case XF_EXP:
+    case XF_EXP_INVERSE:
+        defaultGamma = 2.2;
+        break;
+    }
+
+    if (bt709tolinear || lineartobt709 || bt709tosrgb || srgbtobt709) {
+        /* Use the new syntax wherein the gamma values come from options,
+           not arguments.  So if there's an argument, it's a file name.
+        */
+        getGammaFromOpts(cmdlineP, gammaSpec, gammaOpt,
+                         rgammaSpec, ggammaSpec, bgammaSpec, defaultGamma);
+
+        if (argc-1 < 1)
+            cmdlineP->filespec = "-";
+        else {
+            cmdlineP->filespec = argv[1];
+            if (argc-1 > 1)
+                pm_error("Too many arguments (%u).  With this function, there "
+                         "is at most one argument:  the file name", argc-1);
+        }
+    } else {
+        if (gammaSpec || rgammaSpec || ggammaSpec || bgammaSpec)
+            pm_error("With this function, you specify the gamma values in "
+                     "arguments, not with the -gamma, etc.");
+        interpretOldArguments(argc, argv, defaultGamma, cmdlineP);
+    }
+}
+
+
+
+static void
+buildPowGamma(xelval       table[],
+              xelval const maxval,
+              xelval const newMaxval,
+              double const gamma) {
+/*----------------------------------------------------------------------------
+   Build a gamma table of size maxval+1 for the given gamma value.
+  
+   This function depends on pow(3m).  If you don't have it, you can
+   simulate it with '#define pow(x,y) exp((y)*log(x))' provided that
+   you have the exponential function exp(3m) and the natural logarithm
+   function log(3m).  I can't believe I actually remembered my log
+   identities.
+-----------------------------------------------------------------------------*/
+    xelval i;
+    double const oneOverGamma = 1.0 / gamma;
+
+    for (i = 0 ; i <= maxval; ++i) {
+        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 */
+    }
+}
+
+
+
+static void
+buildBt709Gamma(xelval       table[],
+                xelval const maxval,
+                xelval const newMaxval,
+                double const gamma) {
+/*----------------------------------------------------------------------------
+   Build a gamma table of size maxval+1 for the ITU Recommendation
+   BT.709 gamma transfer function.
+
+   'gamma' must be 1/0.45 for true Rec. 709.
+-----------------------------------------------------------------------------*/
+    double const oneOverGamma = 1.0 / gamma;
+    xelval i;
+
+    /* This transfer function is linear for sample values 0
+       .. maxval*.018 and an exponential for larger sample values.
+       The exponential is slightly stretched and translated, though,
+       unlike the popular pure exponential gamma transfer function.
+    */
+    xelval const linearCutoff = (xelval) (maxval * 0.018 + 0.5);
+    double const linearExpansion = 
+        (1.099 * pow(0.018, oneOverGamma) - 0.099) / 0.018;
+    double const maxvalScaler = (double)newMaxval/maxval;
+
+    for (i = 0; i <= linearCutoff; ++i) 
+        table[i] = i * linearExpansion * maxvalScaler + 0.5;
+    for (; i <= maxval; ++i) {
+        double const normalized = ((double) i) / maxval;
+            /* Xel sample value normalized to 0..1 */
+        double const v = 1.099 * pow(normalized, oneOverGamma) - 0.099;
+        table[i] = MIN((xelval)(v * newMaxval + 0.5), newMaxval);  
+            /* denormalize, round, and clip */
+    }
+}
+
+
+
+static void
+buildBt709GammaInverse(xelval       table[],
+                       xelval const maxval,
+                       xelval const newMaxval,
+                       double const gamma) {
+/*----------------------------------------------------------------------------
+   Build a gamma table of size maxval+1 for the Inverse of the ITU
+   Rec. BT.709 gamma transfer function.
+
+   'gamma' must be 1/0.45 for true Rec. 709.
+-----------------------------------------------------------------------------*/
+    double const oneOverGamma = 1.0 / gamma;
+    xelval i;
+
+    /* This transfer function is linear for sample values 0
+       .. maxval*.018 and an exponential for larger sample values.
+       The exponential is slightly stretched and translated, though,
+       unlike the popular pure exponential gamma transfer function.
+    */
+
+    xelval const linearCutoff = (xelval) (maxval * 0.018 + 0.5);
+    double const linearCompression = 
+        0.018 / (1.099 * pow(0.018, oneOverGamma) - 0.099);
+    double const maxvalScaler = (double)newMaxval/maxval;
+
+    for (i = 0; i <= linearCutoff / linearCompression; ++i) 
+        table[i] = i * linearCompression * maxvalScaler + 0.5;
+
+    for (; i <= maxval; ++i) {
+        double const normalized = ((double) i) / maxval;
+            /* Xel sample value normalized to 0..1 */
+        double const v = pow((normalized + 0.099) / 1.099, gamma);
+        table[i] = MIN((xelval)(v * newMaxval + 0.5), newMaxval);  
+            /* denormalize, round, and clip */
+    }
+}
+
+
+
+static void
+buildSrgbGamma(xelval       table[],
+               xelval const maxval,
+               xelval const newMaxval,
+               double const gamma) {
+/*----------------------------------------------------------------------------
+   Build a gamma table of size maxval+1 for the IEC SRGB gamma
+   transfer function (Standard IEC 61966-2-1).
+
+   'gamma' must be 2.4 for true SRGB
+-----------------------------------------------------------------------------*/
+    double const oneOverGamma = 1.0 / gamma;
+    xelval i;
+
+    /* This transfer function is linear for sample values 0
+       .. maxval*.040405 and an exponential for larger sample values.
+       The exponential is slightly stretched and translated, though,
+       unlike the popular pure exponential gamma transfer function.
+    */
+    xelval const linearCutoff = (xelval) maxval * 0.0031308 + 0.5;
+    double const linearExpansion = 
+        (1.055 * pow(0.0031308, oneOverGamma) - 0.055) / 0.0031308;
+    double const maxvalScaler = (double)newMaxval/maxval;
+
+    for (i = 0; i <= linearCutoff; ++i) 
+        table[i] = i * linearExpansion * maxvalScaler + 0.5;
+    for (; i <= maxval; ++i) {
+        double const normalized = ((double) i) / maxval;
+            /* Xel sample value normalized to 0..1 */
+        double const v = 1.055 * pow(normalized, oneOverGamma) - 0.055;
+        table[i] = MIN((xelval)(v * newMaxval + 0.5), newMaxval);  
+            /* denormalize, round, and clip */
+    }
+}
+
+
+
+static void
+buildSrgbGammaInverse(xelval       table[],
+                      xelval const maxval,
+                      xelval const newMaxval,
+                      double const gamma) {
+/*----------------------------------------------------------------------------
+   Build a gamma table of size maxval+1 for the Inverse of the IEC SRGB gamma
+   transfer function (Standard IEC 61966-2-1).
+
+   'gamma' must be 2.4 for true SRGB
+-----------------------------------------------------------------------------*/
+    double const oneOverGamma = 1.0 / gamma;
+    xelval i;
+
+    /* This transfer function is linear for sample values 0
+       .. maxval*.040405 and an exponential for larger sample values.
+       The exponential is slightly stretched and translated, though,
+       unlike the popular pure exponential gamma transfer function.
+    */
+    xelval const linearCutoff = (xelval) maxval * 0.0031308 + 0.5;
+    double const linearCompression = 
+        0.0031308 / (1.055 * pow(0.0031308, oneOverGamma) - 0.055);
+    double const maxvalScaler = (double)newMaxval/maxval;
+
+    for (i = 0; i <= linearCutoff / linearCompression; ++i) 
+        table[i] = i * linearCompression * maxvalScaler + 0.5;
+    for (; i <= maxval; ++i) {
+        double const normalized = ((double) i) / maxval;
+            /* Xel sample value normalized to 0..1 */
+        double const v = pow((normalized + 0.055) / 1.055, gamma);
+        table[i] = MIN((xelval)(v * newMaxval + 0.5), newMaxval);  
+            /* denormalize, round, and clip */
+    }
+}
+
+
+
+static void
+buildBt709ToSrgbGamma(xelval       table[],
+                      xelval const maxval,
+                      xelval const newMaxval,
+                      double const gammaSrgb) {
+/*----------------------------------------------------------------------------
+   Build a gamma table of size maxval+1 for the combination of the
+   inverse of ITU Rec BT.709 and the forward SRGB gamma transfer
+   functions.  I.e. this converts from Rec 709 to SRGB.
+
+   'gammaSrgb' must be 2.4 for true SRGB.
+-----------------------------------------------------------------------------*/
+    double const oneOverGamma709  = 0.45;
+    double const gamma709         = 1.0 / oneOverGamma709;
+    double const oneOverGammaSrgb = 1.0 / gammaSrgb;
+    double const normalizer       = 1.0 / maxval;
+
+    /* This transfer function is linear for sample values 0
+       .. maxval*.018 and an exponential for larger sample values.
+       The exponential is slightly stretched and translated, though,
+       unlike the popular pure exponential gamma transfer function.
+    */
+
+    xelval const linearCutoff709 = (xelval) (maxval * 0.018 + 0.5);
+    double const linearCompression709 = 
+        0.018 / (1.099 * pow(0.018, oneOverGamma709) - 0.099);
+
+    double const linearCutoffSrgb = 0.0031308;
+    double const linearExpansionSrgb = 
+        (1.055 * pow(0.0031308, oneOverGammaSrgb) - 0.055) / 0.0031308;
+
+    xelval i;
+
+    for (i = 0; i <= maxval; ++i) {
+        double const normalized = i * normalizer;
+            /* Xel sample value normalized to 0..1 */
+        double radiance;
+        double srgb;
+
+        if (i < linearCutoff709 / linearCompression709)
+            radiance = normalized * linearCompression709;
+        else
+            radiance = pow((normalized + 0.099) / 1.099, gamma709);
+
+        if (radiance < linearCutoffSrgb)
+            srgb = radiance * linearExpansionSrgb;
+        else
+            srgb = 1.055 * pow(normalized, oneOverGammaSrgb) - 0.055;
+
+        table[i] = srgb * newMaxval + 0.5;
+    }
+}
+
+
+
+static void
+buildSrgbToBt709Gamma(xelval       table[],
+                      xelval const maxval,
+                      xelval const newMaxval,
+                      double const gamma709) {
+/*----------------------------------------------------------------------------
+   Build a gamma table of size maxval+1 for the combination of the
+   inverse of SRGB and the forward ITU Rec BT.709 gamma transfer
+   functions.  I.e. this converts from SRGB to Rec 709.
+
+   'gamma709' must be 1/0.45 for true Rec. 709.
+-----------------------------------------------------------------------------*/
+    double const oneOverGamma709  = 1.0 / gamma709;
+    double const gammaSrgb        = 2.4;
+    double const oneOverGammaSrgb = 1.0 / gammaSrgb;
+    double const normalizer       = 1.0 / maxval;
+
+    /* This transfer function is linear for sample values 0
+       .. maxval*.040405 and an exponential for larger sample values.
+       The exponential is slightly stretched and translated, though,
+       unlike the popular pure exponential gamma transfer function.
+    */
+    xelval const linearCutoffSrgb = (xelval) maxval * 0.0031308 + 0.5;
+    double const linearCompressionSrgb = 
+        0.0031308 / (1.055 * pow(0.0031308, oneOverGammaSrgb) - 0.055);
+
+    xelval const linearCutoff709 = (xelval) (maxval * 0.018 + 0.5);
+    double const linearExpansion709 = 
+        (1.099 * pow(0.018, oneOverGamma709) - 0.099) / 0.018;
+
+    xelval i;
+
+    for (i = 0; i <= maxval; ++i) {
+        double const normalized = i * normalizer;
+            /* Xel sample value normalized to 0..1 */
+        double radiance;
+        double bt709;
+
+        if (i < linearCutoffSrgb / linearCompressionSrgb)
+            radiance = normalized * linearCompressionSrgb;
+        else
+            radiance = pow((normalized + 0.099) / 1.099, gammaSrgb);
+
+        if (radiance < linearCutoff709)
+            bt709 = radiance * linearExpansion709;
+        else
+            bt709 = 1.055 * pow(normalized, oneOverGamma709) - 0.055;
+
+        table[i] = bt709 * newMaxval + 0.5;
+    }
+}
+
+
+
+static void
+createGammaTables(enum transferFunction const transferFunction,
+                  xelval                const maxval,
+                  xelval                const newMaxval,
+                  double                const rgamma, 
+                  double                const ggamma, 
+                  double                const bgamma,
+                  xelval **             const rtableP,
+                  xelval **             const gtableP,
+                  xelval **             const btableP) {
+
+    /* Allocate space for the tables. */
+    MALLOCARRAY(*rtableP, maxval+1);
+    MALLOCARRAY(*gtableP, maxval+1);
+    MALLOCARRAY(*btableP, maxval+1);
+    if (*rtableP == NULL || *gtableP == NULL || *btableP == NULL)
+        pm_error("Can't get memory to make gamma transfer tables");
+
+    /* Build the gamma corection tables. */
+    switch (transferFunction) {
+    case XF_BT709RAMP: {
+        buildBt709Gamma(*rtableP, maxval, newMaxval, rgamma);
+        buildBt709Gamma(*gtableP, maxval, newMaxval, ggamma);
+        buildBt709Gamma(*btableP, maxval, newMaxval, bgamma);
+    } break;
+
+    case XF_BT709RAMP_INVERSE: {
+        buildBt709GammaInverse(*rtableP, maxval, newMaxval, rgamma);
+        buildBt709GammaInverse(*gtableP, maxval, newMaxval, ggamma);
+        buildBt709GammaInverse(*btableP, maxval, newMaxval, bgamma);
+    } break;
+
+    case XF_SRGBRAMP: {
+        buildSrgbGamma(*rtableP, maxval, newMaxval, rgamma);
+        buildSrgbGamma(*gtableP, maxval, newMaxval, ggamma);
+        buildSrgbGamma(*btableP, maxval, newMaxval, bgamma);
+    } break;
+
+    case XF_SRGBRAMP_INVERSE: {
+        buildSrgbGammaInverse(*rtableP, maxval, newMaxval, rgamma);
+        buildSrgbGammaInverse(*gtableP, maxval, newMaxval, ggamma);
+        buildSrgbGammaInverse(*btableP, maxval, newMaxval, bgamma);
+    } break;
+
+    case XF_EXP: {
+        buildPowGamma(*rtableP, maxval, newMaxval, rgamma);
+        buildPowGamma(*gtableP, maxval, newMaxval, ggamma);
+        buildPowGamma(*btableP, maxval, newMaxval, bgamma);
+    } break;
+
+    case XF_EXP_INVERSE: {
+        buildPowGamma(*rtableP, maxval, newMaxval, 1.0/rgamma);
+        buildPowGamma(*gtableP, maxval, newMaxval, 1.0/ggamma);
+        buildPowGamma(*btableP, maxval, newMaxval, 1.0/bgamma);
+    } break;
+
+    case XF_BT709_TO_SRGB: {
+        buildBt709ToSrgbGamma(*rtableP, maxval, newMaxval, rgamma);
+        buildBt709ToSrgbGamma(*gtableP, maxval, newMaxval, ggamma);
+        buildBt709ToSrgbGamma(*btableP, maxval, newMaxval, bgamma);
+    } break;
+
+    case XF_SRGB_TO_BT709: {
+        buildSrgbToBt709Gamma(*rtableP, maxval, newMaxval, rgamma);
+        buildSrgbToBt709Gamma(*gtableP, maxval, newMaxval, ggamma);
+        buildSrgbToBt709Gamma(*btableP, maxval, newMaxval, bgamma);
+    } break;
+    }
+}
+
+
+
+static void
+convertRaster(FILE *   const ifP,
+              FILE *   const ofP,
+              int      const cols,
+              int      const rows,
+              xelval   const maxval,
+              int      const format,
+              xelval   const outputMaxval,
+              int      const outputFormat,
+              xelval * const rtable,
+              xelval * const gtable,
+              xelval * const btable) {
+
+    xel * xelrow;
+    unsigned int row;
+
+    xelrow = pnm_allocrow(cols);
+
+    for (row = 0; row < rows; ++row) {
+        pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
+
+        pnm_promoteformatrow(xelrow, cols, maxval, format, 
+                             maxval, outputFormat);
+
+        switch (PNM_FORMAT_TYPE(outputFormat)) {
+        case PPM_TYPE: {
+            unsigned int col;
+            for (col = 0; col < cols; ++col) {
+                xelval const r = PPM_GETR(xelrow[col]);
+                xelval const g = PPM_GETG(xelrow[col]);
+                xelval const b = PPM_GETB(xelrow[col]);
+                PPM_ASSIGN(xelrow[col], rtable[r], gtable[g], btable[b]);
+            }
+        } break;
+
+        case PGM_TYPE: {
+            unsigned int col;
+            for (col = 0; col < cols; ++col) {
+                xelval const xel = PNM_GET1(xelrow[col]);
+                PNM_ASSIGN1(xelrow[col], gtable[xel]);
+            }
+        } break;
+        default:
+            pm_error("Internal error.  Impossible format type");
+        }
+        pnm_writepnmrow(ofP, xelrow, cols, outputMaxval, outputFormat, 0);
+    }
+    pnm_freerow(xelrow);
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    xelval maxval;
+    int rows, cols, format;
+    xelval outputMaxval;
+    int outputFormat;
+    xelval * rtable;
+    xelval * gtable;
+    xelval * btable;
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.filespec);
+
+    pnm_readpnminit(ifP, &cols, &rows, &maxval, &format);
+
+    if (PNM_FORMAT_TYPE(format) == PPM_TYPE)
+        outputFormat = PPM_TYPE;
+    else if (cmdline.rgamma != cmdline.ggamma 
+             || cmdline.ggamma != cmdline.bgamma) 
+        outputFormat = PPM_TYPE;
+    else 
+        outputFormat = PGM_TYPE;
+
+    if (PNM_FORMAT_TYPE(format) != outputFormat) {
+        if (outputFormat == PPM_TYPE)
+            pm_message("Promoting to PPM");
+        if (outputFormat == PGM_TYPE)
+            pm_message("Promoting to PGM");
+    }
+
+    outputMaxval = cmdline.makeNewMaxval ? cmdline.maxval : maxval;
+
+    createGammaTables(cmdline.transferFunction, maxval,
+                      outputMaxval,
+                      cmdline.rgamma, cmdline.ggamma, cmdline.bgamma,
+                      &rtable, &gtable, &btable);
+
+    pnm_writepnminit(stdout, cols, rows, outputMaxval, outputFormat, 0);
+
+    convertRaster(ifP, stdout, cols, rows, maxval, format,
+                  outputMaxval, outputFormat,
+                  rtable, gtable, btable);
+
+    pm_close(ifP);
+    pm_close(stdout);
+
+    return 0;
+}
diff --git a/editor/pnmhisteq.c b/editor/pnmhisteq.c
new file mode 100644
index 00000000..2c6893bd
--- /dev/null
+++ b/editor/pnmhisteq.c
@@ -0,0 +1,416 @@
+/*
+                 pnmhisteq.c
+
+           Equalize histogram for a PNM image
+
+  By Bryan Henderson 2005.09.10, based on ideas from the program of
+  the same name by John Walker (kelvin@fourmilab.ch) -- March MVM.
+  WWW home page: http://www.fourmilab.ch/ in 1995.
+
+  This program is contributed to the public domain by its author.
+*/
+
+#include <string.h>
+
+#include "pnm.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFileName;
+    unsigned int gray;
+    const char * wmap;
+    const char * rmap;
+    unsigned int verbose;
+};
+
+
+
+static void
+parseCommandLine(int argc, char ** 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 rmapSpec, wmapSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "rmap",     OPT_STRING, &cmdlineP->rmap, 
+            &rmapSpec,          0);
+    OPTENT3(0, "wmap",     OPT_STRING, &cmdlineP->wmap, 
+            &wmapSpec,          0);
+    OPTENT3(0, "gray",     OPT_FLAG,   NULL,
+            &cmdlineP->gray,    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 may have parms that are negative numbers */
+
+    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+
+    if (!wmapSpec)
+        cmdlineP->wmap = NULL;
+    if (!rmapSpec)
+        cmdlineP->rmap = NULL;
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else {
+        cmdlineP->inputFileName = argv[1];
+        if (argc-1 > 1)
+            pm_error("Too many arguments (%d).  The only argument is the "
+                     "input file name.", argc-1);
+    }
+}
+
+
+
+static void
+computeLuminosityHistogram(xel * const *   const xels,
+                           unsigned int    const rows,
+                           unsigned int    const cols,
+                           xelval          const maxval,
+                           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
+  a PPM, we calculate the luminosity of each pixel from its RGB
+  components.
+-----------------------------------------------------------------------------*/
+    xelval lmin, lmax;
+    unsigned int pixelCount;
+    unsigned int * lumahist;
+
+    MALLOCARRAY(lumahist, maxval + 1);
+    if (lumahist == NULL)
+        pm_error("Out of storage allocating array for %u histogram elements",
+                 maxval + 1);
+
+    {
+        unsigned int i;
+        /* Initialize histogram to zeroes everywhere */
+        for (i = 0; i <= maxval; ++i)
+            lumahist[i] = 0;
+    }
+    lmin = maxval;  /* initial value */
+    lmax = 0;       /* initial value */
+
+    switch (PNM_FORMAT_TYPE(format)) {
+    case PGM_TYPE:
+    case PBM_TYPE: {
+        /* Compute intensity histogram */
+
+        unsigned int row;
+
+        pixelCount = rows * cols;
+        for (row = 0; row < rows; ++row) {
+            unsigned int col;
+            for (col = 0; col < cols; ++col) {
+                xelval const l = PNM_GET1(xels[row][col]);
+                lmin = MIN(lmin, l);
+                lmax = MAX(lmax, l);
+                ++lumahist[l];
+            }
+        }
+    }
+    break;
+    case PPM_TYPE: {
+        unsigned int row;
+
+        for (row = 0, pixelCount = 0; row < rows; ++row) {
+            unsigned int col;
+            for (col = 0; col < cols; ++col) {
+                xel const thisXel = xels[row][col];
+                if (!monoOnly || PPM_ISGRAY(thisXel)) {
+                    xelval const l = PPM_LUMIN(thisXel);
+
+                    lmin = MIN(lmin, l);
+                    lmax = MAX(lmax, l);
+
+                    ++lumahist[l];
+                    ++pixelCount;
+                }
+            }
+        }
+    }
+    break;
+    default:
+        pm_error("invalid input format format");
+    }
+
+    *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;
+}
+
+
+
+static void
+readMapFile(const char * const rmapFileName,
+            xelval       const maxval,
+            gray *       const lumamap) {
+
+    int rmcols, rmrows; 
+    gray rmmaxv;
+    int rmformat;
+    FILE * rmapfP;
+        
+    rmapfP = pm_openr(rmapFileName);
+    pgm_readpgminit(rmapfP, &rmcols, &rmrows, &rmmaxv, &rmformat);
+    
+    if (rmmaxv != maxval)
+        pm_error("maxval in map file (%u) different from input (%u)",
+                 rmmaxv, maxval);
+    
+    if (rmrows != 1)
+        pm_error("Map must have 1 row.  Yours has %u", rmrows);
+    
+    if (rmcols != maxval + 1)
+        pm_error("Map must have maxval + 1 (%u) columns.  Yours has %u",
+                 maxval + 1, rmcols);
+    
+    pgm_readpgmrow(rmapfP, lumamap, maxval+1, rmmaxv, rmformat);
+    
+    pm_close(rmapfP);
+}
+
+
+
+static void
+computeMap(const unsigned int * const lumahist,
+           xelval               const maxval,
+           unsigned int         const pixelCount,
+           gray *               const lumamap) {
+
+    /* Calculate initial histogram equalization curve. */
+    
+    unsigned int i;
+    unsigned int pixsum;
+    xelval maxluma;
+
+    for (i = 0, pixsum = 0; i <= maxval; ++i) {
+            
+        /* 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);
+
+        pixsum += lumahist[i];
+    }
+
+    findMaxLuma(lumahist, maxval, &maxluma);
+
+    {
+        double const lscale = (double)maxval /
+            ((lumahist[maxluma] > 0) ?
+             (double) lumamap[maxluma] : (double) maxval);
+
+        unsigned int i;
+
+        /* Normalize so that the brightest pixels are set to maxval. */
+
+        for (i = 0; i <= maxval; ++i)
+            lumamap[i] = MIN(maxval, ROUNDU(lumamap[i] * lscale));
+    }
+}
+
+
+
+static void
+getMapping(const char *         const rmapFileName,
+           const unsigned int * const lumahist,
+           xelval               const maxval,
+           unsigned int         const pixelCount,
+           gray **              const lumamapP) {
+/*----------------------------------------------------------------------------
+  Calculate the luminosity mapping table which gives the
+  histogram-equalized luminosity for each original luminosity.
+-----------------------------------------------------------------------------*/
+    gray * lumamap;
+
+    lumamap = pgm_allocrow(maxval+1);
+
+    if (rmapFileName)
+        readMapFile(rmapFileName, maxval, lumamap);
+    else
+        computeMap(lumahist, maxval, pixelCount, lumamap);
+
+    *lumamapP = lumamap;
+}
+
+
+
+static void
+reportMap(const unsigned int * const lumahist,
+          xelval               const maxval,
+          const gray *         const lumamap) {
+
+    unsigned int i;
+
+    fprintf(stderr, "  Luminosity map    Number of\n");
+    fprintf(stderr, " Original    New     Pixels  \n");
+
+    for (i = 0; i <= maxval; ++i) {
+        if (lumahist[i] > 0) {
+            fprintf(stderr,"%6d -> %6d  %8u\n", i,
+                    lumamap[i], lumahist[i]);
+        }
+    }
+}
+
+
+
+static void
+remap(xel **       const xels,
+      unsigned int const cols,
+      unsigned int const rows,
+      xelval       const maxval,
+      int          const format,
+      bool         const monoOnly,
+      const gray * const lumamap) {
+/*----------------------------------------------------------------------------
+   Update the array 'xels' to have the new intensities.
+-----------------------------------------------------------------------------*/
+    switch (PNM_FORMAT_TYPE(format)) {
+    case PPM_TYPE: {
+        unsigned int row;
+        for (row = 0; row < rows; ++row) {
+            unsigned int col;
+            for (col = 0; col < cols; ++col) {
+                xel const thisXel = xels[row][col];
+                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);
+                }
+            }
+        }
+    }
+    break;
+
+    case PBM_TYPE:
+    case PGM_TYPE: {
+        unsigned int row;
+        for (row = 0; row < rows; ++row) {
+            unsigned int col;
+            for (col = 0; col < cols; ++col)
+                PNM_ASSIGN1(xels[row][col],
+                            lumamap[PNM_GET1(xels[row][col])]);
+        }
+    }
+    break;
+    }
+}
+
+
+
+static void
+writeMap(const char * const wmapFileName,
+         const gray * const lumamap,
+         xelval       const maxval) {
+
+    FILE * const wmapfP = pm_openw(wmapFileName);
+
+    pgm_writepgminit(wmapfP, maxval+1, 1, maxval, 0);
+
+    pgm_writepgmrow(wmapfP, lumamap, maxval+1, maxval, 0);
+
+    pm_close(wmapfP);
+}
+
+
+
+int
+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 */
+    xelval maxval;            /* Maxval of input image */
+    int format;               /* Format indicator (PBM/PGM/PPM) */
+    xel ** xels;              /* Pixel array */
+    unsigned int pixelCount;
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    xels = pnm_readpnm(ifP, &cols, &rows, &maxval, &format);
+
+    pm_close(ifP);
+
+    computeLuminosityHistogram(xels, rows, cols, maxval, format,
+                               cmdline.gray, &lumahist, &lmin, &lmax,
+                               &pixelCount);
+
+    getMapping(cmdline.rmap, lumahist, maxval, pixelCount, &lumamap);
+
+    if (cmdline.verbose)
+        reportMap(lumahist, maxval, lumamap);
+
+    remap(xels, cols, rows, maxval, format, !!cmdline.gray, lumamap);
+
+    pnm_writepnm(stdout, xels, cols, rows, maxval, format, 0);
+
+    if (cmdline.wmap)
+        writeMap(cmdline.wmap, lumamap, maxval);
+
+    pgm_freerow(lumamap);
+
+    return 0;
+}
diff --git a/editor/pnmindex.c b/editor/pnmindex.c
new file mode 100644
index 00000000..cb7d3702
--- /dev/null
+++ b/editor/pnmindex.c
@@ -0,0 +1,638 @@
+/*============================================================================
+                              pnmindex   
+==============================================================================
+
+  build a visual index of a bunch of PNM images
+
+  This used to be a C shell program, and then a BASH program.  Neither
+  were portable enough, and the program is too complex for either of
+  those languages anyway, so now it's in C.
+
+  By Bryan Henderson 2005.04.24.
+
+  Contributed to the public domain by its author.
+
+============================================================================*/
+
+#define _BSD_SOURCE   /* Make sure strdup is in string.h */
+
+#include <assert.h>
+#include <unistd.h>
+#include <stdarg.h>
+#include <errno.h>
+#include <sys/stat.h>
+
+
+#include "pnm.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+#include "nstring.h"
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    unsigned int  inputFileCount;
+    const char ** inputFileName;
+    unsigned int  size;
+    unsigned int  across;
+    unsigned int  colors;
+    unsigned int  black;
+    unsigned int  noquant;
+    const char *  title;
+    unsigned int  verbose;
+};
+
+static bool verbose;
+
+
+
+static void PM_GNU_PRINTF_ATTR(1,2)
+systemf(const char * const fmt,
+        ...) {
+
+    va_list varargs;
+    
+    size_t dryRunLen;
+    
+    va_start(varargs, fmt);
+    
+    vsnprintfN(NULL, 0, fmt, varargs, &dryRunLen);
+
+    va_end(varargs);
+
+    if (dryRunLen + 1 < dryRunLen)
+        /* arithmetic overflow */
+        pm_error("Command way too long");
+    else {
+        size_t const allocSize = dryRunLen + 1;
+        char * shellCommand;
+        shellCommand = malloc(allocSize);
+        if (shellCommand == NULL)
+            pm_error("Can't get storage for %u-character command",
+                     allocSize);
+        else {
+            va_list varargs;
+            size_t realLen;
+            int rc;
+
+            va_start(varargs, fmt);
+
+            vsnprintfN(shellCommand, allocSize, fmt, varargs, &realLen);
+                
+            assert(realLen == dryRunLen);
+            va_end(varargs);
+
+            if (verbose)
+                pm_message("shell cmd: %s", shellCommand);
+
+            rc = system(shellCommand);
+            if (rc != 0)
+                pm_error("shell command '%s' failed.  rc %d",
+                         shellCommand, rc);
+            
+            strfree(shellCommand);
+        }
+    }
+}
+        
+
+
+static void
+parseCommandLine(int argc, char ** argv, 
+                 struct cmdlineInfo * const cmdlineP) {
+
+    unsigned int option_def_index;
+    optEntry *option_def;
+        /* Instructions to optParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int quant;
+    unsigned int sizeSpec, colorsSpec, acrossSpec, titleSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "black",       OPT_FLAG,   NULL,                  
+            &cmdlineP->black,         0);
+    OPTENT3(0, "noquant",     OPT_FLAG,   NULL,                  
+            &cmdlineP->noquant,       0);
+    OPTENT3(0, "quant",       OPT_FLAG,   NULL,                  
+            &quant,                   0);
+    OPTENT3(0, "verbose",     OPT_FLAG,   NULL,                  
+            &cmdlineP->verbose,       0);
+    OPTENT3(0, "size",        OPT_UINT,   &cmdlineP->size,
+            &sizeSpec,                0);
+    OPTENT3(0, "colors",      OPT_UINT,   &cmdlineP->colors,
+            &colorsSpec,              0);
+    OPTENT3(0, "across",      OPT_UINT,   &cmdlineP->across,
+            &acrossSpec,              0);
+    OPTENT3(0, "title",       OPT_STRING, &cmdlineP->title,
+            &titleSpec,               0);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = FALSE; 
+
+    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdline_p and others. */
+
+    if (quant && cmdlineP->noquant)
+        pm_error("You can't specify both -quant and -noquat");
+
+    if (!colorsSpec)
+        cmdlineP->colors = 256;
+    
+    if (!sizeSpec)
+        cmdlineP->size = 100;
+
+    if (!acrossSpec)
+        cmdlineP->across = 6;
+
+    if (!titleSpec)
+        cmdlineP->title = NULL;
+
+    if (colorsSpec && cmdlineP->noquant)
+        pm_error("-colors doesn't make any sense with -noquant");
+
+    if (argc-1 < 1)
+        pm_error("You must name at least one file that contains an image "
+                 "to go into the index");
+
+    cmdlineP->inputFileCount = argc-1;
+
+    MALLOCARRAY_NOFAIL(cmdlineP->inputFileName, cmdlineP->inputFileCount);
+
+    {
+        unsigned int i;
+        for (i = 0; i < cmdlineP->inputFileCount; ++i) {
+            cmdlineP->inputFileName[i] = strdup(argv[i+1]);
+            if (cmdlineP->inputFileName[i] == NULL)
+                pm_error("Unable to allocate memory for a file name");
+        }
+    }
+}
+
+
+
+static void
+freeCmdline(struct cmdlineInfo const cmdline) {
+
+    unsigned int i;
+
+    for (i = 0; i < cmdline.inputFileCount; ++i)
+        strfree(cmdline.inputFileName[i]);
+
+    free(cmdline.inputFileName);
+
+}
+
+
+
+static void
+makeTempDir(const char ** const tempDirP) {
+
+    const char * const tmpdir = getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp";
+
+    const char * mytmpdir;
+    int rc;
+
+    asprintfN(&mytmpdir, "%s/pnmindex_%d", tmpdir, getpid());
+
+    rc = mkdir(mytmpdir, 0700);
+    if (rc != 0)
+        pm_error("Unable to create temporary file directory '%s'.  mkdir() "
+                 "fails with errno %d (%s)",
+                 mytmpdir, errno, strerror(errno));
+
+    *tempDirP = mytmpdir;
+}
+
+
+
+static void
+removeTempDir(const char * const tempDir) {
+
+    int rc;
+
+    rc = rmdir(tempDir);
+    if (rc != 0)
+        pm_error("Failed to remove temporary file directory '%s'.  "
+                 "rmdir() fails with errno %d (%s)",
+                 tempDir, errno, strerror(errno));
+}
+
+
+static const char *
+rowFileName(const char * const dirName,
+            unsigned int const row) {
+
+    const char * fileName;
+    
+    asprintfN(&fileName, "%s/pi.%u", dirName, row);
+    
+    return fileName;
+}
+
+
+
+static void
+makeTitle(const char * const title,
+          unsigned int const rowNumber,
+          bool         const blackBackground,
+          const char * const tempDir) {
+
+    const char * const invertStage = blackBackground ? "| pnminvert " : "";
+
+    const char * fileName;
+
+    fileName = rowFileName(tempDir, rowNumber);
+
+    /* This quoting is not adequate.  We really should do this without
+       a shell at all.
+    */
+    systemf("pbmtext \"%s\" "
+            "%s"
+            "> %s", 
+            title, invertStage, fileName);
+
+    strfree(fileName);
+}
+
+
+
+static void
+copyImage(const char * const inputFileName,
+          const char * const outputFileName) {
+
+    systemf("cat %s > %s", inputFileName, outputFileName);
+} 
+
+
+
+static void
+copyScaleQuantImage(const char * const inputFileName,
+                    const char * const outputFileName,
+                    int          const format,
+                    unsigned int const size,
+                    unsigned int const quant,
+                    unsigned int const colors) {
+
+    const char * scaleCommand;
+
+    switch (PNM_FORMAT_TYPE(format)) {
+    case PBM_TYPE:
+        asprintfN(&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);
+        break;
+        
+    case PPM_TYPE:
+        if (quant)
+            asprintfN(&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);
+        break;
+    default:
+        pm_error("Unrecognized Netpbm format: %d", format);
+    }
+
+    systemf("%s", scaleCommand);
+
+    strfree(scaleCommand);
+}
+
+
+
+static int
+formatTypeMax(int const typeA,
+              int const typeB) {
+
+    if (typeA == PPM_TYPE || typeB == PPM_TYPE)
+        return PPM_TYPE; 
+    else if (typeA == PGM_TYPE || typeB == PGM_TYPE)
+        return PGM_TYPE;
+    else
+        return PBM_TYPE;
+}
+
+
+
+static const char *
+thumbnailFileName(const char * const dirName,
+                  unsigned int const row,
+                  unsigned int const col) {
+
+    const char * fileName;
+    
+    asprintfN(&fileName, "%s/pi.%u.%u", dirName, row, col);
+    
+    return fileName;
+}
+
+
+
+static const char *
+thumbnailFileList(const char * const dirName,
+                  unsigned int const row,
+                  unsigned int const cols) {
+
+    unsigned int const maxListSize = 4096;
+
+    char * list;
+    unsigned int col;
+
+    list = malloc(maxListSize);
+    if (list == NULL)
+        pm_error("Unable to allocate %u bytes for file list", maxListSize);
+
+    list[0] = '\0';
+    
+    for (col = 0; col < cols; ++col) {
+        const char * const fileName = thumbnailFileName(dirName, row, col);
+
+        if (strlen(list) + strlen(fileName) + 1 > maxListSize - 1)
+            pm_error("File name list too long for this program to handle.");
+        else {
+            strcat(list, " ");
+            strcat(list, fileName);
+        }
+        strfree(fileName);
+    }
+
+    return list;
+}
+
+
+
+static void
+makeImageFile(const char * const thumbnailFileName,
+              const char * const inputFileName,
+              bool         const blackBackground,
+              const char * const outputFileName) {
+
+    const char * const blackWhiteOpt = blackBackground ? "-black" : "-white";
+    const char * const invertStage = blackBackground ? "| pnminvert " : "";
+
+    systemf("pbmtext \"%s\" "
+            "%s"
+            "| pnmcat %s -topbottom %s - "
+            "> %s",
+            inputFileName, invertStage, blackWhiteOpt, 
+            thumbnailFileName, outputFileName);
+}    
+
+
+
+static void
+makeThumbnail(const char *  const inputFileName,
+              unsigned int  const size,
+              bool          const black,
+              bool          const quant,
+              unsigned int  const colors,
+              const char *  const tempDir,
+              unsigned int  const row,
+              unsigned int  const col,
+              int *         const formatP) {
+
+    FILE * ifP;
+    int imageCols, imageRows, format;
+    xelval maxval;
+    const char * tmpfile;
+    const char * fileName;
+        
+    ifP = pm_openr(inputFileName);
+    pnm_readpnminit(ifP, &imageCols, &imageRows, &maxval, &format);
+    pm_close(ifP);
+    
+    asprintfN(&tmpfile, "%s/pi.tmp", tempDir);
+
+    if (imageCols < size && imageRows < size)
+        copyImage(inputFileName, tmpfile);
+    else
+        copyScaleQuantImage(inputFileName, tmpfile, format, 
+                            size, quant, colors);
+
+    fileName = thumbnailFileName(tempDir, row, col);
+        
+    makeImageFile(tmpfile, inputFileName, black, fileName);
+
+    unlink(tmpfile);
+
+    strfree(fileName);
+    strfree(tmpfile);
+
+    *formatP = format;
+}
+        
+
+
+static void
+unlinkThumbnailFiles(const char * const dirName,
+                     unsigned int const row,
+                     unsigned int const cols) {
+
+    unsigned int col;
+    
+    for (col = 0; col < cols; ++col) {
+        const char * const fileName = thumbnailFileName(dirName, row, col);
+
+        unlink(fileName);
+
+        strfree(fileName);
+    }
+}
+
+
+
+static void
+unlinkRowFiles(const char * const dirName,
+               unsigned int const rows) {
+
+    unsigned int row;
+    
+    for (row = 0; row < rows; ++row) {
+        const char * const fileName = rowFileName(dirName, row);
+
+        unlink(fileName);
+
+        strfree(fileName);
+    }
+}
+
+
+
+static void
+combineIntoRowAndDelete(unsigned int const row,
+                        unsigned int const cols,
+                        int          const maxFormatType,
+                        bool         const blackBackground,
+                        bool         const quant,
+                        unsigned int const colors,
+                        const char * const tempDir) {
+
+    const char * const blackWhiteOpt = blackBackground ? "-black" : "-white";
+
+    const char * fileName;
+    const char * quantStage;
+    const char * fileList;
+    
+    fileName = rowFileName(tempDir, row);
+
+    unlink(fileName);
+
+    if (maxFormatType == PPM_TYPE && quant)
+        asprintfN(&quantStage, "| pnmquant -quiet %u ", colors);
+    else
+        quantStage = strdup("");
+
+    fileList = thumbnailFileList(tempDir, row, cols);
+
+    systemf("pnmcat %s -leftright -jbottom %s "
+            "%s"
+            ">%s",
+            blackWhiteOpt, fileList, quantStage, fileName);
+
+    strfree(fileList);
+    strfree(quantStage);
+    strfree(fileName);
+
+    unlinkThumbnailFiles(tempDir, row, cols);
+}
+
+
+
+static const char *
+rowFileList(const char * const dirName,
+            unsigned int const rows) {
+
+    unsigned int const maxListSize = 4096;
+
+    unsigned int row;
+    char * list;
+
+    list = malloc(maxListSize);
+    if (list == NULL)
+        pm_error("Unable to allocate %u bytes for file list", maxListSize);
+
+    list[0] = '\0';
+
+    for (row = 0; row < rows; ++row) {
+        const char * const fileName = rowFileName(dirName, row);
+
+        if (strlen(list) + strlen(fileName) + 1 > maxListSize - 1)
+            pm_error("File name list too long for this program to handle.");
+        
+        else {
+            strcat(list, " ");
+            strcat(list, fileName);
+        }
+        strfree(fileName);
+    }
+
+    return list;
+}
+
+
+
+static void
+writeRowsAndDelete(unsigned int const rows,
+                   int          const maxFormatType,
+                   bool         const blackBackground,
+                   bool         const quant,
+                   unsigned int const colors,
+                   const char * const tempDir) {
+
+    const char * const blackWhiteOpt = blackBackground ? "-black" : "-white";
+
+    const char * quantStage;
+    const char * fileList;
+    
+    if (maxFormatType == PPM_TYPE && quant)
+        asprintfN(&quantStage, "| pnmquant -quiet %u ", colors);
+    else
+        quantStage = strdup("");
+
+    fileList = rowFileList(tempDir, rows);
+
+    systemf("pnmcat %s -topbottom %s %s",
+            blackWhiteOpt, fileList, quantStage);
+
+    strfree(fileList);
+    strfree(quantStage);
+
+    unlinkRowFiles(tempDir, rows);
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+    struct cmdlineInfo cmdline;
+    const char * tempDir;
+    int maxFormatType;
+    unsigned int colsInRow;
+    unsigned int rowsDone;
+    unsigned int i;
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    verbose = cmdline.verbose;
+    
+    makeTempDir(&tempDir);
+
+    maxFormatType = PBM_TYPE;
+    colsInRow = 0;
+    rowsDone = 0;
+
+    if (cmdline.title)
+        makeTitle(cmdline.title, rowsDone++, cmdline.black, tempDir);
+
+    for (i = 0; i < cmdline.inputFileCount; ++i) {
+        const char * const inputFileName = cmdline.inputFileName[i];
+
+        int format;
+
+        makeThumbnail(inputFileName, cmdline.size, cmdline.black, 
+                      !cmdline.noquant, cmdline.colors, tempDir,
+                      rowsDone, colsInRow, &format);
+
+        maxFormatType = formatTypeMax(maxFormatType, PNM_FORMAT_TYPE(format));
+
+        ++colsInRow;
+        if (colsInRow >= cmdline.across || i == cmdline.inputFileCount-1) {
+            combineIntoRowAndDelete(
+                rowsDone, colsInRow, maxFormatType,
+                cmdline.black, !cmdline.noquant, cmdline.colors,
+                tempDir);
+            ++rowsDone;
+            colsInRow = 0;
+        }
+    }
+
+    writeRowsAndDelete(rowsDone, maxFormatType, cmdline.black,
+                       !cmdline.noquant, cmdline.colors, tempDir);
+
+    removeTempDir(tempDir);
+
+    freeCmdline(cmdline);
+
+    pm_close(stdout);
+
+    return 0;
+}
diff --git a/editor/pnmindex.csh b/editor/pnmindex.csh
new file mode 100755
index 00000000..c6f1e844
--- /dev/null
+++ b/editor/pnmindex.csh
@@ -0,0 +1,189 @@
+#!/bin/csh -f
+#
+# pnmindex - build a visual index of a bunch of anymaps
+#
+# Copyright (C) 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.
+
+# -title and -quant added by John Heidemann 13-Sep-00.
+
+set size=100		# make the images about this big
+set across=6		# show this many images per row
+set colors=256		# quantize results to this many colors
+set back="-white"	# default background color
+set doquant=true	# quantize or not
+set title=""		# default title (none)
+
+while ( 1 )
+    switch ( "$1" )
+
+	case -s*:
+	if ( $#argv < 2 ) goto usage
+	set size="$2"
+	shift
+	shift
+	breaksw
+
+	case -a*:
+	if ( $#argv < 2 ) goto usage
+	set across="$2"
+	shift
+	shift
+	breaksw
+
+	case -t*:
+	if ( $#argv < 2 ) goto usage
+	set title="$2"
+	shift
+	shift
+	breaksw
+
+	case -c*:
+	set colors="$2"
+	shift
+	shift
+	breaksw
+
+	case -noq*:
+	set doquant=false
+	shift
+	breaksw
+
+	case -q*:
+	set doquant=true
+	shift
+	breaksw
+
+	case -b*:
+	set back="-black"
+	shift
+	breaksw
+
+	case -w*:
+	set back="-white"
+	shift
+	breaksw
+
+	case -*:
+	goto usage
+	breaksw
+
+	default:
+	break
+	breaksw
+
+    endsw
+end
+
+if ( $#argv == 0 ) then
+    goto usage
+endif
+
+set tmpfile=/tmp/pi.tmp.$$
+rm -f $tmpfile
+set maxformat=PBM
+
+set rowfiles=()
+set imagefiles=()
+@ row = 1
+@ col = 1
+
+if ( "$title" != "" ) then
+    set rowfile=/tmp/pi.${row}.$$
+    rm -f $rowfile
+    pbmtext "$title" > $rowfile
+    set rowfiles=( $rowfiles $rowfile )
+    @ row += 1
+endif
+
+foreach i ( $argv )
+
+    set description=`pnmfile $i`
+    if ( $description[4] <= $size && $description[6] <= $size ) then
+	cat $i > $tmpfile
+    else
+	switch ( $description[2] )
+	    case PBM:
+	    pnmscale -quiet -xysize $size $size $i | pgmtopbm > $tmpfile
+	    breaksw
+
+	    case PGM:
+	    pnmscale -quiet -xysize $size $size $i > $tmpfile
+	    if ( $maxformat == PBM ) then
+		set maxformat=PGM
+	    endif
+	    breaksw
+
+	    default:
+	    if ( $doquant == false ) then
+	        pnmscale -quiet -xysize $size $size $i > $tmpfile
+	    else
+	        pnmscale -quiet -xysize $size $size $i | ppmquant -quiet $colors > $tmpfile
+	    endif
+	    set maxformat=PPM
+	    breaksw
+	endsw
+    endif
+    set imagefile=/tmp/pi.${row}.${col}.$$
+    rm -f $imagefile
+    if ( "$back" == "-white" ) then
+	pbmtext "$i" | pnmcat $back -tb $tmpfile - > $imagefile
+    else
+	pbmtext "$i" | pnminvert | pnmcat $back -tb $tmpfile - > $imagefile
+    endif
+    rm -f $tmpfile
+    set imagefiles=( $imagefiles $imagefile )
+
+    if ( $col >= $across ) then
+	set rowfile=/tmp/pi.${row}.$$
+	rm -f $rowfile
+	if ( $maxformat != PPM || $doquant == false ) then
+	    pnmcat $back -lr -jbottom $imagefiles > $rowfile
+	else
+	    pnmcat $back -lr -jbottom $imagefiles | ppmquant -quiet $colors > $rowfile
+	endif
+	rm -f $imagefiles
+	set imagefiles=()
+	set rowfiles=( $rowfiles $rowfile )
+	@ col = 1
+	@ row += 1
+    else
+	@ col += 1
+    endif
+
+end
+
+if ( $#imagefiles > 0 ) then
+    set rowfile=/tmp/pi.${row}.$$
+    rm -f $rowfile
+    if ( $maxformat != PPM || $doquant == false ) then
+	pnmcat $back -lr -jbottom $imagefiles > $rowfile
+    else
+	pnmcat $back -lr -jbottom $imagefiles | ppmquant -quiet $colors > $rowfile
+    endif
+    rm -f $imagefiles
+    set rowfiles=( $rowfiles $rowfile )
+endif
+
+if ( $#rowfiles == 1 ) then
+    cat $rowfiles
+else
+    if ( $maxformat != PPM || $doquant == false ) then
+	pnmcat $back -tb $rowfiles
+    else
+	pnmcat $back -tb $rowfiles | ppmquant -quiet $colors
+    endif
+endif
+rm -f $rowfiles
+
+exit 0
+
+usage:
+echo "usage: $0 [-size N] [-across N] [-colors N] [-black] pnmfile ..."
+exit 1
diff --git a/editor/pnmindex.sh b/editor/pnmindex.sh
new file mode 100755
index 00000000..15ba1abd
--- /dev/null
+++ b/editor/pnmindex.sh
@@ -0,0 +1,215 @@
+#!/bin/sh
+#
+# pnmindex - build a visual index of a bunch of PNM images
+#
+# Copyright (C) 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.
+
+size=100        # make the images about this big
+across=6        # show this many images per row
+colors=256      # quantize results to this many colors
+back="-white"   # default background color
+doquant=true    # quantize or not
+title=""        # default title (none)
+
+usage ()
+{
+  echo "usage: $0 [-size N] [-across N] [-colors N] [-black] pnmfile ..."
+  exit 1
+}
+
+while :; do
+    case "$1" in
+
+    -s*)
+        if [ $# -lt 2 ]; then usage; fi
+        size="$2"
+        shift
+        shift
+    ;;
+
+    -a*)
+        if [ $# -lt 2 ]; then usage; fi
+        across="$2"
+        shift
+        shift
+    ;;
+
+    -t*)
+        if [ $# -lt 2 ]; then usage; fi
+        title="$2"
+        shift
+        shift
+    ;;
+
+    -c*)
+        if [ $# -lt 2 ]; then usage; fi
+        colors="$2"
+        shift
+        shift
+    ;;
+
+    -b*)
+        back="-black"
+        shift
+    ;;
+
+    -w*)
+        back="-white"
+        shift
+    ;;
+
+    -noq*)
+        doquant=false
+        shift
+    ;;
+
+    -q*)
+        doquant=true
+        shift
+    ;;
+
+    -*)
+        usage
+    ;;
+
+    *)
+        break
+    ;;
+    esac
+done
+
+if [ $# -eq 0 ]; then
+    usage
+fi
+
+tempdir="${TMPDIR-/tmp}/pnmindex.$$"
+mkdir $tempdir || { echo "Could not create temporary file. Exiting."; exit 1;}
+chmod 700 $tempdir
+
+trap 'rm -rf $tempdir' 0 1 3 15
+
+tmpfile=$tempdir/pi.tmp
+maxformat=PBM
+
+rowfiles=()
+imagefiles=()
+row=1
+col=1
+
+if [ "$title"x != ""x ] ; then
+#    rowfile=`tempfile -p pirow -m 600`
+    rowfile=$tempdir/pi.${row}
+    pbmtext "$title" > $rowfile
+    rowfiles=(${rowfiles[*]} $rowfile )
+    row=$(($row + 1))
+fi
+
+for i in "$@"; do
+
+    description=(`pnmfile $i`)
+
+    format=${description[1]}
+    width=${description[3]}
+    height=${description[5]}
+
+    if [ $? -ne 0 ]; then
+        echo pnmfile returned an error
+        exit $?
+    fi
+
+    if [ $width -le $size ] && \
+       [ $height -le $size ]; then
+        cat $i > $tmpfile
+    else
+        case $format in
+
+        PBM) 
+            pamscale -quiet -xysize $size $size $i | pgmtopbm > $tmpfile
+        ;;
+
+        PGM)
+            pamscale -quiet -xysize $size $size $i > $tmpfile
+            if [ $maxformat = PBM ]; then
+                maxformat=PGM
+            fi
+        ;;
+
+        *) 
+            if [ "$doquant" = "true" ] ; then
+                pamscale -quiet -xysize $size $size $i | \
+                pnmquant -quiet $colors > $tmpfile
+            else
+                pamscale -quiet -xysize $size $size $i > $tmpfile
+            fi
+            maxformat=PPM
+        ;;
+        esac
+    fi
+
+    imagefile=$tempdir/pi.${row}.${col}
+    rm -f $imagefile
+    if [ "$back" = "-white" ]; then
+        pbmtext "$i" | pnmcat $back -tb $tmpfile - > $imagefile
+    else
+        pbmtext "$i" | pnminvert | pnmcat $back -tb $tmpfile - > $imagefile
+    fi
+    imagefiles=( ${imagefiles[*]} $imagefile )
+
+    if [ $col -ge $across ]; then
+        rowfile=$tempdir/pi.${row}
+        rm -f $rowfile
+
+        if [ $maxformat != PPM -o "$doquant" = "false" ]; then
+            pnmcat $back -lr -jbottom ${imagefiles[*]} > $rowfile
+        else
+            pnmcat $back -lr -jbottom ${imagefiles[*]} | \
+            pnmquant -quiet $colors > $rowfile
+        fi
+
+        rm -f ${imagefiles[*]}
+        unset imagefiles
+        imagefiles=()
+        rowfiles=( ${rowfiles[*]} $rowfile )
+        col=1
+        row=$(($row + 1))
+    else
+        col=$(($col + 1))
+    fi
+done
+
+# All the full rows have been put in row files.  
+# Now put the final partial row in its row file.
+
+if [ ${#imagefiles[*]} -gt 0 ]; then
+    rowfile=$tempdir/pi.${row}
+    rm -f $rowfile
+    if [ $maxformat != PPM -o "$doquant" = "false" ]; then
+        pnmcat $back -lr -jbottom ${imagefiles[*]} > $rowfile
+    else
+        pnmcat $back -lr -jbottom ${imagefiles[*]} | \
+        pnmquant -quiet $colors > $rowfile
+    fi
+    rm -f ${imagefiles[*]}
+    rowfiles=( ${rowfiles[*]} $rowfile )
+fi
+
+if [ ${#rowfiles[*]} -eq 1 ]; then
+    cat $rowfiles
+else
+    if [ $maxformat != PPM -o "$doquant" = "false" ]; then
+        pnmcat $back -tb ${rowfiles[*]}
+    else
+        pnmcat $back -tb ${rowfiles[*]} | pnmquant -quiet $colors
+    fi
+fi
+rm -f ${rowfiles[*]}
+
+exit 0
+
diff --git a/editor/pnminvert.c b/editor/pnminvert.c
new file mode 100644
index 00000000..40fee9be
--- /dev/null
+++ b/editor/pnminvert.c
@@ -0,0 +1,115 @@
+/* pnminvert.c - read a portable anymap and invert it
+**
+** Copyright (C) 1989 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.
+*/
+
+#include "pnm.h"
+
+#define CHARBITS (sizeof(unsigned char)*8)
+
+
+
+static void
+invertPbm(FILE * const ifP,
+          FILE * const ofP,
+          int    const cols,
+          int    const rows,
+          int    const format) {
+/*----------------------------------------------------------------------------
+   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; 
+    unsigned int row;
+    
+    bitrow = pbm_allocrow_packed(cols);
+    
+    for (row = 0; row < rows; ++row) {
+        unsigned int colChar;
+        
+        pbm_readpbmrow_packed(ifP, bitrow, cols, format);
+        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;
+        }
+        pbm_writepbmrow_packed(ofP, bitrow, cols, 0);
+    }
+    pbm_freerow_packed(bitrow);
+}
+
+
+
+static void
+invertPnm(FILE * const ifP,
+          FILE * const ofP,
+          int    const cols,
+          int    const rows,
+          xelval const maxval,
+          int    const format) {
+
+    xel * xelrow;
+    unsigned int row;
+    
+    xelrow = pnm_allocrow(cols);
+    
+    for (row = 0; row < rows; ++row) {
+        unsigned int col;
+        pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
+        for (col = 0; col < cols; ++col)
+            pnm_invertxel(&xelrow[col], maxval, format);
+        
+        pnm_writepnmrow(ofP, xelrow, cols, maxval, format, 0);
+    }
+    pnm_freerow(xelrow);
+}
+
+
+
+int
+main(int argc, char * argv[]) {
+    FILE* ifP;
+    xelval maxval;
+    int rows, cols, format;
+
+    pnm_init(&argc, argv);
+
+    if (argc-1 > 1)
+        pm_error("There is at most 1 argument - the input file name.  "
+                 "You specified %d", argc-1);
+    if (argc-1 == 1)
+        ifP = pm_openr(argv[1]);
+    else
+        ifP = stdin;
+
+    pnm_readpnminit(ifP, &cols, &rows, &maxval, &format);
+    pnm_writepnminit(stdout, cols, rows, maxval, format, 0);
+    
+    if (PNM_FORMAT_TYPE(format) == PBM_TYPE)
+        /* Take fast path */
+        invertPbm(ifP, stdout, cols, rows, format);
+    else
+        /* PPM , PGM  (logic also works for PBM) */
+        invertPnm(ifP, stdout, cols, rows, maxval, format);
+
+    pm_close(ifP);
+    pm_close(stdout);
+    
+    return 0;
+}
+
+
diff --git a/editor/pnminvert.test b/editor/pnminvert.test
new file mode 100644
index 00000000..606e4e5c
--- /dev/null
+++ b/editor/pnminvert.test
@@ -0,0 +1,15 @@
+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 4215652354 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
new file mode 100755
index 00000000..31420f99
--- /dev/null
+++ b/editor/pnmmargin
@@ -0,0 +1,88 @@
+#!/bin/sh
+#
+# ppmmargin - add a margin to a PNM image
+#
+# Copyright (C) 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.
+
+tempdir="${TMPDIR-/tmp}/pnmmargin.$$"
+mkdir $tempdir || { echo "Could not create temporary file. Exiting."; exit 1;}
+chmod 700 $tempdir
+
+trap 'rm -rf $tempdir' 0 1 3 15
+
+tmp1=$tempdir/pnmm1
+tmp2=$tempdir/pnmm2
+tmp3=$tempdir/pnmm3
+tmp4=$tempdir/pnmm4
+
+color="-gofigure"
+
+# Parse args.
+while true ; do
+    case "$1" in
+	-w* )
+	color="-white"
+	shift
+	;;
+	-b* )
+	color="-black"
+	shift
+	;;
+	-c* )
+	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
+
+if [ ! ${1-""} ] ; then
+    echo "usage: $0 [-white|-black|-color <colorspec>] <size> [pnmfile]" 1>&2
+    exit 1
+fi
+size="$1"
+shift
+
+if [ ${2-""} ] ; then
+    echo "usage: $0 [-white|-black|-color <colorspec>] <size> [pnmfile]" 1>&2
+    exit 1
+fi
+
+# Capture input file in a tmp file, in case it's a pipe.
+cat $@ > $tmp1
+
+# Construct spacer files.
+case "$color" in
+    -gofigure )
+    pnmcut 0 0 1 1 $tmp1 | pnmtile $size 1 > $tmp2
+    ;;
+    -white | -black )
+    pbmmake $color $size 1 > $tmp2
+    ;;
+    * )
+    ppmmake $color $size 1 > $tmp2
+    ;;
+esac
+pamflip -rotate90 $tmp2 > $tmp3
+
+# Cat things together.
+pnmcat -lr $tmp2 $tmp1 $tmp2 > $tmp4
+pnmcat -tb $tmp3 $tmp4 $tmp3
diff --git a/editor/pnmmontage.c b/editor/pnmmontage.c
new file mode 100644
index 00000000..9eb2d7be
--- /dev/null
+++ b/editor/pnmmontage.c
@@ -0,0 +1,439 @@
+/* pnmmontage.c - build a montage of portable anymaps
+ *
+ * Copyright 2000 Ben Olmstead.
+ *
+ * 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 <limits.h>
+#include <string.h>
+
+#include "pam.h"
+#include "shhopt.h"
+#include "nstring.h"
+#include "mallocvar.h"
+
+typedef struct { int f[sizeof(int) * 8 + 1]; } factorset;
+typedef struct { int x; int y; } coord;
+
+static int qfactor = 200;
+static int quality = 5;
+
+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 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);
+}
+
+static __inline__ int imax(int n, int m) { return (n > m ? n : m); }
+
+static int 
+checkcollision(coord *locs, coord *szs, coord *cloc, coord *csz, int n)
+{
+  int i;
+  for (i = 0; i < n; ++i)
+  {
+    if ((locs[i].x < cloc->x + csz->x) &&
+        (locs[i].y < cloc->y + csz->y) &&
+        (locs[i].x + szs[i].x > cloc->x) &&
+        (locs[i].y + szs[i].y > cloc->y))
+      return (1);
+  }
+  return (0);
+}
+
+static void 
+recursefindpack(coord *current, coord currentsz, coord *set, 
+                coord *best, int minarea, int *maxarea, 
+                int depth, int n, int xinc, int yinc)
+{
+  coord c;
+  if (depth == n)
+  {
+    if (currentsz.x * currentsz.y < *maxarea)
+    {
+      memcpy(best, current, sizeof(coord) * n);
+      *maxarea = currentsz.x * currentsz.y;
+    }
+    return;
+  }
+
+  for (current[depth].x = 0; 
+       imax(current[depth].x + set[depth].x, currentsz.x) * 
+           imax(currentsz.y, set[depth].y) < *maxarea; 
+       current[depth].x += xinc)
+  {
+    for (current[depth].y = 0; 
+         imax(current[depth].x + set[depth].x, currentsz.x) * 
+             imax(currentsz.y, current[depth].y + set[depth].y) < *maxarea; 
+         current[depth].y += yinc)
+    {
+      c.x = imax(current[depth].x + set[depth].x, currentsz.x);
+      c.y = imax(current[depth].y + set[depth].y, currentsz.y);
+      if (!checkcollision(current, set, &current[depth], &set[depth], depth))
+      {
+        recursefindpack(current, c, set, best, minarea, maxarea, 
+                        depth + 1, n, xinc, yinc);
+        if (*maxarea <= minarea)
+          return;
+      }
+    }
+  }
+}
+
+static void 
+findpack(struct pam *imgs, int n, coord *coords)
+{
+  int minarea;
+  int i;
+  int rdiv;
+  int cdiv;
+  int minx = -1;
+  int miny = -1;
+  coord *current;
+  coord *set;
+  int z = INT_MAX;
+  coord c = { 0, 0 };
+
+  if (quality > 1)
+  {
+    for (minarea = i = 0; i < n; ++i)
+      minarea += imgs[i].height * imgs[i].width,
+      minx = imax(minx, imgs[i].width),
+      miny = imax(miny, imgs[i].height);
+
+    minarea = minarea * qfactor / 100;
+  }
+  else
+  {
+    minarea = INT_MAX - 1;
+  }
+
+  /* It's relatively easy to show that, if all the images
+   * are multiples of a particular size, then a best
+   * packing will always align the images on a grid of
+   * that size.
+   *
+   * This speeds computation immensely.
+   */
+  for (rdiv = imgs[0].height, i = 1; i < n; ++i)
+    rdiv = gcd(imgs[i].height, rdiv);
+
+  for (cdiv = imgs[0].width, i = 1; i < n; ++i)
+    cdiv = gcd(imgs[i].width, cdiv);
+
+  MALLOCARRAY(current, n);
+  MALLOCARRAY(set, n);
+  for (i = 0; i < n; ++i)
+    set[i].x = imgs[i].width,
+    set[i].y = imgs[i].height;
+  recursefindpack(current, c, set, coords, minarea, &z, 0, n, cdiv, rdiv);
+}
+
+
+
+static void 
+adjustDepth(tuple *            const tuplerow,
+            const struct pam * const inpamP,
+            const struct pam * const outpamP,
+            coord              const coord) {
+
+    if (inpamP->depth < outpamP->depth) {
+        unsigned int i;
+        for (i = coord.x; i < coord.x + inpamP->width; ++i) {
+            int j;
+            for (j = inpamP->depth; j < outpamP->depth; ++j)
+                tuplerow[i][j] = tuplerow[i][inpamP->depth - 1];
+        }
+    }
+}
+
+
+
+static void 
+adjustMaxval(tuple *            const tuplerow,
+             const struct pam * const inpamP,
+             const struct pam * const outpamP,
+             coord              const coord) {
+
+    if (inpamP->maxval < outpamP->maxval) {
+        int i;
+        for (i = coord.x; i < coord.x + inpamP->width; ++i) {
+            int j;
+            for (j = 0; j < outpamP->depth; ++j)
+                tuplerow[i][j] *= outpamP->maxval / inpamP->maxval;
+        }
+    }
+}
+
+
+
+static void
+writePam(struct pam *       const outpamP,
+         unsigned int       const nfiles,
+         const coord *      const coords,
+         const struct pam * const imgs) {
+
+    tuple *tuplerow;
+    int i;
+  
+    pnm_writepaminit(outpamP);
+
+    tuplerow = pnm_allocpamrow(outpamP);
+
+    for (i = 0; i < outpamP->height; ++i) {
+        int j;
+        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]);
+
+                adjustMaxval(tuplerow, &imgs[j], outpamP, coords[j]);
+
+            }
+        }
+        pnm_writepamrow(outpamP, tuplerow);
+    }
+    pnm_freepamrow(tuplerow);
+}
+
+
+
+int 
+main(int argc, char **argv)
+{
+  struct pam *imgs;
+  struct pam outimg;
+  struct pam p;
+  int nfiles;
+  int i, j;
+  unsigned int q[10];
+  coord *coords;
+  const char *headfname = NULL;
+  const char *datafname = NULL;
+  const char *prefix = "";
+  FILE *header;
+  FILE *data;
+  char **names;
+  char *c;
+
+  optEntry *option_def = malloc(100*sizeof(optEntry));
+      /* Instructions to OptParseOptions3 on how to parse our options.
+       */
+  optStruct3 opt;
+
+  unsigned int option_def_index;
+
+  option_def_index = 0;   /* incremented by OPTENTRY */
+  OPTENT3( 0,  "data",    OPT_STRING, &datafname, NULL, 0);
+  OPTENT3( 0,  "header",  OPT_STRING, &headfname, NULL, 0);
+  OPTENT3('q', "quality", OPT_UINT,   &qfactor,   NULL, 0);
+  OPTENT3('p', "prefix",  OPT_STRING, &prefix,    NULL, 0);
+  OPTENT3('0', "0",       OPT_FLAG,   NULL, &q[0],      0);
+  OPTENT3('1', "1",       OPT_FLAG,   NULL, &q[1],      0);
+  OPTENT3('2', "2",       OPT_FLAG,   NULL, &q[2],      0);
+  OPTENT3('3', "3",       OPT_FLAG,   NULL, &q[3],      0);
+  OPTENT3('4', "4",       OPT_FLAG,   NULL, &q[4],      0);
+  OPTENT3('5', "5",       OPT_FLAG,   NULL, &q[5],      0);
+  OPTENT3('6', "6",       OPT_FLAG,   NULL, &q[6],      0);
+  OPTENT3('7', "7",       OPT_FLAG,   NULL, &q[7],      0);
+  OPTENT3('8', "8",       OPT_FLAG,   NULL, &q[8],      0);
+  OPTENT3('9', "9",       OPT_FLAG,   NULL, &q[9],      0);
+
+  opt.opt_table = option_def;
+  opt.short_allowed = FALSE;
+  opt.allowNegNum = FALSE;
+
+  pnm_init(&argc, argv);
+
+  /* Check for flags. */
+  optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+
+  if (headfname)
+    header = pm_openw(headfname);
+
+  if (datafname)
+    data = pm_openw(datafname);
+
+  for (i = 0; i < 10; ++i)
+  {
+    if (q[i])
+    {
+      quality = i;
+      switch (quality)
+      {
+        case 0: case 1: break;
+        case 2: case 3: case 4: case 5: case 6: 
+            qfactor = 100 * (8 - quality); 
+            break;
+        case 7: qfactor = 150; break;
+        case 8: qfactor = 125; break;
+        case 9: qfactor = 100; break;
+      }
+    }
+  }
+
+  if (1 < argc)
+    nfiles = argc - 1;
+  else
+    nfiles = 1;
+
+  MALLOCARRAY(imgs, nfiles);
+  MALLOCARRAY(coords, nfiles);
+  MALLOCARRAY(names, nfiles);
+  
+  if (!imgs || !coords || !names)
+    pm_error("out of memory");
+
+  if (1 < argc)
+  {
+    for (i = 0; i < nfiles; ++i)
+    {
+      if (strchr(argv[i+1], ':'))
+      {
+        imgs[i].file = pm_openr(strchr(argv[i+1], ':') + 1);
+        *strchr(argv[i+1], ':') = 0;
+        names[i] = argv[i+1];
+      }
+      else
+      {
+        imgs[i].file = pm_openr(argv[i+1]);
+        names[i] = argv[i+1];
+      }
+    }
+  }
+  else
+  {
+    imgs[0].file = stdin;
+  }
+
+  pnm_readpaminit(imgs[0].file, &imgs[0], PAM_STRUCT_SIZE(tuple_type));
+  outimg.maxval = imgs[0].maxval;
+  outimg.format = imgs[0].format;
+  memcpy(outimg.tuple_type, imgs[0].tuple_type, sizeof(imgs[0].tuple_type));
+  outimg.depth = imgs[0].depth;
+
+  for (i = 1; i < nfiles; ++i)
+  {
+    pnm_readpaminit(imgs[i].file, &imgs[i], PAM_STRUCT_SIZE(tuple_type));
+    if (PAM_FORMAT_TYPE(imgs[i].format) > PAM_FORMAT_TYPE(outimg.format))
+      outimg.format = imgs[i].format,
+      memcpy(outimg.tuple_type, imgs[i].tuple_type, 
+             sizeof(imgs[i].tuple_type));
+    outimg.maxval = imax(imgs[i].maxval, outimg.maxval);
+    outimg.depth = imax(imgs[i].depth, outimg.depth);
+  }
+
+  for (i = 0; i < nfiles - 1; ++i)
+    for (j = i + 1; j < nfiles; ++j)
+      if (imgs[j].width * imgs[j].height > imgs[i].width * imgs[i].height)
+        p = imgs[i], imgs[i] = imgs[j], imgs[j] = p,
+        c = names[i], names[i] = names[j], names[j] = c;
+
+  findpack(imgs, nfiles, coords);
+
+  outimg.height = outimg.width = 0;
+  for (i = 0; i < nfiles; ++i)
+  {
+    outimg.width = imax(outimg.width, imgs[i].width + coords[i].x);
+    outimg.height = imax(outimg.height, imgs[i].height + coords[i].y);
+  }
+
+  outimg.size = sizeof(outimg);
+  outimg.len = sizeof(outimg);
+  outimg.file = stdout;
+  outimg.bytes_per_sample = 0;
+  for (i = outimg.maxval; i; i >>= 8)
+    ++outimg.bytes_per_sample;
+
+  writePam(&outimg, nfiles, coords, imgs);
+
+  if (datafname)
+  {
+    fprintf(data, ":0:0:%u:%u\n", outimg.width, outimg.height);
+
+    for (i = 0; i < nfiles; ++i)
+    {
+      fprintf(data, "%s:%u:%u:%u:%u\n", names[i], coords[i].x,
+          coords[i].y, imgs[i].width, imgs[i].height);
+    }
+  }
+
+  if (headfname)
+  {
+    fprintf(header, "#define %sOVERALLX %u\n"
+                    "#define %sOVERALLY %u\n"
+                    "\n",
+                    prefix, outimg.width,
+                    prefix, outimg.height);
+
+    for (i = 0; i < nfiles; ++i)
+    {
+      *strchr(names[i], '.') = 0;
+      for (j = 0; names[i][j]; ++j)
+      {
+        if (ISLOWER(names[i][j]))
+          names[i][j] = TOUPPER(names[i][j]);
+      }
+      fprintf(header, "#define %s%sX %u\n"
+                      "#define %s%sY %u\n"
+                      "#define %s%sSZX %u\n"
+                      "#define %s%sSZY %u\n"
+                      "\n",
+                      prefix, names[i], coords[i].x,
+                      prefix, names[i], coords[i].y,
+                      prefix, names[i], imgs[i].width,
+                      prefix, names[i], imgs[i].height);
+    }
+  }
+
+  for (i = 0; i < nfiles; ++i)
+    pm_close(imgs[i].file);
+  pm_close(stdout);
+
+  if (headfname)
+    pm_close(header);
+
+  if (datafname)
+    pm_close(data);
+
+  return 0;
+}
diff --git a/editor/pnmnlfilt.c b/editor/pnmnlfilt.c
new file mode 100644
index 00000000..20705f82
--- /dev/null
+++ b/editor/pnmnlfilt.c
@@ -0,0 +1,1028 @@
+/* pnmnlfilt.c - 4 in 1 (2 non-linear) filter
+**             - smooth an anyimage
+**             - do alpha trimmed mean filtering on an anyimage
+**             - do optimal estimation smoothing on an anyimage
+**             - do edge enhancement on an anyimage
+**
+** Version 1.0
+**
+** The implementation of an alpha-trimmed mean filter
+** is based on the description in IEEE CG&A May 1990
+** Page 23 by Mark E. Lee and Richard A. Redner.
+**
+** The paper recommends using a hexagon sampling region around each
+** pixel being processed, allowing an effective sub pixel radius to be
+** specified. The hexagon values are sythesized by area sampling the
+** rectangular pixels with a hexagon grid. The seven hexagon values
+** obtained from the 3x3 pixel grid are used to compute the alpha
+** trimmed mean. Note that an alpha value of 0.0 gives a conventional
+** mean filter (where the radius controls the contribution of
+** surrounding pixels), while a value of 0.5 gives a median filter.
+** Although there are only seven values to trim from before finding
+** the mean, the algorithm has been extended from that described in
+** CG&A by using interpolation, to allow a continuous selection of
+** alpha value between and including 0.0 to 0.5  The useful values
+** for radius are between 0.3333333 (where the filter will have no
+** effect because only one pixel is sampled), to 1.0, where all
+** pixels in the 3x3 grid are sampled.
+**
+** The optimal estimation filter is taken from an article "Converting Dithered
+** Images Back to Gray Scale" by Allen Stenger, Dr Dobb's Journal, November
+** 1992, and this article references "Digital Image Enhancement andNoise Filtering by
+** Use of Local Statistics", Jong-Sen Lee, IEEE Transactions on Pattern Analysis and
+** Machine Intelligence, March 1980.
+**
+** Also borrow the  technique used in pgmenhance(1) to allow edge
+** enhancement if the alpha value is negative.
+**
+** Author:
+**         Graeme W. Gill, 30th Jan 1993
+**         graeme@labtam.oz.au
+**
+** 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 <math.h>
+
+#include "pm_c_util.h"
+#include "pnm.h"
+
+/* MXIVAL is the maximum input sample value we can handle.
+   It is limited by our willingness to allocate storage in various arrays
+   that are indexed by sample values.
+
+   We use PPM_MAXMAXVAL because that used to be the maximum possible
+   sample value in the format, and most images still limit themselves to
+   this value.
+*/
+
+#define MXIVAL PPM_MAXMAXVAL   
+
+xelval omaxval; 
+    /* global so that pixel processing code can get at it quickly */
+int noisevariance;      
+    /* global so that pixel processing code can get at it quickly */
+
+/*
+ * Declared static here rather than passing a jillion options in the call to
+ * do_one_frame().   Also it makes a huge amount of sense to only malloc the
+ * row buffers once instead of for each frame (with the corresponding free'ing
+ * of course).
+*/
+static  xel *irows[3];
+static  xel *irow0, *irow1, *irow2, *orow;
+static  double radius=0.0,alpha= -1.0;
+static  int rows, cols, format, oformat, row, col;
+static  int (*atfunc)(int *);
+static  xelval maxval;
+
+#define NOIVAL (MXIVAL + 1)             /* number of possible input values */
+
+#define SCALEB 8                                /* scale bits */
+#define SCALE (1 << SCALEB)     /* scale factor */
+#define MXSVAL (MXIVAL * SCALE) /* maximum scaled values */
+
+#define CSCALEB 2                               /* coarse scale bits */
+#define CSCALE (1 << CSCALEB)   /* coarse scale factor */
+#define MXCSVAL (MXIVAL * CSCALE)       /* maximum coarse scaled values */
+#define NOCSVAL (MXCSVAL + 1)   /* number of coarse scaled values */
+#define SCTOCSC(x) ((x) >> (SCALEB - CSCALEB))  /*  scaled to coarse scaled */
+#define CSCTOSC(x) ((x) << (SCALEB - CSCALEB))  /*  course scaled to scaled */
+
+#ifndef MAXINT
+# define MAXINT 0x7fffffff      /* assume this is a 32 bit machine */
+#endif
+
+/* round and scale floating point to scaled integer */
+#define ROUNDSCALE(x) ((int)(((x) * (double)SCALE) + 0.5))
+/* round and un-scale scaled integer value */
+#define RUNSCALE(x) (((x) + (1 << (SCALEB-1))) >> SCALEB) 
+/* rounded un-scale */
+#define UNSCALE(x) ((x) >> SCALEB)
+
+static double
+sqr(double const arg) {
+    return arg * arg;
+}
+
+
+
+/* We restrict radius to the values: 0.333333 <= radius <= 1.0 */
+/* so that no fewer and no more than a 3x3 grid of pixels around */
+/* the pixel in question needs to be read. Given this, we only */
+/* need 3 or 4 weightings per hexagon, as follows: */
+/*                  _ _                         */
+/* Vertical hex:   |_|_|  1 2                   */
+/*                 |X|_|  0 3                   */
+/*                                       _      */
+/*              _                      _|_|   1 */
+/* Middle hex: |_| 1  Horizontal hex: |X|_| 0 2 */
+/*             |X| 0                    |_|   3 */
+/*             |_| 2                            */
+
+/* all filters */
+int V0[NOIVAL],V1[NOIVAL],V2[NOIVAL],V3[NOIVAL];        /* vertical hex */
+int M0[NOIVAL],M1[NOIVAL],M2[NOIVAL];                   /* middle hex */
+int H0[NOIVAL],H1[NOIVAL],H2[NOIVAL],H3[NOIVAL];        /* horizontal hex */
+
+/* alpha trimmed and edge enhancement only */
+int ALFRAC[NOIVAL * 8];                 /* fractional alpha divider table */
+
+/* optimal estimation only */
+int AVEDIV[7 * NOCSVAL];                /* divide by 7 to give average value */
+int SQUARE[2 * NOCSVAL];                /* scaled square lookup table */
+
+/* ************************************************** *
+   Hexagon intersecting square area functions 
+   Compute the area of the intersection of a triangle 
+   and a rectangle 
+   ************************************************** */
+
+/* Triangle orientation is per geometric axes (not graphical axies) */
+
+#define NW 0    /* North west triangle /| */
+#define NE 1    /* North east triangle |\ */
+#define SW 2    /* South west triangle \| */
+#define SE 3    /* South east triangle |/ */
+#define STH 2
+#define EST 1
+
+#define SWAPI(a,b) (t = a, a = -b, b = -t)
+
+static double 
+triang_area(double rx0, double ry0, double rx1, double ry1,
+            double tx0, double ty0, double tx1, double ty1,
+            int tt) {
+/* rx0,ry0,rx1,ry1:       rectangle boundaries */
+/* tx0,ty0,tx1,ty1:       triangle boundaries */
+/* tt:                    triangle type */
+
+    double a,b,c,d;
+    double lx0,ly0,lx1,ly1;
+
+    /* Convert everything to a NW triangle */
+    if (tt & STH) {
+        double t;
+        SWAPI(ry0,ry1);
+        SWAPI(ty0,ty1);
+    }
+    if (tt & EST) {
+        double t;
+        SWAPI(rx0,rx1);
+        SWAPI(tx0,tx1);
+    }
+    /* Compute overlapping box */
+    if (tx0 > rx0)
+        rx0 = tx0;
+    if (ty0 > ry0)
+        ry0 = ty0;
+    if (tx1 < rx1)
+        rx1 = tx1;
+    if (ty1 < ry1)
+        ry1 = ty1;
+    if (rx1 <= rx0 || ry1 <= ry0)
+        return 0.0;
+
+    /* Need to compute diagonal line intersection with the box */
+    /* First compute co-efficients to formulas x = a + by and y = c + dx */
+    b = (tx1 - tx0)/(ty1 - ty0);
+    a = tx0 - b * ty0;
+    d = (ty1 - ty0)/(tx1 - tx0);
+    c = ty0 - d * tx0;
+    
+    /* compute top or right intersection */
+    tt = 0;
+    ly1 = ry1;
+    lx1 = a + b * ly1;
+    if (lx1 <= rx0)
+        return (rx1 - rx0) * (ry1 - ry0);
+    else if (lx1 > rx1) {
+        /* could be right hand side */
+        lx1 = rx1;
+        ly1 = c + d * lx1;
+        if (ly1 <= ry0)
+            return (rx1 - rx0) * (ry1 - ry0);
+        tt = 1; /* right hand side intersection */
+    }
+    /* compute left or bottom intersection */
+    lx0 = rx0;
+    ly0 = c + d * lx0;
+    if (ly0 >= ry1)
+        return (rx1 - rx0) * (ry1 - ry0);
+    else if (ly0 < ry0) {
+        /* could be right hand side */
+        ly0 = ry0;
+        lx0 = a + b * ly0;
+        if (lx0 >= rx1)
+            return (rx1 - rx0) * (ry1 - ry0);
+        tt |= 2;        /* bottom intersection */
+    }
+    
+    if (tt == 0) {
+        /* top and left intersection */
+        /* rectangle minus triangle */
+        return ((rx1 - rx0) * (ry1 - ry0))
+            - (0.5 * (lx1 - rx0) * (ry1 - ly0));
+    } else if (tt == 1) {
+        /* right and left intersection */
+        return ((rx1 - rx0) * (ly0 - ry0))
+            + (0.5 * (rx1 - rx0) * (ly1 - ly0));
+    } else if (tt == 2) {
+        /* top and bottom intersection */
+        return ((rx1 - lx1) * (ry1 - ry0))
+            + (0.5 * (lx1 - lx0) * (ry1 - ry0));
+    } else {
+        /* tt == 3 */ 
+        /* right and bottom intersection */
+        /* triangle */
+        return (0.5 * (rx1 - lx0) * (ly1 - ry0));
+    }
+}
+
+
+
+static double
+rectang_area(double rx0, double ry0, double rx1, double ry1, 
+             double tx0, double ty0, double tx1, double ty1) {
+/* Compute rectangle area */
+/* rx0,ry0,rx1,ry1:  rectangle boundaries */
+/* tx0,ty0,tx1,ty1:  rectangle boundaries */
+
+    /* Compute overlapping box */
+    if (tx0 > rx0)
+        rx0 = tx0;
+    if (ty0 > ry0)
+        ry0 = ty0;
+    if (tx1 < rx1)
+        rx1 = tx1;
+    if (ty1 < ry1)
+        ry1 = ty1;
+    if (rx1 <= rx0 || ry1 <= ry0)
+        return 0.0;
+    return (rx1 - rx0) * (ry1 - ry0);
+}
+
+
+
+
+static double 
+hex_area(double sx, double sy, double hx, double hy, double d) {
+/* compute the area of overlap of a hexagon diameter d, */
+/* centered at hx,hy, with a unit square of center sx,sy. */
+/* sx,sy:    square center */
+/* hx,hy,d:  hexagon center and diameter */
+
+    double hx0,hx1,hx2,hy0,hy1,hy2,hy3;
+    double sx0,sx1,sy0,sy1;
+
+    /* compute square co-ordinates */
+    sx0 = sx - 0.5;
+    sy0 = sy - 0.5;
+    sx1 = sx + 0.5;
+    sy1 = sy + 0.5;
+
+    /* compute hexagon co-ordinates */
+    hx0 = hx - d/2.0;
+    hx1 = hx;
+    hx2 = hx + d/2.0;
+    hy0 = hy - 0.5773502692 * d;    /* d / sqrt(3) */
+    hy1 = hy - 0.2886751346 * d;    /* d / sqrt(12) */
+    hy2 = hy + 0.2886751346 * d;    /* d / sqrt(12) */
+    hy3 = hy + 0.5773502692 * d;    /* d / sqrt(3) */
+
+    return
+        triang_area(sx0,sy0,sx1,sy1,hx0,hy2,hx1,hy3,NW) +
+        triang_area(sx0,sy0,sx1,sy1,hx1,hy2,hx2,hy3,NE) +
+        rectang_area(sx0,sy0,sx1,sy1,hx0,hy1,hx2,hy2) +
+        triang_area(sx0,sy0,sx1,sy1,hx0,hy0,hx1,hy1,SW) +
+        triang_area(sx0,sy0,sx1,sy1,hx1,hy0,hx2,hy1,SE);
+}
+
+
+
+
+static void
+setupAvediv(void) {
+
+    unsigned int i;
+
+    for (i=0; i < (7 * NOCSVAL); ++i) {
+        /* divide scaled value by 7 lookup */
+        AVEDIV[i] = CSCTOSC(i)/7;       /* scaled divide by 7 */
+    }
+
+}
+
+
+
+
+static void
+setupSquare(void) {
+
+    unsigned int i;
+
+    for (i=0; i < (2 * NOCSVAL); ++i) {
+        /* compute square and rescale by (val >> (2 * SCALEB + 2)) table */
+        int const val = CSCTOSC(i - NOCSVAL); 
+        /* NOCSVAL offset to cope with -ve input values */
+        SQUARE[i] = (val * val) >> (2 * SCALEB + 2);
+    }
+}
+
+
+
+
+static void
+setup1(double   const alpha,
+       double   const radius,
+       double   const maxscale,
+       int *    const alpharangeP,
+       double * const meanscaleP,
+       double * const mmeanscaleP,
+       double * const alphafractionP,
+       int *    const noisevarianceP) {
+
+
+    setupAvediv();
+    setupSquare();
+
+    if (alpha >= 0.0 && alpha <= 0.5) {
+        /* alpha trimmed mean */
+        double const noinmean =  ((0.5 - alpha) * 12.0) + 1.0;
+            /* number of elements (out of a possible 7) used in the mean */
+
+        *mmeanscaleP = *meanscaleP = maxscale/noinmean;
+        if (alpha == 0.0) {
+            /* mean filter */ 
+            *alpharangeP = 0;
+            *alphafractionP = 0.0;            /* not used */
+        } else if (alpha < (1.0/6.0)) {
+            /* mean of 5 to 7 middle values */
+            *alpharangeP = 1;
+            *alphafractionP = (7.0 - noinmean)/2.0;
+        } else if (alpha < (1.0/3.0)) {
+            /* mean of 3 to 5 middle values */
+            *alpharangeP = 2;
+            *alphafractionP = (5.0 - noinmean)/2.0;
+        } else {
+            /* mean of 1 to 3 middle values */
+            /* alpha == 0.5 == median filter */
+            *alpharangeP = 3;
+            *alphafractionP = (3.0 - noinmean)/2.0;
+        }
+    } else if (alpha >= 1.0 && alpha <= 2.0) {
+        /* optimal estimation - alpha controls noise variance threshold. */
+        double const alphaNormalized = alpha - 1.0;
+            /* normalize it to 0.0 -> 1.0 */
+        double const noinmean = 7.0;
+        *alpharangeP = 5;                 /* edge enhancement function */
+        *mmeanscaleP = *meanscaleP = maxscale;  /* compute scaled hex values */
+        *alphafractionP = 1.0/noinmean;   
+            /* Set up 1:1 division lookup - not used */
+        *noisevarianceP = sqr(alphaNormalized * omaxval) / 8.0;    
+            /* estimate of noise variance */
+    } else if (alpha >= -0.9 && alpha <= -0.1) {
+        /* edge enhancement function */
+        double const posAlpha = -alpha;
+            /* positive alpha value */
+        *alpharangeP = 4;                 /* edge enhancement function */
+        *meanscaleP = maxscale * (-posAlpha/((1.0 - posAlpha) * 7.0));
+            /* mean of 7 and scaled by -posAlpha/(1-posAlpha) */
+        *mmeanscaleP = maxscale * (1.0/(1.0 - posAlpha) + *meanscaleP);    
+            /* middle pixel has 1/(1-posAlpha) as well */
+        *alphafractionP = 0.0;    /* not used */
+    } else {
+        /* An entry condition on 'alpha' makes this impossible */
+        pm_error("INTERNAL ERROR: impossible alpha value: %f", alpha);
+    }
+}
+
+
+
+
+static void
+setupAlfrac(double const alphafraction) {
+    /* set up alpha fraction lookup table used on big/small */
+
+    unsigned int i;
+
+    for (i=0; i < (NOIVAL * 8); ++i) {
+        ALFRAC[i] = ROUNDSCALE(i * alphafraction);
+    }
+}
+
+
+
+
+static void
+setupPixelWeightingTables(double const radius,
+                          double const meanscale,
+                          double const mmeanscale) {
+
+    /* Setup pixel weighting tables - note we pre-compute mean
+       division here too. 
+    */
+    double const hexhoff = radius/2;      
+        /* horizontal offset of vertical hex centers */
+    double const hexvoff = 3.0 * radius/sqrt(12.0); 
+        /* vertical offset of vertical hex centers */
+
+    double const tabscale  = meanscale  / (radius * hexvoff);
+    double const mtabscale = mmeanscale / (radius * hexvoff);
+
+    /* scale tables to normalize by hexagon area, and number of
+       hexes used in mean 
+    */
+    double const v0 =
+        hex_area(0.0,  0.0, hexhoff, hexvoff, radius) * tabscale;
+    double const v1 =
+        hex_area(0.0,  1.0, hexhoff, hexvoff, radius) * tabscale;
+    double const v2 =
+        hex_area(1.0,  1.0, hexhoff, hexvoff, radius) * tabscale;
+    double const v3 =
+        hex_area(1.0,  0.0, hexhoff, hexvoff, radius) * tabscale;
+    double const m0 =
+        hex_area(0.0,  0.0, 0.0,     0.0,     radius) * mtabscale;
+    double const m1 =
+        hex_area(0.0,  1.0, 0.0,     0.0,     radius) * mtabscale;
+    double const m2 =
+        hex_area(0.0, -1.0, 0.0,     0.0,     radius) * mtabscale;
+    double const h0 =
+        hex_area(0.0,  0.0, radius,  0.0,     radius) * tabscale;
+    double const h1 =
+        hex_area(1.0,  1.0, radius,  0.0,     radius) * tabscale;
+    double const h2 =
+        hex_area(1.0,  0.0, radius,  0.0,     radius) * tabscale;
+    double const h3 =
+        hex_area(1.0, -1.0, radius,  0.0,     radius) * tabscale;
+
+    unsigned int i;
+
+    for (i=0; i <= MXIVAL; ++i) {
+        V0[i] = ROUNDSCALE(i * v0);
+        V1[i] = ROUNDSCALE(i * v1);
+        V2[i] = ROUNDSCALE(i * v2);
+        V3[i] = ROUNDSCALE(i * v3);
+        M0[i] = ROUNDSCALE(i * m0);
+        M1[i] = ROUNDSCALE(i * m1);
+        M2[i] = ROUNDSCALE(i * m2);
+        H0[i] = ROUNDSCALE(i * h0);
+        H1[i] = ROUNDSCALE(i * h1);
+        H2[i] = ROUNDSCALE(i * h2);
+        H3[i] = ROUNDSCALE(i * h3);
+    }
+}
+
+
+
+
+/* Table initialization function - return alpha range */
+static int 
+atfilt_setup(double const alpha,
+             double const radius,
+             double const maxscale) {
+
+    int alpharange;                 /* alpha range value 0 - 5 */
+    double meanscale;               /* scale for finding mean */
+    double mmeanscale;              /* scale for finding mean - midle hex */
+    double alphafraction;   
+        /* fraction of next largest/smallest to subtract from sum */
+
+    setup1(alpha, radius, maxscale,
+           &alpharange, &meanscale, &mmeanscale, &alphafraction,
+           &noisevariance);
+
+    setupAlfrac(alphafraction);
+
+    setupPixelWeightingTables(radius, meanscale, mmeanscale);
+
+    return alpharange;
+}
+
+
+
+static int 
+atfilt0(int * p) {
+/* Core pixel processing function - hand it 3x3 pixels and return result. */
+/* Mean filter */
+    /* 'p' is 9 pixel values from 3x3 neighbors */
+    int retv;
+    /* map to scaled hexagon values */
+    retv = M0[p[0]] + M1[p[3]] + M2[p[7]];
+    retv += H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]];
+    retv += V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]];
+    retv += V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]];
+    retv += H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]];
+    retv += V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]];
+    retv += V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]];
+    return UNSCALE(retv);
+}
+
+#define CHECK(xx) {\
+        h0 += xx; \
+        if (xx > big) \
+            big = xx; \
+        else if (xx < small) \
+            small = xx; }
+
+static int 
+atfilt1(int * p) {
+/* Mean of 5 - 7 middle values */
+/* 'p' is 9 pixel values from 3x3 neighbors */
+
+    int h0,h1,h2,h3,h4,h5,h6;       /* hexagon values    2 3   */
+                                    /*                  1 0 4  */
+                                    /*                   6 5   */
+    int big,small;
+    /* map to scaled hexagon values */
+    h0 = M0[p[0]] + M1[p[3]] + M2[p[7]];
+    h1 = H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]];
+    h2 = V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]];
+    h3 = V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]];
+    h4 = H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]];
+    h5 = V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]];
+    h6 = V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]];
+    /* sum values and also discover the largest and smallest */
+    big = small = h0;
+    CHECK(h1);
+    CHECK(h2);
+    CHECK(h3);
+    CHECK(h4);
+    CHECK(h5);
+    CHECK(h6);
+    /* Compute mean of middle 5-7 values */
+    return UNSCALE(h0 -ALFRAC[(big + small)>>SCALEB]);
+}
+#undef CHECK
+
+#define CHECK(xx) {\
+        h0 += xx; \
+        if (xx > big1) {\
+            if (xx > big0) {\
+                big1 = big0; \
+                big0 = xx; \
+            } else \
+                big1 = xx; \
+        } \
+        if (xx < small1) {\
+            if (xx < small0) {\
+                small1 = small0; \
+                small0 = xx; \
+                } else \
+                    small1 = xx; \
+        }\
+    }
+
+
+static int 
+atfilt2(int *p) {
+/* Mean of 3 - 5 middle values */
+/* 'p' is 9 pixel values from 3x3 neighbors */
+    int h0,h1,h2,h3,h4,h5,h6;       /* hexagon values    2 3   */
+                                    /*                  1 0 4  */
+                                    /*                   6 5   */
+    int big0,big1,small0,small1;
+    /* map to scaled hexagon values */
+    h0 = M0[p[0]] + M1[p[3]] + M2[p[7]];
+    h1 = H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]];
+    h2 = V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]];
+    h3 = V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]];
+    h4 = H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]];
+    h5 = V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]];
+    h6 = V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]];
+    /* sum values and also discover the 2 largest and 2 smallest */
+    big0 = small0 = h0;
+    small1 = MAXINT;
+    big1 = 0;
+    CHECK(h1);
+    CHECK(h2);
+    CHECK(h3);
+    CHECK(h4);
+    CHECK(h5);
+    CHECK(h6);
+    /* Compute mean of middle 3-5 values */
+    return UNSCALE(h0 -big0 -small0 -ALFRAC[(big1 + small1)>>SCALEB]);
+}
+
+#undef CHECK
+
+#define CHECK(xx) {\
+        h0 += xx; \
+        if (xx > big2) \
+                { \
+                if (xx > big1) \
+                        { \
+                        if (xx > big0) \
+                                { \
+                                big2 = big1; \
+                                big1 = big0; \
+                                big0 = xx; \
+                                } \
+                        else \
+                                { \
+                                big2 = big1; \
+                                big1 = xx; \
+                                } \
+                        } \
+                else \
+                        big2 = xx; \
+                } \
+        if (xx < small2) \
+                { \
+                if (xx < small1) \
+                        { \
+                        if (xx < small0) \
+                                { \
+                                small2 = small1; \
+                                small1 = small0; \
+                                small0 = xx; \
+                                } \
+                        else \
+                                { \
+                                small2 = small1; \
+                                small1 = xx; \
+                                } \
+                        } \
+                else \
+                        small2 = xx; \
+                                         }}
+
+static int 
+atfilt3(int *p) {
+/* Mean of 1 - 3 middle values. If only 1 value, then this is a median
+   filter. 
+*/
+/* 'p' is pixel values from 3x3 neighbors */
+    int h0,h1,h2,h3,h4,h5,h6;       /* hexagon values    2 3   */
+                                    /*                  1 0 4  */
+                                    /*                   6 5   */
+    int big0,big1,big2,small0,small1,small2;
+    /* map to scaled hexagon values */
+    h0 = M0[p[0]] + M1[p[3]] + M2[p[7]];
+    h1 = H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]];
+    h2 = V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]];
+    h3 = V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]];
+    h4 = H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]];
+    h5 = V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]];
+    h6 = V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]];
+    /* sum values and also discover the 3 largest and 3 smallest */
+    big0 = small0 = h0;
+    small1 = small2 = MAXINT;
+    big1 = big2 = 0;
+    CHECK(h1);
+    CHECK(h2);
+    CHECK(h3);
+    CHECK(h4);
+    CHECK(h5);
+    CHECK(h6);
+    /* Compute mean of middle 1-3 values */
+    return  UNSCALE(h0 -big0 -big1 -small0 -small1 
+                    -ALFRAC[(big2 + small2)>>SCALEB]);
+}
+#undef CHECK
+
+static int 
+atfilt4(int *p) {
+/* Edge enhancement */
+/* notice we use the global omaxval */
+/* 'p' is 9 pixel values from 3x3 neighbors */
+
+    int hav;
+    /* map to scaled hexagon values and compute enhance value */
+    hav = M0[p[0]] + M1[p[3]] + M2[p[7]];
+    hav += H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]];
+    hav += V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]];
+    hav += V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]];
+    hav += H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]];
+    hav += V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]];
+    hav += V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]];
+    if (hav < 0)
+        hav = 0;
+    hav = UNSCALE(hav);
+    if (hav > omaxval)
+        hav = omaxval;
+    return hav;
+}
+
+static int 
+atfilt5(int *p) {
+/* Optimal estimation - do smoothing in inverse proportion */
+/* to the local variance. */
+/* notice we use the globals noisevariance and omaxval*/
+/* 'p' is 9 pixel values from 3x3 neighbors */
+
+    int mean,variance,temp;
+    int h0,h1,h2,h3,h4,h5,h6;       /* hexagon values    2 3   */
+                                    /*                  1 0 4  */
+                                    /*                   6 5   */
+    /* map to scaled hexagon values */
+    h0 = M0[p[0]] + M1[p[3]] + M2[p[7]];
+    h1 = H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]];
+    h2 = V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]];
+    h3 = V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]];
+    h4 = H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]];
+    h5 = V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]];
+    h6 = V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]];
+    mean = h0 + h1 + h2 + h3 + h4 + h5 + h6;
+    mean = AVEDIV[SCTOCSC(mean)];   /* compute scaled mean by dividing by 7 */
+    temp = (h1 - mean); variance = SQUARE[NOCSVAL + SCTOCSC(temp)];  
+        /* compute scaled variance */
+    temp = (h2 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)]; 
+        /* and rescale to keep */
+    temp = (h3 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)]; 
+        /* within 32 bit limits */
+    temp = (h4 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)];
+    temp = (h5 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)];
+    temp = (h6 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)];
+    temp = (h0 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)];   
+    /* (temp = h0 - mean) */
+    if (variance != 0)      /* avoid possible divide by 0 */
+        temp = mean + (variance * temp) / (variance + noisevariance);   
+            /* optimal estimate */
+    else temp = h0;
+    if (temp < 0)
+        temp = 0;
+    temp = RUNSCALE(temp);
+    if (temp > omaxval)
+        temp = omaxval;
+    return temp;
+}
+
+
+
+static void 
+do_one_frame(FILE *ifp) {
+
+    pnm_writepnminit( stdout, cols, rows, omaxval, oformat, 0 );
+    
+    if ( PNM_FORMAT_TYPE(oformat) == PPM_TYPE ) {
+        int pr[9],pg[9],pb[9];          /* 3x3 neighbor pixel values */
+        int r,g,b;
+
+        for ( row = 0; row < rows; row++ ) {
+            int po,no;           /* offsets for left and right colums in 3x3 */
+            xel *ip0, *ip1, *ip2, *op;
+
+            if (row == 0) {
+                irow0 = irow1;
+                pnm_readpnmrow( ifp, irow1, cols, maxval, format );
+            }
+            if (row == (rows-1))
+                irow2 = irow1;
+            else
+                pnm_readpnmrow( ifp, irow2, cols, maxval, format );
+
+            for (col = cols-1,po= col>0?1:0,no=0,
+                     ip0=irow0,ip1=irow1,ip2=irow2,op=orow;
+                 col >= 0;
+                 col--,ip0++,ip1++,ip2++,op++, no |= 1,po = col!= 0 ? po : 0) {
+                                /* grab 3x3 pixel values */
+                pr[0] = PPM_GETR( *ip1 );
+                pg[0] = PPM_GETG( *ip1 );
+                pb[0] = PPM_GETB( *ip1 );
+                pr[1] = PPM_GETR( *(ip1-no) );
+                pg[1] = PPM_GETG( *(ip1-no) );
+                pb[1] = PPM_GETB( *(ip1-no) );
+                pr[5] = PPM_GETR( *(ip1+po) );
+                pg[5] = PPM_GETG( *(ip1+po) );
+                pb[5] = PPM_GETB( *(ip1+po) );
+                pr[3] = PPM_GETR( *(ip2) );
+                pg[3] = PPM_GETG( *(ip2) );
+                pb[3] = PPM_GETB( *(ip2) );
+                pr[2] = PPM_GETR( *(ip2-no) );
+                pg[2] = PPM_GETG( *(ip2-no) );
+                pb[2] = PPM_GETB( *(ip2-no) );
+                pr[4] = PPM_GETR( *(ip2+po) );
+                pg[4] = PPM_GETG( *(ip2+po) );
+                pb[4] = PPM_GETB( *(ip2+po) );
+                pr[6] = PPM_GETR( *(ip0+po) );
+                pg[6] = PPM_GETG( *(ip0+po) );
+                pb[6] = PPM_GETB( *(ip0+po) );
+                pr[8] = PPM_GETR( *(ip0-no) );
+                pg[8] = PPM_GETG( *(ip0-no) );
+                pb[8] = PPM_GETB( *(ip0-no) );
+                pr[7] = PPM_GETR( *(ip0) );
+                pg[7] = PPM_GETG( *(ip0) );
+                pb[7] = PPM_GETB( *(ip0) );
+                r = (*atfunc)(pr);
+                g = (*atfunc)(pg);
+                b = (*atfunc)(pb);
+                PPM_ASSIGN( *op, r, g, b );
+            }
+            pnm_writepnmrow( stdout, orow, cols, omaxval, oformat, 0 );
+            if (irow1 == irows[2]) {
+                irow1 = irows[0];
+                irow2 = irows[1];
+                irow0 = irows[2];
+            } else if (irow1 == irows[1]) {
+                irow2 = irows[0];
+                irow0 = irows[1];
+                irow1 = irows[2];
+            }
+            else    /* must be at irows[0] */
+            {
+                irow0 = irows[0];
+                irow1 = irows[1];
+                irow2 = irows[2];
+            }
+        }
+    } else {
+        /* Else must be PGM */
+        int p[9];               /* 3x3 neighbor pixel values */
+        int pv;
+        int promote;
+
+        /* we scale maxval to omaxval */
+        promote = ( PNM_FORMAT_TYPE(format) != PNM_FORMAT_TYPE(oformat) );
+
+        for ( row = 0; row < rows; row++ ) {
+            int po,no;          /* offsets for left and right colums in 3x3 */
+            xel *ip0, *ip1, *ip2, *op;
+
+            if (row == 0) {
+                irow0 = irow1;
+                pnm_readpnmrow( ifp, irow1, cols, maxval, format );
+                if ( promote )
+                    pnm_promoteformatrow( irow1, cols, maxval, 
+                                          format, maxval, oformat );
+            }
+            if (row == (rows-1))
+                irow2 = irow1;
+            else {
+                pnm_readpnmrow( ifp, irow2, cols, maxval, format );
+                if ( promote )
+                    pnm_promoteformatrow( irow2, cols, maxval, 
+                                          format, maxval, oformat );
+            }
+
+            for (col = cols-1,po= col>0?1:0,no=0,
+                     ip0=irow0,ip1=irow1,ip2=irow2,op=orow;
+                 col >= 0;
+                 col--,ip0++,ip1++,ip2++,op++, no |= 1,po = col!= 0 ? po : 0) {
+                /* grab 3x3 pixel values */
+                p[0] = PNM_GET1( *ip1 );
+                p[1] = PNM_GET1( *(ip1-no) );
+                p[5] = PNM_GET1( *(ip1+po) );
+                p[3] = PNM_GET1( *(ip2) );
+                p[2] = PNM_GET1( *(ip2-no) );
+                p[4] = PNM_GET1( *(ip2+po) );
+                p[6] = PNM_GET1( *(ip0+po) );
+                p[8] = PNM_GET1( *(ip0-no) );
+                p[7] = PNM_GET1( *(ip0) );
+                pv = (*atfunc)(p);
+                PNM_ASSIGN1( *op, pv );
+            }
+            pnm_writepnmrow( stdout, orow, cols, omaxval, oformat, 0 );
+            if (irow1 == irows[2]) {
+                irow1 = irows[0];
+                irow2 = irows[1];
+                irow0 = irows[2];
+            } else if (irow1 == irows[1]) {
+                irow2 = irows[0];
+                irow0 = irows[1];
+                irow1 = irows[2];
+            } else {
+                /* must be at irows[0] */
+                irow0 = irows[0];
+                irow1 = irows[1];
+                irow2 = irows[2];
+            }
+        }
+    }
+}
+
+
+
+static void
+verifySame(unsigned int const imageSeq, 
+           int const imageCols, int const imageRows,
+           xelval const imageMaxval, int const imageFormat,
+           int const cols, int const rows,
+           xelval const maxval, int const format) {
+/*----------------------------------------------------------------------------
+   Issue error message and exit the program if the imageXXX arguments don't
+   match the XXX arguments.
+-----------------------------------------------------------------------------*/
+    if (imageCols != cols)
+        pm_error("Width of Image %u (%d) is not the same as Image 0 (%d)",
+                 imageSeq, imageCols, cols);
+    if (imageRows != rows)
+        pm_error("Height of Image %u (%d) is not the same as Image 0 (%d)",
+                 imageSeq, imageRows, rows);
+    if (imageMaxval != maxval)
+        pm_error("Maxval of Image %u (%u) is not the same as Image 0 (%u)",
+                 imageSeq, imageMaxval, maxval);
+    if (imageFormat != format)
+        pm_error("Format of Image %u is not the same as Image 0",
+                 imageSeq);
+}
+
+
+
+int (*atfuncs[6]) (int *) = {atfilt0,atfilt1,atfilt2,atfilt3,atfilt4,atfilt5};
+
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    FILE * ifp;
+	bool eof;  /* We've hit the end of the input stream */
+    unsigned int imageSeq;  /* Sequence number of image, starting from 0 */
+
+    const char* const usage = "alpha radius pnmfile\n"
+        "0.0 <= alpha <= 0.5 for alpha trimmed mean -or- \n"
+        "1.0 <= alpha <= 2.0 for optimal estimation -or- \n"
+        "-0.1 >= alpha >= -0.9 for edge enhancement\n"
+        "0.3333 <= radius <= 1.0 specify effective radius\n";
+
+    pnm_init( &argc, argv );
+
+    if ( argc < 3 || argc > 4 )
+        pm_usage( usage );
+
+    if ( sscanf( argv[1], "%lf", &alpha ) != 1 )
+        pm_usage( usage );
+    if ( sscanf( argv[2], "%lf", &radius ) != 1 )
+        pm_usage( usage );
+        
+    if ((alpha > -0.1 && alpha < 0.0) || (alpha > 0.5 && alpha < 1.0))
+        pm_error( "Alpha must be in range 0.0 <= alpha <= 0.5 "
+                  "for alpha trimmed mean" );
+    if (alpha > 2.0)
+        pm_error( "Alpha must be in range 1.0 <= alpha <= 2.0 "
+                  "for optimal estimation" );
+    if (alpha < -0.9 || (alpha > -0.1 && alpha < 0.0))
+        pm_error( "Alpha must be in range -0.9 <= alpha <= -0.1 "
+                  "for edge enhancement" );
+    if (radius < 0.333 || radius > 1.0)
+        pm_error( "Radius must be in range 0.333333333 <= radius <= 1.0" );
+
+    if ( argc == 4 )
+        ifp = pm_openr( argv[3] );
+    else
+        ifp = stdin;
+        
+    pnm_readpnminit( ifp, &cols, &rows, &maxval, &format );
+        
+    if (maxval > MXIVAL) 
+        pm_error("The maxval of the input image (%d) is too large.\n"
+                 "This program's limit is %d.", 
+                 maxval, MXIVAL);
+        
+    oformat = PNM_FORMAT_TYPE(format);
+    /* force output to max precision without forcing new 2-byte format */
+    omaxval = MIN(maxval, PPM_MAXMAXVAL);
+        
+    atfunc = atfuncs[atfilt_setup(alpha, radius,
+                                  (double)omaxval/(double)maxval)];
+
+    if ( oformat < PGM_TYPE ) {
+        oformat = RPGM_FORMAT;
+        pm_message( "promoting file to PGM" );
+    }
+
+    orow = pnm_allocrow(cols);
+    irows[0] = pnm_allocrow(cols);
+    irows[1] = pnm_allocrow(cols);
+    irows[2] = pnm_allocrow(cols);
+    irow0 = irows[0];
+    irow1 = irows[1];
+    irow2 = irows[2];
+
+    eof = FALSE;  /* We're already in the middle of the first image */
+    imageSeq = 0;
+    while (!eof) {
+        do_one_frame(ifp);
+        pm_nextimage(ifp, &eof);
+        if (!eof) {
+            /* Read and validate header of next image */
+            int imageCols, imageRows;
+            xelval imageMaxval;
+            int imageFormat;
+
+            ++imageSeq;
+            pnm_readpnminit(ifp, &imageCols, &imageRows, 
+                            &imageMaxval, &imageFormat);
+            verifySame(imageSeq,
+                       imageCols, imageRows, imageMaxval, imageFormat,
+                       cols, rows, maxval, format);
+        }
+    }
+
+    pnm_freerow(irow0);
+    pnm_freerow(irow1);
+    pnm_freerow(irow2);
+    pnm_freerow(orow);
+    pm_close(ifp);
+
+    return 0;
+}
+
+
diff --git a/editor/pnmnorm.c b/editor/pnmnorm.c
new file mode 100644
index 00000000..51d954a8
--- /dev/null
+++ b/editor/pnmnorm.c
@@ -0,0 +1,620 @@
+/******************************************************************************
+                                  Pnmnorm
+*******************************************************************************
+
+  This program normalizes the contrast in a Netpbm image.
+
+  by Bryan Henderson bryanh@giraffe-data.com San Jose CA March 2002.
+  Adapted from Ppmnorm.
+
+  Ppmnorm is by Wilson H. Bent, Jr. (whb@usc.edu)
+  Extensively hacked from pgmnorm.c, which carries the following note:
+  
+  Copyright (C) 1989, 1991 by Jef Poskanzer.
+  
+  Permission to use, copy, modify, and distribute this software and its
+  documentation for any purpose and without fee is hereby granted, provided
+  that the above copyright notice appear in all copies and that both that
+  copyright notice and this permission notice appear in supporting
+  documentation.  This software is provided "as is" without express or
+  implied warranty.
+  
+  (End of note from pgmnorm.c)
+
+  Pgmnorm's man page also said:
+  
+  Partially based on the fbnorm filter in Michael Mauldin's "Fuzzy Pixmap"
+  package.
+*****************************************************************************/
+
+#include <assert.h>
+
+#include "pnm.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+enum brightMethod {BRIGHT_LUMINOSITY, BRIGHT_COLORVALUE, BRIGHT_SATURATION};
+
+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 */
+    unsigned int bvalueSpec;
+    xelval bvalue;
+    unsigned int bpercentSpec;
+    float bpercent;
+    unsigned int wvalueSpec;
+    xelval wvalue;
+    unsigned int wpercentSpec;
+    float wpercent;
+    enum brightMethod brightMethod;
+    unsigned int keephues;
+    float maxExpansion;
+        /* The maximum allowed expansion factor for expansion specified
+           by per centile.  This is a factor, not a per cent increase.
+           E.g. 50% increase means a factor of 1.50.
+        */
+};
+
+
+
+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 luminosity, colorvalue, saturation;
+    unsigned int maxexpandSpec;
+    float maxexpand;
+    
+    unsigned int option_def_index;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0,   "bpercent",      OPT_FLOAT,   
+            &cmdlineP->bpercent,   &cmdlineP->bpercentSpec, 0);
+    OPTENT3(0,   "wpercent",      OPT_FLOAT,   
+            &cmdlineP->wpercent,   &cmdlineP->wpercentSpec, 0);
+    OPTENT3(0,   "bvalue",        OPT_UINT,   
+            &cmdlineP->bvalue,     &cmdlineP->bvalueSpec, 0);
+    OPTENT3(0,   "wvalue",        OPT_UINT,   
+            &cmdlineP->wvalue,     &cmdlineP->wvalueSpec, 0);
+    OPTENT3(0,   "maxexpand",     OPT_FLOAT,   
+            &maxexpand,            &maxexpandSpec, 0);
+    OPTENT3(0,   "keephues",      OPT_FLAG,   
+            NULL,                  &cmdlineP->keephues, 0);
+    OPTENT3(0,   "luminosity",    OPT_FLAG,   
+            NULL,                  &luminosity, 0);
+    OPTENT3(0,   "colorvalue",    OPT_FLAG,   
+            NULL,                  &colorvalue, 0);
+    OPTENT3(0,   "saturation",    OPT_FLAG,   
+            NULL,                  &saturation, 0);
+    OPTENT3(0,   "brightmax",     OPT_FLAG,   
+            NULL,                  &colorvalue, 0);
+
+    /* Note: -brightmax was documented and accepted long before it was
+       actually implemented.  By the time we implemented it, we
+       decided -colorvalue was a better name for it.
+    */
+
+    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 (!cmdlineP->wpercentSpec)
+        cmdlineP->wpercent = 1.0;
+    if (!cmdlineP->bpercentSpec)
+        cmdlineP->bpercent = 2.0;
+
+    if (cmdlineP->wpercent < 0.0)
+        pm_error("You specified a negative value for wpercent: %f",
+                 cmdlineP->wpercent);
+    if (cmdlineP->bpercent < 0.0)
+        pm_error("You specified a negative value for bpercent: %f",
+                 cmdlineP->bpercent);
+    if (cmdlineP->wpercent > 100.0)
+        pm_error("You specified a per centage > 100 for wpercent: %f",
+                 cmdlineP->wpercent);
+    if (cmdlineP->bpercent > 100.0)
+        pm_error("You specified a per centage > 100 for bpercent: %f",
+                 cmdlineP->bpercent);
+
+    if (luminosity + colorvalue + saturation > 1)
+        pm_error("You can specify only one of "
+                 "-luminosity, -colorvalue, and -saturation");
+    else {
+        if (colorvalue)
+            cmdlineP->brightMethod = BRIGHT_COLORVALUE;
+        else if (saturation)
+            cmdlineP->brightMethod = BRIGHT_SATURATION;
+        else
+            cmdlineP->brightMethod = BRIGHT_LUMINOSITY;
+    }
+    if (maxexpandSpec) {
+        if (maxexpand < 0)
+            pm_error("-maxexpand must be positive.  You specified %f",
+                     maxexpand);
+        else
+            cmdlineP->maxExpansion = 1 + (float)maxexpand/100;
+    } else
+        cmdlineP->maxExpansion = 1e6;  /* essentially infinite */
+
+    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 void
+buildHistogram(FILE *   const ifp, 
+               int      const cols,
+               int      const rows,
+               xelval   const maxval,
+               int      const format,
+               unsigned int   hist[],
+               enum brightMethod const brightMethod) {
+/*----------------------------------------------------------------------------
+   Build the histogram of brightness values for the image that is in file
+   'ifp', which is positioned just after the header (at the raster).
+
+   The histogram is the array hist[] such that hist[x] is the number
+   of xels in the image that have brightness x.  That brightness is
+   either the color value (intensity of most intense component) of the
+   xel or it is the luminosity of the xel, depending on
+   'brightMethod'.  In either case, it is based on the same maxval as
+   the image, which is 'maxval'.  The image is 'cols' columns wide by
+   'rows' rows high.
+
+   Leave the file positioned arbitrarily.
+-----------------------------------------------------------------------------*/
+    int row;
+    xel * xelrow;
+    
+    xelrow = pnm_allocrow(cols);
+
+    {
+        unsigned int i;
+        for (i = 0; i <= maxval; ++i)
+            hist[i] = 0;
+    }
+    for (row = 0; row < rows; ++row) {
+        int col;
+        pnm_readpnmrow(ifp, xelrow, cols, maxval, format);
+        for (col = 0; col < cols; ++col) {
+            xelval brightness;
+            xel const p = xelrow[col];
+            if (PNM_FORMAT_TYPE(format) == PPM_TYPE) {
+                switch(brightMethod) {
+                case BRIGHT_LUMINOSITY:
+                    brightness = PPM_LUMIN(p);
+                    break;
+                case BRIGHT_COLORVALUE:
+                    brightness = ppm_colorvalue(p);
+                    break;
+                case BRIGHT_SATURATION:
+                    brightness = ppm_saturation(p, maxval);
+                    break;
+                }
+            } else
+                brightness = PNM_GET1(p);
+            ++hist[brightness];
+        }
+    }
+    pnm_freerow(xelrow);
+}
+
+
+
+static void
+computeBottomPercentile(unsigned int         hist[], 
+                        unsigned int   const highest,
+                        unsigned int   const total,
+                        float          const percent, 
+                        unsigned int * const percentileP) {
+/*----------------------------------------------------------------------------
+   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[],
+   given to us to save us the time of computing it).
+-----------------------------------------------------------------------------*/
+    unsigned int cutoff = total * percent / 100.0;
+    unsigned int count;
+    unsigned int percentile;
+
+    percentile = 0; /* initial value */
+    count = hist[0];  /* initial value */
+
+    while (count < cutoff) {
+        if (percentile == highest)
+            pm_error("Internal error: computeBottomPercentile() received"
+                     "a 'total' value greater than the sum of the hist[]"
+                     "values");
+        ++percentile;
+        count += hist[percentile];
+    }        
+    *percentileP = percentile;
+}
+
+
+
+static void
+computeTopPercentile(unsigned int         hist[], 
+                     unsigned int   const highest, 
+                     unsigned int   const total,
+                     float          const percent, 
+                     unsigned int * const percentileP) {
+/*----------------------------------------------------------------------------
+   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[],
+   given to us to save us the time of computing it).
+-----------------------------------------------------------------------------*/
+    unsigned int cutoff = total * percent / 100.0;
+    unsigned int count;
+    unsigned int percentile;
+
+    percentile = highest; /* initial value */
+    count = hist[highest];
+
+    while (count < cutoff) {
+        --percentile;
+        count += hist[percentile];
+    }
+    *percentileP = percentile;
+}
+
+
+
+static void
+computeAdjustmentForExpansionLimit(xelval   const maxval,
+                                   xelval   const unlBvalue,
+                                   xelval   const unlWvalue,
+                                   float    const maxExpansion,
+                                   xelval * const bLowerP,
+                                   xelval * const wRaiseP) {
+/*----------------------------------------------------------------------------
+   Assuming 'unlBvalue' and 'unlWvalue' are the appropriate bvalue and
+   wvalue to normalize the image to 0 .. maxval, compute the amount
+   by which the bvalue must be raised and the wvalue lowered from that
+   in order to cap the expansion factor at 'maxExpansion'.
+
+   E.g. if 'maxval' is 100, 'unlBvalue' is 20 and 'unlWvalue' is 70, that
+   implies an expansion factor of 100/50 (because the range goes from
+   70-20, which is 50, to 100 - 0, which is 100).  If 'maxEpansion' is
+   1.333, these values are unacceptable.  To get down to the desired 1.333
+   factor, we need the span of bvalue to wvalue to be 75, not 50.  So
+   we need to raise the bvalue and lower the wvalue by a total of 25.
+   We apportion that adjustment to bvalue and wvalue in proportion to
+   how close each is already to it's end (which we call the margin).
+   'unlBvalue' is 20 from its end, while 'unlWvalue' is 30 from its end,
+   so we want to lower the bvalue by 10 and raise the wvalue by 15.
+   Ergo we return *bLowerP = 10 and *wRaise = 15.
+-----------------------------------------------------------------------------*/
+    unsigned int const newRange = maxval - 0;
+        /* The range of sample values after normalization, if we used
+           the unlimited bvalue and wvalue
+        */
+    unsigned int const oldRange = unlWvalue - unlBvalue;
+        /* The range of sample values in the original image that normalize
+           to 0 .. maxval, if we used the unlimited bvalue and wvalue
+        */
+    float const unlExpansion = (float)newRange/oldRange;
+    
+    if (unlExpansion <= maxExpansion) {
+        /* No capping is necessary.  Unlimited values are already within
+           range.
+           */
+        *bLowerP = 0;
+        *wRaiseP = 0;
+    } else {
+        unsigned int const totalWidening = newRange/maxExpansion - oldRange;
+            /* Amount by which the (bvalue, wvalue) range must be widened
+               to limit expansion to 'maxExpansion'
+            */
+        unsigned int const bMargin = unlBvalue - 0;
+        unsigned int const wMargin = maxval - unlWvalue;
+
+        /* Apportion 'totalWidening' between the black and and the
+           white end
+        */
+        *bLowerP =
+            ROUNDU((float)bMargin / (bMargin + wMargin) * totalWidening);
+        *wRaiseP =
+            ROUNDU((float)wMargin / (bMargin + wMargin) * totalWidening);
+
+        pm_message("limiting expansion of %.1f%% to %.1f%%",
+                   (unlExpansion - 1) * 100, (maxExpansion -1) * 100);
+    }
+}
+
+
+
+static void
+computeEndValues(FILE *             const ifp,
+                 int                const cols,
+                 int                const rows,
+                 xelval             const maxval,
+                 int                const format,
+                 struct cmdlineInfo const cmdline,
+                 xelval *           const bvalueP,
+                 xelval *           const wvalueP) {
+/*----------------------------------------------------------------------------
+   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.
+-----------------------------------------------------------------------------*/
+    unsigned int * hist;  /* malloc'ed */
+
+    MALLOCARRAY(hist, PNM_OVERALLMAXVAL+1);
+
+    if (hist == NULL)
+        pm_error("Unable to allocate storage for intensity histogram.");
+    else {
+        xelval unlimitedBvalue, unlimitedWvalue;
+        unsigned int bLower, wRaise;
+
+        buildHistogram(ifp, cols, rows, maxval, format, hist,
+                       cmdline.brightMethod);
+
+        if (cmdline.bvalueSpec && !cmdline.bpercentSpec) {
+            unlimitedBvalue = cmdline.bvalue;
+        } else {
+            xelval percentBvalue;
+            computeBottomPercentile(hist, maxval, cols*rows, cmdline.bpercent, 
+                                    &percentBvalue);
+            if (cmdline.bvalueSpec)
+                unlimitedBvalue = MIN(percentBvalue, cmdline.bvalue);
+            else
+                unlimitedBvalue = percentBvalue;
+        }
+
+        if (cmdline.wvalueSpec && !cmdline.wpercentSpec) {
+            unlimitedWvalue = cmdline.wvalue;
+        } else {
+            xelval percentWvalue;
+            computeTopPercentile(hist, maxval, cols*rows, cmdline.wpercent, 
+                                 &percentWvalue);
+            if (cmdline.wvalueSpec)
+                unlimitedWvalue = MIN(percentWvalue, cmdline.wvalue);
+            else
+                unlimitedWvalue = percentWvalue;
+        }
+
+        computeAdjustmentForExpansionLimit(
+            maxval, unlimitedBvalue, unlimitedWvalue, cmdline.maxExpansion,
+            &bLower, &wRaise);
+
+        *bvalueP = unlimitedBvalue - bLower;
+        *wvalueP = unlimitedWvalue + wRaise;
+
+        free(hist);
+    }
+}
+
+
+
+static void
+computeTransferFunction(xelval            const bvalue, 
+                        xelval            const wvalue,
+                        xelval            const maxval,
+                        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
+   xel with brightness x.  Brightness in this case means either luminosity
+   or color value (and it doesn't matter to us which).
+
+   '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).
+
+   Define function only for values 0..maxval.
+-----------------------------------------------------------------------------*/
+    xelval * newBrightness;
+    xelval i;
+
+    MALLOCARRAY(newBrightness, maxval+1);
+
+    if (newBrightness == NULL)
+        pm_error("Unable to allocate memory for transfer function.");
+
+    /* Clip the lowest brightnesses to zero */
+    if (bvalue > 0) 
+        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);
+    }
+
+    /* Clip the highest brightnesses to maxval */
+    for (i = wvalue+1; i <= maxval; ++i)
+        newBrightness[i] = maxval;
+
+    *newBrightnessP = newBrightness;
+}
+            
+
+
+static float
+brightScaler(xel               const p,
+             pixval            const maxval,
+             xelval            const newBrightness[],
+             enum brightMethod const brightMethod) {
+/*----------------------------------------------------------------------------
+  Return the multiple by which the brightness pixel of color 'p' (based
+  on maxval 'maxval') should be changed according to the transfer
+  function newBrightness[], using the 'brightMethod' measure of
+  brightness.
+
+  For example, if 'brightMethod' is BRIGHT_LUMINOSITY, p is has
+  luminosity 50, and newBrightness[50] is 75, we would return 1.5.
+-----------------------------------------------------------------------------*/
+    xelval oldBrightness;
+    float scaler;
+             
+    switch (brightMethod) {
+    case BRIGHT_LUMINOSITY:
+        oldBrightness = PPM_LUMIN(p);
+        break;
+    case BRIGHT_COLORVALUE:
+        oldBrightness = ppm_colorvalue(p);
+        break;
+    case BRIGHT_SATURATION:
+        oldBrightness = ppm_saturation(p, maxval);
+        break;
+    }
+    if (oldBrightness == 0) {
+        assert(newBrightness[oldBrightness] == 0);
+        /* Doesn't matter what we scale by.  zero times anything is zero. */
+        scaler = 1.0;
+    } else
+        scaler = (float)newBrightness[oldBrightness]/oldBrightness;
+
+    return scaler;
+}
+            
+
+
+static void
+writeRowNormalized(xel *             const xelrow,
+                   int               const cols,
+                   xelval            const maxval,
+                   int               const format,
+                   enum brightMethod const brightMethod,
+                   bool              const keephues,
+                   xelval            const newBrightness[],
+                   xel *             const rowbuf) {
+/*----------------------------------------------------------------------------
+   Write to Standard Output a normalized version of the xel row 
+   'xelrow'.  Normalize it via the transfer function newBrightness[].
+
+   Use 'rowbuf' as a work buffer.  It is at least 'cols' columns wide.
+-----------------------------------------------------------------------------*/
+    xel * const outrow = rowbuf;
+                
+    unsigned int col;
+    for (col = 0; col < cols; ++col) {
+        xel const p = xelrow[col];
+
+        if (PPM_FORMAT_TYPE(format) == PPM_TYPE) {
+            if (keephues) {
+                float const scaler =
+                    brightScaler(p, maxval, newBrightness, brightMethod);
+
+                xelval const r = MIN((int)(PPM_GETR(p)*scaler+0.5), maxval);
+                xelval const g = MIN((int)(PPM_GETG(p)*scaler+0.5), maxval);
+                xelval const b = MIN((int)(PPM_GETB(p)*scaler+0.5), maxval);
+                PNM_ASSIGN(outrow[col], r, g, b);
+            } else 
+                PNM_ASSIGN(outrow[col], 
+                           newBrightness[PPM_GETR(p)], 
+                           newBrightness[PPM_GETG(p)], 
+                           newBrightness[PPM_GETB(p)]);
+        } else 
+            PNM_ASSIGN1(outrow[col], newBrightness[PNM_GET1(p)]);
+    }
+    pnm_writepnmrow(stdout, outrow, cols, maxval, format, 0);
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE *ifP;
+    pm_filepos imagePos;
+    xelval maxval;
+    int rows, cols, format;
+    xelval bvalue, wvalue;
+    
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr_seekable(cmdline.inputFilespec);
+
+    /* 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);
+        
+    if (wvalue <= bvalue)
+        pm_error("The colors which become black would overlap the "
+                 "colors which become white.");
+    else {
+        xelval * newBrightness;
+        int row;
+        xel * xelrow;
+        xel * rowbuf;
+        
+        xelrow = pnm_allocrow(cols);
+
+        pm_message("remapping %d..%d to %d..%d", bvalue, wvalue, 0, maxval);
+
+        computeTransferFunction(bvalue, wvalue, maxval, &newBrightness);
+
+        pm_seek2(ifP, &imagePos, sizeof(imagePos));
+        pnm_writepnminit(stdout, cols, rows, maxval, format, 0);
+
+        rowbuf = pnm_allocrow(cols);
+
+        for (row = 0; row < rows; ++row) {
+            pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
+            writeRowNormalized(xelrow, cols, maxval, format,
+                               cmdline.keephues, cmdline.brightMethod,
+                               newBrightness, rowbuf);
+        }
+        free(newBrightness);
+        pnm_freerow(rowbuf);
+        pnm_freerow(xelrow);
+    }
+    pm_close(ifP);
+    return 0;
+}
diff --git a/editor/pnmpad.c b/editor/pnmpad.c
new file mode 100644
index 00000000..e1fbdaec
--- /dev/null
+++ b/editor/pnmpad.c
@@ -0,0 +1,387 @@
+/* pnmpad.c - add border to sides of a portable anymap
+ ** AJCD 4/9/90
+ */
+
+/*
+ * Changelog
+ *
+ * 2002/01/25 - Rewrote options parsing code.
+ *      Added pad-to-width and pad-to-height with custom
+ *      alignment.  MVB.
+ */
+
+#include <string.h>
+#include <stdio.h>
+
+#include "pnm.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+
+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 */
+    unsigned int xsize;
+    unsigned int xsizeSpec;
+    unsigned int ysize;
+    unsigned int ysizeSpec;
+    unsigned int left;
+    unsigned int right;
+    unsigned int top;
+    unsigned int bottom;
+    unsigned int leftSpec;
+    unsigned int rightSpec;
+    unsigned int topSpec;
+    unsigned int bottomSpec;
+    float xalign;
+    float yalign;
+    unsigned int white;     /* >0: pad white; 0: pad black */
+    unsigned int verbose;
+};
+
+
+
+static void
+parseCommandLine(int argc, char ** 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 blackOpt;
+    unsigned int xalignSpec, yalignSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0,   "xsize",     OPT_UINT,    &cmdlineP->xsize,       
+            &cmdlineP->xsizeSpec, 0);
+    OPTENT3(0,   "width",     OPT_UINT,    &cmdlineP->xsize,
+            &cmdlineP->xsizeSpec, 0);
+    OPTENT3(0,   "ysize",     OPT_UINT,    &cmdlineP->ysize,
+            &cmdlineP->ysizeSpec, 0);
+    OPTENT3(0,   "height",    OPT_UINT,    &cmdlineP->ysize,
+            &cmdlineP->ysizeSpec, 0);
+    OPTENT3(0,   "left",      OPT_UINT,    &cmdlineP->left, 
+            &cmdlineP->leftSpec, 0);
+    OPTENT3(0,   "right",     OPT_UINT,    &cmdlineP->right, 
+            &cmdlineP->rightSpec, 0);
+    OPTENT3(0,   "top",       OPT_UINT,    &cmdlineP->top, 
+            &cmdlineP->topSpec, 0);
+    OPTENT3(0,   "bottom",    OPT_UINT,    &cmdlineP->bottom, 
+            &cmdlineP->bottomSpec, 0);
+    OPTENT3(0,   "xalign",    OPT_FLOAT,   &cmdlineP->xalign,
+            &xalignSpec,           0);
+    OPTENT3(0,   "halign",    OPT_FLOAT,   &cmdlineP->xalign,
+            &xalignSpec,           0);
+    OPTENT3(0,   "yalign",    OPT_FLOAT,   &cmdlineP->yalign,
+            &yalignSpec,           0);
+    OPTENT3(0,   "valign",    OPT_FLOAT,   &cmdlineP->yalign,
+            &yalignSpec,           0);
+    OPTENT3(0,   "black",     OPT_FLAG,    NULL, 
+            &blackOpt,           0);
+    OPTENT3(0,   "white",     OPT_FLAG,    NULL,
+            &cmdlineP->white,    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 *cmdlineP and others. */
+
+    if (blackOpt && cmdlineP->white)
+        pm_error("You cannot specify both -black and -white");
+
+    if (xalignSpec && (cmdlineP->leftSpec || cmdlineP->rightSpec))
+        pm_error("You cannot specify both -xalign and -left or -right");
+
+    if (yalignSpec && (cmdlineP->topSpec || cmdlineP->bottomSpec))
+        pm_error("You cannot specify both -yalign and -top or -bottom");
+
+    if (xalignSpec && !cmdlineP->xsizeSpec)
+        pm_error("-xalign is meaningless without -width");
+
+    if (yalignSpec && !cmdlineP->ysizeSpec)
+        pm_error("-yalign is meaningless without -height");
+
+    if (xalignSpec) {
+        if (cmdlineP->xalign < 0)
+            pm_error("You have specified a negative -halign value (%f)", 
+                     cmdlineP->xalign);
+        if (cmdlineP->xalign > 1)
+            pm_error("You have specified a -halign value (%f) greater than 1", 
+                     cmdlineP->xalign);
+    } else
+        cmdlineP->xalign = 0.5;
+
+    if (yalignSpec) {
+        if (cmdlineP->yalign < 0)
+            pm_error("You have specified a negative -halign value (%f)", 
+                     cmdlineP->yalign);
+        if (cmdlineP->yalign > 1)
+            pm_error("You have specified a -valign value (%f) greater than 1", 
+                     cmdlineP->yalign);
+    } else
+        cmdlineP->yalign = 0.5;
+
+    /* get optional input filename */
+    if (argc-1 > 1)
+        pm_error("This program takes at most 1 parameter.  You specified %d",
+                 argc-1);
+    else if (argc-1 == 1) 
+        cmdlineP->input_filespec = argv[1];
+    else 
+        cmdlineP->input_filespec = "-";
+}
+
+
+
+static void
+parseCommandLineOld(int argc, char ** argv,
+                    struct cmdlineInfo * const cmdlineP) {
+
+    /* This syntax was abandonned in February 2002. */
+    pm_message("Warning: old style options are deprecated!");
+
+    cmdlineP->xsize = cmdlineP->ysize = 0;
+    cmdlineP->left = cmdlineP->right = cmdlineP->top = cmdlineP->bottom = 0;
+    cmdlineP->xalign = cmdlineP->yalign = 0.5;
+    cmdlineP->white = cmdlineP->verbose = FALSE;
+
+    while (argc >= 2 && argv[1][0] == '-') {
+        if (strcmp(argv[1]+1,"black") == 0) cmdlineP->white = FALSE;
+        else if (strcmp(argv[1]+1,"white") == 0) cmdlineP->white = TRUE;
+        else switch (argv[1][1]) {
+        case 'l':
+            if (atoi(argv[1]+2) < 0)
+                pm_error("left border too small");
+            else
+                cmdlineP->left = atoi(argv[1]+2);
+            break;
+        case 'r':
+            if (atoi(argv[1]+2) < 0)
+                pm_error("right border too small");
+            else
+                cmdlineP->right = atoi(argv[1]+2);
+            break;
+        case 'b':
+            if (atoi(argv[1]+2) < 0)
+                pm_error("bottom border too small");
+            else
+                cmdlineP->bottom = atoi(argv[1]+2);
+            break;
+        case 't':
+            if (atoi(argv[1]+2) < 0)
+                pm_error("top border too small");
+            else
+                cmdlineP->top = atoi(argv[1]+2);
+            break;
+        default:
+            pm_usage("[-white|-black] [-l#] [-r#] [-t#] [-b#] [pnmfile]");
+        }
+        argc--, argv++;
+    }
+
+    cmdlineP->xsizeSpec = (cmdlineP->xsize > 0);
+    cmdlineP->ysizeSpec = (cmdlineP->ysize > 0);
+
+    if (argc > 2)
+        pm_usage("[-white|-black] [-l#] [-r#] [-t#] [-b#] [pnmfile]");
+
+    if (argc == 2)
+        cmdlineP->input_filespec = argv[1];
+    else
+        cmdlineP->input_filespec = "-";
+}
+
+
+
+static void
+computeHorizontalPadSizes(struct cmdlineInfo const cmdline,
+                          int                const cols,
+                          unsigned int *     const lpadP,
+                          unsigned int *     const rpadP) {
+
+    if (cmdline.xsizeSpec) {
+        if (cmdline.leftSpec && cmdline.rightSpec) {
+            if (cmdline.left + cols + cmdline.right < cmdline.xsize) {
+                pm_error("Left padding (%u), and right "
+                         "padding (%u) are insufficient to bring the "
+                         "image width of %d up to %u.",
+                         cmdline.left, cmdline.right, cols, cmdline.xsize);
+            } else {
+                *lpadP = cmdline.left;
+                *rpadP = cmdline.right;
+            }
+        } 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 (cmdline.xsize > cols) {
+                *lpadP = ROUNDU((cmdline.xsize - cols) * cmdline.xalign);
+                *rpadP = cmdline.xsize - cols - *lpadP;
+            } else {
+                *lpadP = 0;
+                *rpadP = 0;
+            }
+        }
+    } else {
+        *lpadP = cmdline.leftSpec ? cmdline.left : 0;
+        *rpadP = cmdline.rightSpec ? cmdline.right : 0;
+    }
+}
+
+
+
+static void
+computeVerticalPadSizes(struct cmdlineInfo const cmdline,
+                        int                const rows,
+                        unsigned int *     const tpadP,
+                        unsigned int *     const bpadP) {
+
+    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;
+    }
+}
+
+
+
+static void
+computePadSizes(struct cmdlineInfo const cmdline,
+                int                const cols,
+                int                const rows,
+                unsigned int *     const lpadP,
+                unsigned int *     const rpadP,
+                unsigned int *     const tpadP,
+                unsigned int *     const bpadP) {
+
+    computeHorizontalPadSizes(cmdline, cols, lpadP, rpadP);
+
+    computeVerticalPadSizes(cmdline, rows, tpadP, bpadP);
+
+    if (cmdline.verbose)
+        pm_message("Padding: left: %u; right: %u; top: %u; bottom: %u",
+                   *lpadP, *rpadP, *tpadP, *bpadP);
+}
+
+
+
+int
+main(int argc, char ** argv) {
+
+    struct cmdlineInfo cmdline;
+    FILE *ifP;
+    xel *xelrow, *bgrow, background;
+    xelval maxval;
+    int rows, cols, newcols, row, col, format;
+    bool depr_cmd; /* use deprecated commandline interface */
+    unsigned int lpad, rpad, tpad, bpad;
+
+    pnm_init( &argc, argv );
+
+    /* detect deprecated options */
+    depr_cmd = FALSE;  /* initial assumption */
+    if (argc > 1 && argv[1][0] == '-') {
+        if (argv[1][1] == 't' || argv[1][1] == 'b'
+            || argv[1][1] == 'l' || argv[1][1] == 'r') {
+            if (argv[1][2] >= '0' && argv[1][2] <= '9')
+                depr_cmd = TRUE;
+        }
+    } 
+    if (argc > 2 && argv[2][0] == '-') {
+        if (argv[2][1] == 't' || argv[2][1] == 'b'
+            || argv[2][1] == 'l' || argv[2][1] == 'r') {
+            if (argv[2][2] >= '0' && argv[2][2] <= '9')
+                depr_cmd = TRUE;
+        }
+    }
+
+    if (depr_cmd) 
+        parseCommandLineOld(argc, argv, &cmdline);
+    else 
+        parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.input_filespec);
+
+    pnm_readpnminit(ifP, &cols, &rows, &maxval, &format);
+    if (cmdline.white)
+        background = pnm_whitexel(maxval, format);
+    else
+        background = pnm_blackxel(maxval, format);
+
+    if (cmdline.verbose) pm_message("image WxH = %dx%d", cols, rows);
+
+    computePadSizes(cmdline, cols, rows, &lpad, &rpad, &tpad, &bpad);
+
+    newcols = cols + lpad + rpad;
+    xelrow = pnm_allocrow(newcols);
+    bgrow = pnm_allocrow(newcols);
+
+    for (col = 0; col < newcols; col++)
+        xelrow[col] = bgrow[col] = background;
+
+    pnm_writepnminit(stdout, newcols, rows + tpad + bpad, maxval, format, 0);
+
+    for (row = 0; row < tpad; row++)
+        pnm_writepnmrow(stdout, bgrow, newcols, maxval, format, 0);
+
+    for (row = 0; row < rows; row++) {
+        pnm_readpnmrow(ifP, &xelrow[lpad], cols, maxval, format);
+        pnm_writepnmrow(stdout, xelrow, newcols, maxval, format, 0);
+    }
+
+    for (row = 0; row < bpad; row++)
+        pnm_writepnmrow(stdout, bgrow, newcols, maxval, format, 0);
+
+    pnm_freerow(xelrow);
+    pnm_freerow(bgrow);
+
+    pm_close(ifP);
+
+    return 0;
+}
diff --git a/editor/pnmpaste.c b/editor/pnmpaste.c
new file mode 100644
index 00000000..38b316c6
--- /dev/null
+++ b/editor/pnmpaste.c
@@ -0,0 +1,185 @@
+/* pnmpaste.c - paste a rectangle into a portable anymap
+**
+** Copyright (C) 1989 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.
+*/
+
+#include "pm_c_util.h"
+#include "pnm.h"
+
+int
+main( argc, argv )
+    int argc;
+    char* argv[];
+    {
+    FILE* ifp1;
+    FILE* ifp2;
+    register xel* xelrow1;
+    register xel* xelrow2;
+    register xel* x1P;
+    register xel* x2P;
+    xelval maxval1, maxval2, newmaxval;
+    int argn, rows1, cols1, format1, x, y;
+    int rows2, cols2, format2, newformat, row;
+    register int col;
+    char function;
+    const char* const usage = "[-replace|-or|-and|-xor] frompnmfile x y [intopnmfile]";
+
+    pnm_init( &argc, argv );
+
+    argn = 1;
+    function = 'r';
+
+    /* Check for flags. */
+    if ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' )
+	{
+	if ( pm_keymatch( argv[argn], "-replace", 2 ) )
+	    function = 'r';
+	else if ( pm_keymatch( argv[argn], "-or", 2 ) )
+	    function = 'o';
+	else if ( pm_keymatch( argv[argn], "-and", 2 ) )
+	    function = 'a';
+	else if ( pm_keymatch( argv[argn], "-xor", 2 ) )
+	    function = 'x';
+	else
+	    pm_usage( usage );
+	++argn;
+	}
+
+    if ( argn == argc )
+	pm_usage( usage );
+    ifp1 = pm_openr( argv[argn] );
+    ++argn;
+
+    if ( argn == argc )
+	pm_usage( usage );
+    if ( sscanf( argv[argn], "%d", &x ) != 1 )
+	pm_usage( usage );
+    ++argn;
+    if ( argn == argc )
+	pm_usage( usage );
+    if ( sscanf( argv[argn], "%d", &y ) != 1 )
+	pm_usage( usage );
+    ++argn;
+
+    if ( argn != argc )
+	{
+	ifp2 = pm_openr( argv[argn] );
+	++argn;
+	}
+    else
+	ifp2 = stdin;
+
+    if ( argn != argc )
+	pm_usage( usage );
+
+    pnm_readpnminit( ifp1, &cols1, &rows1, &maxval1, &format1 );
+    xelrow1 = pnm_allocrow(cols1);
+    pnm_readpnminit( ifp2, &cols2, &rows2, &maxval2, &format2 );
+    xelrow2 = pnm_allocrow(cols2);
+
+    if ( x <= -cols2 )
+	pm_error(
+	    "x is too negative -- the second anymap has only %d cols",
+	    cols2 );
+    else if ( x >= cols2 )
+	pm_error(
+	    "x is too large -- the second anymap has only %d cols",
+	    cols2 );
+    if ( y <= -rows2 )
+	pm_error(
+	    "y is too negative -- the second anymap has only %d rows",
+	    rows2 );
+    else if ( y >= rows2 )
+	pm_error(
+	    "y is too large -- the second anymap has only %d rows",
+	    rows2 );
+
+    if ( x < 0 )
+	x += cols2;
+    if ( y < 0 )
+	y += rows2;
+
+    if ( x + cols1 > cols2 )
+	pm_error( "x + width is too large by %d pixels", x + cols1 - cols2 );
+    if ( y + rows1 > rows2 )
+	pm_error( "y + height is too large by %d pixels", y + rows1 - rows2 );
+
+    newformat = MAX( PNM_FORMAT_TYPE(format1), PNM_FORMAT_TYPE(format2) );
+    newmaxval = MAX( maxval1, maxval2 );
+
+    if ( function != 'r' && newformat != PBM_TYPE )
+	pm_error( "no logical operations allowed for non-bitmaps" );
+
+    pnm_writepnminit( stdout, cols2, rows2, newmaxval, newformat, 0 );
+
+    for ( row = 0; row < rows2; ++row )
+	{
+	pnm_readpnmrow( ifp2, xelrow2, cols2, maxval2, format2 );
+	pnm_promoteformatrow( xelrow2, cols2, maxval2, format2,
+	    newmaxval, newformat );
+
+	if ( row >= y && row < y + rows1 )
+	    {
+	    pnm_readpnmrow( ifp1, xelrow1, cols1, maxval1, format1 );
+	    pnm_promoteformatrow( xelrow1, cols1, maxval1, format1,
+		newmaxval, newformat );
+	    for ( col = 0, x1P = xelrow1, x2P = &(xelrow2[x]);
+		  col < cols1; ++col, ++x1P, ++x2P )
+		{
+		register xelval b1, b2;
+
+		switch ( function )
+		    {
+		    case 'r':
+		    *x2P = *x1P;
+		    break;
+
+		    case 'o':
+		    b1 = PNM_GET1( *x1P );
+		    b2 = PNM_GET1( *x2P );
+		    if ( b1 != 0 || b2 != 0 )
+			PNM_ASSIGN1( *x2P, newmaxval );
+		    else
+			PNM_ASSIGN1( *x2P, 0 );
+		    break;
+
+		    case 'a':
+		    b1 = PNM_GET1( *x1P );
+		    b2 = PNM_GET1( *x2P );
+		    if ( b1 != 0 && b2 != 0 )
+			PNM_ASSIGN1( *x2P, newmaxval );
+		    else
+			PNM_ASSIGN1( *x2P, 0 );
+		    break;
+
+		    case 'x':
+		    b1 = PNM_GET1( *x1P );
+		    b2 = PNM_GET1( *x2P );
+		    if ( ( b1 != 0 && b2 == 0 ) || ( b1 == 0 && b2 != 0 ) )
+			PNM_ASSIGN1( *x2P, newmaxval );
+		    else
+			PNM_ASSIGN1( *x2P, 0 );
+		    break;
+
+		    default:
+		    pm_error( "can't happen" );
+		    }
+		}
+	    }
+
+	pnm_writepnmrow( stdout, xelrow2, cols2, newmaxval, newformat, 0 );
+	}
+    
+    pm_close( ifp1 );
+    pm_close( ifp2 );
+    pm_close( stdout );
+
+    exit( 0 );
+    }
diff --git a/editor/pnmquant b/editor/pnmquant
new file mode 100755
index 00000000..175f6906
--- /dev/null
+++ b/editor/pnmquant
@@ -0,0 +1,275 @@
+#!/usr/bin/perl -w
+
+##############################################################################
+#                         pnmquant 
+##############################################################################
+#  By Bryan Henderson, San Jose CA; December 2001.
+#
+#  Contributed to the public domain by its author.
+##############################################################################
+
+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
+
+my ($TRUE, $FALSE) = (1,0);
+
+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);
+}
+
+
+
+sub parseCommandLine(@) {
+
+    local @ARGV = @_;  # GetOptions takes input from @ARGV only
+
+    my %cmdline;
+
+    my $validOptions = GetOptions(\%cmdline,
+                                  "center",
+                                  "meancolor",
+                                  "meanpixel",
+                                  "spreadbrightness",
+                                  "spreadluminosity",
+                                  "floyd|fs!",
+                                  "quiet",
+                                  "plain");
+
+    if (!$validOptions) {
+        print(STDERR "Invalid option syntax.\n");
+        exit(1);
+    }
+    if (@ARGV > 2) {
+        print(STDERR "This program takes at most 2 arguments.  You specified ",
+              scalar(@ARGV), "\n");
+        exit(1);
+    } 
+    if (@ARGV < 1) {
+        print(STDERR 
+              "You must specify the number of colors as an argument.\n");
+        exit(1);
+    }
+    my $infile;
+    $cmdline{ncolors} = $ARGV[0];
+    
+    if (!($cmdline{ncolors} =~ m{ ^[[:digit:]]+$ }x ) || 
+        $cmdline{ncolors} == 0) {
+        print(STDERR 
+              "Number of colors argument '$cmdline{ncolors}' " .
+              "is not a positive integer.\n");
+        exit(1);
+    }
+
+    if (@ARGV > 1) {
+        $cmdline{infile} = $ARGV[1];
+    } else {
+        $cmdline{infile} = "-";
+    }
+
+    return(\%cmdline);
+}
+
+
+
+sub setAutoflush($) {
+    my ($fh) = @_;
+
+    # Better would be $fh->autoflush() (with use IO:Handle), but older Perl
+    # doesn't have it.
+
+    my $oldFh = select($fh);
+    $OUTPUT_AUTOFLUSH = $TRUE;
+    select ($oldFh);
+}
+
+
+
+sub openSeekableAsStdin($) {
+    my ($infile) = @_;
+
+    # Open the input file $infile and connect it to Standard Input
+    # (assuming Standard Input is now open as the Perl file handle STDIN).
+    # If $infile is "-", that means just leave Standard Input alone.  But if
+    # Standard Input is not seekable, copy it to a temporary regular
+    # file and return a file handle for that.  I.e. the file handle we
+    # return is guaranteed to be seekable.
+
+    # Note: this all works only because STDIN is already set up on file
+    # descriptor 0.  Otherwise, open(STDIN, ...) would just create a brand
+    # new file handle named STDIN on some arbitrary file descriptor.
+    
+    if ($infile eq "-") {
+        my $seekWorked = sysseek(STDIN, 0, $SEEK_SET);
+        if ($seekWorked) {
+            # STDIN is already as we need it.
+        } else {
+            # It isn't seekable, so we must copy it to a temporary regular file
+            # and return that as the input file.
+            
+            my ($inFh, $inFilename) = tempFile(".pnm");
+            if (!defined($inFh)) {
+                die("Unable to create temporary file.  Errno=$ERRNO");
+            }
+            unlink($inFilename) or
+                die("Unable to unlink temporary file.  Errno=$ERRNO");
+
+            setAutoflush($inFh);
+
+            while (<STDIN>) {
+                print($inFh $_);
+            }
+            sysseek($inFh, 0, $SEEK_SET) 
+                or die("Seek of temporary input file failed!  " .
+                       "Errno = $ERRNO");
+            *INFH = *$inFh;  # Because open() rejects '<&$inFh' 
+            open(STDIN, "<&INFH");
+            tell(INFH);  # Avoids bogus "INFH is not referenced" warning
+        }
+    } else {
+        open(STDIN, "<", $infile) 
+            or die("Unable to open input file '$infile'.  Errno=$ERRNO");
+    }
+}
+
+
+
+sub makeColormap($$$$$) {
+
+    my ($ncolors, $opt_meanpixel, $opt_meancolor, $opt_spreadluminosity,
+        $opt_quiet) = @_;
+
+    # Make a colormap of $ncolors colors from the image on Standard Input.
+    # Put it in a temporary file and return its name.
+
+    my ($mapfileFh, $mapfileSpec) = tempFile(".pnm");
+
+    if (!defined($mapfileFh)) {
+        print(STDERR "Unable to create temporary file for colormap.  " .
+              "errno = $ERRNO\n");
+        exit(1);
+    }
+
+    my $averageOpt;
+    if (defined($opt_meanpixel)) {
+        $averageOpt = "-meanpixel";
+    } elsif (defined($opt_meancolor)) {
+        $averageOpt = "-meancolor";
+    } else {
+        $averageOpt = "-center";
+    }
+
+    my $spreadOpt;
+    if (defined($opt_spreadluminosity)) {
+        $spreadOpt = "-spreadluminosity";
+    } else {
+        $spreadOpt = "-spreadbrightness";
+    }
+
+    my @options;
+    @options = ($averageOpt, $spreadOpt);
+    if (defined($opt_quiet)) {
+        push(@options, '-quiet');
+    }
+
+    open(STDOUT, ">", $mapfileSpec);
+
+    my $maprc = system("pnmcolormap", $ncolors, @options);
+
+    if ($maprc != 0) {
+        print(STDERR "pnmcolormap failed, rc=$maprc\n");
+        exit(1);
+    } 
+    return $mapfileSpec;
+}
+
+
+
+sub remap($$$$) {
+
+    my ($mapfileSpec, $opt_floyd, $opt_plain, $opt_quiet) = @_;
+
+    # Remap the image on Standard Input to Standard Output, using the colors
+    # from the colormap file named $mapfileSpec.
+
+    my @options;
+    @options = ();  # initial value
+
+    if ($opt_floyd) {
+        push(@options, "-floyd");
+    }
+    if ($opt_plain) {
+        push(@options, "-plain");
+    }
+    if ($opt_quiet) {
+        push(@options, "-quiet");
+    }
+
+    my $remaprc = system("pnmremap", "-mapfile=$mapfileSpec", @options);
+    
+    if ($remaprc != 0) {
+        print(STDERR "pnmremap failed, rc=$remaprc\n");
+        exit(1);
+    }
+}
+
+
+
+##############################################################################
+#                              MAIN PROGRAM
+##############################################################################
+
+my $cmdlineR = parseCommandLine(@ARGV);
+
+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 
+
+
+my $mapfileSpec = makeColormap($cmdlineR->{ncolors}, 
+                               $cmdlineR->{meanpixel}, 
+                               $cmdlineR->{meancolor}, 
+                               $cmdlineR->{spreadluminosity},
+                               $cmdlineR->{quiet});
+
+# Note that we use sysseek() instead of seek(), because we're positioning
+# the file to be read by our non-Perl child process, rather than for reading
+# through the Perl I/O layer.
+
+sysseek(STDIN, 0, $SEEK_SET) 
+    or die("seek back to zero on input file failed.");
+
+
+open(STDOUT, ">&OLDOUT");
+
+remap($mapfileSpec, 
+      $cmdlineR->{floyd}, 
+      $cmdlineR->{plain},
+      $cmdlineR->{quiet});
+
+unlink($mapfileSpec) or
+    die("Unable to unlink map file.  Errno=$ERRNO");
diff --git a/editor/pnmremap.c b/editor/pnmremap.c
new file mode 100644
index 00000000..2102fe63
--- /dev/null
+++ b/editor/pnmremap.c
@@ -0,0 +1,872 @@
+/******************************************************************************
+                               pnmremap.c
+*******************************************************************************
+
+  Replace colors in an input image with colors from a given colormap image.
+
+  For PGM input, do the equivalent.
+
+  By Bryan Henderson, San Jose, CA 2001.12.17
+
+  Derived from ppmquant, originally by Jef Poskanzer.
+
+  Copyright (C) 1989, 1991 by Jef Poskanzer.
+  Copyright (C) 2001 by Bryan Henderson.
+
+  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 <limits.h>
+#include <math.h>
+#include <assert.h>
+
+#include "pm_c_util.h"
+#include "mallocvar.h"
+#include "nstring.h"
+#include "shhopt.h"
+#include "pam.h"
+#include "pammap.h"
+
+#define MAXCOLORS 32767u
+
+enum missingMethod {
+    MISSING_FIRST,
+    MISSING_SPECIFIED,
+    MISSING_CLOSE
+};
+
+#define FS_SCALE 1024
+
+struct fserr {
+    long** thiserr;
+    long** nexterr;
+    bool fsForward;
+};
+
+
+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 * mapFilespec;    /* Filespec of colormap file */
+    unsigned int floyd;   /* Boolean: -floyd/-fs option */
+    enum missingMethod missingMethod;
+    char * missingcolor;      
+        /* -missingcolor value.  Null if not specified */
+    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 nofloyd, firstisdefault;
+    unsigned int missingSpec, mapfileSpec;
+
+    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,   "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 );
+
+    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 */
+
+    cmdlineP->missingcolor = NULL;  /* default value */
+    
+    optParseOptions3( &argc, argv, opt, sizeof(opt), 0 );
+        /* Uses and sets argc, argv, and some of *cmdline_p and others. */
+
+    if (cmdlineP->floyd && nofloyd)
+        pm_error("You cannot specify both -floyd and -nofloyd options.");
+
+    if (firstisdefault && missingSpec)
+        pm_error("You cannot specify both -missing and -firstisdefault.");
+
+    if (firstisdefault)
+        cmdlineP->missingMethod = MISSING_FIRST;
+    else if (missingSpec)
+        cmdlineP->missingMethod = MISSING_SPECIFIED;
+    else
+        cmdlineP->missingMethod = MISSING_CLOSE;
+
+    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 void
+rgbToDepth1(const struct pam * const pamP,
+            tuple *            const tupleRow) {
+    
+    unsigned int col;
+
+    for (col = 0; col < pamP->width; ++col) {
+        unsigned int plane;
+        double grayvalue;
+        grayvalue = 0.0;  /* initial value */
+        for (plane = 0; plane < pamP->depth; ++plane)
+            grayvalue += pnm_lumin_factor[plane] * tupleRow[col][plane];
+        tupleRow[col][0] = (sample) (grayvalue + 0.5);
+    }
+}
+
+
+
+static void
+grayscaleToDepth3(const struct pam * const pamP,
+                  tuple *            const tupleRow) {
+    
+    unsigned int col;
+
+    assert(pamP->allocation_depth >= 3);
+
+    for (col = 0; col < pamP->width; ++col) {
+        tupleRow[col][1] = tupleRow[col][0];
+        tupleRow[col][2] = tupleRow[col][0];
+    }
+}
+
+
+
+static void
+adjustDepth(const struct pam * const pamP,
+            tuple *            const tupleRow,
+            unsigned int       const newDepth) {
+/*----------------------------------------------------------------------------
+   Change the depth of the raster row tupleRow[] of the image
+   described by 'pamP' to newDepth.
+
+   We don't change the memory allocation; tupleRow[] must already have
+   space allocated for at least 'newDepth' planes.  When we're done,
+   all but the first 'newDepth' planes are meaningless, but the space is
+   still there.
+
+   The only depth changes we know how to do are:
+
+     - from tuple type RGB, depth 3 to depth 1 
+
+       We change it to grayscale or black and white.
+
+     - from tuple type GRAYSCALE or BLACKANDWHITE depth 1 to depth 3.
+
+       We change it to RGB.
+
+   For any other depth change request, we issue an error message and abort
+   the program.
+-----------------------------------------------------------------------------*/
+    if (newDepth != pamP->depth) {
+
+        if (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.  "
+                         "The only depth to which I know how to convert "
+                         "an RGB tuple is 1.",
+                         newDepth, pamP->depth);
+            } else
+                rgbToDepth1(pamP, tupleRow);
+        } else if (stripeq(pamP->tuple_type, "GRAYSCALE") ||
+                   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 "
+                         "or BLACKANDWHITE.  "
+                         "The only depth to which I know how to convert "
+                         "a GRAYSCALE or BLACKANDWHITE tuple is 3.",
+                         newDepth, pamP->depth);
+            } else
+                grayscaleToDepth3(pamP, tupleRow);
+        } else {
+            pm_error("Map image depth of %u differs from input image depth "
+                     "of %u, and the input image does not have a tuple type "
+                     "that I know how to convert to the map depth.  "
+                     "I can convert RGB, GRAYSCALE, and BLACKANDWHITE.  "
+                     "The input image is '%.*s'.",
+                     newDepth, pamP->depth, 
+                     (int)sizeof(pamP->tuple_type), pamP->tuple_type);
+        }
+    }
+}
+
+
+
+
+static void
+computeColorMapFromMap(struct pam *   const mappamP, 
+                       tuple **       const maptuples, 
+                       tupletable *   const colormapP,
+                       unsigned int * const newcolorsP) {
+/*----------------------------------------------------------------------------
+   Produce a colormap containing the colors that we will use in the output.
+
+   Make it include exactly those colors that are in the image
+   described by *mappamP and maptuples[][].
+
+   Return the number of colors in the returned colormap as *newcolorsP.
+-----------------------------------------------------------------------------*/
+    unsigned int colors; 
+
+    if (mappamP->width == 0 || mappamP->height == 0)
+        pm_error("colormap file contains no pixels");
+
+    *colormapP = 
+        pnm_computetuplefreqtable(mappamP, maptuples, MAXCOLORS, &colors);
+    if (*colormapP == NULL)
+        pm_error("too many colors in colormap!");
+    pm_message("%d colors found in colormap", colors);
+    *newcolorsP = colors;
+}
+
+
+
+static void
+initFserr(struct pam *   const pamP,
+          struct fserr * const fserrP) {
+/*----------------------------------------------------------------------------
+   Initialize the Floyd-Steinberg error vectors
+-----------------------------------------------------------------------------*/
+    unsigned int plane;
+
+    unsigned int const fserrSize = pamP->width + 2;
+
+    MALLOCARRAY(fserrP->thiserr, pamP->depth);
+    if (fserrP->thiserr == NULL)
+        pm_error("Out of memory allocating Floyd-Steinberg structures "
+                 "for depth %u", pamP->depth);
+    MALLOCARRAY(fserrP->nexterr, pamP->depth);
+    if (fserrP->nexterr == NULL)
+        pm_error("Out of memory allocating Floyd-Steinberg structures "
+                 "for depth %u", pamP->depth);
+    
+    for (plane = 0; plane < pamP->depth; ++plane) {
+        MALLOCARRAY(fserrP->thiserr[plane], fserrSize);
+        if (fserrP->thiserr[plane] == NULL)
+            pm_error("Out of memory allocating Floyd-Steinberg structures "
+                     "for Plane %u, size %u", plane, fserrSize);
+        MALLOCARRAY(fserrP->nexterr[plane], fserrSize);
+        if (fserrP->nexterr[plane] == NULL)
+            pm_error("Out of memory allocating Floyd-Steinberg structures "
+                     "for Plane %u, size %u", plane, fserrSize);
+    }
+
+    srand((int)(time(0) ^ getpid()));
+
+    {
+        int col;
+
+        for (col = 0; col < fserrSize; ++col) {
+            unsigned int plane;
+            for (plane = 0; plane < pamP->depth; ++plane) 
+                fserrP->thiserr[plane][col] = 
+                    rand() % (FS_SCALE * 2) - FS_SCALE;
+                    /* (random errors in [-1 .. 1]) */
+        }
+    }
+    fserrP->fsForward = TRUE;
+}
+
+
+
+static void
+floydInitRow(struct pam * const pamP, struct fserr * const fserrP) {
+
+    int col;
+    
+    for (col = 0; col < pamP->width + 2; ++col) {
+        unsigned int plane;
+        for (plane = 0; plane < pamP->depth; ++plane) 
+            fserrP->nexterr[plane][col] = 0;
+    }
+}
+
+
+
+static void
+floydAdjustColor(struct pam *   const pamP, 
+                 tuple          const tuple, 
+                 struct fserr * const fserrP, 
+                 int            const col) {
+/*----------------------------------------------------------------------------
+  Use Floyd-Steinberg errors to adjust actual color.
+-----------------------------------------------------------------------------*/
+    unsigned int plane;
+
+    for (plane = 0; plane < pamP->depth; ++plane) {
+        long int const s =
+            tuple[plane] + fserrP->thiserr[plane][col+1] / FS_SCALE;
+        tuple[plane] = MIN(pamP->maxval, MAX(0,s));
+    }
+}
+
+
+
+static void
+floydPropagateErr(struct pam *   const pamP, 
+                  struct fserr * const fserrP, 
+                  int            const col, 
+                  tuple          const oldtuple, 
+                  tuple          const newtuple) {
+/*----------------------------------------------------------------------------
+  Propagate Floyd-Steinberg error terms.
+
+  The error is due to substituting the tuple value 'newtuple' for the
+  tuple value 'oldtuple' (both described by *pamP).  The error terms
+  are meant to be used to introduce a compensating error into the
+  future selection of tuples nearby in the image.
+-----------------------------------------------------------------------------*/
+    unsigned int plane;
+    for (plane = 0; plane < pamP->depth; ++plane) {
+        long const newSample = newtuple[plane];
+        long const oldSample = oldtuple[plane];
+        long const err = (oldSample - newSample) * FS_SCALE;
+            
+        if (fserrP->fsForward) {
+            fserrP->thiserr[plane][col + 2] += ( err * 7 ) / 16;
+            fserrP->nexterr[plane][col    ] += ( err * 3 ) / 16;
+            fserrP->nexterr[plane][col + 1] += ( err * 5 ) / 16;
+            fserrP->nexterr[plane][col + 2] += ( err     ) / 16;
+        } else {
+            fserrP->thiserr[plane][col    ] += ( err * 7 ) / 16;
+            fserrP->nexterr[plane][col + 2] += ( err * 3 ) / 16;
+            fserrP->nexterr[plane][col + 1] += ( err * 5 ) / 16;
+            fserrP->nexterr[plane][col    ] += ( err     ) / 16;
+        }
+    }
+}
+
+
+
+static void
+floydSwitchDir(struct pam * const pamP, struct fserr * const fserrP) {
+
+    unsigned int plane;
+
+    for (plane = 0; plane < pamP->depth; ++plane) {
+        long * const temperr = fserrP->thiserr[plane];
+        fserrP->thiserr[plane] = fserrP->nexterr[plane];
+        fserrP->nexterr[plane] = temperr;
+    }
+    fserrP->fsForward = ! fserrP->fsForward;
+}
+
+
+
+struct colormapFinder {
+/*----------------------------------------------------------------------------
+   This is an object that finds a color in a colormap.  The methods
+   'searchColormapClose' and 'searchColormapExact' belong to it.
+
+   This object ought to encompass the hash table as well some day and
+   possibly encapsulate the color map altogether and just be an object
+   that opaquely maps input colors to output colors.
+-----------------------------------------------------------------------------*/
+    tupletable colormap;
+    unsigned int colors;
+        /* Number of colors in 'colormap'.  At least 1 */
+    unsigned int distanceDivider;
+        /* The value by which our intermediate distance calculations
+           have to be divided to make sure we don't overflow our
+           unsigned int data structure.
+           
+           To the extent 'distanceDivider' is greater than 1, closest
+           color results will be approximate -- there could
+           conceivably be a closer one that we miss.
+        */
+};
+
+
+
+static void
+createColormapFinder(struct pam *             const pamP,
+                     tupletable               const colormap,
+                     unsigned int             const colors,
+                     struct colormapFinder ** const colormapFinderPP) {
+
+    struct colormapFinder * colormapFinderP;
+
+    MALLOCVAR_NOFAIL(colormapFinderP);
+
+    colormapFinderP->colormap = colormap;
+    colormapFinderP->colors = colors;
+
+    {
+        unsigned int const maxHandleableSqrDiff = 
+            (unsigned int)UINT_MAX / pamP->depth;
+        
+        if (SQR(pamP->maxval) > maxHandleableSqrDiff)
+            colormapFinderP->distanceDivider = (unsigned int)
+                (SQR(pamP->maxval) / maxHandleableSqrDiff + 0.1 + 1.0);
+                /* The 0.1 is a fudge factor to keep us out of rounding 
+                   trouble.  The 1.0 effects a round-up.
+                */
+        else
+            colormapFinderP->distanceDivider = 1;
+    }
+    *colormapFinderPP = colormapFinderP;
+}
+
+
+
+static void
+destroyColormapFinder(struct colormapFinder * const colormapFinderP) {
+
+    free(colormapFinderP);
+}
+
+
+
+static void
+searchColormapClose(struct pam *            const pamP,
+                    tuple                   const tuple,
+                    struct colormapFinder * const colorFinderP,
+                    int *                   const colormapIndexP) {
+/*----------------------------------------------------------------------------
+   Search the colormap indicated by *colorFinderP for the color closest to
+   that of tuple 'tuple'.  Return its index as *colormapIndexP.
+
+   *pamP describes the tuple 'tuple' and *colorFinderP has to be
+   compatible with it (i.e. the tuples in the color map must also be
+   described by *pamP).
+
+   We compute distance between colors simply as the cartesian distance
+   between them in the RGB space.  An alternative would be to look at
+   the chromaticities and luminosities of the colors.  In experiments
+   in 2003, we found that this was actually worse in many cases.  One
+   might think that two colors are closer if they have similar hues
+   than when they are simply different brightnesses of the same hue.
+   Human subjects asked to compare two colors normally say so.  But
+   when replacing the color of a pixel in an image, the luminosity is
+   much more important, because you need to retain the luminosity
+   relationship between adjacent pixels.  If you replace a pixel with
+   one that has the same chromaticity as the original, but much
+   darker, it may stand out among its neighbors in a way the original
+   pixel did not.  In fact, on an image with blurred edges, we saw
+   ugly effects at the edges when we substituted colors using a
+   chromaticity-first color closeness formula.
+-----------------------------------------------------------------------------*/
+    unsigned int i;
+    unsigned int dist;
+        /* The closest distance we've found so far between the value of
+           tuple 'tuple' and a tuple in the colormap.  This is measured as
+           the square of the cartesian distance between the tuples, except
+           that it's divided by 'distanceDivider' to make sure it will fit
+           in an unsigned int.
+        */
+
+    dist = UINT_MAX;  /* initial value */
+
+    assert(colorFinderP->colors > 0);
+
+    for (i = 0; i < colorFinderP->colors; ++i) {
+        unsigned int newdist;  /* candidate for new 'dist' value */
+        unsigned int plane;
+
+        newdist = 0;
+
+        for (plane=0; plane < pamP->depth; ++plane) {
+            newdist += 
+                SQR(tuple[plane] - colorFinderP->colormap[i]->tuple[plane]) 
+                / colorFinderP->distanceDivider;
+        }
+        if (newdist < dist) {
+            *colormapIndexP = i;
+            dist = newdist;
+        }
+    }
+}
+
+
+
+static void
+searchColormapExact(struct pam *            const pamP,
+                    struct colormapFinder * const colorFinderP,
+                    tuple                   const tuple,
+                    int *                   const colormapIndexP,
+                    bool *                  const foundP) {
+/*----------------------------------------------------------------------------
+   Search the colormap indicated by *colorFinderP for the color of
+   tuple 'tuple'.  If it's in the map, return its index as
+   *colormapIndexP and return *foundP == TRUE.  Otherwise, return
+   *foundP = FALSE.
+
+   *pamP describes the tuple 'tuple' and *colorFinderP has to be
+   compatible with it (i.e. the tuples in the color map must also be
+   described by *pamP).
+-----------------------------------------------------------------------------*/
+    unsigned int i;
+    bool found;
+    
+    found = FALSE;  /* initial value */
+    for (i = 0; i < colorFinderP->colors && !found; ++i) {
+        unsigned int plane;
+        found = TRUE;  /* initial assumption */
+        for (plane=0; plane < pamP->depth; ++plane) 
+            if (tuple[plane] != colorFinderP->colormap[i]->tuple[plane]) 
+                found = FALSE;
+        if (found) 
+            *colormapIndexP = i;
+    }
+    *foundP = found;
+}
+
+
+
+static void
+lookupThroughHash(struct pam *            const pamP, 
+                  tuple                   const tuple, 
+                  bool                    const needExactMatch,
+                  struct colormapFinder * const colorFinderP,
+                  tuplehash               const colorhash,       
+                  int *                   const colormapIndexP,
+                  bool *                  const usehashP) {
+/*----------------------------------------------------------------------------
+   Look up the color of tuple 'tuple' in the color map indicated by
+   'colorFinderP' and, if it's in there, return its index as
+   *colormapIndexP.  If not, return *colormapIndexP == -1.
+
+   Both the tuple 'tuple' and the colors in color map 'colormap' are
+   described by *pamP.
+
+   If 'needExactMatch' isn't true, we find the closest color in the color map,
+   and never return *colormapIndex == -1.
+
+   lookaside at the hash table 'colorhash' to possibly avoid the cost of
+   a full lookup.  If we do a full lookup, we add the result to 'colorhash'
+   unless *usehashP is false, and if that makes 'colorhash' full, we set
+   *usehashP false.
+-----------------------------------------------------------------------------*/
+    int found;
+
+    /* Check hash table to see if we have already matched this color. */
+    pnm_lookuptuple(pamP, colorhash, tuple, &found, colormapIndexP);
+    if (!found) {
+        /* No, have to do a full lookup */
+        if (needExactMatch) {
+            bool found;
+
+            searchColormapExact(pamP, colorFinderP, tuple,
+                                colormapIndexP, &found);
+            if (!found)
+                *colormapIndexP = -1;
+        } else 
+            searchColormapClose(pamP, tuple, colorFinderP, colormapIndexP);
+        if (*usehashP) {
+            bool fits;
+            pnm_addtotuplehash(pamP, colorhash, tuple, *colormapIndexP, 
+                               &fits);
+            if (!fits) {
+                pm_message("out of memory adding to hash table; "
+                           "proceeding without it");
+                *usehashP = FALSE;
+            }
+        }
+    }
+}
+
+
+
+static void
+convertRow(struct pam *            const pamP, 
+           tuple                         tuplerow[],
+           tupletable              const colormap,
+           struct colormapFinder * const colorFinderP,
+           tuplehash               const colorhash, 
+           bool *                  const usehashP,
+           bool                    const floyd, 
+           enum missingMethod      const missingMethod, 
+           tuple                   const defaultColor,
+           struct fserr *          const fserrP,
+           unsigned int *          const missingCountP) {
+/*----------------------------------------------------------------------------
+  Replace the colors in row tuplerow[] (described by *pamP) with the
+  new colors.
+
+  Use and update the Floyd-Steinberg state *fserrP.
+
+  Return the number of pixels that were not matched in the color map as
+  *missingCountP.
+
+  *colorFinderP is a color finder based on 'colormap' -- it tells us what
+  index of 'colormap' corresponds to a certain color.
+-----------------------------------------------------------------------------*/
+    int col;
+    int limitcol;
+        /* The column at which to stop processing the row.  If we're scanning
+           forwards, this is the rightmost column.  If we're scanning 
+           backward, this is the leftmost column.
+        */
+    
+    if (floyd)
+        floydInitRow(pamP, fserrP);
+
+    *missingCountP = 0;  /* initial value */
+    
+    if ((!floyd) || fserrP->fsForward) {
+        col = 0;
+        limitcol = pamP->width;
+    } else {
+        col = pamP->width - 1;
+        limitcol = -1;
+    }
+    do {
+        int colormapIndex;
+            /* Index into the colormap of the replacement color, or -1 if
+               there is no usable color in the color map.
+            */
+
+        if (floyd) 
+            floydAdjustColor(pamP, tuplerow[col], fserrP, col);
+
+        lookupThroughHash(pamP, tuplerow[col], 
+                          missingMethod != MISSING_CLOSE, colorFinderP, 
+                          colorhash, &colormapIndex, usehashP);
+        if (floyd)
+            floydPropagateErr(pamP, fserrP, col, tuplerow[col], 
+                              colormap[colormapIndex]->tuple);
+
+        if (colormapIndex == -1) {
+            ++*missingCountP;
+            switch (missingMethod) {
+            case MISSING_SPECIFIED:
+                pnm_assigntuple(pamP, tuplerow[col], defaultColor);
+                break;
+            case MISSING_FIRST:
+                pnm_assigntuple(pamP, tuplerow[col], colormap[0]->tuple);
+                break;
+            default:
+                pm_error("Internal error: invalid value of missingMethod");
+            }
+        } else 
+            pnm_assigntuple(pamP, tuplerow[col], 
+                            colormap[colormapIndex]->tuple);
+
+        if (floyd && !fserrP->fsForward) 
+            --col;
+        else
+            ++col;
+    } while (col != limitcol);
+
+    if (floyd)
+        floydSwitchDir(pamP, fserrP);
+}
+
+
+
+static void
+copyRaster(struct pam *   const inpamP, 
+           struct pam *   const outpamP,
+           tupletable     const colormap, 
+           unsigned int   const colormapSize,
+           bool           const floyd, 
+           enum missingMethod const missingMethod,
+           tuple          const defaultColor, 
+           unsigned int * const missingCountP) {
+
+    tuplehash const colorhash = pnm_createtuplehash();
+    struct colormapFinder * colorFinderP;
+    bool usehash;
+    struct fserr fserr;
+    tuple * tuplerow = pnm_allocpamrow(inpamP);
+    int row;
+
+    if (outpamP->maxval != inpamP->maxval && missingMethod != MISSING_CLOSE)
+        pm_error("The maxval of the colormap (%u) is not equal to the "
+                 "maxval of the input image (%u).  This is allowable only "
+                 "if you are doing an approximate mapping (i.e. you don't "
+                 "specify -firstisdefault or -missingcolor)",
+                 (unsigned int)outpamP->maxval, (unsigned int)inpamP->maxval);
+
+    usehash = TRUE;
+
+    createColormapFinder(outpamP, colormap, colormapSize, &colorFinderP);
+
+    if (floyd)
+        initFserr(inpamP, &fserr);
+
+    *missingCountP = 0;  /* initial value */
+
+    for (row = 0; row < inpamP->height; ++row) {
+        unsigned int missingCount;
+
+        pnm_readpamrow(inpamP, tuplerow);
+
+        /* The following modify tuplerow, to make it consistent with
+           *outpamP instead of *inpamP.
+        */
+        adjustDepth(inpamP, tuplerow, outpamP->depth); 
+        pnm_scaletuplerow(inpamP, tuplerow, tuplerow, outpamP->maxval);
+
+        /* The following both consults and adds to 'colorhash' and
+           its associated 'usehash'.  It modifies 'tuplerow' too.
+        */
+        convertRow(outpamP, tuplerow, colormap, colorFinderP, colorhash, 
+                   &usehash,
+                   floyd, missingMethod, defaultColor, &fserr, &missingCount);
+        
+        *missingCountP += missingCount;
+        
+        pnm_writepamrow(outpamP, tuplerow);
+    }
+    destroyColormapFinder(colorFinderP);
+    pnm_freepamrow(tuplerow);
+    pnm_destroytuplehash(colorhash);
+}
+
+
+
+
+static void
+remap(FILE * const ifP,
+      const struct pam * const outpamCommonP,
+      tupletable         const colormap, 
+      unsigned int       const colormapSize,
+      bool               const floyd,
+      enum missingMethod const missingMethod,
+      tuple              const defaultColor,
+      bool               const verbose) {
+
+    bool eof;
+    eof = FALSE;
+    while (!eof) {
+        struct pam inpam, outpam;
+        unsigned int missingCount;
+            /* Number of pixels that were not matched in the color map (where
+               missingMethod is MISSING_CLOSE, this is always zero).
+            */
+
+        pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(allocation_depth));
+    
+        outpam = *outpamCommonP;
+        outpam.width  = inpam.width;
+        outpam.height = inpam.height;
+
+        pnm_writepaminit(&outpam);
+
+        /* Set up so input buffers have extra space as needed to
+           convert the input to the output depth.
+        */
+        pnm_setminallocationdepth(&inpam, outpam.depth);
+    
+        copyRaster(&inpam, &outpam, colormap, colormapSize, floyd,
+                   missingMethod, defaultColor, &missingCount);
+        
+        if (verbose)
+            pm_message("%d pixels not matched in color map", missingCount);
+        
+        pnm_nextimage(ifP, &eof);
+    }
+}
+
+
+
+
+int
+main(int argc, char * argv[] ) {
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    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.
+        */
+    tupletable colormap;
+    unsigned int colormapSize;
+    tuple defaultColor;
+        /* A tuple of the color that should replace any input color that is 
+           not in the colormap, if we're doing MISSING_SPECIFIED.
+        */
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilespec);
+    {
+        FILE * mapfile;
+        struct pam mappam;
+        tuple ** maptuples;
+
+        mapfile = pm_openr(cmdline.mapFilespec);
+        maptuples = pnm_readpam(mapfile, &mappam, PAM_STRUCT_SIZE(tuple_type));
+        pm_close(mapfile);
+
+        computeColorMapFromMap(&mappam, maptuples, &colormap, &colormapSize);
+        pnm_freepamarray(maptuples, &mappam);
+
+        outpamCommon = mappam; 
+        outpamCommon.file = stdout;
+    }
+
+    defaultColor = pnm_allocpamtuple(&outpamCommon);
+    if (cmdline.missingcolor && outpamCommon.depth == 3) {
+        pixel const color = 
+            ppm_parsecolor(cmdline.missingcolor, outpamCommon.maxval);
+        defaultColor[PAM_RED_PLANE] = PPM_GETR(color);
+        defaultColor[PAM_GRN_PLANE] = PPM_GETG(color);
+        defaultColor[PAM_BLU_PLANE] = PPM_GETB(color);
+    }
+
+    remap(ifP, &outpamCommon, colormap, colormapSize, 
+          cmdline.floyd, cmdline.missingMethod, defaultColor,
+          cmdline.verbose);
+
+    pnm_freepamtuple(defaultColor);
+
+    pm_close(stdout);
+
+    pm_close(ifP);
+
+    return 0;
+}
diff --git a/editor/pnmrotate.c b/editor/pnmrotate.c
new file mode 100644
index 00000000..64c69e2a
--- /dev/null
+++ b/editor/pnmrotate.c
@@ -0,0 +1,808 @@
+/* pnmrotate.c - read a portable anymap and rotate it by some angle
+**
+** 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   /* get M_PI in math.h */
+
+#include <math.h>
+#include <assert.h>
+
+#include "pnm.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+#define SCALE 4096
+#define HALFSCALE 2048
+
+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 file */
+    float angle;                /* Angle to rotate, in radians */
+    unsigned int noantialias;
+    const char * background;  /* NULL if none */
+    unsigned int keeptemp;  /* For debugging */
+    unsigned int verbose;
+};
+
+
+enum rotationDirection {CLOCKWISE, COUNTERCLOCKWISE};
+
+struct shearParm {
+    /* These numbers tell how to shear a pixel, but I haven't figured out 
+       yet exactly what each means.
+    */
+    long fracnew0;
+    long omfracnew0;
+    unsigned int shiftWhole;
+    unsigned int shiftUnits;
+};
+
+
+
+static void
+parseCommandLine(int argc, 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 = malloc(100*sizeof(optEntry));
+        /* Instructions to OptParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int backgroundSpec;
+    unsigned int option_def_index;
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    OPTENT3(0, "background",  OPT_STRING, &cmdlineP->background, 
+            &backgroundSpec,        0);
+    OPTENT3(0, "noantialias", OPT_FLAG,   NULL, 
+            &cmdlineP->noantialias, 0);
+    OPTENT3(0, "keeptemp",    OPT_FLAG,   NULL, 
+            &cmdlineP->keeptemp,    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 = TRUE;  /* We may have parms that are negative numbers */
+
+    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (!backgroundSpec)
+        cmdlineP->background = NULL;
+
+    if (argc-1 < 1)
+        pm_error("You must specify at least one argument:  the angle "
+                 "to rotate.");
+    else {
+        int rc;
+        float angleArg;
+
+        rc = sscanf(argv[1], "%f", &angleArg);
+
+        if (rc != 1)
+            pm_error("Invalid angle argument: '%s'.  Must be a floating point "
+                     "number of degrees.", argv[1]);
+        else if (angleArg < -90.0 || angleArg > 90.0)
+            pm_error("angle must be between -90 and 90, inclusive.  "
+                     "You specified %f.  "
+                     "Use 'pamflip' for other rotations.", angleArg);
+        else {
+            /* Convert to radians */
+            cmdlineP->angle = angleArg * M_PI / 180.0;
+
+            if (argc-1 < 2)
+                cmdlineP->inputFilespec = "-";
+            else {
+                cmdlineP->inputFilespec = argv[2];
+                
+                if (argc-1 > 2)
+                    pm_error("Program takes at most two arguments "
+                             "(angle and filename).  You specified %d",
+                             argc-1);
+            }
+        }
+    }
+}
+
+
+
+static void
+storeImage(const char * const fileName,
+           xel **       const xels,
+           unsigned int const cols,
+           unsigned int const rows,
+           xelval       const maxval,
+           int          const format) {
+
+    FILE * ofP;
+
+    ofP = pm_openw(fileName);
+
+    pnm_writepnm(ofP, xels, cols, rows, maxval, format, 0);
+
+    pm_close(ofP);
+}
+
+  
+
+static void
+computeNewFormat(bool     const antialias, 
+                 int      const format,
+                 xelval   const maxval,
+                 int *    const newformatP,
+                 xelval * const newmaxvalP) {
+
+    if (antialias && PNM_FORMAT_TYPE(format) == PBM_TYPE) {
+        *newformatP = PGM_TYPE;
+        *newmaxvalP = PGM_MAXMAXVAL;
+        pm_message("promoting from PBM to PGM - "
+                   "use -noantialias to avoid this");
+    } else {
+        *newformatP = format;
+        *newmaxvalP = maxval;
+    }
+}
+
+
+
+static xel
+backgroundColor(const char * const backgroundColorName,
+                xel *        const topRow,
+                int          const cols,
+                xelval       const maxval,
+                int          const format) {
+
+    xel retval;
+
+    if (backgroundColorName) {
+        retval = ppm_parsecolor(backgroundColorName, maxval);
+
+        switch(PNM_FORMAT_TYPE(format)) {
+        case PGM_TYPE:
+            if (!PPM_ISGRAY(retval))
+                pm_error("Image is PGM (grayscale), "
+                         "but you specified a non-gray "
+                         "background color '%s'", backgroundColorName);
+
+            break;
+        case PBM_TYPE:
+            if (!PNM_EQUAL(retval, pnm_whitexel(maxval, format)) &&
+                !PNM_EQUAL(retval, pnm_blackxel(maxval, format)))
+                pm_error("Image is PBM (black and white), "
+                         "but you specified '%s', which is neither black "
+                         "nor white, as background color", 
+                         backgroundColorName);
+            break;
+        }
+    } else 
+        retval = pnm_backgroundxelrow(topRow, cols, maxval, format);
+
+    return retval;
+}
+
+
+
+static void
+reportBackground(xel const bgColor) {
+
+    pm_message("Background color %u/%u/%u",
+               PPM_GETR(bgColor), PPM_GETG(bgColor), PPM_GETB(bgColor));
+}
+
+
+
+static void
+shearX(xel * const inRow, 
+       xel * const outRow, 
+       int   const cols, 
+       int   const format,
+       xel   const bgxel,
+       bool  const antialias,
+       float const shiftAmount,
+       int   const newcols) {
+/*----------------------------------------------------------------------------
+   Shift a the row inRow[] right by 'shiftAmount' pixels and return the
+   result as outRow[].
+
+   The input row is 'cols' columns wide, whereas the output row is
+   'newcols'.
+
+   The format of the input row is 'format'.
+
+   We shift the row on a background of color 'bgxel'.
+
+   The output row has the same format and maxval as the input.
+   
+   'shiftAmount' may not be negative.
+   
+   'shiftAmount' can be fractional, so we either just go by the
+   nearest integer value or mix pixels to achieve the shift, depending
+   on 'antialias'.
+-----------------------------------------------------------------------------*/
+    assert(shiftAmount >= 0.0);
+
+    if (antialias) {
+        unsigned int const shiftWhole = (unsigned int) shiftAmount;
+        long const fracShift = (shiftAmount - shiftWhole) * SCALE;
+        long const omfracShift = SCALE - fracShift;
+
+        unsigned int col;
+        xel * nxP;
+        xel prevxel;
+
+        for (col = 0; col < newcols; ++col)
+            outRow[col] = bgxel;
+            
+        prevxel = bgxel;
+        for (col = 0, nxP = &(outRow[shiftWhole]);
+             col < cols; ++col, ++nxP) {
+
+            xel const p = inRow[col];
+
+            switch (PNM_FORMAT_TYPE(format)) {
+            case PPM_TYPE:
+                PPM_ASSIGN(*nxP,
+                           (fracShift * PPM_GETR(prevxel) 
+                            + omfracShift * PPM_GETR(p) 
+                            + HALFSCALE) / SCALE,
+                           (fracShift * PPM_GETG(prevxel) 
+                            + omfracShift * PPM_GETG(p) 
+                            + HALFSCALE) / SCALE,
+                           (fracShift * PPM_GETB(prevxel) 
+                            + omfracShift * PPM_GETB(p) 
+                            + HALFSCALE) / SCALE );
+                break;
+                
+            default:
+                PNM_ASSIGN1(*nxP,
+                            (fracShift * PNM_GET1(prevxel) 
+                             + omfracShift * PNM_GET1(p) 
+                             + HALFSCALE) / SCALE );
+                break;
+            }
+            prevxel = p;
+        }
+        if (fracShift> 0 && shiftWhole + cols < newcols) {
+            switch (PNM_FORMAT_TYPE(format)) {
+            case PPM_TYPE:
+                PPM_ASSIGN(*nxP,
+                           (fracShift * PPM_GETR(prevxel) 
+                            + omfracShift * PPM_GETR(bgxel) 
+                            + HALFSCALE) / SCALE,
+                           (fracShift * PPM_GETG(prevxel) 
+                            + omfracShift * PPM_GETG(bgxel) 
+                            + HALFSCALE) / SCALE,
+                           (fracShift * PPM_GETB(prevxel) 
+                            + omfracShift * PPM_GETB(bgxel) 
+                            + HALFSCALE) / SCALE );
+                break;
+                    
+            default:
+                PNM_ASSIGN1(*nxP,
+                            (fracShift * PNM_GET1(prevxel) 
+                             + omfracShift * PNM_GET1(bgxel) 
+                             + HALFSCALE) / SCALE );
+                break;
+            }
+        }
+    } else {
+        unsigned int const shiftCols = (unsigned int) (shiftAmount + 0.5);
+        unsigned int col;
+        unsigned int outcol;
+
+        outcol = 0;  /* initial value */
+        
+        for (col = 0; col < shiftCols; ++col)
+            outRow[outcol++] = bgxel;
+        for (col = 0; col < cols; ++col)
+            outRow[outcol++] = inRow[col];
+        for (col = shiftCols + cols; col < newcols; ++col)
+            outRow[outcol++] = bgxel;
+        
+        assert(outcol == newcols);
+    }
+}
+
+
+
+static void
+shearXFromInputFile(FILE *                 const ifP,
+                    unsigned int           const cols,
+                    unsigned int           const rows,
+                    xelval                 const maxval,
+                    int                    const format,
+                    enum rotationDirection const direction,
+                    float                  const xshearfac,
+                    xelval                 const newmaxval,
+                    int                    const newformat,
+                    bool                   const antialias,
+                    const char *           const background,
+                    xel ***                const shearedXelsP,
+                    unsigned int *         const newcolsP,
+                    xel *                  const bgColorP) {
+/*----------------------------------------------------------------------------
+   Shear X from input file into newly malloced xel array.  Return that
+   array as *shearedColsP, and its width as *tempColsP.  Everything else
+   about the sheared image is the same as for the input image.
+
+   The input image on file 'ifP' is described by 'cols', 'rows',
+   'maxval', and 'format'.
+
+   Along the way, figure out what the background color of the output should
+   be based on the contents of the file and the user's directive
+   'background' and return that as *bgColorP.
+-----------------------------------------------------------------------------*/
+    unsigned int const maxShear = (rows - 0.5) * xshearfac + 0.5;
+    unsigned int const newcols = cols + maxShear;
+    
+    xel ** shearedXels;
+    xel * xelrow;
+    xel bgColor;
+    unsigned int row;
+
+    shearedXels = pnm_allocarray(newcols, rows);
+
+    xelrow = pnm_allocrow(cols);
+
+    for (row = 0; row < rows; ++row) {
+        /* The shear factor is designed to shear over the entire width
+           from the left edge of of the left pixel to the right edge of
+           the right pixel.  We use the distance of the center of this
+           pixel from the relevant edge to compute shift amount:
+        */
+        float const xDistance = 
+            (direction == COUNTERCLOCKWISE ? row + 0.5 : (rows-0.5 - row));
+        float const shiftAmount = xshearfac * xDistance;
+
+        pnm_readpnmrow(ifP, xelrow, cols, maxval, format);
+
+        pnm_promoteformatrow(xelrow, cols, maxval, format, 
+                             newmaxval, newformat);
+
+        if (row == 0)
+            bgColor =
+                backgroundColor(background, xelrow, cols, newmaxval, format);
+
+        shearX(xelrow, shearedXels[row], cols, newformat, bgColor,
+               antialias, shiftAmount, newcols);
+    }
+    pnm_freerow(xelrow);
+
+    *shearedXelsP = shearedXels;
+    *newcolsP = newcols;
+
+    assert(rows >= 1);  /* Ergo, bgColor is defined */
+    *bgColorP = bgColor;
+}
+
+
+
+static void 
+shearYNoAntialias(xel **           const inxels,
+                  xel **           const outxels,
+                  int              const cols,
+                  int              const inrows,
+                  int              const outrows,
+                  int              const format,
+                  xel              const bgColor,
+                  struct shearParm const shearParm[]) {
+/*----------------------------------------------------------------------------
+   Shear the image in 'inxels' ('cols' x 'inrows') vertically into
+   'outxels' ('cols' x 'outrows'), both format 'format'.  shearParm[X]
+   tells how much to shear pixels in Column X (clipped to Rows 0
+   through 'outrow' -1) and 'bgColor' is what to use for background
+   where there is none of the input in the output.
+
+   We do not do any antialiasing.  We simply move whole pixels.
+
+   We go row by row instead of column by column to save real memory.  Going
+   row by row, the working set is only a few pages, whereas going column by
+   column, it would be one page per output row plus one page per input row.
+-----------------------------------------------------------------------------*/
+    unsigned int inrow;
+    unsigned int outrow;
+
+    /* Fill the output with background */
+    for (outrow = 0; outrow < outrows; ++outrow) {
+        unsigned int col;
+        for (col = 0; col < cols; ++col)
+            outxels[outrow][col] = bgColor;
+    }
+
+    /* Overlay that background with sheared image */
+    for (inrow = 0; inrow < inrows; ++inrow) {
+        unsigned int col;
+        for (col = 0; col < cols; ++col) {
+            int const outrow = inrow + shearParm[col].shiftUnits;
+            if (outrow >= 0 && outrow < outrows)
+                outxels[outrow][col] = inxels[inrow][col];
+        }
+    }
+}
+
+
+
+static void
+shearYColAntialias(xel ** const inxels, 
+                   xel ** const outxels,
+                   int    const col,
+                   int    const inrows,
+                   int    const outrows,
+                   int    const format,
+                   xel    const bgxel,
+                   struct shearParm shearParm[]) {
+/*-----------------------------------------------------------------------------
+  Shear a column vertically.
+-----------------------------------------------------------------------------*/
+    long const fracnew0   = shearParm[col].fracnew0;
+    long const omfracnew0 = shearParm[col].omfracnew0;
+    int  const shiftWhole = shearParm[col].shiftWhole;
+        
+    int outrow;
+
+    xel prevxel;
+    int inrow;
+        
+    /* Initialize everything to background color */
+    for (outrow = 0; outrow < outrows; ++outrow)
+        outxels[outrow][col] = bgxel;
+
+    prevxel = bgxel;
+    for (inrow = 0; inrow < inrows; ++inrow) {
+        int const outrow = inrow + shiftWhole;
+
+        if (outrow >= 0 && outrow < outrows) {
+            xel * const nxP = &(outxels[outrow][col]);
+            xel const x = inxels[inrow][col];
+            switch ( PNM_FORMAT_TYPE(format) ) {
+            case PPM_TYPE:
+                PPM_ASSIGN(*nxP,
+                           (fracnew0 * PPM_GETR(prevxel) 
+                            + omfracnew0 * PPM_GETR(x) 
+                            + HALFSCALE) / SCALE,
+                           (fracnew0 * PPM_GETG(prevxel) 
+                            + omfracnew0 * PPM_GETG(x) 
+                            + HALFSCALE) / SCALE,
+                           (fracnew0 * PPM_GETB(prevxel) 
+                            + omfracnew0 * PPM_GETB(x) 
+                            + HALFSCALE) / SCALE );
+                break;
+                        
+            default:
+                PNM_ASSIGN1(*nxP,
+                            (fracnew0 * PNM_GET1(prevxel) 
+                             + omfracnew0 * PNM_GET1(x) 
+                             + HALFSCALE) / SCALE );
+                break;
+            }
+            prevxel = x;
+        }
+    }
+    if (fracnew0 > 0 && shiftWhole + inrows < outrows) {
+        xel * const nxP = &(outxels[shiftWhole + inrows][col]);
+        switch (PNM_FORMAT_TYPE(format)) {
+        case PPM_TYPE:
+            PPM_ASSIGN(*nxP,
+                       (fracnew0 * PPM_GETR(prevxel) 
+                        + omfracnew0 * PPM_GETR(bgxel) 
+                        + HALFSCALE) / SCALE,
+                       (fracnew0 * PPM_GETG(prevxel) 
+                        + omfracnew0 * PPM_GETG(bgxel) 
+                        + HALFSCALE) / SCALE,
+                       (fracnew0 * PPM_GETB(prevxel) 
+                        + omfracnew0 * PPM_GETB(bgxel) 
+                        + HALFSCALE) / SCALE);
+            break;
+                
+        default:
+            PNM_ASSIGN1(*nxP,
+                        (fracnew0 * PNM_GET1(prevxel) 
+                         + omfracnew0 * PNM_GET1(bgxel) 
+                         + HALFSCALE) / SCALE);
+            break;
+        }
+    }
+} 
+
+
+
+static void
+shearImageY(xel **                 const inxels,
+            int                    const cols,
+            int                    const inrows,
+            int                    const format,
+            xel                    const bgxel,
+            bool                   const antialias,
+            enum rotationDirection const direction,
+            float                  const yshearfac,
+            int                    const yshearjunk,
+            xel ***                const outxelsP,
+            unsigned int *         const outrowsP) {
+    
+    unsigned int const maxShear = (cols - 0.5) * yshearfac + 0.5;
+    unsigned int const outrows = inrows + maxShear - 2 * yshearjunk;
+
+    struct shearParm * shearParm;  /* malloc'ed */
+    int col;
+    xel ** outxels;
+    
+    outxels = pnm_allocarray(cols, outrows);
+
+    MALLOCARRAY(shearParm, cols);
+    if (shearParm == NULL)
+        pm_error("Unable to allocate memory for shearParm");
+
+    for (col = 0; col < cols; ++col) {
+        /* The shear factor is designed to shear over the entire height
+           from the top edge of of the top pixel to the bottom edge of
+           the bottom pixel.  We use the distance of the center of this
+           pixel from the relevant edge to compute shift amount:
+        */
+        float const yDistance = 
+            (direction == CLOCKWISE ? col + 0.5 : (cols-0.5 - col));
+        float const shiftAmount = yshearfac * yDistance;
+
+        shearParm[col].fracnew0   = (shiftAmount - (int)shiftAmount) * SCALE;
+        shearParm[col].omfracnew0 = SCALE - shearParm[col].fracnew0;
+        shearParm[col].shiftWhole = (int)shiftAmount - yshearjunk;
+        shearParm[col].shiftUnits = (int)(shiftAmount + 0.5) - yshearjunk;
+    }
+    if (!antialias)
+        shearYNoAntialias(inxels, outxels, cols, inrows, outrows, format,
+                          bgxel, shearParm);
+    else {
+        /* TODO: do this row-by-row, same as for noantialias, to save
+           real memory.
+        */
+        for (col = 0; col < cols; ++col) 
+            shearYColAntialias(inxels, outxels, col, inrows, outrows, format, 
+                               bgxel, shearParm);
+    }
+    free(shearParm);
+    
+    *outxelsP = outxels;
+    *outrowsP = outrows;
+}
+
+
+
+static void
+shearFinal(xel * const inRow, 
+           xel * const outRow, 
+           int   const incols, 
+           int   const outcols,
+           int   const format,
+           xel   const bgxel,
+           bool  const antialias,
+           float const shiftAmount,
+           int   const x2shearjunk) {
+
+
+    assert(shiftAmount >= 0.0);
+
+    {
+        unsigned int col;
+        for (col = 0; col < outcols; ++col)
+            outRow[col] = bgxel;
+    }
+
+    if (antialias) {
+        long const fracnew0   = (shiftAmount - (int) shiftAmount) * SCALE; 
+        long const omfracnew0 = SCALE - fracnew0; 
+        unsigned int const shiftWhole = (int)shiftAmount - x2shearjunk;
+
+        xel prevxel;
+        unsigned int col;
+
+        prevxel = bgxel;
+        for (col = 0; col < incols; ++col) {
+            int const new = shiftWhole + col;
+            if (new >= 0 && new < outcols) {
+                xel * const nxP = &(outRow[new]);
+                xel const x = inRow[col];
+                switch (PNM_FORMAT_TYPE(format)) {
+                case PPM_TYPE:
+                    PPM_ASSIGN(*nxP,
+                               (fracnew0 * PPM_GETR(prevxel) 
+                                + omfracnew0 * PPM_GETR(x) 
+                                + HALFSCALE) / SCALE,
+                               (fracnew0 * PPM_GETG(prevxel) 
+                                + omfracnew0 * PPM_GETG(x) 
+                                + HALFSCALE) / SCALE,
+                               (fracnew0 * PPM_GETB(prevxel) 
+                                + omfracnew0 * PPM_GETB(x) 
+                                + HALFSCALE) / SCALE);
+                    break;
+                    
+                default:
+                    PNM_ASSIGN1(*nxP,
+                                (fracnew0 * PNM_GET1(prevxel) 
+                                 + omfracnew0 * PNM_GET1(x) 
+                                 + HALFSCALE) / SCALE );
+                    break;
+                }
+                prevxel = x;
+            }
+        }
+        if (fracnew0 > 0 && shiftWhole + incols < outcols) {
+            xel * const nxP = &(outRow[shiftWhole + incols]);
+            switch (PNM_FORMAT_TYPE(format)) {
+            case PPM_TYPE:
+                PPM_ASSIGN(*nxP,
+                           (fracnew0 * PPM_GETR(prevxel) 
+                            + omfracnew0 * PPM_GETR(bgxel) 
+                            + HALFSCALE) / SCALE,
+                           (fracnew0 * PPM_GETG(prevxel) 
+                            + omfracnew0 * PPM_GETG(bgxel) 
+                            + HALFSCALE) / SCALE,
+                           (fracnew0 * PPM_GETB(prevxel) 
+                            + omfracnew0 * PPM_GETB(bgxel) 
+                            + HALFSCALE) / SCALE);
+                break;
+                
+            default:
+                PNM_ASSIGN1(*nxP,
+                            (fracnew0 * PNM_GET1(prevxel) 
+                             + omfracnew0 * PNM_GET1(bgxel) 
+                             + HALFSCALE) / SCALE );
+                break;
+            }
+        }
+    } else {
+        unsigned int const shiftCols =
+            (unsigned int)(shiftAmount + 0.5) - x2shearjunk;
+
+        unsigned int col;
+        for (col = 0; col < incols; ++col) {
+            unsigned int const outcol = shiftCols + col;
+            if (outcol >= 0 && outcol < outcols)
+                outRow[outcol] = inRow[col];
+        }
+    }
+}
+
+
+
+static void
+shearXToOutputFile(FILE *                 const ofP,
+                   xel **                 const xels,
+                   unsigned int           const cols, 
+                   unsigned int           const rows,
+                   xelval                 const maxval,
+                   int                    const format,
+                   enum rotationDirection const direction,
+                   float                  const xshearfac,
+                   int                    const x2shearjunk,
+                   xel                    const bgColor,
+                   bool                   const antialias) {
+/*----------------------------------------------------------------------------
+   Shear horizontally the image in 'xels' and write the result to file
+   'ofP'.  'cols', 'rows', 'maxval', and 'format' describe the image in
+   'xels'.  They also describe the output image, except that it will be
+   wider as dictated by the shearing parameters.
+
+   Shear over background color 'bgColor'.
+
+   Do a smooth pixel-mixing shear iff 'antialias' is true.
+-----------------------------------------------------------------------------*/
+    unsigned int const maxShear = (rows - 0.5) * xshearfac + 0.5;
+    unsigned int const newcols = cols + maxShear - 2 * x2shearjunk;
+
+    unsigned int row;
+    xel * xelrow;
+    
+    pnm_writepnminit(stdout, newcols, rows, maxval, format, 0);
+
+    xelrow = pnm_allocrow(newcols);
+
+    for (row = 0; row < rows; ++row) {
+        /* The shear factor is designed to shear over the entire width
+           from the left edge of of the left pixel to the right edge of
+           the right pixel.  We use the distance of the center of this
+           pixel from the relevant edge to compute shift amount:
+        */
+        float const xDistance = 
+            (direction == COUNTERCLOCKWISE ? row + 0.5 : (rows-0.5 - row));
+        float const shiftAmount = xshearfac * xDistance;
+
+        shearFinal(xels[row], xelrow, cols, newcols, format, 
+                   bgColor, antialias, shiftAmount, x2shearjunk);
+
+        pnm_writepnmrow(stdout, xelrow, newcols, maxval, format, 0);
+    }
+    pnm_freerow(xelrow);
+}
+
+
+
+int
+main(int argc, char *argv[]) { 
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    xel ** shear1xels;
+    xel ** shear2xels;
+    xel bgColor;
+    int rows, cols, format;
+    int newformat;
+    unsigned int newrows;
+    int newRowsWithJunk;
+    unsigned int shear1Cols;
+    int yshearjunk, x2shearjunk;
+    xelval maxval, newmaxval;
+    float xshearfac, yshearfac;
+    enum rotationDirection direction;
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilespec);
+
+    pnm_readpnminit(ifP, &cols, &rows, &maxval, &format);
+    
+    computeNewFormat(!cmdline.noantialias, format, maxval, 
+                     &newformat, &newmaxval);
+
+    xshearfac = fabs(tan(cmdline.angle / 2.0));
+    yshearfac = fabs(sin(cmdline.angle));
+    direction = cmdline.angle > 0 ? COUNTERCLOCKWISE : CLOCKWISE;
+
+    /* The algorithm we use, for maximum speed, is 3 simple shears:
+       A horizontal, a vertical, and another horizontal.
+    */
+
+    shearXFromInputFile(ifP, cols, rows, maxval, format,
+                        direction, xshearfac,
+                        newmaxval, newformat,
+                        !cmdline.noantialias, cmdline.background,
+                        &shear1xels, &shear1Cols, &bgColor);
+    
+    pm_close(ifP);
+
+    if (cmdline.verbose)
+        reportBackground(bgColor);
+
+    if (cmdline.keeptemp)
+        storeImage("pnmrotate_stage1.pnm", shear1xels, shear1Cols, rows,
+                   newmaxval, newformat);
+
+    yshearjunk = (shear1Cols - cols) * yshearfac;
+    newRowsWithJunk = (shear1Cols - 1) * yshearfac + rows + 0.999999;
+    x2shearjunk = (newRowsWithJunk - rows - yshearjunk - 1) * xshearfac;
+
+    shearImageY(shear1xels, shear1Cols, rows, newformat,
+                bgColor, !cmdline.noantialias, direction,
+                yshearfac, yshearjunk,
+                &shear2xels, &newrows);
+
+    pnm_freearray(shear1xels, rows);
+
+    if (cmdline.keeptemp)
+        storeImage("pnmrotate_stage2.pnm", shear2xels, shear1Cols, newrows, 
+                   newmaxval, newformat);
+
+    shearXToOutputFile(stdout, shear2xels, shear1Cols, newrows,
+                       newmaxval, newformat,
+                       direction, xshearfac, x2shearjunk, 
+                       bgColor, !cmdline.noantialias);
+
+    pnm_freearray(shear2xels, newrows);
+    pm_close(stdout);
+    
+    return 0;
+}
diff --git a/editor/pnmscale.c b/editor/pnmscale.c
new file mode 100644
index 00000000..f75f440c
--- /dev/null
+++ b/editor/pnmscale.c
@@ -0,0 +1,748 @@
+/* pnmscale.c - read a portable anymap and scale it
+**
+** 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.
+**
+*/
+
+/* 
+
+      DON'T ADD NEW FUNCTION TO THIS PROGRAM.  ADD IT TO pamscale.c
+      INSTEAD.
+
+*/
+
+ 
+#include <math.h>
+#include <string.h>
+
+#include "pnm.h"
+#include "shhopt.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 cmdline_info {
+    /* 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 */
+    unsigned int xsize;
+    unsigned int ysize;
+    float xscale;
+    float yscale;
+    unsigned int xbox;
+    unsigned int ybox;
+    unsigned int pixels;
+    unsigned int verbose;
+    unsigned int nomix;
+};
+
+
+static void
+parse_command_line(int argc, char ** argv,
+                   struct cmdline_info *cmdline_p) {
+/*----------------------------------------------------------------------------
+   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 = malloc( 100*sizeof( optEntry ) );
+        /* Instructions to optParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+    unsigned int xysize;
+    int xsize, ysize, pixels;
+    int reduce;
+    float xscale, yscale, scale_parm;
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    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, "xysize",    OPT_FLAG,    NULL, &xysize,              0);
+    OPTENT3(0, "verbose",   OPT_FLAG,    NULL, &cmdline_p->verbose,  0);
+    OPTENT3(0, "nomix",     OPT_FLAG,    NULL, &cmdline_p->nomix,    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 );
+        /* Uses and sets argc, argv, and some of *cmdline_p and others. */
+
+    if (xsize == 0)
+        pm_error("-xsize/width must be greater than zero.");
+    if (ysize == 0)
+        pm_error("-ysize/height must be greater than zero.");
+    if (xscale != -1.0 && xscale <= 0.0)
+        pm_error("-xscale must be greater than zero.");
+    if (yscale != -1.0 && yscale <= 0.0)
+        pm_error("-yscale must be greater than zero.");
+    if (reduce <= 0 && reduce != -1)
+        pm_error("-reduce must be greater than zero.");
+
+    if (xsize != -1 && xscale != -1)
+        pm_error("Cannot specify both -xsize/width and -xscale.");
+    if (ysize != -1 && yscale != -1)
+        pm_error("Cannot specify both -ysize/height and -yscale.");
+    
+    if (xysize && 
+        (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 || 
+         reduce != -1 || pixels != -1) )
+        pm_error("Cannot specify -xysize with other dimension options.");
+    if (pixels != -1 && 
+        (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 ||
+         reduce != -1) )
+        pm_error("Cannot specify -pixels with other dimension options.");
+    if (reduce != -1 && 
+        (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1) )
+        pm_error("Cannot specify -reduce with other dimension options.");
+
+    if (pixels == 0)
+        pm_error("-pixels must be greater than zero");
+
+    /* Get the program parameters */
+
+    if (xysize) {
+        /* parameters are xbox, ybox, and optional filespec */
+        scale_parm = 0.0;
+        if (argc-1 < 2)
+            pm_error("You must supply at least two parameters with -xysize:\n "
+                     "x and y dimensions of the bounding box.");
+        else if (argc-1 > 3)
+            pm_error("Too many arguments.  With -xysize, you need 2 or 3 "
+                     "arguments.");
+        else {
+            char * endptr;
+            cmdline_p->xbox = strtol(argv[1], &endptr, 10);
+            if (strlen(argv[1]) > 0 && *endptr != '\0')
+                pm_error("horizontal xysize not an integer: '%s'", argv[1]);
+            if (cmdline_p->xbox <= 0)
+                pm_error("horizontal size is not positive: %d", 
+                         cmdline_p->xbox);
+
+            cmdline_p->ybox = strtol(argv[2], &endptr, 10);
+            if (strlen(argv[2]) > 0 && *endptr != '\0')
+                pm_error("vertical xysize not an integer: '%s'", argv[2]);
+            if (cmdline_p->ybox <= 0)
+                pm_error("vertical size is not positive: %d", 
+                         cmdline_p->ybox);
+            
+            if (argc-1 < 3)
+                cmdline_p->input_filespec = "-";
+            else
+                cmdline_p->input_filespec = argv[3];
+        }
+    } else {
+        cmdline_p->xbox = 0;
+        cmdline_p->ybox = 0;
+        
+        if (xsize == -1 && xscale == -1 && ysize == -1 && yscale == -1
+            && pixels == -1 && reduce == -1) {
+            /* parameters are scale factor and optional filespec */
+            if (argc-1 < 1)
+                pm_error("With no dimension options, you must supply at least "
+                         "one parameter: \nthe scale factor.");
+            else {
+                scale_parm = atof(argv[1]);
+
+                if (scale_parm == 0.0)
+                    pm_error("The scale parameter %s is not "
+                             "a positive number.",
+                             argv[1]);
+                else {
+                    if (argc-1 < 2)
+                        cmdline_p->input_filespec = "-";
+                    else
+                        cmdline_p->input_filespec = argv[2];
+                }
+            }
+        } else {
+            /* Only parameter allowed is optional filespec */
+            if (argc-1 < 1)
+                cmdline_p->input_filespec = "-";
+            else
+                cmdline_p->input_filespec = argv[1];
+
+            if (reduce != -1) {
+                scale_parm = ((double) 1.0) / ((double) reduce);
+                pm_message("reducing by %d gives scale factor of %f.", 
+                           reduce, scale_parm);
+            } else
+                scale_parm = 0.0;
+        }
+    }
+
+    cmdline_p->xsize = xsize == -1 ? 0 : xsize;
+    cmdline_p->ysize = ysize == -1 ? 0 : ysize;
+    cmdline_p->pixels = pixels == -1 ? 0 : pixels;
+
+    if (scale_parm) {
+        cmdline_p->xscale = scale_parm;
+        cmdline_p->yscale = scale_parm;
+    } else {
+        cmdline_p->xscale = xscale == -1.0 ? 0.0 : xscale;
+        cmdline_p->yscale = yscale == -1.0 ? 0.0 : yscale;
+    }
+}
+
+
+
+static void
+compute_output_dimensions(const struct cmdline_info cmdline, 
+                          const int rows, const int cols,
+                          int * newrowsP, int * newcolsP) {
+
+    if (cmdline.pixels) {
+        if (rows * cols <= cmdline.pixels) {
+            *newrowsP = rows;
+            *newcolsP = cols;
+        } else {
+            const double scale =
+                sqrt( (float) cmdline.pixels / ((float) cols * (float) rows));
+            *newrowsP = rows * scale;
+            *newcolsP = cols * scale;
+        }
+    } else if (cmdline.xbox) {
+        const double aspect_ratio = (float) cols / (float) rows;
+        const double box_aspect_ratio = 
+            (float) cmdline.xbox / (float) cmdline.ybox;
+        
+        if (box_aspect_ratio > aspect_ratio) {
+            *newrowsP = cmdline.ybox;
+            *newcolsP = *newrowsP * aspect_ratio + 0.5;
+        } else {
+            *newcolsP = cmdline.xbox;
+            *newrowsP = *newcolsP / aspect_ratio + 0.5;
+        }
+    } else {
+        if (cmdline.xsize)
+            *newcolsP = cmdline.xsize;
+        else if (cmdline.xscale)
+            *newcolsP = cmdline.xscale * cols + .5;
+        else if (cmdline.ysize)
+            *newcolsP = cols * ((float) cmdline.ysize/rows) +.5;
+        else
+            *newcolsP = cols;
+
+        if (cmdline.ysize)
+            *newrowsP = cmdline.ysize;
+        else if (cmdline.yscale)
+            *newrowsP = cmdline.yscale * rows +.5;
+        else if (cmdline.xsize)
+            *newrowsP = rows * ((float) cmdline.xsize/cols) +.5;
+        else
+            *newrowsP = 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;
+}        
+
+
+
+static void
+horizontal_scale(const xel inputxelrow[], xel newxelrow[], 
+                 const int cols, const int newcols, const float xscale, 
+                 const int format, const xelval maxval,
+                 float * const stretchP) {
+/*----------------------------------------------------------------------------
+   Take the input row inputxelrow[], which is 'cols' columns wide, and
+   scale it by a factor of 'xscale', to create
+   the output row newxelrow[], which is 'newcols' columns wide.
+
+   'format' and 'maxval' describe the Netpbm format of the both input and
+   output rows.
+-----------------------------------------------------------------------------*/
+    float r, g, b;
+    float fraccoltofill, fraccolleft;
+    unsigned int col;
+    unsigned int newcol;
+    
+    newcol = 0;
+    fraccoltofill = 1.0;  /* Output column is "empty" now */
+    r = g = b = 0;          /* initial value */
+    for (col = 0; col < cols; ++col) {
+        /* Process one pixel from input ('inputxelrow') */
+        fraccolleft = xscale;
+        /* Output all columns, if any, that can be filled using information
+           from this input column, in addition to what's already in the output
+           column.
+        */
+        while (fraccolleft >= fraccoltofill) {
+            /* Generate one output pixel in 'newxelrow'.  It will consist
+               of anything accumulated from prior input pixels in 'r','g', 
+               and 'b', plus a fraction of the current input pixel.
+            */
+            switch (PNM_FORMAT_TYPE(format)) {
+            case PPM_TYPE:
+                r += fraccoltofill * PPM_GETR(inputxelrow[col]);
+                g += fraccoltofill * PPM_GETG(inputxelrow[col]);
+                b += fraccoltofill * PPM_GETB(inputxelrow[col]);
+                PPM_ASSIGN( newxelrow[newcol], 
+                            MIN(maxval, (int) (r + 0.5)), 
+                            MIN(maxval, (int) (g + 0.5)), 
+                            MIN(maxval, (int) (b + 0.5))
+                    );
+                break;
+
+            default:
+                g += fraccoltofill * PNM_GET1(inputxelrow[col]);
+                PNM_ASSIGN1( newxelrow[newcol], MIN(maxval, (int) (g + 0.5)));
+                break;
+            }
+            fraccolleft -= fraccoltofill;
+            /* Set up to start filling next output column */
+            newcol++;
+            fraccoltofill = 1.0;
+            r = g = b = 0.0;
+        }
+        /* 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.
+        */
+        if (fraccolleft > 0.0) {
+            switch (PNM_FORMAT_TYPE(format)) {
+            case PPM_TYPE:
+                r += fraccolleft * PPM_GETR(inputxelrow[col]);
+                g += fraccolleft * PPM_GETG(inputxelrow[col]);
+                b += fraccolleft * PPM_GETB(inputxelrow[col]);
+                break;
+                    
+            default:
+                g += fraccolleft * PNM_GET1(inputxelrow[col]);
+                break;
+            }
+            fraccoltofill -= fraccolleft;
+        }
+    }
+
+    if (newcol < newcols-1 || newcol > newcols)
+        pm_error("Internal error: last column filled is %d, but %d "
+                 "is the rightmost output column.",
+                 newcol, newcols-1);
+
+    if (newcol < newcols ) {
+        /* We were still working on the last output column when we 
+           ran out of input columns.  This would be because of rounding
+           down, and we should be missing only a tiny fraction of that
+           last output column.
+        */
+
+        *stretchP = fraccoltofill;
+
+        switch (PNM_FORMAT_TYPE(format)) {
+        case PPM_TYPE:
+            r += fraccoltofill * PPM_GETR(inputxelrow[cols-1]);
+            g += fraccoltofill * PPM_GETG(inputxelrow[cols-1]);
+            b += fraccoltofill * PPM_GETB(inputxelrow[cols-1]);
+
+            PPM_ASSIGN(newxelrow[newcol], 
+                       MIN(maxval, (int) (r + 0.5)), 
+                       MIN(maxval, (int) (g + 0.5)), 
+                       MIN(maxval, (int) (b + 0.5))
+                );
+            break;
+                
+        default:
+            g += fraccoltofill * PNM_GET1(inputxelrow[cols-1]);
+            PNM_ASSIGN1( newxelrow[newcol], MIN(maxval, (int) (g + 0.5)));
+            break;
+        }
+    } else 
+        *stretchP = 0;
+}
+
+
+
+static void
+zeroAccum(int const cols, int const format, 
+          float rs[], float gs[], float bs[]) {
+
+    int col;
+
+    for ( col = 0; col < cols; ++col )
+        rs[col] = gs[col] = bs[col] = 0.0;
+}
+
+
+
+static void
+accumOutputRow(xel * const xelrow, float const fraction, 
+               float rs[], float gs[], float bs[], 
+               int const cols, int const format) {
+/*----------------------------------------------------------------------------
+   Take 'fraction' times the color in row xelrow and add it to 
+   rs/gs/bs.  'fraction' is less than 1.0.
+-----------------------------------------------------------------------------*/
+    int col;
+
+    switch ( PNM_FORMAT_TYPE(format) ) {
+    case PPM_TYPE:
+        for ( col = 0; col < cols; ++col ) {
+            rs[col] += fraction * PPM_GETR(xelrow[col]);
+            gs[col] += fraction * PPM_GETG(xelrow[col]);
+            bs[col] += fraction * PPM_GETB(xelrow[col]);
+        }
+        break;
+
+    default:
+        for ( col = 0; col < cols; ++col)
+            gs[col] += fraction * PNM_GET1(xelrow[col]);
+        break;
+    }
+}
+
+
+
+static void
+makeRow(xel * const xelrow, float rs[], float gs[], float bs[],
+        int const cols, xelval const maxval, int const format) {
+/*----------------------------------------------------------------------------
+   Make an xel row at 'xelrow' with format 'format' and
+   maxval 'maxval' out of the color values in 
+   rs[], gs[], and bs[].
+-----------------------------------------------------------------------------*/
+    int col;
+
+    switch ( PNM_FORMAT_TYPE(format) ) {
+    case PPM_TYPE:
+        for ( col = 0; col < cols; ++col) {
+            PPM_ASSIGN(xelrow[col], 
+                       MIN(maxval, (int) (rs[col] + 0.5)), 
+                       MIN(maxval, (int) (gs[col] + 0.5)), 
+                       MIN(maxval, (int) (bs[col] + 0.5))
+                );
+        }
+        break;
+
+    default:
+        for ( col = 0; col < cols; ++col ) {
+            PNM_ASSIGN1(xelrow[col], 
+                        MIN(maxval, (int) (gs[col] + 0.5)));
+        }
+        break;
+    }
+}
+
+
+
+static void
+scaleWithMixing(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,
+                float const xscale, float const yscale,
+                bool const verbose) {
+/*----------------------------------------------------------------------------
+   Scale the image on input file 'ifP' (which is described by 
+   'cols', 'rows', 'format', and 'maxval') by xscale horizontally and
+   yscale vertically and write the result to standard output as format
+   'newformat' and with maxval 'newmaxval'.
+
+   The input file is positioned past the header, to the beginning of the
+   raster.  The output file is too.
+
+   Mix colors from input rows together in the output rows.
+-----------------------------------------------------------------------------*/
+    /* Here's how we think of the color mixing scaling operation:  
+       
+       First, I'll describe scaling in one dimension.  Assume we have
+       a one row image.  A raster row is ordinarily a sequence of
+       discrete pixels which have no width and no distance between
+       them -- only a sequence.  Instead, think of the raster row as a
+       bunch of pixels 1 unit wide adjacent to each other.  For
+       example, we are going to scale a 100 pixel row to a 150 pixel
+       row.  Imagine placing the input row right above the output row
+       and stretching it so it is the same size as the output row.  It
+       still contains 100 pixels, but they are 1.5 units wide each.
+       Our goal is to make the output row look as much as possible
+       like the input row, while observing that a pixel can be only
+       one color.
+
+       Output Pixel 0 is completely covered by Input Pixel 0, so we
+       make Output Pixel 0 the same color as Input Pixel 0.  Output
+       Pixel 1 is covered half by Input Pixel 0 and half by Input
+       Pixel 1.  So we make Output Pixel 1 a 50/50 mix of Input Pixels
+       0 and 1.  If you stand back far enough, input and output will
+       look the same.
+
+       This works for all scale factors, both scaling up and scaling down.
+       
+       This program always stretches or squeezes the input row to be the
+       same length as the output row; The output row's pixels are always
+       1 unit wide.
+
+       The same thing works in the vertical direction.  We think of
+       rows as stacked strips of 1 unit height.  We conceptually
+       stretch the image vertically first (same process as above, but
+       in place of a single-color pixels, we have a vector of colors).
+       Then we take each row this vertical stretching generates and
+       stretch it horizontally.  
+    */
+
+    xel* xelrow;  /* An input row */
+    xel* vertScaledRow;
+        /* An output row after vertical scaling, but before horizontal
+           scaling
+        */
+    xel* newxelrow;
+    float rowsleft;
+        /* The number of rows of output that need to be formed from the
+           current input row (the one in xelrow[]), less the number that 
+           have already been formed (either in the rs/gs/bs accumulators
+           or output to the file).  This can be fractional because of the
+           way we define rows as having height.
+        */
+    float fracrowtofill;
+        /* The fraction of the current output row (the one in vertScaledRow[])
+           that hasn't yet been filled in from an input row.
+        */
+    float *rs, *gs, *bs;
+        /* The red, green, and blue color intensities so far accumulated
+           from input rows for the current output row.
+        */
+    int rowsread;
+        /* Number of rows of the input file that have been read */
+    int row;
+    
+    xelrow = pnm_allocrow(cols); 
+    vertScaledRow = pnm_allocrow(cols);
+    rs = (float*) pm_allocrow( cols, sizeof(float) );
+    gs = (float*) pm_allocrow( cols, sizeof(float) );
+    bs = (float*) pm_allocrow( cols, sizeof(float) );
+    rowsread = 0;
+    rowsleft = 0.0;
+    zeroAccum(cols, format, rs, gs, bs);
+    fracrowtofill = 1.0;
+
+    newxelrow = pnm_allocrow( newcols );
+
+    for ( row = 0; row < newrows; ++row ) {
+        /* First scale Y from xelrow[] into vertScaledRow[]. */
+
+        if ( newrows == rows ) { /* shortcut Y scaling if possible */
+            pnm_readpnmrow( ifP, vertScaledRow, cols, newmaxval, format );
+	    } else {
+            while (fracrowtofill > 0) {
+                if (rowsleft <= 0.0) {
+                    if (rowsread < rows) {
+                        pnm_readpnmrow(ifP, xelrow, cols, newmaxval, format);
+                        ++rowsread;
+                    } else {
+                        /* We need another input row to fill up this
+                           output row, but there aren't any more.
+                           That's because of rounding down on our
+                           scaling arithmetic.  So we go ahead with
+                           the data from the last row we read, which
+                           amounts to stretching out the last output
+                           row.  
+                        */
+                        if (verbose)
+                            pm_message("%f of bottom row stretched due to "
+                                       "arithmetic imprecision", 
+                                       fracrowtofill);
+                    }
+                    rowsleft = yscale;
+                }
+                if (rowsleft < fracrowtofill) {
+                    accumOutputRow(xelrow, rowsleft, rs, gs, bs, 
+                                   cols, format);
+                    fracrowtofill -= rowsleft;
+                    rowsleft = 0.0;
+                } else {
+                    accumOutputRow(xelrow, fracrowtofill, rs, gs, bs,
+                                   cols, format);
+                    rowsleft = rowsleft - fracrowtofill;
+                    fracrowtofill = 0.0;
+                }
+            }
+            makeRow(vertScaledRow, rs, gs, bs, cols, newmaxval, format);
+            zeroAccum(cols, format, rs, gs, bs);
+            fracrowtofill = 1.0;
+	    }
+
+        /* Now scale vertScaledRow horizontally into newxelrow and write
+           it out. 
+        */
+
+        if (newcols == cols)	/* shortcut X scaling if possible */
+            pnm_writepnmrow(stdout, vertScaledRow, newcols, 
+                            newmaxval, newformat, 0);
+        else {
+            float stretch;
+
+            horizontal_scale(vertScaledRow, newxelrow, cols, newcols, xscale, 
+                             format, newmaxval, &stretch);
+            
+            if (verbose && row == 0)
+                pm_message("%f of right column stretched due to "
+                           "arithmetic imprecision", 
+                           stretch);
+            
+            pnm_writepnmrow(stdout, newxelrow, newcols, 
+                            newmaxval, newformat, 0 );
+        }
+	}
+    pnm_freerow(newxelrow);
+    pnm_freerow(xelrow);
+    pnm_freerow(vertScaledRow);
+}
+
+
+
+static void
+scaleWithoutMixing(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,
+                   float const xscale, float const yscale) {
+/*----------------------------------------------------------------------------
+   Scale the image on input file 'ifP' (which is described by 
+   'cols', 'rows', 'format', and 'maxval') by xscale horizontally and
+   yscale vertically and write the result to standard output as format
+   'newformat' and with maxval 'newmaxval'.
+
+   The input file is positioned past the header, to the beginning of the
+   raster.  The output file is too.
+
+   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;
+    int row;
+    int rowInXelrow;
+
+    xelrow = pnm_allocrow(cols); 
+    rowInXelrow = -1;
+
+    newxelrow = pnm_allocrow(newcols);
+
+    for (row = 0; row < newrows; ++row) {
+        int col;
+        
+        int const inputRow = (int) (row / yscale);
+
+        for (; rowInXelrow < inputRow; ++rowInXelrow) 
+            pnm_readpnmrow(ifP, xelrow, cols, newmaxval, format);
+        
+
+        for (col = 0; col < newcols; ++col) {
+            int const inputCol = (int) (col / xscale);
+            
+            newxelrow[col] = xelrow[inputCol];
+        }
+
+        pnm_writepnmrow(stdout, newxelrow, newcols, 
+                        newmaxval, newformat, 0 );
+	}
+    pnm_freerow(xelrow);
+    pnm_freerow(newxelrow);
+}
+
+
+
+int
+main(int argc, char **argv ) {
+
+    struct cmdline_info cmdline;
+    FILE* ifP;
+    int rows, cols, format, newformat, newrows, newcols;
+    xelval maxval, newmaxval;
+    float xscale, yscale;
+
+    pnm_init( &argc, argv );
+
+    parse_command_line(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.input_filespec);
+
+    pnm_readpnminit( ifP, &cols, &rows, &maxval, &format );
+
+    /* Promote PBM files 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;
+    }
+    compute_output_dimensions(cmdline, rows, cols, &newrows, &newcols);
+
+    /* We round the scale factor down so that we never fill up the 
+       output while (a fractional pixel of) input remains unused.  Instead,
+       we will run out of input while (a fractional pixel of) output is 
+       unfilled -- which is easier for our algorithm to handle.
+       */
+    xscale = (float) newcols / cols;
+    yscale = (float) newrows / rows;
+
+    if (cmdline.verbose) {
+        pm_message("Scaling by %f horizontally to %d columns.", 
+                   xscale, newcols );
+        pm_message("Scaling by %f vertically to %d rows.", 
+                   yscale, newrows);
+    }
+
+    if (xscale * cols < newcols - 1 ||
+        yscale * rows < newrows - 1) 
+        pm_error("Arithmetic precision of this program is inadequate to "
+                 "do the specified scaling.  Use a smaller input image "
+                 "or a slightly different scale factor.");
+
+    pnm_writepnminit(stdout, newcols, newrows, newmaxval, newformat, 0);
+
+    if (cmdline.nomix) 
+        scaleWithoutMixing(ifP, cols, rows, maxval, format,
+                           newcols, newrows, newmaxval, newformat, 
+                           xscale, yscale);
+    else
+        scaleWithMixing(ifP, cols, rows, maxval, format,
+                        newcols, newrows, newmaxval, newformat, 
+                        xscale, yscale, cmdline.verbose);
+
+    pm_close(ifP);
+    pm_close(stdout);
+    
+    exit(0);
+}
diff --git a/editor/pnmscalefixed.c b/editor/pnmscalefixed.c
new file mode 100644
index 00000000..d562c670
--- /dev/null
+++ b/editor/pnmscalefixed.c
@@ -0,0 +1,590 @@
+/* pnmscale.c - read a portable anymap and scale it
+**
+** 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.
+**
+** Modified:
+**
+** June 6, 2001: Christopher W. Boyd <cboyd@pobox.com>
+**               - added -reduce N to allow scaling by integer value
+**                 in this case, scale_comp becomes 1/N and x/yscale
+**                 get set as they should
+**    
+**
+*/
+ 
+#include <math.h>
+#include "pnm.h"
+#include "shhopt.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.
+*/
+
+/* We do all our arithmetic in integers.  In order not to get killed by the
+   rounding, we scale every number up by the factor SCALE, do the 
+   arithmetic, then scale it back down.
+   */
+#define SCALE 4096
+#define HALFSCALE 2048
+
+
+struct cmdline_info {
+    /* 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 */
+    unsigned int xsize;
+    unsigned int ysize;
+    float xscale;
+    float yscale;
+    unsigned int xbox;
+    unsigned int ybox;
+    unsigned int pixels;
+    unsigned int verbose;
+};
+
+
+static void
+parse_command_line(int argc, char ** argv,
+                   struct cmdline_info *cmdline_p) {
+/*----------------------------------------------------------------------------
+   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.
+         */
+    optStruct2 opt;
+
+    unsigned int option_def_index;
+    int xysize, xsize, ysize, pixels;
+    int reduce;
+    float xscale, yscale, scale_parm;
+
+    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);
+
+    /* Set the defaults. -1 = unspecified */
+    xsize = -1;
+    ysize = -1;
+    xscale = -1.0;
+    yscale = -1.0;
+    pixels = -1;
+    xysize = 0;
+    reduce = -1;
+    cmdline_p->verbose = FALSE;
+
+    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 */
+
+    optParseOptions2(&argc, argv, opt, 0);
+        /* Uses and sets argc, argv, and some of *cmdline_p and others. */
+
+    if (xsize == 0)
+        pm_error("-xsize/width must be greater than zero.");
+    if (ysize == 0)
+        pm_error("-ysize/height must be greater than zero.");
+    if (xscale != -1.0 && xscale <= 0.0)
+        pm_error("-xscale must be greater than zero.");
+    if (yscale != -1.0 && yscale <= 0.0)
+        pm_error("-yscale must be greater than zero.");
+    if (reduce <= 0 && reduce != -1)
+        pm_error("-reduce must be greater than zero.");
+
+    if (xsize != -1 && xscale != -1)
+        pm_error("Cannot specify both -xsize/width and -xscale.");
+    if (ysize != -1 && yscale != -1)
+        pm_error("Cannot specify both -ysize/height and -yscale.");
+    
+    if (xysize && 
+        (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 || 
+         reduce != -1 || pixels != -1) )
+        pm_error("Cannot specify -xysize with other dimension options.");
+    if (pixels != -1 && 
+        (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 ||
+         reduce != -1) )
+        pm_error("Cannot specify -pixels with other dimension options.");
+    if (reduce != -1 && 
+        (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1) )
+        pm_error("Cannot specify -reduce with other dimension options.");
+
+    if (pixels == 0)
+        pm_error("-pixels must be greater than zero");
+
+    /* Get the program parameters */
+
+    if (xysize) {
+        /* parameters are xbox, ybox, and optional filespec */
+        scale_parm = 0.0;
+        if (argc-1 < 2)
+            pm_error("You must supply at least two parameters with -xysize:\n "
+                     "x and y dimensions of the bounding box.");
+        else if (argc-1 > 3)
+            pm_error("Too many arguments.  With -xysize, you need 2 or 3 "
+                     "arguments.");
+        else {
+            cmdline_p->xbox = atoi(argv[1]);
+            cmdline_p->ybox = atoi(argv[2]);
+            
+            if (argc-1 < 3)
+                cmdline_p->input_filespec = "-";
+            else
+                cmdline_p->input_filespec = argv[3];
+        }
+    } else {
+        cmdline_p->xbox = 0;
+        cmdline_p->ybox = 0;
+        
+        if (xsize == -1 && xscale == -1 && ysize == -1 && yscale == -1
+            && pixels == -1 && reduce == -1) {
+            /* parameters are scale factor and optional filespec */
+            if (argc-1 < 1)
+                pm_error("With no dimension options, you must supply at least "
+                         "one parameter: \nthe scale factor.");
+            else {
+                scale_parm = atof(argv[1]);
+
+                if (scale_parm == 0.0)
+                    pm_error("The scale parameter %s is not "
+                             "a positive number.",
+                             argv[1]);
+                else {
+                    if (argc-1 < 2)
+                        cmdline_p->input_filespec = "-";
+                    else
+                        cmdline_p->input_filespec = argv[2];
+                }
+            }
+        } else {
+            /* Only parameter allowed is optional filespec */
+            if (argc-1 < 1)
+                cmdline_p->input_filespec = "-";
+            else
+                cmdline_p->input_filespec = argv[1];
+
+            if (reduce != -1) {
+                scale_parm = ((double) 1.0) / ((double) reduce);
+                pm_message("reducing by %d gives scale factor of %f.", 
+                           reduce, scale_parm);
+            } else
+                scale_parm = 0.0;
+        }
+    }
+
+    cmdline_p->xsize = xsize == -1 ? 0 : xsize;
+    cmdline_p->ysize = ysize == -1 ? 0 : ysize;
+    cmdline_p->pixels = pixels == -1 ? 0 : pixels;
+
+    if (scale_parm) {
+        cmdline_p->xscale = scale_parm;
+        cmdline_p->yscale = scale_parm;
+    } else {
+        cmdline_p->xscale = xscale == -1.0 ? 0.0 : xscale;
+        cmdline_p->yscale = yscale == -1.0 ? 0.0 : yscale;
+    }
+}
+
+
+
+static void
+compute_output_dimensions(const struct cmdline_info cmdline, 
+                          const int rows, const int cols,
+                          int * newrowsP, int * newcolsP) {
+
+    if (cmdline.pixels) {
+        if (rows * cols <= cmdline.pixels) {
+            *newrowsP = rows;
+            *newcolsP = cols;
+        } else {
+            const double scale =
+                sqrt( (float) cmdline.pixels / ((float) cols * (float) rows));
+            *newrowsP = rows * scale;
+            *newcolsP = cols * scale;
+        }
+    } else if (cmdline.xbox) {
+        const double aspect_ratio = (float) cols / (float) rows;
+        const double box_aspect_ratio = 
+            (float) cmdline.xbox / (float) cmdline.ybox;
+        
+        if (box_aspect_ratio > aspect_ratio) {
+            *newrowsP = cmdline.ybox;
+            *newcolsP = *newrowsP * aspect_ratio + 0.5;
+        } else {
+            *newcolsP = cmdline.xbox;
+            *newrowsP = *newcolsP / aspect_ratio + 0.5;
+        }
+    } else {
+        if (cmdline.xsize)
+            *newcolsP = cmdline.xsize;
+        else if (cmdline.xscale)
+            *newcolsP = cmdline.xscale * cols + .5;
+        else if (cmdline.ysize)
+            *newcolsP = cols * ((float) cmdline.ysize/rows) +.5;
+        else
+            *newcolsP = cols;
+
+        if (cmdline.ysize)
+            *newrowsP = cmdline.ysize;
+        else if (cmdline.yscale)
+            *newrowsP = cmdline.yscale * rows +.5;
+        else if (cmdline.xsize)
+            *newrowsP = rows * ((float) cmdline.xsize/cols) +.5;
+        else
+            *newrowsP = 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;
+}        
+
+
+
+static void
+horizontal_scale(const xel inputxelrow[], xel newxelrow[], 
+                 const int cols, const int newcols, const long sxscale, 
+                 const int format, const xelval maxval,
+                 int * stretchP) {
+/*----------------------------------------------------------------------------
+   Take the input row inputxelrow[], which is 'cols' columns wide, and
+   scale it by a factor of 'sxcale', which is in SCALEths to create
+   the output row newxelrow[], which is 'newcols' columns wide.
+
+   'format' and 'maxval' describe the Netpbm format of the both input and
+   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.
+-----------------------------------------------------------------------------*/
+    long r, g, b;
+    long fraccoltofill, fraccolleft;
+    unsigned int col;
+    unsigned int newcol;
+    
+    newcol = 0;
+    fraccoltofill = SCALE;  /* Output column is "empty" now */
+    r = g = b = 0;          /* initial value */
+    for (col = 0; col < cols; ++col) {
+        /* Process one pixel from input ('inputxelrow') */
+        fraccolleft = sxscale;
+        /* Output all columns, if any, that can be filled using information
+           from this input column, in addition what's already in the output
+           column.
+        */
+        while (fraccolleft >= fraccoltofill) {
+            /* Generate one output pixel in 'newxelrow'.  It will consist
+               of anything accumulated from prior input pixels in 'r','g', 
+               and 'b', plus a fraction of the current input pixel.
+            */
+            switch (PNM_FORMAT_TYPE(format)) {
+            case PPM_TYPE:
+                r += fraccoltofill * PPM_GETR(inputxelrow[col]);
+                g += fraccoltofill * PPM_GETG(inputxelrow[col]);
+                b += fraccoltofill * PPM_GETB(inputxelrow[col]);
+                r /= SCALE;
+                if ( r > maxval ) r = maxval;
+                g /= SCALE;
+                if ( g > maxval ) g = maxval;
+                b /= SCALE;
+                if ( b > maxval ) b = maxval;
+                PPM_ASSIGN( newxelrow[newcol], r, g, b );
+                break;
+
+            default:
+                g += fraccoltofill * PNM_GET1(inputxelrow[col]);
+                g /= SCALE;
+                if ( g > maxval ) g = maxval;
+                PNM_ASSIGN1( newxelrow[newcol], g );
+                break;
+            }
+            fraccolleft -= fraccoltofill;
+            /* Set up to start filling next output column */
+            newcol++;
+            fraccoltofill = SCALE;
+            r = g = b = 0;
+        }
+        /* 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.
+        */
+        if (fraccolleft > 0) {
+            switch (PNM_FORMAT_TYPE(format)) {
+            case PPM_TYPE:
+                r += fraccolleft * PPM_GETR(inputxelrow[col]);
+                g += fraccolleft * PPM_GETG(inputxelrow[col]);
+                b += fraccolleft * PPM_GETB(inputxelrow[col]);
+                break;
+                    
+            default:
+                g += fraccolleft * PNM_GET1(inputxelrow[col]);
+                break;
+            }
+            fraccoltofill -= fraccolleft;
+        }
+    }
+
+    *stretchP = 0;   /* initial value */
+    while (newcol < newcols) {
+        /* We ran out of input columns before we filled up the output
+           columns.  This would be because of rounding down.  For small
+           images, we're probably missing only a tiny fraction of a column, 
+           but for large images, it could be multiple columns.
+
+           So we fake the remaining output columns by copying the rightmost
+           legitimate pixel.  We call this stretching.
+           */
+
+        *stretchP += fraccoltofill;
+
+        switch (PNM_FORMAT_TYPE(format)) {
+        case PPM_TYPE:
+            r += fraccoltofill * PPM_GETR(inputxelrow[cols-1]);
+            g += fraccoltofill * PPM_GETG(inputxelrow[cols-1]);
+            b += fraccoltofill * PPM_GETB(inputxelrow[cols-1]);
+
+            r += HALFSCALE;  /* for rounding */
+            r /= SCALE;
+            if ( r > maxval ) r = maxval;
+            g += HALFSCALE;  /* for rounding */
+            g /= SCALE;
+            if ( g > maxval ) g = maxval;
+            b += HALFSCALE;  /* for rounding */
+            b /= SCALE;
+            if ( b > maxval ) b = maxval;
+            PPM_ASSIGN(newxelrow[newcol], r, g, b );
+            break;
+                
+        default:
+            g += fraccoltofill * PNM_GET1(inputxelrow[cols-1]);
+            g += HALFSCALE;  /* for rounding */
+            g /= SCALE;
+            if ( g > maxval ) g = maxval;
+            PNM_ASSIGN1(newxelrow[newcol], g );
+            break;
+        }
+        newcol++;
+        fraccoltofill = SCALE;
+    }
+}
+
+
+int
+main(int argc, char **argv ) {
+
+    struct cmdline_info cmdline;
+    FILE* ifp;
+    xel* xelrow;
+    xel* tempxelrow;
+    xel* newxelrow;
+    xel* xP;
+    xel* nxP;
+    int rows, cols, format, newformat, rowsread, newrows, newcols;
+    int row, col, needtoreadrow;
+    xelval maxval, newmaxval;
+    long sxscale, syscale;
+    long fracrowtofill, fracrowleft;
+    long* rs;
+    long* gs;
+    long* bs;
+    int vertical_stretch;
+        /* The number of rows we had to fill by stretching because of 
+           rounding error, which made us run out of input rows before we
+           had filled up the output rows.
+           */
+
+    pnm_init( &argc, argv );
+
+    parse_command_line(argc, argv, &cmdline);
+
+    ifp = pm_openr(cmdline.input_filespec);
+
+    pnm_readpnminit( ifp, &cols, &rows, &maxval, &format );
+
+    /* Promote PBM files 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;
+    }
+    compute_output_dimensions(cmdline, rows, cols, &newrows, &newcols);
+
+    /* We round the scale factor down so that we never fill up the
+       output while (a fractional pixel of) input remains unused.
+       Instead, we will run out of input while some of the output is
+       unfilled.  We can address that by stretching, whereas the other
+       case would require throwing away some of the input.
+    */
+    sxscale = SCALE * newcols / cols;
+    syscale = SCALE * newrows / rows;
+
+    if (cmdline.verbose) {
+        pm_message("Scaling by %ld/%d = %f horizontally to %d columns.", 
+                   sxscale, SCALE, (float) sxscale/SCALE, newcols );
+        pm_message("Scaling by %ld/%d = %f vertically to %d rows.", 
+                   syscale, SCALE, (float) syscale/SCALE, newrows);
+    }
+
+    xelrow = pnm_allocrow(cols);
+    if (newrows == rows)	/* shortcut Y scaling if possible */
+        tempxelrow = xelrow;
+    else
+        tempxelrow = pnm_allocrow( cols );
+    rs = (long*) pm_allocrow( cols, sizeof(long) );
+    gs = (long*) pm_allocrow( cols, sizeof(long) );
+    bs = (long*) pm_allocrow( cols, sizeof(long) );
+    rowsread = 0;
+    fracrowleft = syscale;
+    needtoreadrow = 1;
+    for ( col = 0; col < cols; ++col )
+	rs[col] = gs[col] = bs[col] = HALFSCALE;
+    fracrowtofill = SCALE;
+    vertical_stretch = 0;
+    
+    pnm_writepnminit( stdout, newcols, newrows, newmaxval, newformat, 0 );
+    newxelrow = pnm_allocrow( newcols );
+    
+    for ( row = 0; row < newrows; ++row ) {
+        /* First scale vertically from xelrow into tempxelrow. */
+        if ( newrows == rows ) { /* shortcut vertical scaling if possible */
+            pnm_readpnmrow( ifp, xelrow, cols, newmaxval, format );
+	    } else {
+            while ( fracrowleft < fracrowtofill ) {
+                if ( needtoreadrow )
+                    if ( rowsread < rows ) {
+                        pnm_readpnmrow( ifp, xelrow, cols, newmaxval, format );
+                        ++rowsread;
+                    }
+                switch ( PNM_FORMAT_TYPE(format) ) {
+                case PPM_TYPE:
+                    for ( col = 0, xP = xelrow; col < cols; ++col, ++xP ) {
+                        rs[col] += fracrowleft * PPM_GETR( *xP );
+                        gs[col] += fracrowleft * PPM_GETG( *xP );
+                        bs[col] += fracrowleft * PPM_GETB( *xP );
+                    }
+                    break;
+
+                default:
+                    for ( col = 0, xP = xelrow; col < cols; ++col, ++xP )
+                        gs[col] += fracrowleft * PNM_GET1( *xP );
+                    break;
+                }
+                fracrowtofill -= fracrowleft;
+                fracrowleft = syscale;
+                needtoreadrow = 1;
+            }
+            /* Now fracrowleft is >= fracrowtofill, so we can produce a row. */
+            if ( needtoreadrow ) {
+                if ( rowsread < rows ) {
+                    pnm_readpnmrow( ifp, xelrow, cols, newmaxval, format );
+                    ++rowsread;
+                    needtoreadrow = 0;
+                } else {
+                    /* We need another input row to fill up this output row,
+                       but there aren't any more.  That's because of rounding
+                       down on our scaling arithmetic.  So we go ahead with 
+                       the data from the last row we read, which amounts to 
+                       stretching out the last output row.
+                    */
+                    vertical_stretch += fracrowtofill;
+                }
+            }
+            switch ( PNM_FORMAT_TYPE(format) ) {
+            case PPM_TYPE:
+                for ( col = 0, xP = xelrow, nxP = tempxelrow;
+                      col < cols; ++col, ++xP, ++nxP ) {
+                    register long r, g, b;
+
+                    r = rs[col] + fracrowtofill * PPM_GETR( *xP );
+                    g = gs[col] + fracrowtofill * PPM_GETG( *xP );
+                    b = bs[col] + fracrowtofill * PPM_GETB( *xP );
+                    r /= SCALE;
+                    if ( r > newmaxval ) r = newmaxval;
+                    g /= SCALE;
+                    if ( g > newmaxval ) g = newmaxval;
+                    b /= SCALE;
+                    if ( b > newmaxval ) b = newmaxval;
+                    PPM_ASSIGN( *nxP, r, g, b );
+                    rs[col] = gs[col] = bs[col] = HALFSCALE;
+                }
+                break;
+
+            default:
+                for ( col = 0, xP = xelrow, nxP = tempxelrow;
+                      col < cols; ++col, ++xP, ++nxP ) {
+                    register long g;
+                    
+                    g = gs[col] + fracrowtofill * PNM_GET1( *xP );
+                    g /= SCALE;
+                    if ( g > newmaxval ) g = newmaxval;
+                    PNM_ASSIGN1( *nxP, g );
+                    gs[col] = HALFSCALE;
+                }
+                break;
+            }
+            fracrowleft -= fracrowtofill;
+            if ( fracrowleft == 0 ) {
+                fracrowleft = syscale;
+                needtoreadrow = 1;
+            }
+            fracrowtofill = SCALE;
+	    }
+
+        /* Now scale tempxelrow horizontally into newxelrow & write it out. */
+
+        if (newcols == cols)	/* shortcut X scaling if possible */
+            pnm_writepnmrow(stdout, tempxelrow, newcols, 
+                            newmaxval, newformat, 0);
+        else {
+            int stretch;
+
+            horizontal_scale(tempxelrow, newxelrow, cols, newcols, sxscale, 
+                             format, newmaxval, &stretch);
+            
+            if (cmdline.verbose && row == 0 && stretch != 0)
+                pm_message("%d/%d = %f right columns filled by stretching "
+                           "due to arithmetic imprecision", 
+                           stretch, SCALE, (float) stretch/SCALE);
+            
+            pnm_writepnmrow(stdout, newxelrow, newcols, 
+                            newmaxval, newformat, 0 );
+        }
+	}
+
+    if (cmdline.verbose && vertical_stretch != 0)
+        pm_message("%d/%d = %f bottom rows filled by stretching due to "
+                   "arithmetic imprecision", 
+                   vertical_stretch, SCALE, 
+                   (float) vertical_stretch/SCALE);
+    
+    pm_close( ifp );
+    pm_close( stdout );
+    
+    exit( 0 );
+}
diff --git a/editor/pnmshear.c b/editor/pnmshear.c
new file mode 100644
index 00000000..1b2d36a8
--- /dev/null
+++ b/editor/pnmshear.c
@@ -0,0 +1,227 @@
+/* pnmshear.c - read a portable anymap and shear it by some angle
+**
+** 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   /* get M_PI in math.h */
+
+#include <math.h>
+#include <string.h>
+
+#include "pnm.h"
+#include "shhopt.h"
+
+#define SCALE 4096
+#define HALFSCALE 2048
+
+struct cmdline_info {
+    /* 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 */
+    double       angle;           /* requested shear angle, in radians */
+    unsigned int noantialias;     /* -noantialias option */
+};
+
+
+
+static void
+parse_command_line(int argc, char ** argv,
+                   struct cmdline_info *cmdlineP) {
+
+    optStruct3 opt;
+    unsigned int option_def_index = 0;
+    optEntry *option_def = malloc(100*sizeof(optEntry));
+
+    OPTENT3(0, "noantialias",      OPT_FLAG,  NULL, &cmdlineP->noantialias, 0);
+    
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;
+    opt.allowNegNum = TRUE;
+
+    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+    
+    if (argc-1 < 1)
+        pm_error("Need an argument:  the shear angle.\n");
+    else {
+        char *endptr;
+        cmdlineP->angle = strtod(argv[1], &endptr) * M_PI / 180;
+        if (*endptr != '\0' || strlen(argv[1]) == 0)
+            pm_error("Angle argument is not a valid floating point number: "
+                     "'%s'", argv[1]);
+        if (argc-1 < 2)
+            cmdlineP->input_filespec = "-";
+        else {
+            cmdlineP->input_filespec = argv[2];
+            if (argc-1 > 2)
+                pm_error("too many arguments (%d).  "
+                         "The only arguments are shear angle and filespec.",
+                         argc-1);
+        }
+    }
+}
+
+
+static void
+makeNewXel(xel * const outputXelP, xel const curXel, xel const prevXel,
+           double const fracnew0, double const omfracnew0, int const format) {
+/*----------------------------------------------------------------------------
+   Create an output xel as *outputXel, which is part curXel and part
+   prevXel, the part given by the fractions omfracnew0 and fracnew0,
+   respectively.  These fraction values are the numerator of a fraction
+   whose denominator is SCALE.
+
+   The format of the pixel is 'format'.
+-----------------------------------------------------------------------------*/
+
+    switch ( PNM_FORMAT_TYPE(format) ) {
+    case PPM_TYPE:
+        PPM_ASSIGN( *outputXelP,
+                    ( fracnew0 * PPM_GETR(prevXel) 
+                      + omfracnew0 * PPM_GETR(curXel) 
+                      + HALFSCALE ) / SCALE,
+                    ( fracnew0 * PPM_GETG(prevXel) 
+                      + omfracnew0 * PPM_GETG(curXel) 
+                      + HALFSCALE ) / SCALE,
+                    ( fracnew0 * PPM_GETB(prevXel) 
+                      + omfracnew0 * PPM_GETB(curXel) 
+                      + HALFSCALE ) / SCALE );
+        break;
+        
+    default:
+        PNM_ASSIGN1( *outputXelP,
+                     ( fracnew0 * PNM_GET1(prevXel) 
+                       + omfracnew0 * PNM_GET1(curXel) 
+                       + HALFSCALE ) / SCALE );
+        break;
+    }
+}
+
+
+static void
+shear_row(xel * const xelrow, int const cols, 
+          xel * const newxelrow, int const newcols, 
+          double const shearCols,
+          int const format, xel const bgxel, bool const antialias) {
+/*----------------------------------------------------------------------------
+   Shear the row 'xelrow' by 'shearCols' columns, and return the result as
+   'newxelrow'.  They are 'cols' and 'newcols' columns wide, respectively.
+   
+   Fill in the part of the output row that doesn't contain image data with
+   'bgxel'.
+
+   Use antialiasing iff 'antialias'.
+
+   The format of the input xels (which implies something about the
+   output xels too) is 'format'.
+-----------------------------------------------------------------------------*/
+    int const intShearCols = (int) shearCols;
+        
+    if ( antialias ) {
+        const long fracnew0 = ( shearCols - intShearCols ) * SCALE;
+        const long omfracnew0 = SCALE - fracnew0;
+
+        int col;
+        xel prevXel;
+            
+        for ( col = 0; col < newcols; ++col )
+            newxelrow[col] = bgxel;
+
+        prevXel = bgxel;
+        for ( col = 0; col < cols; ++col){
+            makeNewXel(&newxelrow[intShearCols + col],
+                       xelrow[col], prevXel, fracnew0, omfracnew0,
+                       format);
+            prevXel = xelrow[col];
+        }
+        if ( fracnew0 > 0 ) 
+            /* Need to add a column for what's left over */
+            makeNewXel(&newxelrow[intShearCols + cols],
+                       bgxel, prevXel, fracnew0, omfracnew0, format);
+    } else {
+        int col;
+        for ( col = 0; col < intShearCols; ++col )
+            newxelrow[col] = bgxel;
+        for ( col = 0; col < cols; ++col )
+            newxelrow[intShearCols+col] = xelrow[col];
+        for ( col = intShearCols + cols; col < newcols; ++col )
+            newxelrow[col] = bgxel;
+    }
+}
+
+
+
+int
+main(int argc, char * argv[]) {
+    FILE* ifp;
+    xel* xelrow;
+    xel* newxelrow;
+    xel bgxel;
+    int rows, cols, format; 
+    int newformat, newcols; 
+    int row;
+    xelval maxval, newmaxval;
+    double shearfac;
+
+    struct cmdline_info cmdline;
+
+    pnm_init( &argc, argv );
+
+    parse_command_line( argc, argv, &cmdline );
+
+    ifp = pm_openr( cmdline.input_filespec );
+
+    pnm_readpnminit( ifp, &cols, &rows, &maxval, &format );
+    xelrow = pnm_allocrow( cols );
+
+    /* Promote PBM files to PGM. */
+    if ( !cmdline.noantialias && PNM_FORMAT_TYPE(format) == PBM_TYPE ) {
+        newformat = PGM_TYPE;
+        newmaxval = PGM_MAXMAXVAL;
+        pm_message( "promoting from PBM to PGM - "
+                    "use -noantialias to avoid this" );
+    } else {
+        newformat = format;
+        newmaxval = maxval;
+    }
+
+    shearfac = tan( cmdline.angle );
+    if ( shearfac < 0.0 )
+        shearfac = -shearfac;
+
+    newcols = rows * shearfac + cols + 0.999999;
+
+    pnm_writepnminit( stdout, newcols, rows, newmaxval, newformat, 0 );
+    newxelrow = pnm_allocrow( newcols );
+
+    bgxel = pnm_backgroundxelrow( xelrow, cols, newmaxval, format );
+
+    for ( row = 0; row < rows; ++row ) {
+        double shearCols;
+
+        pnm_readpnmrow( ifp, xelrow, cols, newmaxval, format );
+
+        if ( cmdline.angle > 0.0 )
+            shearCols = row * shearfac;
+        else
+            shearCols = ( rows - row ) * shearfac;
+
+        shear_row(xelrow, cols, newxelrow, newcols, 
+                  shearCols, format, bgxel, !cmdline.noantialias);
+
+        pnm_writepnmrow( stdout, newxelrow, newcols, newmaxval, newformat, 0 );
+    }
+
+    pm_close( ifp );
+    pm_close( stdout );
+
+    exit( 0 );
+}
+
diff --git a/editor/pnmsmooth.README b/editor/pnmsmooth.README
new file mode 100644
index 00000000..fc5329bd
--- /dev/null
+++ b/editor/pnmsmooth.README
@@ -0,0 +1,21 @@
+README for pnmsmooth.c 2.0
+
+This is a replacement for the pnmsmooth script that is distributed with
+pbmplus/netpbm.  This version of pnmsmooth is written as a C program rather
+than a shell script.  It accepts command line arguments to specify a size 
+other than 3x3 for the convolution matrix and an argument for dumping the 
+resultant convolution matrix as a PGM file.  By default it uses the same 3x3
+matrix size as the pnmsmooth script and can be used as a direct replacement.
+
+Also included are an updated man page and a patch file to update the Imakefile
+in the pnm directory.  You may want to apply the patches by hand if you have
+already modified the Imakefile to add other new programs.   You will then
+have to remake the Makefiles and then type 'make all' in the pnm directory.
+
+- Mike
+
+----------------------------------------------------------------------------
+Mike Burns                                              System Administrator
+burns@chem.psu.edu                                   Department of Chemistry
+(814) 863-2123                             The Pennsylvania State University
+
diff --git a/editor/pnmsmooth.c b/editor/pnmsmooth.c
new file mode 100644
index 00000000..a18511c7
--- /dev/null
+++ b/editor/pnmsmooth.c
@@ -0,0 +1,241 @@
+/* pnmsmooth.c - smooth out an image by replacing each pixel with the 
+**               average of its width x height neighbors.
+**
+** Version 2.0   December 5, 1994
+**
+** Copyright (C) 1994 by Mike Burns (burns@chem.psu.edu)
+**
+** 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.
+*/
+
+/*
+  Written by Mike Burns December 5, 1994 and called Version 2.0.
+  Based on ideas from a shell script by Jef Poskanzer, 1989, 1991.
+  The shell script had no options.
+*/
+
+
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+#include "pm_c_util.h"
+#include "mallocvar.h"
+#include "shhopt.h"
+#include "nstring.h"
+#include "pnm.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;  /* Filespec of input file */
+    unsigned int width;
+    unsigned int height;
+    const char * dump;
+};
+
+
+
+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 widthSpec, heightSpec, dumpSpec, sizeSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+    
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0,   "dump",          OPT_STRING,   
+            &cmdlineP->dump,            &dumpSpec, 0);
+    OPTENT3(0,   "width",         OPT_UINT,
+            &cmdlineP->width,           &widthSpec, 0);
+    OPTENT3(0,   "height",        OPT_UINT,
+            &cmdlineP->height,          &heightSpec, 0);
+    OPTENT3(0,   "size",          OPT_FLAG,
+            NULL,                       &sizeSpec, 0);
+
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
+
+    optParseOptions3( &argc, argv, opt, sizeof(opt), 0 );
+        /* Uses and sets argc, argv, and some of *cmdline_p and others. */
+
+    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
+           irregular syntax in which the -size option had two values,
+           e.g. "-size <width> <height>" And the options had to go
+           before the arguments.  So an old pnmsmooth command looks to us
+           like a command with the -size flag option and the first two
+           arguments being the width and height.
+        */
+
+        if (widthSpec || heightSpec)
+            pm_error("-size is obsolete.  Use -width and -height instead");
+
+        if (argc-1 > 3)
+            pm_error("Too many arguments.  With -size, there are at most "
+                     "3 arguments.");
+        else if (argc-1 < 2)
+            pm_error("Not enough arguments.  With -size, the first two "
+                     "arguments are width and height");
+        else {
+            cmdlineP->width  = atoi(argv[1]);
+            cmdlineP->height = atoi(argv[2]);
+            
+            if (argc-1 < 3)
+                cmdlineP->inputFilespec = "-";
+            else
+                cmdlineP->inputFilespec = argv[3];
+        }
+    } else {
+        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];
+    }
+    if (cmdlineP->width % 2 != 1)
+        pm_error("The convolution matrix must have an odd number of columns.  "
+                 "You specified %u", cmdlineP->width);
+
+    if (cmdlineP->height % 2 != 1)
+        pm_error("The convolution matrix must have an odd number of rows.  "
+                 "You specified %u", cmdlineP->height);
+}
+
+
+
+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);
+    }
+    pnm_freerow(outputrow);
+}
+
+
+
+static void
+runPnmconvol(const char * const inputFilespec,
+             const char * const convolutionImageFilespec) {
+
+    /* fork a Pnmconvol process */
+    pid_t rc;
+
+    rc = fork();
+    if (rc < 0)
+        pm_error("fork() failed.  errno=%d (%s)", errno, strerror(errno));
+    else if (rc == 0) {
+        /* child process executes following code */
+
+        execlp("pnmconvol",
+               "pnmconvol", convolutionImageFilespec, inputFilespec,
+               NULL);
+
+        pm_error("error executing pnmconvol command.  errno=%d (%s)",
+                 errno, strerror(errno));
+    } else {
+        /* This is the parent */
+        pid_t const childPid = rc;
+
+        int status;
+
+        /* wait for child to finish */
+        while (wait(&status) != childPid);
+    }
+}
+
+
+
+int
+main(int argc, char ** argv) {
+
+    struct cmdlineInfo cmdline;
+    FILE * convFileP;
+    const char * tempfileName;
+
+    pnm_init(&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);
+
+    pm_close(convFileP);
+
+    if (cmdline.dump) {
+        /* We're done.  Convolution image is in user's file */
+    } else {
+        runPnmconvol(cmdline.inputFilespec, tempfileName);
+
+        unlink(tempfileName);
+        strfree(tempfileName);
+    }
+    return 0;
+}
diff --git a/editor/pnmstitch.c b/editor/pnmstitch.c
new file mode 100644
index 00000000..61f02a04
--- /dev/null
+++ b/editor/pnmstitch.c
@@ -0,0 +1,2408 @@
+/*
+ * Copyright (c) 2002 Mark Salyzyn
+ * All rights reserved.
+ *
+ * TERMS AND CONDITIONS OF USE
+ *
+ * Redistribution and use in source form, with or without modification, are
+ * permitted provided that redistributions of source code must retain the
+ * above copyright notice, this list of conditions and the following
+ * disclaimer.
+ *
+ * This software is provided `as is' by Mark Salyzyn and any express or implied
+ * warranties, including, but not limited to, the implied warranties of
+ * merchantability and fitness for a particular purpose, are disclaimed. In no
+ * event shall Mark Salyzyn be liable for any direct, indirect, incidental,
+ * special, exemplary or consequential damages (including, but not limited to,
+ * procurement of substitute goods or services; loss of use, data, or profits;
+ * or business interruptions) however caused and on any theory of liability,
+ * whether in contract, strict liability, or tort (including negligence or
+ * otherwise) arising in any way out of the use of this software, even if
+ * advised of the possibility of such damage.
+ *
+ * Any restrictions or encumberances added to this source code or derivitives,
+ * is prohibited.
+ *
+ *  Name: pnmstitch.c
+ *  Description: Automated panoramic stitcher.
+ *      Many digital Cameras have a panorama mode where they hold on to
+ *    the right hand side of an image, shifted to the left hand side of their
+ *    view screen for subsequent pictures, facilitating the manual stitching
+ *    up of a panoramic shot. These same cameras are shipped with software
+ *    to manually or automatically stitch images together into the composite
+ *    image. However, these programs are dedicated for a specific OS.
+ *    In the pnmstitch program, it analyzes the match between the images,
+ *    generates a transform, processes the transform on the images, and
+ *    blends the overlapping regions. In addition, there is an output filter
+ *    to process automatic cropping of the resultant image.
+ *      The stitching software here works by constraining the right
+ *    hand side of the right hand image as `fixed' per-se, after offset
+ *    evaluation and only the left hand side of the right hand image is
+ *    mangled. Thus, the algorithm is optimized for stitching a right hand
+ *    image to the right hand half (half being a loose term) of the left hand
+ *    image.
+ *  Author: Mark Salyzyn <mark@bohica.net>  June 2002
+ *  Version: 0.0.4
+ *
+ *  Modifications: 0.0.4  July 31 2002 Mark Salyzyn <mark@bohica.net>
+ *                                  &  Bryan Henderson <bryanh@giraffe-data.com>
+ *      - FreeBSD port.
+ *      - merge changes to incorporate into netpbm tree.
+ *  Modifications: 0.0.3  July 27 2002  Mark Salyzyn <mark@bohica.net>
+ *                                  &   "George M. Sipe" <geo@sipe.org>
+ *      - Deal with subtle differences between BSD and GNU getopt
+ *        facilitating the Linux port.
+ *  Modifications: 0.0.2  July 25 2002  Mark Salyzyn <mark@bohica.net>
+ *      - RotateSliver needs to use higher resolution match.
+ *      - RotateCrop code interfered with StraightThrough code
+ *        resulting in an incorrect pnm image output.
+ *  Modifications: 0.0.1  July 18 2002  Mark Salyzyn <mark@bohica.net>
+ *      - Added BiLinearSliver, RotateSliver and HorizontalCrop
+ *
+ *  ToDo:
+ *      - Split this into multiple files ... nah, keep it in one to
+ *        keep it from polluting the netpbm tree.
+ *      - Add and refine the videorbits algorithm. One piece of public
+ *        domain software that is pnm aware is called videorbits. It
+ *        needs considerably more overlap than the Digital Cameras set
+ *        up (of course, anyone can to a panorama with more overlap
+ *        with our without the feature) as it uses what is called video
+ *        flow to generate the match. It is designed more for a series
+ *        of images from a video camera. videorbits has three programs,
+ *        one generates the transform, the next processes the
+ *        transform, and the final blends the images together much as
+ *        this one piece program does.
+ *      - speedups in matching algorithm
+ *      - refinement in accuracy of matching algorithm
+ *      - 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
+ *        that there is no transformation effects on the left image.
+ *      - user selectable blending algorithms?
+ */
+
+#define _BSD_SOURCE 1   /* Make sure strdup() is in string.h */
+#define _XOPEN_SOURCE 500  /* Make sure strdup() is in string.h */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <limits.h>
+#include <math.h>
+
+#include "pm_c_util.h"
+#include "shhopt.h"
+#include "nstring.h"
+#include "mallocvar.h"
+#include "pam.h"
+
+/*
+ *  Structures
+ */
+
+/*
+ *  Image structure
+ */
+typedef struct {
+    const char * name;  /* File Name                */
+    struct pam   pam;   /* netpbm image description */
+    tuple **     tuple; /* in-memory copy of image  */
+} Image;
+
+/*
+ *  Output class
+ *  The following methods and data allocations are used for output filter.
+ */
+typedef struct output {
+    /* name */
+    const char * Name;
+    /* methods */
+    bool      (* Alloc)(struct output * me, const char * file,
+                        unsigned int width, unsigned int height,
+                        struct pam * prototype);
+    void      (* DeAlloc)(struct output * me);
+    tuple    *(* Row)(struct output * me, unsigned row);
+    void      (* FlushRow)(struct output * me, unsigned row);
+    void      (* FlushImage)(struct output * me);
+    /* data */
+    Image      * image;
+    void       * extra;
+} Output;
+
+extern Output OutputMethods[];
+
+/*
+ *  Stitching class
+ *  The following methods and data allocations are used for operations
+ *  surrounding stitching of an image.
+ */
+typedef struct stitcher {
+    /* name */
+    const char * Name;
+    /* methods */
+    bool      (* Alloc)(struct stitcher *me);
+    void      (* DeAlloc)(struct stitcher *me);
+    void      (* Constrain)(struct stitcher *me, int x, int y,
+                            int width, int height);
+        /* Set transformation parameter constraints.  This affects the
+           function of a future 'Match' method execution.
+        */
+    bool      (* Match)(struct stitcher *me, Image * Left, Image * Right);
+        /* Determine the transformation parameters for the stitching.
+           I.e. determine the parameters that affect future invocations
+           of the transformation methods below.  You must execute a 
+           'Match' before executing any of the transformation methods.
+        */
+    /*-----------------------------------------------------------------------
+      The transformation methods answer the question, "Which pixel in the left
+      image and which pixel in the right image contribute to the pixel at
+      Column X, Column Y of the output?
+
+      If there is no pixel in the left image that contributes to the output
+      pixel in question, the methods return column or row numbers outside
+      the bounds of the left image (possibly negative).  Likewise for the 
+      right image.
+    */
+    float     (* XLeft)(struct stitcher *me, int x, int y);
+        /* column number of the pixel from the left image */
+    float     (* YLeft)(struct stitcher *me, int x, int y);
+        /* row number of the pixel from the left image */
+    float     (* XRight)(struct stitcher *me, int x, int y);
+        /* column number of the pixel from the right image */
+    float     (* YRight)(struct stitcher *me, int x, int y);
+        /* row number of the pixel from the left image */
+    /*----------------------------------------------------------------------*/
+    /* Output methods */
+    void      (* Output)(struct stitcher *me, FILE * fp);
+    /* private data */
+    int          x, y, width, height;
+    /* For a Linear Sliver stitcher, 'x' and 'y' are simply the offset you
+       add to an output location to get the location in the right image of the
+       pixel that corresponds to that output pixel.
+    */
+    float      * parms;
+} Stitcher;
+
+extern Stitcher StitcherMethods[];
+
+/*
+ *  Prototypes
+ */
+static int pnmstitch(const char * const left,
+                     const char * const right,
+                     const char * const out,
+                     int          const x,
+                     int          const y,
+                     int          const width,
+                     int          const height,
+                     const char * const stitcher,
+                     const char * const filter);
+
+struct cmdlineInfo {
+    /*
+     * All the information the user supplied in the command line,
+     * in a form easy for the program to use.
+     */
+    const char * leftFilespec;   /* '-' if stdin */
+    const char * rightFilespec;  /* '-' if stdin */
+    const char * outputFilespec; /* '-' if stdout */
+    const char * stitcher;
+    const char * filter;
+    int          width;
+    int          height;
+    int          xrightpos;
+    int          yrightpos;
+    unsigned int verbose;
+};
+
+static char minus[] = "-";
+
+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 = malloc( 100*sizeof( optEntry ) );
+        /* Instructions to optParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+
+    char *outputOpt;
+    unsigned int widthSpec, heightSpec, outputSpec, 
+        xrightposSpec, yrightposSpec, stitcherSpec, filterSpec;
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "width",       OPT_UINT,   &cmdlineP->width, 
+            &widthSpec,         0);
+    OPTENT3(0, "height",      OPT_UINT,   &cmdlineP->height, 
+            &heightSpec,        0);
+    OPTENT3(0, "verbose",     OPT_FLAG,   NULL,                  
+            &cmdlineP->verbose, 0 );
+    OPTENT3(0, "output",      OPT_STRING, &outputOpt, 
+            &outputSpec,        0);
+    OPTENT3(0, "xrightpos",   OPT_UINT,   &cmdlineP->xrightpos, 
+            &xrightposSpec,     0);
+    OPTENT3(0, "yrightpos",   OPT_UINT,   &cmdlineP->yrightpos, 
+            &yrightposSpec,     0);
+    OPTENT3(0, "stitcher",    OPT_STRING, &cmdlineP->stitcher, 
+            &stitcherSpec,      0);
+    OPTENT3(0, "filter",      OPT_STRING, &cmdlineP->filter, 
+            &filterSpec,        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 (!widthSpec) {
+        cmdlineP->width = INT_MAX;
+    }
+    if (!heightSpec) {
+        cmdlineP->height = INT_MAX;
+    }
+    if (!xrightposSpec) {
+        cmdlineP->xrightpos = INT_MAX;
+    }
+    if (!yrightposSpec) {
+        cmdlineP->yrightpos = INT_MAX;
+    }
+    if (!stitcherSpec) {
+        cmdlineP->stitcher = "BiLinearSliver";
+    }
+    if (!filterSpec) {
+        cmdlineP->filter = "StraightThrough";
+    }
+
+    if (argc-1 > 3) {
+        pm_error("Program takes at most three arguments: left, right, and "
+                 "output file specifications.  You specified %d", argc-1);
+        /* NOTREACHED */
+    } else {
+        if (argc-1 == 0) {
+            cmdlineP->leftFilespec = minus;
+            cmdlineP->rightFilespec = minus;
+        } else if (argc-1 == 1) {
+            cmdlineP->leftFilespec = minus;
+            cmdlineP->rightFilespec = argv[1];
+        } else {
+            cmdlineP->leftFilespec = argv[1];
+            cmdlineP->rightFilespec = argv[2];
+        }
+        if (argc-1 == 3 && outputSpec) {
+            pm_error("You cannot specify --output and also name the "
+                     "output file with the 3rd argument.");
+            /* NOTREACHED */
+        } else if (argc-1 == 3) {
+            cmdlineP->outputFilespec = argv[3];
+        } else if (outputSpec) {
+            cmdlineP->outputFilespec = outputOpt;
+        } else {
+            cmdlineP->outputFilespec = minus;
+        }
+    }
+} /* parseCommandLine() - end */
+
+static int  verbose;
+
+/*
+ *  Parse the command line, call pnmstitch to perform work.
+ */
+int
+main (int argc, char **argv)
+{
+    struct cmdlineInfo cmdline;
+
+    pnm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    verbose = cmdline.verbose;
+
+    return pnmstitch (cmdline.leftFilespec, 
+                      cmdline.rightFilespec, 
+                      cmdline.outputFilespec, 
+                      cmdline.xrightpos, 
+                      cmdline.yrightpos, 
+                      cmdline.width, 
+                      cmdline.height, 
+                      cmdline.stitcher, 
+                      cmdline.filter);
+} /* main() - end */
+
+
+
+/*
+ *  allocate a clear image structure.
+ */
+static Image *
+allocate_image(void)
+{
+    Image * retVal;
+    
+    MALLOCVAR(retVal);
+
+    if (retVal != NULL) 
+        memset (retVal, 0, (unsigned)sizeof(Image));
+
+    return retVal;
+}
+
+
+
+/*
+ *  free an image structure.
+ */
+static void
+free_image(Image * image)
+{
+    if (image->name) {
+        strfree(image->name);
+        image->name = NULL;     
+    }
+    if (image->tuple) {
+        pnm_freepamarray(image->tuple, &image->pam);
+        image->tuple = NULL; 
+    }
+    if (image->pam.file) {
+        fclose (image->pam.file);
+        image->pam.file = NULL;
+    }
+    free (image);
+}
+
+
+
+static void
+openWithPossibleExtension(const char *  const baseName,
+                          FILE **       const ifPP,
+                          const char ** const filenameP) {
+
+    /* list of possible extensions for input file */
+    const char * const extlist[] = { 
+        "", ".pnm", ".pam", ".pgm", ".pbm", ".ppm" 
+    };
+
+    FILE * ifP;
+    unsigned int extIndex;
+
+    ifP = NULL;   /* initial value -- no file opened yet */
+
+    for (extIndex = 0; extIndex < ARRAY_SIZE(extlist) && !ifP; ++extIndex) {
+        
+        const char * trialName;
+        
+        asprintfN(&trialName, "%s%s", baseName, extlist[extIndex]);
+        
+        ifP = fopen(trialName, "rb");
+        
+        if (ifP)
+            *filenameP = trialName;
+        else
+            strfree(trialName);
+    }
+    if (!ifP) 
+        pm_error ("Failed to open input file named '%s' "
+                  "or '%s' with one of various common extensions.", 
+                  baseName, baseName);
+    
+    *ifPP = ifP;
+}
+
+
+
+/*
+ *  Create an image object for the PNM/PAM image in the file name 'name'
+ *  This includes reading the entire image.
+ */
+static Image *
+readinit(const char * const name)
+{
+    Image * const image = allocate_image();
+    Image * retVal;
+
+    if (image == NULL) 
+        retVal = NULL;
+    else {
+        FILE * ifP;
+
+        if (strcmp(name,minus) == 0) {
+            ifP = stdin;
+            image->name = strdup("<stdin>");
+        } else {
+            openWithPossibleExtension(name, &ifP, &image->name);
+        }
+        image->tuple = pnm_readpam(ifP, &(image->pam), 
+                                   PAM_STRUCT_SIZE(tuple_type));
+        fclose (ifP);
+        image->pam.file = NULL;
+
+        if (image->tuple == NULL) {
+            free_image(image);
+            retVal = NULL;
+        } else
+            retVal = image;
+    }
+    return retVal;
+} /* readinit() - end */
+
+/*
+ *  Prepare an image to be output.
+ *      Too bad we can't help them and add a .pnm on the filename,
+ *      since this would be `bad'. on readinit we can check if the .pnm
+ *      needs to be added ...
+ */
+static bool
+writeinit(Image * image)
+{
+    if (strcmp(image->name,minus) == 0) {
+        image->pam.file = stdout;
+        strfree(image->name);
+        image->name = strdup("<stdout>");
+    } else {
+        image->pam.file = pm_openw(image->name);
+    }
+    return TRUE;
+} /* writeinit() - end */
+
+/*
+ *  Compare a subimage to an image.
+ *  The most time consuming actions surround this subroutine.
+ *  Return the magnitude of the difference between the specified region
+ *  in image 'left' and the specified region in image 'right'.
+ *  The magnitude is defined as the sum of the squares of the differences
+ *  between intensities of each of the 3 colors over all the pixels.
+ *
+ *  The region in the left image is the rectangle with top left corner at
+ *  Column 'lx', Row 'ly', with dimensions 'width' columns by 'height'
+ *  rows.  The region in the right image is a rectangle the same dimensions
+ *  with upper left corner at Column 'rx', Row 'ry'.
+ *
+ *  Caller must ensure that the regions indicated are entirely within the
+ *  their respective images.
+ */
+static unsigned long
+regionDifference(Image * left,
+                 int     lx,
+                 int     ly,
+                 Image * right,
+                 int     rx,
+                 int     ry,
+                 int     width,
+                 int     height)
+{
+    unsigned long total;
+    unsigned      row;
+    
+    total = 0;  /* initial value */
+
+    for (row = 0; row < height; ++row) {
+        unsigned column;
+
+        for (column = 0; column < width; ++column) {
+            unsigned plane;
+
+            for (plane = 0; plane < left->pam.depth; ++plane) {
+                sample const leftSample = left->tuple[row][column][plane];
+                sample const rightSample = right->tuple[row][column][plane];
+                total += SQR(leftSample - rightSample);
+            }
+        }
+    }
+    return total;
+}
+
+
+
+/*
+ *  Generate a recent sorted histogram of best matches.
+ */
+typedef struct {
+    unsigned long total;
+    int           x;
+    int           y;
+} Best;
+/* Arbitrary, except 9 points surrounding a hot spot plus one layer more */
+#define NUM_BEST 9
+
+/*
+ *  Allocate the Best structure.
+ */
+static Best *
+allocate_best(void)
+{
+    Best * retVal;
+
+    MALLOCARRAY(retVal, NUM_BEST);
+
+    if (retVal != NULL) {
+        unsigned int    i;
+        for (i = 0; i < NUM_BEST; ++i) {
+            retVal[i].total = ULONG_MAX;
+            retVal[i].x = INT_MAX;
+            retVal[i].y = INT_MAX;
+        }
+    }
+    return retVal;
+}
+
+/*
+ *  Free the Best structure
+ */
+#define free_best(best) free(best)
+
+/*
+ *  Placement helper for the Best structure.
+ */
+static void
+update_best(Best * best, unsigned long total, int x, int y)
+{
+    int i;
+    if (best[NUM_BEST-1].total <= total) {
+        return;
+    }
+    for (i = NUM_BEST - 1; i > 0; --i) {
+        if (best[i-1].total < total) {
+            break;
+        }
+        best[i] = best[i-1];
+    }
+    best[i].total = total;
+    best[i].x = x;
+    best[i].y = y;
+}
+
+/*
+ *  Print helper for the Best structure.
+ */
+static void
+pr_best(Best * const best)
+{
+    int i;
+    if (best != (Best *)NULL)
+    for (i = 0; i < NUM_BEST; ++i) {
+        fprintf (stderr, " (%d,%d)%lu",
+          best[i].x, best[i].y, best[i].total);
+    }
+} /* pr_best() - end */
+
+static void *
+findObject(const char * const name, void * start, unsigned size)
+{
+    char  ** object;
+    char  ** best;
+    unsigned length;
+
+    if (name == (char *)NULL) {
+        return start;
+    }
+    for (length = 0, best = (char **)NULL, object = (char **)start;
+      *object != (char *)NULL;
+      object = (char **)(((char *)object) + size)) {
+        const char * np      = name;
+        char       * op      = *object;
+        unsigned     matched = 0;
+
+        /* case insensitive match */
+        while ((*np != '\0') && (*op != '\0')
+         && ((*np == *op) || (tolower(*np) == tolower(*op)))) {
+            ++np;
+            ++op;
+            ++matched;
+        }
+        if ((*np == '\0') && (*op == '\0')) {
+            break;
+        }
+        if ((matched >= length) && (*np == '\0')) {
+            if (matched == length) {
+                best = (char **)NULL;
+            } else {
+                best = object;
+            }
+            length = matched;
+        }
+    }
+    if (*object == (char *)NULL) {
+        object = best;
+    }
+    if (object == (char **)NULL) {
+        fprintf (stderr,
+          "Unknown driver \"%s\". available drivers are:\n", name);
+        for (object = (char **)start;
+          *object != (char *)NULL;
+          object = (char **)(((char *)object) + size)) {
+            fprintf (stderr, "\t%s%s\n", *object,
+              (object == (char **)start) ? " (default)" : "");
+        }
+    }
+    return object;
+}
+
+/*
+ *  The general wrapper for both the Output and the Stitcher algorithms.
+ */
+
+/* Determine the mask corners for both Left and Right images */
+static void
+determineMaskCorners(Stitcher * Stitch,
+                     Image    * Left,
+                     Image    * Right,
+                     int        xp[],
+                     int        yp[])
+{
+    int i;
+
+    xp[0] = xp[1] = xp[4] = xp[5] = 0;
+    yp[0] = yp[2] = yp[4] = yp[6] = 0;
+    xp[2] = xp[3] = Left->pam.width;
+    yp[1] = yp[3] = Left->pam.height;
+    xp[6] = xp[7] = Right->pam.width;
+    yp[5] = yp[7] = Right->pam.height;
+    for (i = 0; i < 8; ++i) {
+        int     x, y, xx, yy, count = 65536; /* max iterations */
+        float (*X)(Stitcher *me, int x, int y);
+        float (*Y)(Stitcher *me, int x, int y);
+
+        if (i < 4) {
+            X = Stitch->XLeft;
+            Y = Stitch->YLeft;
+        } else {
+            X = Stitch->XRight;
+            Y = Stitch->YRight;
+        }
+        x = xp[i];
+        y = yp[i];
+        /* will not work if rotated 90o or if gain > 10 */
+        do {
+            xx = ((*X)(Stitch, xp[i], yp[i]) + 0.5) - x;
+            if (xx < 0) {
+                if (xx > -100) {
+                    ++xp[i];
+                } else {
+                    xp[i] -= xx / 10;
+                }
+            } else if (xx > 0) {
+                if (xx < 100) {
+                    --xp[i];
+                } else {
+                    xp[i] -= xx / 10;
+                }
+            }
+            yy = ((*Y)(Stitch, xp[i], yp[i]) + 0.5) - y;
+            if (yy < 0) {
+                if (yy > -100) {
+                    ++yp[i];
+                } else {
+                    yp[i] -= yy / 10;
+                }
+            } else if (yy > 0) {
+                if (yy < 100) {
+                    --yp[i];
+                } else {
+                    yp[i] -= yy / 10;
+                }
+            }
+        } while (((xx != 0) || (yy != 0)) && (--count != 0));
+    }
+    if (verbose) {
+        (*(Stitch->Output))(Stitch, stderr);
+        if (verbose > 2) {
+            static char quotes[] = "'\0\0\"\0\0'\"\0\"\"";
+            fprintf (stderr, " Left:");
+            for (i = 0; i < 8; ++i) {
+                if (i == 4) {
+                    fprintf (stderr, "\n Right:");
+                }
+                fprintf (stderr, " x%s,y%s=%d,%d",
+                  &quotes[(i%4)*3], &quotes[(i%4)*3],
+                  xp[i], yp[i]);
+            }
+        }
+    }
+} /* determineMaskCorners() - end */
+
+static void
+calculateXyWidthHeight(int         xp[],
+                       int         yp[],
+                       int * const xP,
+                       int * const yP,
+                       int * const widthP,
+                       int * const heightP)
+{
+    int x, y, width, height, i;
+
+    /* Calculate generic x,y left top corner, and the width and height */
+    x = xp[0];
+    y = yp[0];
+    width = height = 0;
+    for (i = 1; i < 8; ++i) {
+        if (xp[i] < x) {
+            width += x - xp[i];
+            x = xp[i];
+        } else if ((x + width) < xp[i]) {
+            width = xp[i] - x;
+        }
+        if (yp[i] < y) {
+            height += y - yp[i];
+            y = yp[i];
+        } else if ((y + height) < yp[i]) {
+            height = yp[i] - y;
+        }
+    }
+    *xP = x; *yP = y;
+    *widthP = width; *heightP = height;
+} /* calculateXyWidthHeight() - end */
+
+static void
+printPlan(int xp[], int yp[], Image * Left, Image * Right)
+{
+    /* Calculate Left image transformed bounds */
+    int X, Y, W, H, i;
+
+    X = xp[0];
+    Y = yp[0];
+    W = H = 0;
+    for (i = 1; i < 4; ++i) {
+        if (xp[i] < X) {
+            W += X - xp[i];
+            X = xp[i];
+        } else if ((X + W) < xp[i]) {
+            W = xp[i] - X;
+        }
+        if (yp[i] < Y) {
+            H += Y - yp[i];
+            Y = yp[i];
+        } else if ((Y + H) < yp[i]) {
+            H = yp[i] - Y;
+        }
+    }
+    fprintf (stderr,
+      "%s[%u,%u=>%d,%d](%d,%d)",
+      Left->name, Left->pam.width, Left->pam.height,
+      W, H, X, Y);
+    X = xp[i];
+    Y = yp[i];
+    W = H = 0;
+    for (++i; i < 8; ++i) {
+        if (xp[i] < X) {
+            W += X - xp[i];
+            X = xp[i];
+        } else if ((X + W) < xp[i]) {
+            W = xp[i] - X;
+        }
+        if (yp[i] < Y) {
+            H += Y - yp[i];
+            Y = yp[i];
+        } else if ((Y + H) < yp[i]) {
+            H = yp[i] - Y;
+        }
+    }
+    fprintf (stderr,
+      "+%s[%u,%u=>%d,%d](%d,%d)",
+      Right->name, Right->pam.width, Right->pam.height,
+      W, H, X, Y);
+} /* printPlan() - end */
+
+
+
+static void
+stitchOnePixel(Image *    const Left,
+               Image *    const Right,
+               struct pam const outpam,
+               int        const row,
+               int        const column,
+               int        const y,
+               int        const right_row,
+               int        const right_column,
+               unsigned * const firstRightP,
+               tuple      const outPixel) {
+               
+    unsigned plane;
+
+    for (plane = 0; plane < outpam.depth; ++plane) {
+        sample leftPixel, rightPixel;
+        /* Left `mix' is easy to find */
+        leftPixel = (column < Left->pam.width)
+                    ? (y < 0)
+                        ? ((row < -y) || (row >= (Left->pam.height - y)))
+                           ? 0
+                           : Left->tuple[row + y][column][plane]
+                        : (row < Left->pam.height)
+                          ? Left->tuple[row][column][plane]
+                          : 0
+                     : 0;
+        rightPixel = 0;
+        if (right_column >= 0) {
+            rightPixel = Right->tuple[right_row][right_column][plane];
+            if ((rightPixel > 0) && (*firstRightP == 0)) 
+                *firstRightP = column;
+        }
+        if (leftPixel == 0) {
+            leftPixel = rightPixel;
+        } else if ((*firstRightP <= column)
+                   && (column < Left->pam.width)
+                   && (rightPixel > 0)) {
+            /* blend 7/8 over half of stitch */
+            int const w = Left->pam.width - *firstRightP;
+            if (column < (*firstRightP + w/2)) {
+                int const v = (w * 4) / 7;
+                leftPixel = (sample)(
+                    ((leftPixel
+                      * (unsigned long)(*firstRightP + v - column))
+                     + (rightPixel
+                        * (unsigned long)(column - *firstRightP)))
+                    / (unsigned long)v);
+            } else {
+                int const v = w * 4;
+                leftPixel = (sample)(
+                    ((leftPixel 
+                      * (unsigned long)(Left->pam.width - column))
+                     + (rightPixel
+                        * (unsigned long)(column - Left->pam.width + v)))
+                    / (unsigned long)v);
+            }
+        }
+        outPixel[plane] = leftPixel;
+    }
+}
+
+
+
+static void
+stitchOneRow(Image *    const Left,
+             Image *    const Right,
+             Output *   const Out,
+             Stitcher * const Stitch,
+             int        const row,
+             int        const y) {
+
+    /*
+     *  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
+     * 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
+     * storage needs. The algorithm below is complicated most by
+     * the blending determinations, overlapping a left untransformed
+     * image with a right transformed image with a black background is
+     * all that remains.
+     */
+    /*
+     * Normalize transformation against origin, the
+     * transformation algorithm was in reference to the right
+     * hand side of the left hand image before.
+     */
+    tuple * const Row = (*(Out->Row))(Out,row);
+
+    unsigned column, firstRight;
+
+    firstRight = 0;  /* initial value */
+
+    for (column = 0; column < Out->image->pam.width; ++column) {
+        int right_row, right_column;
+
+        right_row = -1;
+        right_column = (*(Stitch->XRight))(Stitch, column,
+                                           (y < 0) ? (row + y) : row) + 0.5;
+        if ((0 <= right_column)
+            && (right_column < Right->pam.width)) {
+            right_row = (*(Stitch->YRight))(Stitch, column,
+                                            (y < 0) ? (row + y) : row) + 0.5;
+            if ((right_row < 0)
+                || (Right->pam.height <= right_row)) {
+                right_column = -1;
+                right_row    = -1;
+            }
+        } else 
+            right_column = -1;
+
+        /* Create the pixel at column 'column' of row 'row' of the
+           output 'Out': Row[column].
+        */
+        stitchOnePixel(Left, Right, Out->image->pam, row, column, y, 
+                       right_row, right_column, &firstRight, Row[column]);
+    }
+}
+
+
+
+static void 
+stitchit(Image *      const Left, 
+         Image *      const Right, 
+         const char * const outfilename,
+         const char * const filter,
+         Stitcher *   const Stitch,
+         int *        const retvalP) {
+
+    Output * const Out = findObject(filter, &OutputMethods[0],
+                                    sizeof(OutputMethods[0]));
+    unsigned   row;
+    int        xp[8], yp[8], x, y, width, height;
+    
+    if ((Out == (Output *)NULL) || (Out->Name == (char *)NULL)) 
+        *retvalP = -2;
+    else {
+        if (verbose)
+            fprintf (stderr, "Selected %s output filter algorithm\n",
+                     Out->Name);
+
+        /* Determine the mask corners for both Left and Right images */
+        determineMaskCorners(Stitch, Left, Right, xp, yp);
+
+        /* Output the combined images */
+                
+        /* Calculate generic x,y left top corner, and the width and height */
+        calculateXyWidthHeight(xp, yp, &x, &y, &width, &height);
+                
+        if (verbose) 
+            printPlan(xp, yp, Left, Right);
+    
+        if (!(*(Out->Alloc))(Out, outfilename, width, height, &Left->pam))
+            *retvalP = -9;
+        else {
+            if (verbose) {
+                fprintf (stderr,
+                         "=%s[%u,%u=>%d,%d](%d,%d)\n",
+                         Out->image->name, Out->image->pam.width,
+                         Out->image->pam.height, width, height, x, y);
+            }
+            for (row = 0; row < Out->image->pam.height; row++) {
+                /* Generate row number 'row' of the output image 'Out' */
+                stitchOneRow(Left, Right, Out, Stitch, row, y);
+                (*(Out->FlushRow))(Out,row);
+            }
+            (*(Out->FlushImage))(Out);
+            (*(Out->DeAlloc))(Out);
+        
+            *retvalP = 0;
+        }
+    }
+}
+
+
+
+static int
+pnmstitch(const char * const leftfilename,
+          const char * const rightfilename,
+          const char * const outfilename,
+          int          const reqx,
+          int          const reqy,
+          int          const reqWidth,
+          int          const reqHeight,
+          const char * const stitcher,
+          const char * const filter)
+{
+    Stitcher * const Stitch = findObject(stitcher, &StitcherMethods[0],
+                                         sizeof(StitcherMethods[0]));
+    Image    * Left;
+    Image    * Right;
+    int        retval;
+
+    if ((Stitch == (Stitcher *)NULL) || (Stitch->Name == (char *)NULL)) 
+        retval = -1;
+    else {
+        if (verbose) 
+            fprintf (stderr, "Selected %s stitcher algorithm\n",
+                     Stitch->Name);
+
+        /* Left hand image read into memory */
+        Left = readinit(leftfilename);
+        if (Left == NULL)
+            retval = -3;
+        else {
+            /* Right hand image read into memory */
+            Right = readinit(rightfilename);
+            if (Right == NULL)
+                retval = -4;
+            else {
+                if (Left->pam.depth != Right->pam.depth) {
+                    fprintf(stderr, "Images should have matching depth.  "
+                            "The left image has depth %d, "
+                            "while the right has depth %d.", 
+                            Left->pam.depth, Right->pam.depth);
+                    retval = -5;
+                } else if (Left->pam.maxval != Right->pam.maxval) {
+                    fprintf (stderr,
+                             "Images should have matching maxval.  "
+                             "The left image has maxval %u, "
+                             "while the right has maxval %u.",
+                             (unsigned)Left->pam.maxval, 
+                             (unsigned)Right->pam.maxval);
+                    retval = -6;
+                } else if ((*(Stitch->Alloc))(Stitch) == FALSE) 
+                    retval = -7;
+                else {
+                    (*(Stitch->Constrain))(Stitch, reqx, reqy, 
+                                           reqWidth, reqHeight);
+
+                    if ((*(Stitch->Match))(Stitch, Left, Right) == FALSE) 
+                        retval = -8;
+                    else 
+                        stitchit(Left, Right, outfilename, filter, Stitch, 
+                                 &retval);
+                }
+                free_image(Right);
+            }
+            free_image(Left);
+        }
+    }
+    return retval;
+}
+
+
+
+/* Output Methods */
+
+/* Helper methods */
+
+static void
+OutputDeAlloc(Output * me)
+{
+    if (me->image != (Image *)NULL) {
+        /* Free up resources */
+        free_image (me->image);
+        me->image = (Image *)NULL;
+    }
+    if (me->extra != (void *)NULL) {
+        free (me->extra);
+        me->extra = (void *)NULL;
+    }
+} /* OutputDeAlloc() - end */
+
+static bool
+OutputAlloc(Output     * const me,
+            const char * const file,
+            unsigned int const width,
+            unsigned int const height,
+            struct pam * const prototype)
+{
+    /* Output the combined images */
+    me->extra = (void *)NULL;
+    me->image = allocate_image();
+    if (me->image == (Image *)NULL) {
+        return FALSE;
+    }
+    me->image->pam = *prototype;
+    me->image->pam.width = width;
+    me->image->pam.height = height;
+    /* Give the output a name */
+    me->image->name = strdup(file);
+    /* Initialize output arrays */
+    if (writeinit(me->image) == FALSE) {
+        OutputDeAlloc(me);
+        return FALSE;
+    }
+    return TRUE;
+} /* OutputAlloc() - end */
+
+/* StraightThrough output method */
+
+static void
+StraightThroughDeAlloc(Output * me)
+{
+    /* Trick the proper freeing of resouces on the Output Image */
+    me->image->pam.height = 1;
+    OutputDeAlloc(me);
+} /* StraightThroughDeAlloc() - end */
+
+static bool
+StraightThroughAlloc(Output     * const me,
+                     const char * const file,
+                     unsigned int const width,
+                     unsigned int const height,
+                     struct pam * const prototype)
+{
+    if (OutputAlloc(me, file, width, height, prototype) == FALSE) {
+        StraightThroughDeAlloc(me);
+    }
+    /* Trick the proper allocation of resouces on the Output Image */
+    me->image->pam.height = 1;
+    me->image->tuple = pnm_allocpamarray(&me->image->pam);
+    if (me->image->tuple == (tuple **)NULL) {
+        StraightThroughDeAlloc(me);
+        return FALSE;
+    }
+    me->image->pam.height = height;
+    pnm_writepaminit(&me->image->pam);
+    return TRUE;
+} /* StraightThroughAlloc() - end */
+
+static tuple *
+StraightThroughRow(Output * me, unsigned row)
+{
+    UNREFERENCED_PARAMETER(row);
+    return me->image->tuple[0];
+} /* StraightThroughRow() - end */
+
+static void
+StraightThroughFlushRow(Output * me, unsigned row)
+{
+    UNREFERENCED_PARAMETER(row);
+    if (me->image != (Image *)NULL) {
+        pnm_writepamrow(&me->image->pam, me->image->tuple[0]);
+    }
+} /* StraightThroughFlushRow() - end */
+
+static void
+StraightThroughFlushImage(Output * me)
+{
+    UNREFERENCED_PARAMETER(me);
+} /* StraightThroughFlushImage() - end */
+
+/* Horizontal Crop output method */
+
+#define HorizontalCropDeAlloc StraightThroughDeAlloc
+
+typedef struct {
+    int state;
+    int lostInSpace;
+} HorizontalCropExtra;
+
+static bool
+HorizontalCropAlloc(Output     * const me,
+                    const char * const file,
+                    unsigned int const width,
+                    unsigned int const height,
+                    struct pam * const prototype)
+{
+    unsigned long pos;
+
+    if (StraightThroughAlloc(me, file, width, height, prototype) == FALSE) {
+        return FALSE;
+    }
+    me->extra = (void *)malloc(sizeof(HorizontalCropExtra));
+    if (me->extra == (void *)NULL) {
+        HorizontalCropDeAlloc(me);
+        return FALSE;
+    }
+    memset (me->extra, 0, sizeof(HorizontalCropExtra));
+    /* Test if we can seek, important since we rewrite the header */
+    pos = ftell(me->image->pam.file);
+    if ((fseek(me->image->pam.file, 1L, SEEK_SET) != 0)
+     || (ftell(me->image->pam.file) != 1L)) {
+        fprintf (stderr, "%s needs to output to a seekable entity\n",
+          me->Name);
+    }
+    (void)fseek(me->image->pam.file, pos, SEEK_SET);
+    return TRUE;
+} /* HorizontalCropAlloc() - end */
+
+#define HorizontalCropRow StraightThroughRow
+
+static void
+HorizontalCropFlushRow(Output * me, unsigned row)
+{
+    unsigned column;
+    unsigned threshold;
+#   define HorizontalCropThreshold 4
+    UNREFERENCED_PARAMETER(row);
+
+    if (me->image == (Image *)NULL) {
+        return;
+    }
+    if (((HorizontalCropExtra *)(me->extra))->state == 2) {
+        ((HorizontalCropExtra *)(me->extra))->lostInSpace++;
+        return;
+    }
+    /* Any pitch black pixels? */
+    threshold = HorizontalCropThreshold;
+    for (column = 0; column < me->image->pam.width; ++column) {
+        unsigned plane = 0;
+        while (me->image->tuple[0][column][plane] == (sample)0) {
+            if (++plane >= me->image->pam.depth) {
+                if (--threshold == 0) {
+                    if (((HorizontalCropExtra *)(me->extra))->state == 1) {
+                        ((HorizontalCropExtra *)(me->extra))->state = 2;
+                    }
+                    ((HorizontalCropExtra *)(me->extra))->lostInSpace++;
+                    return;
+                }
+            }
+        }
+        if (plane < me->image->pam.depth) {
+            threshold = HorizontalCropThreshold;
+        }
+    }
+    ((HorizontalCropExtra *)(me->extra))->state = 1;
+    pnm_writepamrow(&me->image->pam, me->image->tuple[0]);
+} /* HorizontalCropFlushRow() - end */
+
+static void
+HorizontalCropFlushImage(Output * me)
+{
+    me->image->pam.height -= ((HorizontalCropExtra *)(me->extra))->lostInSpace;
+    if (verbose) {
+        fprintf (stderr, "%s has set image size to %d x %d\n",
+          me->Name, me->image->pam.width, me->image->pam.height);
+    }
+    if (fseek(me->image->pam.file, 0L, SEEK_SET) == 0) {
+        pnm_writepaminit(&me->image->pam);
+    } else {
+        fprintf (stderr,
+          "%s failed to seek to beginning to rewrite the header\n",
+          me->Name);
+    }
+} /* HorizontalCropFlushImage() - end */
+
+/* Rotate Crop output method */
+
+#define RotateCropDeAlloc OutputDeAlloc
+
+static bool
+RotateCropAlloc(Output     * const me,
+                const char * const file,
+                unsigned int const width,
+                unsigned int const height,
+                struct pam * const prototype)
+{
+    if (OutputAlloc(me, file, width, height, prototype) == FALSE) {
+        RotateCropDeAlloc(me);
+    }
+    me->image->tuple = pnm_allocpamarray(&me->image->pam);
+    if (me->image->tuple == (tuple **)NULL) {
+        RotateCropDeAlloc(me);
+        return FALSE;
+    }
+    return TRUE;
+} /* RotateCropAlloc() - end */
+
+static tuple *
+RotateCropRow(Output * me, unsigned row)
+{
+    return me->image->tuple[row];
+} /* RotateCropRow() - end */
+
+static void
+RotateCropFlushRow(Output * me, unsigned row)
+{
+    UNREFERENCED_PARAMETER(me);
+    UNREFERENCED_PARAMETER(row);
+} /* RotateCropFlushRow() - end */
+
+/*
+ *  Algorithm under construction.
+ *
+ */
+static void
+RotateCropFlushImage(Output * me)
+{
+    /* Cop Out for now ... */
+    pnm_writepam(&me->image->pam, me->image->tuple);
+} /* RotateCropFlushImage() - end */
+
+/* Output Method Table */
+
+Output OutputMethods[] = {
+    { "StraightThrough", StraightThroughAlloc, StraightThroughDeAlloc,
+      StraightThroughRow, StraightThroughFlushRow,
+      StraightThroughFlushImage },
+    { "HorizontalCrop", HorizontalCropAlloc, HorizontalCropDeAlloc,
+      HorizontalCropRow, HorizontalCropFlushRow, HorizontalCropFlushImage },
+    { "RotateCrop (unimplemented)", RotateCropAlloc, RotateCropDeAlloc,
+      RotateCropRow, RotateCropFlushRow, RotateCropFlushImage },
+    { (char *)NULL }
+};
+
+/* Stitcher Methods */
+
+/* These names are for the 8 parameters of a stitch, in any of the 3
+   methods this program presently implements.  Each is a subscript in
+   the parms[] array for the Stitcher object that represents a linear
+   stitching method.  
+   
+   There are also other sets of names for the 8 parameters, such as
+   Rotate_a.  I don't know why.  Maybe historical.
+*/
+
+#define Sliver_A   0
+#define Sliver_B   1
+#define Sliver_C   2
+#define Sliver_D   3
+#define Sliver_xp  4
+#define Sliver_yp  5
+#define Sliver_xpp 6
+#define Sliver_ypp 7
+
+/* Linear Stitcher Methods */
+
+static void
+LinearDeAlloc(Stitcher * me)
+{
+    if (me->parms != (float *)NULL) {
+        free (me->parms);
+        me->parms = (float *)NULL;
+    }
+} /* LinearDeAlloc() - end */
+
+static bool
+LinearAlloc(Stitcher * me)
+{
+    bool retval;
+
+    MALLOCARRAY(me->parms, 8);
+    if (me->parms == NULL) 
+        retval = FALSE;
+    else {
+        /* Constraints unset */
+        me->x = INT_MAX;
+        me->y = INT_MAX;
+        me->width = INT_MAX;
+        me->height = INT_MAX;
+        /* Unity transform matrix */
+        me->parms[Sliver_A]   = 1.0;
+        me->parms[Sliver_B]   = 0.0;
+        me->parms[Sliver_C]   = 0.0;
+        me->parms[Sliver_D]   = 0.0;
+        me->parms[Sliver_xp]  = 0.0;
+        me->parms[Sliver_yp]  = 1.0;
+        me->parms[Sliver_xpp] = 0.0;
+        me->parms[Sliver_ypp] = 0.0;
+        retval = TRUE;
+    }
+    return retval;
+}
+
+
+
+static void
+LinearConstrain(Stitcher * me, int x, int y, int width, int height)
+{
+    me->x = x;
+    me->y = y;
+    me->width = width;
+    me->height = height;
+} /* LinearConstrain() - end */
+
+/*
+ *  First pass is to find an approximate match. To do so, we take a
+ *  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.
+ *
+ *  Blind alleys:
+ *      - reduced resolution can match in totally wrong regions,
+ *        as such it can not be used to improve the speed by
+ *        getting close, then fine tuning at full resolution.
+ *      - vector (color) average of sample matched to running
+ *        vector average on left image in an attempt to improve
+ *        positional accuracy of a reduced resolution image
+ *        produced even more artifacts.
+ *      - A complete boxed sliver did not find a minima, as for
+ *        too large or too small of a square sample. heuristics
+ *        show that it works between 1/128 to 1/16 of the total
+ *        image dimension. Smaller, of course, improves speed,
+ *        but has the possibility of less accuracy.
+ *
+ *  Transformation parameters
+ *      x=x.+a
+ *      y=y'+b
+ *  Where x,y represents the original point, and x.,y.
+ *  represents the transformed point. Thus:
+ *
+ * Transformed image:
+ * ((x'+x")/2,(y'+y"-H)/2)               ((x'+x"+2W)/2,(y'+y"-H)/2)
+ * ((x'+x")/2,(y'+y"+H)/2)               ((x'+x"+2W)/2,(y'+y"+H)/2
+ *
+ * Corresponding to Original (dot) image:
+ * (0,0)                 (Right->pam.width,0)
+ * (0,Right->pam.height) (Right->pam.width,Right->pam.height)
+ *
+ *  Our matching data points are centered on x.=width/2, and
+ * scan for transformation results with a variety of y. values:
+ *  x=a*width/2+by.+c*width*y./2+d
+ *  y=e*width/2+fy.+g*width*y./2+h
+ * we set:
+ *  A=b+c*width/2
+ *  B=a*width/2+d
+ *  C=f+g*width/2
+ *  D=e*width/2+h
+ * thus simplifying to:
+ *  x=Ay.+B
+ *  y=Cy.+D
+ * adding in a weighting factor of w, the error equation is:
+ *   2                       2           2
+ *  E(A,B,C,D)=w * ((Ay.+B-x) + (Cy.+D-y))
+ * thus
+ *    2
+ *  dE(A)=2wy.(Ay.+B-x) => 0=A{wy.y. + B{wy. - {wy.x
+ *    2
+ *      dE(B)=2w(Ay.+B-x)   => 0=A{wy.   + B{w   - {wx
+ *      A=({wy.x{w-{wx{wy.)/({wy.y.{w-{wy.{wy.)
+ *      B=({wx-A{wy.)/{w
+ * and
+ *    2
+ *      dE(C)=2wy.(Cy.+D-y) => 0=C{wy.y. + D{wy. - {wy.y
+ *    2
+ *      dE(D)=2w(Cy.+D-y)   => 0=C{wy.   + D{w   - {wy
+ *      C=({wy.y{w-{wy{wy.)/({wy.y.{w-{wy.{wy.)
+ *      D=({wy-C{wy.)/{w
+ * requiring us to collect:
+ *   {wy.x=sumydotx
+ *     {wx=sumx
+ *        {wy.=sumydot
+ *      {wy.y.=sumydotydot
+ *          {w=sum
+ *       {wy.y=sumydoty
+ *         {wy=sumy
+ * Once we have A, B, C and D, we can calculate the x',y' and x",y"
+ * values as follows (based on geometric interpolation from the above
+ * constraints):
+ *  x'=AH/2+B-AHW/(2W-width)
+ *  y'=CH/2+D-CHW/(2W-width)
+ *  x"=AH/2+B+AHW/(2W-width)
+ *  y"=CH/2+D+CHW/(2W-width)
+ * These two points can be used either in the Linear or the BiLinear to
+ * establish a transform.
+ */
+
+#define IMAGE_PORTION 64
+#define SKIP_SLIVER   1
+
+/* Following global variables are for use by SliverMatch() */
+static unsigned long starPeriod;
+    /* The number of events between printing of a "*" progress
+       indicator.  
+    */
+static unsigned long starCount;
+    /* The number of events until the next * progress indicator needs to be
+       printed.
+    */
+
+static void
+starEvent() {
+    
+    if (--starCount == 0) {
+        starCount = starPeriod;
+        fprintf (stderr, "*");
+    }
+}
+
+
+static void
+starInit(unsigned long const period) {
+    starPeriod = period;
+    starCount = period;
+}
+
+
+static void
+starResetPeriod(unsigned long const period) {
+    starPeriod = period;
+    if (starCount > period)
+        starCount = period;
+}
+
+static void
+findBestMatches(Image *  const Left,
+                Image *  const Right,
+                int      const x,
+                int      const y,
+                int      const width,
+                int      const height,
+                int      const offY,
+                unsigned const Xmin,
+                unsigned const Xmax,
+                int      const Ymin,
+                int      const Ymax,
+                Best           best[NUM_BEST]) { 
+/*----------------------------------------------------------------------------
+  Compare the rectangle 'width' columns by 'height' rows with upper
+  left corner at Column 'x', Row y+offY in image 'Right' to a bunch of
+  rectangles of the same size in image 'Left' and generate a list of the
+  rectangles in 'Left' that best match the one in 'Right'.
+
+  The specific rectangles in 'Left' we examine are those with upper left
+  corner (X,Y+offY) where X is in [Xmin, Xmax) and Y is in [Ymin, Ymax).
+
+  We return the ordered list of best matches as best[].
+
+  Caller must ensure that each of the rectangles in question is fully
+  contained with its respective image.
+-----------------------------------------------------------------------------*/
+    unsigned X, Y;
+    /* Exhaustively find the best match */
+    for (X = Xmin; X < Xmax; ++X) {
+        int const widthOfOverlap = X - Left->pam.width;
+        for (Y = Ymin; Y < Ymax; ++Y) {
+            unsigned long difference = regionDifference(
+                Left, X, Y + offY,
+                Right, x, y + offY,
+                width, height);
+            update_best(best, difference, widthOfOverlap, Y + offY);
+            starEvent();
+        }
+    }
+}
+
+
+
+static void
+allocate_best_array(Best *** const bestP, unsigned const bestSize) {
+
+    Best ** best;
+    unsigned int i;
+
+    MALLOCARRAY(best, bestSize);
+    if (best == NULL)
+        pm_error("No memory for Best array");
+    
+    for (i = 0; i < bestSize; ++i) 
+        best[i] = allocate_best();
+    *bestP = best;
+}
+
+
+
+static void determineXYRange(Stitcher * const me,
+                             Image *    const Left,
+                             Image *    const Right,
+                             unsigned * const XminP,
+                             unsigned * const XmaxP,
+                             int *      const YminP,
+                             int *      const YmaxP) {
+    
+    if (me->x == INT_MAX) {
+        *XmaxP = Left->pam.width - me->width;
+        /* I can't bring myself to go half way */
+        *XminP = Left->pam.width - (7 * Right->pam.width / 16);
+    } else {
+        *XminP = me->x;
+        *XmaxP = me->x + 1;
+    }
+    if (me->y == INT_MAX) {
+        /* Middle 1/4 */
+        *YminP = Left->pam.height * 3 / 8;
+        *YmaxP = Left->pam.height - (*YminP) - me->height;
+    } else {
+        *YminP = me->y;
+        *YmaxP = me->y + 1;
+    }
+    if (verbose) 
+        pm_message("Test %d<x<%d %d<y<%d", *XminP, *XmaxP, *YminP, *YmaxP);
+}
+
+
+
+/*
+ *  Find the weighted best line fit using the left hand margin of the
+ * right hand image.
+ */
+static bool
+SliverMatch(Stitcher * me, Image * Left, Image * Right,
+            unsigned image_portion, unsigned skip_sliver)
+{
+    /* up/down 3/10, make sure has an odd number of members */
+    unsigned const bestSize = 
+        1 + 2 * ((image_portion * 3) / (10 * skip_sliver));
+    Best       ** best; /* malloc'ed array of Best * */
+    float         sumydotx, sumx, sumydot, sum;
+    float         sumydoty, sumy, sumydotydot;
+    int           yDiff;
+    unsigned      X, Xmin, Xmax, num, xmin, xmax;
+    int           x, y, Y, Ymin, Ymax, in, ymin, ymax;
+
+    /* Harry Sticks Geeses */
+    if (me->width == INT_MAX) {
+        me->width = Right->pam.width / image_portion;
+    }
+    if ((me->width > (Right->pam.width/2))
+     || (me->width > (Left->pam.width/2))) {
+        pm_error ("stitch sample too wide %d\n", me->width);
+        /* NOTREACHED */
+    }
+    if (me->height == INT_MAX) {
+        me->height = Right->pam.height / image_portion;
+    }
+    if ((me->height > Right->pam.height)
+     || (me->height > Left->pam.height)) {
+        pm_error ("stitch sample too high %d\n", me->height);
+        /* NOTREACHED */
+    }
+    yDiff = (Right->pam.height * skip_sliver) / image_portion;
+    starInit((unsigned long)-1L);
+
+    allocate_best_array(&best, bestSize);
+
+    determineXYRange(me, Left, Right, &Xmin, &Xmax, &Ymin, &Ymax);
+
+    /* Find the best */
+    if ((verbose == 1) || (verbose == 2)) {
+        fprintf (stderr, "%79s|\r|", "");
+        starInit((unsigned long)
+                 (
+                     (unsigned long)(Xmax - Xmin)
+                     * (unsigned long)(Ymax - Ymin)
+                     ) * (unsigned long)bestSize
+                 / 78L);
+    }
+
+    /* A point in the middle of the right image */
+    x = 0;
+    y = (Right->pam.height - me->height) / 2;
+    /*
+     *  Exhaustively search for the best match, improvements
+     * in the algorithm here, if any, would give us the best
+     * bang for the buck when it comes to improving performance.
+         */
+        /*
+         *  First pass through the right hand images to determine
+         * which are good candidate (top 90 percentile) for content of
+         * features that we may have a chance of testing with.
+         */
+    {
+        float   minf, maxf;
+        float * features;
+
+        MALLOCARRAY(features, bestSize);
+        minf = maxf = 0.0;
+        for (in = 0; in < bestSize; ++in) {
+            int const offY = yDiff * (in - (bestSize/2));
+            float SUM[3], SUMSQ[3];
+            int plane;
+            for (plane = 0; plane < MIN(Right->pam.depth,3); ++plane) {
+                SUM[plane] = SUMSQ[plane] = 0.0;
+            }
+            for (X = x; X < (x + me->width); ++X) {
+                for (Y = y + offY; Y < (y + offY + me->height); ++Y) {
+                    for (plane = 0; 
+                         plane < MIN(Right->pam.depth,3); 
+                         ++plane) {
+                        sample point = Right->tuple[Y][X][plane];
+                        SUM[plane]   += point;
+                        SUMSQ[plane] += point * point;
+                    }
+                }
+            }
+            /* How many features */
+            features[in] = 0.0;
+            for (plane = 0; plane < MIN(Right->pam.depth,3); ++plane) {
+                features[in] += SUMSQ[plane] - 
+                    (SUM[plane]*SUM[plane]/(float)(me->width*me->height));
+            }
+            if ((minf == 0.0) || (features[in] < minf)) {
+                minf = features[in];
+            }
+            if ((maxf == 0.0) || (features[in] > maxf)) {
+                maxf = features[in];
+            }
+        }
+        /* Select 90% in the contrast range */
+        minf = (minf + maxf) / 10;
+        for (in = 0; in < bestSize; ++in) {
+            if (features[in] < minf) {
+                free_best(best[in]);
+                best[in] = (Best *)NULL;
+            }
+        }
+    }
+    /* Loop through the constraints to find the best match */
+    sumydotx=sumx=sumydot=sumydotydot=sum=sumydoty=sumy=0.0;
+    xmin = UINT_MAX;
+    xmax = 0;
+    ymin = INT_MAX;
+    ymax = INT_MIN;
+    in = num = 0;
+    for (;;) {
+        float w;
+        int offY = yDiff * (in - (bestSize/2));
+        /* See if this one to be skipped because of too few features */
+        if (best[in] == (Best *)NULL) {
+            if (in > (bestSize/2)) {
+                in = bestSize - in;
+            } else if (in < (bestSize/2)) {
+                in = (bestSize-1) - in;
+            } else {
+                break;
+            }
+            if ((verbose == 1) || (verbose == 2))
+                for (X = Xmin; X < Xmax; ++X) {
+                    for (Y = Ymin; Y < Ymax; ++Y) 
+                        starEvent();
+                }
+            continue;
+        }
+        findBestMatches(Left, Right, x, y, me->width, me->height, 
+                        offY, Xmin, Xmax, Ymin, Ymax,
+                        best[in]);
+        /* slop (noise in NUM_BEST) */
+        {
+            float SUMx, SUMxx, SUMy, SUMyy, SUMw;
+            unsigned i;
+            SUMx = SUMxx = SUMy = SUMyy = SUMw = 0.0;
+            for (i = 0; i < NUM_BEST; ++i) {
+                /* best[in][i] describes the ith closest region in the right
+                   image to the region in the left image whose top corner is
+                   at (x, y+offY).
+                */
+                float const w2 = (best[in][i].total > 0)
+                    ? (1.0 / (float)best[in][i].total)
+                    : 1.0;
+                SUMx  += w2 * best[in][i].x;
+                SUMy  += w2 * best[in][i].y;
+                SUMxx += w2 * (best[in][i].x * best[in][i].x);
+                SUMyy += w2 * (best[in][i].y * best[in][i].y);
+                SUMw  += w2;
+            }
+            /* Find our weighted error */
+            w = SUMw
+                / ((SUMxx - (SUMx*SUMx)/SUMw) + (SUMyy - (SUMy*SUMy)/SUMw));
+        }
+        /* magnify slop */
+        w *= w;
+        Y = y + offY;
+        sumy        += w * best[in][0].y;
+        sumx        += w * best[in][0].x;
+        sum         += w;
+        sumydot     += w * Y;
+        sumydotydot += w * Y * Y;
+        sumydoty    += w * Y * best[in][0].y;
+        sumydotx    += w * Y * best[in][0].x;
+        /* Calculate the best fit line for these matches */
+        me->parms[Sliver_C] = ((sumydotydot * sum)
+                               - (sumydot * sumydot));
+        if (me->parms[Sliver_C] == 0.0) {
+            me->parms[Sliver_A] = 0.0;
+        } else {
+            me->parms[Sliver_A] = ((sumydotx * sum)
+                                   - (sumx * sumydot))
+                / me->parms[Sliver_C];
+            me->parms[Sliver_C] = ((sumydoty * sum)
+                                   - (sumy * sumydot))
+                / me->parms[Sliver_C];
+        }
+        if (sum == 0.0) {
+            me->parms[Sliver_B] = me->parms[Sliver_D] = 0;
+        } else {
+            me->parms[Sliver_B] = (sumx
+                                   - (me->parms[Sliver_A] * sumydot))
+                / sum;
+            me->parms[Sliver_D] = (sumy
+                                   - (me->parms[Sliver_C] * sumydot))
+                / sum;
+        }
+        if (verbose > 2) {
+            fprintf (stderr, "%.4g*(%d,%d)@(%d,%d)\n",
+                     w, best[in][0].x + Left->pam.width, best[in][0].y,
+                     me->width / 2, Y);
+        }
+        /* Record history of limits */
+        if ((best[in][0].x + Left->pam.width) < xmin) {
+            xmin = best[in][0].x + Left->pam.width;
+        }
+        if (xmax < (best[in][0].x + Left->pam.width)) {
+            xmax = best[in][0].x + Left->pam.width;
+        }
+        if ((best[in][0].y - offY) < ymin) {
+            ymin = best[in][0].y - offY;
+        }
+        if (ymax < (best[in][0].y - offY)) {
+            ymax = best[in][0].y - offY;
+        }
+        /* Lets restrict the search a bit now */
+        if (++num > 1) {
+            if (me->x == INT_MAX) {
+                int newXmin, newXmax, hold;
+                /* Use the formula to determine the bounds */
+                newXmin = (int)(me->parms[Sliver_B] + 0.5)
+                    + Left->pam.width;
+                newXmax = (int)((me->parms[Sliver_A]
+                                 * (float)Left->pam.height)
+                                + me->parms[Sliver_B] + 0.5)
+                    + Left->pam.width;
+                if (newXmax < newXmin) {
+                    hold = newXmin;
+                    newXmin = newXmax;
+                    newXmax = hold;
+                }
+                /* Trust little ... */
+                hold = (3 * newXmin - newXmax) / 2;
+                newXmax = (3 * newXmax - newXmin) / 2;
+                newXmin = hold;
+                /* Don't go inside history */
+                if (xmin < newXmin) {
+                    newXmin = xmin + Left->pam.width;
+                }
+                if (newXmax < xmax) {
+                    newXmax = xmax + Left->pam.width;
+                }
+                /* If it is `wacky' drop it */
+                if ((newXmax - Xmax) < (Right->pam.width / 3)) {
+                    /* Now upgrade new minimum and maximum */
+                    hold = Xmin;
+                    if ((Xmin < newXmin) && (newXmin < Xmax)) {
+                        hold = newXmin;
+                    }
+                    if ((Xmin < newXmax) && (newXmax < Xmax)) {
+                        Xmax = newXmax;
+                    }
+                    Xmin = hold;
+                }
+            }
+            if (me->y == INT_MAX) {
+                int newYmin, newYmax, hold;
+                float tmp;
+                /* Use the formula to determine the bounds */
+                newYmin = tmp = me->parms[Sliver_D]
+                    + ((float)(Left->pam.height + 1))
+                    / 2;
+                newYmax = (int)((me->parms[Sliver_C]
+                                 * (float)Left->pam.height)
+                                + tmp) - Left->pam.height;
+                if (newYmax < newYmin) {
+                    hold = newYmin;
+                    newYmin = newYmax;
+                    newYmax = hold;
+                }
+                /* Trust little ... */
+                hold = (3 * newYmin - newYmax) / 2;
+                newYmax = (3 * newYmax - newYmin) / 2;
+                newYmin = hold;
+                /* Don't go inside history */
+                if (ymin < newYmin) {
+                    newYmin = ymin;
+                }
+                if (newYmax < ymax) {
+                    newYmax = ymax;
+                }
+                /* Now upgrade new minimum and maximum */
+                hold = Ymin;
+                if ((Ymin < newYmin) && (newYmin < Ymax)) {
+                    hold = newYmin;
+                }
+                if ((Ymin < newYmax) && (newYmax < Ymax)) {
+                    Ymax = newYmax;
+                }
+                Ymin = hold;
+            }
+            if ((verbose == 1) || (verbose == 2)) {
+                starResetPeriod((unsigned long)(
+                    ((unsigned long)(Xmax - Xmin)
+                    * (unsigned long)(Ymax - Ymin))
+                                * (unsigned long)bestSize
+                                / 78L));
+            }
+        }
+        if (in > (bestSize/2)) {
+            in = bestSize - in;
+        } else if (in < (bestSize/2)) {
+            in = (bestSize-1) - in;
+        } else {
+            break;
+        }
+    }
+    if ((verbose == 1) || (verbose == 2)) {
+        fprintf (stderr, "\n");
+    }
+    if (verbose > 2) {
+        fprintf (stderr, "Up  ");
+        pr_best(best[bestSize-1]);
+        fprintf (stderr, "\nMid ");
+        pr_best(best[bestSize/2]);
+        fprintf (stderr, "\nDown");
+        pr_best(best[0]);
+        fprintf (stderr, "\n");
+    }
+
+    if (verbose) {
+        if (verbose > 1) {
+            fprintf (stderr,
+                     "[y=%.4g [x=%.4g [=%.4g [y.=%.4g "
+                     "[y.y.=%.4g [y.y=%.4g [y.x=%.4g\n",
+                     sumy, sumx, sum, sumydot, sumydotydot,
+                     sumydoty, sumydotx);
+        }
+        fprintf (stderr, "x=%.4gY%+.4g\ny=%.4gY%+.4g\n",
+                 me->parms[Sliver_A], me->parms[Sliver_B],
+                 me->parms[Sliver_C], me->parms[Sliver_D]);
+    }
+    /*
+         *  Free up resources
+         */
+    for (in = 0; in < bestSize; ++in) {
+        if (best[in] != (Best *)NULL) {
+            free_best (best[in]);
+            best[in] = (Best *)NULL;
+        }
+    }
+    free(best);
+
+        /* Calculate x',y' and x",y" from best fit line formula */
+    sum = (float)(Right->pam.width * Right->pam.height)
+        / (float)(2 * Right->pam.width - me->width);
+    me->parms[Sliver_xpp] = me->parms[Sliver_A] * sum;
+    me->parms[Sliver_ypp] = me->parms[Sliver_C] * sum;
+    sumx = me->parms[Sliver_A] * (Right->pam.height / 2)
+        + me->parms[Sliver_B];
+    sumx += Left->pam.width;
+    sumy = me->parms[Sliver_C] * (Right->pam.height / 2)
+        + me->parms[Sliver_D];
+    me->parms[Sliver_xp] = sumx - me->parms[Sliver_xpp];
+    me->parms[Sliver_yp] = sumy - me->parms[Sliver_ypp];
+    me->parms[Sliver_xpp] += sumx;
+    me->parms[Sliver_ypp] += sumy;
+    if (verbose > 1) {
+        fprintf (stderr, "x',y'=%.4g,%.4g x\",y\"=%.4g,%.4g\n",
+                 me->parms[Sliver_xp], me->parms[Sliver_yp],
+                 me->parms[Sliver_xpp], me->parms[Sliver_ypp]);
+    }
+    return TRUE;
+} /* SliverMatch() - end */
+
+/* These are not used.  Perhaps they are forerunners of the more
+   expressive Sliver_A, etc. names.
+*/
+#define Linear_a   0
+#define Linear_b   1
+#define Linear_c   2
+#define Linear_d   3
+#define Linear_e   4
+#define Linear_f   5
+#define Linear_g   6
+#define Linear_h   7
+
+static bool
+LinearMatch(Stitcher * me, Image * Left, Image * Right)
+{
+    if (SliverMatch(me, Left, Right, IMAGE_PORTION, SKIP_SLIVER * 8) 
+        == FALSE) {
+        return FALSE;
+    }
+
+    me->x = - (me->parms[Sliver_xp] + me->parms[Sliver_xpp] + 1) / 2;
+    me->y = - (me->parms[Sliver_yp] + me->parms[Sliver_ypp]
+          + (1 - Left->pam.height)) / 2;
+
+    if (verbose) 
+        pm_message("LinearMatch translation parameters are (%d,%d)",
+                   me->x, me->y);
+
+    return TRUE;
+} /* LinearMatch() - end */
+
+/*
+ *  Transformation parameters
+ *      left  x' = x
+ *      left  y' = y
+ *      right x' = x + me->x
+ *      right y' = y + me->y
+ */
+static float
+LinearXLeft(Stitcher * me, int x, int y)
+{
+    UNREFERENCED_PARAMETER(y);
+    return x;
+} /* LinearXLeft() - end */
+
+static float
+LinearYLeft(Stitcher * me, int x, int y)
+{
+    UNREFERENCED_PARAMETER(x);
+    return y;
+} /* LinearYLeft() - end */
+
+static float
+LinearXRight(Stitcher * me, int x, int y)
+{
+    UNREFERENCED_PARAMETER(y);
+    return (x + me->x);
+} /* LinearXRight() - end */
+
+static float
+LinearYRight(Stitcher * me, int x, int y)
+{
+    UNREFERENCED_PARAMETER(x);
+    return (y + me->y);
+} /* LinearYRight() - end */
+
+static void
+LinearOutput(Stitcher * me, FILE * fp)
+{
+    fprintf (fp, "x'=x%+d\ny'=y%+d\n", me->x, me->y);
+} /* LinearOutput() - end */
+
+/* BiLinear Stitcher Methods */
+
+static void
+BiLinearDeAlloc(Stitcher * me)
+{
+    LinearDeAlloc(me);
+}
+
+
+
+static bool
+BiLinearAlloc(Stitcher * me)
+{
+    return LinearAlloc(me);
+}
+
+
+
+static void
+BiLinearConstrain(Stitcher * me, int x, int y, int width, int height)
+{
+    LinearConstrain(me, x, y, width, height);
+    if (x != INT_MAX) {
+        me->parms[3] -= x;
+    }
+    if (y != INT_MAX) {
+        me->parms[7] -= y;
+    }
+} /* BiLinearConstrain() - end */
+
+/*
+ *  First pass is to find an approximate match. To do so, we take a
+ *  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.
+ *
+ *  Blind alleys:
+ *      - Tried a simpler constraint for right side to be `back'
+ *        to image, twisted too much sometimes:
+ *         . . .
+ *         W=aW+bH+cWH+d
+ *         H=eW+fH+gWH+h
+ *         W=aW+d
+ *         0=eW+h
+ *        Solve the equations resulted in:
+ *         a = W/(W-x") - cy"
+ *         b = -Wc
+ *         c = W/((x"-W)(y'-y"))
+ *         d = (1-a)W
+ *         e = y'(y"x'+W-Hx'-x"y")/(x'y"x"-Wx'y"-Wy'x"-WWy'+x'y'x"-Wx'y'-W)
+ *         f = 1 - Wg
+ *         g = (e + (H-y")/(x"-W))/y"
+ *         h = -We
+ *        Results left here for historical reasons.
+ *
+ *  Transformation parameters
+ *      x=ax.+by.+cx.y.+d
+ *      y=ex.+fy.+gx.y.+h
+ *  Where x,y represents the original point, and x.,y.
+ *  represents the transformed point. Thus:
+ *
+ * Transformed image:
+ * (x',y')               (x'",y'")
+ * (x",y")               (x"",y"")
+ *
+ * Corresponding to Original (dot) image:
+ * (0,0)                 (Right->pam.width,0)
+ * (0,Right->pam.height) (Right->pam.width,Right->pam.height)
+ *
+ * Define:
+ *      H=Right->pam.height
+ *      W=Right->pam.width
+ * Given that I want a flat presentation that both reduces the distortion
+ * necessary on an image, reduces the cropping losses, and flattens out the
+ * spherical or orbit distortions; it was chosen to constrain the right side
+ * in the middle horizontal, and pivot the left side in that middle (hopefully
+ * minimally) and to allow the image only vertical and horizontal location
+ * placement. Rotating the entire image could increase cropping losses
+ * especially if the focus was not down the center of the image on a
+ * graduated field causing the distortion to accumulate in subsequent
+ * images. Trapezoidal would cause the distortion to accumulate in subsequent
+ * images as well, resetting to `square' gradually towards the right would
+ * allow the next image to restart a match placing the distortions mainly
+ * in the stitching zone where averaging and the slight expectation of
+ * artifacts would minimize the effects. These constraints can be explained
+ * mathematically as the following:
+ *  x'" + x"" - 2W = x' + x"
+ *       y'" + y"" = y' + y"
+ *                 x'" = x""
+ *         y'" + H = y""
+ * resulting in the right side of the image being completely explained by the
+ * placement of the left hand side:
+ *  x'"=(x'+x"+2W)/2
+ *  y'"=(y'+y"-H)/2
+ *  x""=(x'+x"+2W)/2
+ *  y""=(y'+y"+H)/2
+ *
+ * Describing the `X' polygon using geometry and ratios:
+ *  X=A(y-(y'+y")/2)(x-(x'+x"+2W)/2) + (x - (x'+x")/2))
+ *    A=2(x"-x')/((y'-y")(x'-x"-2W))
+ *  a=2(y'(x'-x")-W(y'-y"))/((y'-y")(x'-x"-2W))
+ *  b=(x'-x")(x'+x"+2W)/((y'-y")(x'-x"-2W))
+ *  c=2(x"-x')/((y'-y")(x'-x"-2W))
+ *  d=(2W(x"y'-x'y")+y'(x"x"-x'x'))/((y'-y")(x'-x"-2W))
+ *
+ * Describing the `Y' polygon using geometry and ratios (note use of X rather
+ * than x, this has the effect of linearalizing the polygon).
+ *  Y=((y'-y"+H)/W(y'-y"))(y-(y'+y")/2)(X-HW/(y'-y"+H)) + H/2
+ *  e=(y'+y")(y'-y"+H)/2W(y"-y')
+ *  f=H/(y"-y')
+ *  g=(y'-y"+H)/W(y'-y")
+ *  h=Hy'/(y'-y")
+ *
+ * FYI: Reverse transform using the same formula style is:
+ *  a=(x"-x'+2W)/2W
+ *  b=(x"-x')/H
+ *  c=(x'-x")/WH
+ *  d=x'
+ *  e=(y"-y'-H)/2W
+ *  f=(y"-y')/H
+ *  g=(y'-y"+H)/WH
+ *  h=y'
+ */
+
+#define BiLinear_a   0
+#define BiLinear_b   1
+#define BiLinear_c   2
+#define BiLinear_d   3
+#define BiLinear_e   4
+#define BiLinear_f   5
+#define BiLinear_g   6
+#define BiLinear_h   7
+
+static bool
+BiLinearMatch(Stitcher * me, Image * Left, Image * Right)
+{
+    float xp, yp, xpp, ypp;
+
+    if (SliverMatch(me, Left, Right, IMAGE_PORTION, SKIP_SLIVER) == FALSE) {
+        return FALSE;
+    }
+    /* If too wacky, flatten out */
+    xp  = me->parms[Sliver_xp];
+    yp  = me->parms[Sliver_yp];
+    xpp = me->parms[Sliver_xpp];
+    ypp = me->parms[Sliver_ypp];
+    if ((me->parms[Sliver_A] < -0.3)
+     || (0.3 < me->parms[Sliver_A])) {
+        xp = xpp = (xp + xpp) / 2;
+    }
+    if ((me->parms[Sliver_C] < 0.6)
+     || (1.5 < me->parms[Sliver_D])) {
+        yp = (yp + ypp - (float)Right->pam.height) / 2;
+        ypp = yp + Right->pam.height;
+    }
+
+    /*
+     *  Calculate any necessary transformations on the
+     * right image to improve the stitching match. We have Done a
+     * weighted best fit line on the points we have collected
+     * thus far, now translate this to the constrained
+     * transformation equations.
+     */
+    /* a = y"-y' */
+    me->parms[BiLinear_a] = ypp-yp;
+    /* c = x'-x" */
+    me->parms[BiLinear_c] = xp-xpp;
+    /* d = (y"-y')(x"-x'+2W) = (y'-y")(x'-x"-2W) */
+    me->parms[BiLinear_d] = me->parms[BiLinear_a]
+                          * ((float)
+                             (2*Right->pam.width)-me->parms[BiLinear_c]);
+    /* a = 2(y'(x'-x")+W(y"-y'))/((y'-y")(x'-x"-2W)) */
+    me->parms[BiLinear_a] = 2*(yp*me->parms[BiLinear_c]
+                          + me->parms[BiLinear_a]*(float)(Right->pam.width))
+                          / me->parms[BiLinear_d];
+    /* b = (x'-x")(x'+x"+2W)/((y'-y")(x'-x"-2W)) */
+    me->parms[BiLinear_b] = me->parms[BiLinear_c]
+                          * (xp+xpp+(float)(2*Right->pam.width))
+                          / me->parms[BiLinear_d];
+    /* c = -2(x'-x")/((y'-y")(x'-x"-2W)) */
+    me->parms[BiLinear_c]*= -2/me->parms[BiLinear_d];
+    /* d = (2W(x"y'-x'y")+y'(x"x"-x'x'))/((y'-y")(x'-x"-2W)) */
+    me->parms[BiLinear_d] = ((xpp*yp-xp*ypp)*(float)(2*Right->pam.width)
+                          + yp*(xpp*xpp-xp*xp))
+                          / me->parms[BiLinear_d];
+
+    /* f = y"-y' */
+    me->parms[BiLinear_f] = ypp-yp;
+    /* g = (y"-y'-H)/W(y"-y') */
+    me->parms[BiLinear_g] = (me->parms[BiLinear_f]-(float)Right->pam.height)
+                          / me->parms[BiLinear_f]
+                          / (float)Right->pam.width;
+    /* e = (y'+y")(y'-y"+H)/2W(y"-y') = -g(y'+y")/2 */
+    me->parms[BiLinear_e] = (yp+ypp)*me->parms[BiLinear_g]/-2;
+    /* f=H/(y"-y') */
+    me->parms[BiLinear_f] = ((float)Right->pam.height)
+                          / me->parms[BiLinear_f];
+    /* h = Hy'/(y'-y") = -fy' */
+    me->parms[BiLinear_h] = -yp*me->parms[BiLinear_f];
+
+    return TRUE;
+} /* BiLinearMatch() - end */
+
+/*
+ *  Transformation parameters
+ *      x`=x
+ *      y`=y
+ */
+#define BiLinearXLeft LinearXLeft
+#define BiLinearYLeft LinearYLeft
+
+/*
+ *  Transformation parameters
+ *      x`=ax+by+cxy+d
+ *      y`=ex`+fy+gx`y+h
+ */
+static float
+BiLinearXRight(Stitcher * me, int x, int y)
+{
+    return (me->parms[BiLinear_a] * x) + (me->parms[BiLinear_b] * y)
+         + (me->parms[BiLinear_c] * (x * y)) + me->parms[BiLinear_d];
+} /* BiLinearXRight() - end */
+
+static float
+BiLinearYRight(Stitcher * me, int x, int y)
+{
+    /* A little trick I learned from a biker */
+    float X = BiLinearXRight(me, x, y);
+    return (me->parms[BiLinear_e] * X) + (me->parms[BiLinear_f] * y)
+         + (me->parms[BiLinear_g] * (X * y)) + me->parms[BiLinear_h];
+} /* BiLinearYRight() - end */
+
+static void
+BiLinearOutput(Stitcher * me, FILE * fp)
+{
+    fprintf (fp,
+      "x'=%.6gx%+.6gy%+.6gxy%+.6g\ny'=%.6gx'%+.6gy%+.6gx'y%+.6g\n",
+      me->parms[BiLinear_a], me->parms[BiLinear_b], me->parms[BiLinear_c],
+      me->parms[BiLinear_d], me->parms[BiLinear_e], me->parms[BiLinear_f],
+      me->parms[BiLinear_g], me->parms[BiLinear_h]);
+} /* BiLinearOutput() - end */
+
+/* Rotate Stitcher Methods */
+
+#define RotateDeAlloc   BiLinearDeAlloc
+
+#define RotateAlloc     BiLinearAlloc
+
+#define RotateConstrain BiLinearConstrain
+
+/*
+ *  First pass is to utilize the SliverMatch.
+ *
+ *  Transformation parameters
+ *      x=ax.+by.+d
+ *      y=ex.+fy.+h
+ *  Where x,y represents the original point, and x.,y.
+ *  represents the transformed point. Thus:
+ *
+ * Transformed image:
+ * (x',y')               (x'",y'")
+ * (x",y")               (x"",y"")
+ *
+ * Corresponding to Original (dot) image:
+ * (0,0)                 (Right->pam.width,0)
+ * (0,Right->pam.height) (Right->pam.width,Right->pam.height)
+ *
+ * Define:
+ *      H=Right->pam.height
+ *      W=Right->pam.width
+ *
+ */
+
+#define Rotate_a   0
+#define Rotate_b   1
+#define Rotate_c   2
+#define Rotate_d   3
+#define Rotate_e   4
+#define Rotate_f   5
+#define Rotate_g   6
+#define Rotate_h   7
+
+static bool
+RotateMatch(Stitcher * me, Image * Left, Image * Right)
+{
+    float xp, yp, xpp, ypp;
+
+    if (SliverMatch(me, Left, Right, IMAGE_PORTION, SKIP_SLIVER) == FALSE) {
+        return FALSE;
+    }
+    xp  = me->parms[Sliver_xp];
+    yp  = me->parms[Sliver_yp];
+    xpp = me->parms[Sliver_xpp];
+    ypp = me->parms[Sliver_ypp];
+
+    me->parms[Rotate_c] = (xp - xpp);
+    me->parms[Rotate_c]*= me->parms[Rotate_c];
+    me->parms[Rotate_g] = (yp - ypp);
+    me->parms[Rotate_g]*= me->parms[Rotate_g];
+    me->parms[Rotate_a] = me->parms[Rotate_f] = sqrt(me->parms[Rotate_g]
+                                              / (me->parms[Rotate_c]
+                                                + me->parms[Rotate_g]));
+    me->parms[Rotate_b] = me->parms[Rotate_e] = sqrt(me->parms[Rotate_c]
+                                              / (me->parms[Rotate_c]
+                                                + me->parms[Rotate_g]));
+    if (xp < xpp) {
+        me->parms[Rotate_b] = -me->parms[Rotate_b];
+    } else {
+        me->parms[Rotate_e] = -me->parms[Rotate_e];
+    }
+    /* negative (for reverse transform below) xp & yp set for unity gain */
+    xp = ((me->parms[Rotate_b] * (float)Right->pam.height) + xp + xpp) / -2;
+    yp = ((me->parms[Rotate_f] * (float)Right->pam.height) - yp - ypp) / 2;
+    me->parms[Rotate_d] = xp * me->parms[Rotate_a] + yp * me->parms[Rotate_b];
+    me->parms[Rotate_h] = xp * me->parms[Rotate_e] + yp * me->parms[Rotate_f];
+    return TRUE;
+} /* RotateMatch() - end */
+
+/*
+ *  Transformation parameters
+ *      x`=x
+ *      y`=y
+ */
+#define RotateXLeft BiLinearXLeft
+#define RotateYLeft BiLinearYLeft
+
+/*
+ *  Transformation parameters
+ *      x`=ax+by+d
+ *      y`=ex+fy+h
+ */
+
+static float
+RotateXRight(Stitcher * me, int x, int y)
+{
+    return (me->parms[Rotate_a] * x) + (me->parms[Rotate_b] * y)
+          + me->parms[Rotate_d];
+} /* RotateXRight() - end */
+
+static float
+RotateYRight(Stitcher * me, int x, int y)
+{
+    return (me->parms[Rotate_e] * x) + (me->parms[Rotate_f] * y)
+          + me->parms[Rotate_h];
+} /* RotateYRight() - end */
+
+static void
+RotateOutput(Stitcher * me, FILE * fp)
+{
+    fprintf (fp,
+      "x'=%.6gx%+.6gy%+.6g\ny'=%.6gx%+.6gy%+.6g\n",
+      me->parms[Rotate_a], me->parms[Rotate_b], me->parms[Rotate_d],
+      me->parms[Rotate_e], me->parms[Rotate_f], me->parms[Rotate_h]);
+} /* RotateOutput() - end */
+
+/* Stitcher Method Table */
+
+Stitcher StitcherMethods[] = {
+    { "RotateSliver", RotateAlloc, RotateDeAlloc, RotateConstrain,
+      RotateMatch, RotateXLeft, RotateYLeft, RotateXRight,
+      RotateYRight, RotateOutput },
+    { "BiLinearSliver", BiLinearAlloc, BiLinearDeAlloc, BiLinearConstrain,
+      BiLinearMatch, BiLinearXLeft, BiLinearYLeft, BiLinearXRight,
+      BiLinearYRight, BiLinearOutput },
+    { "LinearSliver", LinearAlloc, LinearDeAlloc, LinearConstrain,
+      LinearMatch, LinearXLeft, LinearYLeft, LinearXRight,
+      LinearYRight, LinearOutput },
+    { (char *)NULL }
+};
diff --git a/editor/pnmtile.c b/editor/pnmtile.c
new file mode 100644
index 00000000..96bf658d
--- /dev/null
+++ b/editor/pnmtile.c
@@ -0,0 +1,63 @@
+/* pnmtile.c - replicate a portable anymap into a specified size
+**
+** Copyright (C) 1989 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.
+*/
+
+#include "pnm.h"
+
+int
+main( argc, argv )
+    int argc;
+    char* argv[];
+    {
+    FILE* ifp;
+    xel** xels;
+    register xel* xelrow;
+    xelval maxval;
+    int rows, cols, format, width, height, row, col;
+    const char* const usage = "width height [pnmfile]";
+
+    pnm_init( &argc, argv );
+
+    if ( argc < 3 || argc > 4 )
+	pm_usage( usage );
+
+    if ( sscanf( argv[1], "%d", &width ) != 1 )
+	pm_usage( usage );
+    if ( sscanf( argv[2], "%d", &height ) != 1 )
+	pm_usage( usage );
+
+    if ( width < 1 )
+	pm_error( "width is less than 1" );
+    if ( height < 1 )
+	pm_error( "height is less than 1" );
+
+    if ( argc == 4 )
+	ifp = pm_openr( argv[3] );
+    else
+	ifp = stdin;
+
+    xels = pnm_readpnm( ifp, &cols, &rows, &maxval, &format );
+    pm_close( ifp );
+
+    xelrow = pnm_allocrow( width );
+
+    pnm_writepnminit( stdout, width, height, maxval, format, 0 );
+    for ( row = 0; row < height; ++row )
+	{
+	for ( col = 0; col < width; ++col )
+	    xelrow[col] = xels[row % rows][col % cols];
+	pnm_writepnmrow( stdout, xelrow, width, maxval, format, 0 );
+	}
+
+    pm_close( stdout );
+
+    exit( 0 );
+    }
diff --git a/editor/ppm3d.c b/editor/ppm3d.c
new file mode 100644
index 00000000..c37ceeb1
--- /dev/null
+++ b/editor/ppm3d.c
@@ -0,0 +1,138 @@
+/* ppmto3d.c - convert a portable pixmap to a portable graymap
+**
+** Copyright (C) 1989 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.
+*/
+
+#include "ppm.h"
+#include "lum.h"
+
+static void
+computeGrayscaleRow(const pixel * const inputRow,
+                    gray *        const outputRow,
+                    pixval        const maxval,
+                    unsigned int  const cols) {
+
+    if (maxval <= 255) {
+        unsigned int col;
+        /* Use fast approximation to 0.299 r + 0.587 g + 0.114 b. */
+        for (col = 0; col < cols; ++col)
+            outputRow[col] = ppm_fastlumin(inputRow[col]);
+    } else {
+        unsigned int col;
+        /* Can't use fast approximation, so fall back on floats. */
+        for (col = 0; col < cols; ++col)
+            outputRow[col] = PPM_LUMIN(inputRow[col]) + 0.5;
+    }
+}
+
+
+
+int
+main (int argc, char *argv[]) {
+
+    int offset; 
+    int cols, rows, row;
+    pixel* pixelrow;
+    pixval maxval;
+
+    FILE* Lifp;
+    pixel* Lpixelrow;
+    gray* Lgrayrow;
+    int Lrows, Lcols, Lformat;
+    pixval Lmaxval;
+   
+    FILE* Rifp;
+    pixel* Rpixelrow;
+    gray* Rgrayrow;
+    int Rrows, Rcols, Rformat;
+    pixval Rmaxval;
+   
+    ppm_init (&argc, argv);
+
+    if (argc-1 > 3 || argc-1 < 2) 
+        pm_error("Wrong number of arguments (%d).  Arguments are "
+                 "leftppmfile rightppmfile [horizontal_offset]", argc-1);
+
+    Lifp = pm_openr (argv[1]);
+    Rifp = pm_openr (argv[2]);
+
+    if (argc-1 >= 3) 
+        offset = atoi (argv[3]);
+    else
+        offset = 30;
+
+    ppm_readppminit (Lifp, &Lcols, &Lrows, &Lmaxval, &Lformat);
+    ppm_readppminit (Rifp, &Rcols, &Rrows, &Rmaxval, &Rformat);
+    
+    if ((Lcols != Rcols) || (Lrows != Rrows) || 
+        (Lmaxval != Rmaxval) || 
+        (PPM_FORMAT_TYPE(Lformat) != PPM_FORMAT_TYPE(Rformat)))
+        pm_error ("Pictures are not of same size and format");
+    
+    cols = Lcols;
+    rows = Lrows;
+    maxval = Lmaxval;
+   
+    ppm_writeppminit (stdout, cols, rows, maxval, 0);
+    Lpixelrow = ppm_allocrow (cols);
+    Lgrayrow = pgm_allocrow (cols);
+    Rpixelrow = ppm_allocrow (cols);
+    Rgrayrow = pgm_allocrow (cols);
+    pixelrow = ppm_allocrow (cols);
+
+    for (row = 0; row < rows; ++row) {
+        ppm_readppmrow(Lifp, Lpixelrow, cols, maxval, Lformat);
+        ppm_readppmrow(Rifp, Rpixelrow, cols, maxval, Rformat);
+
+        computeGrayscaleRow(Lpixelrow, Lgrayrow, maxval, cols);
+        computeGrayscaleRow(Rpixelrow, Rgrayrow, maxval, cols);
+        {
+            int col;
+            gray* LgP;
+            gray* RgP;
+            pixel* pP;
+            for (col = 0, pP = pixelrow, LgP = Lgrayrow, RgP = Rgrayrow;
+                 col < cols + offset;
+                 ++col) {
+            
+                if (col < offset/2)
+                    ++LgP;
+                else if (col >= offset/2 && col < offset) {
+                    const pixval Blue = (pixval) (float) *LgP;
+                    const pixval Red = (pixval) 0;
+                    PPM_ASSIGN (*pP, Red, Blue, Blue);
+                    ++LgP;
+                    ++pP;
+                } else if (col >= offset && col < cols) {
+                    const pixval Red = (pixval) (float) *RgP;
+                    const pixval Blue = (pixval) (float) *LgP;
+                    PPM_ASSIGN (*pP, Red, Blue, Blue);
+                    ++LgP;
+                    ++RgP;
+                    ++pP;
+                } else if (col >= cols && col < cols + offset/2) {
+                    const pixval Blue = (pixval) 0;
+                    const pixval Red = (pixval) (float) *RgP;
+                    PPM_ASSIGN (*pP, Red, Blue, Blue);
+                    ++RgP;
+                    ++pP;
+                } else
+                    ++RgP;
+            }
+        }    
+        ppm_writeppmrow(stdout, pixelrow, cols, maxval, 0);
+    }
+
+    pm_close(Lifp);
+    pm_close(Rifp);
+    pm_close(stdout);
+
+    return 0;
+}
diff --git a/editor/ppmbrighten.c b/editor/ppmbrighten.c
new file mode 100644
index 00000000..93649082
--- /dev/null
+++ b/editor/ppmbrighten.c
@@ -0,0 +1,337 @@
+/* ppmbrighten.c - allow user control over Value and Saturation of PPM file
+**
+** Copyright (C) 1989 by Jef Poskanzer.
+** Copyright (C) 1990 by Brian Moffet.
+**
+** Permission to use, copy, modify, and distribute this software and its
+** documentation for any purpose and without fee is hereby granted, provided
+** that the above copyright notice appear in all copies and that both that
+** copyright notice and this permission notice appear in supporting
+** documentation.  This software is provided "as is" without express or
+** implied warranty.
+*/
+
+#include "ppm.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+#define MULTI   1000
+
+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 */
+    float saturation;
+    float value;
+    unsigned int normalize;
+};
+
+
+
+
+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 saturationSpec, valueSpec;
+    int saturationOpt, valueOpt;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "saturation",  OPT_INT,    &saturationOpt,
+            &saturationSpec,      0 );
+    OPTENT3(0, "value",       OPT_INT,    &valueOpt,
+            &valueSpec,           0 );
+    OPTENT3(0, "normalize",   OPT_FLAG,   NULL,
+            &cmdlineP->normalize, 0 );
+
+
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
+
+    optParseOptions3( &argc, argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+    
+    if (saturationSpec) {
+        if (saturationOpt < -100)
+            pm_error("Saturation reduction cannot be more than 100%%.  "
+                     "You specified %d", saturationOpt);
+        else
+            cmdlineP->saturation = 1.0 + (float)saturationOpt / 100;
+    } else
+        cmdlineP->saturation = 1.0;
+
+    if (valueSpec) {
+        if (valueOpt < -100)
+            pm_error("Value reduction cannot be more than 100%%.  "
+                     "You specified %d", valueOpt);
+        else
+            cmdlineP->value = 1.0 + (float)valueOpt / 100;
+    } else
+        cmdlineP->value = 1.0;
+
+    if (argc-1 < 1)
+        cmdlineP->inputFilespec = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFilespec = argv[1];
+    else
+        pm_error("Program takes at most one argument:  file specification");
+}
+
+
+
+static __inline__ unsigned int
+mod(int const dividend, unsigned int const divisor) {
+
+    int remainder = dividend % divisor;
+
+    if (remainder < 0)
+        return divisor + remainder;
+    else 
+        return (unsigned int) remainder;
+}
+
+
+
+static void 
+RGBtoHSV(pixel          const color,
+         pixval         const maxval,
+         unsigned int * const hP, 
+         unsigned int * const sP, 
+         unsigned int * const vP) {
+
+    unsigned int const R = (MULTI * PPM_GETR(color) + maxval - 1) / maxval;
+    unsigned int const G = (MULTI * PPM_GETG(color) + maxval - 1) / maxval;
+    unsigned int const B = (MULTI * PPM_GETB(color) + maxval - 1) / maxval;
+
+    unsigned int s, v;
+    unsigned int t;
+    unsigned int sector;
+
+    v = MAX(R, MAX(G, B));
+
+    t = MIN(R, MIN(G, B));
+
+    if (v == 0)
+        s = 0;
+    else
+        s = ((v - t)*MULTI)/v;
+
+    if (s == 0)
+        sector = 0;
+    else {
+        unsigned int const cr = (MULTI * (v - R))/(v - t);
+        unsigned int const cg = (MULTI * (v - G))/(v - t);
+        unsigned int const cb = (MULTI * (v - B))/(v - t);
+
+        if (R == v)
+            sector = mod((int)(cb - cg), 6*MULTI);
+        else if (G == v)
+            sector = mod((int)((2*MULTI) + cr - cb), 6*MULTI);
+        else if (B == v)
+            sector = mod((int)((4*MULTI) + cg - cr), 6*MULTI);
+        else
+            pm_error("Internal error: neither r, g, nor b is maximum");
+    }
+
+    *hP = sector * 60;
+    *sP = s;
+    *vP = v;
+}
+
+
+
+static void
+HSVtoRGB(unsigned int   const h, 
+         unsigned int   const s, 
+         unsigned int   const v, 
+         pixval         const maxval,
+         pixel *        const colorP) {
+    
+    unsigned int R, G, B;
+
+    if (s == 0) {
+        R = v;
+        G = v;
+        B = v;
+    } else {
+        unsigned int const sectorSize = 60 * MULTI;
+            /* Color wheel is divided into six 60 degree sectors. */
+        unsigned int const sector = (h/sectorSize);
+            /* The sector in which our color resides.  Value is in 0..5 */
+        unsigned int const f = (h - sector*sectorSize)/60;
+            /* The fraction of the way the color is from one side of
+               our sector to the other side, going clockwise.  Value is
+               in [0, MULTI).
+            */
+        unsigned int const m = (v * (MULTI - s)) / MULTI;
+        unsigned int const n = (v * (MULTI - (s * f)/MULTI)) / MULTI;
+        unsigned int const k = (v * (MULTI - (s * (MULTI - f))/MULTI)) / MULTI;
+
+        switch (sector) {
+        case 0:
+            R = v;
+            G = k;
+            B = m;
+            break;
+        case 1:
+            R = n;
+            G = v;
+            B = m;
+            break;
+        case 2:
+            R = m;
+            G = v;
+            B = k;
+            break;
+        case 3:
+            R = m;
+            G = n;
+            B = v;
+            break;
+        case 4:
+            R = k;
+            G = m;
+            B = v;
+            break;
+        case 5:
+            R = v;
+            G = m;
+            B = n;
+            break;
+        default:
+            pm_error("Invalid H value passed to HSVtoRGB: %u/%u", h, MULTI);
+        }
+    }
+    PPM_ASSIGN(*colorP, 
+               (R * maxval) / MULTI,
+               (G * maxval) / MULTI,
+               (B * maxval) / MULTI);
+}
+
+
+
+static void
+getMinMax(FILE *         const ifP,
+          int            const cols,
+          int            const rows,
+          pixval         const maxval,
+          int            const format,
+          unsigned int * const minValueP,
+          unsigned int * const maxValueP) {
+
+    pixel * pixelrow;
+    unsigned int minValue, maxValue;
+    int row;
+
+    pixelrow = ppm_allocrow(cols);
+
+    maxValue = 0;
+    minValue = MULTI;
+    for (row = 0; row < rows; ++row) {
+        unsigned int col;
+        ppm_readppmrow(ifP, pixelrow, cols, maxval, format);
+        for (col = 0; col < cols; ++col) {
+            unsigned int H, S, V;
+
+            RGBtoHSV(pixelrow[col], maxval, &H, &S, &V);
+            maxValue = MAX(maxValue, V);
+            minValue = MIN(minValue, V);
+        }
+    }
+    ppm_freerow(pixelrow);
+
+    *minValueP = minValue;
+    *maxValueP = maxValue;
+}
+
+
+
+int
+main(int argc, char * argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE *ifP;
+    pixval minValue, maxValue;
+    pixel *pixelrow;
+    pixval maxval;
+    int rows, cols, format, row;
+
+    ppm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    if (cmdline.normalize)
+        ifP = pm_openr_seekable(cmdline.inputFilespec);
+    else
+        ifP = pm_openr(cmdline.inputFilespec);
+
+    ppm_readppminit(ifP, &cols, &rows, &maxval, &format);
+
+    if (cmdline.normalize) {
+        pm_filepos rasterPos;
+        pm_tell2(ifP, &rasterPos, sizeof(rasterPos));
+        getMinMax(ifP, cols, rows, maxval, format, &minValue, &maxValue);
+        pm_seek2(ifP, &rasterPos, sizeof(rasterPos));
+        pm_message("Minimum value %u%% of full intensity "
+                   "being remapped to zero.",
+                   (minValue*100+MULTI/2)/MULTI);
+        pm_message("Maximum value %u%% of full intensity "
+                   "being remapped to full.",
+                   (maxValue*100+MULTI/2)/MULTI);
+    }
+
+    pixelrow = ppm_allocrow(cols);
+
+    ppm_writeppminit(stdout, cols, rows, maxval, 0);
+
+    for (row = 0; row < rows; ++row) {
+        unsigned int col;
+        ppm_readppmrow(ifP, pixelrow, cols, maxval, format);
+        for (col = 0; col < cols; ++col) {
+            unsigned int H, S, V;
+
+            RGBtoHSV(pixelrow[col], maxval, &H, &S, &V);
+            
+            if (cmdline.normalize) {
+                V -= minValue;
+                V = (V * MULTI) /
+                    (MULTI - (minValue+MULTI-maxValue));
+            }
+
+            S = MIN(MULTI, (unsigned int) (S * cmdline.saturation + 0.5));
+            V = MIN(MULTI, (unsigned int) (V * cmdline.value + 0.5));
+
+            HSVtoRGB(H, S, V, maxval, &pixelrow[col]);
+        }
+
+        ppm_writeppmrow(stdout, pixelrow, cols, maxval, 0);
+    }
+    ppm_freerow(pixelrow);
+
+    pm_close(ifP);
+
+    /* If the program failed, it previously aborted with nonzero completion
+       code, via various function calls.
+    */
+    return 0;
+}
diff --git a/editor/ppmchange.c b/editor/ppmchange.c
new file mode 100644
index 00000000..92d55527
--- /dev/null
+++ b/editor/ppmchange.c
@@ -0,0 +1,232 @@
+/* ppmchange.c - change a given color to another
+**
+** Copyright (C) 1991 by Wilson H. Bent, Jr.
+**
+** 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.
+**
+** Modified by Alberto Accomazzi (alberto@cfa.harvard.edu).
+**     28 Jan 94 -  Added multiple color substitution function.
+*/
+
+#include "ppm.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+#define TCOLS 256
+#define SQRT3 1.73205080756887729352
+    /* The square root of 3 */
+
+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 */
+    int ncolors;      /* Number of valid entries in color0[], color1[] */
+    char * oldcolorname[TCOLS];  /* colors user wants replaced */
+    char * newcolorname[TCOLS];  /* colors with which he wants them replaced */
+    int closeness;    
+       /* -closeness option value.  Zero if no -closeness option */
+    char * remainder_colorname;  
+      /* Color user specified for -remainder.  Null pointer if he didn't
+         specify -remainder.
+      */
+    unsigned int closeok;
+};
+
+
+
+static void
+parseCommandLine(int argc, char ** 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 closenessSpec, remainderSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENTRY */
+    OPTENT3(0, "closeness",     OPT_UINT,
+            &cmdlineP->closeness,           &closenessSpec,     0);
+    OPTENT3(0, "remainder",     OPT_STRING,
+            &cmdlineP->remainder_colorname, &remainderSpec,     0);
+    OPTENT3(0, "closeok",       OPT_FLAG,
+            NULL,                           &cmdlineP->closeok, 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);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (!closenessSpec)
+        cmdlineP->remainder_colorname = NULL;
+
+    if (!closenessSpec)
+        cmdlineP->closeness = 0;
+
+    if ((argc-1) % 2 == 0) 
+        cmdlineP->input_filespec = "-";
+    else
+        cmdlineP->input_filespec = argv[argc-1];
+
+    {
+        int argn;
+        cmdlineP->ncolors = 0;  /* initial value */
+        for (argn = 1; 
+             argn+1 < argc && cmdlineP->ncolors < TCOLS; 
+             argn += 2) {
+            cmdlineP->oldcolorname[cmdlineP->ncolors] = argv[argn];
+            cmdlineP->newcolorname[cmdlineP->ncolors] = argv[argn+1];
+            cmdlineP->ncolors++;
+        }
+    }
+}
+
+
+
+static double
+sqrf(float const F) {
+    return F*F;
+}
+
+
+
+static int 
+colormatch(pixel const comparand, 
+           pixel const comparator, 
+           float const closeness) {
+/*----------------------------------------------------------------------------
+   Return true iff 'comparand' matches 'comparator' in color within the
+   fuzz factor 'closeness'.
+-----------------------------------------------------------------------------*/
+    /* Fast path for usual case */
+    if (closeness == 0)
+        return PPM_EQUAL(comparand, comparator);
+
+    return PPM_DISTANCE(comparand, comparator) <= sqrf(closeness);
+}
+
+
+
+static void
+changeRow(const pixel * const inrow, 
+          pixel *       const outrow, 
+          int           const cols,
+          int           const ncolors, 
+          const pixel         colorfrom[], 
+          const pixel         colorto[],
+          bool          const remainder_specified, 
+          pixel         const remainder_color, 
+          float         const closeness) {
+/*----------------------------------------------------------------------------
+   Replace the colors in a single row.  There are 'ncolors' colors to 
+   replace.  The to-replace colors are in the array colorfrom[], and the
+   replace-with colors are in corresponding elements of colorto[].
+   Iff 'remainder_specified' is true, replace all colors not mentioned
+   in colorfrom[] with 'remainder_color'.  Use the closeness factor
+   'closeness' in determining if something in the input row matches
+   a color in colorfrom[].
+
+   The input row is 'inrow'.  The output is returned as 'outrow', in
+   storage which must be already allocated.  Both are 'cols' columns wide.
+-----------------------------------------------------------------------------*/
+    int col;
+
+    for (col = 0; col < cols; ++col) {
+        int i;
+        int have_match; /* logical: It's a color user said to change */
+        pixel newcolor;  
+        /* Color to which we must change current pixel.  Undefined unless
+           'have_match' is true.
+        */
+
+        have_match = FALSE;  /* haven't found a match yet */
+        for (i = 0; i < ncolors && !have_match; ++i) {
+            have_match = colormatch(inrow[col], colorfrom[i], closeness);
+            newcolor = colorto[i];
+        }
+        if (have_match)
+            outrow[col] = newcolor;
+        else if (remainder_specified)
+            outrow[col] = remainder_color;
+        else
+            outrow[col] = inrow[col];
+    }
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    int format;
+    int rows, cols;
+    pixval maxval;
+    float closeness;
+    int row;
+    pixel* inrow;
+    pixel* outrow;
+
+    pixel oldcolor[TCOLS];  /* colors user wants replaced */
+    pixel newcolor[TCOLS];  /* colors with which he wants them replaced */
+    pixel remainder_color;
+      /* Color user specified for -remainder.  Undefined if he didn't
+         specify -remainder.
+      */
+
+    ppm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+    
+    ifP = pm_openr(cmdline.input_filespec);
+
+    ppm_readppminit(ifP, &cols, &rows, &maxval, &format);
+
+    if (cmdline.remainder_colorname)
+        remainder_color = ppm_parsecolor2(cmdline.remainder_colorname, maxval,
+                                          cmdline.closeok);
+    { 
+        int i;
+        for (i = 0; i < cmdline.ncolors; ++i) {
+            oldcolor[i] = ppm_parsecolor2(cmdline.oldcolorname[i], maxval,
+                                          cmdline.closeok);
+            newcolor[i] = ppm_parsecolor2(cmdline.newcolorname[i], maxval,
+                                          cmdline.closeok);
+        }
+    }
+    closeness = SQRT3 * maxval * cmdline.closeness/100;
+
+    ppm_writeppminit( stdout, cols, rows, maxval, 0 );
+    inrow = ppm_allocrow(cols);
+    outrow = ppm_allocrow(cols);
+
+    /* Scan for the desired color */
+    for (row = 0; row < rows; row++) {
+        ppm_readppmrow(ifP, inrow, cols, maxval, format);
+
+        changeRow(inrow, outrow, cols, cmdline.ncolors, oldcolor, newcolor,
+                  cmdline.remainder_colorname != NULL,
+                  remainder_color, closeness);
+
+        ppm_writeppmrow(stdout, outrow, cols, maxval, 0);
+    }
+
+    pm_close(ifP);
+
+    return 0;
+}
diff --git a/editor/ppmcolormask.c b/editor/ppmcolormask.c
new file mode 100644
index 00000000..57e5c825
--- /dev/null
+++ b/editor/ppmcolormask.c
@@ -0,0 +1,245 @@
+/*=========================================================================
+                             ppmcolormask
+===========================================================================
+
+  This program produces a PBM mask of areas containing a certain color.
+
+  By Bryan Henderson, Olympia WA; April 2000.
+
+  Contributed to the public domain by its author.
+=========================================================================*/
+
+#include <assert.h>
+#include <string.h>
+
+#include "pm_c_util.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+#include "nstring.h"
+#include "ppm.h"
+#include "pbm.h"
+
+enum matchType {
+    MATCH_EXACT,
+    MATCH_BK
+};
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFilename;
+    unsigned int colorCount;
+    struct {
+        enum matchType matchType;
+        union {
+            pixel    color;   /* matchType == MATCH_EXACT */
+            bk_color bkColor; /* matchType == MATCH_BK */
+        } u;
+    } maskColor[16];
+    unsigned int verbose;
+};
+
+
+
+static void
+parseColorOpt(const char *         const colorOpt,
+              struct cmdlineInfo * const cmdlineP) {
+
+    unsigned int colorCount;
+    char * colorOptWork;
+    char * cursor;
+    bool eol;
+    
+    colorOptWork = strdup(colorOpt);
+    cursor = &colorOptWork[0];
+    
+    eol = FALSE;    /* initial value */
+    colorCount = 0; /* initial value */
+    while (!eol && colorCount < ARRAY_SIZE(cmdlineP->maskColor)) {
+        const char * token;
+        token = strsepN(&cursor, ",");
+        if (token) {
+            if (STRNEQ(token, "bk:", 3)) {
+                cmdlineP->maskColor[colorCount].matchType = MATCH_BK;
+                cmdlineP->maskColor[colorCount].u.bkColor =
+                    ppm_bk_color_from_name(&token[3]);
+            } else {
+                cmdlineP->maskColor[colorCount].matchType = MATCH_EXACT;
+                cmdlineP->maskColor[colorCount].u.color =
+                    ppm_parsecolor(token, PPM_MAXMAXVAL);
+            }
+            ++colorCount;
+        } else
+            eol = TRUE;
+    }
+    free(colorOptWork);
+
+    cmdlineP->colorCount = colorCount;
+}
+
+
+
+static void
+parseCommandLine(int argc, char ** argv,
+                 struct cmdlineInfo *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!
+-----------------------------------------------------------------------------*/
+    optEntry * option_def;
+        /* Instructions to OptParseOptions3 on how to parse our options. */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+    const char * colorOpt;
+    unsigned int colorSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "color",      OPT_STRING, &colorOpt, &colorSpec,           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 may have parms that are negative numbers */
+
+    optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and all of *cmdlineP. */
+
+    if (colorSpec)
+        parseColorOpt(colorOpt, cmdlineP);
+
+    if (colorSpec) {
+        if (argc-1 < 1)
+            cmdlineP->inputFilename = "-";  /* he wants stdin */
+        else if (argc-1 == 1)
+            cmdlineP->inputFilename = argv[1];
+        else
+            pm_error("Too many arguments.  When you specify -color, "
+                     "the only argument accepted is the optional input "
+                     "file name.");
+    } else {
+        if (argc-1 < 1)
+            pm_error("You must specify the -color option.");
+        else {
+            cmdlineP->colorCount = 1;
+            cmdlineP->maskColor[0].matchType = MATCH_EXACT;
+            cmdlineP->maskColor[0].u.color =
+                ppm_parsecolor(argv[1], PPM_MAXMAXVAL);
+
+            if (argc - 1 < 2)
+                cmdlineP->inputFilename = "-";  /* he wants stdin */
+            else if (argc-1 == 2)
+                cmdlineP->inputFilename = argv[2];
+            else 
+                pm_error("Too many arguments.  The only arguments accepted "
+                         "are the mask color and optional input file name");
+        }
+    }
+}
+
+
+
+static bool
+isBkColor(pixel    const comparator,
+          pixval   const maxval,
+          bk_color const comparand) {
+
+    /* TODO: keep a cache of the bk color for each color in
+       a colorhash_table.
+    */
+    
+    bk_color const comparatorBk = ppm_bk_color_from_color(comparator, maxval);
+
+    return comparatorBk == comparand;
+}
+
+
+
+static bool
+colorIsInSet(pixel              const color,
+             pixval             const maxval,
+             struct cmdlineInfo const cmdline) {
+
+    bool isInSet;
+    unsigned int i;
+
+    for (i = 0, isInSet = FALSE;
+         i < cmdline.colorCount && !isInSet; ++i) {
+
+        assert(i < ARRAY_SIZE(cmdline.maskColor));
+
+        switch(cmdline.maskColor[i].matchType) {
+        case MATCH_EXACT:
+            if (PPM_EQUAL(color, cmdline.maskColor[i].u.color))
+                isInSet = TRUE;
+            break;
+        case MATCH_BK:
+            if (isBkColor(color, maxval, cmdline.maskColor[i].u.bkColor))
+                isInSet = TRUE;
+            break;
+        }
+    }
+    return isInSet;
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    struct cmdlineInfo cmdline;
+
+    FILE * ifP;
+
+    /* Parameters of input image: */
+    int rows, cols;
+    pixval maxval;
+    int format;
+
+    ppm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilename);
+
+    ppm_readppminit(ifP, &cols, &rows, &maxval, &format);
+    pbm_writepbminit(stdout, cols, rows, 0);
+    {
+        pixel * const inputRow = ppm_allocrow(cols);
+        bit *   const maskRow  = pbm_allocrow(cols);
+
+        unsigned int numPixelsMasked;
+
+        unsigned int row;
+        for (row = 0, numPixelsMasked = 0; row < rows; ++row) {
+            int col;
+            ppm_readppmrow(ifP, inputRow, cols, maxval, format);
+            for (col = 0; col < cols; ++col) {
+                if (colorIsInSet(inputRow[col], maxval, cmdline)) {
+                    maskRow[col] = PBM_BLACK;
+                    ++numPixelsMasked;
+                } else 
+                    maskRow[col] = PBM_WHITE;
+            }
+            pbm_writepbmrow(stdout, maskRow, cols, 0);
+        }
+
+        if (cmdline.verbose)
+            pm_message("%u pixels found matching %u requested colors",
+                       numPixelsMasked, cmdline.colorCount);
+
+        pbm_freerow(maskRow);
+        ppm_freerow(inputRow);
+    }
+    pm_close(ifP);
+
+    return 0;
+}
+
+
+
diff --git a/editor/ppmdim.c b/editor/ppmdim.c
new file mode 100644
index 00000000..4e64965a
--- /dev/null
+++ b/editor/ppmdim.c
@@ -0,0 +1,112 @@
+
+/*********************************************************************/
+/* ppmdim -  dim a picture down to total blackness                   */
+/* Frank Neumann, October 1993                                       */
+/* V1.4 16.11.1993                                                   */
+/*                                                                   */
+/* version history:                                                  */
+/* V1.0 ~ 15.August 1993    first version                            */
+/* V1.1 03.09.1993          uses ppm libs & header files             */
+/* V1.2 03.09.1993          integer arithmetics instead of float     */
+/*                          (gains about 50 % speed up)              */
+/* V1.3 10.10.1993          reads only one line at a time - this     */
+/*                          saves LOTS of memory on big pictures     */
+/* V1.4 16.11.1993          Rewritten to be NetPBM.programming con-  */
+/*                          forming                                  */
+/*********************************************************************/
+
+#include "ppm.h"
+
+/* global variables */
+#ifdef AMIGA
+static char *version = "$VER: ppmdim 1.4 (16.11.93)"; /* Amiga version identification */
+#endif
+
+/**************************/
+/* start of main function */
+/**************************/
+int main(argc, argv)
+int argc;
+char *argv[];
+{
+	FILE* ifp;
+	int argn, rows, cols, format, i = 0, j = 0;
+	pixel *srcrow, *destrow;
+	pixel *pP = NULL, *pP2 = NULL;
+	pixval maxval;
+	double dimfactor;
+	long longfactor;
+	const char * const usage = "dimfactor [ppmfile]\n        dimfactor: 0.0 = total blackness, 1.0 = original picture\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", &dimfactor) != 1)
+		pm_usage(usage);
+	if (dimfactor < 0.0 || dimfactor > 1.0)
+		pm_error("dim factor must be in the range from 0.0 to 1.0 ");
+	++argn;
+
+	/* parse in filename (if present, stdin otherwise) */
+	if (argn != argc)
+	{
+		ifp = pm_openr(argv[argn]);
+		++argn;
+	}
+	else
+		ifp = stdin;
+
+	if (argn != argc)
+		pm_usage(usage);
+
+	/* read first data from file */
+	ppm_readppminit(ifp, &cols, &rows, &maxval, &format);
+
+	/* no error checking required here, ppmlib does it all for us */
+	srcrow = ppm_allocrow(cols);
+
+	longfactor = (long)(dimfactor * 65536);
+
+	/* allocate a row of pixel data for the new pixels */
+	destrow = ppm_allocrow(cols);
+
+	ppm_writeppminit(stdout, cols, rows, maxval, 0);
+
+	/** now do the dim'ing **/
+	/* the 'float' parameter for dimming is sort of faked - in fact, we */
+	/* convert it to a range from 0 to 65536 for integer math. Shouldn't */
+	/* be something you'll have to worry about, though. */
+
+	for (i = 0; i < rows; i++)
+	{
+		ppm_readppmrow(ifp, srcrow, cols, maxval, format);
+
+		pP = srcrow;
+		pP2 = destrow;
+
+		for (j = 0; j < cols; j++)
+		{
+			PPM_ASSIGN(*pP2, (PPM_GETR(*pP) * longfactor) >> 16,
+							 (PPM_GETG(*pP) * longfactor) >> 16,
+							 (PPM_GETB(*pP) * longfactor) >> 16);
+
+			pP++;
+			pP2++;
+		}
+
+		/* write out one line of graphic data */
+		ppm_writeppmrow(stdout, destrow, cols, maxval, 0);
+	}
+
+	pm_close(ifp);
+	ppm_freerow(srcrow);
+	ppm_freerow(destrow);
+
+	exit(0);
+}
+
diff --git a/editor/ppmdist.c b/editor/ppmdist.c
new file mode 100644
index 00000000..90c2e3d3
--- /dev/null
+++ b/editor/ppmdist.c
@@ -0,0 +1,170 @@
+#include "ppm.h"
+#include "mallocvar.h"
+
+
+/*
+ * Yep, it's a very simple algorithm, but it was something I wanted to have
+ * available.
+ */
+
+struct colorToGrayEntry {
+    pixel           color;
+    gray            gray;
+    int             frequency;
+};
+
+/*
+ * BUG: This number was chosen pretty arbitrarily.  The program is * probably
+ * only useful for a very small numbers of colors - and that's * not only
+ * because of the O(n) search that's used.  The idea lends * itself primarily
+ * to low color (read: simple, machine generated) images.
+ */
+#define MAXCOLORS 255
+
+
+static gray
+newGrayValue(pixel *pix, struct colorToGrayEntry *colorToGrayMap, int colors) {
+
+    int color;
+    /*
+     * Allowing this to be O(n), since the program is intended for small
+     * n.  Later, perhaps sort by color (r, then g, then b) and bsearch.
+     */
+    for (color = 0; color < colors; color++) {
+        if (PPM_EQUAL(*pix, colorToGrayMap[color].color))
+            return colorToGrayMap[color].gray;
+    }
+    pm_error("This should never happen - contact the maintainer");
+    return (-1);
+}
+
+
+
+static int
+cmpColorToGrayEntryByIntensity(const void *entry1, const void *entry2) {
+
+    return ((struct colorToGrayEntry *) entry1)->gray -
+        ((struct colorToGrayEntry *) entry2)->gray;
+}
+
+
+
+static int
+cmpColorToGrayEntryByFrequency(const void * entry1, const void * entry2) {
+
+    return ((struct colorToGrayEntry *) entry1)->frequency -
+        ((struct colorToGrayEntry *) entry2)->frequency;
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    FILE           *ifp;
+    int             col, cols, row, rows, color, colors, argn;
+    int             frequency;
+    pixval          maxval;
+    pixel         **pixels;
+    pixel          *pP;
+    colorhist_vector hist;
+    gray           *grayrow;
+    gray           *gP;
+    struct colorToGrayEntry *colorToGrayMap;
+
+
+    ppm_init(&argc, argv);
+
+    argn = 1;
+    /* Default is to sort colors by intensity */
+    frequency = 0;
+
+    while (argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0') {
+        if (pm_keymatch(argv[argn], "-frequency", 2))
+            frequency = 1;
+        else if (pm_keymatch(argv[argn], "-intensity", 2))
+            frequency = 0;
+        else
+            pm_usage( "[-frequency|-intensity] [ppmfile]" );
+        ++argn;
+    }
+
+    if (argn < argc) {
+        ifp = pm_openr(argv[argn]);
+        ++argn;
+    } else
+        ifp = stdin;
+
+    pixels = ppm_readppm(ifp, &cols, &rows, &maxval);
+    pm_close(ifp);
+    /* all done with the input file - it's entirely in memory */
+
+    /*
+     * Compute a histogram of the colors in the input.  This is good for
+     * both frequency, and indirectly the intensity, of a color.
+     */
+    hist = ppm_computecolorhist(pixels, cols, rows, MAXCOLORS, &colors);
+
+    if (hist == (colorhist_vector) 0)
+        /*
+         * BUG: This perhaps should use an exponential backoff, in
+         * the number of colors, until success - cf pnmcolormap's
+         * approach.  The results are then more what's expected, but
+         * not necessarily very useful.
+         */
+        pm_error("Too many colors - Try reducing with pnmquant");
+
+    /* copy the colors into another structure for sorting */
+    MALLOCARRAY(colorToGrayMap, colors);
+    for (color = 0; color < colors; color++) {
+        colorToGrayMap[color].color = hist[color].color;
+        colorToGrayMap[color].frequency = hist[color].value;
+        /*
+         * This next is derivable, of course, but it's far faster to
+         * store it precomputed.  This can be skipped, when sorting
+         * by frequency - but again, for a small number of colors
+         * it's a small matter.
+         */
+        colorToGrayMap[color].gray = PPM_LUMIN(hist[color].color);
+    }
+
+    /*
+     * sort by intensity - sorting by frequency (in the histogram) is
+     * worth considering as a future addition.
+     */
+    if (frequency)
+        qsort(colorToGrayMap, colors, sizeof(struct colorToGrayEntry),
+              &cmpColorToGrayEntryByFrequency);
+    else
+        qsort(colorToGrayMap, colors, sizeof(struct colorToGrayEntry),
+              &cmpColorToGrayEntryByIntensity);
+
+    /*
+     * create mapping between the n colors in input, to n evenly spaced
+     * grayscale intensities.  This is done by overwriting the neatly
+     * formed gray values corresponding to the input-colors, with a new
+     * set of evenly spaced gray values.  Since maxval can be changed on
+     * a lark, we just use gray levels 0..colors-1, and adjust maxval
+     * accordingly
+     */
+    maxval = colors - 1;
+    for (color = 0; color < colors; color++)
+        colorToGrayMap[color].gray = color;
+
+    /* write pgm file, mapping colors to intensities */
+    pgm_writepgminit(stdout, cols, rows, maxval, 0);
+
+    grayrow = pgm_allocrow(cols);
+
+    for (row = 0; row < rows; row++) {
+        for (col = 0, pP = pixels[row], gP = grayrow; col < cols;
+             col++, pP++, gP++)
+            *gP = newGrayValue(pP, colorToGrayMap, colors);
+        pgm_writepgmrow(stdout, grayrow, cols, maxval, 0);
+    }
+
+    pm_close(stdout);
+
+    exit(0);
+}
+
diff --git a/editor/ppmdither.c b/editor/ppmdither.c
new file mode 100644
index 00000000..beb45e2f
--- /dev/null
+++ b/editor/ppmdither.c
@@ -0,0 +1,309 @@
+/* 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.
+*/
+
+#include "ppm.h"
+#include "mallocvar.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)
+
+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 unsigned int
+dither(pixval const p,
+       pixval const maxval,
+       unsigned int const d,
+       unsigned int const ditheredMaxval) {
+/*----------------------------------------------------------------------------
+  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.
+
+  'd' is the entry in the dithering matrix for the position of this pixel
+  within the dithered square.
+-----------------------------------------------------------------------------*/
+    unsigned int const ditherSquareMaxval = ditheredMaxval * dith_dm2;
+        /* 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) / dith_dm2;
+}
+
+
+/* 
+ *	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) { 
+
+    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.
+     */
+    int i;
+    for (i = 0, d = 0; i < dith_power; i++, x >>= 1, y >>= 1)
+        d = (d << 2) | (((x & 1) ^ (y & 1)) << 1) | (y & 1);
+    return(d);
+} /* end dith_value */
+
+
+
+static unsigned int **
+dith_matrix(unsigned int const dith_dim) {
+/*----------------------------------------------------------------------------
+   Create the dithering matrix for dimension 'dith_dim'.
+
+   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 ** dith_mat;
+    {
+        unsigned int const dith_mat_sz = 
+            (dith_dim * sizeof(int *)) + /* pointers */
+            (dith_dim * dith_dim * sizeof(int)); /* data */
+
+        dith_mat = (unsigned int **) malloc(dith_mat_sz);
+
+        if (dith_mat == NULL) 
+            pm_error("Out of memory.  "
+                     "Cannot allocate %d bytes for dithering matrix.",
+                     dith_mat_sz);
+    }
+    {
+        unsigned int * const dat = (unsigned int *) &dith_mat[dith_dim];
+        unsigned int y;
+        for (y = 0; y < dith_dim; y++)
+            dith_mat[y] = &dat[y * dith_dim];
+    }
+    {
+        unsigned int y;
+        for (y = 0; y < dith_dim; 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");
+        }
+    }
+    return dith_mat;
+}
+
+    
+
+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) {
+/*----------------------------------------------------------------------------
+   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.
+-----------------------------------------------------------------------------*/
+    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;
+    }
+
+    dith_mat = dith_matrix(dith_dim);
+} /* end dith_setup */
+
+
+/* 
+ *  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)];
+        }
+}
+
+
+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);
+}
diff --git a/editor/ppmdraw.c b/editor/ppmdraw.c
new file mode 100644
index 00000000..5a4be96b
--- /dev/null
+++ b/editor/ppmdraw.c
@@ -0,0 +1,885 @@
+#define _XOPEN_SOURCE    /* Make sure M_PI is in math.h */
+#define _BSD_SOURCE      /* Make sure strdup is in string.h */
+
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+#include <math.h>
+
+#include "pm_c_util.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+#include "nstring.h"
+#include "ppm.h"
+#include "ppmdraw.h"
+#include "ppmdfont.h"
+
+static bool verbose;
+
+
+static double
+sindeg(double const angle) {
+
+    return sin((double)angle / 360 * 2 * M_PI);
+}
+
+
+static double
+cosdeg(double const angle) {
+
+    return cos((double)angle / 360 * 2 * M_PI);
+}
+
+
+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 */
+    const char * scriptfile;     /* NULL means none. '-' means stdin */
+    const char * script;         /* NULL means none */
+    unsigned int verbose;
+};
+
+
+
+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;
+
+    unsigned int scriptSpec, scriptfileSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "script",      OPT_STRING,    &cmdlineP->script,
+            &scriptSpec,      0);
+    OPTENT3(0, "scriptfile",  OPT_STRING,    &cmdlineP->scriptfile,
+            &scriptfileSpec,  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 *cmdlineP and others. */
+    
+    if (!scriptSpec && !scriptfileSpec)
+        pm_error("You must specify either -script or -scriptfile");
+
+    if (scriptSpec && scriptfileSpec)
+        pm_error("You may not specify both -script and -scriptfile");
+
+    if (!scriptSpec)
+        cmdlineP->script = NULL;
+    if (!scriptfileSpec)
+        cmdlineP->scriptfile = NULL;
+
+    if (argc-1 < 1) {
+        if (cmdlineP->scriptfile && strcmp(cmdlineP->scriptfile, "-") == 0)
+            pm_error("You can't specify Standard Input for both the "
+                     "input image and the script file");
+        else
+            cmdlineP->inputFilename = "-";
+    }
+    else if (argc-1 == 1)
+        cmdlineP->inputFilename = argv[1];
+    else
+        pm_error("Program takes at most one argument:  input file name");
+}
+
+
+struct pos {
+    unsigned int x;
+    unsigned int y;
+};
+
+struct drawState {
+    struct pos currentPos;
+    pixel color;
+};
+
+
+
+static void
+initDrawState(struct drawState * const drawStateP,
+              pixval             const maxval) {
+
+    drawStateP->currentPos.x = 0;
+    drawStateP->currentPos.y = 0;
+    PPM_ASSIGN(drawStateP->color, maxval, maxval, maxval);
+}
+
+
+
+static void
+readScriptFile(const char *  const scriptFileName,
+               const char ** const scriptP) {
+
+    FILE * scriptFileP;
+    char * script;
+    size_t scriptAllocation;
+    size_t bytesReadSoFar;
+
+    scriptAllocation = 4096;
+
+    MALLOCARRAY(script, scriptAllocation);
+
+    if (script == NULL)
+        pm_error("out of memory reading script from file");
+
+    scriptFileP = pm_openr(scriptFileName);
+
+    bytesReadSoFar = 0;
+    while (!feof(scriptFileP)) {
+        size_t bytesRead;
+
+        if (scriptAllocation - bytesReadSoFar < 2) {
+            scriptAllocation += 4096;
+            REALLOCARRAY(script, scriptAllocation);
+            if (script == NULL)
+                pm_error("out of memory reading script from file");
+        }
+        bytesRead = fread(script + bytesReadSoFar, 1,
+                          scriptAllocation - bytesReadSoFar - 1, scriptFileP);
+        bytesReadSoFar += bytesRead;
+    }
+    pm_close(scriptFileP);
+
+    {
+        unsigned int i;
+        for (i = 0; i < bytesReadSoFar; ++i)
+            if (!isprint(script[i]) && !isspace(script[i]))
+                pm_error("Script contains byte that is not printable ASCII "
+                         "character: 0x%02x", script[i]);
+    }
+    script[bytesReadSoFar] = '\0';  /* terminating NUL */
+
+    *scriptP = script;
+}
+
+
+
+enum drawVerb {
+    VERB_SETPOS,
+    VERB_SETLINETYPE,
+    VERB_SETLINECLIP,
+    VERB_SETCOLOR,
+    VERB_SETFONT,
+    VERB_LINE,
+    VERB_LINE_HERE,
+    VERB_SPLINE3,
+    VERB_CIRCLE,
+    VERB_FILLEDRECTANGLE,
+    VERB_TEXT,
+    VERB_TEXT_HERE
+};
+
+struct setposArg {
+    int x;
+    int y;
+};
+
+struct setlinetypeArg {
+    int type;
+};
+
+struct setlineclipArg {
+    unsigned int clip;
+};
+
+struct setcolorArg {
+    const char * colorName;
+};
+
+struct setfontArg {
+    const char * fontFileName;
+};
+
+struct lineArg {
+    int x0;
+    int y0;
+    int x1;
+    int y1;
+};
+
+struct lineHereArg {
+    int right;
+    int down;
+};
+
+struct spline3Arg {
+    int x0;
+    int y0;
+    int x1;
+    int y1;
+    int x2;
+    int y2;
+};
+
+struct circleArg {
+    int cx;
+    int cy;
+    unsigned int radius;
+};
+
+struct filledrectangleArg {
+    int x;
+    int y;
+    unsigned int width;
+    unsigned int height;
+};
+
+struct textArg {
+    int xpos;
+    int ypos;
+    unsigned int height;
+    int angle;
+    const char * text;
+};
+
+
+struct drawCommand {
+    enum drawVerb verb;
+    union {
+        struct setposArg          setposArg;
+        struct setlinetypeArg     setlinetypeArg;
+        struct setlineclipArg     setlineclipArg;
+        struct setcolorArg        setcolorArg;
+        struct setfontArg         setfontArg;
+        struct lineArg            lineArg;
+        struct lineHereArg        lineHereArg;
+        struct spline3Arg         spline3Arg;
+        struct circleArg          circleArg;
+        struct filledrectangleArg filledrectangleArg;
+        struct textArg            textArg;
+    } u;
+};
+
+
+
+static void
+freeDrawCommand(const struct drawCommand * const commandP) {
+    
+    switch (commandP->verb) {
+    case VERB_SETPOS:
+        break;
+    case VERB_SETLINETYPE:
+        break;
+    case VERB_SETLINECLIP:
+        break;
+    case VERB_SETCOLOR:
+        strfree(commandP->u.setcolorArg.colorName);
+        break;
+    case VERB_SETFONT:
+        strfree(commandP->u.setfontArg.fontFileName);
+        break;
+    case VERB_LINE:
+        break;
+    case VERB_LINE_HERE:
+        break;
+    case VERB_SPLINE3:
+        break;
+    case VERB_CIRCLE:
+        break;
+    case VERB_FILLEDRECTANGLE:
+        break;
+    case VERB_TEXT:
+    case VERB_TEXT_HERE:
+        strfree(commandP->u.textArg.text);
+        break;
+    }
+    
+
+    free((void *) commandP);
+}
+
+
+
+struct commandListElt {
+    struct commandListElt * nextP;
+    const struct drawCommand * commandP;
+};
+
+
+struct script {
+    struct commandListElt * commandListHeadP;
+    struct commandListElt * commandListTailP;
+};
+
+
+
+static void
+freeScript(struct script * const scriptP) {
+
+    struct commandListElt * p;
+
+    for (p = scriptP->commandListHeadP; p; p = p->nextP) {
+        freeDrawCommand(p->commandP);
+        free(p);
+    }
+
+    free(scriptP);
+}
+
+
+
+static void
+doTextHere(pixel **                   const pixels,
+           unsigned int               const cols,
+           unsigned int               const rows,
+           pixval                     const maxval,
+           const struct drawCommand * const commandP,
+           struct drawState *         const drawStateP) {
+    
+    ppmd_text(pixels, cols, rows, maxval,
+              drawStateP->currentPos.x,
+              drawStateP->currentPos.y,
+              commandP->u.textArg.height,
+              commandP->u.textArg.angle,
+              commandP->u.textArg.text,
+              PPMD_NULLDRAWPROC,
+              &drawStateP->color);
+    
+    {
+        int left, top, right, bottom;
+        
+        ppmd_text_box(commandP->u.textArg.height, 0,
+                      commandP->u.textArg.text,
+                      &left, &top, &right, &bottom);
+        
+
+        drawStateP->currentPos.x +=
+            ROUND((right-left) * cosdeg(commandP->u.textArg.angle));
+        drawStateP->currentPos.y -=
+            ROUND((right-left) * sindeg(commandP->u.textArg.angle));
+    }
+}
+
+
+
+static void
+executeScript(struct script * const scriptP,
+              pixel **        const pixels,
+              unsigned int    const cols,
+              unsigned int    const rows,
+              pixval          const maxval) {
+
+    struct drawState drawState;
+    unsigned int seq;
+        /* Sequence number of current command (0 = first, etc.) */
+    struct commandListElt * p;
+        /* Pointer to current element in command list */
+
+    initDrawState(&drawState, maxval);
+
+    for (p = scriptP->commandListHeadP, seq = 0; p; p = p->nextP, ++seq) {
+        const struct drawCommand * const commandP = p->commandP;
+
+        if (verbose)
+            pm_message("Command %u: %u", seq, commandP->verb);
+
+        switch (commandP->verb) {
+        case VERB_SETPOS:
+            drawState.currentPos.x = commandP->u.setposArg.x;
+            drawState.currentPos.y = commandP->u.setposArg.y;
+            break;
+        case VERB_SETLINETYPE:
+            ppmd_setlinetype(commandP->u.setlinetypeArg.type);
+            break;
+        case VERB_SETLINECLIP:
+            ppmd_setlineclip(commandP->u.setlineclipArg.clip);
+            break;
+        case VERB_SETCOLOR:
+            drawState.color =
+                ppm_parsecolor2(commandP->u.setcolorArg.colorName,
+                                maxval, TRUE);
+            break;
+        case VERB_SETFONT: {
+            FILE * ifP;
+            const struct ppmd_font * fontP;
+            ifP = pm_openr(commandP->u.setfontArg.fontFileName);
+            ppmd_read_font(ifP, &fontP);
+            ppmd_set_font(fontP);
+            pm_close(ifP);
+        } break;
+        case VERB_LINE:
+            ppmd_line(pixels, cols, rows, maxval,
+                      commandP->u.lineArg.x0, commandP->u.lineArg.y0,
+                      commandP->u.lineArg.x1, commandP->u.lineArg.y1,
+                      PPMD_NULLDRAWPROC,
+                      &drawState.color);
+            break;
+        case VERB_LINE_HERE: {
+            struct pos endPos;
+
+            endPos.x = drawState.currentPos.x + commandP->u.lineHereArg.right;
+            endPos.y = drawState.currentPos.y + commandP->u.lineHereArg.down;
+
+            ppmd_line(pixels, cols, rows, maxval,
+                      drawState.currentPos.x, drawState.currentPos.y,
+                      endPos.x, endPos.y,
+                      PPMD_NULLDRAWPROC,
+                      &drawState.color);
+            drawState.currentPos = endPos;
+        } break;
+        case VERB_SPLINE3:
+            ppmd_spline3(pixels, cols, rows, maxval,
+                         commandP->u.spline3Arg.x0,
+                         commandP->u.spline3Arg.y0,
+                         commandP->u.spline3Arg.x1,
+                         commandP->u.spline3Arg.y1,
+                         commandP->u.spline3Arg.x2,
+                         commandP->u.spline3Arg.y2,
+                         PPMD_NULLDRAWPROC,
+                         &drawState.color);
+            break;
+        case VERB_CIRCLE:
+            ppmd_circle(pixels, cols, rows, maxval,
+                        commandP->u.circleArg.cx,
+                        commandP->u.circleArg.cy,
+                        commandP->u.circleArg.radius,
+                        PPMD_NULLDRAWPROC,
+                        &drawState.color);
+            break;
+        case VERB_FILLEDRECTANGLE:
+            ppmd_filledrectangle(pixels, cols, rows, maxval,
+                                 commandP->u.filledrectangleArg.x,
+                                 commandP->u.filledrectangleArg.y,
+                                 commandP->u.filledrectangleArg.width,
+                                 commandP->u.filledrectangleArg.height,
+                                 PPMD_NULLDRAWPROC,
+                                 &drawState.color);
+            break;
+        case VERB_TEXT:
+            ppmd_text(pixels, cols, rows, maxval,
+                      commandP->u.textArg.xpos,
+                      commandP->u.textArg.ypos,
+                      commandP->u.textArg.height,
+                      commandP->u.textArg.angle,
+                      commandP->u.textArg.text,
+                      PPMD_NULLDRAWPROC,
+                      &drawState.color);
+            break;
+        case VERB_TEXT_HERE:
+            doTextHere(pixels, cols, rows, maxval, commandP, &drawState);
+            break;
+        }
+    }
+}
+
+
+
+struct tokenSet {
+    
+    const char * token[10];
+    unsigned int count;
+    
+};
+
+
+
+static void
+parseDrawCommand(struct tokenSet             const commandTokens,
+                 const struct drawCommand ** const drawCommandPP) {
+
+    struct drawCommand * drawCommandP;
+
+    if (commandTokens.count < 1)
+        pm_error("No tokens in command.");
+    else {
+        const char * const verb = commandTokens.token[0];
+
+        MALLOCVAR(drawCommandP);
+        if (drawCommandP == NULL)
+            pm_error("Out of memory to parse '%s' command", verb);
+
+        if (STREQ(verb, "setpos")) {
+            drawCommandP->verb = VERB_SETPOS;
+            if (commandTokens.count < 3)
+                pm_error("Not enough tokens for a 'setpos' command.  "
+                         "Need %u.  Got %u", 3, commandTokens.count);
+            else {
+                drawCommandP->u.setposArg.x = atoi(commandTokens.token[1]);
+                drawCommandP->u.setposArg.y = atoi(commandTokens.token[2]);
+            }
+        } else if (STREQ(verb, "setlinetype")) {
+            drawCommandP->verb = VERB_SETLINETYPE;
+            if (commandTokens.count < 2)
+                pm_error("Not enough tokens for a 'setlinetype' command.  "
+                         "Need %u.  Got %u", 2, commandTokens.count);
+            else {
+                const char * const typeArg = commandTokens.token[1];
+                if (STREQ(typeArg, "normal"))
+                    drawCommandP->u.setlinetypeArg.type = PPMD_LINETYPE_NORMAL;
+                else if (STREQ(typeArg, "normal"))
+                    drawCommandP->u.setlinetypeArg.type = 
+                        PPMD_LINETYPE_NODIAGS;
+                else
+                    pm_error("Invalid type");
+            }
+        } else if (STREQ(verb, "setlineclip")) {
+            drawCommandP->verb = VERB_SETLINECLIP;
+            if (commandTokens.count < 2)
+                pm_error("Not enough tokens for a 'setlineclip' command.  "
+                         "Need %u.  Got %u", 2, commandTokens.count);
+            else
+                drawCommandP->u.setlineclipArg.clip =
+                    atoi(commandTokens.token[1]);
+        } else if (STREQ(verb, "setcolor")) {
+            drawCommandP->verb = VERB_SETCOLOR;
+            if (commandTokens.count < 2)
+                pm_error("Not enough tokens for a 'setcolor' command.  "
+                         "Need %u.  Got %u", 2, commandTokens.count);
+            else
+                drawCommandP->u.setcolorArg.colorName =
+                    strdup(commandTokens.token[1]);
+        } else if (STREQ(verb, "setfont")) {
+            drawCommandP->verb = VERB_SETFONT;
+            if (commandTokens.count < 2)
+                pm_error("Not enough tokens for a 'setfont' command.  "
+                         "Need %u.  Got %u", 2, commandTokens.count);
+            else
+                drawCommandP->u.setfontArg.fontFileName =
+                    strdup(commandTokens.token[1]);
+        } else if (STREQ(verb, "line")) {
+            drawCommandP->verb = VERB_LINE;
+            if (commandTokens.count < 5)
+                pm_error("Not enough tokens for a 'line' command.  "
+                         "Need %u.  Got %u", 5, commandTokens.count);
+            else {
+                drawCommandP->u.lineArg.x0 = atoi(commandTokens.token[1]);
+                drawCommandP->u.lineArg.y0 = atoi(commandTokens.token[2]);
+                drawCommandP->u.lineArg.x1 = atoi(commandTokens.token[3]);
+                drawCommandP->u.lineArg.y1 = atoi(commandTokens.token[4]);
+            } 
+        } else if (STREQ(verb, "line_here")) {
+            drawCommandP->verb = VERB_LINE_HERE;
+            if (commandTokens.count < 3)
+                pm_error("Not enough tokens for a 'line_here' command.  "
+                         "Need %u.  Got %u", 3, commandTokens.count);
+            else {
+                struct lineHereArg * const argP =
+                    &drawCommandP->u.lineHereArg;
+                argP->right = atoi(commandTokens.token[1]);
+                argP->down = atoi(commandTokens.token[2]);
+            } 
+       } else if (STREQ(verb, "spline3")) {
+            drawCommandP->verb = VERB_SPLINE3;
+            if (commandTokens.count < 7)
+                pm_error("Not enough tokens for a 'spline3' command.  "
+                         "Need %u.  Got %u", 7, commandTokens.count);
+            else {
+                struct spline3Arg * const argP =
+                    &drawCommandP->u.spline3Arg;
+                argP->x0 = atoi(commandTokens.token[1]);
+                argP->y0 = atoi(commandTokens.token[2]);
+                argP->x1 = atoi(commandTokens.token[3]);
+                argP->y1 = atoi(commandTokens.token[4]);
+                argP->x2 = atoi(commandTokens.token[5]);
+                argP->y2 = atoi(commandTokens.token[6]);
+            } 
+        } else if (STREQ(verb, "circle")) {
+            drawCommandP->verb = VERB_CIRCLE;
+            if (commandTokens.count < 4)
+                pm_error("Not enough tokens for a 'circle' 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)
+                pm_error("Not enough tokens for a 'filledrectangle' command.  "
+                         "Need %u.  Got %u", 4, commandTokens.count);
+            else {
+                struct filledrectangleArg * const argP =
+                    &drawCommandP->u.filledrectangleArg;
+                argP->x      = atoi(commandTokens.token[1]);
+                argP->y      = atoi(commandTokens.token[2]);
+                argP->width  = atoi(commandTokens.token[3]);
+                argP->height = atoi(commandTokens.token[4]);
+            } 
+        } else if (STREQ(verb, "text")) {
+            drawCommandP->verb = VERB_TEXT;
+            if (commandTokens.count < 6)
+                pm_error("Not enough tokens for a 'text' command.  "
+                         "Need %u.  Got %u", 6, commandTokens.count);
+            else {
+                drawCommandP->u.textArg.xpos  = atoi(commandTokens.token[1]);
+                drawCommandP->u.textArg.ypos  = atoi(commandTokens.token[2]);
+                drawCommandP->u.textArg.height= atoi(commandTokens.token[3]);
+                drawCommandP->u.textArg.angle = atoi(commandTokens.token[4]);
+                drawCommandP->u.textArg.text  = strdup(commandTokens.token[5]);
+                if (drawCommandP->u.textArg.text == NULL)
+                    pm_error("Out of storage parsing 'text' command");
+            }
+        } else if (STREQ(verb, "text_here")) {
+            drawCommandP->verb = VERB_TEXT_HERE;
+            if (commandTokens.count < 4)
+                pm_error("Not enough tokens for a 'text_here' command.  "
+                         "Need %u.  Got %u", 4, commandTokens.count);
+            else {
+                drawCommandP->u.textArg.height= atoi(commandTokens.token[1]);
+                drawCommandP->u.textArg.angle = atoi(commandTokens.token[2]);
+                drawCommandP->u.textArg.text  = strdup(commandTokens.token[3]);
+                if (drawCommandP->u.textArg.text == NULL)
+                    pm_error("Out of storage parsing 'text_here' command");
+            }
+        } else
+            pm_error("Unrecognized verb '%s'", verb);
+    }
+    *drawCommandPP = drawCommandP;
+}
+
+
+
+static void
+disposeOfCommandTokens(struct tokenSet * const tokenSetP,
+                       struct script *   const scriptP) {
+
+    /* We've got a whole command in 'tokenSet'.  Parse it into *scriptP
+       and reset tokenSet to empty.
+    */
+    
+    struct commandListElt * commandListEltP;
+    
+    MALLOCVAR(commandListEltP);
+    if (commandListEltP == NULL)
+        pm_error("Out of memory allocating command list element frame");
+
+    parseDrawCommand(*tokenSetP, &commandListEltP->commandP);
+
+    {
+        unsigned int i;
+        for (i = 0; i < tokenSetP->count; ++i)
+            strfree(tokenSetP->token[i]);
+        tokenSetP->count = 0;
+    }
+    /* Put the list element for this command at the tail of the list */
+    commandListEltP->nextP = NULL;
+    if (scriptP->commandListTailP)
+        scriptP->commandListTailP->nextP = commandListEltP;
+    else
+        scriptP->commandListHeadP = commandListEltP;
+
+    scriptP->commandListTailP = commandListEltP;
+}
+
+
+
+static void
+processToken(const char *      const scriptText,
+             unsigned int      const cursor,
+             unsigned int      const tokenStart,
+             struct script *   const scriptP,
+             struct tokenSet * const tokenSetP) {
+
+    char * token;
+    unsigned int const tokenLength = cursor - tokenStart;
+    MALLOCARRAY_NOFAIL(token, tokenLength + 1);
+    memcpy(token, &scriptText[tokenStart], tokenLength);
+    token[tokenLength] = '\0';
+    
+    if (STREQ(token, ";")) {
+        disposeOfCommandTokens(tokenSetP, scriptP);
+        free(token);
+    } else {
+        if (tokenSetP->count >= ARRAY_SIZE(tokenSetP->token))
+            pm_error("too many tokens");
+        else
+            tokenSetP->token[tokenSetP->count++] = token;
+    }
+}
+
+
+
+static void
+parseScript(const char *     const scriptText,
+            struct script ** const scriptPP) {
+
+    struct script * scriptP;
+    unsigned int cursor;  /* cursor in scriptText[] */
+    bool intoken;      /* Cursor is inside token */
+    unsigned int tokenStart;
+        /* Position in 'scriptText' where current token starts.
+           Meaningless if 'intoken' is false.
+        */
+    bool quotedToken;
+        /* Current token is a quoted string.  Meaningless if 'intoken'
+           is false 
+        */
+    struct tokenSet tokenSet;
+
+    MALLOCVAR_NOFAIL(scriptP);
+
+    scriptP->commandListHeadP = NULL;
+    scriptP->commandListTailP = NULL;
+
+    /* A token begins with a non-whitespace character.  A token ends before
+       a whitespace character or semicolon or end of script, except that if
+       the token starts with a double quote, whitespace and semicolon don't
+       end it and another double quote does.
+
+       Semicolon (unquoted) is a token by itself.
+    */
+
+    tokenSet.count = 0;
+    intoken = FALSE;
+    tokenStart = 0;
+    cursor = 0;
+
+    while (scriptText[cursor] != '\0') {
+        char const scriptChar = scriptText[cursor];
+        
+        if (intoken) {
+            if ((quotedToken && scriptChar == '"') ||
+                (!quotedToken && (isspace(scriptChar) || scriptChar == ';'))) {
+                /* We've passed a token. */
+
+                processToken(scriptText, cursor, tokenStart, scriptP,
+                             &tokenSet);
+
+                intoken = FALSE;
+                if (scriptChar != ';')
+                    ++cursor;
+            } else
+                ++cursor;
+        } else {
+            if (!isspace(scriptChar)) {
+                /* A token starts here */
+
+                if (scriptChar == ';')
+                    /* It ends here too -- semicolon is token by itself */
+                    processToken(scriptText, cursor+1, cursor, scriptP,
+                                 &tokenSet);
+                else {
+                    intoken = TRUE;
+                    quotedToken = (scriptChar == '"');
+                    if (quotedToken)
+                        tokenStart = cursor + 1;
+                    else
+                        tokenStart = cursor;
+                }
+            }
+            ++cursor;
+        }            
+    }
+
+    if (intoken) {
+        /* Parse the last token, which was terminated by end of string */
+        if (quotedToken)
+            pm_error("Script ends in the middle of a quoted string");
+        processToken(scriptText, cursor, tokenStart, scriptP, &tokenSet);
+    }
+
+    if (tokenSet.count > 0) {
+        /* Parse the last command, which was not terminated with a semicolon.
+         */
+        disposeOfCommandTokens(&tokenSet, scriptP);
+    }
+
+    *scriptPP = scriptP;
+}
+
+
+
+static void
+getScript(struct cmdlineInfo const cmdline,
+          struct script **   const scriptPP) {
+
+    const char * scriptText;
+
+    if (cmdline.script) {
+        scriptText = strdup(cmdline.script);
+        if (scriptText == NULL)
+            pm_error("Out of memory creating script");
+    } else if (cmdline.scriptfile)
+        readScriptFile(cmdline.scriptfile, &scriptText);
+    else
+        pm_error("INTERNAL ERROR: no script");
+
+    if (verbose)
+        pm_message("Executing script '%s'", scriptText);
+
+    parseScript(scriptText, scriptPP);
+
+    strfree(scriptText);
+}
+
+          
+
+static void
+doOneImage(FILE *          const ifP,
+           struct script * const scriptP)  {
+
+    pixel ** pixels;
+    pixval maxval;
+    int rows, cols;
+    
+    pixels = ppm_readppm(ifP, &cols, &rows, &maxval);
+    
+    executeScript(scriptP, pixels, cols, rows, maxval);
+    
+    ppm_writeppm(stdout, pixels, cols, rows, maxval, 0);
+    
+    ppm_freearray(pixels, rows);
+}
+
+
+
+int
+main(int argc, char * argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    struct script * scriptP;
+    bool eof;
+
+    ppm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    verbose = cmdline.verbose;
+
+    ifP = pm_openr(cmdline.inputFilename);
+
+    getScript(cmdline, &scriptP);
+
+    eof = FALSE;
+    while (!eof) {
+        doOneImage(ifP, scriptP);
+        ppm_nextimage(ifP, &eof);
+    }
+
+    freeScript(scriptP);
+
+    pm_close(ifP);
+
+    /* If the program failed, it previously aborted with nonzero completion
+       code, via various function calls.
+    */
+    return 0;
+}
diff --git a/editor/ppmfade b/editor/ppmfade
new file mode 100755
index 00000000..2507eaf2
--- /dev/null
+++ b/editor/ppmfade
@@ -0,0 +1,309 @@
+#!/usr/bin/perl -w
+#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+#
+#  This program creates a fade (a sequence of frames) between two images.
+#
+#  By Bryan Henderson, Olympia WA; March 2000
+#
+#  Contributed to the public domain by its author.
+#
+#  Inspired by the program Pbmfade by Wesley C. Barris of AHPCRC,
+#  Minnesota Supercomputer Center, Inc. January 7, 1994.  Pbmfade does
+#  much the same thing, but handles non-Netpbm formats too, and is 
+#  implemented in a more primitive language.
+#
+#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+use strict;
+
+my $SPREAD =  1;
+my $SHIFT =   2;
+my $RELIEF =  3;
+my $OIL =     4;
+my $EDGE =    5;
+my $BENTLEY = 6;
+my $BLOCK =   7;
+my $MIX =     8;
+#
+#  Set some defaults.
+#
+my $nframes = 30;			# total number of files created (1 sec)
+my $first_file = "undefined";
+my $last_file = "undefined";
+my $base_name = "fade";		# default base name of output files
+my $image = "ppm";		# default output storage format
+my $mode = $SPREAD;		# default fading mode
+#
+#  Check those command line args.
+#
+if (@ARGV == 0) {
+    usage();
+}
+
+my $n;  # argument number
+
+for ($n = 0; $n < @ARGV; $n++) {
+    if ("$ARGV[$n]" eq "-f") {
+        $n++;
+        $first_file = $ARGV[$n];
+        if (-e $first_file) {
+        } else {
+            print "I can't find $first_file\n";
+            exit 20;
+        }
+    } elsif ($ARGV[$n] eq "-l") {
+        $n++;
+        $last_file = $ARGV[$n];
+        if (-e $last_file) {
+        } else {
+            print "I can't find $last_file\n";
+            exit 20;
+        }
+    } elsif ($ARGV[$n] eq "-base") {
+        $n++;
+        $base_name = $ARGV[$n];
+    } elsif ($ARGV[$n] eq "-spread") {
+        $mode = $SPREAD;
+    } elsif ($ARGV[$n] eq "-shift") {
+        $mode = $SHIFT;
+    } elsif ($ARGV[$n] eq "-relief") {
+        $mode = $RELIEF;
+    } elsif ($ARGV[$n] eq "-oil") {
+        $mode = $OIL;
+    } elsif ("$ARGV[$n]" eq "-edge") {
+        $mode = $EDGE;
+    } elsif ("$ARGV[$n]" eq "-bentley") {
+        $mode = $BENTLEY;
+    } elsif ("$ARGV[$n]" eq "-block") {
+        $mode = $BLOCK;
+    } elsif ("$ARGV[$n]" eq "-mix") {
+        $mode = $MIX;
+    } elsif ($ARGV[$n] eq "-help" || $ARGV[$n] eq "-h") {
+        usage();
+    } else {
+        print "Unknown argument: $ARGV[$n]\n";
+        exit 100;
+    } 
+}
+#
+#  Define a couple linear ramps.
+#
+# We don't use element 0 of these arrays.
+my @spline10 = (0, 0, 0.11, 0.22, 0.33, 0.44, 0.55, 0.66, 0.77, 0.88, 1.0);
+my @spline20 = (0, 0, 0.05, 0.11, 0.16, 0.21, 0.26, 0.32, 0.37, 0.42, 0.47, 
+                0.53, 0.58, 0.63, 0.69, 0.74, 0.79, 0.84, 0.89, 0.95, 1.0);
+#
+#  Just what are we supposed to do?
+#
+my ($height, $width);    # width and height of our frames
+if ($first_file ne "undefined") {
+    if ((`pnmfile $first_file` =~ m{\b(\d+)\sby\s(\d+)} )) { 
+        $width = $1; $height = $2;
+    } else {
+        print("Unrecognized results from pnmfile on $first_file.\n");
+        exit(50);
+    }
+} elsif ($last_file ne "undefined") {
+    if ((`pnmfile $last_file` =~ m{\b(\d+)\sby\s(\d+)} )) { 
+        $width = $1; $height = $2;
+    } else {
+        print("Unrecognized results from pnmfile on $first_file.\n");
+        exit(50);
+    }
+} else {
+    print("ppmfade:  You must specify -f or -l (or both)\n");
+    exit(90);
+}
+
+print("Frames are " . $width . "W x " . $height . "H\n");
+
+if ($first_file eq "undefined") {
+    print "Fading from black to ";
+    system("ppmmake \\#000 $width $height >junk1$$.ppm");
+} else {
+    print "Fading from $first_file to ";
+    system("cp", $first_file, "junk1$$.ppm");
+}
+
+if ($last_file eq "undefined") {
+    print "black.\n";
+    system("ppmmake \\#000 $width $height >junk2$$.ppm");
+} else {
+    print "$last_file\n";
+    system("cp", $last_file, "junk2$$.ppm");
+}
+
+#
+#  Perform the fade.
+#
+
+# Here's what our temporary files are:
+#   junk1$$.ppm: The original (fade-from) image
+#   junk2$$.ppm: The target (fade-from) image
+#   junk3$$.ppm: The frame of the fade for the current iteration of the 
+#                the for loop.
+#   junk1a$$.ppm: If the fade involves a ppmmix sequence from one intermediate
+#                 image to another, this is the first frame of that 
+#                 sequence.
+#   junk2a$$.ppm: This is the last frame of the above-mentioned ppmmix sequence
+
+my $i;    # Frame number
+for ($i = 1; $i <= $nframes; $i++) {
+    print("Creating $i of $nframes...\n");
+    if ($mode eq $SPREAD) {
+        if ($i <= 10) {
+            my $n = $spline20[$i] * 100;
+            system("ppmspread $n junk1$$.ppm >junk3$$.ppm");
+        } elsif ($i <= 20) {
+            my $n;
+            $n = $spline20[$i] * 100;
+            system("ppmspread $n junk1$$.ppm >junk1a$$.ppm");
+            $n = (1-$spline20[$i-10]) * 100;
+            system("ppmspread $n junk2$$.ppm >junk2a$$.ppm");
+            $n = $spline10[$i-10];
+            system("ppmmix $n junk1a$$.ppm junk2a$$.ppm >junk3$$.ppm");
+        } else {
+            my $n = (1-$spline20[$i-10])*100;
+            system("ppmspread $n junk2$$.ppm >junk3$$.ppm");
+        }
+    } elsif ($mode eq $SHIFT) {
+        if ($i <= 10) {
+            my $n = $spline20[$i] * 100;
+            system("ppmshift $n junk1$$.ppm >junk3$$.ppm");
+        } elsif ($i <= 20) {
+            my $n;
+            $n = $spline20[$i] * 100;
+            system("ppmshift $n junk1$$.ppm >junk1a$$.ppm");
+            $n = (1-$spline20[$i-10])*100;
+            system("ppmshift $n junk2$$.ppm >junk2a$$.ppm");
+            $n = $spline10[$i-10];
+            system("ppmmix $n junk1a$$.ppm junk2a$$.ppm >junk3$$.ppm");
+        } else {
+            my $n = (1-$spline20[$i-10]) * 100;
+            system("ppmshift $n junk2$$.ppm >junk3$$.ppm");
+        }
+    } elsif ($mode eq $RELIEF) {
+        if ($i == 1) {
+            system("ppmrelief junk1$$.ppm >junk1r$$.ppm");
+        }
+        if ($i <= 10) {
+            my $n = $spline10[$i];
+            system("ppmmix $n junk1$$.ppm junk1r$$.ppm >junk3$$.ppm");
+        } elsif ($i <= 20) {
+            my $n = $spline10[$i-10];
+            system("ppmmix $n junk1r$$.ppm junk2r$$.ppm >junk3$$.ppm");
+        } else {
+            my $n = $spline10[$i-20];
+            system("ppmmix $n junk2r$$.ppm junk2$$.ppm >junk3$$.ppm");
+        }
+        if ($i == 10) {
+            system("ppmrelief junk2$$.ppm >junk2r$$.ppm");
+        }
+    } elsif ($mode eq $OIL) {
+        if ($i == 1) {
+            system("ppmtopgm junk1$$.ppm | pgmoil >junko$$.ppm");
+            system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " .
+                   ">junk1o$$.ppm");
+        }
+        if ($i <= 10) {
+            my $n = $spline10[$i];
+            system("ppmmix $n junk1$$.ppm junk1o$$.ppm >junk3$$.ppm");
+        } elsif ($i <= 20) {
+            my $n = $spline10[$i-10];
+            system("ppmmix $n junk1o$$.ppm junk2o$$.ppm >junk3$$.ppm");
+        } else {
+            my $n = $spline10[$i-20];
+            system("ppmmix $n junk2o$$.ppm junk2$$.ppm >junk3$$.ppm");
+        }
+        if ($i == 10) {
+            system("ppmtopgm junk2$$.ppm | pgmoil >junko$$.ppm");
+            system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " .
+                   ">junk2o$$.ppm");
+        }
+    } elsif ($mode eq $EDGE) {
+        if ($i == 1) {
+            system("ppmtopgm junk1$$.ppm | pgmedge >junko$$.ppm");
+            system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " .
+                   ">junk1o$$.ppm");
+        }
+        if ($i <= 10) {
+            my $n = $spline10[$i];
+            system("ppmmix $n junk1$$.ppm junk1o$$.ppm >junk3$$.ppm");
+        } elsif ($i <= 20) {
+            my $n = $spline10[$i-10];
+            system("ppmmix $n junk1o$$.ppm junk2o$$.ppm >junk3$$.ppm");
+        } else {
+            my $n = $spline10[$i-20];
+            system("ppmmix $n junk2o$$.ppm junk2$$.ppm >junk3$$.ppm");
+        }
+        if ($i == 10) {
+            system("ppmtopgm junk2$$.ppm | pgmedge >junko$$.ppm");
+            system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " .
+                   ">junk2o$$.ppm");
+        } 
+    } elsif ($mode eq $BENTLEY) {
+        if ($i == 1) {
+            system("ppmtopgm junk1$$.ppm | pgmbentley >junko$$.ppm");
+            system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " .
+                   ">junk1o$$.ppm");
+        }
+        if ($i <= 10) {
+            my $n = $spline10[$i];
+            system("ppmmix $n junk1$$.ppm junk1o$$.ppm >junk3$$.ppm");
+        } elsif ($i <= 20) {
+            my $n = $spline10[$i-10];
+            system("ppmmix $n junk1o$$.ppm junk2o$$.ppm >junk3$$.ppm");
+        } else {
+            my $n = $spline10[$i-20];
+            system("ppmmix $n junk2o$$.ppm junk2$$.ppm >junk3$$.ppm");
+        }
+        if ($i == 10) {
+            system("ppmtopgm junk2$$.ppm | pgmbentley >junko$$.ppm");
+            system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " .
+                   ">junk2o$$.ppm");
+        }
+    } elsif ($mode eq $BLOCK) {
+        if ($i <= 10) {
+            my $n = 1 - 1.9*$spline20[$i];
+            system("pamscale $n junk1$$.ppm | " .
+                   "pamscale -width $width -height $height >junk3$$.ppm");
+        } elsif ($i <= 20) {
+            my $n = $spline10[$i-10];
+            system("ppmmix $n junk1a$$.ppm junk2a$$.ppm >junk3$$.ppm");
+        } else {
+            my $n = 1 - 1.9*$spline20[31-$i];
+            system("pamscale $n junk2$$.ppm | " .
+                   "pamscale -width $width -height $height >junk3$$.ppm");
+        }
+        if ($i == 10) {
+            system("cp", "junk3$$.ppm", "junk1a$$.ppm");
+            system("pamscale $n junk2$$.ppm | " .
+                   "pamscale -width $width -height $height >junk2a$$.ppm");
+        }    
+    } elsif ($mode eq $MIX) {
+        my $fade_factor = sqrt(1/($nframes-$i+1));
+        system("ppmmix $fade_factor junk1$$.ppm junk2$$.ppm >junk3$$.ppm");
+    } else {
+        print("Internal error: impossible mode value '$mode'\n");
+    }
+
+    my $outfile = sprintf("%s.%04d.ppm", $base_name, $i);
+    system("cp", "junk3$$.ppm", $outfile);
+}
+
+#
+#  Clean up shop.
+#
+system("rm junk*$$.ppm");
+
+exit(0);
+
+
+
+sub usage() {
+   print "Usage: ppmfade [-f first_file] [-l last_file]\n";
+   print "               [-spread|-relief|-oil|-edge|-bentley|-block]\n";
+   print "               [-base basename]\n";
+   print "Notes: Default base: fade\n";
+   print "       The resulting image files will be named fade.NNNN.ppm.\n";
+   exit(100);
+}
diff --git a/editor/ppmflash.c b/editor/ppmflash.c
new file mode 100644
index 00000000..d1d048df
--- /dev/null
+++ b/editor/ppmflash.c
@@ -0,0 +1,114 @@
+
+/*********************************************************************/
+/* ppmflash -  brighten a picture up to total whiteout               */
+/* Frank Neumann, August 1993                                        */
+/* V1.4 16.11.1993                                                   */
+/*                                                                   */
+/* version history:                                                  */
+/* V1.0 ~ 15.August 1993    first version                            */
+/* V1.1 03.09.1993          uses ppm libs & header files             */
+/* V1.2 03.09.1993          integer arithmetics instead of float     */
+/*                          (gains about 50 % speed up)              */
+/* V1.3 11.10.1993          reads only one line at a time - this     */
+/*                          saves LOTS of memory on big picturs      */
+/* V1.4 16.11.1993          Rewritten to be NetPBM.programming con-  */
+/*                          forming                                  */
+/*********************************************************************/
+
+#include "ppm.h"
+
+/* global variables */
+#ifdef AMIGA
+static char *version = "$VER: ppmflash 1.4 (16.11.93)";
+#endif
+
+/**************************/
+/* start of main function */
+/**************************/
+int main(argc, argv)
+    int argc;
+    char *argv[];
+{
+    FILE* ifp;
+    int argn, rows, cols, i, j, format;
+    pixel *srcrow, *destrow;
+    pixel *pP, *pP2;
+    pixval maxval;
+    double flashfactor;
+    long longfactor;
+    const char* const usage = "flashfactor [ppmfile]\n        flashfactor: 0.0 = original picture, 1.0 = total whiteout\n";
+
+    /* parse in 'default' parameters */
+    ppm_init( &argc, argv );
+
+    argn = 1;
+
+    /* parse in flash factor */
+    if (argn == argc)
+        pm_usage(usage);
+    if (sscanf(argv[argn], "%lf", &flashfactor) != 1)
+        pm_usage(usage);
+    if (flashfactor < 0.0 || flashfactor > 1.0)
+        pm_error("flash factor must be in the range from 0.0 to 1.0 ");
+    ++argn;
+
+    /* parse in filename (if present, stdin otherwise) */
+    if (argn != argc)
+    {
+        ifp = pm_openr(argv[argn]);
+        ++argn;
+    }
+    else
+        ifp = stdin;
+
+    if (argn != argc)
+        pm_usage(usage);
+
+    /* read first data from file */
+    ppm_readppminit(ifp, &cols, &rows, &maxval, &format);
+
+    /* no error checking required here, ppmlib does it all for us */
+    srcrow = ppm_allocrow(cols);
+
+    longfactor = (long)(flashfactor * 65536);
+
+    /* allocate a row of pixel data for the new pixels */
+    destrow = ppm_allocrow(cols);
+
+    ppm_writeppminit(stdout, cols, rows, maxval, 0);
+
+    /** now do the flashing **/
+    /* the 'float' parameter for flashing is sort of faked - in fact, we */
+    /* convert it to a range from 0 to 65536 for integer math. Shouldn't */
+    /* be something you'll have to worry about, though. */
+
+    for (i = 0; i < rows; i++) {
+        ppm_readppmrow(ifp, srcrow, cols, maxval, format);
+
+        pP = srcrow;
+        pP2 = destrow;
+
+        for (j = 0; j < cols; j++) {
+            PPM_ASSIGN(*pP2, 
+                       PPM_GETR(*pP) + 
+                       (((maxval - PPM_GETR(*pP)) * longfactor) >> 16),
+                       PPM_GETG(*pP) + 
+                       (((maxval - PPM_GETG(*pP)) * longfactor) >> 16),
+                       PPM_GETB(*pP) + 
+                       (((maxval - PPM_GETB(*pP)) * longfactor) >> 16));
+
+            pP++;
+            pP2++;
+        }
+
+        /* write out one line of graphic data */
+        ppm_writeppmrow(stdout, destrow, cols, maxval, 0);
+    }
+
+    pm_close(ifp);
+    ppm_freerow(srcrow);
+    ppm_freerow(destrow);
+
+    exit(0);
+}
+
diff --git a/editor/ppmglobe.c b/editor/ppmglobe.c
new file mode 100644
index 00000000..ee1a57c3
--- /dev/null
+++ b/editor/ppmglobe.c
@@ -0,0 +1,172 @@
+/*
+ * This code written 2003
+ * by Max Gensthaler <Max@Gensthaler.de>
+ * Distributed under the Gnu Public License (GPL)
+ *
+ * Gensthaler called it 'ppmglobemap'.
+ *
+ * Translations of comments and C dialect by Bryan Henderson May 2003.
+ */
+
+
+#define _XOPEN_SOURCE  /* get M_PI in math.h */
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <math.h>
+
+#include "ppm.h"
+#include "colorname.h"
+#include "shhopt.h"
+#include "mallocvar.h"
+
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFileName;  /* Filename of input files */
+    unsigned int stripcount;
+    const char * background;
+    unsigned int closeok;
+};
+
+
+
+static void
+parseCommandLine(int argc, char ** 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 backgroundSpec;
+
+    MALLOCARRAY_NOFAIL(option_def, 100);
+
+    option_def_index = 0;   /* incremented by OPTENT3 */
+    OPTENT3(0, "background",     OPT_STRING, &cmdlineP->background, 
+            &backgroundSpec, 0);
+    OPTENT3(0, "closeok",        OPT_FLAG, NULL,
+            &cmdlineP->closeok, 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 (!backgroundSpec)
+        cmdlineP->background = NULL;
+
+    if (argc - 1 < 1) 
+        pm_error("You must specify at least one argument:  the strip count");
+    else {
+        int const stripcount = atoi(argv[1]);
+        if (stripcount <= 0)
+            pm_error("The strip count must be positive.  You specified %d",
+                     stripcount);
+            
+        cmdlineP->stripcount = stripcount;
+
+        if (argc-1 < 2)
+            cmdlineP->inputFileName = "-";
+        else
+            cmdlineP->inputFileName = argv[2];
+    
+        if (argc - 1 > 2)
+            pm_error("There are at most two arguments: strip count "
+                     "and input file name.  "
+                     "You specified %u", argc-1);
+    }
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    pixel ** srcPixels;
+    pixel ** dstPixels;
+    int srcCols, srcRows;
+    unsigned int dstCols, dstRows;
+    pixval srcMaxval, dstMaxval;
+    unsigned int stripWidth;
+        /* Width in pixels of each strip.  In the output image, this means
+           the rectangular strip in which the lens-shaped foreground strip
+           is placed..
+        */
+    unsigned int row;
+    pixel backgroundColor;
+
+    ppm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+    
+    ifP = pm_openr(cmdline.inputFileName);
+    
+    srcPixels = ppm_readppm(ifP, &srcCols, &srcRows, &srcMaxval);
+
+    pm_close(ifP);
+
+    stripWidth = srcCols / cmdline.stripcount;
+
+    if (stripWidth < 1)
+        pm_error("You asked for %u strips, but the image is only "
+                 "%u pixels wide, so that is impossible.",
+                 cmdline.stripcount, srcCols);
+
+    dstCols   = stripWidth * cmdline.stripcount;
+    dstRows   = srcRows;
+    dstMaxval = srcMaxval;
+
+    if (cmdline.background == NULL)
+        PPM_ASSIGN(backgroundColor, 0, 0, 0);
+    else
+        pm_parse_dictionary_name(cmdline.background,
+                                 dstMaxval, cmdline.closeok,
+                                 &backgroundColor);
+
+    dstPixels = ppm_allocarray(dstCols, dstRows);
+    
+    for (row = 0; row < dstRows; ++row) {
+        double const factor = sin(M_PI * row / dstRows);
+            /* Amount by which we squeeze the foreground image of each
+               strip in this row.
+            */
+        int const stripBorder = (int)((stripWidth*(1.0-factor)/2.0) + 0.5);
+            /* Distance from the edge (either one) of a strip to the
+               foreground image within that strip -- i.e. number of pixels
+               of background color, which User will cut out with scissors
+               after he prints the image.
+            */
+        unsigned int dstCol;
+
+        for (dstCol = 0; dstCol < dstCols; ++dstCol) {
+            if (dstCol % stripWidth < stripBorder
+                || dstCol % stripWidth >= stripWidth - stripBorder)
+                dstPixels[row][dstCol] = backgroundColor;
+            else {
+                unsigned int const leftEdge =
+                    (dstCol / stripWidth) * stripWidth;
+                unsigned int const srcCol = leftEdge +
+                    (int)((dstCol % stripWidth - stripBorder) / factor + 0.5);
+                dstPixels[row][dstCol] = srcPixels[row][srcCol];
+            }
+        }
+    }
+
+    ppm_writeppm(stdout, dstPixels, dstCols, dstRows, dstMaxval, 0);
+
+    return 0;
+}
diff --git a/editor/ppmlabel.c b/editor/ppmlabel.c
new file mode 100644
index 00000000..885d7d36
--- /dev/null
+++ b/editor/ppmlabel.c
@@ -0,0 +1,212 @@
+/*
+
+           Add text labels to a PPM image
+
+           by John Walker  --  kelvin@fourmilab.ch
+           WWW home page: http://www.fourmilab.ch/
+                  June 1995
+*/
+
+#define _XOPEN_SOURCE   /* get M_PI in math.h */
+
+#include <math.h>
+#include <string.h>
+
+#include "pm_c_util.h"
+#include "ppm.h"
+#include "ppmdraw.h"
+
+#define dtr(x)  (((x) * M_PI) / 180.0)
+
+static int argn, rows, cols, x, y, size, angle, transparent;
+static pixel **pixels;
+static pixval maxval;
+static pixel rgbcolor, backcolor;
+
+/*  DRAWTEXT  --  Draw text at current location and advance to
+          start of next line.  */
+
+static void 
+drawtext(const char * const text) {
+
+    if (!transparent && strlen(text) > 0) {
+        struct fillobj * handle;
+
+        int left, top, right, bottom;
+        int lx, ly;
+        int p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y;
+        double sina, cosa;
+
+        handle = ppmd_fill_create();
+        
+        ppmd_text_box(size, 0, text, &left, &top, &right, &bottom);
+
+        /* Displacement vector */ 
+
+        lx = right;
+        ly = -(top - bottom);
+
+        /* Sine and cosine */
+
+        sina = sin(dtr(angle));
+        cosa = cos(dtr(angle));
+
+        /* Rotated extent box corners */
+
+        p1x = (int) ((x + left * cosa + bottom * sina) + 0.5);
+        p1y = (int) ((y + bottom * cosa + -left * sina) + 0.5);
+
+#define WERF    ppmd_fill_drawproc, handle
+
+        p2x = (int) (p1x - sina * ly + 0.5);
+        p2y = (int) ((p1y - cosa * ly) + 0.5);
+
+        p3x = (int) (p1x + cosa * lx + -sina * ly + 0.5);
+        p3y = (int) ((p1y - (cosa * ly + sina * lx)) + 0.5);
+
+        p4x = (int) (p1x + cosa * lx + 0.5);
+        p4y = (int) ((p1y - sina * lx) + 0.5);
+
+        ppmd_line(pixels, cols, rows, maxval,
+                  p1x, p1y, p2x, p2y,
+                  WERF);
+        ppmd_line(pixels, cols, rows, maxval,
+                  p2x, p2y, p3x, p3y,
+                  WERF);
+        ppmd_line(pixels, cols, rows, maxval,
+                  p3x, p3y, p4x, p4y,
+                  WERF);
+        ppmd_line(pixels, cols, rows, maxval,
+                  p4x, p4y, p1x, p1y,
+                  WERF);
+
+
+        ppmd_fill(pixels, cols, rows, maxval,
+                  handle, PPMD_NULLDRAWPROC, (char *) &backcolor);
+
+        ppmd_fill_destroy(handle);
+    }
+    ppmd_text(pixels, cols, rows, maxval,
+              x, y, size, angle, text,
+              PPMD_NULLDRAWPROC, (char *) &rgbcolor);
+
+    /* For convenience, simulate a carriage return to the next line.
+       This allows multiple "-text" specifications or multiple lines
+       in a -file input to write consecutive lines of text in a
+       generally reasonable fashion.
+    */
+
+    x += (int) ((cos(dtr(angle + 270)) * size * 1.75) + 0.5);
+    y -= (int) ((sin(dtr(angle + 270)) * size * 1.75) + 0.5);
+}
+
+
+
+int
+main(int argc, char *argv[]) {
+
+    FILE *ifP;
+
+    /* Process standard command line arguments */
+
+    ppm_init(&argc, argv);
+
+    argn = 1;
+
+    /* Check for explicit input file specification,  Note that
+       we count on the fact that every command line switch
+       takes a single argument.  If this becomes untrue due
+       to a change in the future, you'll have to make this
+       test smarter.
+    */
+
+    if ((argn != argc) && (argc == 2 || argv[argc - 2][0] != '-')) {
+        ifP = pm_openr(argv[argc - 1]);
+        argc--;
+    } else
+        ifP = stdin;
+
+    /* Load input image */
+
+    pixels = ppm_readppm(ifP, &cols, &rows, &maxval);
+    pm_close(ifP);
+
+    /* Set initial defaults */
+
+    x = 0;
+    y = rows / 2;
+    size = 12;
+    angle = 0;
+    PPM_ASSIGN(rgbcolor, maxval, maxval, maxval);
+    PPM_ASSIGN(backcolor, 0, 0, 0);
+    transparent = TRUE;
+
+    while (argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0') {
+
+        if (pm_keymatch(argv[argn], "-angle", 1)) {
+            argn++;
+            if ((argn == argc) || (sscanf(argv[argn], "%d", &angle) != 1))
+                pm_error("-angle doesn't have a value");
+
+        } else if (pm_keymatch(argv[argn], "-background", 1)) {
+            argn++;
+            if (strcmp(argv[argn], "transparent") == 0) {
+                transparent = TRUE;
+            } else {
+                transparent = FALSE;
+                backcolor = ppm_parsecolor(argv[argn], maxval);
+            }
+
+        } else if (pm_keymatch(argv[argn], "-color", 1)
+                   || pm_keymatch(argv[argn], "-colour", 1)) {
+            argn++;
+            rgbcolor = ppm_parsecolor(argv[argn], maxval);
+
+        } else if (pm_keymatch(argv[argn], "-file", 1)) {
+            char s[512];
+
+            argn++;
+            ifP = pm_openr(argv[argn]);
+            while (fgets(s, sizeof s, ifP) != NULL) {
+                while (s[0] != 0 && s[strlen(s) - 1] < ' ') {
+                    s[strlen(s) - 1] = 0;
+                }
+                drawtext(s);
+            }
+            pm_close(ifP);
+
+        } else if (pm_keymatch(argv[argn], "-size", 1)) {
+            argn++;
+            if ((argn == argc) || (sscanf(argv[argn], "%d", &size) != 1))
+                pm_error("-size doesn't have a value");
+        } else if (pm_keymatch(argv[argn], "-text", 1)) {
+            argn++;
+            drawtext(argv[argn]);
+
+        } else if (pm_keymatch(argv[argn], "-u", 1)) {
+            pm_error("-u doesn't have a value");
+
+        } else if (pm_keymatch(argv[argn], "-x", 1)) {
+            argn++;
+            if ((argn == argc) || (sscanf(argv[argn], "%d", &x) != 1))
+                pm_error("-x doesn't have a value");
+
+        } else if (pm_keymatch(argv[argn], "-y", 1)) {
+            argn++;
+            if ((argn == argc) || (sscanf(argv[argn], "%d", &y) != 1))
+                pm_error("-y doesn't have a value");
+
+        } else
+            pm_error("Unrecognized option: '%s'", argv[argn]);
+        argn++;
+    }
+
+    if (argn != argc)
+        pm_error("Extraneous arguments");
+
+    ppm_writeppm(stdout, pixels, cols, rows, maxval, 0);
+
+    ppm_freearray(pixels, rows);
+
+    return 0;
+}
diff --git a/editor/ppmmix.c b/editor/ppmmix.c
new file mode 100644
index 00000000..5306d1cf
--- /dev/null
+++ b/editor/ppmmix.c
@@ -0,0 +1,131 @@
+
+/*********************************************************************/
+/* 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) )
+        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);
+
+	longfactor = (long)(fadefactor * 65536);
+
+	/* allocate a row of pixel data for the new pixels */
+	destrow = ppm_allocrow(cols);
+
+	ppm_writeppminit(stdout, cols, rows, maxval, 0);
+
+	for (i = 0; i < rows; i++)
+	{
+		ppm_readppmrow(ifp1, srcrow1, cols, maxval, format);
+		ppm_readppmrow(ifp2, srcrow2, cols, maxval, format);
+
+		pP1 = srcrow1;
+		pP2 = srcrow2;
+        pP3 = destrow;
+
+		for (j = 0; j < cols; j++)
+		{
+			r1 = PPM_GETR(*pP1);
+			g1 = PPM_GETG(*pP1);
+			b1 = PPM_GETB(*pP1);
+
+			r2 = PPM_GETR(*pP2);
+			g2 = PPM_GETG(*pP2);
+			b2 = PPM_GETB(*pP2);
+
+			r3 = r1 + (((r2 - r1) * longfactor) >> 16);
+			g3 = g1 + (((g2 - g1) * longfactor) >> 16);
+			b3 = b1 + (((b2 - b1) * longfactor) >> 16);
+
+
+			PPM_ASSIGN(*pP3, r3, g3, b3);
+
+			pP1++;
+			pP2++;
+			pP3++;
+		}
+
+		/* 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);
+}
+
diff --git a/editor/ppmntsc.c b/editor/ppmntsc.c
new file mode 100644
index 00000000..b9f2ac2f
--- /dev/null
+++ b/editor/ppmntsc.c
@@ -0,0 +1,499 @@
+/* This is ppmntsc.c, a program to adjust saturation values in an image
+   so they are legal for NTSC or PAL.
+
+   It is derived from the program rlelegal.c, dated June 5, 1995,
+   which is described below and propagates that program's copyright.
+   The derivation was done by Bryan Henderson on 2000.04.21 to convert
+   it from operating on the RLE format to operating on the PPM format
+   and to rewrite it in a cleaner style, taking advantage of modern C
+   compiler technology.  
+*/
+
+
+/*
+ * This software is copyrighted as noted below.  It may be freely copied,
+ * modified, and redistributed, provided that the copyright notice is 
+ * preserved on all copies.
+ * 
+ * There is no warranty or other guarantee of fitness for this software,
+ * it is provided solely "as is".  Bug reports or fixes may be sent
+ * to the author, who may or may not act on them as he desires.
+ *
+ * You may not include this software in a program or other software product
+ * without supplying the source, or without informing the end-user that the 
+ * source is available for no extra charge.
+ *
+ * If you modify this software, you should include a notice giving the
+ * name of the person performing the modification, the date of modification,
+ * and the reason for such modification.
+ */
+
+/* 
+ * rlelegal.c - Make RGB colors legal in the YIQ or YUV color systems.
+ * 
+ * Author:	Wes Barris
+ * 		Minnesota Supercomputer Center, Inc.
+ * Date:	Fri Oct 15, 1993
+ * @Copyright, Research Equipment Inc., d/b/a Minnesota Supercomputer
+ * Center, Inc., 1993
+
+ */
+
+#define _BSD_SOURCE 1      /* Make sure strdup() is in string.h */
+#define _XOPEN_SOURCE 500  /* Make sure strdup() is in string.h */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "ppm.h"
+#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 */
+
+struct cmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFilename;
+    unsigned int verbose;
+    unsigned int debug;
+    unsigned int pal;
+    enum {ALL, LEGAL_ONLY, ILLEGAL_ONLY, CORRECTED_ONLY} output;
+};
+
+
+
+
+static void 
+rgbtoyiq(const int r, const int g, const int b, 
+         double * const y_p, 
+         double * const i_p, 
+         double * const q_p) {
+    
+    *y_p = .299*(r/255.0) + .587*(g/255.0) + .114*(b/255.0);
+    *i_p = .596*(r/255.0) - .274*(g/255.0) - .322*(b/255.0);
+    *q_p = .211*(r/255.0) - .523*(g/255.0) + .312*(b/255.0);
+}
+
+
+
+static void 
+yiqtorgb(const double y, const double i, const double q, 
+         int * const r_p, int * const g_p, int * const b_p) {
+    *r_p = 255.0*(1.00*y + .9562*i + .6214*q);
+    *g_p = 255.0*(1.00*y - .2727*i - .6468*q);
+    *b_p = 255.0*(1.00*y -1.1037*i +1.7006*q);
+}
+
+
+
+static void 
+rgbtoyuv(const int r, const int g, const int b, 
+         double * const y_p, 
+         double * const u_p, 
+         double * const v_p) {
+    *y_p =  .299*(r/255.0) + .587*(g/255.0) + .114*(b/255.0);
+    *u_p = -.147*(r/255.0) - .289*(g/255.0) + .437*(b/255.0);
+    *v_p =  .615*(r/255.0) - .515*(g/255.0) - .100*(b/255.0);
+}
+
+
+
+static void 
+yuvtorgb(const double y, const double u, const double v, 
+         int * const r_p, int * const g_p, int * const b_p) {
+    
+    *r_p = 255.0*(1.00*y + .0000*u +1.1398*v);
+    *g_p = 255.0*(1.00*y - .3938*u - .5805*v);
+    *b_p = 255.0*(1.00*y +2.0279*u + .0000*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
+    ) {
+    
+    double sat_old, sat_new;
+    /*
+     * 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;
+    } else {
+        *y_new_p = y;
+        *i_new_p = i;
+        *q_new_p = q;
+        *action_p = ALREADY_LEGAL;
+    }
+    return;
+}
+
+
+
+static void
+make_legal_yuv(const double y, const double u, const double v, 
+               double * const y_new_p, 
+               double * const u_new_p, 
+               double * const v_new_p,
+               enum legalize * const action_p
+    ) {
+    
+    double sat_old, sat_new;
+    /*
+     * U and V are legs of a right triangle.  Saturation is the hypotenuse.
+     */
+    sat_old = sqrt(u*u + v*v);
+    if (y+sat_old >= 1.334) {
+        const double diff = 0.5*((y+sat_old) - 1.334);
+        *y_new_p = y - diff;
+        sat_new = 1.333 - *y_new_p;
+        *u_new_p = u*(sat_new/sat_old);
+        *v_new_p = v*(sat_new/sat_old);
+        *action_p = LOWER_SAT;
+    } else if (y-sat_old <= -0.339) {
+        const double diff = 0.5*((sat_old-y) - 0.339);
+        *y_new_p = y + diff;
+        sat_new = 0.338 + *y_new_p;
+        *u_new_p = u*(sat_new/sat_old);
+        *v_new_p = v*(sat_new/sat_old);
+        *action_p = RAISE_SAT;
+    } else {
+        *u_new_p = u;
+        *v_new_p = v;
+        *action_p = ALREADY_LEGAL;
+    }
+    return;
+}
+
+
+
+static void
+make_legal_yiq_i(const int r_in, const int g_in, const int b_in, 
+                 int * const r_out_p, 
+                 int * const g_out_p, 
+                 int * const b_out_p,
+                 enum legalize * const action_p
+    ) {
+    
+    double y, i, q;
+    double y_new, i_new, q_new;
+    /*
+     * 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);
+    if (*action_p != ALREADY_LEGAL)
+        /*
+         * Given the new I and Q, compute new RGB values.
+        */
+        yiqtorgb(y_new, i_new, q_new, r_out_p, g_out_p, b_out_p);
+    else {
+        *r_out_p = r_in;
+        *g_out_p = g_in;
+        *b_out_p = b_in;
+      }
+    return;
+}
+
+
+
+static void
+make_legal_yuv_i(const int r_in, const int g_in, const int b_in, 
+                 int * const r_out_p, 
+                 int * const g_out_p, 
+                 int * const b_out_p,
+                 enum legalize * const action_p
+    ){
+    
+    double y, u, v;
+    double y_new, u_new, v_new;  
+    /*
+     * Convert to YUV and compute the new saturation.
+     */
+    rgbtoyuv(r_in, g_in, b_in, &y, &u, &v);
+    make_legal_yuv(y, u, v, &y_new, &u_new, &v_new, action_p);
+    if (*action_p != ALREADY_LEGAL)
+        /*
+         * Given the new U and V, compute new RGB values.
+         */
+        yuvtorgb(y_new, u_new, v_new, r_out_p, g_out_p, b_out_p);
+    else {
+        *r_out_p = r_in;
+        *g_out_p = g_in;
+        *b_out_p = b_in;
+    }
+    return;
+}
+
+
+
+static void 
+make_legal_yiq_b(const pixel input,
+                 pixel * const output_p,
+                 enum legalize * const action_p) {
+
+
+    int ir_in, ig_in, ib_in;
+    int ir_out, ig_out, ib_out;
+    
+    ir_in = (int)PPM_GETR(input);
+    ig_in = (int)PPM_GETG(input);
+    ib_in = (int)PPM_GETB(input);
+
+    make_legal_yiq_i(ir_in, ig_in, ib_in, &ir_out, &ig_out, &ib_out, action_p);
+
+    PPM_ASSIGN(*output_p, ir_out, ig_out, ib_out);
+
+    return;
+}
+
+
+
+static void 
+make_legal_yuv_b(const pixel input,
+                 pixel * const output_p,
+                 enum legalize * const action_p) {
+
+    int ir_in, ig_in, ib_in;
+    int ir_out, ig_out, ib_out;
+    
+    ir_in = (int)PPM_GETR(input);
+    ig_in = (int)PPM_GETG(input);
+    ib_in = (int)PPM_GETB(input);
+    make_legal_yuv_i(ir_in, ig_in, ib_in, &ir_out, &ig_out, &ib_out, action_p);
+
+    PPM_ASSIGN(*output_p, ir_out, ig_out, ib_out);
+
+    return;
+}
+
+
+
+static void 
+report_mapping(const pixel old_pixel, const pixel new_pixel) {
+/*----------------------------------------------------------------------------
+  Assuming old_pixel and new_pixel 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)
+            );
+
+        last_changed_pixel = old_pixel;
+        first_time = FALSE;
+    }    
+}
+
+
+
+static void
+convert_one_image(FILE * const ifp, struct cmdlineInfo const cmdline, 
+                  bool * const eofP, 
+                  int * const hicountP, 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);
+    {
+        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 black;
+        /* A constant - black pixel */
+
+        PPM_ASSIGN(black, 0, 0, 0);
+
+        PPM_ASSIGN(last_illegal_pixel, 0, 0, 0);  /* initial value */
+        {
+            int row;
+
+            *hicountP = 0; *locountP = 0;  /* initial values */
+
+            for (row = 0; row < rows; ++row) {
+                int col;
+                ppm_readppmrow(ifp, input_row, cols, maxval, format);
+                for (col = 0; col < cols; ++col) {
+                    pixel corrected;
+                    /* Corrected or would-be corrected value for pixel */
+                    enum legalize action;
+                    /* What action was used to make pixel legal */
+                    if (cmdline.pal)
+                        make_legal_yuv_b(input_row[col],
+                                         &corrected,
+                                         &action);
+                    else
+                        make_legal_yiq_b(input_row[col],
+                                         &corrected,
+                                         &action);
+                        
+                    if (action == LOWER_SAT) 
+                        (*hicountP)++;
+                    if (action == RAISE_SAT)
+                        (*locountP)++;
+                    if (cmdline.debug) report_mapping(input_row[col],
+                                                      corrected);
+                    switch (cmdline.output) {
+                    case ALL:
+                        output_row[col] = corrected;
+                        break;
+                    case LEGAL_ONLY:
+                        output_row[col] = (action == ALREADY_LEGAL) ?
+                            input_row[col] : black;
+                        break;
+                    case ILLEGAL_ONLY:
+                        output_row[col] = (action != ALREADY_LEGAL) ?
+                            input_row[col] : black;
+                        break;
+                    case CORRECTED_ONLY:
+                        output_row[col] = (action != ALREADY_LEGAL) ?
+                            corrected : black;
+                        break;
+                    }
+                }
+                ppm_writeppmrow(stdout, output_row, cols, maxval, FALSE);
+            }
+        }
+        ppm_freerow(output_row);
+        ppm_freerow(input_row);
+    }
+}
+
+
+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) {
+    
+    struct cmdlineInfo cmdline;
+    FILE * ifP;
+    int total_hicount, total_locount;
+    int image_count;
+
+    bool eof;
+
+    ppm_init(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFilename);
+
+    image_count = 0;    /* initial value */
+    total_hicount = 0;  /* initial value */
+    total_locount = 0;  /* initial value */
+
+    eof = FALSE;
+    while (!eof) {
+        int hicount, locount;
+        convert_one_image(ifP, cmdline, &eof, &hicount, &locount);
+        image_count++;
+        total_hicount += hicount;
+        total_locount += 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_close(ifP);
+
+    return 0;
+}
diff --git a/editor/ppmquant b/editor/ppmquant
new file mode 100755
index 00000000..11bce6d2
--- /dev/null
+++ b/editor/ppmquant
@@ -0,0 +1,30 @@
+#!/usr/bin/perl -w
+##############################################################################
+#  This is nothing but a compatibility interface for Pnmquant.
+#  An old program coded to call Ppmquant will continue working because
+#  this interface exists.  All new (or newly modified) programs should
+#  call Pnmquant or Pnmremap instead.
+#
+#  In days past, Pnmquant and Pnmremap did not exist.  Ppmquant did
+#  the job of both Pnmremap and Pnmquant, but only on PPM images.
+##############################################################################
+
+use strict;
+
+use Getopt::Long;
+
+my $TRUE=1; my $FALSE = 0;
+
+my @ppmquantArgv = @ARGV;
+
+Getopt::Long::Configure('pass_through');
+
+my $validOptions = GetOptions('mapfile' => \my $mapfileopt);
+
+my $mapfileOptionPresent = ($validOptions && $mapfileopt);
+
+if ($mapfileOptionPresent) {
+    system('pnmremap', @ppmquantArgv);
+} else {
+    system('pnmquant', @ppmquantArgv);
+}
diff --git a/editor/ppmquantall b/editor/ppmquantall
new file mode 100755
index 00000000..af1ce22c
--- /dev/null
+++ b/editor/ppmquantall
@@ -0,0 +1,97 @@
+#!/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().
+
+set widths=()
+set 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 $tempdir || { echo "Could not create temporary file. Exiting."; exit 1;}
+chmod 700 $tempdir
+
+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
new file mode 100644
index 00000000..9a89bca0
--- /dev/null
+++ b/editor/ppmquantall.csh
@@ -0,0 +1,57 @@
+#!/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/ppmrelief.c b/editor/ppmrelief.c
new file mode 100644
index 00000000..5e0669c3
--- /dev/null
+++ b/editor/ppmrelief.c
@@ -0,0 +1,90 @@
+/* ppmrelief.c - generate a relief map of a portable pixmap
+**
+** Copyright (C) 1990 by Wilson H. Bent, Jr.
+**
+** 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 "ppm.h"
+
+int
+main(int argc, char * argv[]) {
+
+    FILE* ifp;
+    pixel** inputbuf;
+    pixel* outputrow;
+    int argn, rows, cols, format, row;
+    register int col;
+    pixval maxval, mv2;
+    const char* const usage = "[ppmfile]";
+
+    ppm_init( &argc, argv );
+
+    argn = 1;
+
+    if ( argn != argc ) {
+        ifp = pm_openr( argv[argn] );
+        ++argn;
+    } else
+        ifp = stdin;
+
+    if ( argn != argc )
+        pm_usage( usage );
+    
+    ppm_readppminit( ifp, &cols, &rows, &maxval, &format );
+    mv2 = maxval / 2;
+
+    /* Allocate space for 3 input rows, plus an output row. */
+    inputbuf = ppm_allocarray( cols, 3 );
+    outputrow = ppm_allocrow( cols );
+
+    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 );
+
+    /* Write out the first row, all zeros. */
+    for ( col = 0; col < cols; ++col )
+        PPM_ASSIGN( outputrow[col], 0, 0, 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.
+    */
+    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 = PPM_GETR( inputbuf[rowa][col] ) +
+                ( mv2 - PPM_GETR( inputbuf[rowb][col + 2] ) );
+            g = PPM_GETG( inputbuf[rowa][col] ) +
+                ( mv2 - PPM_GETG( inputbuf[rowb][col + 2] ) );
+            b = PPM_GETB( inputbuf[rowa][col] ) +
+                ( mv2 - PPM_GETB( inputbuf[rowb][col + 2] ) );
+            PPM_ASSIGN( outputrow[col + 1], r, g, b );
+        }
+        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 );
+
+    pm_close( ifp );
+    pm_close( stdout );
+
+    exit( 0 );
+}
diff --git a/editor/ppmshadow b/editor/ppmshadow
new file mode 100755
index 00000000..2a32fca0
--- /dev/null
+++ b/editor/ppmshadow
@@ -0,0 +1,273 @@
+#!/usr/bin/perl -w
+
+#                         P P M S H A D O W
+
+#            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
+#   *do* any image processing--it simply invokes components of
+#   Jef Poskanzer's PBMplus package (which must be present on
+#   the path when this script is run) to bludgeon the source
+#   image into a plausible result.
+#
+#               This program is in the public domain.
+#
+#
+
+use strict;
+require 5.0;
+#  The good open() syntax, with the mode separate from the file name,
+#  came after 5.0.  So did mkdir() with default mode.
+
+my $true=1; my $false=0;
+
+
+sub getDimensions($) {
+    my ($fileName) = @_;
+#-----------------------------------------------------------------------------
+#  Return the dimensions of the Netpbm image in the named file
+#-----------------------------------------------------------------------------
+    my ($width, $height);
+    my $pamfileOutput = `pamfile $fileName`;
+    if ($pamfileOutput =~ m/.*\sP[BGP]M\s.*,\s*(\d*)\sby\s(\d*)/) {
+        ($width, $height) = ($1, $2);
+    } else {
+        die("Unrecognized output from 'pamfile' shell command");
+    }
+    return ($width, $height);
+}    
+
+
+sub makeConvolutionKernel($$) {
+    my ($convkernelfile, $ckern) = @_;
+
+    #   Create convolution kernel file to generate shadow
+    
+    open(OF, ">$convkernelfile") or die();
+    printf(OF "P2\n$ckern $ckern\n%d\n", $ckern * $ckern * 2);
+    my $a = ($ckern * $ckern) + 1;
+    my $i;
+    for ($i = 0; $i < $ckern; $i++) {
+        my $j;
+        for ($j = 0; $j < $ckern; $j++) {
+            printf(OF "%d%s", $a, ($j < ($ckern - 1)) ? " " : "\n");
+        }
+    }
+    close(OF);
+}
+
+
+
+##############################################################################
+#                           MAINLINE
+##############################################################################
+
+
+my $tmpdir = $ENV{TMPDIR} || "/tmp";
+my $ourtmp = "$tmpdir/ppmshadow$$";
+mkdir($ourtmp, 0777) or
+    die("Unable to create directory for temporary files '$ourtmp");
+
+#   Process command line options
+
+
+my $ifile; # Input file name
+my ($xoffset, $yoffset);
+
+my $convolve = 11;                   # Default blur convolution kernel size
+my $keeptemp = $false;               # Don't preserve intermediate files
+my $translucent = $false;            # Default not translucent
+
+while (@ARGV) {
+    my $arg = shift;
+    if ((substr($arg, 0, 1) eq '-') && (length($arg) > 1)) {
+        my $opt;
+        $opt = substr($arg, 1, 1);
+        $opt =~ tr/A-Z/a-z/;
+        if ($opt eq 'b') {        # -B n  --  Blur size
+            if (!defined($convolve = shift)) {
+                die("Argument missing after -b option\n");
+            }
+            if (($convolve < 11) && (($convolve & 1) == 0)) {
+                $convolve++;      # Round up even kernel specification
+            }
+        } elsif ($opt eq 'k') {   # -K  --  Keep temporary files
+            $keeptemp = $true;
+        } elsif ($opt eq 't') {   # -T  --  Translucent image
+            $translucent = $true;
+        } elsif ($opt eq 'x') {   # -X n  --  X offset
+            if (!defined($xoffset = shift)) {
+                die("Argument missing after -x option\n");
+            }
+            if ($xoffset < 0) {
+                $xoffset = -$xoffset;
+            }
+        } elsif ($opt eq 'y') {   # -Y n  --  Y offset
+            if (!defined($yoffset = shift)) {
+                die("Argument missing after -x option\n");
+            }
+            if ($yoffset < 0) {
+                $yoffset = -$xoffset;
+            }
+        }
+    } else {
+        if (defined $ifile) {
+            die("Duplicate input file specification.");
+        }
+        $ifile = $arg;   
+    }
+}
+
+#   Apply defaults for arguments not specified
+
+if (!(defined $xoffset)) {
+    #   Xoffset defaults to half the blur distance
+    $xoffset = int($convolve / 2);
+}
+
+if (!(defined $yoffset)) {
+    #   Yoffset defaults to Xoffset, however specified
+    $yoffset = $xoffset;
+}
+
+# Save the Standard Output open instance so we can use the STDOUT
+# file descriptor to pass files to our children.
+open(OLDOUT, ">&STDOUT");
+select(OLDOUT);  # avoids Perl bug where it says we never use STDOUT 
+
+my $infile = "$ourtmp/infile.ppm";
+
+if (defined($ifile) && $ifile ne "-") {
+    open(STDIN, "<$ifile") or die();
+}
+open(STDOUT, ">$infile") or die("Unable to open '$infile' as STDOUT");
+system("ppmtoppm");
+
+# You would think we could and should close stdin and stdout now, but if
+# we do that, system() pipelines later on fail mysteriously.  They don't
+# seem to be able to open stdin and stdout pipes properly if stdin and 
+# stdout didn't already exist.  2002.09.07 BJH
+
+my ($sourceImageWidth, $sourceImageHeight) = getDimensions($infile);
+
+#   Create an all-background-color image (same size as original image)
+
+my $backgroundfile = "$ourtmp/background.ppm";
+system("pamcut -left=0 -top=0 -width=1 -height=1 $infile | " .
+       "pamscale -xsize=$sourceImageWidth " .
+       "-ysize=$sourceImageHeight >$backgroundfile");
+
+#   Create mask file for background.  It is white wherever there is background
+#   image in the input.
+
+my $bgmaskfile = "$ourtmp/bgmask.pbm";
+system("pamarith -difference $infile $backgroundfile | pnminvert | ppmtopgm " .
+       "| pgmtopbm -thresh -value 1.0 >$bgmaskfile");
+
+my $ckern = $convolve <= 11 ? $convolve : 11;
+
+my $convkernelfile = "$ourtmp/convkernel.pgm";
+
+makeConvolutionKernel($convkernelfile, $ckern);
+
+if ($translucent) {
+
+    #   Convolve the input color image with the kernel
+    #   to create a translucent shadow image.
+
+    system("pnmconvol $convkernelfile $infile >$ourtmp/blurred.ppm");
+    unlink("$convkernelfile") unless $keeptemp;
+    while ($ckern < $convolve) {
+        system("pnmsmooth $ourtmp/blurred.ppm >$ourtmp/convolvedx.ppm");
+        rename("$ourtmp/convolvedx.ppm", "$ourtmp/blurred.ppm");
+        ++$ckern;
+    }
+} else {
+
+    #   Convolve the positive mask with the kernel to create shadow
+ 
+    my $blurredblackshadfile = "$ourtmp/blurredblackshad.pgm";
+    system("pamdepth -quiet 255 $bgmaskfile | " .
+           "pnmconvol $convkernelfile >$blurredblackshadfile");
+    unlink($convkernelfile) unless $keeptemp;
+
+    while ($ckern < $convolve) {
+        my $smoothedfile = "$ourtmp/smoothed.pgm";
+        system("pnmsmooth $blurredblackshadfile >$smoothedfile");
+        rename($smoothedfile, $blurredblackshadfile);
+        ++$ckern;
+    }
+
+    #   Multiply the shadow by the background color
+
+    system("pamarith -multiply $blurredblackshadfile $backgroundfile " .
+           ">$ourtmp/blurred.ppm");
+    unlink($blurredblackshadfile) unless $keeptemp;
+}
+
+#   Cut shadow image down to size of our frame.
+
+my $shadowfile = "$ourtmp/shadow.ppm";
+{
+    my $width = $sourceImageWidth - $xoffset;
+    my $height = $sourceImageHeight - $yoffset;
+    open(STDIN, "<$ourtmp/blurred.ppm") or die();
+    open(STDOUT, ">$shadowfile") or die();
+    system("pamcut", "-left=0", "-top=0", 
+           "-width=$width", "-height=$height");
+}
+unlink("$ourtmp/blurred.ppm") unless $keeptemp;
+
+#   Make mask for foreground
+
+my $fgmaskfile = "$ourtmp/fgmask.pbm";
+open(STDIN, "<$bgmaskfile") or die();
+open(STDOUT, ">$fgmaskfile") or die();
+system("pnminvert");
+
+#   Make image which is just foreground; rest is black.
+
+my $justfgfile = "$ourtmp/justfg.ppm";
+open(STDOUT, ">$justfgfile") or die();
+system("pamarith", "-multiply", $infile, $fgmaskfile);
+
+unlink($fgmaskfile) unless $keeptemp;
+unlink($infile) unless $keeptemp;
+
+#   Paste shadow onto background.
+
+my $shadbackfile = "$ourtmp/shadback.ppm";
+open(STDOUT, ">$shadbackfile") or die();
+system("pnmpaste", "-replace", $shadowfile, $xoffset, $yoffset,
+       $backgroundfile);
+unlink($shadowfile) unless $keeptemp;
+unlink($backgroundfile) unless $keeptemp;
+
+#   Knock out (make black) foreground area
+
+my $allbutfgfile = "$ourtmp/allbutfg.ppm";
+open(STDOUT, ">$allbutfgfile") or die();
+system("pamarith", "-multiply", $shadbackfile, $bgmaskfile);
+
+unlink($shadbackfile) unless $keeptemp;
+unlink($bgmaskfile) unless $keeptemp;
+
+#   Place foreground in blacked out area, send to original Standard Output.
+
+open(STDOUT, ">&OLDOUT");
+
+system("pamarith", "-add", $justfgfile, $allbutfgfile);
+unlink($justfgfile) unless $keeptemp;
+unlink($allbutfgfile) unless $keeptemp;
+
+if (!$keeptemp) {
+    rmdir($ourtmp) or die ("Unable to remove temporary directory '$ourtmp'");
+}
diff --git a/editor/ppmshadow.doc b/editor/ppmshadow.doc
new file mode 100644
index 00000000..1539c708
--- /dev/null
+++ b/editor/ppmshadow.doc
@@ -0,0 +1,627 @@
+<html>
+<head>
+<title>pnmshadow: How it Works</title>
+
+</head>
+
+<body>
+
+<center>
+<h1><img src="figures/how_title.jpg" width=417 height=116 alt="pnmshadow: How it Works"></h1>
+</center>
+
+<hr>
+<p>
+
+This document describes the process, including PBMplus commands
+and the intermediate images they create, by
+which <b><a href="./">pnmshadow</a></b>
+adds black shadows to source images.
+A <a href="how-t.html">companion document</a>
+describes how translucent shadows are created when the
+<b>-t</b> option is specified.
+
+<h3>The Starting Point</h3>
+
+Let's start with the following source image, 536 pixels wide and 141
+pixels high.  We convert the image from whatever form in which
+it was originally created (GIF, JPEG, etc.) to a PPM file before
+processing it with <b>pnmshadow</b>.
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadin.gif" width=536 height=141 alt="Input image">
+</table>
+</center>
+
+<h3>The Blank Background</h3>
+
+We start by determining the size of the input image with
+<b>pnmfile</b> and then constructing an image with the same
+size as the input image consisting entirely of the background
+color, which is defined as the color of the pixel at the upper
+left corner of the source image.  This is performed by the
+command:
+
+<p>
+<pre>
+    pnmcut 0 0 1 1 <em>ifile</em> | pnmscale -xsize <em>xsize</em> -ysize <em>ysize</em> &gt;<em>fname</em>-5.ppm
+</pre>
+<p>
+
+yielding the image:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt5.gif" width=536 height=141 alt="Blank background image">
+</table>
+</center>
+
+<h3>The Positive Mask</h3>
+
+A positive mask image is created in which all pixels of the background
+color are set to white and all other pixels are black.  This is accomplished
+by subtracting the blank background image from the input (using the
+<tt>-difference</tt> option on <b>pnmarith</b> to avoid clipping at
+zero or the maximum pixel value), then inverting the result and
+thresholding it to a monochrome bitmap.
+
+<p>
+<pre>
+    pnmarith -difference <em>ifile</em> <em>fname</em>-5.ppm | pnminvert | ppmtopgm | pgmtopbm -thresh -value 1.0 &gt;<em>fname</em>-1.ppm
+</pre>
+<p>
+
+This produces the following mask image.
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt1.gif" width=536 height=141 alt="Positive mask image">
+</table>
+</center>
+
+<h3>The Blurred Image</h3>
+
+Since we wish to simulate a shadow from a nearby extended
+light source rather than a sharp shadow as cast by the
+Sun, we need to prepare a blurred version of the original
+image.  If the <b>-t</b> option is not specified on
+<b>pnmshadow</b> the shadow cast by an object of any color
+is always black, so the positive mask serves as the source image
+when preparing the shadow.  A convolution kernel which averages
+the number of pixels specified by the <b>-b</b> option
+(default 11), written into the temporary file <tt><em>fname</em>-2.ppm</tt>
+in ASCII PGM format, and then the blurred image is created with
+the command:
+
+<p>
+<pre>
+    pnmconvol <em>fname</em>-2.ppm <em>fname</em>-1.ppm &gt;<em>fname</em>-3.ppm
+</pre>
+<p>
+
+With the default blur setting of 11 pixels, the blurred image
+below is generated.
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt3.jpg" width=536 height=141 alt="Blurred shadow">
+</table>
+</center>
+
+<h3>Shadow on Background Color</h3>
+
+Having generated the blurred shadow from the monochrome mask image,
+it will consist of pixels ranging from white to black on a white
+background.  In order to preserve the background color in the
+original image, we multiply the shadow by the blank background color
+image created previously.  White pixels take on the background color
+and pixels belonging to the shadow are scaled to be relative to the
+background.
+
+<p>
+<pre>
+    pnmarith -multiply <em>fname</em>-3.ppm <em>fname</em>-5.ppm &gt;<em>fname</em>-10.ppm
+</pre>
+<p>
+
+This yields the following shadow, with the background of the
+original image.
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt10.jpg" width=536 height=141 alt="Shadow with background color">
+</table>
+</center>
+
+<h3>Offset Shadow Clip</h3>
+
+Shadows, even bogus ones like we're generating, usually look best when
+cast by a light source diagonally displaced from the centre of the
+shadow-casting object.  To achieve this effect, we first cut a
+rectangle from the blurred shadow image reduced in size by the
+the number of pixels specified by the <b>-b</b> option
+which default to half the blur (<b>-b</b>) setting.  The
+<em>xsize</em> and <em>ysize</em> arguments in the following
+command are the size of the input image in pixels less the shadow
+displacement in the respective axis.
+
+<p>
+<pre>
+    pnmcut 0 0 <em>xsize</em> <em>ysize</em> <em>fname</em>-10.ppm &gt;<em>fname</em>-4.ppm
+</pre>
+<p>
+
+The shadow clip is the identical to the shadow on background color, but
+smaller by the offset in each direction.
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt4.jpg" width=531 height=136 alt="Offset shadow clip">
+</table>
+</center>
+
+<h3>Offset Shadow</h3>
+
+Now we're ready to assemble the shadow offset by the specified number
+of pixels.  We do this by pasting the image cut in the previous step
+into the blank background, yielding an image the same size as the
+source image with the blurred shadow displaced to the right and
+down.
+
+<p>
+<pre>
+    pnmpaste -replace <em>fname</em>-4.ppm <em>xoffset</em> <em>yoffset</em> <em>fname</em>-5.ppm &gt;<em>fname</em>-6.ppm
+</pre>
+<p>
+
+This gives the following result:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt6.jpg" width=536 height=141 alt="Offset shadow">
+</table>
+</center>
+
+<h3>Inverse Mask</h3>
+
+In order to stitch everything together, we need an inverse of the
+mask prepared earlier--one where black pixels represent the background
+and all other material is white.  This is easily accomplished by
+running the positive mask through <b>pnminvert</b>:
+
+<p>
+<pre>
+    pnminvert <em>fname</em>-1.ppm &gt;<em>fname</em>-7.ppm
+</pre>
+<p>
+
+yielding:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt7.gif" width=536 height=141 alt="Inverse mask">
+</table>
+</center>
+
+<h3>Masked Input Image</h3>
+
+Now we use the inverse mask prepared in the previous step to create
+an image containing all non-background pixels from the source image,
+with background pixels set to black.  We simply multiply the
+inverse mask by the source image:
+
+<p>
+<pre>
+    pnmarith -multiply <em>ifile</em> <em>fname</em>-7.ppm &gt;<em>fname</em>-8.ppm
+</pre>
+<p>
+
+<em>et voilą:</em>
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt8.gif" width=536 height=141 alt="Masked Input Image">
+</table>
+</center>
+
+<h3>Shadow with Source Masked</h3>
+
+Our last intermediate step before joining the image with its
+shadow is preparing a shadow image with all non-background pixels
+in the source image set to black.  This ensures that when we add
+the image and the shadow, the shadow will not override any pixel
+in the source image.
+
+<p>
+<pre>
+    pnmarith -multiply <em>fname</em>-6.ppm <em>fname</em>-1.ppm &gt;<em>fname</em>-9.ppm
+</pre>
+<p>
+
+This is accomplished by multiplying the shadow by the positive
+mask image, which sets all non-background pixels in the source
+image to black:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt9.jpg" width=536 height=141 alt="Shadow with Source Masked">
+</table>
+</center>
+
+<h3>The Final Product</h3>
+
+At long last, we're ready to put together the pieces and deliver
+the result to our ever-patient user.  This amounts simply to 
+adding the masked input image (consisting solely of non-background
+pixels from the original image) to the shadow with source masked
+(in which all source pixels are black):
+
+<p>
+<pre>
+    pnmarith -add <em>fname</em>-8.ppm <em>fname</em>-9.ppm
+</pre>
+<p>
+
+The resulting image, with shadow, is as follows:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadout.jpg" width=536 height=141 alt="Output: Image with shadow">
+</table>
+</center>
+
+<h3>Smooth Operator</h3>
+
+Since many computer graphics programs create sharp edges on
+text, it's often best to create an image at a greater resolution
+than that used for presentation, then scale it to the final
+resolution with a tool which resamples the image, thus
+minimising jagged edges by averaging adjacent
+pixels.  Using the output of <b>pnmshadow</b> as the starting
+point and scaling to half size with <b>pnmscale</b>, we arrive at
+the following smoothed image, with shadow, ready to adorn a
+Web page:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadout2.jpg" width=268 height=71 alt="Half scale image with shadow">
+</table>
+</center>
+
+<h4><a href="how-t.html">How it works with translucent shadows</a></h4>
+<h4><a href="./"><b>pnmshadow</b> main page</a></h4>
+
+<p>
+<hr>
+<p>
+<address>
+by <a href="/">John Walker</a><br>
+August 8th, 1997
+</address>
+
+</body>
+</html>
+<html>
+<head>
+<title>pnmshadow: How it Works in Translucent Mode</title>
+
+</head>
+
+<body>
+
+<center>
+<h1><img src="figures/how_title-t.jpg" width=421 height=159 alt="pnmshadow: How it Works in Translucent Mode"></h1>
+</center>
+
+<hr>
+
+<p>
+
+This document describes the process, including PBMplus commands
+and the intermediate images they create, by
+which <b><a href="./">pnmshadow</a></b>
+adds translucent shadows when the <b>-t</b> command line
+option is specified.  A <a href="how.html">companion document</a>
+describes how the default black shadows are generated.
+
+<h3>The Starting Point</h3>
+
+Let's start with the following source image, 536 pixels wide and 141
+pixels high.  We convert the image from whatever form in which
+it was originally created (GIF, JPEG, etc.) to a PPM file before
+processing it with <b>pnmshadow</b>.
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadin.gif" width=536 height=141 alt="Input image">
+</table>
+</center>
+
+<h3>The Blank Background</h3>
+
+We start by determining the size of the input image with
+<b>pnmfile</b> and then constructing an image with the same
+size as the input image consisting entirely of the background
+color, which is defined as the color of the pixel at the upper
+left corner of the source image.  This is performed by the
+command:
+
+<p>
+<pre>
+    pnmcut 0 0 1 1 <em>ifile</em> | pnmscale -xsize <em>xsize</em> -ysize <em>ysize</em> &gt;<em>fname</em>-5.ppm
+</pre>
+<p>
+
+yielding the image:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt5.gif" width=536 height=141 alt="Blank background image">
+</table>
+</center>
+
+<h3>The Positive Mask</h3>
+
+A positive mask image is created in which all pixels of the background
+color are set to white and all other pixels are black.  This is accomplished
+by subtracting the blank background image from the input (using the
+<tt>-difference</tt> option on <b>pnmarith</b> to avoid clipping at
+zero or the maximum pixel value), then inverting the result and
+thresholding it to a monochrome bitmap.
+
+<p>
+<pre>
+    pnmarith -difference <em>ifile</em> <em>fname</em>-5.ppm | pnminvert | ppmtopgm | pgmtopbm -thresh -value 1.0 &gt;<em>fname</em>-1.ppm
+</pre>
+<p>
+
+This produces the following mask image.
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt1.gif" width=536 height=141 alt="Positive mask image">
+</table>
+</center>
+
+<h3>The Blurred Image</h3>
+
+Since we wish to simulate a shadow from a nearby extended
+light source rather than a sharp shadow as cast by the
+Sun, we need to prepare a blurred version of the original
+image.
+A convolution kernel which averages
+the number of pixels specified by the <b>-b</b> option
+(default 11), written into the temporary file <tt><em>fname</em>-2.ppm</tt>
+in ASCII PGM format, and then the blurred image is created with
+the command:
+
+<p>
+<pre>
+    pnmconvol <em>fname</em>-2.ppm <em>ifile</em> &gt;<em>fname</em>-10.ppm
+</pre>
+<p>
+
+With the default blur setting of 11 pixels, the blurred image
+below is generated.
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt10-t.jpg" width=536 height=141 alt="Blurred shadow">
+</table>
+</center>
+
+<h3>Offset Shadow Clip</h3>
+
+Shadows, even bogus ones like we're generating, usually look best when
+cast by a light source diagonally displaced from the centre of the
+shadow-casting object.  To achieve this effect, we first cut a
+rectangle from the blurred shadow image reduced in size by the
+the number of pixels specified by the <b>-b</b> option
+which default to half the blur (<b>-b</b>) setting.  The
+<em>xsize</em> and <em>ysize</em> arguments in the following
+command are the size of the input image in pixels less the shadow
+displacement in the respective axis.
+
+<p>
+<pre>
+    pnmcut 0 0 <em>xsize</em> <em>ysize</em> <em>fname</em>-10.ppm &gt;<em>fname</em>-4.ppm
+</pre>
+<p>
+
+The shadow clip is the identical to the shadow on background color, but
+smaller by the offset in each direction.
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt4-t.jpg" width=531 height=136 alt="Offset shadow clip">
+</table>
+</center>
+
+<h3>Offset Shadow</h3>
+
+Now we're ready to assemble the shadow offset by the specified number
+of pixels.  We do this by pasting the image cut in the previous step
+into the blank background, yielding an image the same size as the
+source image with the blurred shadow displaced to the right and
+down.
+
+<p>
+<pre>
+    pnmpaste -replace <em>fname</em>-4.ppm <em>xoffset</em> <em>yoffset</em> <em>fname</em>-5.ppm &gt;<em>fname</em>-6.ppm
+</pre>
+<p>
+
+This gives the following result:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt6-t.jpg" width=536 height=141 alt="Offset shadow">
+</table>
+</center>
+
+<h3>Inverse Mask</h3>
+
+In order to stitch everything together, we need an inverse of the
+mask prepared earlier--one where black pixels represent the background
+and all other material is white.  This is easily accomplished by
+running the positive mask through <b>pnminvert</b>:
+
+<p>
+<pre>
+    pnminvert <em>fname</em>-1.ppm &gt;<em>fname</em>-7.ppm
+</pre>
+<p>
+
+yielding:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt7.gif" width=536 height=141 alt="Inverse mask">
+</table>
+</center>
+
+<h3>Masked Input Image</h3>
+
+Now we use the inverse mask prepared in the previous step to create
+an image containing all non-background pixels from the source image,
+with background pixels set to black.  We simply multiply the
+inverse mask by the source image:
+
+<p>
+<pre>
+    pnmarith -multiply <em>ifile</em> <em>fname</em>-7.ppm &gt;<em>fname</em>-8.ppm
+</pre>
+<p>
+
+<em>et voilą:</em>
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt8.gif" width=536 height=141 alt="Masked Input Image">
+</table>
+</center>
+
+<h3>Shadow with Source Masked</h3>
+
+Our last intermediate step before joining the image with its
+shadow is preparing a shadow image with all non-background pixels
+in the source image set to black.  This ensures that when we add
+the image and the shadow, the shadow will not override any pixel
+in the source image.
+
+<p>
+<pre>
+    pnmarith -multiply <em>fname</em>-6.ppm <em>fname</em>-1.ppm &gt;<em>fname</em>-9.ppm
+</pre>
+<p>
+
+This is accomplished by multiplying the shadow by the positive
+mask image, which sets all non-background pixels in the source
+image to black:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadt9-t.jpg" width=536 height=141 alt="Shadow with Source Masked">
+</table>
+</center>
+
+<h3>The Final Product</h3>
+
+At long last, we're ready to put together the pieces and deliver
+the image to our ever-patient user.  This amounts simply to 
+adding the masked input image (consisting solely of non-background
+pixels from the original image) to the shadow with source masked
+(in which all source pixels are black):
+
+<p>
+<pre>
+    pnmarith -add <em>fname</em>-8.ppm <em>fname</em>-9.ppm
+</pre>
+<p>
+
+The resulting image, with shadow, is as follows:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadout-t.jpg" width=536 height=141 alt="Output: Image with translucent shadow">
+</table>
+</center>
+
+<h3>Smooth Operator</h3>
+
+Since many computer graphics programs create sharp edges on
+text, it's often best to create an image at a greater resolution
+than that used for presentation, then scale it to the final
+resolution with a tool which resamples the image, thus
+minimising jagged edges by averaging adjacent
+pixels.  Using the output of <b>pnmshadow</b> as the starting
+point and scaling to half size with <b>pnmscale</b>, we arrive at
+the following smoothed image, with shadow, ready to adorn a
+Web page:
+
+<p>
+<center>
+<table border=5>
+<tr><td>
+<img src="figures/shadout2-t.jpg" width=268 height=71 alt="Half scale image with translucent shadow">
+</table>
+</center>
+
+<h4><a href="how.html">How it works with black shadows</a></h4>
+<h4><a href="./"><b>pnmshadow</b> main page</a></h4>
+
+<p>
+<hr>
+<p>
+<address>
+by <a href="/">John Walker</a><br>
+August 8th, 1997
+</address>
+
+</body>
+</html>
diff --git a/editor/ppmshift.c b/editor/ppmshift.c
new file mode 100644
index 00000000..1f8a599b
--- /dev/null
+++ b/editor/ppmshift.c
@@ -0,0 +1,137 @@
+
+/*********************************************************************/
+/* ppmshift -  shift lines of a picture left or right by x pixels    */
+/* Frank Neumann, October 1993                                       */
+/* V1.1 16.11.1993                                                   */
+/*                                                                   */
+/* version history:                                                  */
+/* V1.0    11.10.1993  first version                                 */
+/* V1.1    16.11.1993  Rewritten to be NetPBM.programming conforming */
+/*********************************************************************/
+
+#include "ppm.h"
+
+/* global variables */
+#ifdef AMIGA
+static char *version = "$VER: ppmshift 1.1 (16.11.93)"; /* Amiga version identification */
+#endif
+
+/**************************/
+/* start of main function */
+/**************************/
+int main(argc, argv)
+int argc;
+char *argv[];
+{
+	FILE* ifp;
+	time_t timenow;
+	int argn, rows, cols, format, i = 0, j = 0;
+	pixel *srcrow, *destrow;
+	pixel *pP = NULL, *pP2 = NULL;
+	pixval maxval;
+	int shift, nowshift;
+	const char * const usage = "shift [ppmfile]\n        shift: maximum number of pixels to shift a line by\n";
+
+	/* parse in 'default' parameters */
+	ppm_init(&argc, argv);
+
+	argn = 1;
+
+	/* parse in shift number */
+	if (argn == argc)
+		pm_usage(usage);
+	if (sscanf(argv[argn], "%d", &shift) != 1)
+		pm_usage(usage);
+	if (shift < 0)
+		pm_error("shift factor must be 0 or more");
+	++argn;
+
+	/* parse in filename (if present, stdin otherwise) */
+	if (argn != argc)
+	{
+		ifp = pm_openr(argv[argn]);
+		++argn;
+	}
+	else
+		ifp = stdin;
+
+	if (argn != argc)
+		pm_usage(usage);
+
+	/* read first data from file */
+	ppm_readppminit(ifp, &cols, &rows, &maxval, &format);
+
+	if (shift > cols)
+    {
+		shift = cols;
+        pm_message("shift amount is larger than picture width - reset to %d", shift);
+    }
+
+	/* no error checking required here, ppmlib does it all for us */
+	srcrow = ppm_allocrow(cols);
+
+	/* allocate a row of pixel data for the new pixels */
+	destrow = ppm_allocrow(cols);
+
+	ppm_writeppminit(stdout, cols, rows, maxval, 0);
+
+	/* get time of day to feed the random number generator */
+	timenow = time(NULL);
+	srand(timenow);
+
+	/** now do the shifting **/
+	/* the range by which a line is shifted lays in the range from */
+	/* -shift/2 .. +shift/2 pixels; however, within this range it is */
+    /* randomly chosen */
+	for (i = 0; i < rows; i++)
+	{
+		if (shift != 0)
+			nowshift = (rand() % (shift+1)) - ((shift+1) / 2);
+		else
+			nowshift = 0;
+
+		ppm_readppmrow(ifp, srcrow, cols, maxval, format);
+
+		pP = srcrow;
+		pP2 = destrow;
+
+		/* if the shift value is less than zero, we take the original pixel line and */
+		/* copy it into the destination line translated to the left by x pixels. The */
+        /* empty pixels on the right end of the destination line are filled up with  */
+		/* the pixel that is the right-most in the original pixel line.              */
+		if (nowshift < 0)
+		{
+			pP+= abs(nowshift);
+			for (j = 0; j < cols; j++)
+			{
+				PPM_ASSIGN(*pP2, PPM_GETR(*pP), PPM_GETG(*pP), PPM_GETB(*pP));
+				pP2++;
+                if (j < (cols+nowshift)-1)
+					pP++;
+			}
+		}
+		/* if the shift value is 0 or positive, the first <nowshift> pixels of the */
+		/* destination line are filled with the first pixel from the source line,  */
+		/* and the rest of the source line is copied to the dest line              */
+		else
+		{
+			for (j = 0; j < cols; j++)
+			{
+				PPM_ASSIGN(*pP2, PPM_GETR(*pP), PPM_GETG(*pP), PPM_GETB(*pP));
+				pP2++;
+                if (j >= nowshift)
+					pP++;
+			}
+		}
+
+		/* write out one line of graphic data */
+		ppm_writeppmrow(stdout, destrow, cols, maxval, 0);
+	}
+
+	pm_close(ifp);
+	ppm_freerow(srcrow);
+	ppm_freerow(destrow);
+
+	exit(0);
+}
+
diff --git a/editor/ppmspread.c b/editor/ppmspread.c
new file mode 100644
index 00000000..569d1266
--- /dev/null
+++ b/editor/ppmspread.c
@@ -0,0 +1,127 @@
+/*********************************************************************/
+/* ppmspread -  randomly displace a PPM's pixels by a certain amount */
+/* Frank Neumann, October 1993                                       */
+/* V1.1 16.11.1993                                                   */
+/*                                                                   */
+/* version history:                                                  */
+/* V1.0 12.10.1993    first version                                  */
+/* V1.1 16.11.1993    Rewritten to be NetPBM.programming conforming  */
+/*********************************************************************/
+
+#include <string.h>
+#include "ppm.h"
+
+/* global variables */
+#ifdef AMIGA
+static char *version = "$VER: ppmspread 1.1 (16.11.93)"; /* Amiga version identification */
+#endif
+
+/**************************/
+/* start of main function */
+/**************************/
+int main(argc, argv)
+int argc;
+char *argv[];
+{
+	FILE* ifp;
+	int argn, rows, cols, i, j;
+	int xdis, ydis, xnew, ynew;
+	pixel **destarray, **srcarray;
+	pixel *pP, *pP2;
+	pixval maxval;
+	pixval r1, g1, b1;
+	int amount;
+	time_t timenow;
+	const char * const usage = "amount [ppmfile]\n        amount: # of pixels to displace a pixel by at most\n";
+
+	/* parse in 'default' parameters */
+	ppm_init(&argc, argv);
+
+	argn = 1;
+
+	/* parse in amount & seed */
+	if (argn == argc)
+		pm_usage(usage);
+	if (sscanf(argv[argn], "%d", &amount) != 1)
+		pm_usage(usage);
+	if (amount < 0)
+		pm_error("amount should be a positive number");
+	++argn;
+
+	/* parse in filename (if present, stdin otherwise) */
+	if (argn != argc)
+	{
+		ifp = pm_openr(argv[argn]);
+		++argn;
+	}
+	else
+		ifp = stdin;
+
+	if (argn != argc)
+		pm_usage(usage);
+
+	/* read entire picture into buffer */
+	srcarray = ppm_readppm(ifp, &cols, &rows, &maxval);
+
+	/* allocate an entire picture buffer for dest picture */
+	destarray = ppm_allocarray(cols, rows);
+
+	/* clear out the buffer */
+	for (i=0; i < rows; i++)
+		memset(destarray[i], 0, cols * sizeof(pixel));
+
+	/* set seed for random number generator */
+	/* get time of day to feed the random number generator */
+	timenow = time(NULL);
+	srand(timenow);
+
+	/* start displacing pixels */
+	for (i = 0; i < rows; i++)
+	{
+		pP = srcarray[i];
+
+		for (j = 0; j < cols; j++)
+		{
+			xdis = (rand() % (amount+1)) - ((amount+1) / 2);
+			ydis = (rand() % (amount+1)) - ((amount+1) / 2);
+
+			xnew = j + xdis;
+			ynew = i + ydis;
+
+			/* only set the displaced pixel if it's within the bounds of the image */
+			if (xnew >= 0 && xnew < cols && ynew >= 0 && ynew < rows)
+			{
+				/* displacing a pixel is accomplished by swapping it with another */
+				/* pixel in its vicinity - so, first store other pixel's RGB      */
+                pP2 = srcarray[ynew] + xnew;
+				r1 = PPM_GETR(*pP2);
+				g1 = PPM_GETG(*pP2);
+				b1 = PPM_GETB(*pP2);
+				/* set second pixel to new value */
+				pP2 = destarray[ynew] + xnew;
+				PPM_ASSIGN(*pP2, PPM_GETR(*pP), PPM_GETG(*pP), PPM_GETB(*pP));
+
+				/* now, set first pixel to (old) value of second */
+				pP2 = destarray[i] + j;
+				PPM_ASSIGN(*pP2, r1, g1, b1);
+			}
+			else
+			{
+                /* displaced pixel is out of bounds; leave the old pixel there */
+                pP2 = destarray[i] + j;
+				PPM_ASSIGN(*pP2, PPM_GETR(*pP), PPM_GETG(*pP), PPM_GETB(*pP));
+			}
+			pP++;
+		}
+	}
+
+	/* write out entire dest picture in one go */
+	ppm_writeppm(stdout, destarray, cols, rows, maxval, 0);
+
+	pm_close(ifp);
+	ppm_freearray(srcarray, rows);
+	ppm_freearray(destarray, rows);
+
+	exit(0);
+}
+
diff --git a/editor/ppmtv.c b/editor/ppmtv.c
new file mode 100644
index 00000000..da25102a
--- /dev/null
+++ b/editor/ppmtv.c
@@ -0,0 +1,105 @@
+
+/*********************************************************************/
+/* ppmtv -  make a 'look-alike ntsc' picture from a PPM file       */
+/* Frank Neumann, October 1993                                       */
+/* V1.1 16.11.1993                                                   */
+/*                                                                   */
+/* version history:                                                  */
+/* V1.0 12.10.1993    first version                                  */
+/* V1.1 16.11.1993    Rewritten to be NetPBM.programming conforming  */
+/*********************************************************************/
+
+#include "ppm.h"
+
+/**************************/
+/* start of main function */
+/**************************/
+int main(argc, argv)
+int argc;
+char *argv[];
+{
+	FILE* ifp;
+	int argn, rows, cols, format, i = 0, j = 0;
+	pixel *srcrow, *destrow;
+	pixel *pP = NULL, *pP2 = NULL;
+	pixval maxval;
+	double dimfactor;
+	long longfactor;
+	const char * const usage = "dimfactor [ppmfile]\n        dimfactor: 0.0 = total blackness, 1.0 = original picture\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", &dimfactor) != 1)
+		pm_usage(usage);
+	if (dimfactor < 0.0 || dimfactor > 1.0)
+		pm_error("dim factor must be in the range from 0.0 to 1.0 ");
+	++argn;
+
+	/* parse in filename (if present, stdin otherwise) */
+	if (argn != argc)
+	{
+		ifp = pm_openr(argv[argn]);
+		++argn;
+	}
+	else
+		ifp = stdin;
+
+	if (argn != argc)
+		pm_usage(usage);
+
+	/* read first data from file */
+	ppm_readppminit(ifp, &cols, &rows, &maxval, &format);
+
+	/* no error checking required here, ppmlib does it all for us */
+	srcrow = ppm_allocrow(cols);
+
+	longfactor = (long)(dimfactor * 65536);
+
+	/* allocate a row of pixel data for the new pixels */
+	destrow = ppm_allocrow(cols);
+
+	ppm_writeppminit(stdout, cols, rows, maxval, 0);
+
+	/** now do the ntsc'ing (actually very similar to ppmdim) **/
+	for (i = 0; i < rows; i++)
+	{
+		ppm_readppmrow(ifp, srcrow, cols, maxval, format);
+
+		pP = srcrow;
+		pP2 = destrow;
+
+		for (j = 0; j < cols; j++)
+		{
+			/* every alternating row is left in unchanged condition */
+			if (i & 1)
+			{
+				PPM_ASSIGN(*pP2, PPM_GETR(*pP), PPM_GETG(*pP), PPM_GETB(*pP));
+			}
+			/* and the other lines are dimmed to the specified factor */
+			else
+			{
+				PPM_ASSIGN(*pP2, (PPM_GETR(*pP) * longfactor) >> 16,
+								 (PPM_GETG(*pP) * longfactor) >> 16,
+								 (PPM_GETB(*pP) * longfactor) >> 16);
+			}
+			pP++;
+			pP2++;
+		}
+
+		/* write out one line of graphic data */
+		ppm_writeppmrow(stdout, destrow, cols, maxval, 0);
+	}
+
+	pm_close(ifp);
+	ppm_freerow(srcrow);
+	ppm_freerow(destrow);
+
+	exit(0);
+}
+