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