/* 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 _DEFAULT_SOURCE /* New name for SVID & BSD source defines */
#define _XOPEN_SOURCE 500 /* Make sure strdup() is in string.h */
#define _BSD_SOURCE /* Make sure strdup() is in <string.h> */
#include <assert.h>
#include <limits.h>
#include <string.h>
#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);
free(option_def);
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[i+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) {
/*----------------------------------------------------------------------------
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(unsigned int const arg) {
/*----------------------------------------------------------------------------
The prime factors of 'arg'.
-----------------------------------------------------------------------------*/
unsigned int n;
unsigned int i, j;
Factorset retval;
/* Initialize array element to zero */
for (i = 0; i < ARRAY_SIZE(retval.f); ++i)
retval.f[i] = 0;
/* Set array elements starting with the first to the factors */
for (i = 2, j = 0, n = arg; n > 1; ) {
if (n % i == 0) {
retval.f[j++] = i;
n /= i;
} else
++i;
}
return retval;
}
static int
gcf(unsigned int const n,
unsigned int const m) {
/*----------------------------------------------------------------------------
Greatest common factor of 'n' and 'm'
-----------------------------------------------------------------------------*/
Factorset const nFactors = factor(n);
Factorset const mFactors = factor(m);
unsigned int i, j;
unsigned int g;
i = j = 0;
g = 1;
while (nFactors.f[i] && mFactors.f[j]) {
if (nFactors.f[i] == mFactors.f[j]) {
g *= nFactors.f[i];
++i;
++j;
} else if (nFactors.f[i] < mFactors.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 imgCt,
Coord ** const coordsP,
unsigned int const quality,
unsigned int const qfactor) {
Coord * coords; /* malloc'ed array */
unsigned int minarea;
unsigned int i;
unsigned int rdiv;
unsigned int cdiv;
Rectangle * current; /* malloc'ed array */
unsigned int z;
Coord c;
MALLOCARRAY(coords, imgCt);
if (!coords)
pm_error("Out of memory allocating %u-element coords array", imgCt);
z = UINT_MAX; /* initial value */
c.x = 0; c.y = 0; /* initial value */
if (quality > 1) {
unsigned int realMinarea;
for (realMinarea = i = 0; i < imgCt; ++i)
realMinarea += imgs[i].height * imgs[i].width;
minarea = realMinarea * qfactor / 100;
} else
minarea = UINT_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 < imgCt; ++i)
rdiv = gcf(imgs[i].height, rdiv);
for (cdiv = imgs[0].width, i = 1; i < imgCt; ++i)
cdiv = gcf(imgs[i].width, cdiv);
MALLOCARRAY(current, imgCt);
for (i = 0; i < imgCt; ++i) {
current[i].size.x = imgs[i].width;
current[i].size.y = imgs[i].height;
}
recursefindpack(current, c, coords, minarea, &z, 0, imgCt, cdiv, rdiv,
quality, qfactor);
free(current);
*coordsP = coords;
}
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) {
unsigned int i;
for (i = coord.x; i < coord.x + inpamP->width; ++i) {
unsigned int j;
for (j = 0; j < outpamP->depth; ++j)
tuplerow[i][j] *= outpamP->maxval / inpamP->maxval;
}
}
}
static void
makeRowBlack(struct pam * const pamP,
tuple * const tuplerow) {
unsigned int col;
for (col = 0; col < pamP->width; ++col) {
unsigned int plane;
for (plane = 0; plane < pamP->depth; ++plane)
tuplerow[col][plane] = 0;
}
}
static void
writePam(struct pam * const outpamP,
unsigned int const imgCt,
const Coord * const coords,
const struct pam * const imgs) {
/*----------------------------------------------------------------------------
Write the entire composite image. There are 'imgCt' source images,
described by imgs[]. Their placement in the output is coords[].
Properties of the output image, including where to write it
and its dimensions are *outpamP.
-----------------------------------------------------------------------------*/
tuple * tuplerow;
unsigned int row; /* Row number in the output image */
pnm_writepaminit(outpamP);
tuplerow = pnm_allocpamrow(outpamP);
for (row = 0; row < outpamP->height; ++row) {
unsigned int imgIdx;
makeRowBlack(outpamP, tuplerow); /* initial value */
for (imgIdx = 0; imgIdx < imgCt; ++imgIdx) {
const Coord * const imgCoordP = &coords[imgIdx];
const struct pam * const imgPamP = &imgs[imgIdx];
if (imgCoordP->y <= row && row < imgCoordP->y + imgPamP->height) {
pnm_readpamrow(imgPamP, &tuplerow[imgCoordP->x]);
adjustDepth(tuplerow, imgPamP, outpamP, *imgCoordP);
adjustMaxval(tuplerow, imgPamP, outpamP, *imgCoordP);
}
}
pnm_writepamrow(outpamP, tuplerow);
}
pnm_freepamrow(tuplerow);
}
static void
writeData(FILE * const dataFileP,
unsigned int const width,
unsigned int const height,
unsigned int const imgCt,
const char ** const names,
const Coord * const coords,
const struct pam * const imgs) {
unsigned int imgIdx;
fprintf(dataFileP, ":0:0:%u:%u\n", width, height);
for (imgIdx = 0; imgIdx < imgCt; ++imgIdx) {
fprintf(dataFileP, "%s:%u:%u:%u:%u\n", names[imgIdx], coords[imgIdx].x,
coords[imgIdx].y, imgs[imgIdx].width, imgs[imgIdx].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;
}
static unsigned int
qfactorFromQuality(unsigned int const quality,
unsigned int const quality2) {
unsigned int qfactor;
switch (quality2) {
case 0: case 1:
qfactor = quality;
break;
case 2: case 3: case 4: case 5: case 6:
qfactor = 100 * (8 - 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",
quality2);
}
return qfactor;
}
static void
openFiles(struct CmdlineInfo const cmdline,
unsigned int * const fileCtP,
struct pam ** const imgPamP,
const char *** const namesP) {
unsigned int fileCt;
struct pam * imgPam;
const char ** names;
fileCt = cmdline.nFiles > 0 ? cmdline.nFiles : 1;
MALLOCARRAY(imgPam, fileCt);
MALLOCARRAY(names, fileCt);
if (!imgPam || !names)
pm_error("out of memory");
if (cmdline.nFiles > 0) {
unsigned int i;
for (i = 0; i < cmdline.nFiles; ++i) {
imgPam[i].file = pm_openr(cmdline.inFileName[i]);
names[i] = strdup(cmdline.inFileName[i]);
}
} else {
imgPam[0].file = stdin;
names[0] = strdup("stdin");
}
*fileCtP = fileCt;
*imgPamP = imgPam;
*namesP = names;
}
static void
readFileHeaders(struct pam * const imgPam,
unsigned int const fileCt) {
unsigned int i;
for (i = 0; i < fileCt; ++i)
pnm_readpaminit(imgPam[i].file, &imgPam[i],
PAM_STRUCT_SIZE(tuple_type));
}
static void
closeFiles(const struct pam * const imgPam,
unsigned int const fileCt,
FILE * const headerFileP,
FILE * const dataFileP) {
unsigned int i;
for (i = 0; i < fileCt; ++i)
pm_close(imgPam[i].file);
pm_close(stdout);
if (headerFileP)
pm_close(headerFileP);
if (dataFileP)
pm_close(dataFileP);
}
int
main(int argc, const char **argv) {
struct CmdlineInfo cmdline;
struct pam * imgPam; /* malloced */
struct pam outimg;
unsigned int fileCt;
Coord * coords; /* malloced */
FILE * headerFileP;
FILE * dataFileP;
const char ** names; /* malloced */
unsigned int qfactor; /* In per cent */
pm_proginit(&argc, argv);
parseCommandLine(argc, argv, &cmdline);
headerFileP = cmdline.header ? pm_openw(cmdline.header) : NULL;
dataFileP = cmdline.data ? pm_openw(cmdline.data) : NULL;
qfactor = qfactorFromQuality(cmdline.quality, cmdline.quality2);
openFiles(cmdline, &fileCt, &imgPam, &names);
readFileHeaders(imgPam, fileCt);
sortImagesByArea(fileCt, imgPam, names);
findpack(imgPam, fileCt, &coords, cmdline.quality2, qfactor);
computeOutputType(&outimg.maxval, &outimg.format, outimg.tuple_type,
&outimg.depth, fileCt, imgPam);
computeOutputDimensions(&outimg.width, &outimg.height, fileCt,
imgPam, coords);
outimg.size = sizeof(outimg);
outimg.len = PAM_STRUCT_SIZE(allocation_depth);
pnm_setminallocationdepth(&outimg, outimg.depth);
outimg.plainformat = false;
outimg.file = stdout;
writePam(&outimg, fileCt, coords, imgPam);
if (dataFileP)
writeData(dataFileP, outimg.width, outimg.height,
fileCt, names, coords, imgPam);
if (headerFileP)
writeHeader(headerFileP, cmdline.prefix, outimg.width, outimg.height,
fileCt, names, coords, imgPam);
closeFiles(imgPam, fileCt, headerFileP, dataFileP);
free(coords);
free(imgPam);
free(names);
return 0;
}