/*============================================================================= pamcat =============================================================================== Concatenate images. By Bryan Henderson and Akira Urushibata. Contributed to the public domain by its authors. =============================================================================*/ #include #include "pm_c_util.h" #include "mallocvar.h" #include "shhopt.h" #include "bitarith.h" #include "nstring.h" #include "pam.h" #include "pbm.h" #define LEFTBITS pm_byteLeftBits #define RIGHTBITS pm_byteRightBits enum PadColorMethod {PAD_BLACK, PAD_WHITE, PAD_AUTO}; /* The method of determining the color of padding when images are not the same height or width. Always white (maxval samples) always black (zero samples) or determined from what looks like background for the image in question. */ enum Orientation {TOPBOTTOM, LEFTRIGHT}; /* Direction of concatenation */ enum Justification {JUST_CENTER, JUST_MIN, JUST_MAX}; /* Justification of images in concatenation */ struct CmdlineInfo { /* All the information the user supplied in the command line, in a form easy for the program to use. */ const char ** inputFileName; unsigned int fileCt; enum PadColorMethod padColorMethod; enum Orientation orientation; enum Justification justification; unsigned int verbose; }; 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; unsigned int black, white; unsigned int jtop, jbottom, jleft, jright, jcenter; MALLOCARRAY_NOFAIL(option_def, 100); option_def_index = 0; /* incremented by OPTENT3 */ 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); OPTENT3(0, "verbose", OPT_FLAG, NULL, &cmdlineP->verbose, 0); opt.opt_table = option_def; opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ opt.allowNegNum = FALSE; /* We have no parms that are negative numbers */ 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->padColorMethod = PAD_BLACK; else if (white) cmdlineP->padColorMethod = PAD_WHITE; else cmdlineP->padColorMethod = PAD_AUTO; if (jtop + jbottom + jleft + jright + jcenter > 1) pm_error("You may specify only one 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->inputFileName, 1); cmdlineP->inputFileName[0] = "-"; cmdlineP->fileCt = 1; } else { unsigned int i; unsigned int stdinCt; /* Number of input files user specified as Standard Input */ MALLOCARRAY_NOFAIL(cmdlineP->inputFileName, argc-1); for (i = 0, stdinCt = 0; i < argc-1; ++i) { cmdlineP->inputFileName[i] = argv[1+i]; if (streq(argv[1+i], "-")) ++stdinCt; } cmdlineP->fileCt = argc-1; if (stdinCt > 1) pm_error("At most one input image can come from Standard Input. " "You specified %u", stdinCt); } } static const char * tupletypeX(bool const allVisual, unsigned int const colorDepth, sample const maxMaxval, bool const haveOpacity) { const char * retval; if (allVisual) { switch (colorDepth) { case 1: if (maxMaxval == 1) retval = haveOpacity ? "BLACKANDWHITE_ALPHA" : "BLACKANDWHITE"; else retval = haveOpacity ? "GRAYSCALE_ALPHA" : "GRAYSCALE"; break; case 3: retval = haveOpacity ? "RGB_ALPHA" : "RGB"; break; default: assert(false); } } else retval = ""; return retval; } typedef struct { /* This describes a transformation from one tuple type to another, e.g. from BLACKANDWHITE to GRAY_ALPHA. For transformations bewteen the defined ones for visual images, only the "up" transformations are covered. */ bool mustPromoteColor; /* Plane 0, which is the black/white or grayscale plane and also the red plane must be copied as the red, green, and blue planes (0, 1, and 2). */ bool mustPromoteOpacity; /* Plane 1, which is the opacity plane for black and white or grayscale tuples, must be copied as the RGB opacity plane (3). */ bool mustCreateOpacity; /* The opacity plane value must be set to opaque */ bool mustPadZero; /* Where the target tuple type is deeper than the source tuple type, all higher numbered planes must be cleared to zero. This is mutually exclusive with the rest of the musts. */ } TtTransform; static TtTransform ttXformForImg(const struct pam * const inpamP, const struct pam * const outpamP) { /*---------------------------------------------------------------------------- The transform required to transform tuples of the kind described by *inpamP to tuples of the kind described by *outpamP (e.g. from grayscale to RGB, which involves replicating one plane into three). We assume *outpamP tuples are of a type that is at least as expressive as *inpamP tuples. So e.g. outpamP->tuple_type cannot be "GRAYSCALE" if inpamP->tuple_type is "RGB". -----------------------------------------------------------------------------*/ TtTransform retval; if (inpamP->visual && outpamP->visual) { retval.mustPromoteColor = (outpamP->color_depth > inpamP->color_depth); retval.mustPromoteOpacity = (outpamP->color_depth > inpamP->color_depth && (outpamP->have_opacity && inpamP->have_opacity)); retval.mustCreateOpacity = (outpamP->have_opacity && !inpamP->have_opacity); retval.mustPadZero = false; } else { retval.mustPromoteColor = false; retval.mustPromoteOpacity = false; retval.mustCreateOpacity = false; retval.mustPadZero = true; } return retval; } static void reportPlans(unsigned int const fileCt, const struct pam * const outpamP) { pm_message("Concatenating %u input images", fileCt); pm_message("Output width, height, depth: %u x %u x %u", outpamP->width, outpamP->height, outpamP->depth); if (outpamP->format == RPBM_FORMAT) pm_message("Using PBM fast path and producing raw PBM output"); else if (outpamP->format == PBM_FORMAT) pm_message("Output format: Plain PBM"); else { pm_message("Output maxval (max of all inputs): %lu", outpamP->maxval); switch (outpamP->format) { case PGM_FORMAT: pm_message("Output format: Plain PGM"); break; case RPGM_FORMAT: pm_message("Output format: Raw PGM"); break; case PPM_FORMAT: pm_message("Output format: Plain PPM"); break; case RPPM_FORMAT: pm_message("Output format: Raw PPM"); break; case PAM_FORMAT: pm_message("Output format: PAM"); if (strlen(outpamP->tuple_type) > 0) pm_message("Output tuple type: '%s'", outpamP->tuple_type); else pm_message("Output tuple type is null string because " "input images have various non-visual tuple types"); break; } } } static void computeOutputParms(unsigned int const fileCt, enum Orientation const orientation, const struct pam * const inpam, /* array */ bool const verbose, struct pam * const outpamP) { double newCols, newRows; unsigned int maxDepth; sample maxMaxval; int newFormat; const char * firstTupletype; bool allSameTt; bool allVisual; unsigned int maxColorDepth; bool haveOpacity; unsigned int fileSeq; for (fileSeq = 0, newCols = 0, newRows = 0, maxDepth = 0, maxMaxval = 0, newFormat = 0, allVisual = true, maxColorDepth = 0, haveOpacity = false, firstTupletype = NULL, allSameTt = true; fileSeq < fileCt; ++fileSeq) { const struct pam * const inpamP = &inpam[fileSeq]; switch (orientation) { case LEFTRIGHT: newCols += inpamP->width; newRows = MAX(newRows, inpamP->height); break; case TOPBOTTOM: newRows += inpamP->height; newCols = MAX(newCols, inpamP->width); break; } if (!firstTupletype) firstTupletype = inpamP->tuple_type; if (inpamP->tuple_type != firstTupletype) allSameTt = false; if (inpamP->visual) { maxColorDepth = MAX(maxColorDepth, inpamP->color_depth); if (inpamP->have_opacity) haveOpacity = true; } else allVisual = false; maxDepth = MAX(maxDepth, inpamP->depth); maxMaxval = MAX(maxMaxval, inpamP->maxval); if (PAM_FORMAT_TYPE(inpamP->format) > PAM_FORMAT_TYPE(newFormat)) newFormat = inpamP->format; } assert(newCols > 0); assert(newRows > 0); assert(maxMaxval > 0); assert(newFormat > 0); if (newCols > INT_MAX) pm_error("Output width too large: %.0f.", newCols); if (newRows > INT_MAX) pm_error("Output height too large: %.0f.", newRows); outpamP->size = sizeof(*outpamP); outpamP->len = PAM_STRUCT_SIZE(tuple_type); /* 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. */ outpamP->height = (unsigned int)newRows; outpamP->width = (unsigned int)newCols; if (allVisual) outpamP->depth = MAX(maxDepth, maxColorDepth + (haveOpacity ? 1 : 0)); else outpamP->depth = maxDepth; outpamP->allocation_depth = 0; /* This means same as depth */ outpamP->maxval = maxMaxval; outpamP->format = newFormat; if (allSameTt) STRSCPY(outpamP->tuple_type, firstTupletype); else STRSCPY(outpamP->tuple_type, tupletypeX(allVisual, maxColorDepth, maxMaxval, haveOpacity)); outpamP->comment_p = NULL; outpamP->plainformat = false; if (verbose) reportPlans(fileCt, outpamP); } 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(). */ typedef struct { /* 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 */ } LrImgCtlPbm; static void createLrImgCtlPbm(const struct pam * const inpam, /* array */ unsigned int const fileCt, unsigned int const outHeight, enum Justification const justification, enum PadColorMethod const padColorMethod, LrImgCtlPbm ** const imgCtlP) { /*---------------------------------------------------------------------------- Read the first row of each image in inpam[] and return that and additional information about images as *imgCtlP. -----------------------------------------------------------------------------*/ LrImgCtlPbm * imgCtl; /* array, size 'fileCt' */ unsigned int fileSeq; MALLOCARRAY_NOFAIL(imgCtl, fileCt); for (fileSeq = 0; fileSeq < fileCt; ++fileSeq) { LrImgCtlPbm * const imgCtlP = &imgCtl[fileSeq]; const struct pam * const inpamP = &inpam[fileSeq]; switch (justification) { case JUST_MIN: imgCtlP->padtop = 0; break; case JUST_MAX: imgCtlP->padtop = outHeight - inpam[fileSeq].height; break; case JUST_CENTER: imgCtlP->padtop = (outHeight - inpamP->height) / 2; break; } imgCtlP->offset = (fileSeq == 0) ? 0 : imgCtl[fileSeq-1].offset + inpam[fileSeq-1].width; if (inpamP->height == outHeight) /* no padding */ imgCtlP->proberow = NULL; else { /* determine pad color for image i */ switch (padColorMethod) { case PAD_AUTO: { bit bgBit; imgCtlP->proberow = pbm_allocrow_packed((unsigned int)inpamP->width + 7); pbm_readpbmrow_bitoffset( inpamP->file, imgCtlP->proberow, inpamP->width, inpamP->format, imgCtlP->offset % 8); bgBit = pbm_backgroundbitrow( imgCtlP->proberow, inpamP->width, imgCtlP->offset % 8); imgCtlP->background = bgBit == PBM_BLACK ? 0xff : 0x00; } break; case PAD_BLACK: imgCtlP->proberow = NULL; imgCtlP->background = 0xff; break; case PAD_WHITE: imgCtlP->proberow = NULL; imgCtlP->background = 0x00; break; } } } *imgCtlP = imgCtl; } static void destroyPbmImgCtl(LrImgCtlPbm * const imgCtl, /* array */ unsigned int const fileCt) { unsigned int i; for (i = 0; i < fileCt; ++i) { if (imgCtl[i].proberow) free(imgCtl[i].proberow); } free(imgCtl); } static void concatenateLeftRightPbm(struct pam * const outpamP, const struct pam * const inpam, /* array */ unsigned int const fileCt, enum Justification const justification, enum PadColorMethod const padColorMethod) { unsigned char * const outrow = pbm_allocrow_packed(outpamP->width); /* We use just one outrow. All padding and image data (with the exception of following imgCtl.proberow) goes directly into this packed PBM row. */ LrImgCtlPbm * imgCtl; /* malloc'ed array, one element per image. Shadows inpam[] */ unsigned int row; createLrImgCtlPbm(inpam, fileCt, outpamP->height, justification, padColorMethod, &imgCtl); outrow[pbm_packed_bytes(outpamP->width)-1] = 0x00; for (row = 0; row < outpamP->height; ++row) { unsigned int fileSeq; for (fileSeq = 0; fileSeq < fileCt; ++fileSeq) { const LrImgCtlPbm * const imgCtlP = &imgCtl[fileSeq]; const struct pam * const inpamP = &inpam[fileSeq]; if ((row == 0 && imgCtlP->padtop > 0) || row == imgCtlP->padtop + inpamP->height) { /* This row begins a run of padding, either above or below file 'i', so set 'outrow' to padding. */ padFillBitrow(outrow, imgCtlP->background, inpamP->width, imgCtlP->offset); } if (row == imgCtlP->padtop && imgCtlP->proberow != NULL) { /* Top row has been read to proberow[] to determine background. Copy it to outrow[]. */ copyBitrow(imgCtlP->proberow, outrow, inpamP->width, imgCtlP->offset); } else if (row >= imgCtlP->padtop && row < imgCtlP->padtop + inpamP->height) { pbm_readpbmrow_bitoffset( inpamP->file, outrow, inpamP->width, inpamP->format, imgCtlP->offset); } else { /* It's a row of padding, so outrow[] is already set appropriately. */ } } pbm_writepbmrow_packed(outpamP->file, outrow, outpamP->width, 0); } destroyPbmImgCtl(imgCtl, fileCt); pbm_freerow_packed(outrow); } static void concatenateTopBottomPbm(const struct pam * const outpamP, const struct pam * const inpam, /* array */ unsigned int const fileCt, enum Justification const justification, enum PadColorMethod const padColorMethod) { unsigned char * const outrow = pbm_allocrow_packed(outpamP->width); /* 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 fileSeq; unsigned int row, startRow; outrow[pbm_packed_bytes(outpamP->width)-1] = 0x00; switch (padColorMethod){ case PAD_AUTO: /* do nothing */ break; case PAD_BLACK: background = 0xff; break; case PAD_WHITE: background = 0x00; break; } for (fileSeq = 0; fileSeq < fileCt; ++fileSeq) { const struct pam * const inpamP = &inpam[fileSeq]; if (inpamP->width == outpamP->width) { /* No padding */ startRow = 0; backChange = FALSE; padleft = 0; outrow[pbm_packed_bytes(outpamP->width)-1] = 0x00; } else { /* Determine amount of padding and color */ switch (justification) { case JUST_MIN: padleft = 0; break; case JUST_MAX: padleft = outpamP->width - inpamP->width; break; case JUST_CENTER: padleft = (outpamP->width - inpamP->width) / 2; break; } switch (padColorMethod) { case PAD_AUTO: { bit bgBit; startRow = 1; pbm_readpbmrow_bitoffset( inpamP->file, outrow, inpamP->width, inpamP->format, padleft); bgBit = pbm_backgroundbitrow(outrow, inpamP->width, padleft); background = bgBit == PBM_BLACK ? 0xff : 0x00; backChange = (fileSeq == 0 || background != backgroundPrev); } break; case PAD_WHITE: case PAD_BLACK: startRow = 0; backChange = (fileSeq == 0); break; } if (backChange || (fileSeq > 0 && inpam[fileSeq-1].width > inpamP->width)) { unsigned int const padright = outpamP->width - padleft - inpamP->width; if (padleft > 0) padFillBitrow(outrow, background, padleft, 0); if (padright > 0) padFillBitrow(outrow, background, padright, padleft + inpamP->width); } } if (startRow == 1) /* Top row already read for auto background color determination. Write it out. */ pbm_writepbmrow_packed(outpamP->file, outrow, outpamP->width, 0); for (row = startRow; row < inpamP->height; ++row) { pbm_readpbmrow_bitoffset(inpamP->file, outrow, inpamP->width, inpamP->format, padleft); pbm_writepbmrow_packed(outpamP->file, outrow, outpamP->width, 0); } backgroundPrev = background; } pbm_freerow_packed(outrow); } static void padPlanesRow(const struct pam * const inpamP, tuple * const outrow, const struct pam * const outpamP) { /*---------------------------------------------------------------------------- Rearrange the planes of *outrow as needed to transform them into tuples as described by *outpamP from tuples as described by *inpamP. -----------------------------------------------------------------------------*/ TtTransform const ttTransform = ttXformForImg(inpamP, outpamP); assert(inpamP->allocation_depth >= outpamP->depth); if (ttTransform.mustPromoteOpacity) { unsigned int col; assert(outpamP->depth >= PAM_TRN_PLANE); for (col = 0; col < inpamP->width; ++col) { outrow[col][outpamP->opacity_plane] = outrow[col][inpamP->opacity_plane]; } } if (ttTransform.mustPromoteColor) { unsigned int col; assert(outpamP->depth >= PAM_GRN_PLANE); assert(outpamP->depth >= PAM_BLU_PLANE); for (col = 0; col < inpamP->width; ++col) { assert(PAM_RED_PLANE == 0); outrow[col][PAM_GRN_PLANE] = outrow[col][0]; outrow[col][PAM_BLU_PLANE] = outrow[col][0]; } } if (ttTransform.mustCreateOpacity) { unsigned int col; for (col = 0; col < inpamP->width; ++col) outrow[col][outpamP->opacity_plane] = outpamP->maxval; } if (ttTransform.mustPadZero) { unsigned int plane; for (plane = inpamP->depth; plane < outpamP->depth; ++plane) { unsigned int col; for (col = 0; col < inpamP->width; ++col) outrow[col][plane] = 0; } } } typedef struct { /*---------------------------------------------------------------------------- Parameters and state for placing a row of a particular input image in the output in a left-right concatenation. -----------------------------------------------------------------------------*/ tuple * cachedRow; /* Contents of the current row of the image, with depth and maxval adjusted for output, in malloc'ed space belonging to this object. Input file is positioned past this row. Null if data not present and input file is positioned to the current row. */ tuple * out; /* Point in output row buffer where the row from this image goes */ tuple background; unsigned int padtop; /* Number of rows of padding that go above this image in the output */ } LrImgCtl; static void createLrImgCtlArray(const struct pam * const inpam, /* array */ unsigned int const fileCt, tuple * const newTuplerow, const struct pam * const outpamP, enum Justification const justification, enum PadColorMethod const padColorMethod, LrImgCtl ** const imgCtlP) { LrImgCtl * imgCtl; /* array */ unsigned int fileSeq; MALLOCARRAY_NOFAIL(imgCtl, fileCt); for (fileSeq = 0; fileSeq < fileCt; ++fileSeq) { LrImgCtl * const thisEntryP = &imgCtl[fileSeq]; const struct pam * const inpamP = &inpam[fileSeq]; switch (justification) { /* Determine top padding */ case JUST_MIN: thisEntryP->padtop = 0; break; case JUST_MAX: thisEntryP->padtop = outpamP->height - inpamP->height; break; case JUST_CENTER: thisEntryP->padtop = (outpamP->height - inpamP->height) / 2; break; } thisEntryP->out = (fileSeq == 0 ? &newTuplerow[0] : imgCtl[fileSeq-1].out + inpam[fileSeq-1].width); if (inpamP->height == outpamP->height) { /* no vertical padding */ thisEntryP->cachedRow = NULL; pnm_createBlackTuple(outpamP, &thisEntryP->background); /* Meaningless because no padding */ } else { /* Determine pad color */ switch (padColorMethod){ case PAD_AUTO: thisEntryP->cachedRow = pnm_allocpamrow(inpamP); pnm_readpamrow(inpamP, thisEntryP->cachedRow); pnm_scaletuplerow(inpamP, thisEntryP->cachedRow, thisEntryP->cachedRow, outpamP->maxval); padPlanesRow(inpamP, thisEntryP->cachedRow, outpamP); { struct pam cachedRowPam; cachedRowPam = *outpamP; cachedRowPam.width = inpamP->width; thisEntryP->background = pnm_backgroundtuplerow( &cachedRowPam, thisEntryP->cachedRow); } break; case PAD_BLACK: thisEntryP->cachedRow = NULL; pnm_createBlackTuple(outpamP, &thisEntryP->background); break; case PAD_WHITE: thisEntryP->cachedRow = NULL; pnm_createWhiteTuple(outpamP, &thisEntryP->background); break; } } if (outpamP->visual) { /* Any opacity sample in background color tuple is meaningless at this point; make it opaque. */ if (outpamP->have_opacity) { thisEntryP->background[outpamP->opacity_plane] = outpamP->maxval; } } } *imgCtlP = imgCtl; } static void destroyLrImgCtlArray(LrImgCtl * const imgCtl, /* array */ unsigned int const fileCt) { unsigned int fileSeq; for (fileSeq = 0; fileSeq < fileCt; ++fileSeq) { LrImgCtl * const thisEntryP = &imgCtl[fileSeq]; pnm_freepamtuple(thisEntryP->background); pnm_freepamrow(thisEntryP->cachedRow); } free(imgCtl); } static void concatenateLeftRightGen(const struct pam * const outpamP, const struct pam * const inpam, /* array */ unsigned int const fileCt, enum Justification const justification, enum PadColorMethod const padColorMethod) { tuple * const outrow = pnm_allocpamrow(outpamP); LrImgCtl * imgCtl; unsigned int row; createLrImgCtlArray(inpam, fileCt, outrow, outpamP, justification, padColorMethod, &imgCtl); for (row = 0; row < outpamP->height; ++row) { unsigned int fileSeq; for (fileSeq = 0; fileSeq < fileCt; ++fileSeq) { LrImgCtl * const thisEntryP = &imgCtl[fileSeq]; const struct pam * const inpamP = &inpam[fileSeq]; if ((row == 0 && thisEntryP->padtop > 0) || row == thisEntryP->padtop + inpamP->height) { /* This row begins a run of padding, either above or below image 'fileSeq', so set its part of outrow[] to padding. */ unsigned int col; for (col = 0; col < inpamP->width; ++col) { pnm_assigntuple(outpamP, thisEntryP->out[col], thisEntryP->background); } } if (row == thisEntryP->padtop && thisEntryP->cachedRow) { /* We're at the top row of image 'fileSeq', and that row has already been read to cachedRow[] to determine background. Copy it to image fileseq's part of outrow[]. */ unsigned int col; for (col = 0; col < inpamP->width; ++col) { pnm_assigntuple(outpamP, thisEntryP->out[col], thisEntryP->cachedRow[col]); } free(thisEntryP->cachedRow); thisEntryP->cachedRow = NULL; } else if (row >= thisEntryP->padtop && row < thisEntryP->padtop + inpamP->height) { pnm_readpamrow(inpamP, thisEntryP->out); pnm_scaletuplerow(inpamP, thisEntryP->out, thisEntryP->out, outpamP->maxval); padPlanesRow(inpamP, thisEntryP->out, outpamP); } else { /* It's a row of padding, so image filesSeq's part of outrow[] is already set appropriately. */ } } /* Note that imgCtl[N].out[] is an alias to part of outrow[], so outrow[] has been set. */ pnm_writepamrow(outpamP, outrow); } destroyLrImgCtlArray(imgCtl, fileCt); pnm_freepamrow(outrow); } static tuple initialBackgroundColor(const struct pam * const outpamP, enum PadColorMethod const padColorMethod) { tuple retval; switch (padColorMethod) { case PAD_AUTO: /* Background is different for each input image */ retval = pnm_allocpamtuple(outpamP); /* Dummy value; just need something to free */ break; case PAD_BLACK: pnm_createBlackTuple(outpamP, &retval); break; case PAD_WHITE: pnm_createWhiteTuple(outpamP, &retval); break; } if (outpamP->visual) { /* Any opacity sample in background color tuple is meaningless at this point; make it opaque. */ if (outpamP->have_opacity) retval[outpamP->opacity_plane] = outpamP->maxval; } return retval; } static unsigned int leftPadAmount(const struct pam * const outpamP, const struct pam * const inpamP, enum Justification const justification) { switch (justification) { case JUST_MIN: return 0; case JUST_MAX: return outpamP->width - inpamP->width; case JUST_CENTER: return (outpamP->width - inpamP->width) / 2; } assert(false); } static void setHorizPadding(tuple * const newTuplerow, const struct pam * const outpamP, bool const backChanged, const struct pam * const inpam, /* array */ unsigned int const imageSeq, unsigned int const padLeft, tuple const background) { /*---------------------------------------------------------------------------- Set the left and right padding for an output row in a top-bottom concatenation. 'newTuplerow' is where we set the padding (and also assumed to hold the contents of the previous output row). *outpamP describes it. 'backChanged' means the background color is different for the current row from that of the previous row. inpam[] is the array of descriptors for all the input images. 'imageSeq' is the index into this array for the current image. 'background' is the background color to set. -----------------------------------------------------------------------------*/ if (backChanged || (imageSeq > 0 && inpam[imageSeq-1].width > inpam[imageSeq].width)) { unsigned int col; for (col = 0; col < padLeft; ++col) pnm_assigntuple(outpamP, newTuplerow[col], background); for (col = padLeft + inpam[imageSeq].width; col < outpamP->width; ++col) { pnm_assigntuple(outpamP, newTuplerow[col], background); } } else { /* No need to pad because newTuplerow[] already contains the correct padding from the previous row. */ } } static void readFirstTBRowAndDetermineBackground(const struct pam * const inpamP, const struct pam * const outpamP, tuple * const out, tuple * const backgroundP) { /*---------------------------------------------------------------------------- Read the first row of an input image into 'out', adjusting it to conform to the output depth and maxval described by *outpamP. The image is positioned to the first row at entry. From this row, determine the background color for the input image and return it as *backgroundP (a newly malloced tuple). -----------------------------------------------------------------------------*/ pnm_readpamrow(inpamP, out); pnm_scaletuplerow(inpamP, out, out, outpamP->maxval); padPlanesRow(inpamP, out, outpamP); { struct pam partialOutpam; /* Descriptor for the input image with depth and maxval adjusted to that of the output image. */ tuple background; partialOutpam = *outpamP; partialOutpam.width = inpamP->width; background = pnm_backgroundtuplerow(&partialOutpam, out); if (outpamP->visual) { /* Make the background opaque. */ if (outpamP->have_opacity) background[outpamP->opacity_plane] = outpamP->maxval; } *backgroundP = background; } } static void concatenateTopBottomGen(const struct pam * const outpamP, const struct pam * const inpam, /* array */ unsigned int const fileCt, enum Justification const justification, enum PadColorMethod const padColorMethod) { tuple * const newTuplerow = pnm_allocpamrow(outpamP); tuple * out; /* The location in newTuplerow[] that the row from the current input image goes. */ unsigned int fileSeq; tuple background; tuple backgroundPrev; background = initialBackgroundColor(outpamP, padColorMethod); for (fileSeq = 0; fileSeq < fileCt; ++fileSeq) { const struct pam * const inpamP = &inpam[fileSeq]; unsigned int row; unsigned int startRow; bool backChanged; /* The background color is different from that of the previous input image. */ if (inpamP->width == outpamP->width) { /* no padding */ startRow = 0; backChanged = false; out = &newTuplerow[0]; } else { unsigned int const padLeft = leftPadAmount(outpamP, inpamP, justification); if (padColorMethod == PAD_AUTO) { out = &newTuplerow[padLeft]; backgroundPrev = background; readFirstTBRowAndDetermineBackground( inpamP, outpamP, out, &background); backChanged = fileSeq == 0 || pnm_tupleequal(outpamP, background, backgroundPrev); pnm_freepamtuple(backgroundPrev); startRow = 1; } else { /* Background color is constant: black or white */ startRow = 0; out = &newTuplerow[padLeft]; backChanged = (fileSeq == 0); } setHorizPadding(newTuplerow, outpamP, backChanged, inpam, fileSeq, padLeft, background); } if (startRow == 1) /* Top row was already read for auto background color determination. Write it out. */ pnm_writepamrow(outpamP, newTuplerow); for (row = startRow; row < inpamP->height; ++row) { pnm_readpamrow(inpamP, out); pnm_scaletuplerow(inpamP, out, out, outpamP->maxval); padPlanesRow(inpamP, out, outpamP); pnm_writepamrow(outpamP, newTuplerow); } } pnm_freepamtuple(background); pnm_freepamrow(newTuplerow); } int main(int argc, const char ** argv) { struct CmdlineInfo cmdline; struct pam * inpam; /* malloc'ed array */ struct pam outpam; unsigned int i; pm_proginit(&argc, argv); parseCommandLine(argc, argv, &cmdline); MALLOCARRAY_NOFAIL(inpam, cmdline.fileCt); for (i = 0; i < cmdline.fileCt; ++i) { FILE * const ifP = pm_openr(cmdline.inputFileName[i]); inpam[i].comment_p = NULL; /* Don't want to see the comments */ pnm_readpaminit(ifP, &inpam[i], PAM_STRUCT_SIZE(opacity_plane)); } computeOutputParms(cmdline.fileCt, cmdline.orientation, inpam, cmdline.verbose, &outpam); outpam.file = stdout; for (i = 0; i < cmdline.fileCt; ++i) pnm_setminallocationdepth(&inpam[i], outpam.depth); pnm_writepaminit(&outpam); if (outpam.format == RPBM_FORMAT) { switch (cmdline.orientation) { case LEFTRIGHT: concatenateLeftRightPbm(&outpam, inpam, cmdline.fileCt, cmdline.justification, cmdline.padColorMethod); break; case TOPBOTTOM: concatenateTopBottomPbm(&outpam, inpam, cmdline.fileCt, cmdline.justification, cmdline.padColorMethod); break; } } else { switch (cmdline.orientation) { case LEFTRIGHT: concatenateLeftRightGen(&outpam, inpam, cmdline.fileCt, cmdline.justification, cmdline.padColorMethod); break; case TOPBOTTOM: concatenateTopBottomGen(&outpam, inpam, cmdline.fileCt, cmdline.justification, cmdline.padColorMethod); break; } } for (i = 0; i < cmdline.fileCt; ++i) pm_close(inpam[i].file); free(cmdline.inputFileName); free(inpam); pm_close(stdout); return 0; }