about summary refs log tree commit diff
path: root/editor/pnmrotate.c
diff options
context:
space:
mode:
Diffstat (limited to 'editor/pnmrotate.c')
-rw-r--r--editor/pnmrotate.c808
1 files changed, 808 insertions, 0 deletions
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;
+}