Index: /trunk/pstamp/.cvsignore
===================================================================
--- /trunk/pstamp/.cvsignore	(revision 16132)
+++ /trunk/pstamp/.cvsignore	(revision 16132)
@@ -0,0 +1,17 @@
+bin
+aclocal.m4
+autom4te.cache
+compile
+config.log
+config.status
+configure
+depcomp
+install-sh
+Makefile.in
+missing
+Makefile
+config.cache
+config.guess
+config.sub
+ltmain.sh
+libtool
Index: /trunk/pstamp/Makefile.am
===================================================================
--- /trunk/pstamp/Makefile.am	(revision 16132)
+++ /trunk/pstamp/Makefile.am	(revision 16132)
@@ -0,0 +1,3 @@
+SUBDIRS = src
+
+CLEANFILES = *~ core core.*
Index: /trunk/pstamp/autogen.sh
===================================================================
--- /trunk/pstamp/autogen.sh	(revision 16132)
+++ /trunk/pstamp/autogen.sh	(revision 16132)
@@ -0,0 +1,103 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+srcdir=`dirname $0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+cd $srcdir
+
+PROJECT=pstamp
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL="aclocal $ACLOCAL_FLAGS"
+AUTOHEADER=autoheader
+AUTOMAKE=automake
+AUTOCONF=autoconf
+
+($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $LIBTOOlIZE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/libtool/"
+        DIE=1
+}
+
+($ACLOCAL --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $ACLOCAL installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOHEADER --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOHEADER installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOMAKE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOCONF --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOCONF installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+if test "$DIE" -eq 1; then
+        exit 1
+fi
+
+test $TEST_TYPE $FILE || {
+        echo "You must run this script in the top-level $PROJECT directory"
+        exit 1
+}
+
+if test -z "$*"; then
+        echo "I am going to run ./configure with no arguments - if you wish "
+        echo "to pass any to it, please specify them on the $0 command line."
+fi
+
+$LIBTOOLIZE --copy --force || echo "$LIBTOOlIZE failed"
+$ACLOCAL || echo "$ACLOCAL failed"
+$AUTOHEADER || echo "$AUTOHEADER failed"
+$AUTOMAKE --add-missing --force-missing --copy || echo "$AUTOMAKE failed"
+$AUTOCONF || echo "$AUTOCONF failed"
+
+cd $ORIGDIR
+
+run_configure=true
+for arg in $*; do
+    case $arg in
+        --no-configure)
+            run_configure=false
+            ;;
+        *)
+            ;;
+    esac
+done
+
+if $run_configure; then
+    $srcdir/configure --enable-maintainer-mode "$@"
+    echo
+    echo "Now type 'make' to compile $PROJECT."
+else
+    echo
+    echo "Now run 'configure' and 'make' to compile $PROJECT."
+fi
Index: /trunk/pstamp/configure.ac
===================================================================
--- /trunk/pstamp/configure.ac	(revision 16132)
+++ /trunk/pstamp/configure.ac	(revision 16132)
@@ -0,0 +1,47 @@
+dnl Process this file with autoconf to produce a configure script.
+AC_PREREQ(2.61)
+
+AC_INIT([ppstamp], [0.1.1], [ipp-support@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([src])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+AM_CONFIG_HEADER([src/config.h])
+AM_MAINTAINER_MODE
+
+IPP_STDCFLAGS
+
+AC_LANG(C)
+AC_GNU_SOURCE
+AC_PROG_CC_C99
+AC_PROG_INSTALL
+AC_PROG_LIBTOOL
+AC_SYS_LARGEFILE
+
+dnl ------------------------------------------------------------
+
+AC_PATH_PROG([ERRORCODES], [psParseErrorCodes], [missing])
+if test "$ERRORCODES" = "missing" ; then
+  AC_MSG_ERROR([psParseErrorCodes is required])
+fi
+
+
+PKG_CHECK_MODULES([PSLIB], [pslib >= 1.0.0])
+PKG_CHECK_MODULES([PSMODULE], [psmodules >= 1.0.0])
+PKG_CHECK_MODULES([PPSTATS], [ppStats >= 1.0.0]) 
+PKG_CHECK_MODULES([PSPHOT], [psphot >= 0.8.0]) 
+
+dnl Set CFLAGS for build
+IPP_STDOPTS
+CFLAGS="${CFLAGS} -Wall -Werror"
+echo "PPSTAMP_CFLAGS: $PPSTAMP_CFLAGS"
+echo "PPSTAMP_LIBS: $PPSTAMP_LIBS"
+
+AC_SUBST([PPSTAMP_CFLAGS])
+AC_SUBST([PPSTAMP_LIBS])
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+])
+
+AC_OUTPUT
Index: /trunk/pstamp/src/.cvsignore
===================================================================
--- /trunk/pstamp/src/.cvsignore	(revision 16132)
+++ /trunk/pstamp/src/.cvsignore	(revision 16132)
@@ -0,0 +1,10 @@
+ppstamp
+Makefile
+Makefile.in
+.deps
+config.h
+config.h.in
+stamp-h1
+ppstampErrorCodes.c
+ppstampErrorCodes.h
+.libs
Index: /trunk/pstamp/src/Makefile.am
===================================================================
--- /trunk/pstamp/src/Makefile.am	(revision 16132)
+++ /trunk/pstamp/src/Makefile.am	(revision 16132)
@@ -0,0 +1,55 @@
+bin_PROGRAMS = ppstamp pstamprequest pstampparse
+
+noinst_HEADERS = \
+	ppstamp.h  \
+        pstampErrorCodes.h
+
+ppstamp_CPPFLAGS = $(PSLIB_CFLAGS) $(PSMODULE_CFLAGS) 
+pstamprequest_CPPFLAGS = $(PSLIB_CFLAGS) $(PSMODULE_CFLAGS) 
+#pstampparse_CPPFLAGS = $(PSLIB_CFLAGS) $(PSMODULE_CFLAGS)  $(IPPDB_CFLAGS)
+pstampparse_CPPFLAGS = $(PSLIB_CFLAGS) $(PSMODULE_CFLAGS)
+
+# $(PSASTRO_CFLAGS) $(PPSTATS_CFLAGS) $(ppstamp_CFLAGS)
+
+ppstamp_LDFLAGS = $(PSLIB_LIBS) $(PSMODULE_LIBS) 
+pstamprequest_LDFLAGS = $(PSLIB_LIBS) $(PSMODULE_LIBS) 
+#pstampparse_LDFLAGS = $(PSLIB_LIBS) $(PSMODULE_LIBS) $(IPPDB_LIBS)
+pstampparse_LDFLAGS = $(PSLIB_LIBS) $(PSMODULE_LIBS)
+
+ppstamp_SOURCES = \
+	ppstamp.c \
+	ppstampArguments.c \
+	ppstampCleanup.c \
+        pstampErrorCodes.c \
+	ppstampMakeStamp.c \
+	ppstampMosaic.c \
+	ppstampOptions.c \
+	ppstampParseCamera.c \
+        ppstampRegion.c \
+	ppstampVersion.c \
+	pstampGetROI.c
+
+pstamprequest_SOURCES = \
+	pstamprequest.c \
+	pstampGetROI.c \
+	ppstampOptions.c
+
+pstampparse_SOURCES = \
+	pstampparse.c
+
+clean-local:
+	-rm -f TAGS
+
+# Tags for emacs
+tags:
+	etags `find . -name \*.[ch] -print`
+
+### Error codes.
+BUILT_SOURCES = pstampErrorCodes.h pstampErrorCodes.c
+CLEANFILES = pstampErrorCodes.h pstampErrorCodes.c
+
+pstampErrorCodes.h : pstampErrorCodes.dat pstampErrorCodes.h.in
+	$(ERRORCODES) --data=pstampErrorCodes.dat --outdir=. pstampErrorCodes.h
+
+pstampErrorCodes.c : pstampErrorCodes.dat pstampErrorCodes.c.in pstampErrorCodes.h
+	$(ERRORCODES) --data=pstampErrorCodes.dat --outdir=. pstampErrorCodes.c
Index: /trunk/pstamp/src/ppstamp.c
===================================================================
--- /trunk/pstamp/src/ppstamp.c	(revision 16132)
+++ /trunk/pstamp/src/ppstamp.c	(revision 16132)
@@ -0,0 +1,43 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "ppstamp.h"
+
+int main(int argc, char **argv)
+{
+    ppstampOptions *options = NULL;
+    int exitCode;
+
+    psTimerStart(TIMERNAME);
+    psLibInit(NULL);
+    pstampErrorRegister();
+
+    pmConfig *config = ppstampArguments(argc, argv, &options);
+    if (config == NULL) {
+        psErrorStackPrint(stderr, "Unable to parse command-line arguments.");
+        ppstampCleanup(config, options);
+        exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    // define the active I/O files
+    if (!ppstampParseCamera(config)) {
+        psErrorStackPrint(stderr, "Unable to parse camera.");
+        ppstampCleanup(config, options);
+        exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    // find the pixels that we need to copy, setup the output image
+    if (ppstampMakeStamp(config, options)) {
+        exitCode = 0;
+    } else {
+        exitCode = PS_EXIT_DATA_ERROR;
+    }
+
+    psLogMsg ("ppstamp", 3, "Complete ppstamp run: %f sec\n", psTimerMark(TIMERNAME));
+
+    // Cleaning up
+    ppstampCleanup(config, options);
+
+    return exitCode;
+}
Index: /trunk/pstamp/src/ppstamp.h
===================================================================
--- /trunk/pstamp/src/ppstamp.h	(revision 16132)
+++ /trunk/pstamp/src/ppstamp.h	(revision 16132)
@@ -0,0 +1,41 @@
+#ifndef PP_STAMP_H
+#define PP_STAMP_H
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "pstamp.h"
+#include "ppstampOptions.h"
+
+#define TIMERNAME "ppstamp"             // Name of timer
+
+// Determine the processing options
+bool *ppstampOptionsParse(pmConfig *config);
+
+// Get the configuration
+pmConfig *ppstampArguments(int argc, char **argv, ppstampOptions **);
+
+// Determine what type of camera, and initialise
+bool ppstampParseCamera(pmConfig *config);
+
+bool ppstampMakeStamp(pmConfig *config, ppstampOptions *);
+pmFPAfile * ppstampBuildMosaic(pmConfig *config, pmFPAfile *input, pmFPAview *view);
+
+// free memory, check for leaks
+void ppstampCleanup (pmConfig *config, ppstampOptions *options);
+
+/// Return short version information
+psString ppstampVersion(void);
+
+/// Return long version information
+psString ppstampVersionLong(void);
+
+/// Update the metadata with version information for all dependencies
+void ppstampVersionMetadata(psMetadata *metadata, ppstampOptions *options);
+
+psRegion *ppstampCellRegion(const pmCell *cell);
+psRegion *ppstampChipRegion(const pmChip *chip);
+
+extern bool ppstampMegacamWorkaround;
+#endif
Index: /trunk/pstamp/src/ppstampArguments.c
===================================================================
--- /trunk/pstamp/src/ppstampArguments.c	(revision 16132)
+++ /trunk/pstamp/src/ppstampArguments.c	(revision 16132)
@@ -0,0 +1,100 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <pslib.h>
+#include "ppstamp.h"
+#include "ppstampOptions.h"
+
+static void usage (void)
+{
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "USAGE: ppstamp -skycenter RA DEC -pixrange dx dy    [-file INPUT.fits] [-list INPUT.txt] OUTPUT\n");
+    fprintf(stderr, "       ppstamp -skycenter RA DEC -arcrange dRA dDEC [-file INPUT.fits] [-list INPUT.txt] OUTPUT\n");
+    fprintf(stderr, "       ppstamp -pixcenter x y    -pixrange dx dy    [-chip chipname] [-file INPUT.fits] [-list INPUT.txt] OUTPUT\n");
+    fprintf(stderr, "       ppstamp -pixcenter x y    -arcrange dRA dDEC [-chip chipname] [-file INPUT.fits] [-list INPUT.txt] OUTPUT\n");
+    fprintf(stderr, "\n");
+
+    fprintf(stderr, "-skycenter is specified as HH:MM:SS DD:MM:SS or decimal degrees\n");
+    fprintf(stderr, "-arcrange values are seconds of arc\n");
+    fprintf(stderr, "\n");
+    fprintf(stderr, "Optional arguments:\n");
+    fprintf(stderr, "         -chip chipName    selects chip (only used with -pixcenter)\n");
+    fprintf(stderr, "\n");
+
+    exit (2);
+}
+
+pmConfig *ppstampArguments(int argc, char **argv, ppstampOptions **pOptions)
+{
+    int argnum;                         // Argument number of interest
+    bool gotCenter = false;
+    bool gotRange = false;
+    ppstampOptions *options;
+
+    if (argc == 1) {
+        usage();
+    }
+
+    if (psArgumentGet (argc, argv, "-version")) {
+        psString version;
+        version = ppstampVersionLong();   fprintf (stdout, "%s\n", version); psFree (version);
+        version = psModulesVersionLong(); fprintf (stdout, "%s\n", version); psFree (version);
+        version = psLibVersionLong();     fprintf (stdout, "%s\n", version); psFree (version);
+        exit (0);
+    }
+
+    // load the site-wide configuration information
+    pmConfig *config = pmConfigRead(&argc, argv, NULL);
+    if (config == NULL) {
+        psError(PSTAMP_ERR_CONFIG, false, "Can't read site configuration!\n");
+        return(NULL);
+    }
+
+    options = ppstampOptionsAlloc();
+    *pOptions = options;
+
+    if (!pstampGetROI(&options->roip, &argc, argv, &gotCenter, &gotRange)) {
+        usage();
+    }
+
+
+    if (!gotCenter) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "must specify center of region of interest\n");
+        usage();
+    }
+
+    if (!gotRange) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "must specify extent of region of interest\n");
+        usage();
+    }
+
+    if ((argnum = psArgumentGet(argc, argv, "-chip"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        options->chipName = psStringCopy(argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+    }
+
+    // the input file is a required argument; if not found, we will exit
+    bool status = pmConfigFileSetsMD (config->arguments, &argc, argv, "INPUT", "-file", "-list");
+    if (!status) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "must specify INPUT\n");
+        usage();
+    }
+
+    // finally the output file
+    if (argc < 2) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "must specify OUTPUT\n");
+        usage();
+    } else if (argc > 2) {
+        psError(PSTAMP_ERR_ARGUMENTS, true, "unknown argument(s)\n");
+        usage();
+    }
+
+    // Add the output image (which remains on the command-line) to the arguments list
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "Name of the output image", argv[1]);
+
+    // psMetadataPrint(stdout, config->arguments,0);
+
+    return config;
+}
Index: /trunk/pstamp/src/ppstampCleanup.c
===================================================================
--- /trunk/pstamp/src/ppstampCleanup.c	(revision 16132)
+++ /trunk/pstamp/src/ppstampCleanup.c	(revision 16132)
@@ -0,0 +1,25 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+# include "ppstamp.h"
+
+void ppstampCleanup (pmConfig *config, ppstampOptions *options)
+{
+    // Free memory used by ppstamp
+    psFree(config);
+    psFree(options);
+
+    // Free memory used by psModules
+    pmConceptsDone();
+    pmConfigDone();
+    pmModelClassCleanup();
+
+    // Free memory used by psLib
+    psLibFinalize();
+
+    // psMemCheckLeaks (0, NULL, stderr, false);
+    // fprintf (stderr, "Found %d leaks in %s\n", psMemCheckLeaks (0, NULL, NULL, false), "ppstamp");
+
+    return;
+}
Index: /trunk/pstamp/src/ppstampMakeStamp.c
===================================================================
--- /trunk/pstamp/src/ppstampMakeStamp.c	(revision 16132)
+++ /trunk/pstamp/src/ppstampMakeStamp.c	(revision 16132)
@@ -0,0 +1,584 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "ppstamp.h"
+#include "pmAstrometryUtils.h"
+
+#define WCS_NONLIN_TOL 0.001            // Non-linear tolerance for header WCS
+
+typedef enum {
+    PPSTAMP_OFF,
+    PPSTAMP_PARTIALLY_ON,
+    PPSTAMP_ON,
+    PSTAMP_ERROR
+} ppstampOverlap;
+
+static void skyToChip(pmAstromObj *pt, pmFPA *fpa, pmChip *chip);
+
+// convert the input chip's transforms to the output
+static bool convertWCS(pmHDU *outHDU, pmFPA *outFPA, pmChip *outChip, pmChip *inChip, psRegion *roi)
+{
+    pmHDU *hdu = pmHDUFromChip(inChip);
+    PS_ASSERT_PTR_NON_NULL(hdu, 1)
+    PS_ASSERT_PTR_NON_NULL(hdu->header, 1)
+
+    // Change the reference pixel to account for the change in origin between the stamp and
+    // the original image
+    outChip->toFPA   = psPlaneTransformSetCenter(NULL, inChip->toFPA, (int) roi->x0, (int) roi->y0);
+
+    outChip->fromFPA = psPlaneTransformInvert(NULL, outChip->toFPA, *roi, 50);
+
+    // remove these keys which may have been copied from the input header
+    // XXX pmAstromWriteWCS should do this since it's the one that's inserting
+    // the PC00* style keywords
+    if (psMetadataLookup(outHDU->header, "CD1_1")) {
+        psMetadataRemoveKey(outHDU->header, "CD1_1");
+        psMetadataRemoveKey(outHDU->header, "CD1_2");
+        psMetadataRemoveKey(outHDU->header, "CD2_1");
+        psMetadataRemoveKey(outHDU->header, "CD2_2");
+    }
+
+    if (!pmAstromWriteWCS(outHDU->header, outFPA, outChip, WCS_NONLIN_TOL)) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to write WCS to output\n");
+        return false;
+    }
+
+    return true;
+}
+
+static bool copyMetadata(pmFPAfile *output, pmFPAfile *input, pmChip *inChip, ppstampOptions *options)
+{
+    pmChip    *outChip;
+    pmFPAview *view = pmFPAviewAlloc(0);
+
+    // our output file has a single chip
+    view->chip = 0;
+    outChip = pmFPAviewThisChip(view, output->fpa);
+    psFree(view);
+
+    // copy data from the input chip header to the output.
+    // since some of the keywords might be duplicated we may not want to copy both
+
+    // copy the fpa concepts
+    outChip->parent->concepts = psMetadataCopy(outChip->parent->concepts, inChip->parent->concepts);
+
+    pmHDU *inHDU  = pmHDUFromChip(inChip);
+    pmHDU *outHDU = pmHDUGetHighest(output->fpa, outChip, NULL);
+
+    if (inHDU->header) {
+        outHDU->header = psMetadataCopy(outHDU->header, inHDU->header);
+    } else if (!outHDU->header) {
+        outHDU->header = psMetadataAlloc();
+    }
+
+
+    // If input had WCS convert it for stamp
+    if (input->fpa->toSky) {
+        // steal the input fpa's transforms
+        output->fpa->toTPA   = input->fpa->toTPA;
+        output->fpa->fromTPA = input->fpa->fromTPA;
+        output->fpa->toSky   = input->fpa->toSky;
+
+        // drop the references
+        input->fpa->toTPA   = 0;
+        input->fpa->fromTPA = 0;
+        input->fpa->toSky   = 0;
+
+        if (!convertWCS(outHDU, output->fpa, outChip, inChip, &options->roi)) {
+            return false;
+        }
+    } else {
+        psWarning("No WCS present in input\n");
+    }
+
+    ppstampVersionMetadata(outHDU->header, options);
+
+    return true;
+}
+
+#ifdef notdef
+// update the ROI to account for any change in the coordinate system in the
+// mosaic process
+// THERE isn't any change. This is something I was trying to make the megacam
+// transforms come out correctly.
+static bool updateROI(ppstampOptions *options, pmFPAfile *mosaic, pmChip *mosaicChip)
+{
+    // set up the astrometry
+    pmHDU *hdu = pmHDUFromChip(mosaicChip);
+    PS_ASSERT_PTR_NON_NULL(hdu, 1)
+    PS_ASSERT_PTR_NON_NULL(hdu->header, 1)
+
+
+    if (!pmAstromReadWCS(mosaic->fpa, mosaicChip, hdu->header, 1.0)) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to Read WCS from mosaic, cannot proceed\n");
+        return false;
+    }
+    pmAstromObj *center = pmAstromObjAlloc();
+
+    center->sky->r = options->centerRA;
+    center->sky->d = options->centerDEC;
+    center->sky->rErr = 0;
+    center->sky->dErr = 0;
+
+    skyToChip(center, mosaic->fpa, mosaicChip);
+
+    int width = options->dX;
+    int height = options->dY;
+
+    fprintf(stderr, "ROI before: %s\n", psRegionToString(options->roi));
+
+    options->roi.x0 = center->chip->x - width/2;
+    options->roi.x1 = options->roi.x0 + width;
+    options->roi.y0 = center->chip->y - height/2;
+    options->roi.y1 = options->roi.y0 + height;
+
+    fprintf(stderr, "ROI after:  %s\n", psRegionToString(options->roi));
+
+
+    return true;
+}
+#endif
+
+// Build the postage stamp output file
+
+static bool makeStamp(pmConfig *config, ppstampOptions *options, pmFPAfile *input, 
+                pmChip *inChip, pmFPAview *view)
+{
+    int status = false;
+
+    pmFPAfile *output = psMetadataLookupPtr(NULL, config->files, "PPSTAMP.OUTPUT");
+    if (!output) {
+        psError(PS_ERR_UNKNOWN, false, "Can't find output data\n");
+        return false;
+    }
+    char *fpaName = psMetadataLookupStr(NULL, input->fpa->concepts, "FPA.NAME"); // Name of FPA
+    pmFPAview *outview = pmFPAviewAlloc(0);
+    pmFPAAddSourceFromView(output->fpa, fpaName, outview, output->format);
+
+    outview->chip = 0;
+    outview->cell = 0;
+    pmCell *target =  pmFPAviewThisCell(outview, output->fpa); // Target cell
+    psFree(outview);
+    outview = NULL;
+
+    //   psMetadataPrint(stderr, inChip->concepts, 0);
+    
+    // These default to zero. would that be ok?
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.XBIN", PS_META_REPLACE, "Binning in x", 1);
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.YBIN", PS_META_REPLACE, "Binning in y", 1);
+
+
+    // we extract our postage stamp from a mosaic so that
+    // pmChipMosaic can handle the tricky bits of parity, binning, etc.
+    pmFPAfile *mosaic = ppstampBuildMosaic(config, input, view);
+    if (mosaic == NULL) {
+        return false;
+    }
+
+    // one cell one chip
+    pmFPAview *mosaicView = pmFPAviewAlloc(0);
+    mosaicView->chip = 0;
+    mosaicView->cell = 0;
+
+    pmReadout *readout;
+    while ((readout = pmFPAviewNextReadout (mosaicView, mosaic->fpa, 1)) != NULL) {
+        if (!readout->data_exists) {
+            psError(PS_ERR_UNKNOWN, false, "no data in mosaic readout!\n");
+            continue;
+        }
+        pmReadout *outReadout = pmReadoutAlloc(target);
+        if (!outReadout) {
+            psError(PS_ERR_UNKNOWN, false, "failed to allocate output readout\n");
+            status = false;
+            break;
+        }
+
+        psRegion extractRegion = options->roi;
+
+        // Close your eyes while I hack around bug 986
+        if (ppstampMegacamWorkaround) {
+            // the coordinates of the mosaic are shifted 32 pixels from the chip
+            extractRegion.x0 -= 32;
+            extractRegion.x1 -= 32;
+        }
+    
+        psImage *subsetImage = psImageSubset(readout->image, extractRegion);
+
+        if (subsetImage) {
+            // make a copy since we're going to change the image's internals
+            outReadout->image = psImageCopy(NULL, subsetImage, subsetImage->type.type);
+            psFree(subsetImage);
+            if (outReadout->image) {
+                // The image has inherited the subset's col0 and row0. We don't want this in
+                // our output so override the values.
+                // XXX put this into a function in psImage
+                // (What I really needed was a function that does
+                //     "create a copy of this portion of an image and return it as a new image with
+                //      origin 0,0 ")
+                P_PSIMAGE_SET_COL0(outReadout->image, 0);
+                P_PSIMAGE_SET_ROW0(outReadout->image, 0);
+
+                outReadout->data_exists = true;
+                outReadout->parent->data_exists = true;
+                outReadout->parent->parent->data_exists = true;
+                status = true;
+            } else {
+                psError(PS_ERR_UNKNOWN, false, "Copying of readout image failed.\n");
+                status = false;
+            }
+        } else {
+            psError(PS_ERR_UNKNOWN, false, "Image subset failed.\n");
+            status = false;
+        }
+        psFree(outReadout); // drop reference
+    }
+
+    // XXXX remove this or make writing the mosaic to a file a valid option
+    static bool writeMosaic = false;
+    if (writeMosaic) {
+        mosaic->save = true;
+        mosaicView->cell = -1;
+        pmFPAfileIOChecks(config, mosaicView, PM_FPA_AFTER);
+    }
+
+    psFree(mosaicView);
+
+    if (status) {
+        status = copyMetadata(output, input, inChip, options);
+    }
+    return status;
+}
+
+
+
+static bool regionContainsPoint(psRegion *r, psPlane *pt)
+{
+    if (pt->x < r->x0)
+        return false;
+    if (pt->x >= r->x1)
+        return false;
+    if (pt->y < r->y0)
+        return false;
+    if (pt->y >= r->y1)
+        return false;
+
+    return true;
+}
+
+// true if the inner region is equal to or completely contained in
+// the outer region
+static bool regionContainsRegion(psRegion *outer, psRegion *inner)
+{
+    if ((outer->x0 <= inner->x0) &&
+        (outer->y0 <= inner->y0) &&
+        (outer->x1 >= inner->x1) &&
+        (outer->y1 >= inner->y1)) {
+        return true;
+    } else {
+        return false;
+    }
+}
+
+
+static void skyToChip(pmAstromObj *pt, pmFPA *fpa, pmChip *chip)
+{
+    // convert from sky to TP to FP
+    psProject(pt->TP, pt->sky, fpa->toSky);
+    psPlaneTransformApply(pt->FP, fpa->fromTPA, pt->TP);
+    // convert from FP to chip
+    psPlaneTransformApply(pt->chip, chip->fromFPA, pt->FP);
+}
+
+static void chipToSky(pmAstromObj *pt, pmFPA *fpa, pmChip *chip)
+{
+    // chip to FP
+    psPlaneTransformApply(pt->FP, chip->toFPA, pt->chip);
+    // FP to TP to sky
+    psPlaneTransformApply(pt->TP, fpa->toTPA, pt->FP);
+    psDeproject(pt->sky, pt->TP, fpa->toSky);
+
+}
+
+static void compareToBox(psRegion *region, psPlane *pt)
+{
+    if (pt->x < region->x0)
+        region->x0 = pt->x;
+    if (pt->x > region->x1)
+        region->x1 = pt->x;
+    if (pt->y < region->y0)
+        region->y0 = pt->y;
+    if (pt->y > region->y1)
+        region->y1 = pt->y;
+}
+
+// For roi width and height given in sky coordinates find a bounding box in chip coordinates
+// that encloses the requested range
+static void findBoundingBox(ppstampOptions *options, pmFPA *fpa, pmChip *chip, pmAstromObj *center)
+{
+    pmAstromObj *pt = pmAstromObjAlloc();
+
+    if (!options->roip.celestialCenter) {
+        // Center was specified in chip coordinates, transform to the sky
+        chipToSky(center, fpa, chip);
+    }
+
+    // calculate the four corners of the bounding box in sky coordinates, translate them to
+    // chip coordinates and build the ROI by comparison.
+
+    options->roi.x0 = INFINITY;
+    options->roi.x1 = -INFINITY;
+    options->roi.y0 = INFINITY;
+    options->roi.y1 = -INFINITY;
+    
+    pt->sky->rErr = 0;
+    pt->sky->dErr = 0;
+
+    pt->sky->r = center->sky->r - options->roip.dRA/2;
+    pt->sky->d = center->sky->d - options->roip.dDEC/2;
+    skyToChip(pt, fpa, chip);
+    compareToBox(&options->roi, pt->chip);
+
+    pt->sky->r = center->sky->r + options->roip.dRA/2;
+    pt->sky->d = center->sky->d - options->roip.dDEC/2;
+    skyToChip(pt, fpa, chip);
+    compareToBox(&options->roi, pt->chip);
+
+    pt->sky->r = center->sky->r + options->roip.dRA/2;
+    pt->sky->d = center->sky->d + options->roip.dDEC/2;
+    skyToChip(pt, fpa, chip);
+    compareToBox(&options->roi, pt->chip);
+
+    pt->sky->r = center->sky->r - options->roip.dRA/2;
+    pt->sky->d = center->sky->d + options->roip.dDEC/2;
+    skyToChip(pt, fpa, chip);
+    compareToBox(&options->roi, pt->chip);
+
+    psFree(pt);
+
+    // save the width and height in case we need them later
+    options->roip.dX = options->roi.x1 - options->roi.x0;
+    options->roip.dY = options->roi.y1 - options->roi.y0;
+}
+
+#ifdef notdef
+// I'm not using this any more. We mosaic the chip before extracting the pixels
+// so we don't need to deal with cells at this level.
+
+// findCell
+// find cell on given chip that contains the region of interest
+// return the status of the overlap and if the cell completely contains the ROI 
+// set a pointer to reference the cell
+
+static ppstampOverlap findCell(pmFPAview *view, ppstampOptions *options, pmFPAfile *input,
+                                pmAstromObj *center, pmCell **ppCell)
+{
+    pmCell *cell;
+
+    view->cell = -1;
+    while ((cell = pmFPAviewNextCell(view, input->fpa, 1)) != NULL) {
+        psRegion *cellExtent = pmCellExtent(cell);
+        if (regionContainsPoint(cellExtent, center->chip)) {
+            if (regionContainsRegion(cellExtent, &options->roi)) {
+                *ppCell = cell;
+                psFree(cellExtent);
+                return PPSTAMP_ON;
+            } else {
+                fprintf(stderr, "ROI must be confined to single cell. Partial overlap cell %d\n",
+                    view->cell);
+                fprintf(stderr, "Partial Overlap cell %d\n", view->cell);
+                fprintf(stderr, "  ROI:         %s\n", psRegionToString(options->roi));
+                fprintf(stderr, "  Cell Extent: %s\n", psRegionToString(*cellExtent));
+                psFree(cellExtent);
+                return PPSTAMP_PARTIALLY_ON;
+            }
+        }
+        psFree(cellExtent);
+    }
+
+    // This shouldn't ever happen since ROI is on the chip, it must at least partially overlap
+    // one of the cells
+    psError(PS_ERR_PROGRAMMING, false, "findCell couldn't find a cell containing the center\n");
+
+    return PPSTAMP_OFF;
+}
+#endif
+
+
+
+// findROI
+// calculate the region of interest in chip coordinates and determine whether that region
+// is completely contained on a single chip
+
+static ppstampOverlap findROI(ppstampOptions *options, pmFPAview *view, pmFPAfile *input, pmChip *chip,
+                       pmAstromObj *center)
+{
+    psString chipName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME");
+    psRegion    *chipBounds = ppstampChipRegion(chip);
+    bool        onChip = false;
+    ppstampOverlap   returnval = PPSTAMP_OFF;
+
+//    fprintf(stderr, "ppstampChipBounds: %s\n", psRegionToString(*chipBounds));
+
+    // set up the astrometry
+    pmHDU *hdu = pmHDUFromChip(chip);
+    PS_ASSERT_PTR_NON_NULL(hdu, 1)
+    PS_ASSERT_PTR_NON_NULL(hdu->header, 1)
+
+    if (!pmAstromReadWCS(input->fpa, chip, hdu->header, 1.0)) {
+        // we can live without WCS if the ROI is specified in pixels
+
+        if (options->roip.celestialCenter || options->roip.celestialRange) {
+            psError(PSTAMP_ERR_DATA, false, "Failed to Read WCS, cannot proceed\n");
+            psFree(chipBounds);
+            return PSTAMP_ERROR;
+        }
+    }
+
+    if (options->roip.celestialCenter) {
+
+        center->sky->r = options->roip.centerRA;
+        center->sky->d = options->roip.centerDEC;
+        center->sky->rErr = 0;
+        center->sky->dErr = 0;
+
+        skyToChip(center, input->fpa, chip);
+
+        if (regionContainsPoint(chipBounds, center->chip)) {
+            psLogMsg("ppstampMakeStamp", 2, "Found center (%f %f) on chip: %s\n", 
+                center->chip->x, center->chip->y, chipName);
+            onChip = true;
+        }
+    } else {
+        // center specified in pixels. 
+        // If the user specified a name of a chip name wait until we get to that one.
+        // If no chip name was specified, select this one (the first one that had data)
+        if ((options->chipName == NULL) || !strcmp(chipName, options->chipName)) {
+            psLogMsg("ppstampMakeStamp", 2, "Center on chip: %s\n", chipName);
+            center->chip->x = options->roip.centerX;
+            center->chip->y = options->roip.centerY;
+            center->chip->xErr = 0;
+            center->chip->yErr = 0;
+            onChip = true;
+        }
+    }
+
+    if (onChip) {
+        if (options->roip.celestialRange) {
+            findBoundingBox(options, input->fpa, chip, center);
+        } else {
+            int width  = options->roip.dX;
+            int height = options->roip.dY;
+
+            // calculate the ROI in chip coordinates
+            options->roi.x0 = center->chip->x - width / 2;
+            options->roi.x1 = options->roi.x0 + width;
+            options->roi.y0 = center->chip->y - height / 2;
+            options->roi.y1 = options->roi.y0 + height;
+        }
+
+
+        if (regionContainsRegion(chipBounds, &options->roi)) {
+            // returnval = findCell(view, options, input, center, ppCell);
+            psLogMsg("ppstampMakeStamp", 2, "ROI contained on: %s\n", chipName);
+            returnval = PPSTAMP_ON;
+        } else {
+            fprintf(stderr, "Partial Overlap chip %s\n", chipName);
+            fprintf(stderr, "ROI:         %s\n", psRegionToString(options->roi));
+            fprintf(stderr, "Chip Extent: %s\n", psRegionToString(*chipBounds));
+
+            returnval = PPSTAMP_PARTIALLY_ON;
+
+        }
+    }
+    psFree(chipBounds);
+
+    return returnval;
+}
+
+bool ppstampMakeStamp (pmConfig *config, ppstampOptions *options)
+{
+    bool        status = false;
+    bool        returnval = false;;
+    bool        foundOverlap = false;
+    pmAstromObj *center = pmAstromObjAlloc();
+
+    pmFPAfile *input = psMetadataLookupPtr(&status, config->files, "PPSTAMP.INPUT");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "Can't find input data!\n");
+        psFree(center);
+        return false;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc(0);// View for level of interest
+
+    // files associated with the science image
+    if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to load input.");
+        psFree(center);
+        psFree (view);
+        return false;
+    }
+
+    // Loop over the chips and find the one that contains the center
+    pmChip *chip;
+    while ((chip = pmFPAviewNextChip(view, input->fpa, 1)) != NULL) {
+	bool allDone = false;
+
+        if (!chip->process || !chip->file_exists) {
+            continue;
+        }
+
+        if (!pmFPAfileIOChecks(config, view, PM_FPA_BEFORE)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to load chip");
+            status = false;
+            break;
+        }
+
+        ppstampOverlap overlap = findROI(options, view, input, chip, center);
+
+        switch (overlap) {
+        case PPSTAMP_OFF:
+            // keep looking
+            break;
+        case PPSTAMP_ON:
+            returnval = makeStamp(config, options, input, chip, view);
+            allDone = true;
+            foundOverlap = true;
+            break;
+	case PPSTAMP_PARTIALLY_ON:
+            psError(PS_ERR_UNKNOWN, false, "Region of interest must be confined to a single chip\n");
+            // XXX: perhaps print a helpful message about what coordinates would be valid
+            returnval = false;
+            allDone = true;
+            foundOverlap = true;
+            break;
+        case PSTAMP_ERROR:
+            returnval = false;
+            allDone = true;
+            break;
+        default:
+            psError(PS_ERR_PROGRAMMING, false, "findROI returned unexpected value %d\n", overlap);
+            break;
+        }
+
+	pmFPAfileIOChecks(config, view, PM_FPA_AFTER);
+
+        if (allDone) {
+            view->chip = -1;
+            break;
+        }
+    } 
+    pmFPAfileIOChecks(config, view, PM_FPA_AFTER);
+
+    psFree(center);
+    psFree(view);
+
+    if (!foundOverlap) {
+        fprintf(stderr, "ROI not found in input\n");
+    }
+
+    return returnval;
+}
+
+
Index: /trunk/pstamp/src/ppstampMosaic.c
===================================================================
--- /trunk/pstamp/src/ppstampMosaic.c	(revision 16132)
+++ /trunk/pstamp/src/ppstampMosaic.c	(revision 16132)
@@ -0,0 +1,41 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "ppstamp.h"
+
+// Mosaic the cells of a given chip
+
+
+pmFPAfile *ppstampBuildMosaic(pmConfig *config, pmFPAfile *input, pmFPAview *view)
+{
+    bool    status;
+
+    pmFPAfile *mosaic =  psMetadataLookupPtr(&status, config->files, "PPSTAMP.CHIP");
+    if (!status)  {
+        psErrorStackPrint(stderr, "can't find mosaic i/o file\n");
+        exit(EXIT_FAILURE);
+    }
+
+    pmFPAview *mosaicView = pmFPAviewAlloc(0);
+
+    mosaicView->chip = 0;
+    pmChip  *mChip  = pmFPAviewThisChip(mosaicView, mosaic->fpa);
+    pmChip  *inChip = pmFPAviewThisChip(view, input->fpa);
+    if (!mChip->hdu && !mChip->parent->hdu) {
+        const char *name = psMetadataLookupStr(&status, input->fpa->concepts, "FPA.NAME"); // Name of FPA
+        pmFPAAddSourceFromView(mosaic->fpa, name, mosaicView, mosaic->format);
+    }
+    psFree(mosaicView);
+
+    psMaskType blankMask = pmConfigMask("BLANK", config);
+
+    status = pmChipMosaic(mChip, inChip, true, blankMask);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "chip mosaic failed\n");
+        return NULL;
+    }
+
+    return mosaic;
+}
+
Index: /trunk/pstamp/src/ppstampOptions.c
===================================================================
--- /trunk/pstamp/src/ppstampOptions.c	(revision 16132)
+++ /trunk/pstamp/src/ppstampOptions.c	(revision 16132)
@@ -0,0 +1,32 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+#include "ppstampOptions.h"
+
+
+static void pstampOptionsFree(ppstampOptions *options)
+{
+    psFree(options->chipName);
+}
+
+ppstampOptions *ppstampOptionsAlloc(void)
+{
+    ppstampOptions *options = psAlloc(sizeof(ppstampOptions));
+    psMemSetDeallocator(options, (psFreeFunc)pstampOptionsFree);
+
+    options->roip.celestialCenter = false;
+    options->roip.centerX         = 0;
+    options->roip.centerY         = 0;
+    options->roip.centerRA        = 0;
+    options->roip.centerDEC       = 0;
+    options->roip.celestialRange  = false;
+    options->roip.dX   = 0;
+    options->roip.dY   = 0;
+    options->roip.dRA  = 0;
+    options->roip.dDEC = 0;
+    options->chipName  = NULL;
+
+    return options;
+}
Index: /trunk/pstamp/src/ppstampOptions.h
===================================================================
--- /trunk/pstamp/src/ppstampOptions.h	(revision 16132)
+++ /trunk/pstamp/src/ppstampOptions.h	(revision 16132)
@@ -0,0 +1,23 @@
+#ifndef PPSTAMP_OPTIONS_H
+#define PPSTAMP_OPTIONS_H
+
+#include "pstampROI.h"
+
+// Options for ppstamp processing
+typedef struct {
+    // input arguments
+    pstampROI   roip;
+    psString    chipName;
+    psString    cellName;
+    //
+    // Calculated Values
+    //
+    psRegion    roi;            // roi in chip coordinates
+
+} ppstampOptions;
+
+ppstampOptions *ppstampOptionsAlloc(void);
+
+#endif
+
+
Index: /trunk/pstamp/src/ppstampParseCamera.c
===================================================================
--- /trunk/pstamp/src/ppstampParseCamera.c	(revision 16132)
+++ /trunk/pstamp/src/ppstampParseCamera.c	(revision 16132)
@@ -0,0 +1,63 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+# include "ppstamp.h"
+
+bool ppstampMegacamWorkaround = false;
+
+// Set up the ppstamp output Image file
+bool setupOutput(pmConfig *config, pmFPAfile *input)
+{
+    pmFPAfile *output = pmFPAfileDefineSkycell(config, NULL, "PPSTAMP.OUTPUT");
+    output->save = true;
+    return true;
+}
+
+// This function seems mis-named. What are we doing with regards to the Camera?
+// Well we are building the output image based on the camera. Is there
+// something else that I'm missing?
+
+bool ppstampParseCamera(pmConfig *config)
+{
+    bool status = false;
+
+    // the input image defines the camera, and all recipes and options the follow
+    pmFPAfile *input = pmFPAfileDefineFromArgs (&status, config, "PPSTAMP.INPUT", "INPUT");
+    if (!status || !input) {
+        psError(PS_ERR_IO, false, "Failed to build FPA from PPSTAMP.INPUT");
+        return false;
+    }
+    if (input->type != PM_FPA_FILE_IMAGE) {
+        psError(PS_ERR_IO, true, "PPSTAMP.INPUT is not of type IMAGE");
+        return false;
+    }
+
+#ifdef notyet
+    // add recipe options supplied on command line
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, RECIPE_NAME);
+#endif
+
+    // Set up the output target
+    if (!setupOutput(config, input)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to setup output.");
+        return false;
+    }
+
+    pmFPAfile *chipImage = pmFPAfileDefineChipMosaic(config, input->fpa, "PPSTAMP.CHIP");
+    if (!chipImage) {
+        psError(PS_ERR_IO, false, _("Unable to generate new file from PPSTAMP.CHIP"));
+        return NULL;
+    }
+    if (chipImage->type != PM_FPA_FILE_IMAGE) {
+        psError(PS_ERR_IO, true, "PPSTAMP.CHIP is not of type IMAGE");
+        return NULL;
+    }
+
+    if (!strcmp(config->cameraName, "MEGACAM")) {
+        // workaround bug 986 and related
+        ppstampMegacamWorkaround = true;
+    }
+ 
+    return true;
+}
Index: /trunk/pstamp/src/ppstampRegion.c
===================================================================
--- /trunk/pstamp/src/ppstampRegion.c	(revision 16132)
+++ /trunk/pstamp/src/ppstampRegion.c	(revision 16132)
@@ -0,0 +1,121 @@
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+
+#include "ppstamp.h"
+
+// Functions to calculate the image space boundaries of a given Chip or Cell
+// These are calculated in coordinates that match the wcs transformation
+// (adapted from pmFPAExtent.c)
+
+// return cell pixels bounding the readout
+static psRegion *ppstampReadoutRegion(const pmReadout *readout)
+{
+    PS_ASSERT_PTR_NON_NULL(readout, NULL);
+
+    psImage *image = readout->image;    // Image from which to get dimensions
+    if (!image) {
+        return NULL;
+    }
+
+    // This is the difference between this function and pmReadoutExtent. 
+    // pmReadoutExtent ignores the col0, row0 of the readout
+   
+    int col0 = 0;   // should be  image->col0 - readout->col0
+    int row0 = 0;   //            image->row0 - readout->row0
+   
+    return psRegionAlloc(col0, col0 + image->numCols,
+                         row0, row0 + image->numRows);
+}
+
+// return chip pixels bounding the cell (all readouts)
+psRegion *ppstampCellRegion(const pmCell *cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+
+    psArray *readouts = cell->readouts; // Array of component readouts
+    psRegion *cellExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of cell
+    for (long i = 0; i < readouts->n; i++) {
+        pmReadout *readout = readouts->data[i]; // Readout of interest
+        psRegion *roExtent = ppstampReadoutRegion(readout); // Extent of readout
+        cellExtent->x0 = PS_MIN(cellExtent->x0, roExtent->x0);
+        cellExtent->x1 = PS_MAX(cellExtent->x1, roExtent->x1);
+        cellExtent->y0 = PS_MIN(cellExtent->y0, roExtent->y0);
+        cellExtent->y1 = PS_MAX(cellExtent->y1, roExtent->y1);
+        psFree(roExtent);
+    }
+
+    bool mdok;                          // Status of MD lookup
+    int cellX0 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.X0"); // Cell x offset
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find CELL.X0.\n");
+        psFree(cellExtent);
+        return NULL;
+    }
+
+    int cellY0 = psMetadataLookupS32(&mdok, cell->concepts, "CELL.Y0"); // Cell y offset
+    if (!mdok) {
+        psError(PS_ERR_UNKNOWN, true, "Unable to find CELL.Y0.\n");
+        psFree(cellExtent);
+        return NULL;
+    }
+    int xParity = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY");
+    int yParity = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY");
+
+    if (xParity >= 0) {
+        cellExtent->x0 += cellX0;
+        cellExtent->x1 += cellX0;
+    } else {
+        float x0 = cellX0 - cellExtent->x1;
+        float x1 = cellX0 - cellExtent->x0;
+        cellExtent->x0 = x0;
+        cellExtent->x1 = x1;
+    }
+
+    if (yParity >= 0) {
+        cellExtent->y0 += cellY0;
+        cellExtent->y1 += cellY0;
+    } else {
+        float y0 = cellY0 - cellExtent->y1;
+        float y1 = cellY0 - cellExtent->y0;
+        cellExtent->y0 = y0;
+        cellExtent->y1 = y1;
+    }
+
+    return cellExtent;
+}
+
+// return chip pixels included in all cells
+psRegion *ppstampChipRegion(const pmChip *chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    if (ppstampMegacamWorkaround) {
+        // There is an inconsistency in the megacam parameters. 
+        // The offset to the cells is effectively contained in two
+        // places.
+        // The value of CELL.X0 for the 2 cells is 0 and 1024 respectivly
+        // while the two values for readout->image.col0 = 32 and 1056
+        // This fact makes it impossible to calculate the Chip bounds 
+        // in a way consistent with say gpc1
+        // I'm deferring this problem for now.
+        // Since all chips on megacam have the same bounds, I just hard code
+        // the answer. See bug 986
+        return psRegionAlloc(32, 2080, 0, 4612);
+    }
+
+    psArray *cells = chip->cells;       // Array of component cells
+    psRegion *chipExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of chip
+    for (long i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // Cell of interest
+        psRegion *cellExtent = ppstampCellRegion(cell); // Extent of cell
+        chipExtent->x0 = PS_MIN(chipExtent->x0, cellExtent->x0);
+        chipExtent->x1 = PS_MAX(chipExtent->x1, cellExtent->x1);
+        chipExtent->y0 = PS_MIN(chipExtent->y0, cellExtent->y0);
+        chipExtent->y1 = PS_MAX(chipExtent->y1, cellExtent->y1);
+        psFree(cellExtent);
+    }
+
+    return chipExtent;
+}
Index: /trunk/pstamp/src/ppstampVersion.c
===================================================================
--- /trunk/pstamp/src/ppstampVersion.c	(revision 16132)
+++ /trunk/pstamp/src/ppstampVersion.c	(revision 16132)
@@ -0,0 +1,56 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "ppstamp.h"
+
+static const char *cvsTag = "$Name: not supported by cvs2svn $";// CVS tag name
+
+psString ppstampVersion(void)
+{
+    psString version = NULL;            // Version, to return
+    psStringAppend(&version, "%s-%s",PACKAGE_NAME,PACKAGE_VERSION);
+    return version;
+}
+
+psString ppstampVersionLong(void)
+{
+    psString version = ppstampVersion(); // Version, to return
+    psString tag = psStringStripCVS(cvsTag, "Name"); // CVS tag
+    psStringAppend(&version, " (cvs tag %s) %s, %s", tag, __DATE__, __TIME__);
+    psFree(tag);
+    return version;
+}
+
+
+void ppstampVersionMetadata(psMetadata *metadata, ppstampOptions *options)
+{
+    PS_ASSERT_METADATA_NON_NULL(metadata,);
+
+    psString pslib = psLibVersionLong();// psLib version
+    psString psmodules = psModulesVersionLong(); // psModules version
+    psString ppstamp = ppstampVersionLong(); // ppstamp version
+
+    psTime *time = psTimeGetNow(PS_TIME_TAI); // The time now
+    psString timeString = psTimeToISO(time); // The time in an ISO string
+    psFree(time);
+    psString head = NULL;               // Head string
+    psStringAppend(&head, "ppstamp processing at %s. Component information:", timeString);
+    psFree(timeString);
+
+    psMetadataAddStr(metadata, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, head, "");
+    psMetadataAddStr(metadata, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, pslib, "");
+    psMetadataAddStr(metadata, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, psmodules, "");
+    psMetadataAddStr(metadata, PS_LIST_TAIL, "HISTORY", PS_META_DUPLICATE_OK, ppstamp, "");
+    psString roi = NULL;
+    psStringAppend(&roi, "ppstamp: region of interest: %s", psRegionToString(options->roi));
+    psMetadataAddStr(metadata, PS_LIST_TAIL, "HISTORY",  PS_META_DUPLICATE_OK, roi, "");
+
+    psFree(roi);
+    psFree(head);
+    psFree(pslib);
+    psFree(psmodules);
+    psFree(ppstamp);
+
+    return;
+}
Index: /trunk/pstamp/src/pstamp.h
===================================================================
--- /trunk/pstamp/src/pstamp.h	(revision 16132)
+++ /trunk/pstamp/src/pstamp.h	(revision 16132)
@@ -0,0 +1,27 @@
+#ifndef PSTAMP_H
+#define PSTAMP_H
+
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>  // for strcasecmp
+#include <unistd.h>   // for unlink
+#include "pslib.h"
+#include "psmodules.h"
+// #include "psphot.h"
+// #include "psastro.h"
+// #include "ppStats.h"
+#include "pstampErrorCodes.h"
+
+typedef enum {
+    PSTAMP_UNKNOWN = -1,
+    PSTAMP_RAW,
+    PSTAMP_CHIP,
+    PSTAMP_WARP,
+    PSTAMP_DIFF,
+    PSTAMP_STACK
+} pstampImageType;
+
+#define STAMP_REQUEST_EXTNAME "PS1_PS_REQUEST"
+#define STAMP_REQUEST_VERSION "1.0"
+
+#endif
Index: /trunk/pstamp/src/pstampErrorCodes.c.in
===================================================================
--- /trunk/pstamp/src/pstampErrorCodes.c.in	(revision 16132)
+++ /trunk/pstamp/src/pstampErrorCodes.c.in	(revision 16132)
@@ -0,0 +1,25 @@
+/*
+ * The line
+    { PSTAMP_ERR_$X{ErrorCode}, "$X{ErrorDescription}"},
+ * (without the Xs)
+ * will be replaced by values from errorCodes.dat
+ */
+#include "pslib.h"
+#include "pstampErrorCodes.h"
+
+void pstampErrorRegister(void)
+{
+    static psErrorDescription errors[] = {
+       { PSTAMP_ERR_BASE, "First value we use; lower values belong to psLib" },
+       { PSTAMP_ERR_${ErrorCode}, "${ErrorDescription}"},
+    };
+    static int nerror = PSTAMP_ERR_NERROR - PSTAMP_ERR_BASE; // number of values in enum
+
+    for (int i = 0; i < nerror; i++) {
+       psErrorDescription *tmp = psAlloc(sizeof(psErrorDescription));
+       *tmp = errors[i];
+       psErrorRegister(tmp, 1);
+       psFree(tmp);			/* it's on the internal list */
+    }
+    nerror = 0;			                // don't register more than once
+}
Index: /trunk/pstamp/src/pstampErrorCodes.dat
===================================================================
--- /trunk/pstamp/src/pstampErrorCodes.dat	(revision 16132)
+++ /trunk/pstamp/src/pstampErrorCodes.dat	(revision 16132)
@@ -0,0 +1,10 @@
+#
+# This file is used to generate pstampErrorCodes.h
+#
+BASE = 800		First value we use; lower values belong to psLib
+UNKNOWN			Unknown PM error code
+NOT_IMPLEMENTED		Desired feature is not yet implemented
+ARGUMENTS		Incorrect arguments
+CONFIG			Problem in configure files
+IO			Problem in FITS I/O
+DATA                    Problem in data values
Index: /trunk/pstamp/src/pstampErrorCodes.h.in
===================================================================
--- /trunk/pstamp/src/pstampErrorCodes.h.in	(revision 16132)
+++ /trunk/pstamp/src/pstampErrorCodes.h.in	(revision 16132)
@@ -0,0 +1,18 @@
+#if !defined(PSTAMP_ERROR_CODES_H)
+#define PSTAMP_ERROR_CODES_H
+/*
+ * The line
+ *  PSTAMP_ERR_$X{ErrorCode},
+ * (without the X)
+ *
+ * will be replaced by values from errorCodes.dat
+ */
+typedef enum {
+    PSTAMP_ERR_BASE = 650,
+    PSTAMP_ERR_${ErrorCode},
+    PSTAMP_ERR_NERROR
+} pstampErrorCode;
+
+void pstampErrorRegister(void);
+
+#endif
Index: /trunk/pstamp/src/pstampGetROI.c
===================================================================
--- /trunk/pstamp/src/pstampGetROI.c	(revision 16132)
+++ /trunk/pstamp/src/pstampGetROI.c	(revision 16132)
@@ -0,0 +1,185 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "pstamp.h"
+#include "pstampROI.h"
+#include "ohana.h"
+
+static bool get2Angles(int argnum, int *pArgc, char **argv, bool bothDegrees, bool makePositive, double *p1, double *p2);
+static bool get2Ints(int argnum, int *pArgc, char **argv, bool makePositive, int *p1, int *p2);
+
+
+bool pstampGetROI(pstampROI *roip, int *pArgc, char **argv, bool *gotCenter, bool *gotRange)
+{
+    int argnum;                         // Argument number of interest
+
+    if ((argnum = psArgumentGet(*pArgc, argv, "-pixcenter"))) {
+        *gotCenter = true;
+        roip->celestialCenter = false;
+        psArgumentRemove(argnum, pArgc, argv);
+
+        roip->center[0] = argv[argnum];
+        roip->center[1] = argv[argnum+1];
+
+        if (!get2Ints(argnum, pArgc, argv, false, &roip->centerX, &roip->centerY)) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "invalid pixcenter specification: %s %s\n",
+                argv[argnum], argv[argnum+1]);
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        psArgumentRemove(argnum, pArgc, argv);
+    }
+
+    if ((argnum = psArgumentGet(*pArgc, argv, "-skycenter"))) {
+        if (*gotCenter) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "can't specify both -pixcenter and -skycenter\n");;
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+
+        roip->center[0] = argv[argnum];
+        roip->center[1] = argv[argnum+1];
+
+        double raDeg, decDeg;
+        if (!get2Angles(argnum, pArgc, argv, false, false, &raDeg, &decDeg)) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "invalid skycenter specification: %s %s\n",
+                argv[argnum], argv[argnum+1]);
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        psArgumentRemove(argnum, pArgc, argv);
+        roip->centerRA  = DEG_TO_RAD(raDeg);
+        roip->centerDEC = DEG_TO_RAD(decDeg);
+        *gotCenter = true;
+        roip->celestialCenter = true;
+    }
+
+    if ((argnum = psArgumentGet(*pArgc, argv, "-pixrange"))) {
+        *gotRange = true;
+        roip->celestialRange = false;
+        psArgumentRemove(argnum, pArgc, argv);
+        roip->range[0] = argv[argnum];
+        roip->range[1] = argv[argnum+1];
+
+        if (!get2Ints(argnum, pArgc, argv, true, &roip->dX, &roip->dY)) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "invalid pixrange specification: %s %s\n",
+                argv[argnum], argv[argnum+1]);
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        psArgumentRemove(argnum, pArgc, argv);
+    }
+
+    if ((argnum = psArgumentGet(*pArgc, argv, "-arcrange"))) {
+        if (*gotRange) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "specify only one of -pixrange or -arcrange\n");;
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        *gotRange = true;
+        roip->celestialRange = true;
+
+        roip->range[0] = argv[argnum];
+        roip->range[1] = argv[argnum+1];
+
+        // arcrange values are seconds of arc
+        roip->dRA  = SEC_TO_RAD(fabs(atof(argv[argnum])));
+        roip->dDEC = SEC_TO_RAD(fabs(atof(argv[argnum+1])));
+
+        psArgumentRemove(argnum, pArgc, argv);
+        psArgumentRemove(argnum, pArgc, argv);
+    }
+    // I'm leaving in the -celrange option (HH:MM:SS DD:MM:SS), but not publicizing it
+    if ((argnum = psArgumentGet(*pArgc, argv, "-celrange"))) {
+        if (*gotRange) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "specify one of -pixrange, -arcrange, or -celrange\n");;
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        *gotRange = true;
+        roip->celestialRange = true;
+
+        roip->range[0] = argv[argnum];
+        roip->range[1] = argv[argnum+1];
+
+        double deg1, deg2;
+        if (!get2Angles(argnum, pArgc, argv, false, true, &deg1, &deg2)) {
+            psError(PSTAMP_ERR_ARGUMENTS, true, "invalid celrange specification: %s %s\n",
+                argv[argnum], argv[argnum+1]);
+            return false;
+        }
+        psArgumentRemove(argnum, pArgc, argv);
+        psArgumentRemove(argnum, pArgc, argv);
+        roip->dRA = DEG_TO_RAD(deg1);
+        roip->dDEC = DEG_TO_RAD(deg2);
+    }
+
+    if (!*gotCenter || !*gotRange) {
+        return false;
+    }
+
+    return true;
+}
+
+static bool validNumber(char *string)
+{
+    char *p = string;
+
+    if ((*p == '+') || (*p == '-')) {
+        p++;
+    }
+    return isdigit(*p);
+}
+
+static bool get2Ints(int argnum, int *pArgc, char **argv, bool makePositive, int *p1, int *p2)
+{
+    if (*pArgc < 2) {
+        return false;
+    }
+
+    if (!validNumber(argv[argnum])) {
+        return false;
+    }
+    *p1 = atoi(argv[argnum]);
+
+    if (!validNumber(argv[argnum+1])) {
+        return false;
+    }
+    *p2 = atoi(argv[argnum+1]);
+
+    if (makePositive) {
+        *p1 = abs(*p1);
+        *p2 = abs(*p2);
+    }
+
+    return true;
+}
+
+static bool get2Angles(int argnum, int *pArgc, char **argv, bool bothInDegrees, bool makePositive,
+    double *p1, double *p2)
+{
+    bool rval;
+
+    if (*pArgc < 2) {
+        return false;
+    }
+
+    if (bothInDegrees) {
+        // both values are angles of arc DD:MM:SS or decimal degrees
+        rval   = ohana_dms_to_ddd(p1, argv[argnum]);
+        if (rval) {
+            rval  = ohana_dms_to_ddd(p1, argv[argnum+1]);
+        }
+    } else {
+        // first value may be in HH:MM:SS
+        rval = ohana_str_to_radec(p1, p2, argv[argnum], argv[argnum+1]);
+    }
+
+    if (rval && makePositive) {
+        *p1 = abs(*p1);
+        *p2 = abs(*p2);
+    }
+
+    return rval;
+}
Index: /trunk/pstamp/src/pstampROI.h
===================================================================
--- /trunk/pstamp/src/pstampROI.h	(revision 16132)
+++ /trunk/pstamp/src/pstampROI.h	(revision 16132)
@@ -0,0 +1,23 @@
+#ifndef PSTAMP_ROI_H
+#define PSTAMP_ROI_H
+
+// Struct defining the Postage Stamp Region of Interest
+
+typedef struct {
+    double centerRA;
+    double centerDEC;
+    int    centerX;
+    int    centerY;
+    double dRA;
+    double dDEC;
+    int    dX;
+    int    dY;
+    bool   celestialCenter;       // true if center is in RA/dec
+    bool   celestialRange;        // true if range is in RA/dec
+    char   *center[2];            // copy of command line strings for center (from argv don't free)
+    char   *range[2];             // copy of command line strings for range (from argv don't free)
+} pstampROI;
+
+bool pstampGetROI(pstampROI *roip, int *pArgc, char **argv, bool *gotCenter, bool *gotRange);
+
+#endif
Index: /trunk/pstamp/src/pstampparse.c
===================================================================
--- /trunk/pstamp/src/pstampparse.c	(revision 16132)
+++ /trunk/pstamp/src/pstampparse.c	(revision 16132)
@@ -0,0 +1,649 @@
+// pstampparse  - read a fits table describing a pstamp request and prepares it for processing
+
+#include <pslib.h>
+#include <psmodules.h>
+#include <string.h>
+#include "pstamp.h"
+#include "pstampROI.h"
+
+typedef struct {
+    pmConfig    *config;
+    psString    fileName;
+    psMetadata  *md;
+    psString    commandLine;
+    psString    roiString;
+} pspOptions;
+
+static void usage()
+{
+    // XXX: fill this out
+    fprintf(stderr, "argument error\n");
+    exit(1);
+}
+
+
+static pspOptions *parseArguments(int argc, char *argv[])
+{
+    pspOptions *options = psAlloc(sizeof(pspOptions));
+    memset(options, 0, sizeof(*options));
+    // int         argnum;
+
+    // options to add
+    //      mode:   -queuejobs -execute -preview
+
+    options->config = pmConfigRead(&argc, argv, NULL);
+    if (!options->config) {
+        psError(PS_ERR_UNKNOWN, false, "cannot find site file");
+        psFree(options);
+        return NULL;
+    }
+
+    if (argc == 2) {
+        options->fileName = psStringCopy(argv[1]);
+    } else if (argc == 1) {
+        fprintf(stderr, "input file name is required\n");
+        usage();
+    } else {
+        fprintf(stderr, "unknown arguments supplied:");
+        fprintf(stderr, " %s", argv[1]);
+        for (int i=2; i<argc; i++) {
+            fprintf(stderr, ", %s", argv[i]);
+        }
+        fprintf(stderr, "\n");
+        usage();
+    }
+
+    return options;
+}
+
+static bool readTable(pspOptions *options)
+{
+    psFits *fitsFile = psFitsOpen(options->fileName, "r");
+    if (fitsFile == NULL) {
+        psError(PS_ERR_IO, false, "failed to open %s for output", options->fileName);
+        return false;
+    }
+
+    psArray *array = psFitsReadTable(fitsFile);
+
+    psFitsClose(fitsFile);
+
+    if (array == NULL) {
+        psError(PS_ERR_IO, false, "psFitsReadTable failed for %s", options->fileName);
+        return false;
+    } 
+
+    options->md = array->data[0];
+
+    return true;
+}
+
+static bool setupDB(pspOptions *options)
+{
+    // XXX: need to select the database name based on the project name specified in the input
+
+    options->config->database = pmConfigDB(options->config);
+
+    if (!options->config->database) {
+        psError(PS_ERR_UNKNOWN, false, "Can't configure database");
+        return false;
+    }
+
+    return true;
+}
+
+// TODO: move this to a shared file
+static pstampImageType getImageType(pspOptions *options)
+{
+    psString type = psMetadataLookupStr(NULL, options->md, "IMG_TYPE");
+    pstampImageType itype;
+
+    if (type == NULL) {
+        psError(PS_ERR_UNKNOWN, true, "NULL image type");
+        itype = PSTAMP_UNKNOWN;
+        return itype;
+    }
+
+    if (strcmp(type, "raw") == 0) {
+        itype = PSTAMP_RAW;
+    } else if (strcmp(type, "chip") == 0) {
+        itype = PSTAMP_CHIP;
+    } else if (strcmp(type, "warp") == 0) {
+        itype = PSTAMP_WARP;
+    } else if (strcmp(type, "diff") == 0) {
+        itype = PSTAMP_DIFF;
+    } else if (strcmp(type, "stack") == 0) {
+        itype = PSTAMP_STACK;
+    } else {
+        psError(PS_ERR_UNKNOWN, true, "unknown image type %s", type);
+        itype = PSTAMP_UNKNOWN;
+    }
+
+    return itype;
+}
+
+// return a string with the contents of a unsigned 64 bit value
+static psString idToStr(psU64 id)
+{
+    psString idstr = psStringAlloc(64);
+    sprintf(idstr, "%lu", id);
+
+    return idstr;
+}
+
+
+// runQuery
+//
+// run the given database query and return an array of strings containing the values of target
+// the type for the values returned by the query is either a string or psU64.
+
+static psArray *runQuery(pspOptions *options, psString query, psString target,
+                psString key1, psString key2, bool targetIsString, psArray *output)
+{
+    // query string must be in the form
+    //  "SELECT %s from sometable WHERE somecolumn = %s and someothercolumn = %s
+    //        target                                key1                       key2
+    //
+    //  or if key2 is null
+    //
+    //  "SELECT %s from sometable WHERE somecolumn = %s"
+    //         target                               key1
+    //
+    // 
+    // we return an array containing strins for the values
+
+    if (!p_psDBRunQuery(options->config->database, query, target, key1, key2)) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return NULL;
+    }
+
+    psArray *queryOutput = p_psDBFetchResult(options->config->database);
+    if (!queryOutput) {
+        psError(PS_ERR_UNKNOWN, false, "database error");
+        return NULL;
+    }
+
+    psU64 len = psArrayLength(queryOutput);
+    if (!len) {
+        psTrace("pstampparse", PS_LOG_INFO, "no rows found");
+        psFree(queryOutput);
+        return NULL;
+    }
+
+    if (output == NULL) {
+        output = psArrayAlloc(0);
+    }
+
+    for (int i=0; i < len; i++) {
+        psMetadata *row = queryOutput->data[i];
+        if (targetIsString) {
+            psString val = psMetadataLookupStr(NULL, row, target);
+            if (val != 0) {
+                output = psArrayAdd(output, len -i, val);
+            }
+        } else {
+            psU64 val = psMetadataLookupU64(NULL, row, target);
+            if (val != 0) {
+                output = psArrayAdd(output, len -i, idToStr(val));
+            }
+        }
+    }
+
+    psFree(queryOutput);
+
+    return output;
+}
+
+static psArray *findURIs(pspOptions *options, psString table, psString id_type, psString id, psString class_id)
+{
+    psString query = NULL;
+
+    psStringAppend(&query, "SELECT %%s from %s WHERE %s = %%s", table, id_type);
+    if (class_id) {
+        psStringAppend(&query, "%s", " and class_id = '%s'" );
+    }
+
+    psArray *results = runQuery(options, query, "uri", id, class_id, true, NULL);
+
+    psFree(query);
+
+    return results;
+}
+
+// given an array of strings return the last entry
+
+static psString lastID(psArray *array)
+{
+    if (array == NULL) {
+        return 0;
+    }
+    psU64 len = psArrayLength(array);
+    
+    psString id = NULL;
+    if (len > 0) {
+        id = (psString) array->data[len-1];
+        psMemIncrRefCounter(id);
+    }
+
+    // XXX: Hmm if we're doing last(runQuery(....)) we need to free the
+    // result array, however freeing this in here seems like a dangerous API
+    // but hey it's a static function
+
+    psFree(array);
+
+    return id;
+}
+
+
+
+#define chipIDForExp(_opt, _id) \
+            lastID(runQuery((_opt), "SELECT %s from chipRun  WHERE exp_id = %s", "chip_id", \
+                            (_id), NULL, false, NULL))
+
+#define camIDForChip(_opt, _id) \
+    lastID(runQuery((_opt), "SELECT %s from camRun  WHERE chip_id = %s", "cam_id", \
+                        (_id), NULL, false, NULL))
+
+#define warpIDForCam(_opt, _id) \
+        lastID(runQuery((_opt), "SELECT %s from warpInputExp  WHERE cam_id = %s", "warp_id", \
+                            (_id), NULL, false, NULL))
+
+
+
+static psArray *findChipURIs(pspOptions *options, psString exp_id, psString class_id)
+{
+    psString chip_id = chipIDForExp(options, exp_id);
+
+    if (chip_id == NULL)
+        return NULL;
+
+    psArray *chip_files = runQuery(options, 
+                        "SELECT %s from chipProcessedImfile WHERE chip_id = %s AND class_id = '%s'",
+                        "uri", chip_id, class_id, true, NULL);
+    psFree(chip_id);
+
+    return chip_files;
+}
+
+static psArray *findWarpURIs(pspOptions *options, psString exp_id)
+{
+    psString chip_id = chipIDForExp(options, exp_id);
+
+    if (chip_id == NULL)
+        return NULL;
+
+    psString cam_id = camIDForChip(options, chip_id);
+    if (cam_id == NULL)
+        return NULL;
+
+    psString warp_id = warpIDForCam(options, cam_id);
+
+    psArray *warp_files = runQuery(options, 
+                        "SELECT %s from warpSkyfile WHERE warp_id = %s"
+                        " AND uri IS NOT NULL and (fault = 0 or fault is NULL)",
+                           "uri",   warp_id, NULL, true, NULL);
+
+    psFree(warp_id);
+    psFree(cam_id);
+    psFree(chip_id);
+
+    return warp_files;
+}
+
+static psArray *findStackURIs(pspOptions *options, psString exp_id)
+{
+    psString chip_id = chipIDForExp(options, exp_id);
+
+    if (chip_id == NULL)
+        return NULL;
+
+    psString cam_id = camIDForChip(options, chip_id);
+    if (cam_id == NULL)
+        return NULL;
+
+    psString warp_id = warpIDForCam(options, cam_id);
+
+    psArray *stackIDs = runQuery(options, "SELECT %%S FROM stackInputSkyfile WHERE warp_id = %s",
+                            "stack_id", warp_id, NULL, false, NULL);
+
+    psFree(warp_id);
+    psFree(cam_id);
+    psFree(chip_id);
+
+    if (stackIDs == NULL) {
+        return NULL;
+    }
+
+    psArray *uris = psArrayAlloc(0);
+
+    psU64 len = psArrayLength(stackIDs);
+    
+    // loop over the stack's for this exposure and add the images
+    for (int i = 0; i < len ; i++ ) {
+        psString stack_id = (psString) stackIDs->data[i];
+        uris = runQuery(options, "SELECT %s FROM stackSumSkyfile where stack_id = %s",
+                    "uri", stack_id, NULL, true, uris);
+    }
+    psFree(stackIDs);
+
+    return uris;
+}
+
+static psArray *findDiffURIs(pspOptions *options, psString exp_id)
+{
+    psString chip_id = chipIDForExp(options, exp_id);
+
+    if (chip_id == NULL)
+        return NULL;
+
+    psString cam_id = camIDForChip(options, chip_id);
+    if (cam_id == NULL)
+        return NULL;
+
+    psString warp_id = warpIDForCam(options, cam_id);
+
+    psArray *diffIDs = runQuery(options,
+        "SELECT %s FROM diffInputSkyfile WHERE warp_id = %s",
+            "diff_id", warp_id, NULL, false, NULL);
+
+    psFree(warp_id);
+    psFree(cam_id);
+    psFree(chip_id);
+
+    if (diffIDs == NULL) {
+        return NULL;
+    }
+
+    psArray *uris = psArrayAlloc(0);
+
+    psU64 len = psArrayLength(diffIDs);
+    
+    for (int i = 0; i < len ; i++ ) {
+        psString diff_id = (psString) diffIDs->data[i];
+        uris = runQuery(options, "SELECT %s FROM diffSkyfile where diff_id = %s",
+                    "uri", diff_id, NULL, true, uris);
+    }
+
+    psFree(diffIDs);
+
+    return uris;
+}
+
+static psArray * parseByID(pspOptions *options)
+{
+    bool status = false;
+    pstampImageType itype = getImageType(options);
+    psString class_id = psMetadataLookupStr(&status, options->md, "CLASS_ID");
+    psString id = psMetadataLookupStr(NULL, options->md, "ID");
+    psArray *uris = NULL;
+
+    if (id == NULL) {
+        fprintf(stderr, "parseByID: need id\n");
+        return NULL;
+    }
+
+    switch (itype) {
+    case PSTAMP_RAW:
+        uris = findURIs(options, "rawImfile", "exp_id", id, class_id);
+        break;
+    case PSTAMP_CHIP:
+        uris = findURIs(options, "chipProcessedImfile", "chip_id", id, class_id);
+        break;
+    case PSTAMP_WARP:
+        uris = findURIs(options, "warpSkyfile", "warp_id", id, NULL);
+        break;
+    case PSTAMP_DIFF:
+        uris = findURIs(options, "diffSkyfile", "diff_id", id, NULL);
+        break;
+    case PSTAMP_STACK:
+        uris = findURIs(options, "stackSumSkyfile", "stack_id", id, NULL);
+        break;
+    default:
+        // note: getImageType prints a message
+        return NULL;
+    }
+
+#ifdef VERBOSE
+        for (int i=0; i<psArrayLength(uris); i++) {
+            psString uri = (psString) uris->data[i];
+            printf("uri: %s\n", uri ? (psString) uri : "NULL uri");
+        } 
+#endif
+
+
+    return uris;
+}
+
+psString exposureNameToID(pspOptions *options, psString exposure)
+{
+    psArray *results;
+    
+    results = runQuery(options, "SELECT %s from rawExp WHERE exp_name = '%s'",
+                        "exp_id", exposure, NULL, false, NULL);
+
+    return lastID(results);
+}
+
+static psArray * parseByExp(pspOptions *options)
+{
+    // psError(PS_ERR_UNKNOWN, true, "REQ_TYPE: byexp not implemented yet");
+    bool status = false;
+    pstampImageType itype = getImageType(options);
+    psString class_id = psMetadataLookupStr(&status, options->md, "CLASS_ID");
+    psString exposure = psMetadataLookupStr(NULL, options->md, "EXPOSURE");
+    psArray *uris = NULL;
+
+    if (exposure == NULL) {
+        psError(PS_ERR_UNKNOWN, true, "parseByExp: need exposure name");
+        return NULL;
+    }
+
+    psString exp_id = exposureNameToID(options, exposure);
+    if (exp_id == 0) {
+        return NULL;
+    }
+
+    switch (itype) {
+    case PSTAMP_RAW:
+        if (class_id == NULL) {
+            // XXX: we could relax this requirement if roi is specified in sky coords and
+            // process the whole fpa. That might even be a useful feature.
+            psError(PS_ERR_UNKNOWN, true, "stamps from raw images require CLASS_ID");
+            return NULL;
+        }
+        uris = findURIs(options, "rawImfile", "exp_id", exp_id, class_id);
+        break;
+    case PSTAMP_CHIP:
+        if (class_id == NULL) {
+            // XXX: we could relax this requirement if roi is specified in sky coords and
+            // process the whole fpa. That might even be a useful feature.
+            psError(PS_ERR_UNKNOWN, true, "stamps from chip processed images require CLASS_ID");
+            return NULL;
+        }
+        uris = findChipURIs(options, exp_id, class_id);
+        break;
+    case PSTAMP_WARP:
+        uris = findWarpURIs(options, exp_id);
+        break;
+    case PSTAMP_DIFF:
+        uris = findDiffURIs(options, exp_id);
+        break;
+    case PSTAMP_STACK:
+        uris = findStackURIs(options, exp_id);
+        break;
+    default:
+        // note: getImageType prints an error message
+        return NULL;
+    }
+
+    psFree(exp_id);
+
+    return uris;
+}
+
+static psArray * parseByCoord(pspOptions *options)
+{
+    psError(PS_ERR_UNKNOWN, true, "REQ_TYPE: bycoord not implemented yet");
+    return NULL;
+}
+
+static psString parseROI(pspOptions *options)
+{
+    bool status = false;;
+    psString roiString = NULL;
+    psString ra = psMetadataLookupStr(&status, options->md, "RA");
+
+    if (ra != NULL) {
+        psString dec =  psMetadataLookupStr(&status, options->md, "DEC");
+        if (dec == NULL) {
+            psError(PS_ERR_UNKNOWN, true, "bad request file: RA specified without DEC");
+            return NULL;
+        }
+        psStringAppend(&roiString, " -skycenter %s %s", ra, dec);
+    } else {
+        psString x = psMetadataLookupStr(&status, options->md, "CENTER_X");
+        if (x == NULL) {
+            psError(PS_ERR_UNKNOWN, true, "bad request file: center not specified");
+            return NULL;
+        }
+        psString y = psMetadataLookupStr(&status, options->md, "CENTER_X");
+        psStringAppend(&roiString, " -pixcenter %s %s", x, y);
+    }
+
+    psString d_ra = psMetadataLookupStr(&status, options->md, "D_RA");
+    if (d_ra != NULL) {
+        psString d_dec =  psMetadataLookupStr(&status, options->md, "D_DEC");
+        if (d_dec == NULL) {
+            psError(PS_ERR_UNKNOWN, true, "bad request file: D_RA specified without D_DEC");
+            return NULL;
+        }
+        psStringAppend(&roiString, " -arcrange %s %s", d_ra, d_dec);
+    } else {
+        psString w = psMetadataLookupStr(&status, options->md, "WIDTH");
+        if (w == NULL) {
+            psError(PS_ERR_UNKNOWN, true, "bad request file: range not specified");
+            return NULL;
+        }
+        psString h = psMetadataLookupStr(&status, options->md, "HEIGHT");
+        psStringAppend(&roiString, " -pixrange %s %s", w, h);
+    }
+
+    return roiString;
+}
+
+static bool
+parseTable(pspOptions *options)
+{
+    // how are we to select the images ?
+    psString req_type = psMetadataLookupStr(NULL, options->md, "REQ_TYPE");
+
+    if ( !req_type) {
+        psError(PS_ERR_UNKNOWN, true, "request file has no REQ_TYPE");
+        return false;
+    }
+    psString roiString = parseROI(options);
+    if (!roiString) {
+        return false;
+    }
+
+    // XXX switch this to have the various parsing functions return the list of uris which 
+    // we will process here
+    psArray *uris = NULL;
+    if (!strcmp(req_type, "byid")) {
+        // directly by exp_id, chip_id, warp_id etc
+        uris = parseByID(options);
+    } else if (!strcmp(req_type, "byexp")) {
+        // images that resulted from a given exposure
+        uris = parseByExp(options);
+    } else if (!strcmp(req_type, "bycoord")) {
+        // images from a particular region of the sky
+        uris = parseByCoord(options);
+    } else {
+        psError(PS_ERR_UNKNOWN, true, "unknown REQ_TYPE: %s", req_type);
+        return false;
+    }
+
+    if (uris == NULL) {
+        // may need a finer grained status code: did something go wrong with the system
+        // or are there just no images matching the request
+        return false;
+    }
+
+    int len = psArrayLength(uris);
+    fprintf(stderr, "URIS:\n");
+    for (int i=0; i<len; i++) {
+        fprintf(stderr, "  %3d %s\n", i, (psString) uris->data[i]);
+    }
+
+#ifdef notyet
+    // XXX: We need more than a single command line since we might have multiple images
+    psStringAppend(&options->commandLine, "ppstamp ");
+
+    // For now just take the last image returned as the Image of interest
+    psString imageFile = uris->data[psArrayLength(uris) - 1];
+
+    psStringAppend(&options->commandLine, " -file %s", imageFile);
+
+    // Set the output file name to user_tag.fits
+    psString user_tag = psMetadataLookupStr(NULL, options->md, "USER_TAG");
+    if (user_tag == NULL) {
+        psError(PS_ERR_UNKNOWN, true, "no user tag in request file");
+        return false;
+    }
+
+    // XXX: need configurable output directory: get from database, recipe, site file, command line arg?
+
+    psStringAppend(&options->commandLine, " %s.fits", user_tag); 
+#endif
+
+    return true;
+}
+
+int main(int argc, char *argv[])
+{
+    pspOptions *options = parseArguments(argc, argv);
+    bool verbose = true;
+
+    // XXX: create a set of status codes to return so the
+    // scripts can distinguish between different errors. 
+    // For example:
+    //      If we can't connect to the DB it's a configuration problem.
+    //      If a particular image doesn't contain coordinates specified that's a user "error"
+    //      if there are no images of a particular ROI outside of a date range, that tells us
+    //      something
+
+    if (!options) {
+        return 1;
+    }
+
+    if (!readTable(options)) {
+        return 1;
+    }
+
+    if (!setupDB(options)) {
+        return 1;
+    }
+
+    if (verbose) {
+        psMetadataPrint(stderr, options->md, 0);
+    }
+
+    if (!parseTable(options)) {
+        psErrorStackPrint(stderr, "unable to parse request table");
+        return 1;
+    }
+
+    // XXX: We need to have multiple modes of operation depending on command line options
+    //      1. add a job to the database.
+    //      2. print the ppstamp command line
+    //      3. execute the command
+    // Actually should probably just let pstampparse do the last one so as
+    // to keep the ippTools model for database interactions intact. 
+    // So far in this program I'm only doing reads from the database not edits.
+
+    if (options->commandLine) {
+        printf("%s\n", options->commandLine);
+    }
+
+    return 0;
+}
Index: /trunk/pstamp/src/pstamprequest.c
===================================================================
--- /trunk/pstamp/src/pstamprequest.c	(revision 16132)
+++ /trunk/pstamp/src/pstamprequest.c	(revision 16132)
@@ -0,0 +1,420 @@
+#include <pslib.h>
+#include <string.h>
+#include "pstamp.h"
+#include "pstampROI.h"
+
+// pstamprequest  - create a fits table for a request to the postage stamp server
+
+
+/*******
+
+Description of program arguments
+
+
+Request Style
+-------------
+One of the following is required to select the target image files
+
+    -byid raw  exp_id class_id
+    -byid chip exp_id chip_id
+    -byid warp warp_id
+    -byid diff diff_id
+    -byid stack stack_id
+
+
+    -byexp  raw | chip | warp | diff | stack     exp_name 
+
+for raw and chip level the chip name must be specified as well (see -chip)
+
+
+    -bycoord
+
+For this style the center of the ROI must be specified in sky coordinates.
+
+
+
+
+ROI
+---
+One of the following is required to specify the center of the region of interest.
+(Note these are the same as the arguments ppstamp uses to specify the ROI).
+
+
+    -skycenter ra dec  
+
+ra and dec are in sexagesimal notation or in decimal degrees
+
+    -pixcenter x y
+
+-chip name may be required depending on the file class of the target
+(ppstamp determines this)
+
+
+One of the following is required to define the size of the ROI
+
+    -pixrange width height
+
+width and height are in pixels
+
+    -arcrange dRA dDEC
+
+dRA and dDEC in seconds of arc
+
+
+Miscellaneous Arguments
+-----------------------
+
+    -project "project name"
+
+    -chip "chip name"
+(required for -byid or -byexp when target file type is at chip level or below and the center
+is specified in pixels)
+
+
+    -after date_time
+    -before date_time
+May be used to narrow down the request for -bycoord queries
+
+
+
+*************************************************************************************************
+Table Columns
+
+Name        Type    Values                                  Required                    Notes
+-------------------------------------------------------------------------------------------------
+VERSION     String  N.N                                     all                         file format version
+PROJECT     String  any                                     all
+REQ_TYPE    String  byid | byexp | bycoord                  all
+IMG_TYPE    String  raw | chip | warp | diff | stack        byid and byexp
+ID          String  any                                     byid
+CLASS_ID    String  any                                     byid raw byexp raw
+EXPOSURE    String  any                                     byexp
+FILTER      String  any                                     only used when bycoord 
+
+RA          String  sexigesimal or decimal deg              null if pixcenter
+DEC         String  segigesimal or decimal deg              null of pixcenter
+CENTER_X    S32     any                                     null if not pixcenter
+CENTER_Y    S32     any                                     null if not pixcenter
+D_RA        F32     > 0                                     0 if pixrange
+D_DEC       F32     > 0                                     0 if pixrange
+WIDTH       U32     > 0                                     0 if not pixrange
+HEIGHT      U32     > 0                                     0 if not pixrange
+
+IMG_VERSION String
+
+The following need some more thought. Perhaps I need TIME_LIMIT  with values like "latest", "after", "before"
+AFTER       String  YYYYMMDD-HH:MM:SS                       only used when bycoord      UTC 
+BEFORE      String  YYYYMMDD-HH:MM:SS                       only used when bycoord      UTC
+OPTIONS     String  -latest
+
+This may be useful for debugging at least.
+FLAGS       String  -ls                                                                 Don't make stamps
+                                                                                        generate list of
+                                                                                        matching images
+
+*/
+
+typedef struct {
+#ifdef USE_DBNAME_FOR_PROJECT
+    pmConfig    *config;
+#endif
+    psMetadata  *md;
+    psString    fileName;
+} psrOptions;
+
+static void usage()
+{
+    psErrorStackPrint(stderr, "Unable to parse command-line arguments.");
+    exit(1);
+}
+
+static void getId(char *idString, int argnum, int *pArgc, char *argv[], psrOptions *options)
+{
+    if (*pArgc < 2) {
+        fprintf(stderr, "must specify %s\n", idString);
+        usage();
+    }
+    psMetadataAdd (options->md, PS_LIST_TAIL, idString, PS_DATA_STRING, "", argv[argnum]);
+    psArgumentRemove(argnum, pArgc, argv);
+}
+
+
+static pstampImageType getType(int argnum, int *pArgc, char *argv[], psrOptions *options, char *paramName)
+{
+    if (*pArgc < (argnum+2)) {
+        fprintf(stderr, "must specify image type and %s\n", paramName);
+        usage();
+    }
+    char *type = argv[argnum];
+
+    psArgumentRemove(argnum, pArgc, argv);
+
+    pstampImageType itype;
+
+    if (strcmp(type, "raw") == 0) {
+        itype = PSTAMP_RAW;
+    } else if (strcmp(type, "chip") == 0) {
+        itype = PSTAMP_CHIP;
+    } else if (strcmp(type, "warp") == 0) {
+        itype = PSTAMP_WARP;
+    } else if (strcmp(type, "diff") == 0) {
+        itype = PSTAMP_DIFF;
+    } else if (strcmp(type, "stack") == 0) {
+        itype = PSTAMP_STACK;
+    } else {
+        fprintf(stderr, "unknown image type %s\n", type);
+        usage();
+    }
+    psMetadataAdd (options->md, PS_LIST_TAIL, "IMG_TYPE", PS_DATA_STRING, "", type);
+
+    getId(paramName, argnum, pArgc, argv, options);
+
+    return itype;
+}
+
+static void doById(int argnum, int *pArgc, char *argv[], psrOptions *options)
+{
+    switch (getType(argnum, pArgc, argv, options, "ID")) {
+    case PSTAMP_RAW:
+        getId("CLASS_ID", argnum, pArgc, argv, options);
+        break;
+    case PSTAMP_CHIP:
+        getId("CLASS_ID", argnum, pArgc, argv, options);
+        break;
+    case PSTAMP_WARP:
+    case PSTAMP_DIFF:
+    case PSTAMP_STACK:
+        break;
+    default:
+        fprintf(stderr, "programming error unexpected image type\n");
+        exit(1);
+    }
+}
+
+static void doByExp(int argnum, int *pArgc, char *argv[], psrOptions *options)
+{
+    switch (getType(argnum, pArgc, argv, options, "EXPOSURE")) {
+    case PSTAMP_RAW:
+        getId("CLASS_ID", argnum, pArgc, argv, options);
+        break;
+    case PSTAMP_CHIP:
+        getId("CLASS_ID", argnum, pArgc, argv, options);
+        break;
+    case PSTAMP_WARP:
+    case PSTAMP_DIFF:
+    case PSTAMP_STACK:
+        break;
+    default:
+        fprintf(stderr, "programming error unexpected image type\n");
+        exit(1);
+    }
+}
+
+static psrOptions *parseArguments(int argc, char *argv[])
+{
+    psrOptions *options = psAlloc(sizeof(psrOptions));
+    psMetadata *md = psMetadataAlloc();
+    int         argnum;
+    bool        gotStyle = false;
+    bool        needCoord = false;
+    bool        gotCenter = false;
+    bool        gotRange = false;
+
+    options->md = md;
+
+    psMetadataAdd (md, PS_LIST_TAIL, "VERSION", PS_DATA_STRING, "", STAMP_REQUEST_VERSION);
+
+#ifdef USE_DBNAME_FOR_PROJECT
+    options->config = pmConfigRead(&argc, argv, NULL);
+    if (!options->config) {
+        psError(PS_ERR_UNKNOWN, false, "cannot find site file");
+        psFree(options->md);
+        psFree(options);
+        return NULL;
+    }
+
+    bool        status = false;
+    psString project = psMetadataLookupStr(&status, options->config->site, "DBNAME");
+    if (!project) {
+        fprintf(stderr, "DBNAME not specified\n");
+    }
+    psMetadataAdd(md, PS_LIST_TAIL, "PROJECT", PS_DATA_STRING, "", project);
+#else
+    // get project from command line
+    if ((argnum = psArgumentGet(argc, argv, "-project"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        if (argc < 2) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "no project name specified");
+            usage();
+        }
+        psMetadataAdd(md, PS_LIST_TAIL, "PROJECT", PS_DATA_STRING, "", argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+    } else {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "project name is required\n");
+        usage();
+    }
+#endif
+
+    // get user tag (base name for the postage stamp files
+    if ((argnum = psArgumentGet(argc, argv, "-user_tag"))) {
+        psArgumentRemove(argnum, &argc, argv);
+        if (argc < 2) {
+            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "missing value for user_tag");
+            usage();
+        }
+        psMetadataAdd(md, PS_LIST_TAIL, "USER_TAG", PS_DATA_STRING, "", argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+    } else {
+        psError(PS_ERR_BAD_PARAMETER_NULL, true, "no user_tag specified");
+        usage();
+    }
+
+    // find style & type
+    if ((argnum = psArgumentGet(argc, argv, "-bycoord"))) {
+        gotStyle = true;
+        psMetadataAdd(md, PS_LIST_TAIL, "REQ_TYPE", PS_DATA_STRING, "", 1+argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+        needCoord = true;
+        // TODO: we need an IMG_TYPE too...
+    }
+
+    if ((argnum = psArgumentGet(argc, argv, "-byid"))) {
+        if (gotStyle) {
+            fprintf(stderr, "only one of -bycoord -byid -byexp may be specified\n");
+            usage(1);
+        }
+        gotStyle = true;
+        psMetadataAdd(md, PS_LIST_TAIL, "REQ_TYPE", PS_DATA_STRING, "", 1+argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+        doById(argnum, &argc, argv, options);
+    }
+
+    if ((argnum = psArgumentGet(argc, argv, "-byexp"))) {
+        if (gotStyle) {
+            fprintf(stderr, "only one of -bycoord -byid -byexp may be specified\n");
+            usage(1);
+        }
+        gotStyle = true;
+        psMetadataAdd(md, PS_LIST_TAIL, "REQ_TYPE", PS_DATA_STRING, "", 1+argv[argnum]);
+        psArgumentRemove(argnum, &argc, argv);
+        doByExp(argnum, &argc, argv, options);
+    } 
+
+    if (!gotStyle) {
+        fprintf(stderr, "one of -bycoord -byid -byexp must be specified\n");
+        usage();
+    }
+
+    // find roi re-using ppstamp's ROI argument parsing code which value checking
+    // we just pass the strings along as provided by the user
+
+    pstampROI roiParam;
+
+    if (!pstampGetROI(&roiParam, &argc, argv, &gotCenter, &gotRange)) {
+        usage();
+    }
+
+    if (roiParam.celestialCenter) {
+        psMetadataAdd (options->md, PS_LIST_TAIL, "RA", PS_DATA_STRING, "", roiParam.center[0]);
+        psMetadataAdd (options->md, PS_LIST_TAIL, "DEC", PS_DATA_STRING, "", roiParam.center[1]);
+    } else {
+        if (needCoord) {
+            fprintf(stderr, "need to specify ROI in sky coordinates\n");
+            usage();
+        }
+        psMetadataAdd (options->md, PS_LIST_TAIL, "CENTER_X", PS_DATA_STRING, "", roiParam.center[0]);
+        psMetadataAdd (options->md, PS_LIST_TAIL, "CENTER_Y", PS_DATA_STRING, "", roiParam.center[1]);
+    }
+    if (roiParam.celestialRange) {
+        psMetadataAdd (options->md, PS_LIST_TAIL, "D_RA", PS_DATA_STRING, "",  roiParam.range[0]);
+        psMetadataAdd (options->md, PS_LIST_TAIL, "D_DEC", PS_DATA_STRING, "", roiParam.range[1]);
+    } else {
+        psMetadataAdd (options->md, PS_LIST_TAIL, "WIDTH", PS_DATA_STRING, "",  roiParam.range[0]);
+        psMetadataAdd (options->md, PS_LIST_TAIL, "HEIGHT", PS_DATA_STRING, "", roiParam.range[1]);
+    }
+
+    // TODO: find other optional arguments
+
+    if (argc == 2) {
+        options->fileName = psStringCopy(argv[1]);
+    } else if (argc == 1) {
+        fprintf(stderr, "output file name is required\n");
+        usage();
+    } else {
+        fprintf(stderr, "too many arguments supplied:");
+        fprintf(stderr, " %s", argv[1]);
+        for (int i=2; i<argc; i++) {
+            fprintf(stderr, ", %s", argv[i]);
+        }
+        fprintf(stderr, "\n");
+        usage();
+    }
+
+    return options;
+}
+
+static bool writeTable(psrOptions *options)
+{
+    psFits *fitsFile = psFitsOpen(options->fileName, "w");
+    if (fitsFile == NULL) {
+        psError(PS_ERR_IO, true, "failed to open %s for output\n", options->fileName);
+        return false;
+    }
+
+    psArray    *table = psArrayAlloc(1);
+
+    table->data[0] = options->md;
+
+    psFitsWriteTable(fitsFile, NULL, table, STAMP_REQUEST_EXTNAME);
+
+    psFitsClose(fitsFile);
+
+    return true;
+}
+
+bool sampleTable(const char *fileName)
+{
+    psFits *fitsFile = psFitsOpen(fileName, "w");
+    int     numRows = 42;
+
+    if (fitsFile == NULL) {
+        psError(PS_ERR_IO, true, "failed to open %s for output\n", fileName);
+        return false;
+    }
+
+    psArray    *table = psArrayAlloc(numRows);
+
+    for (int i=0; i< numRows; i++) {
+        psMetadata *row = psMetadataAlloc();
+        psMetadataAdd (row, PS_LIST_TAIL, "ROW",   PS_DATA_S32,    "", i);
+        psMetadataAdd (row, PS_LIST_TAIL, "FROW",  PS_TYPE_F32,    "", 0.1*i);
+        psMetadataAdd (row, PS_LIST_TAIL, "DUMMY", PS_DATA_STRING, "", "test line");
+
+        table->data[i] = row;
+         
+    }
+
+    psFitsWriteTable(fitsFile, NULL, table, "BILLSEXT");
+
+    psFitsClose(fitsFile);
+
+    return true;
+}
+
+int main(int argc, char *argv[])
+{
+    psrOptions *options = parseArguments(argc, argv);
+
+    if (!options) {
+        return 1;
+    }
+
+//    psMetadataPrint(stderr, options->md, 0);
+
+    if (writeTable(options)) {
+        return 0;
+    } else {
+        // XXX: we should have variable status codes that indicate what the problem was
+        return 1;
+    }
+}
