/* pnmcat.c - concatenate PNM images
**
** Copyright (C) 1989, 1991 by Jef Poskanzer.
**
** Permission to use, copy, modify, and distribute this software and its
** documentation for any purpose and without fee is hereby granted, provided
** that the above copyright notice appear in all copies and that both that
** copyright notice and this permission notice appear in supporting
** documentation. This software is provided "as is" without express or
** implied warranty.
*/
#include <assert.h>
#include "pm_c_util.h"
#include "mallocvar.h"
#include "shhopt.h"
#include "bitarith.h"
#include "nstring.h"
#include "pnm.h"
#define LEFTBITS pm_byteLeftBits
#define RIGHTBITS pm_byteRightBits
enum backcolor {BACK_WHITE, BACK_BLACK, BACK_AUTO};
enum orientation {TOPBOTTOM, LEFTRIGHT};
enum justification {JUST_CENTER, JUST_MIN, JUST_MAX};
struct imgInfo {
/* This obviously should be a struct pam. We should convert this
to 'pamcat'.
*/
FILE * ifP;
int cols;
int rows;
int format;
xelval maxval;
};
struct cmdlineInfo {
/* All the information the user supplied in the command line,
in a form easy for the program to use.
*/
const char ** inputFilespec;
unsigned int nfiles;
enum backcolor backcolor;
enum orientation orientation;
enum justification justification;
};
static void
parseCommandLine(int argc, const char ** const argv,
struct cmdlineInfo * const cmdlineP) {
/*----------------------------------------------------------------------------
Note that the file spec array we return is stored in the storage that
was passed to us as the argv array.
-----------------------------------------------------------------------------*/
optEntry * option_def;
/* Instructions to OptParseOptions3() on how to parse our options.
*/
optStruct3 opt;
unsigned int option_def_index;
unsigned int leftright, topbottom, black, white, jtop, jbottom,
jleft, jright, jcenter;
MALLOCARRAY_NOFAIL(option_def, 100);
option_def_index = 0; /* incremented by OPTENTRY */
OPTENT3(0, "leftright", OPT_FLAG, NULL, &leftright, 0);
OPTENT3(0, "lr", OPT_FLAG, NULL, &leftright, 0);
OPTENT3(0, "topbottom", OPT_FLAG, NULL, &topbottom, 0);
OPTENT3(0, "tb", OPT_FLAG, NULL, &topbottom, 0);
OPTENT3(0, "black", OPT_FLAG, NULL, &black, 0);
OPTENT3(0, "white", OPT_FLAG, NULL, &white, 0);
OPTENT3(0, "jtop", OPT_FLAG, NULL, &jtop, 0);
OPTENT3(0, "jbottom", OPT_FLAG, NULL, &jbottom, 0);
OPTENT3(0, "jleft", OPT_FLAG, NULL, &jleft, 0);
OPTENT3(0, "jright", OPT_FLAG, NULL, &jright, 0);
OPTENT3(0, "jcenter", OPT_FLAG, NULL, &jcenter, 0);
opt.opt_table = option_def;
opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */
opt.allowNegNum = FALSE; /* We have no parms that are negative numbers */
pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
/* Uses and sets argc, argv, and some of *cmdlineP and others. */
free(option_def);
if (leftright + topbottom > 1)
pm_error("You may specify only one of -topbottom (-tb) and "
"-leftright (-lr)");
else if (leftright)
cmdlineP->orientation = LEFTRIGHT;
else if (topbottom)
cmdlineP->orientation = TOPBOTTOM;
else
pm_error("You must specify either -leftright or -topbottom");
if (black + white > 1)
pm_error("You may specify only one of -black and -white");
else if (black)
cmdlineP->backcolor = BACK_BLACK;
else if (white)
cmdlineP->backcolor = BACK_WHITE;
else
cmdlineP->backcolor = BACK_AUTO;
if (jtop + jbottom + jleft + jright + jcenter > 1)
pm_error("You may specify onlyone of -jtop, -jbottom, "
"-jleft, and -jright");
else {
switch (cmdlineP->orientation) {
case LEFTRIGHT:
if (jleft)
pm_error("-jleft is invalid with -leftright");
if (jright)
pm_error("-jright is invalid with -leftright");
if (jtop)
cmdlineP->justification = JUST_MIN;
else if (jbottom)
cmdlineP->justification = JUST_MAX;
else if (jcenter)
cmdlineP->justification = JUST_CENTER;
else
cmdlineP->justification = JUST_CENTER;
break;
case TOPBOTTOM:
if (jtop)
pm_error("-jtop is invalid with -topbottom");
if (jbottom)
pm_error("-jbottom is invalid with -topbottom");
if (jleft)
cmdlineP->justification = JUST_MIN;
else if (jright)
cmdlineP->justification = JUST_MAX;
else if (jcenter)
cmdlineP->justification = JUST_CENTER;
else
cmdlineP->justification = JUST_CENTER;
break;
}
}
if (argc-1 < 1) {
MALLOCARRAY_NOFAIL(cmdlineP->inputFilespec, 1);
cmdlineP->inputFilespec[0] = "-";
cmdlineP->nfiles = 1;
} else {
unsigned int i;
unsigned int stdinCt;
/* Number of input files user specified as Standard Input */
MALLOCARRAY_NOFAIL(cmdlineP->inputFilespec, argc-1);
for (i = 0, stdinCt = 0; i < argc-1; ++i) {
cmdlineP->inputFilespec[i] = argv[1+i];
if (streq(argv[1+i], "-"))
++stdinCt;
}
cmdlineP->nfiles = argc-1;
if (stdinCt > 1)
pm_error("At most one input image can come from Standard Input. "
"You specified %u", stdinCt);
}
}
static void
computeOutputParms(unsigned int const nfiles,
enum orientation const orientation,
struct imgInfo const img[],
unsigned int * const newcolsP,
unsigned int * const newrowsP,
xelval * const newmaxvalP,
int * const newformatP) {
double newcols, newrows;
int newformat;
xelval newmaxval;
unsigned int i;
newcols = 0;
newrows = 0;
for (i = 0; i < nfiles; ++i) {
const struct imgInfo * const imgP = &img[i];
if (i == 0) {
newmaxval = imgP->maxval;
newformat = imgP->format;
} else {
if (PNM_FORMAT_TYPE(imgP->format) > PNM_FORMAT_TYPE(newformat))
newformat = imgP->format;
if (imgP->maxval > newmaxval)
newmaxval = imgP->maxval;
}
switch (orientation) {
case LEFTRIGHT:
newcols += imgP->cols;
if (imgP->rows > newrows)
newrows = imgP->rows;
break;
case TOPBOTTOM:
newrows += imgP->rows;
if (imgP->cols > newcols)
newcols = imgP->cols;
break;
}
}
/* Note that while 'double' is not in general a precise numerical type,
in the case of a sum of integers which is less than INT_MAX, it
is exact, because double's precision is greater than int's.
*/
if (newcols > INT_MAX)
pm_error("Output width too large: %.0f.", newcols);
if (newrows > INT_MAX)
pm_error("Output height too large: %.0f.", newrows);
*newrowsP = (unsigned int)newrows;
*newcolsP = (unsigned int)newcols;
*newmaxvalP = newmaxval;
*newformatP = newformat;
}
static void
copyBitrow(const unsigned char * const source,
unsigned char * const destBitrow,
unsigned int const cols,
unsigned int const offset) {
/*----------------------------------------------------------------------------
Copy from source to destBitrow, without shifting. Preserve
surrounding image data.
-----------------------------------------------------------------------------*/
unsigned char * const dest = & destBitrow[ offset/8 ];
/* Copy destination, with leading full bytes ignored. */
unsigned int const rs = offset % 8;
/* The "little offset", as measured from start of dest. Source
is already shifted by this value.
*/
unsigned int const trs = (cols + rs) % 8;
/* The number of partial bits in the final char. */
unsigned int const colByteCnt = pbm_packed_bytes(cols + rs);
/* # bytes to process, including partial ones on both ends. */
unsigned int const last = colByteCnt - 1;
unsigned char const origHead = dest[0];
unsigned char const origEnd = dest[last];
unsigned int i;
assert(colByteCnt >= 1);
for (i = 0; i < colByteCnt; ++i)
dest[i] = source[i];
if (rs > 0)
dest[0] = LEFTBITS(origHead, rs) | RIGHTBITS(dest[0], 8-rs);
if (trs > 0)
dest[last] = LEFTBITS(dest[last], trs) | RIGHTBITS(origEnd, 8-trs);
}
static void
padFillBitrow(unsigned char * const destBitrow,
unsigned char const padColor,
unsigned int const cols,
unsigned int const offset) {
/*----------------------------------------------------------------------------
Fill destBitrow, starting at offset, with padColor. padColor is a
byte -- 0x00 or 0xff -- not a single bit.
-----------------------------------------------------------------------------*/
unsigned char * const dest = &destBitrow[offset/8];
unsigned int const rs = offset % 8;
unsigned int const trs = (cols + rs) % 8;
unsigned int const colByteCnt = pbm_packed_bytes(cols + rs);
unsigned int const last = colByteCnt - 1;
unsigned char const origHead = dest[0];
unsigned char const origEnd = dest[last];
unsigned int i;
assert(colByteCnt > 0);
for (i = 0; i < colByteCnt; ++i)
dest[i] = padColor;
if (rs > 0)
dest[0] = LEFTBITS(origHead, rs) | RIGHTBITS(dest[0], 8-rs);
if (trs > 0)
dest[last] = LEFTBITS(dest[last], trs) | RIGHTBITS(origEnd, 8-trs);
}
/* concatenateLeftRightPBM() and concatenateLeftRightGen()
employ almost identical algorithms.
The difference is in the data types and functions.
Same for concatenateTopBottomPBM() and concatenateTopBottomGen().
*/
struct imgInfoPbm2 {
/* Information about one image */
unsigned char * proberow;
/* Top row of image, when background color is
auto-determined.
*/
unsigned int offset;
/* start position of image, in bits, counting from left
edge
*/
unsigned char background;
/* Background color. 0x00 means white; 0xff means black */
unsigned int padtop;
/* Top padding amount */
};
static void
getPbmImageInfo(struct imgInfo const img[],
unsigned int const nfiles,
unsigned int const newrows,
enum justification const justification,
enum backcolor const backcolor,
struct imgInfoPbm2 ** const img2P) {
/*----------------------------------------------------------------------------
Read the first row of each image in img[] and return that and additional
information about images as *img2P.
-----------------------------------------------------------------------------*/
struct imgInfoPbm2 * img2;
unsigned int i;
MALLOCARRAY_NOFAIL(img2, nfiles);
for (i = 0; i < nfiles; ++i) {
switch (justification) {
case JUST_MIN: img2[i].padtop = 0; break;
case JUST_MAX: img2[i].padtop = newrows - img[i].rows; break;
case JUST_CENTER: img2[i].padtop = (newrows - img[i].rows) / 2; break;
}
img2[i].offset = (i == 0) ? 0 : img2[i-1].offset + img[i-1].cols;
if (img[i].rows == newrows) /* no padding */
img2[i].proberow = NULL;
else { /* determine pad color for image i */
switch (backcolor) {
case BACK_AUTO: {
bit bgBit;
img2[i].proberow = pbm_allocrow_packed(img[i].cols+7);
pbm_readpbmrow_bitoffset(
img[i].ifP, img2[i].proberow,
img[i].cols, img[i].format, img2[i].offset % 8);
bgBit = pbm_backgroundbitrow(
img2[i].proberow, img[i].cols, img2[i].offset % 8);
img2[i].background = bgBit == PBM_BLACK ? 0xff : 0x00;
} break;
case BACK_BLACK:
img2[i].proberow = NULL;
img2[i].background = 0xff;
break;
case BACK_WHITE:
img2[i].proberow = NULL;
img2[i].background = 0x00;
break;
}
}
}
*img2P = img2;
}
static void
destroyPbmImg2(struct imgInfoPbm2 * const img2,
unsigned int const nfiles) {
unsigned int i;
for (i = 0; i < nfiles; ++i) {
if (img2[i].proberow)
free(img2[i].proberow);
}
free(img2);
}
static void
concatenateLeftRightPbm(FILE * const ofP,
unsigned int const nfiles,
unsigned int const newcols,
unsigned int const newrows,
enum justification const justification,
struct imgInfo const img[],
enum backcolor const backcolor) {
unsigned char * const outrow = pbm_allocrow_packed(newcols);
/* We use just one outrow. All padding and image data (with the
exeption of following img2.proberow) goes directly into this
packed PBM row.
*/
struct imgInfoPbm2 * img2;
/* malloc'ed array, one element per image. Shadows img[] */
unsigned int row;
getPbmImageInfo(img, nfiles, newrows, justification, backcolor, &img2);
outrow[pbm_packed_bytes(newcols)-1] = 0x00;
for (row = 0; row < newrows; ++row) {
unsigned int i;
for (i = 0; i < nfiles; ++i) {
if ((row == 0 && img2[i].padtop > 0) ||
row == img2[i].padtop + img[i].rows) {
/* This row begins a run of padding, either above or below
file 'i', so set 'outrow' to padding.
*/
padFillBitrow(outrow, img2[i].background, img[i].cols,
img2[i].offset);
}
if (row == img2[i].padtop && img2[i].proberow != NULL) {
/* Top row has been read to proberow[] to determine
background. Copy it to outrow[].
*/
copyBitrow(img2[i].proberow, outrow,
img[i].cols, img2[i].offset);
} else if (row >= img2[i].padtop &&
row < img2[i].padtop + img[i].rows) {
pbm_readpbmrow_bitoffset(
img[i].ifP, outrow, img[i].cols, img[i].format,
img2[i].offset);
} else {
/* It's a row of padding, so outrow[] is already set
appropriately.
*/
}
}
pbm_writepbmrow_packed(ofP, outrow, newcols, 0);
}
destroyPbmImg2(img2, nfiles);
pbm_freerow_packed(outrow);
}
static void
concatenateTopBottomPbm(FILE * const ofP,
unsigned int const nfiles,
int const newcols,
int const newrows,
enum justification const justification,
struct imgInfo const img[],
enum backcolor const backcolor) {
unsigned char * const outrow = pbm_allocrow_packed(newcols);
/* Like the left-right PBM case, all padding and image data
goes directly into outrow. There is no proberow.
*/
unsigned char background, backgroundPrev;
/* 0x00 means white; 0xff means black */
unsigned int padleft;
bool backChange;
/* Background color is different from that of the previous
input image.
*/
unsigned int i;
unsigned int row, startRow;
outrow[pbm_packed_bytes(newcols)-1] = 0x00;
switch (backcolor){
case BACK_AUTO: /* do nothing */ break;
case BACK_BLACK: background = 0xff; break;
case BACK_WHITE: background = 0x00; break;
}
for (i = 0; i < nfiles; ++i) {
if (img[i].cols == newcols) {
/* No padding */
startRow = 0;
backChange = FALSE;
padleft = 0;
outrow[pbm_packed_bytes(newcols)-1] = 0x00;
} else {
/* Determine amount of padding and color */
switch (justification) {
case JUST_MIN: padleft = 0; break;
case JUST_MAX: padleft = newcols - img[i].cols; break;
case JUST_CENTER: padleft = (newcols - img[i].cols) / 2; break;
}
switch (backcolor) {
case BACK_AUTO: {
bit bgBit;
startRow = 1;
pbm_readpbmrow_bitoffset(img[i].ifP,
outrow, img[i].cols, img[i].format,
padleft);
bgBit = pbm_backgroundbitrow(outrow, img[i].cols, padleft);
background = bgBit == PBM_BLACK ? 0xff : 0x00;
backChange = (i == 0 || background != backgroundPrev);
} break;
case BACK_WHITE:
case BACK_BLACK:
startRow = 0;
backChange = (i==0);
break;
}
if (backChange || (i > 0 && img[i-1].cols > img[i].cols)) {
unsigned int const padright = newcols - padleft - img[i].cols;
if (padleft > 0)
padFillBitrow(outrow, background, padleft, 0);
if (padright > 0)
padFillBitrow(outrow, background, padright,
padleft + img[i].cols);
}
}
if (startRow == 1)
/* Top row already read for auto background color
determination. Write it out.
*/
pbm_writepbmrow_packed(ofP, outrow, newcols, 0);
for (row = startRow; row < img[i].rows; ++row) {
pbm_readpbmrow_bitoffset(img[i].ifP, outrow, img[i].cols,
img[i].format, padleft);
pbm_writepbmrow_packed(ofP, outrow, newcols, 0);
}
backgroundPrev = background;
}
pbm_freerow_packed(outrow);
}
struct imgGen2 {
xel * xelrow;
xel * inrow;
xel background;
int padtop;
};
static void
getGenImgInfo(struct imgInfo const img[],
unsigned int const nfiles,
xel * const newxelrow,
unsigned int const newrows,
xelval const newmaxval,
int const newformat,
enum justification const justification,
enum backcolor const backcolor,
struct imgGen2 ** const img2P) {
struct imgGen2 * img2;
unsigned int i;
MALLOCARRAY_NOFAIL(img2, nfiles);
for (i = 0; i < nfiles; ++i) {
switch (justification) { /* Determine top padding */
case JUST_MIN:
img2[i].padtop = 0;
break;
case JUST_MAX:
img2[i].padtop = newrows - img[i].rows;
break;
case JUST_CENTER:
img2[i].padtop = (newrows - img[i].rows) / 2;
break;
}
img2[i].inrow =
(i == 0 ? &newxelrow[0] : img2[i-1].inrow + img[i-1].cols);
if (img[i].rows == newrows) /* no padding */
img2[i].xelrow = NULL;
else {
/* Determine pad color */
switch (backcolor){
case BACK_AUTO:
img2[i].xelrow = pnm_allocrow(img[i].cols);
pnm_readpnmrow(img[i].ifP, img2[i].xelrow,
img[i].cols, img[i].maxval, img[i].format);
pnm_promoteformatrow(img2[i].xelrow, img[i].cols,
img[i].maxval, img[i].format,
newmaxval, newformat);
img2[i].background = pnm_backgroundxelrow(
img2[i].xelrow, img[i].cols, newmaxval, newformat);
break;
case BACK_BLACK:
img2[i].xelrow = NULL;
img2[i].background = pnm_blackxel(newmaxval, newformat);
break;
case BACK_WHITE:
img2[i].xelrow = NULL;
img2[i].background = pnm_whitexel(newmaxval, newformat);
break;
}
}
}
*img2P = img2;
}
static void
concatenateLeftRightGen(FILE * const ofP,
unsigned int const nfiles,
unsigned int const newcols,
unsigned int const newrows,
xelval const newmaxval,
int const newformat,
enum justification const justification,
struct imgInfo const img[],
enum backcolor const backcolor) {
xel * const outrow = pnm_allocrow(newcols);
struct imgGen2 * img2;
unsigned int row;
getGenImgInfo(img, nfiles, outrow, newrows,
newmaxval, newformat, justification, backcolor, &img2);
for (row = 0; row < newrows; ++row) {
unsigned int i;
for (i = 0; i < nfiles; ++i) {
if ((row == 0 && img2[i].padtop > 0) ||
row == img2[i].padtop + img[i].rows) {
/* This row begins a run of padding, either above or below
file 'i', so set 'outrow' to padding.
*/
unsigned int col;
for (col = 0; col < img[i].cols; ++col)
img2[i].inrow[col] = img2[i].background;
}
if (row == img2[i].padtop && img2[i].xelrow) {
/* We're at the top row of file 'i', and that row
has already been read to xelrow[] to determine
background. Copy it to 'outrow'.
*/
unsigned int col;
for (col = 0; col < img[i].cols; ++col)
img2[i].inrow[col] = img2[i].xelrow[col];
free(img2[i].xelrow);
} else if (row >= img2[i].padtop &&
row < img2[i].padtop + img[i].rows) {
pnm_readpnmrow(
img[i].ifP, img2[i].inrow, img[i].cols, img[i].maxval,
img[i].format);
pnm_promoteformatrow(
img2[i].inrow, img[i].cols, img[i].maxval,
img[i].format, newmaxval, newformat);
} else {
/* It's a row of padding, so outrow[] is already set
appropriately.
*/
}
}
pnm_writepnmrow(ofP, outrow, newcols, newmaxval, newformat, 0);
}
pnm_freerow(outrow);
}
static void
concatenateTopBottomGen(FILE * const ofP,
unsigned int const nfiles,
int const newcols,
int const newrows,
xelval const newmaxval,
int const newformat,
enum justification const justification,
struct imgInfo const img[],
enum backcolor const backcolor) {
xel * const newxelrow = pnm_allocrow(newcols);
xel * inrow;
unsigned int padleft;
unsigned int i;
unsigned int row, startRow;
xel background, backgroundPrev;
bool backChange;
/* The background color is different from that of the previous
input image.
*/
switch (backcolor) {
case BACK_AUTO: /* do nothing now, determine at start of each image */
break;
case BACK_BLACK: background = pnm_blackxel(newmaxval, newformat);
break;
case BACK_WHITE: background = pnm_whitexel(newmaxval, newformat);
break;
}
for ( i = 0; i < nfiles; ++i, backgroundPrev = background) {
if (img[i].cols == newcols) {
/* no padding */
startRow = 0;
backChange = FALSE;
inrow = newxelrow;
} else { /* Calculate left padding amount */
switch (justification) {
case JUST_MIN: padleft = 0; break;
case JUST_MAX: padleft = newcols - img[i].cols; break;
case JUST_CENTER: padleft = (newcols - img[i].cols) / 2; break;
}
if (backcolor == BACK_AUTO) {
/* Determine background color */
startRow = 1;
inrow = &newxelrow[padleft];
pnm_readpnmrow(img[i].ifP, inrow,
img[i].cols, img[i].maxval, img[i].format);
pnm_promoteformatrow(inrow, img[i].cols, img[i].maxval,
img[i].format,
newmaxval, newformat);
background = pnm_backgroundxelrow(
inrow, img[i].cols, newmaxval, newformat);
backChange = i==0 || !PNM_EQUAL(background, backgroundPrev);
} else {
/* background color is constant: black or white */
startRow = 0;
inrow = &newxelrow[padleft];
backChange = (i==0);
}
if (backChange || (i > 0 && img[i-1].cols > img[i].cols)) {
unsigned int col;
for (col = 0; col < padleft; ++col)
newxelrow[col] = background;
for (col = padleft + img[i].cols; col < newcols; ++col)
newxelrow[col] = background;
}
}
if (startRow == 1)
/* Top row already read for auto background
color determination. Write it out. */
pnm_writepnmrow(ofP, newxelrow, newcols, newmaxval, newformat, 0);
for (row = startRow; row < img[i].rows; ++row) {
pnm_readpnmrow(img[i].ifP,
inrow, img[i].cols, img[i].maxval, img[i].format);
pnm_promoteformatrow(
inrow, img[i].cols, img[i].maxval, img[i].format,
newmaxval, newformat);
pnm_writepnmrow(ofP, newxelrow, newcols, newmaxval, newformat, 0);
}
}
pnm_freerow(newxelrow);
}
int
main(int argc,
const char ** argv) {
struct cmdlineInfo cmdline;
struct imgInfo * img; /* malloc'ed array */
xelval newmaxval;
int newformat;
unsigned int i;
unsigned int newrows, newcols;
pm_proginit(&argc, argv);
parseCommandLine(argc, argv, &cmdline);
MALLOCARRAY_NOFAIL(img, cmdline.nfiles);
for (i = 0; i < cmdline.nfiles; ++i) {
img[i].ifP = pm_openr(cmdline.inputFilespec[i]);
pnm_readpnminit(img[i].ifP, &img[i].cols, &img[i].rows,
&img[i].maxval, &img[i].format);
}
computeOutputParms(cmdline.nfiles, cmdline.orientation, img,
&newcols, &newrows, &newmaxval, &newformat);
pnm_writepnminit(stdout, newcols, newrows, newmaxval, newformat, 0);
if (PNM_FORMAT_TYPE(newformat) == PBM_TYPE) {
switch (cmdline.orientation) {
case LEFTRIGHT:
concatenateLeftRightPbm(stdout, cmdline.nfiles,
newcols, newrows, cmdline.justification,
img, cmdline.backcolor);
break;
case TOPBOTTOM:
concatenateTopBottomPbm(stdout, cmdline.nfiles,
newcols, newrows, cmdline.justification,
img, cmdline.backcolor);
break;
}
} else {
switch (cmdline.orientation) {
case LEFTRIGHT:
concatenateLeftRightGen(stdout, cmdline.nfiles,
newcols, newrows, newmaxval, newformat,
cmdline.justification, img,
cmdline.backcolor);
break;
case TOPBOTTOM:
concatenateTopBottomGen(stdout, cmdline.nfiles,
newcols, newrows, newmaxval, newformat,
cmdline.justification, img,
cmdline.backcolor);
break;
}
}
for (i = 0; i < cmdline.nfiles; ++i)
pm_close(img[i].ifP);
free(cmdline.inputFilespec);
free(img);
pm_close(stdout);
return 0;
}