/* pnmmontage.c - build a montage of portable anymaps * * Copyright 2000 Ben Olmstead. * * Permission to use, copy, modify, and distribute this software and its * documentation for any purpose and without fee is hereby granted, provided * that the above copyright notice appear in all copies and that both that * copyright notice and this permission notice appear in supporting * documentation. This software is provided "as is" without express or * implied warranty. */ #define _BSD_SOURCE /* Make sure strdup() is in */ #include #include #include #include "pm_c_util.h" #include "mallocvar.h" #include "nstring.h" #include "shhopt.h" #include "pam.h" struct cmdlineInfo { const char * header; const char * data; const char * prefix; unsigned int quality; unsigned int quality2; unsigned int nFiles; const char ** inFileName; }; static void parseCommandLine(int argc, const char ** argv, struct cmdlineInfo * const cmdlineP) { /*---------------------------------------------------------------------------- parse program command line described in Unix standard form by argc and argv. Return the information in the options as *cmdlineP. If command line is internally inconsistent (invalid options, etc.), issue error message to stderr and abort program. Note that the strings we return are stored in the storage that was passed to us as the argv array. We also trash *argv. -----------------------------------------------------------------------------*/ optEntry * option_def; /* Instructions to OptParseOptions3 on how to parse our options. */ optStruct3 opt; unsigned int dataSpec, headerSpec, prefixSpec, qualitySpec; unsigned int option_def_index; unsigned int i; unsigned int q[10]; MALLOCARRAY_NOFAIL(option_def, 100); option_def_index = 0; /* incremented by OPTENTRY */ OPTENT3( 0, "data", OPT_STRING, &cmdlineP->data, &dataSpec, 0); OPTENT3( 0, "header", OPT_STRING, &cmdlineP->header, &headerSpec, 0); OPTENT3('q', "quality", OPT_UINT, &cmdlineP->quality, &qualitySpec, 0); OPTENT3('p', "prefix", OPT_STRING, &cmdlineP->prefix, &prefixSpec, 0); OPTENT3('0', "0", OPT_FLAG, NULL, &q[0], 0); OPTENT3('1', "1", OPT_FLAG, NULL, &q[1], 0); OPTENT3('2', "2", OPT_FLAG, NULL, &q[2], 0); OPTENT3('3', "3", OPT_FLAG, NULL, &q[3], 0); OPTENT3('4', "4", OPT_FLAG, NULL, &q[4], 0); OPTENT3('5', "5", OPT_FLAG, NULL, &q[5], 0); OPTENT3('6', "6", OPT_FLAG, NULL, &q[6], 0); OPTENT3('7', "7", OPT_FLAG, NULL, &q[7], 0); OPTENT3('8', "8", OPT_FLAG, NULL, &q[8], 0); OPTENT3('9', "9", OPT_FLAG, NULL, &q[9], 0); opt.opt_table = option_def; opt.short_allowed = FALSE; opt.allowNegNum = FALSE; pm_optParseOptions3(&argc, (char**)argv, opt, sizeof(opt), 0); if (!dataSpec) cmdlineP->data = NULL; if (!headerSpec) cmdlineP->header = NULL; if (!prefixSpec) cmdlineP->prefix = ""; if (!qualitySpec) cmdlineP->quality = 200; /* cmdlineP->quality2 is the greatest number from the --1, --2, etc. options, or 5 if none of those are specified. */ cmdlineP->quality2 = 5; /* initial value */ for (i = 0; i < 10; ++i) { if (q[i]) cmdlineP->quality2 = i; } cmdlineP->nFiles = argc-1; MALLOCARRAY_NOFAIL(cmdlineP->inFileName, argc-1); for (i = 0; i < argc-1; ++i) { if (cmdlineP->data && strchr(argv[i+1], ':')) pm_error("Filename '%s' contains a \":\", which is forbidden " "with -data", argv[i+1]); else cmdlineP->inFileName[i] = strdup(argv[1+1]); } } typedef struct { int f[sizeof(int) * 8 + 1]; } factorset; typedef struct { int x; int y; } coord; typedef struct { coord ul; coord size; } rectangle; static coord lr(rectangle const r) { /*---------------------------------------------------------------------------- Return the coordinates of the lower right corner of 'r' (i.e. the pixel just beyond the lowest rightmost one). -----------------------------------------------------------------------------*/ coord retval; retval.x = r.ul.x + r.size.x; retval.y = r.ul.y + r.size.y; return retval; } static factorset factor(int n) { int i, j; factorset f; for (i = 0; i < sizeof(int) * 8 + 1; ++i) f.f[i] = 0; for (i = 2, j = 0; n > 1; ++i) { if (n % i == 0) f.f[j++] = i, n /= i, --i; } return (f); } static int gcd(int n, int m) { factorset nf, mf; int i, j; int g; nf = factor(n); mf = factor(m); i = j = 0; g = 1; while (nf.f[i] && mf.f[j]) { if (nf.f[i] == mf.f[j]) g *= nf.f[i], ++i, ++j; else if (nf.f[i] < mf.f[j]) ++i; else ++j; } return (g); } static bool overlaps(rectangle const a, rectangle const b) { return (a.ul.x < lr(b).x && a.ul.y < lr(b).y) && (lr(a).x > b.ul.x && lr(a).y > b.ul.y); } static bool collides(rectangle const test, const rectangle * const fieldList, unsigned int const n) { /*---------------------------------------------------------------------------- Return true iff the rectangle 'test' overlaps any of the 'n' rectangles fieldList[]. -----------------------------------------------------------------------------*/ unsigned int i; for (i = 0; i < n; ++i) if (overlaps(fieldList[i], test)) return true; return false; } static void recursefindpack(rectangle * const current, coord const currentsz, coord * const best, unsigned int const minarea, unsigned int * const maxareaP, unsigned int const depth, unsigned int const n, unsigned int const xinc, unsigned int const yinc, unsigned int const quality, unsigned int const qfactor) { if (depth == n) { if (currentsz.x * currentsz.y < *maxareaP) { unsigned int i; for (i = 0; i < n; ++i) best[i] = current[i].ul; *maxareaP = currentsz.x * currentsz.y; } } else { unsigned int i; rectangle * const newP = ¤t[depth]; for (i = 0; ; ++i) { for (newP->ul.x = 0, newP->ul.y = i * yinc; newP->ul.y <= i * yinc;) { coord c; c.x = MAX(lr(*newP).x, currentsz.x); c.y = MAX(lr(*newP).y, currentsz.y); pm_message("current = (%u.%u, %u.%u) new = (%u.%u, %u.%u)", current[0].ul.x, current[0].size.x, current[0].ul.y, current[0].size.y, newP->ul.x, newP->size.x, newP->ul.y, newP->size.y); if (!collides(*newP, current, depth)) { pm_message("Depth %u: Doesn't collide at i=%u", depth,i); recursefindpack(current, c, best, minarea, maxareaP, depth + 1, n, xinc, yinc, quality, qfactor); if (*maxareaP <= minarea) return; } if (newP->ul.x == (i - 1) * xinc) newP->ul.y = 0; if (newP->ul.x < i * xinc) newP->ul.x += xinc; else newP->ul.y += yinc; } } } } static void findpack(struct pam * const imgs, unsigned int const n, coord * const coords, unsigned int const quality, unsigned int const qfactor) { int minarea; int i; int rdiv; int cdiv; int minx; int miny; rectangle * current; unsigned int z; coord c; minx = -1; miny = -1; /* initial value */ z = UINT_MAX; /* initial value */ c.x = 0; c.y = 0; /* initial value */ if (quality > 1) { unsigned int realMinarea; for (realMinarea = i = 0; i < n; ++i) realMinarea += imgs[i].height * imgs[i].width, minx = MAX(minx, imgs[i].width), miny = MAX(miny, imgs[i].height); minarea = realMinarea * qfactor / 100; } else { minarea = INT_MAX - 1; } /* It's relatively easy to show that, if all the images * are multiples of a particular size, then a best * packing will always align the images on a grid of * that size. * * This speeds computation immensely. */ for (rdiv = imgs[0].height, i = 1; i < n; ++i) rdiv = gcd(imgs[i].height, rdiv); for (cdiv = imgs[0].width, i = 1; i < n; ++i) cdiv = gcd(imgs[i].width, cdiv); MALLOCARRAY(current, n); for (i = 0; i < n; ++i) { current[i].size.x = imgs[i].width; current[i].size.y = imgs[i].height; } recursefindpack(current, c, coords, minarea, &z, 0, n, cdiv, rdiv, quality, qfactor); } static void adjustDepth(tuple * const tuplerow, const struct pam * const inpamP, const struct pam * const outpamP, coord const coord) { if (inpamP->depth < outpamP->depth) { unsigned int i; for (i = coord.x; i < coord.x + inpamP->width; ++i) { int j; for (j = inpamP->depth; j < outpamP->depth; ++j) tuplerow[i][j] = tuplerow[i][inpamP->depth - 1]; } } } static void adjustMaxval(tuple * const tuplerow, const struct pam * const inpamP, const struct pam * const outpamP, coord const coord) { if (inpamP->maxval < outpamP->maxval) { int i; for (i = coord.x; i < coord.x + inpamP->width; ++i) { int j; for (j = 0; j < outpamP->depth; ++j) tuplerow[i][j] *= outpamP->maxval / inpamP->maxval; } } } static void writePam(struct pam * const outpamP, unsigned int const nfiles, const coord * const coords, const struct pam * const imgs) { tuple *tuplerow; int i; pnm_writepaminit(outpamP); tuplerow = pnm_allocpamrow(outpamP); for (i = 0; i < outpamP->height; ++i) { int j; for (j = 0; j < nfiles; ++j) { if (coords[j].y <= i && i < coords[j].y + imgs[j].height) { pnm_readpamrow(&imgs[j], &tuplerow[coords[j].x]); adjustDepth(tuplerow, &imgs[j], outpamP, coords[j]); adjustMaxval(tuplerow, &imgs[j], outpamP, coords[j]); } } pnm_writepamrow(outpamP, tuplerow); } pnm_freepamrow(tuplerow); } static void writeData(FILE * const dataFileP, unsigned int const width, unsigned int const height, unsigned int const nfiles, const char ** const names, const coord * const coords, const struct pam * const imgs) { unsigned int i; fprintf(dataFileP, ":0:0:%u:%u\n", width, height); for (i = 0; i < nfiles; ++i) { fprintf(dataFileP, "%s:%u:%u:%u:%u\n", names[i], coords[i].x, coords[i].y, imgs[i].width, imgs[i].height); } } static void writeHeader(FILE * const headerFileP, const char * const prefix, unsigned int const width, unsigned int const height, unsigned int const nfiles, const char ** const names, const coord * const coords, const struct pam * imgs) { unsigned int i; fprintf(headerFileP, "#define %sOVERALLX %u\n", prefix, width); fprintf(headerFileP, "#define %sOVERALLY %u\n", prefix, height); fprintf(headerFileP, "\n"); for (i = 0; i < nfiles; ++i) { char * const buffer = strdup(names[i]); coord const coord = coords[i]; struct pam const img = imgs[i]; unsigned int j; *strchr(buffer, '.') = 0; for (j = 0; buffer[j]; ++j) { if (ISLOWER(buffer[j])) buffer[j] = TOUPPER(buffer[j]); } fprintf(headerFileP, "#define %s%sX %u\n", prefix, buffer, coord.x); fprintf(headerFileP, "#define %s%sY %u\n", prefix, buffer, coord.y); fprintf(headerFileP, "#define %s%sSZX %u\n", prefix, buffer, img.width); fprintf(headerFileP, "#define %s%sSZY %u\n", prefix, buffer, img.height); fprintf(headerFileP, "\n"); } } static void sortImagesByArea(unsigned int const nfiles, struct pam * const imgs, const char ** const names) { /*---------------------------------------------------------------------------- Sort the images described by 'imgs' and 'names' in place, from largest area to smallest. -----------------------------------------------------------------------------*/ /* Bubble sort */ unsigned int i; for (i = 0; i < nfiles - 1; ++i) { unsigned int j; for (j = i + 1; j < nfiles; ++j) { if (imgs[j].width * imgs[j].height > imgs[i].width * imgs[i].height) { struct pam p; const char * c; p = imgs[i]; imgs[i] = imgs[j]; imgs[j] = p; c = names[i]; names[i] = names[j]; names[j] = c; } } } } static void computeOutputType(sample * const maxvalP, int * const formatP, char * const tupleTypeP, unsigned int * const depthP, unsigned int const nfiles, const struct pam * const imgs) { unsigned int i; sample maxval; int format; const char * tupleType; unsigned int depth; assert(nfiles > 0); /* initial guesses */ maxval = imgs[0].maxval; format = imgs[0].format; depth = imgs[0].depth; tupleType = imgs[0].tuple_type; for (i = 1; i < nfiles; ++i) { if (PAM_FORMAT_TYPE(imgs[i].format) > PAM_FORMAT_TYPE(format)) { format = imgs[i].format; tupleType = imgs[i].tuple_type; } maxval = MAX(maxval, imgs[i].maxval); depth = MAX(depth, imgs[i].depth); } *maxvalP = maxval; *formatP = format; *depthP = depth; memcpy(tupleTypeP, tupleType, sizeof(imgs[0].tuple_type)); } static void computeOutputDimensions(int * const widthP, int * const heightP, unsigned int const nfiles, const struct pam * const imgs, const coord * const coords) { unsigned int widthGuess, heightGuess; unsigned int i; widthGuess = 0; /* initial value */ heightGuess = 0; /* initial value */ for (i = 0; i < nfiles; ++i) { widthGuess = MAX(widthGuess, imgs[i].width + coords[i].x); heightGuess = MAX(heightGuess, imgs[i].height + coords[i].y); } *widthP = widthGuess; *heightP = heightGuess; } int main(int argc, const char **argv) { struct cmdlineInfo cmdline; struct pam * imgs; struct pam outimg; unsigned int nfiles; coord * coords; FILE * header; FILE * data; const char ** names; unsigned int i; unsigned int qfactor; /* In per cent */ pm_proginit(&argc, argv); parseCommandLine(argc, argv, &cmdline); header = cmdline.header ? pm_openw(cmdline.header) : NULL; data = cmdline.data ? pm_openw(cmdline.data) : NULL; switch (cmdline.quality2) { case 0: case 1: qfactor = cmdline.quality; break; case 2: case 3: case 4: case 5: case 6: qfactor = 100 * (8 - cmdline.quality2); break; case 7: qfactor = 150; break; case 8: qfactor = 125; break; case 9: qfactor = 100; break; default: pm_error("Internal error - impossible value of 'quality2': %u", cmdline.quality2); } nfiles = cmdline.nFiles > 0 ? cmdline.nFiles : 1; MALLOCARRAY(imgs, nfiles); MALLOCARRAY(coords, nfiles); MALLOCARRAY(names, nfiles); if (!imgs || !coords || !names) pm_error("out of memory"); if (cmdline.nFiles > 0) { unsigned int i; for (i = 0; i < cmdline.nFiles; ++i) { imgs[i].file = pm_openr(cmdline.inFileName[i]); names[i] = strdup(cmdline.inFileName[i]); } } else { imgs[0].file = stdin; names[0] = strdup("stdin"); } for (i = 0; i < nfiles; ++i) pnm_readpaminit(imgs[i].file, &imgs[i], PAM_STRUCT_SIZE(tuple_type)); sortImagesByArea(nfiles, imgs, names); findpack(imgs, nfiles, coords, cmdline.quality2, qfactor); computeOutputType(&outimg.maxval, &outimg.format, outimg.tuple_type, &outimg.depth, nfiles, imgs); computeOutputDimensions(&outimg.width, &outimg.height, nfiles, imgs, coords); pnm_setminallocationdepth(&outimg, outimg.depth); outimg.size = sizeof(outimg); outimg.len = sizeof(outimg); outimg.file = stdout; outimg.bytes_per_sample = 0; for (i = outimg.maxval; i; i >>= 8) ++outimg.bytes_per_sample; writePam(&outimg, nfiles, coords, imgs); if (data) writeData(data, outimg.width, outimg.height, nfiles, names, coords, imgs); if (header) writeHeader(header, cmdline.prefix, outimg.width, outimg.height, nfiles, names, coords, imgs); for (i = 0; i < nfiles; ++i) pm_close(imgs[i].file); pm_close(stdout); if (header) pm_close(header); if (data) pm_close(data); return 0; }