1 /* pnmscale.c - read a portable anymap and scale it
2 **
3 ** Copyright (C) 1989, 1991 by Jef Poskanzer.
4 **
5 ** Permission to use, copy, modify, and distribute this software and its
6 ** documentation for any purpose and without fee is hereby granted, provided
7 ** that the above copyright notice appear in all copies and that both that
8 ** copyright notice and this permission notice appear in supporting
9 ** documentation.  This software is provided "as is" without express or
10 ** implied warranty.
11 **
12 ** Modified:
13 **
14 ** June 6, 2001: Christopher W. Boyd <cboyd@pobox.com>
15 **               - added -reduce N to allow scaling by integer value
16 **                 in this case, scale_comp becomes 1/N and x/yscale
17 **                 get set as they should
18 **
19 **
20 */
21 
22 #include <math.h>
23 
24 #include "pm_c_util.h"
25 #include "mallocvar.h"
26 #include "shhopt.h"
27 #include "pnm.h"
28 
29 /* The pnm library allows us to code this program without branching cases
30    for PGM and PPM, but we do the branch anyway to speed up processing of
31    PGM images.
32 */
33 
34 /* We do all our arithmetic in integers.  In order not to get killed by the
35    rounding, we scale every number up by the factor SCALE, do the
36    arithmetic, then scale it back down.
37    */
38 #define SCALE 4096
39 #define HALFSCALE 2048
40 
41 
42 struct cmdline_info {
43     /* All the information the user supplied in the command line,
44        in a form easy for the program to use.
45     */
46     const char *input_filespec;  /* Filespecs of input files */
47     unsigned int xsize;
48     unsigned int ysize;
49     float xscale;
50     float yscale;
51     unsigned int xbox;
52     unsigned int ybox;
53     unsigned int pixels;
54     unsigned int verbose;
55 };
56 
57 
58 static void
parse_command_line(int argc,char ** argv,struct cmdline_info * cmdline_p)59 parse_command_line(int argc, char ** argv,
60                    struct cmdline_info *cmdline_p) {
61 /*----------------------------------------------------------------------------
62    Note that the file spec array we return is stored in the storage that
63    was passed to us as the argv array.
64 -----------------------------------------------------------------------------*/
65     optEntry * option_def;
66         /* Instructions to OptParseOptions3 on how to parse our options.
67          */
68     optStruct3 opt;
69 
70     unsigned int option_def_index;
71     int xysize, xsize, ysize, pixels;
72     int reduce;
73     float xscale, yscale, scale_parm;
74 
75     MALLOCARRAY_NOFAIL(option_def, 100);
76 
77     option_def_index = 0;   /* incremented by OPTENTRY */
78     OPTENT3(0,   "xsize",     OPT_UINT,    &xsize,              NULL, 0);
79     OPTENT3(0,   "width",     OPT_UINT,    &xsize,              NULL, 0);
80     OPTENT3(0,   "ysize",     OPT_UINT,    &ysize,              NULL, 0);
81     OPTENT3(0,   "height",    OPT_UINT,    &ysize,              NULL, 0);
82     OPTENT3(0,   "xscale",    OPT_FLOAT,   &xscale,             NULL, 0);
83     OPTENT3(0,   "yscale",    OPT_FLOAT,   &yscale,             NULL, 0);
84     OPTENT3(0,   "pixels",    OPT_UINT,    &pixels,             NULL, 0);
85     OPTENT3(0,   "xysize",    OPT_FLAG,    &xysize,             NULL, 0);
86     OPTENT3(0,   "verbose",   OPT_FLAG,    &cmdline_p->verbose, NULL, 0);
87     OPTENT3(0,   "reduce",    OPT_UINT,    &reduce,             NULL, 0);
88 
89     /* Set the defaults. -1 = unspecified */
90     xsize = -1;
91     ysize = -1;
92     xscale = -1.0;
93     yscale = -1.0;
94     pixels = -1;
95     xysize = 0;
96     reduce = -1;
97     cmdline_p->verbose = FALSE;
98 
99     opt.opt_table = option_def;
100     opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
101     opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
102 
103     pm_optParseOptions3(&argc, argv, opt, sizeof(opt), 0);
104         /* Uses and sets argc, argv, and some of *cmdline_p and others. */
105 
106     if (xsize == 0)
107         pm_error("-xsize/width must be greater than zero.");
108     if (ysize == 0)
109         pm_error("-ysize/height must be greater than zero.");
110     if (xscale != -1.0 && xscale <= 0.0)
111         pm_error("-xscale must be greater than zero.");
112     if (yscale != -1.0 && yscale <= 0.0)
113         pm_error("-yscale must be greater than zero.");
114     if (reduce <= 0 && reduce != -1)
115         pm_error("-reduce must be greater than zero.");
116 
117     if (xsize != -1 && xscale != -1)
118         pm_error("Cannot specify both -xsize/width and -xscale.");
119     if (ysize != -1 && yscale != -1)
120         pm_error("Cannot specify both -ysize/height and -yscale.");
121 
122     if (xysize &&
123         (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 ||
124          reduce != -1 || pixels != -1) )
125         pm_error("Cannot specify -xysize with other dimension options.");
126     if (pixels != -1 &&
127         (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 ||
128          reduce != -1) )
129         pm_error("Cannot specify -pixels with other dimension options.");
130     if (reduce != -1 &&
131         (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1) )
132         pm_error("Cannot specify -reduce with other dimension options.");
133 
134     if (pixels == 0)
135         pm_error("-pixels must be greater than zero");
136 
137     /* Get the program parameters */
138 
139     if (xysize) {
140         /* parameters are xbox, ybox, and optional filespec */
141         scale_parm = 0.0;
142         if (argc-1 < 2)
143             pm_error("You must supply at least two parameters with -xysize:\n "
144                      "x and y dimensions of the bounding box.");
145         else if (argc-1 > 3)
146             pm_error("Too many arguments.  With -xysize, you need 2 or 3 "
147                      "arguments.");
148         else {
149             cmdline_p->xbox = atoi(argv[1]);
150             cmdline_p->ybox = atoi(argv[2]);
151 
152             if (argc-1 < 3)
153                 cmdline_p->input_filespec = "-";
154             else
155                 cmdline_p->input_filespec = argv[3];
156         }
157     } else {
158         cmdline_p->xbox = 0;
159         cmdline_p->ybox = 0;
160 
161         if (xsize == -1 && xscale == -1 && ysize == -1 && yscale == -1
162             && pixels == -1 && reduce == -1) {
163             /* parameters are scale factor and optional filespec */
164             if (argc-1 < 1)
165                 pm_error("With no dimension options, you must supply at least "
166                          "one parameter: \nthe scale factor.");
167             else {
168                 scale_parm = atof(argv[1]);
169 
170                 if (scale_parm == 0.0)
171                     pm_error("The scale parameter %s is not "
172                              "a positive number.",
173                              argv[1]);
174                 else {
175                     if (argc-1 < 2)
176                         cmdline_p->input_filespec = "-";
177                     else
178                         cmdline_p->input_filespec = argv[2];
179                 }
180             }
181         } else {
182             /* Only parameter allowed is optional filespec */
183             if (argc-1 < 1)
184                 cmdline_p->input_filespec = "-";
185             else
186                 cmdline_p->input_filespec = argv[1];
187 
188             if (reduce != -1) {
189                 scale_parm = ((double) 1.0) / ((double) reduce);
190                 pm_message("reducing by %d gives scale factor of %f.",
191                            reduce, scale_parm);
192             } else
193                 scale_parm = 0.0;
194         }
195     }
196 
197     cmdline_p->xsize = xsize == -1 ? 0 : xsize;
198     cmdline_p->ysize = ysize == -1 ? 0 : ysize;
199     cmdline_p->pixels = pixels == -1 ? 0 : pixels;
200 
201     if (scale_parm) {
202         cmdline_p->xscale = scale_parm;
203         cmdline_p->yscale = scale_parm;
204     } else {
205         cmdline_p->xscale = xscale == -1.0 ? 0.0 : xscale;
206         cmdline_p->yscale = yscale == -1.0 ? 0.0 : yscale;
207     }
208 }
209 
210 
211 
212 static void
compute_output_dimensions(const struct cmdline_info cmdline,const int rows,const int cols,int * newrowsP,int * newcolsP)213 compute_output_dimensions(const struct cmdline_info cmdline,
214                           const int rows, const int cols,
215                           int * newrowsP, int * newcolsP) {
216 
217     if (cmdline.pixels) {
218         if (rows * cols <= cmdline.pixels) {
219             *newrowsP = rows;
220             *newcolsP = cols;
221         } else {
222             const double scale =
223                 sqrt( (float) cmdline.pixels / ((float) cols * (float) rows));
224             *newrowsP = rows * scale;
225             *newcolsP = cols * scale;
226         }
227     } else if (cmdline.xbox) {
228         const double aspect_ratio = (float) cols / (float) rows;
229         const double box_aspect_ratio =
230             (float) cmdline.xbox / (float) cmdline.ybox;
231 
232         if (box_aspect_ratio > aspect_ratio) {
233             *newrowsP = cmdline.ybox;
234             *newcolsP = *newrowsP * aspect_ratio + 0.5;
235         } else {
236             *newcolsP = cmdline.xbox;
237             *newrowsP = *newcolsP / aspect_ratio + 0.5;
238         }
239     } else {
240         if (cmdline.xsize)
241             *newcolsP = cmdline.xsize;
242         else if (cmdline.xscale)
243             *newcolsP = cmdline.xscale * cols + .5;
244         else if (cmdline.ysize)
245             *newcolsP = cols * ((float) cmdline.ysize/rows) +.5;
246         else
247             *newcolsP = cols;
248 
249         if (cmdline.ysize)
250             *newrowsP = cmdline.ysize;
251         else if (cmdline.yscale)
252             *newrowsP = cmdline.yscale * rows +.5;
253         else if (cmdline.xsize)
254             *newrowsP = rows * ((float) cmdline.xsize/cols) +.5;
255         else
256             *newrowsP = rows;
257     }
258 
259     /* If the calculations above yielded (because of rounding) a zero
260        dimension, we fudge it up to 1.  We do this rather than considering
261        it a specification error (and dying) because it's friendlier to
262        automated processes that work on arbitrary input.  It saves them
263        having to check their numbers to avoid catastrophe.
264     */
265 
266     if (*newcolsP < 1) *newcolsP = 1;
267     if (*newrowsP < 1) *newrowsP = 1;
268 }
269 
270 
271 
272 static void
horizontal_scale(const xel inputxelrow[],xel newxelrow[],const int cols,const int newcols,const long sxscale,const int format,const xelval maxval,int * stretchP)273 horizontal_scale(const xel inputxelrow[], xel newxelrow[],
274                  const int cols, const int newcols, const long sxscale,
275                  const int format, const xelval maxval,
276                  int * stretchP) {
277 /*----------------------------------------------------------------------------
278    Take the input row inputxelrow[], which is 'cols' columns wide, and
279    scale it by a factor of 'sxcale', which is in SCALEths to create
280    the output row newxelrow[], which is 'newcols' columns wide.
281 
282    'format' and 'maxval' describe the Netpbm format of the both input and
283    output rows.
284 
285    *stretchP is the number of columns (could be fractional) on the right
286    that we had to fill by stretching because of rounding problems.
287 -----------------------------------------------------------------------------*/
288     long r, g, b;
289     long fraccoltofill, fraccolleft;
290     unsigned int col;
291     unsigned int newcol;
292 
293     newcol = 0;
294     fraccoltofill = SCALE;  /* Output column is "empty" now */
295     r = g = b = 0;          /* initial value */
296     for (col = 0; col < cols; ++col) {
297         /* Process one pixel from input ('inputxelrow') */
298         fraccolleft = sxscale;
299         /* Output all columns, if any, that can be filled using information
300            from this input column, in addition what's already in the output
301            column.
302         */
303         while (fraccolleft >= fraccoltofill) {
304             /* Generate one output pixel in 'newxelrow'.  It will consist
305                of anything accumulated from prior input pixels in 'r','g',
306                and 'b', plus a fraction of the current input pixel.
307             */
308             switch (PNM_FORMAT_TYPE(format)) {
309             case PPM_TYPE:
310                 r += fraccoltofill * PPM_GETR(inputxelrow[col]);
311                 g += fraccoltofill * PPM_GETG(inputxelrow[col]);
312                 b += fraccoltofill * PPM_GETB(inputxelrow[col]);
313                 r /= SCALE;
314                 if ( r > maxval ) r = maxval;
315                 g /= SCALE;
316                 if ( g > maxval ) g = maxval;
317                 b /= SCALE;
318                 if ( b > maxval ) b = maxval;
319                 PPM_ASSIGN( newxelrow[newcol], r, g, b );
320                 break;
321 
322             default:
323                 g += fraccoltofill * PNM_GET1(inputxelrow[col]);
324                 g /= SCALE;
325                 if ( g > maxval ) g = maxval;
326                 PNM_ASSIGN1( newxelrow[newcol], g );
327                 break;
328             }
329             fraccolleft -= fraccoltofill;
330             /* Set up to start filling next output column */
331             newcol++;
332             fraccoltofill = SCALE;
333             r = g = b = 0;
334         }
335         /* There's not enough left in the current input pixel to fill up
336            a whole output column, so just accumulate the remainder of the
337            pixel into the current output column.
338         */
339         if (fraccolleft > 0) {
340             switch (PNM_FORMAT_TYPE(format)) {
341             case PPM_TYPE:
342                 r += fraccolleft * PPM_GETR(inputxelrow[col]);
343                 g += fraccolleft * PPM_GETG(inputxelrow[col]);
344                 b += fraccolleft * PPM_GETB(inputxelrow[col]);
345                 break;
346 
347             default:
348                 g += fraccolleft * PNM_GET1(inputxelrow[col]);
349                 break;
350             }
351             fraccoltofill -= fraccolleft;
352         }
353     }
354 
355     *stretchP = 0;   /* initial value */
356     while (newcol < newcols) {
357         /* We ran out of input columns before we filled up the output
358            columns.  This would be because of rounding down.  For small
359            images, we're probably missing only a tiny fraction of a column,
360            but for large images, it could be multiple columns.
361 
362            So we fake the remaining output columns by copying the rightmost
363            legitimate pixel.  We call this stretching.
364            */
365 
366         *stretchP += fraccoltofill;
367 
368         switch (PNM_FORMAT_TYPE(format)) {
369         case PPM_TYPE:
370             r += fraccoltofill * PPM_GETR(inputxelrow[cols-1]);
371             g += fraccoltofill * PPM_GETG(inputxelrow[cols-1]);
372             b += fraccoltofill * PPM_GETB(inputxelrow[cols-1]);
373 
374             r += HALFSCALE;  /* for rounding */
375             r /= SCALE;
376             if ( r > maxval ) r = maxval;
377             g += HALFSCALE;  /* for rounding */
378             g /= SCALE;
379             if ( g > maxval ) g = maxval;
380             b += HALFSCALE;  /* for rounding */
381             b /= SCALE;
382             if ( b > maxval ) b = maxval;
383             PPM_ASSIGN(newxelrow[newcol], r, g, b );
384             break;
385 
386         default:
387             g += fraccoltofill * PNM_GET1(inputxelrow[cols-1]);
388             g += HALFSCALE;  /* for rounding */
389             g /= SCALE;
390             if ( g > maxval ) g = maxval;
391             PNM_ASSIGN1(newxelrow[newcol], g );
392             break;
393         }
394         newcol++;
395         fraccoltofill = SCALE;
396     }
397 }
398 
399 
400 int
main(int argc,char ** argv)401 main(int argc, char **argv ) {
402 
403     struct cmdline_info cmdline;
404     FILE* ifp;
405     xel* xelrow;
406     xel* tempxelrow;
407     xel* newxelrow;
408     xel* xP;
409     xel* nxP;
410     int rows, cols, format, newformat, rowsread, newrows, newcols;
411     int row, col, needtoreadrow;
412     xelval maxval, newmaxval;
413     long sxscale, syscale;
414     long fracrowtofill, fracrowleft;
415     long* rs;
416     long* gs;
417     long* bs;
418     int vertical_stretch;
419         /* The number of rows we had to fill by stretching because of
420            rounding error, which made us run out of input rows before we
421            had filled up the output rows.
422            */
423 
424     pnm_init( &argc, argv );
425 
426     parse_command_line(argc, argv, &cmdline);
427 
428     ifp = pm_openr(cmdline.input_filespec);
429 
430     pnm_readpnminit( ifp, &cols, &rows, &maxval, &format );
431 
432     /* Promote PBM files to PGM. */
433     if ( PNM_FORMAT_TYPE(format) == PBM_TYPE ) {
434         newformat = PGM_TYPE;
435         newmaxval = PGM_MAXMAXVAL;
436         pm_message( "promoting from PBM to PGM" );
437 	}  else {
438         newformat = format;
439         newmaxval = maxval;
440     }
441     compute_output_dimensions(cmdline, rows, cols, &newrows, &newcols);
442 
443     /* We round the scale factor down so that we never fill up the
444        output while (a fractional pixel of) input remains unused.
445        Instead, we will run out of input while some of the output is
446        unfilled.  We can address that by stretching, whereas the other
447        case would require throwing away some of the input.
448     */
449     sxscale = SCALE * newcols / cols;
450     syscale = SCALE * newrows / rows;
451 
452     if (cmdline.verbose) {
453         pm_message("Scaling by %ld/%d = %f horizontally to %d columns.",
454                    sxscale, SCALE, (float) sxscale/SCALE, newcols );
455         pm_message("Scaling by %ld/%d = %f vertically to %d rows.",
456                    syscale, SCALE, (float) syscale/SCALE, newrows);
457     }
458 
459     xelrow = pnm_allocrow(cols);
460     if (newrows == rows)	/* shortcut Y scaling if possible */
461         tempxelrow = xelrow;
462     else
463         tempxelrow = pnm_allocrow( cols );
464     rs = (long*) pm_allocrow( cols, sizeof(long) );
465     gs = (long*) pm_allocrow( cols, sizeof(long) );
466     bs = (long*) pm_allocrow( cols, sizeof(long) );
467     rowsread = 0;
468     fracrowleft = syscale;
469     needtoreadrow = 1;
470     for ( col = 0; col < cols; ++col )
471 	rs[col] = gs[col] = bs[col] = HALFSCALE;
472     fracrowtofill = SCALE;
473     vertical_stretch = 0;
474 
475     pnm_writepnminit( stdout, newcols, newrows, newmaxval, newformat, 0 );
476     newxelrow = pnm_allocrow( newcols );
477 
478     for ( row = 0; row < newrows; ++row ) {
479         /* First scale vertically from xelrow into tempxelrow. */
480         if ( newrows == rows ) { /* shortcut vertical scaling if possible */
481             pnm_readpnmrow( ifp, xelrow, cols, newmaxval, format );
482 	    } else {
483             while ( fracrowleft < fracrowtofill ) {
484                 if ( needtoreadrow )
485                     if ( rowsread < rows ) {
486                         pnm_readpnmrow( ifp, xelrow, cols, newmaxval, format );
487                         ++rowsread;
488                     }
489                 switch ( PNM_FORMAT_TYPE(format) ) {
490                 case PPM_TYPE:
491                     for ( col = 0, xP = xelrow; col < cols; ++col, ++xP ) {
492                         rs[col] += fracrowleft * PPM_GETR( *xP );
493                         gs[col] += fracrowleft * PPM_GETG( *xP );
494                         bs[col] += fracrowleft * PPM_GETB( *xP );
495                     }
496                     break;
497 
498                 default:
499                     for ( col = 0, xP = xelrow; col < cols; ++col, ++xP )
500                         gs[col] += fracrowleft * PNM_GET1( *xP );
501                     break;
502                 }
503                 fracrowtofill -= fracrowleft;
504                 fracrowleft = syscale;
505                 needtoreadrow = 1;
506             }
507             /* Now fracrowleft is >= fracrowtofill, so we can produce a row. */
508             if ( needtoreadrow ) {
509                 if ( rowsread < rows ) {
510                     pnm_readpnmrow( ifp, xelrow, cols, newmaxval, format );
511                     ++rowsread;
512                     needtoreadrow = 0;
513                 } else {
514                     /* We need another input row to fill up this output row,
515                        but there aren't any more.  That's because of rounding
516                        down on our scaling arithmetic.  So we go ahead with
517                        the data from the last row we read, which amounts to
518                        stretching out the last output row.
519                     */
520                     vertical_stretch += fracrowtofill;
521                 }
522             }
523             switch ( PNM_FORMAT_TYPE(format) ) {
524             case PPM_TYPE:
525                 for ( col = 0, xP = xelrow, nxP = tempxelrow;
526                       col < cols; ++col, ++xP, ++nxP ) {
527                     register long r, g, b;
528 
529                     r = rs[col] + fracrowtofill * PPM_GETR( *xP );
530                     g = gs[col] + fracrowtofill * PPM_GETG( *xP );
531                     b = bs[col] + fracrowtofill * PPM_GETB( *xP );
532                     r /= SCALE;
533                     if ( r > newmaxval ) r = newmaxval;
534                     g /= SCALE;
535                     if ( g > newmaxval ) g = newmaxval;
536                     b /= SCALE;
537                     if ( b > newmaxval ) b = newmaxval;
538                     PPM_ASSIGN( *nxP, r, g, b );
539                     rs[col] = gs[col] = bs[col] = HALFSCALE;
540                 }
541                 break;
542 
543             default:
544                 for ( col = 0, xP = xelrow, nxP = tempxelrow;
545                       col < cols; ++col, ++xP, ++nxP ) {
546                     register long g;
547 
548                     g = gs[col] + fracrowtofill * PNM_GET1( *xP );
549                     g /= SCALE;
550                     if ( g > newmaxval ) g = newmaxval;
551                     PNM_ASSIGN1( *nxP, g );
552                     gs[col] = HALFSCALE;
553                 }
554                 break;
555             }
556             fracrowleft -= fracrowtofill;
557             if ( fracrowleft == 0 ) {
558                 fracrowleft = syscale;
559                 needtoreadrow = 1;
560             }
561             fracrowtofill = SCALE;
562 	    }
563 
564         /* Now scale tempxelrow horizontally into newxelrow & write it out. */
565 
566         if (newcols == cols)	/* shortcut X scaling if possible */
567             pnm_writepnmrow(stdout, tempxelrow, newcols,
568                             newmaxval, newformat, 0);
569         else {
570             int stretch;
571 
572             horizontal_scale(tempxelrow, newxelrow, cols, newcols, sxscale,
573                              format, newmaxval, &stretch);
574 
575             if (cmdline.verbose && row == 0 && stretch != 0)
576                 pm_message("%d/%d = %f right columns filled by stretching "
577                            "because of arithmetic imprecision",
578                            stretch, SCALE, (float) stretch/SCALE);
579 
580             pnm_writepnmrow(stdout, newxelrow, newcols,
581                             newmaxval, newformat, 0 );
582         }
583 	}
584 
585     if (cmdline.verbose && vertical_stretch != 0)
586         pm_message("%d/%d = %f bottom rows filled by stretching due to "
587                    "arithmetic imprecision",
588                    vertical_stretch, SCALE,
589                    (float) vertical_stretch/SCALE);
590 
591     pm_close( ifp );
592     pm_close( stdout );
593 
594     exit( 0 );
595 }
596