/* 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 #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 exception 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; }