diff options
Diffstat (limited to 'editor')
89 files changed, 33747 insertions, 0 deletions
diff --git a/editor/Makefile b/editor/Makefile new file mode 100644 index 00000000..18165666 --- /dev/null +++ b/editor/Makefile @@ -0,0 +1,86 @@ +ifeq ($(SRCDIR)x,x) + SRCDIR = $(CURDIR)/.. + BUILDDIR = $(SRCDIR) +endif +SUBDIR = editor +VPATH=.:$(SRCDIR)/$(SUBDIR) + +include $(BUILDDIR)/Makefile.config + +# We tend to separate out the build targets so that we don't have +# any more dependencies for a given target than it really needs. +# That way, if there is a problem with a dependency, we can still +# successfully build all the stuff that doesn't depend upon it. +# This package is so big, it's useful even when some parts won't +# build. + +PORTBINARIES = pamaddnoise pamcomp pamcut \ + pamdeinterlace pamdice pamditherbw pamedge \ + pamenlarge \ + pamflip pamfunc pammasksharpen pammixinterlace \ + pamoil pamperspective pampop9 \ + pamscale pamstretch pamthreshold \ + pbmclean pbmlife pbmmask pbmpscale pbmreduce \ + pgmabel pgmbentley pgmdeshadow pgmenhance \ + pgmmedian pgmmorphconv \ + pnmalias pnmcat pnmcomp pnmconvol pnmcrop pnmcut \ + pnmgamma \ + pnmhisteq pnmindex pnminvert pnmmontage \ + pnmnlfilt pnmnorm pnmpad pnmpaste \ + pnmremap pnmrotate \ + pnmscale pnmscalefixed pnmshear pnmsmooth pnmstitch pnmtile \ + ppm3d ppmbrighten ppmchange ppmcolormask \ + ppmdim ppmdist ppmdither ppmdraw \ + ppmflash ppmglobe ppmlabel ppmmix \ + ppmntsc ppmrelief ppmshift ppmspread ppmtv + +# We don't include programs that have special library dependencies in the +# merge scheme, because we don't want those dependencies to prevent us +# from building all the other programs. + +NOMERGEBINARIES = +MERGEBINARIES = $(PORTBINARIES) + + +BINARIES = $(MERGEBINARIES) $(NOMERGEBINARIES) +SCRIPTS = pnmflip ppmfade ppmquant ppmquantall ppmshadow \ + pamstretch-gen pnmmargin pnmquant + +OBJECTS = $(BINARIES:%=%.o) + +MERGE_OBJECTS = $(MERGEBINARIES:%=%.o2) + +.PHONY: all +all: $(BINARIES) + +include $(SRCDIR)/Makefile.common + +install.bin: install.bin.local + +.PHONY: install.bin.local +install.bin.local: $(PKGDIR)/bin +# Remember that $(SYMLINK) might just be a copy command. +# backward compatibility: program used to be named pnmnoraw +# backward compatibility: program used to be pnminterp + cd $(PKGDIR)/bin ; \ + rm -f pnminterp; \ + $(SYMLINK) pamstretch$(EXE) pnminterp +# pamoil replaced pgmoil in June 2001. + cd $(PKGDIR)/bin ; \ + rm -f pgmoil ; \ + $(SYMLINK) pamoil$(EXE) pgmoil +# In March 2002, pnmnorm replaced ppmnorm and pgmnorm + cd $(PKGDIR)/bin ; \ + rm -f ppmnorm ; \ + $(SYMLINK) pnmnorm$(EXE) ppmnorm + cd $(PKGDIR)/bin ; \ + rm -f pgmnorm ; \ + $(SYMLINK) pnmnorm$(EXE) pgmnorm +# In March 2003, pamedge replaced pgmedge + cd $(PKGDIR)/bin ; \ + rm -f pgmedge ; \ + $(SYMLINK) pamedge$(EXE) pgmedge +# In October 2004, pamenlarge replaced pnmenlarge + cd $(PKGDIR)/bin ; \ + rm -f pnmenlarge ; \ + $(SYMLINK) pamenlarge$(EXE) pnmenlarge diff --git a/editor/dithers.h b/editor/dithers.h new file mode 100644 index 00000000..24a9fb39 --- /dev/null +++ b/editor/dithers.h @@ -0,0 +1,87 @@ +/* +** dithers.h +** +** Here are some dithering matrices. They are all taken from "Digital +** Halftoning" by Robert Ulichney, MIT Press, ISBN 0-262-21009-6. +*/ + + +#if 0 +/* +** Order-6 ordered dithering matrix. Note that smaller ordered dithers +** have no advantage over larger ones, so use dither8 instead. +*/ +static int const dither6[8][8] = { + { 1, 59, 15, 55, 2, 56, 12, 52 }, + { 33, 17, 47, 31, 34, 18, 44, 28 }, + { 9, 49, 5, 63, 10, 50, 6, 60 }, + { 41, 25, 37, 21, 42, 26, 38, 22 }, + { 3, 57, 13, 53, 0, 58, 14, 54 }, + { 35, 19, 45, 29, 32, 16, 46, 30 }, + { 11, 51, 7, 61, 8, 48, 4, 62 }, + { 43, 27, 39, 23, 40, 24, 36, 20 } + }; +#endif + +/* Order-8 ordered dithering matrix. */ +static int const dither8[16][16] = { + { 1,235, 59,219, 15,231, 55,215, 2,232, 56,216, 12,228, 52,212}, + { 129, 65,187,123,143, 79,183,119,130, 66,184,120,140, 76,180,116}, + { 33,193, 17,251, 47,207, 31,247, 34,194, 18,248, 44,204, 28,244}, + { 161, 97,145, 81,175,111,159, 95,162, 98,146, 82,172,108,156, 92}, + { 9,225, 49,209, 5,239, 63,223, 10,226, 50,210, 6,236, 60,220}, + { 137, 73,177,113,133, 69,191,127,138, 74,178,114,134, 70,188,124}, + { 41,201, 25,241, 37,197, 21,255, 42,202, 26,242, 38,198, 22,252}, + { 169,105,153, 89,165,101,149, 85,170,106,154, 90,166,102,150, 86}, + { 3,233, 57,217, 13,229, 53,213, 0,234, 58,218, 14,230, 54,214}, + { 131, 67,185,121,141, 77,181,117,128, 64,186,122,142, 78,182,118}, + { 35,195, 19,249, 45,205, 29,245, 32,192, 16,250, 46,206, 30,246}, + { 163, 99,147, 83,173,109,157, 93,160, 96,144, 80,174,110,158, 94}, + { 11,227, 51,211, 7,237, 61,221, 8,224, 48,208, 4,238, 62,222}, + { 139, 75,179,115,135, 71,189,125,136, 72,176,112,132, 68,190,126}, + { 43,203, 27,243, 39,199, 23,253, 40,200, 24,240, 36,196, 20,254}, + { 171,107,155, 91,167,103,151, 87,168,104,152, 88,164,100,148, 84} +}; + +/* Order-3 clustered dithering matrix. */ +static int const cluster3[6][6] = { + { 9,11,10, 8, 6, 7}, + { 12,17,16, 5, 0, 1}, + { 13,14,15, 4, 3, 2}, + { 8, 6, 7, 9,11,10}, + { 5, 0, 1,12,17,16}, + { 4, 3, 2,13,14,15} +}; + +/* Order-4 clustered dithering matrix. */ +static int const cluster4[8][8] = { + { 18,20,19,16,13,11,12,15}, + { 27,28,29,22, 4, 3, 2, 9}, + { 26,31,30,21, 5, 0, 1,10}, + { 23,25,24,17, 8, 6, 7,14}, + { 13,11,12,15,18,20,19,16}, + { 4, 3, 2, 9,27,28,29,22}, + { 5, 0, 1,10,26,31,30,21}, + { 8, 6, 7,14,23,25,24,17} +}; + +/* Order-8 clustered dithering matrix. */ +static int const cluster8[16][16] = { + { 64, 69, 77, 87, 86, 76, 68, 67, 63, 58, 50, 40, 41, 51, 59, 60}, + { 70, 94,100,109,108, 99, 93, 75, 57, 33, 27, 18, 19, 28, 34, 52}, + { 78,101,114,116,115,112, 98, 83, 49, 26, 13, 11, 12, 15, 29, 44}, + { 88,110,123,124,125,118,107, 85, 39, 17, 4, 3, 2, 9, 20, 42}, + { 89,111,122,127,126,117,106, 84, 38, 16, 5, 0, 1, 10, 21, 43}, + { 79,102,119,121,120,113, 97, 82, 48, 25, 8, 6, 7, 14, 30, 45}, + { 71, 95,103,104,105, 96, 92, 74, 56, 32, 24, 23, 22, 31, 35, 53}, + { 65, 72, 80, 90, 91, 81, 73, 66, 62, 55, 47, 37, 36, 46, 54, 61}, + { 63, 58, 50, 40, 41, 51, 59, 60, 64, 69, 77, 87, 86, 76, 68, 67}, + { 57, 33, 27, 18, 19, 28, 34, 52, 70, 94,100,109,108, 99, 93, 75}, + { 49, 26, 13, 11, 12, 15, 29, 44, 78,101,114,116,115,112, 98, 83}, + { 39, 17, 4, 3, 2, 9, 20, 42, 88,110,123,124,125,118,107, 85}, + { 38, 16, 5, 0, 1, 10, 21, 43, 89,111,122,127,126,117,106, 84}, + { 48, 25, 8, 6, 7, 14, 30, 45, 79,102,119,121,120,113, 97, 82}, + { 56, 32, 24, 23, 22, 31, 35, 53, 71, 95,103,104,105, 96, 92, 74}, + { 62, 55, 47, 37, 36, 46, 54, 61, 65, 72, 80, 90, 91, 81, 73, 66} +}; + diff --git a/editor/pamaddnoise.c b/editor/pamaddnoise.c new file mode 100644 index 00000000..9c2d12f7 --- /dev/null +++ b/editor/pamaddnoise.c @@ -0,0 +1,486 @@ +/* +** +** Add gaussian, multiplicative gaussian, impulse, laplacian or +** poisson noise to a portable anymap. +** +** Version 1.0 November 1995 +** +** Copyright (C) 1995 by Mike Burns (burns@cac.psu.edu) +** +** Adapted to Netpbm 2005.08.09, by Bryan Henderson +** +** 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. +*/ + +/* References +** ---------- +** "Adaptive Image Restoration in Signal-Dependent Noise" by R. Kasturi +** Institute for Electronic Science, Texas Tech University 1982 +** +** "Digital Image Processing Algorithms" by Ioannis Pitas +** Prentice Hall, 1993 ISBN 0-13-145814-0 +*/ + +#define _XOPEN_SOURCE /* get M_PI in math.h */ + +#include <math.h> + +#include "pm_c_util.h" +#include "pam.h" + +#define RANDOM_MASK 0x7FFF /* only compare lower 15 bits. Stupid PCs. */ + +static double const EPSILON = 1.0e-5; +static double const arand = 32767.0; /* 2^15-1 in case stoopid computer */ + +enum noiseType { + GAUSSIAN, + IMPULSE, /* aka salt and pepper noise */ + LAPLACIAN, + MULTIPLICATIVE_GAUSSIAN, + POISSON, + MAX_NOISE_TYPES +}; + + + +static void +gaussian_noise(sample const maxval, + sample const origSample, + sample * const newSampleP, + float const sigma1, + float const sigma2) { +/*---------------------------------------------------------------------------- + Add Gaussian noise. + + Based on Kasturi/Algorithms of the ACM +-----------------------------------------------------------------------------*/ + + double x1, x2, xn, yn; + double rawNewSample; + + x1 = (rand() & RANDOM_MASK) / arand; + + if (x1 == 0.0) + x1 = 1.0; + x2 = (rand() & RANDOM_MASK) / arand; + xn = sqrt(-2.0 * log(x1)) * cos(2.0 * M_PI * x2); + yn = sqrt(-2.0 * log(x1)) * sin(2.0 * M_PI * x2); + + rawNewSample = + origSample + (sqrt((double) origSample) * sigma1 * xn) + (sigma2 * yn); + + *newSampleP = MAX(MIN((int)rawNewSample, maxval), 0); +} + + + +static void +impulse_noise(sample const maxval, + sample const origSample, + sample * const newSampleP, + float const tolerance) { +/*---------------------------------------------------------------------------- + Add impulse (salt and pepper) noise +-----------------------------------------------------------------------------*/ + + double const low_tol = tolerance / 2.0; + double const high_tol = 1.0 - (tolerance / 2.0); + double const sap = (rand() & RANDOM_MASK) / arand; + + if (sap < low_tol) + *newSampleP = 0; + else if ( sap >= high_tol ) + *newSampleP = maxval; +} + + + +static void +laplacian_noise(sample const maxval, + double const infinity, + sample const origSample, + sample * const newSampleP, + float const lsigma) { +/*---------------------------------------------------------------------------- + Add Laplacian noise + + From Pitas' book. +-----------------------------------------------------------------------------*/ + double const u = (rand() & RANDOM_MASK) / arand; + + double rawNewSample; + + if (u <= 0.5) { + if (u <= EPSILON) + rawNewSample = origSample - infinity; + else + rawNewSample = origSample + lsigma * log(2.0 * u); + } else { + double const u1 = 1.0 - u; + if (u1 <= 0.5 * EPSILON) + rawNewSample = origSample + infinity; + else + rawNewSample = origSample - lsigma * log(2.0 * u1); + } + *newSampleP = MIN(MAX((int)rawNewSample, 0), maxval); +} + + + +static void +multiplicative_gaussian_noise(sample const maxval, + double const infinity, + sample const origSample, + sample * const newSampleP, + float const mgsigma) { +/*---------------------------------------------------------------------------- + Add multiplicative Gaussian noise + + From Pitas' book. +-----------------------------------------------------------------------------*/ + double rayleigh, gauss; + double rawNewSample; + + { + double const uniform = (rand() & RANDOM_MASK) / arand; + if (uniform <= EPSILON) + rayleigh = infinity; + else + rayleigh = sqrt(-2.0 * log( uniform)); + } + { + double const uniform = (rand() & RANDOM_MASK) / arand; + gauss = rayleigh * cos(2.0 * M_PI * uniform); + } + rawNewSample = origSample + (origSample * mgsigma * gauss); + + *newSampleP = MIN(MAX((int)rawNewSample, 0), maxval); +} + + + +static void +poisson_noise(sample const maxval, + sample const origSample, + sample * const newSampleP, + float const lambda) { +/*---------------------------------------------------------------------------- + Add Poisson noise +-----------------------------------------------------------------------------*/ + double const x = lambda * origSample; + double const x1 = exp(-x); + + double rawNewSample; + float rr; + unsigned int k; + + rr = 1.0; /* initial value */ + k = 0; /* initial value */ + rr = rr * ((rand() & RANDOM_MASK) / arand); + while (rr > x1) { + ++k; + rr = rr * ((rand() & RANDOM_MASK) / arand); + } + rawNewSample = k / lambda; + + *newSampleP = MIN(MAX((int)rawNewSample, 0), maxval); +} + + + +int +main(int argc, char * argv[]) { + + FILE * ifP; + struct pam inpam; + struct pam outpam; + tuple * tuplerow; + const tuple * newtuplerow; + unsigned int row; + double infinity; + + int argn; + const char * inputFilename; + int noise_type; + int seed; + int i; + const char * const usage = "[-type noise_type] [-lsigma x] [-mgsigma x] " + "[-sigma1 x] [-sigma2 x] [-lambda x] [-seed n] " + "[-tolerance ratio] [pgmfile]"; + + const char * const noise_name[] = { + "gaussian", + "impulse", + "laplacian", + "multiplicative_gaussian", + "poisson" + }; + int const noise_id[] = { + GAUSSIAN, + IMPULSE, + LAPLACIAN, + MULTIPLICATIVE_GAUSSIAN, + POISSON + }; + /* minimum number of characters to match noise name for pm_keymatch() */ + int const noise_compare[] = { + 1, + 1, + 1, + 1, + 1 + }; + + /* define default values for configurable options */ + float lambda = 0.05; + float lsigma = 10.0; + float mgsigma = 0.5; + float sigma1 = 4.0; + float sigma2 = 20.0; + float tolerance = 0.10; + + pnm_init(&argc, argv); + + seed = time(NULL) ^ getpid(); + noise_type = GAUSSIAN; + + argn = 1; + while ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' ) + { + if ( pm_keymatch( argv[argn], "-lambda", 3 ) ) + { + ++argn; + if ( argn >= argc ) + { + pm_message( + "incorrect number of arguments for -lambda option" ); + pm_usage( usage ); + } + else if ( argv[argn][0] == '-' ) + { + pm_message( "invalid argument to -lambda option: %s", + argv[argn] ); + pm_usage( usage ); + } + lambda = atof( argv[argn] ); + } + else if ( pm_keymatch( argv[argn], "-lsigma", 3 ) ) + { + ++argn; + if ( argn >= argc ) + { + pm_message( + "incorrect number of arguments for -lsigma option" ); + pm_usage( usage ); + } + else if ( argv[argn][0] == '-' ) + { + pm_message( "invalid argument to -lsigma option: %s", + argv[argn] ); + pm_usage( usage ); + } + lsigma = atof( argv[argn] ); + } + else if ( pm_keymatch( argv[argn], "-mgsigma", 2 ) ) + { + ++argn; + if ( argn >= argc ) + { + pm_message( + "incorrect number of arguments for -mgsigma option" ); + pm_usage( usage ); + } + else if ( argv[argn][0] == '-' ) + { + pm_message( "invalid argument to -mgsigma option: %s", + argv[argn] ); + pm_usage( usage ); + } + mgsigma = atof( argv[argn] ); + } + else if ( pm_keymatch( argv[argn], "-seed", 3 ) ) + { + ++argn; + if ( argn >= argc ) + { + pm_message( "incorrect number of arguments for -seed option" ); + pm_usage( usage ); + } + else if ( argv[argn][0] == '-' ) + { + pm_message( "invalid argument to -seed option: %s", + argv[argn] ); + pm_usage( usage ); + } + seed = atoi(argv[argn]); + } + else if ( pm_keymatch( argv[argn], "-sigma1", 7 ) || + pm_keymatch( argv[argn], "-s1", 3 ) ) + { + ++argn; + if ( argn >= argc ) + { + pm_message( + "incorrect number of arguments for -sigma1 option" ); + pm_usage( usage ); + } + else if ( argv[argn][0] == '-' ) + { + pm_message( "invalid argument to -sigma1 option: %s", + argv[argn] ); + pm_usage( usage ); + } + sigma1 = atof( argv[argn] ); + } + else if ( pm_keymatch( argv[argn], "-sigma2", 7 ) || + pm_keymatch( argv[argn], "-s2", 3 ) ) + { + ++argn; + if ( argn >= argc ) + { + pm_message( + "incorrect number of arguments for -sigma2 option" ); + pm_usage( usage ); + } + else if ( argv[argn][0] == '-' ) + { + pm_message( "invalid argument to -sigma2 option: %s", + argv[argn] ); + pm_usage( usage ); + } + sigma2 = atof( argv[argn] ); + } + else if ( pm_keymatch( argv[argn], "-tolerance", 3 ) ) + { + ++argn; + if ( argn >= argc ) + { + pm_message( + "incorrect number of arguments for -tolerance option" ); + pm_usage( usage ); + } + else if ( argv[argn][0] == '-' ) + { + pm_message( "invalid argument to -tolerance option: %s", + argv[argn] ); + pm_usage( usage ); + } + tolerance = atof( argv[argn] ); + } + else if ( pm_keymatch( argv[argn], "-type", 3 ) ) + { + ++argn; + if ( argn >= argc ) + { + pm_message( "incorrect number of arguments for -type option" ); + pm_usage( usage ); + } + else if ( argv[argn][0] == '-' ) + { + pm_message( "invalid argument to -type option: %s", + argv[argn] ); + pm_usage( usage ); + } + /* search through list of valid noise types and compare */ + i = 0; + while ( ( i < MAX_NOISE_TYPES ) && + !pm_keymatch( argv[argn], + noise_name[i], noise_compare[i] ) ) + ++i; + if ( i >= MAX_NOISE_TYPES ) + { + pm_message( "invalid argument to -type option: %s", + argv[argn] ); + pm_usage( usage ); + } + noise_type = noise_id[i]; + } + else + pm_usage( usage ); + ++argn; + } + + if ( argn < argc ) + { + inputFilename = argv[argn]; + argn++; + } + else + inputFilename = "-"; + + if ( argn != argc ) + pm_usage( usage ); + + srand(seed); + + ifP = pm_openr(inputFilename); + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + outpam = inpam; + outpam.file = stdout; + + pnm_writepaminit(&outpam); + + tuplerow = pnm_allocpamrow(&inpam); + newtuplerow = pnm_allocpamrow(&inpam); + infinity = (double) inpam.maxval; + + for (row = 0; row < inpam.height; ++row) { + unsigned int col; + pnm_readpamrow(&inpam, tuplerow); + for (col = 0; col < inpam.width; ++col) { + unsigned int plane; + for (plane = 0; plane < inpam.depth; ++plane) { + switch (noise_type) { + case GAUSSIAN: + gaussian_noise(inpam.maxval, + tuplerow[col][plane], + &newtuplerow[col][plane], + sigma1, sigma2); + break; + + case IMPULSE: + impulse_noise(inpam.maxval, + tuplerow[col][plane], + &newtuplerow[col][plane], + tolerance); + break; + + case LAPLACIAN: + laplacian_noise(inpam.maxval, infinity, + tuplerow[col][plane], + &newtuplerow[col][plane], + lsigma); + break; + + case MULTIPLICATIVE_GAUSSIAN: + multiplicative_gaussian_noise(inpam.maxval, infinity, + tuplerow[col][plane], + &newtuplerow[col][plane], + mgsigma); + break; + + case POISSON: + poisson_noise(inpam.maxval, + tuplerow[col][plane], + &newtuplerow[col][plane], + lambda); + break; + + } + } + } + pnm_writepamrow(&outpam, newtuplerow); + } + pnm_freepamrow(newtuplerow); + pnm_freepamrow(tuplerow); + + return 0; +} diff --git a/editor/pamcomp.c b/editor/pamcomp.c new file mode 100644 index 00000000..871267b2 --- /dev/null +++ b/editor/pamcomp.c @@ -0,0 +1,619 @@ +/*---------------------------------------------------------------------------- + pamcomp +----------------------------------------------------------------------------- + This program composes two images together, with optional translucence. + + This program is derived from (and replaces) Pnmcomp, whose origin is + as follows: + + Copyright 1992, David Koblas. + 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. + + No code from the original remains in the present version. The + January 2004 version was coded entirely by Bryan Henderson. + Bryan has contributed his work to the public domain. + + The current version is derived from the January 2004 version, with + additional work by multiple authors. +-----------------------------------------------------------------------------*/ + +#define _BSD_SOURCE /* Make sure strcasecmp() is in string.h */ +#include <string.h> +#include <math.h> + +#include "pam.h" +#include "pm_gamma.h" +#include "shhopt.h" +#include "mallocvar.h" + +enum horizPos {BEYONDLEFT, LEFT, CENTER, RIGHT, BEYONDRIGHT}; +enum vertPos {ABOVE, TOP, MIDDLE, BOTTOM, BELOW}; + + +enum sampleScale {INTENSITY_SAMPLE, GAMMA_SAMPLE}; +/* This indicates a scale for a PAM sample value. INTENSITY_SAMPLE means + the value is proportional to light intensity; GAMMA_SAMPLE means the + value is gamma-adjusted as defined in the PGM/PPM spec. In both + scales, the values are continuous and normalized to the range 0..1. + + This scale has no meaning if the PAM is not a visual image. +*/ + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *underlyingFilespec; /* '-' if stdin */ + const char *overlayFilespec; + const char *alphaFilespec; + const char *outputFilespec; /* '-' if stdout */ + int xoff, yoff; /* value of xoff, yoff options */ + float opacity; + unsigned int alphaInvert; + enum horizPos align; + enum vertPos valign; + unsigned int linear; +}; + + + +static void +parseCommandLine(int argc, + 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 option_def_index; + + char *align, *valign; + unsigned int xoffSpec, yoffSpec, alignSpec, valignSpec, opacitySpec, + alphaSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "invert", OPT_FLAG, NULL, + &cmdlineP->alphaInvert, 0); + OPTENT3(0, "xoff", OPT_INT, &cmdlineP->xoff, + &xoffSpec, 0); + OPTENT3(0, "yoff", OPT_INT, &cmdlineP->yoff, + &yoffSpec, 0); + OPTENT3(0, "opacity", OPT_FLOAT, &cmdlineP->opacity, + &opacitySpec, 0); + OPTENT3(0, "alpha", OPT_STRING, &cmdlineP->alphaFilespec, + &alphaSpec, 0); + OPTENT3(0, "align", OPT_STRING, &align, + &alignSpec, 0); + OPTENT3(0, "valign", OPT_STRING, &valign, + &valignSpec, 0); + OPTENT3(0, "linear", OPT_FLAG, NULL, + &cmdlineP->linear, 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 */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + + if (!xoffSpec) + cmdlineP->xoff = 0; + if (!yoffSpec) + cmdlineP->yoff = 0; + if (!alphaSpec) + cmdlineP->alphaFilespec = NULL; + + if (alignSpec) { + if (strcasecmp(align, "BEYONDLEFT") == 0) + cmdlineP->align = BEYONDLEFT; + else if (strcasecmp(align, "LEFT") == 0) + cmdlineP->align = LEFT; + else if (strcasecmp(align, "CENTER") == 0) + cmdlineP->align = CENTER; + else if (strcasecmp(align, "RIGHT") == 0) + cmdlineP->align = RIGHT; + else if (strcasecmp(align, "BEYONDRIGHT") == 0) + cmdlineP->align = BEYONDRIGHT; + else + pm_error("Invalid value for align option: '%s'. Only LEFT, " + "RIGHT, CENTER, BEYONDLEFT, and BEYONDRIGHT are valid.", + align); + } else + cmdlineP->align = LEFT; + + if (valignSpec) { + if (strcasecmp(valign, "ABOVE") == 0) + cmdlineP->valign = ABOVE; + else if (strcasecmp(valign, "TOP") == 0) + cmdlineP->valign = TOP; + else if (strcasecmp(valign, "MIDDLE") == 0) + cmdlineP->valign = MIDDLE; + else if (strcasecmp(valign, "BOTTOM") == 0) + cmdlineP->valign = BOTTOM; + else if (strcasecmp(valign, "BELOW") == 0) + cmdlineP->valign = BELOW; + else + pm_error("Invalid value for valign option: '%s'. Only TOP, " + "BOTTOM, MIDDLE, ABOVE, and BELOW are valid.", + align); + } else + cmdlineP->valign = TOP; + + if (!opacitySpec) + cmdlineP->opacity = 1.0; + + if (argc-1 < 1) + pm_error("Need at least one argument: file specification of the " + "overlay image."); + + cmdlineP->overlayFilespec = argv[1]; + + if (argc-1 >= 2) + cmdlineP->underlyingFilespec = argv[2]; + else + cmdlineP->underlyingFilespec = "-"; + + if (argc-1 >= 3) + cmdlineP->outputFilespec = argv[3]; + else + cmdlineP->outputFilespec = "-"; + + if (argc-1 > 3) + pm_error("Too many arguments. Only acceptable arguments are: " + "overlay image, underlying image, output image"); +} + + + + +static int +commonFormat(int const formatA, + int const formatB) { +/*---------------------------------------------------------------------------- + Return a viable format for the result of composing the two formats + 'formatA' and 'formatB'. +-----------------------------------------------------------------------------*/ + int retval; + + int const typeA = PAM_FORMAT_TYPE(formatA); + int const typeB = PAM_FORMAT_TYPE(formatB); + + if (typeA == PAM_TYPE || typeB == PAM_TYPE) + retval = PAM_FORMAT; + else if (typeA == PPM_TYPE || typeB == PPM_TYPE) + retval = PPM_FORMAT; + else if (typeA == PGM_TYPE || typeB == PGM_TYPE) + retval = PGM_FORMAT; + else if (typeA == PBM_TYPE || typeB == PBM_TYPE) + retval = PBM_FORMAT; + else { + /* Results are undefined for this case, so we do a hail Mary. */ + retval = formatA; + } + return retval; +} + + + +static void +commonTupletype(const char * const tupletypeA, + const char * const tupletypeB, + char * const tupletypeOut, + unsigned int const size) { + + if (strncmp(tupletypeA, "RGB", 3) == 0 || + strncmp(tupletypeB, "RGB", 3) == 0) + strncpy(tupletypeOut, "RGB", size); + else if (strncmp(tupletypeA, "GRAYSCALE", 9) == 0 || + strncmp(tupletypeB, "GRAYSCALE", 9) == 0) + strncpy(tupletypeOut, "GRAYSCALE", size); + else if (strncmp(tupletypeA, "BLACKANDWHITE", 13) == 0 || + strncmp(tupletypeB, "BLACKANDWHITE", 13) == 0) + strncpy(tupletypeOut, "BLACKANDWHITE", size); + else + /* Results are undefined for this case, so we do a hail Mary. */ + strncpy(tupletypeOut, tupletypeA, size); +} + + + +static void +determineOutputType(struct pam * const composedPamP, + struct pam * const underlayPamP, + struct pam * const overlayPamP) { + + composedPamP->height = underlayPamP->height; + composedPamP->width = underlayPamP->width; + + composedPamP->format = commonFormat(underlayPamP->format, + overlayPamP->format); + commonTupletype(underlayPamP->tuple_type, overlayPamP->tuple_type, + composedPamP->tuple_type, + sizeof(composedPamP->tuple_type)); + + composedPamP->maxval = pm_lcm(underlayPamP->maxval, overlayPamP->maxval, + 1, PNM_OVERALLMAXVAL); + + if (strcmp(composedPamP->tuple_type, "RGB") == 0) + composedPamP->depth = 3; + else if (strcmp(composedPamP->tuple_type, "GRAYSCALE") == 0) + composedPamP->depth = 1; + else if (strcmp(composedPamP->tuple_type, "BLACKANDWHITE") == 0) + composedPamP->depth = 1; + else + /* Results are undefined for this case, so we just do something safe */ + composedPamP->depth = MIN(underlayPamP->depth, overlayPamP->depth); +} + + + +static void +warnOutOfFrame( int const originLeft, + int const originTop, + int const overCols, + int const overRows, + int const underCols, + int const underRows ) { + + if (originLeft >= underCols) + pm_message("WARNING: the overlay is entirely off the right edge " + "of the underlying image. " + "It will not be visible in the result. The horizontal " + "overlay position you selected is %d, " + "and the underlying image " + "is only %d pixels wide.", originLeft, underCols ); + else if (originLeft + overCols <= 0) + pm_message("WARNING: the overlay is entirely off the left edge " + "of the underlying image. " + "It will not be visible in the result. The horizontal " + "overlay position you selected is %d and the overlay is " + "only %d pixels wide.", originLeft, overCols); + else if (originTop >= underRows) + pm_message("WARNING: the overlay is entirely off the bottom edge " + "of the underlying image. " + "It will not be visible in the result. The vertical " + "overlay position you selected is %d, " + "and the underlying image " + "is only %d pixels high.", originTop, underRows ); + else if (originTop + overRows <= 0) + pm_message("WARNING: the overlay is entirely off the top edge " + "of the underlying image. " + "It will not be visible in the result. The vertical " + "overlay position you selected is %d and the overlay is " + "only %d pixels high.", originTop, overRows); +} + + + +static void +computeOverlayPosition(int const underCols, + int const underRows, + int const overCols, + int const overRows, + struct cmdlineInfo const cmdline, + int * const originLeftP, + int * const originTopP) { +/*---------------------------------------------------------------------------- + Determine where to overlay the overlay image, based on the options the + user specified and the realities of the image dimensions. + + The origin may be outside the underlying image (so e.g. *originLeftP may + be negative or > image width). That means not all of the overlay image + actually gets used. In fact, there may be no overlap at all. +-----------------------------------------------------------------------------*/ + int xalign, yalign; + + switch (cmdline.align) { + case BEYONDLEFT: xalign = -overCols; break; + case LEFT: xalign = 0; break; + case CENTER: xalign = (underCols-overCols)/2; break; + case RIGHT: xalign = underCols - overCols; break; + case BEYONDRIGHT: xalign = underCols; break; + } + switch (cmdline.valign) { + case ABOVE: yalign = -overRows; break; + case TOP: yalign = 0; break; + case MIDDLE: yalign = (underRows-overRows)/2; break; + case BOTTOM: yalign = underRows - overRows; break; + case BELOW: yalign = underRows; break; + } + *originLeftP = xalign + cmdline.xoff; + *originTopP = yalign + cmdline.yoff; + + warnOutOfFrame(*originLeftP, *originTopP, + overCols, overRows, underCols, underRows); +} + + + +static sample +composeComponents(sample const compA, + sample const compB, + float const distrib, + sample const maxval, + enum sampleScale const sampleScale) { +/*---------------------------------------------------------------------------- + Compose a single component of each of two pixels, with 'distrib' being + the fraction of 'compA' in the result, 1-distrib the fraction of 'compB'. + + The inputs and result are based on a maxval of 'maxval'. + + Note that while 'distrib' in the straightforward case is always in + [0,1], it can in fact be negative or greater than 1. We clip the + result as required to return a legal sample value. +-----------------------------------------------------------------------------*/ + sample retval; + + if (fabs(1.0-distrib) < .001) + /* Fast path for common case */ + retval = compA; + else { + if (sampleScale == INTENSITY_SAMPLE) { + sample const mix = + ROUNDU(compA * distrib + compB * (1.0 - distrib)); + retval = MIN(maxval, MAX(0, mix)); + } else { + float const compANormalized = (float)compA/maxval; + float const compBNormalized = (float)compB/maxval; + float const compALinear = pm_ungamma709(compANormalized); + float const compBLinear = pm_ungamma709(compBNormalized); + float const mix = + compALinear * distrib + compBLinear * (1.0 - distrib); + sample const sampleValue = ROUNDU(pm_gamma709(mix) * maxval); + retval = MIN(maxval, MAX(0, sampleValue)); + } + } + return retval; +} + + + +static void +overlayPixel(tuple const overlayTuple, + struct pam * const overlayPamP, + tuple const underlayTuple, + struct pam * const underlayPamP, + tuplen const alphaTuplen, + bool const invertAlpha, + bool const overlayHasOpacity, + unsigned int const opacityPlane, + tuple const composedTuple, + struct pam * const composedPamP, + float const masterOpacity, + enum sampleScale const sampleScale) { + + float overlayWeight; + + overlayWeight = masterOpacity; /* initial value */ + + if (overlayHasOpacity) + overlayWeight *= (float) + overlayTuple[opacityPlane] / overlayPamP->maxval; + + if (alphaTuplen) { + float const alphaval = + invertAlpha ? (1.0 - alphaTuplen[0]) : alphaTuplen[0]; + overlayWeight *= alphaval; + } + + { + unsigned int plane; + + for (plane = 0; plane < composedPamP->depth; ++plane) + composedTuple[plane] = + composeComponents(overlayTuple[plane], underlayTuple[plane], + overlayWeight, + composedPamP->maxval, + sampleScale); + } +} + + + +static void +adaptRowToOutputFormat(struct pam * const inpamP, + tuple * const tuplerow, + struct pam * const outpamP) { +/*---------------------------------------------------------------------------- + Convert the row in 'tuplerow', which is in a format described by + *inpamP, to the format described by *outpamP. + + 'tuplerow' must have enough allocated depth to do this. +-----------------------------------------------------------------------------*/ + pnm_scaletuplerow(inpamP, tuplerow, tuplerow, outpamP->maxval); + + if (strncmp(outpamP->tuple_type, "RGB", 3) == 0) + pnm_makerowrgb(inpamP, tuplerow); +} + + + +static void +composite(int const originleft, + int const origintop, + struct pam * const underlayPamP, + struct pam * const overlayPamP, + struct pam * const alphaPamP, + bool const invertAlpha, + float const masterOpacity, + struct pam * const composedPamP, + bool const assumeLinear) { +/*---------------------------------------------------------------------------- + Overlay the overlay image in the array 'overlayImage', described by + *overlayPamP, onto the underlying image from the input image file + as described by *underlayPamP, output the composite to the image + file as described by *composedPamP. + + Apply the overlay image with transparency described by the array + 'alpha' and *alphaPamP. + + The underlying image is positioned after its header. + + 'originleft' and 'origintop' are the coordinates in the underlying + image plane where the top left corner of the overlay image is to + go. It is not necessarily inside the underlying image (in fact, + may be negative). Only the part of the overlay that actually + intersects the underlying image, if any, gets into the output. +-----------------------------------------------------------------------------*/ + enum sampleScale const sampleScale = + assumeLinear ? INTENSITY_SAMPLE : GAMMA_SAMPLE; + + int underlayRow; /* NB may be negative */ + int overlayRow; /* NB may be negative */ + tuple * composedTuplerow; + tuple * underlayTuplerow; + tuple * overlayTuplerow; + tuplen * alphaTuplerown; + bool overlayHasOpacity; + unsigned int opacityPlane; + + pnm_getopacity(overlayPamP, &overlayHasOpacity, &opacityPlane); + + composedTuplerow = pnm_allocpamrow(composedPamP); + underlayTuplerow = pnm_allocpamrow(underlayPamP); + overlayTuplerow = pnm_allocpamrow(overlayPamP); + if (alphaPamP) + alphaTuplerown = pnm_allocpamrown(alphaPamP); + + pnm_writepaminit(composedPamP); + + for (underlayRow = MIN(0, origintop), overlayRow = MIN(0, -origintop); + underlayRow < MAX(underlayPamP->height, + origintop + overlayPamP->height); + ++underlayRow, ++overlayRow) { + + if (overlayRow >= 0 && overlayRow < overlayPamP->height) { + pnm_readpamrow(overlayPamP, overlayTuplerow); + adaptRowToOutputFormat(overlayPamP, overlayTuplerow, composedPamP); + if (alphaPamP) + pnm_readpamrown(alphaPamP, alphaTuplerown); + } + if (underlayRow >= 0 && underlayRow < underlayPamP->height) { + pnm_readpamrow(underlayPamP, underlayTuplerow); + adaptRowToOutputFormat(underlayPamP, underlayTuplerow, + composedPamP); + + if (underlayRow < origintop || + underlayRow >= origintop + overlayPamP->height) { + + /* Overlay image does not touch this underlay row. */ + + pnm_writepamrow(composedPamP, underlayTuplerow); + } else { + unsigned int col; + for (col = 0; col < composedPamP->width; ++col) { + int const ovlcol = col - originleft; + + if (ovlcol >= 0 && ovlcol < overlayPamP->width) { + tuplen const alphaTuplen = + alphaPamP ? alphaTuplerown[ovlcol] : NULL; + + overlayPixel(overlayTuplerow[ovlcol], overlayPamP, + underlayTuplerow[col], underlayPamP, + alphaTuplen, invertAlpha, + overlayHasOpacity, opacityPlane, + composedTuplerow[col], composedPamP, + masterOpacity, sampleScale); + } else + /* Overlay image does not touch this column. */ + pnm_assigntuple(composedPamP, composedTuplerow[col], + underlayTuplerow[col]); + } + pnm_writepamrow(composedPamP, composedTuplerow); + } + } + } + pnm_freepamrow(composedTuplerow); + pnm_freepamrow(underlayTuplerow); + pnm_freepamrow(overlayTuplerow); + if (alphaPamP) + pnm_freepamrown(alphaTuplerown); +} + + + +int +main(int argc, char *argv[]) { + + struct cmdlineInfo cmdline; + FILE * underlayFileP; + FILE * overlayFileP; + FILE * alphaFileP; + struct pam underlayPam; + struct pam overlayPam; + struct pam alphaPam; + struct pam composedPam; + int originLeft, originTop; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + overlayFileP = pm_openr(cmdline.overlayFilespec); + pnm_readpaminit(overlayFileP, &overlayPam, + PAM_STRUCT_SIZE(allocation_depth)); + if (cmdline.alphaFilespec) { + alphaFileP = pm_openr(cmdline.alphaFilespec); + pnm_readpaminit(alphaFileP, &alphaPam, + PAM_STRUCT_SIZE(allocation_depth)); + + if (overlayPam.width != alphaPam.width || + overlayPam.height != overlayPam.height) + pm_error("Opacity map and overlay image are not the same size"); + } else + alphaFileP = NULL; + + underlayFileP = pm_openr(cmdline.underlyingFilespec); + + pnm_readpaminit(underlayFileP, &underlayPam, + PAM_STRUCT_SIZE(allocation_depth)); + + computeOverlayPosition(underlayPam.width, underlayPam.height, + overlayPam.width, overlayPam.height, + cmdline, &originLeft, &originTop); + + composedPam.size = sizeof(composedPam); + composedPam.len = PAM_STRUCT_SIZE(allocation_depth); + composedPam.allocation_depth = 0; + composedPam.file = pm_openw(cmdline.outputFilespec); + + determineOutputType(&composedPam, &underlayPam, &overlayPam); + + pnm_setminallocationdepth(&underlayPam, composedPam.depth); + pnm_setminallocationdepth(&overlayPam, composedPam.depth); + + composite(originLeft, originTop, + &underlayPam, &overlayPam, alphaFileP ? &alphaPam : NULL, + cmdline.alphaInvert, cmdline.opacity, + &composedPam, cmdline.linear); + + if (alphaFileP) + pm_close(alphaFileP); + pm_close(overlayFileP); + pm_close(underlayFileP); + pm_close(composedPam.file); + + /* If the program failed, it previously aborted with nonzero completion + code, via various function calls. + */ + return 0; +} diff --git a/editor/pamcut.c b/editor/pamcut.c new file mode 100644 index 00000000..d5de45fb --- /dev/null +++ b/editor/pamcut.c @@ -0,0 +1,573 @@ +/*============================================================================ + pamcut +============================================================================== + Cut a rectangle out of a Netpbm image + + This is inspired by and intended as a replacement for Pnmcut by + Jef Poskanzer, 1989. + + By Bryan Henderson, San Jose CA. Contributed to the public domain + by its author. +============================================================================*/ + +#include <limits.h> +#include <assert.h> +#include "pam.h" +#include "shhopt.h" +#include "mallocvar.h" + +#define UNSPEC INT_MAX + /* UNSPEC is the value we use for an argument that is not specified + by the user. Theoretically, the user could specify this value, + but we hope not. + */ + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *inputFilespec; /* Filespec of input file */ + + /* The following describe the rectangle the user wants to cut out. + the value UNSPEC for any of them indicates that value was not + specified. A negative value means relative to the far edge. + 'width' and 'height' are not negative. These specifications + do not necessarily describe a valid rectangle; they are just + what the user said. + */ + int left; + int right; + int top; + int bottom; + int width; + int height; + unsigned int pad; + + unsigned int verbose; +}; + + + +static void +parseCommandLine(int argc, 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; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "left", OPT_INT, &cmdlineP->left, NULL, 0); + OPTENT3(0, "right", OPT_INT, &cmdlineP->right, NULL, 0); + OPTENT3(0, "top", OPT_INT, &cmdlineP->top, NULL, 0); + OPTENT3(0, "bottom", OPT_INT, &cmdlineP->bottom, NULL, 0); + OPTENT3(0, "width", OPT_INT, &cmdlineP->width, NULL, 0); + OPTENT3(0, "height", OPT_INT, &cmdlineP->height, NULL, 0); + OPTENT3(0, "pad", OPT_FLAG, NULL, &cmdlineP->pad, 0); + OPTENT3(0, "verbose", OPT_FLAG, NULL, &cmdlineP->verbose, 0); + + /* Set the defaults */ + cmdlineP->left = UNSPEC; + cmdlineP->right = UNSPEC; + cmdlineP->top = UNSPEC; + cmdlineP->bottom = UNSPEC; + cmdlineP->width = UNSPEC; + cmdlineP->height = UNSPEC; + + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ + opt.allowNegNum = TRUE; /* We may have parms that are negative numbers */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (cmdlineP->width < 0) + pm_error("-width may not be negative."); + if (cmdlineP->height < 0) + pm_error("-height may not be negative."); + + if ((argc-1) != 0 && (argc-1) != 1 && (argc-1) != 4 && (argc-1) != 5) + pm_error("Wrong number of arguments. " + "Must be 0, 1, 4, or 5 arguments."); + + switch (argc-1) { + case 0: + cmdlineP->inputFilespec = "-"; + break; + case 1: + cmdlineP->inputFilespec = argv[1]; + break; + case 4: + case 5: { + int warg, harg; /* The "width" and "height" command line arguments */ + + if (sscanf(argv[1], "%d", &cmdlineP->left) != 1) + pm_error("Invalid number for left column argument"); + if (sscanf(argv[2], "%d", &cmdlineP->top) != 1) + pm_error("Invalid number for right column argument"); + if (sscanf(argv[3], "%d", &warg) != 1) + pm_error("Invalid number for width argument"); + if (sscanf(argv[4], "%d", &harg) != 1) + pm_error("Invalid number for height argument"); + + if (warg > 0) { + cmdlineP->width = warg; + cmdlineP->right = UNSPEC; + } else { + cmdlineP->width = UNSPEC; + cmdlineP->right = warg -1; + } + if (harg > 0) { + cmdlineP->height = harg; + cmdlineP->bottom = UNSPEC; + } else { + cmdlineP->height = UNSPEC; + cmdlineP->bottom = harg - 1; + } + + if (argc-1 == 4) + cmdlineP->inputFilespec = "-"; + else + cmdlineP->inputFilespec = argv[5]; + break; + } + } +} + + + +static void +computeCutBounds(const int cols, const int rows, + const int leftarg, const int rightarg, + const int toparg, const int bottomarg, + const int widtharg, const int heightarg, + int * const leftcolP, int * const rightcolP, + int * const toprowP, int * const bottomrowP) { +/*---------------------------------------------------------------------------- + From the values given on the command line 'leftarg', 'rightarg', + 'toparg', 'bottomarg', 'widtharg', and 'heightarg', determine what + rectangle the user wants cut out. + + Any of these arguments may be UNSPEC to indicate "not specified". + Any except 'widtharg' and 'heightarg' may be negative to indicate + relative to the far edge. 'widtharg' and 'heightarg' are positive. + + Return the location of the rectangle as *leftcolP, *rightcolP, + *toprowP, and *bottomrowP. +-----------------------------------------------------------------------------*/ + + int leftcol, rightcol, toprow, bottomrow; + /* The left and right column numbers and top and bottom row numbers + specified by the user, except with negative values translated + into the actual values. + + Note that these may very well be negative themselves, such + as when the user says "column -10" and there are only 5 columns + in the image. + */ + + /* Translate negative column and row into real column and row */ + /* Exploit the fact that UNSPEC is a positive number */ + + if (leftarg >= 0) + leftcol = leftarg; + else + leftcol = cols + leftarg; + if (rightarg >= 0) + rightcol = rightarg; + else + rightcol = cols + rightarg; + if (toparg >= 0) + toprow = toparg; + else + toprow = rows + toparg; + if (bottomarg >= 0) + bottomrow = bottomarg; + else + bottomrow = rows + bottomarg; + + /* Sort out left, right, and width specifications */ + + if (leftcol == UNSPEC && rightcol == UNSPEC && widtharg == UNSPEC) { + *leftcolP = 0; + *rightcolP = cols - 1; + } + if (leftcol == UNSPEC && rightcol == UNSPEC && widtharg != UNSPEC) { + *leftcolP = 0; + *rightcolP = 0 + widtharg - 1; + } + if (leftcol == UNSPEC && rightcol != UNSPEC && widtharg == UNSPEC) { + *leftcolP = 0; + *rightcolP = rightcol; + } + if (leftcol == UNSPEC && rightcol != UNSPEC && widtharg != UNSPEC) { + *leftcolP = rightcol - widtharg + 1; + *rightcolP = rightcol; + } + if (leftcol != UNSPEC && rightcol == UNSPEC && widtharg == UNSPEC) { + *leftcolP = leftcol; + *rightcolP = cols - 1; + } + if (leftcol != UNSPEC && rightcol == UNSPEC && widtharg != UNSPEC) { + *leftcolP = leftcol; + *rightcolP = leftcol + widtharg - 1; + } + if (leftcol != UNSPEC && rightcol != UNSPEC && widtharg == UNSPEC) { + *leftcolP = leftcol; + *rightcolP = rightcol; + } + if (leftcol != UNSPEC && rightcol != UNSPEC && widtharg != UNSPEC) { + pm_error("You may not specify left, right, and width.\n" + "Choose at most two of these."); + } + + + /* Sort out top, bottom, and height specifications */ + + if (toprow == UNSPEC && bottomrow == UNSPEC && heightarg == UNSPEC) { + *toprowP = 0; + *bottomrowP = rows - 1; + } + if (toprow == UNSPEC && bottomrow == UNSPEC && heightarg != UNSPEC) { + *toprowP = 0; + *bottomrowP = 0 + heightarg - 1; + } + if (toprow == UNSPEC && bottomrow != UNSPEC && heightarg == UNSPEC) { + *toprowP = 0; + *bottomrowP = bottomrow; + } + if (toprow == UNSPEC && bottomrow != UNSPEC && heightarg != UNSPEC) { + *toprowP = bottomrow - heightarg + 1; + *bottomrowP = bottomrow; + } + if (toprow != UNSPEC && bottomrow == UNSPEC && heightarg == UNSPEC) { + *toprowP = toprow; + *bottomrowP = rows - 1; + } + if (toprow != UNSPEC && bottomrow == UNSPEC && heightarg != UNSPEC) { + *toprowP = toprow; + *bottomrowP = toprow + heightarg - 1; + } + if (toprow != UNSPEC && bottomrow != UNSPEC && heightarg == UNSPEC) { + *toprowP = toprow; + *bottomrowP = bottomrow; + } + if (toprow != UNSPEC && bottomrow != UNSPEC && heightarg != UNSPEC) { + pm_error("You may not specify top, bottom, and height.\n" + "Choose at most two of these."); + } + +} + + + +static void +rejectOutOfBounds(const int cols, const int rows, + const int leftcol, const int rightcol, + const int toprow, const int bottomrow) { + + /* Reject coordinates off the edge */ + + if (leftcol < 0) + pm_error("You have specified a left edge (%d) that is beyond\n" + "the left edge of the image (0)", leftcol); + if (leftcol > cols-1) + pm_error("You have specified a left edge (%d) that is beyond\n" + "the right edge of the image (%d)", leftcol, cols-1); + if (rightcol < 0) + pm_error("You have specified a right edge (%d) that is beyond\n" + "the left edge of the image (0)", rightcol); + if (rightcol > cols-1) + pm_error("You have specified a right edge (%d) that is beyond\n" + "the right edge of the image (%d)", rightcol, cols-1); + if (leftcol > rightcol) + pm_error("You have specified a left edge (%d) that is to the right\n" + "of the right edge you specified (%d)", + leftcol, rightcol); + + if (toprow < 0) + pm_error("You have specified a top edge (%d) that is above the top " + "edge of the image (0)", toprow); + if (toprow > rows-1) + pm_error("You have specified a top edge (%d) that is below the\n" + "bottom edge of the image (%d)", toprow, rows-1); + if (bottomrow < 0) + pm_error("You have specified a bottom edge (%d) that is above the\n" + "top edge of the image (0)", bottomrow); + if (bottomrow > rows-1) + pm_error("You have specified a bottom edge (%d) that is below the\n" + "bottom edge of the image (%d)", bottomrow, rows-1); + if (toprow > bottomrow) + pm_error("You have specified a top edge (%d) that is below\n" + "the bottom edge you specified (%d)", + toprow, bottomrow); +} + + + +static void +writeBlackRows(const struct pam * const outpamP, + int const rows) { +/*---------------------------------------------------------------------------- + Write out 'rows' rows of black tuples of the image described by *outpamP. + + Unless our input image is PBM, PGM, or PPM, or PAM equivalent, we + don't really know what "black" means, so this is just something + arbitrary in that case. +-----------------------------------------------------------------------------*/ + tuple blackTuple; + tuple * blackRow; + int col; + + pnm_createBlackTuple(outpamP, &blackTuple); + + MALLOCARRAY_NOFAIL(blackRow, outpamP->width); + + for (col = 0; col < outpamP->width; ++col) + blackRow[col] = blackTuple; + + pnm_writepamrowmult(outpamP, blackRow, rows); + + free(blackRow); + + pnm_freepamtuple(blackTuple); +} + + + +struct rowCutter { +/*---------------------------------------------------------------------------- + This is an object that gives you pointers you can use to effect the + horizontal cutting and padding of a row just by doing one + pnm_readpamrow() and one pnm_writepamrow(). It works like this: + + The array inputPointers[] contains an element for each pixel in an input + row. If it's a pixel that gets discarded in the cutting process, + inputPointers[] points to a special "discard" tuple. All thrown away + pixels have the same discard tuple to save CPU cache space. If it's + a pixel that gets copied to the output, inputPointers[] points to some + tuple to which outputPointers[] also points. + + The array outputPointers[] contains an element for each pixel in an + output row. If the pixel is one that gets copied from the input, + outputPointers[] points to some tuple to which inputPointers[] also + points. If it's a pixel that gets padded with black, outputPointers[] + points to a constant black tuple. All padded pixels have the same + constant black tuple to save CPU cache space. + + For example, if you have a three pixel input row and are cutting + off the right two pixels, inputPointers[0] points to copyTuples[0] + and inputPointers[1] and inputPointers[2] point to discardTuple. + outputPointers[0] points to copyTuples[0]. + + We arrange to have the padded parts of the output row filled with + black tuples. Unless the input image is PBM, PGM, or PPM, or PAM + equivalent, we don't really know what "black" means, so we fill with + something arbitrary in that case. +-----------------------------------------------------------------------------*/ + tuple * inputPointers; + tuple * outputPointers; + + unsigned int inputWidth; + unsigned int outputWidth; + + /* The following are the tuples to which inputPointers[] and + outputPointers[] may point. + */ + tuple * copyTuples; + tuple blackTuple; + tuple discardTuple; +}; + + + +/* In a typical multi-image stream, all the images have the same + dimensions, so this program creates and destroys identical row + cutters for each image in the stream. If that turns out to take a + significant amount of resource to do, we should create a cache: + keep the last row cutter made, tagged by the parameters used to + create it. If the parameters are the same for the next image, we + just use that cached row cutter; otherwise, we discard it and + create a new one then. +*/ + +static void +createRowCutter(struct pam * const inpamP, + struct pam * const outpamP, + int const leftcol, + int const rightcol, + struct rowCutter ** const rowCutterPP) { + + struct rowCutter * rowCutterP; + tuple * inputPointers; + tuple * outputPointers; + tuple * copyTuples; + tuple blackTuple; + tuple discardTuple; + int col; + + assert(inpamP->depth >= outpamP->depth); + /* Entry condition. If this weren't true, we could not simply + treat an input tuple as an output tuple. + */ + + copyTuples = pnm_allocpamrow(outpamP); + discardTuple = pnm_allocpamtuple(inpamP); + pnm_createBlackTuple(outpamP, &blackTuple); + + MALLOCARRAY_NOFAIL(inputPointers, inpamP->width); + MALLOCARRAY_NOFAIL(outputPointers, outpamP->width); + + /* Put in left padding */ + for (col = leftcol; col < 0; ++col) + outputPointers[col-leftcol] = blackTuple; + + /* Put in extracted columns */ + for (col = MAX(leftcol, 0); + col <= MIN(rightcol, inpamP->width-1); + ++col) { + int const outcol = col - leftcol; + + inputPointers[col] = outputPointers[outcol] = copyTuples[outcol]; + } + + /* Put in right padding */ + for (col = MIN(rightcol, inpamP->width-1) + 1; col <= rightcol; ++col) + outputPointers[col-leftcol] = blackTuple; + + /* Direct input pixels that are getting cut off to the discard tuple */ + + for (col = 0; col < leftcol; ++col) + inputPointers[col] = discardTuple; + + for (col = rightcol + 1; col < inpamP->width; ++col) + inputPointers[col] = discardTuple; + + MALLOCVAR_NOFAIL(rowCutterP); + + rowCutterP->inputWidth = inpamP->width; + rowCutterP->outputWidth = outpamP->width; + rowCutterP->inputPointers = inputPointers; + rowCutterP->outputPointers = outputPointers; + rowCutterP->copyTuples = copyTuples; + rowCutterP->discardTuple = discardTuple; + rowCutterP->blackTuple = blackTuple; + + *rowCutterPP = rowCutterP; +} + + + +static void +destroyRowCutter(struct rowCutter * const rowCutterP) { + + pnm_freepamrow(rowCutterP->copyTuples); + pnm_freepamtuple(rowCutterP->blackTuple); + pnm_freepamtuple(rowCutterP->discardTuple); + free(rowCutterP->inputPointers); + free(rowCutterP->outputPointers); + + free(rowCutterP); +} + + + +static void +cutOneImage(FILE * const ifP, + struct cmdlineInfo const cmdline, + FILE * const ofP) { + + int row; + int leftcol, rightcol, toprow, bottomrow; + struct pam inpam; /* Input PAM image */ + struct pam outpam; /* Output PAM image */ + struct rowCutter * rowCutterP; + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + computeCutBounds(inpam.width, inpam.height, + cmdline.left, cmdline.right, + cmdline.top, cmdline.bottom, + cmdline.width, cmdline.height, + &leftcol, &rightcol, &toprow, &bottomrow); + + if (!cmdline.pad) + rejectOutOfBounds(inpam.width, inpam.height, leftcol, rightcol, + toprow, bottomrow); + + if (cmdline.verbose) { + pm_message("Image goes from Row 0, Column 0 through Row %d, Column %d", + inpam.height-1, inpam.width-1); + pm_message("Cutting from Row %d, Column %d through Row %d Column %d", + toprow, leftcol, bottomrow, rightcol); + } + + outpam = inpam; /* Initial value -- most fields should be same */ + outpam.file = ofP; + outpam.width = rightcol-leftcol+1; + outpam.height = bottomrow-toprow+1; + + pnm_writepaminit(&outpam); + + /* Write out top padding */ + if (0 - toprow > 0) + writeBlackRows(&outpam, 0 - toprow); + + createRowCutter(&inpam, &outpam, leftcol, rightcol, &rowCutterP); + + /* Read input and write out rows extracted from it */ + for (row = 0; row < inpam.height; ++row) { + if (row >= toprow && row <= bottomrow){ + pnm_readpamrow(&inpam, rowCutterP->inputPointers); + pnm_writepamrow(&outpam, rowCutterP->outputPointers); + } else /* row < toprow || row > bottomrow */ + pnm_readpamrow(&inpam, NULL); + + /* Note that we may be tempted just to quit after reaching the bottom + of the extracted image, but that would cause a broken pipe problem + for the process that's feeding us the image. + */ + } + + destroyRowCutter(rowCutterP); + + /* Write out bottom padding */ + if ((bottomrow - (inpam.height-1)) > 0) + writeBlackRows(&outpam, bottomrow - (inpam.height-1)); +} + + + +int +main(int argc, char *argv[]) { + + FILE * const ofP = stdout; + + struct cmdlineInfo cmdline; + FILE* ifP; + bool eof; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilespec); + + eof = FALSE; + while (!eof) { + cutOneImage(ifP, cmdline, ofP); + pnm_nextimage(ifP, &eof); + } + + pm_close(ifP); + pm_close(ofP); + + return 0; +} diff --git a/editor/pamcut.test b/editor/pamcut.test new file mode 100644 index 00000000..be70f1fd --- /dev/null +++ b/editor/pamcut.test @@ -0,0 +1,10 @@ +echo Test 1. Should print 2958909756 124815 +./pamcut -top 0 -left 0 -width 260 -height 160 -pad ../testimg.ppm | cksum +echo Test 2. Should print 1550940962 10933 +./pamcut -top 200 -left 120 -width 40 -height 40 -pad ../testimg.ppm | cksum +echo Test 3. Should print 708474423 14 +./pamcut -top 5 -left 5 -bottom 5 -right 5 ../testimg.ppm | cksum +echo Test 3. Should print 3412257956 129 +pbmmake -g 50 50 | ./pamcut 5 5 30 30 | cksum + + diff --git a/editor/pamdeinterlace.c b/editor/pamdeinterlace.c new file mode 100644 index 00000000..9ed1d8eb --- /dev/null +++ b/editor/pamdeinterlace.c @@ -0,0 +1,132 @@ +/****************************************************************************** + pamdeinterlace +******************************************************************************* + De-interlace an image, i.e. select every 2nd row. + + By Bryan Henderson, San Jose, CA 2001.11.11. + + Contributed to the public domain. +******************************************************************************/ + +#include "pam.h" +#include "shhopt.h" +#include "mallocvar.h" + +enum evenodd {EVEN, ODD}; + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *inputFilespec; /* Filespecs of input files */ + enum evenodd rowsToTake; +}; + + +static void +parseCommandLine(int argc, char ** argv, + struct cmdlineInfo *cmdlineP) { +/*---------------------------------------------------------------------------- + Note that the file spec array we return is stored in the storage that + was passed to us as the argv array. +-----------------------------------------------------------------------------*/ + optStruct3 opt; /* set by OPTENT3 */ + optEntry *option_def; + unsigned int option_def_index; + + unsigned int takeeven, takeodd; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "takeeven", OPT_FLAG, NULL, &takeeven, 0); + OPTENT3(0, "takeodd", OPT_FLAG, NULL, &takeodd, 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 */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (takeeven && takeodd) + pm_error("You cannot specify both -takeeven and -takeodd options."); + + if (takeodd) + cmdlineP->rowsToTake = ODD; + else + cmdlineP->rowsToTake = EVEN; + + if (argc-1 < 1) + cmdlineP->inputFilespec = "-"; + else if (argc-1 == 1) + cmdlineP->inputFilespec = argv[1]; + else + pm_error("You specified too many arguments (%d). The only " + "argument is the optional input file specification.", + argc-1); +} + + + + + +int +main(int argc, char *argv[]) { + + FILE * ifP; + tuple * tuplerow; /* Row from input image */ + unsigned int row; + struct cmdlineInfo cmdline; + struct pam inpam; + struct pam outpam; + + pnm_init( &argc, argv ); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilespec); + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + tuplerow = pnm_allocpamrow(&inpam); + + outpam = inpam; /* Initial value -- most fields should be same */ + outpam.file = stdout; + if (inpam.height % 2 == 0) + outpam.height = inpam.height / 2; + else { + if (cmdline.rowsToTake == ODD) + outpam.height = inpam.height / 2; + else + outpam.height = inpam.height / 2 + 1; + } + + pnm_writepaminit(&outpam); + + { + unsigned int modulusToTake; + /* The row number mod 2 of the rows that are supposed to go into + the output. + */ + + switch (cmdline.rowsToTake) { + case EVEN: modulusToTake = 0; break; + case ODD: modulusToTake = 1; break; + default: pm_error("INTERNAL ERROR: invalid rowsToTake"); + } + + /* Read input and write out rows extracted from it */ + for (row = 0; row < inpam.height; row++) { + pnm_readpamrow(&inpam, tuplerow); + if (row % 2 == modulusToTake) + pnm_writepamrow(&outpam, tuplerow); + } + } + pnm_freepamrow(tuplerow); + pm_close(inpam.file); + pm_close(outpam.file); + + return 0; +} + diff --git a/editor/pamdice.c b/editor/pamdice.c new file mode 100644 index 00000000..062e05e3 --- /dev/null +++ b/editor/pamdice.c @@ -0,0 +1,494 @@ +/***************************************************************************** + pamdice +****************************************************************************** + Slice a Netpbm image vertically and/or horizontally into multiple images. + + By Bryan Henderson, San Jose CA 2001.01.31 + + Contributed to the public domain. + +******************************************************************************/ + +#include <string.h> + +#include "pam.h" +#include "shhopt.h" +#include "nstring.h" +#include "mallocvar.h" + +#define MAXFILENAMELEN 80 + /* Maximum number of characters we accept in filenames */ + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char * inputFilespec; /* '-' if stdin */ + char * outstem; + /* null-terminated string, max MAXFILENAMELEN-10 characters */ + unsigned int sliceVertically; /* boolean */ + unsigned int sliceHorizontally; /* boolean */ + unsigned int width; /* Meaningless if !sliceVertically */ + unsigned int height; /* Meaningless if !sliceHorizontally */ + unsigned int hoverlap; + /* Meaningless if !sliceVertically. Guaranteed < width */ + unsigned int voverlap; + /* Meaningless if !sliceHorizontally. Guaranteed < height */ + unsigned int verbose; +}; + + +static void +parseCommandLine ( int argc, 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 outstemSpec, hoverlapSpec, voverlapSpec; + unsigned int option_def_index; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "width", OPT_UINT, &cmdlineP->width, + &cmdlineP->sliceVertically, 0 ); + OPTENT3(0, "height", OPT_UINT, &cmdlineP->height, + &cmdlineP->sliceHorizontally, 0 ); + OPTENT3(0, "hoverlap", OPT_UINT, &cmdlineP->hoverlap, + &hoverlapSpec, 0 ); + OPTENT3(0, "voverlap", OPT_UINT, &cmdlineP->voverlap, + &voverlapSpec, 0 ); + OPTENT3(0, "outstem", OPT_STRING, &cmdlineP->outstem, + &outstemSpec, 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 */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0 ); + /* Uses and sets argc, argv, and some of *cmdline_p and others. */ + + if (cmdlineP->sliceVertically) { + if (hoverlapSpec) { + if (cmdlineP->hoverlap > cmdlineP->width - 1) + pm_error("-hoverlap value must be less than -width (%u). " + "You specified %u.", + cmdlineP->width, cmdlineP->hoverlap); + } else + cmdlineP->hoverlap = 0; + } + if (cmdlineP->sliceHorizontally) { + if (voverlapSpec) { + if (cmdlineP->voverlap > cmdlineP->height - 1) + pm_error("-voverlap value must be less than -height (%u). " + "You specified %u.", + cmdlineP->height, cmdlineP->voverlap); + } else + cmdlineP->voverlap = 0; + } + + if (!outstemSpec) + pm_error("You must specify the -outstem option to indicate where to " + "put the output images."); + if (argc-1 < 1) + cmdlineP->inputFilespec = "-"; + else if (argc-1 == 1) + cmdlineP->inputFilespec = argv[1]; + else + pm_error("Progam takes at most 1 parameter: the file specification. " + "You specified %d", argc-1); +} + + + +static unsigned int +divup(unsigned int const dividend, + unsigned int const divisor) { +/*---------------------------------------------------------------------------- + Divide 'dividend' by 'divisor' and round up to the next whole number. +-----------------------------------------------------------------------------*/ + return (dividend + divisor - 1) / divisor; +} + + + +static void +computeSliceGeometry(struct cmdlineInfo const cmdline, + struct pam const inpam, + bool const verbose, + unsigned int * const nHorizSliceP, + unsigned int * const sliceHeightP, + unsigned int * const bottomSliceHeightP, + unsigned int * const nVertSliceP, + unsigned int * const sliceWidthP, + unsigned int * const rightSliceWidthP + ) { +/*---------------------------------------------------------------------------- + Compute the geometry of the slices, both common slices and possibly + smaller remainder slices at the top and right. +-----------------------------------------------------------------------------*/ + if (cmdline.sliceHorizontally) { + if (cmdline.height >= inpam.height) + *nHorizSliceP = 1; + else + *nHorizSliceP = 1 + divup(inpam.height - cmdline.height, + cmdline.height - cmdline.voverlap); + *sliceHeightP = cmdline.height; + } else { + *nHorizSliceP = 1; + *sliceHeightP = inpam.height; + } + + *bottomSliceHeightP = + inpam.height - (*nHorizSliceP-1) * (cmdline.height - cmdline.voverlap); + + if (cmdline.sliceVertically) { + if (cmdline.width >= inpam.width) + *nVertSliceP = 1; + else + *nVertSliceP = 1 + divup(inpam.width - cmdline.width, + cmdline.width - cmdline.hoverlap); + *sliceWidthP = cmdline.width; + } else { + *nVertSliceP = 1; + *sliceWidthP = inpam.width; + } + + *rightSliceWidthP = + inpam.width - (*nVertSliceP-1) * (cmdline.width - cmdline.hoverlap); + + if (verbose) { + pm_message("Creating %u images, %u across by %u down; " + "each %u w x %u h", + *nVertSliceP * *nHorizSliceP, + *nVertSliceP, *nHorizSliceP, + *sliceWidthP, *sliceHeightP); + if (*rightSliceWidthP != *sliceWidthP) + pm_message("Right vertical slice is only %u wide", + *rightSliceWidthP); + if (*bottomSliceHeightP != *sliceHeightP) + pm_message("Bottom horizontal slice is only %u high", + *bottomSliceHeightP); + } +} + + + +static unsigned int +ndigits(unsigned int const arg) { +/*---------------------------------------------------------------------------- + Return the minimum number of digits it takes to represent the number + 'arg' in decimal. +-----------------------------------------------------------------------------*/ + unsigned int leftover; + unsigned int i; + + for (leftover = arg, i = 0; leftover > 0; leftover /= 10, ++i); + + return MAX(1, i); +} + + + +static void +computeOutputFilenameFormat(int const format, + char const outstem[], + unsigned int const nHorizSlice, + unsigned int const nVertSlice, + const char ** const filenameFormatP) { + + const char * filenameSuffix; + + switch(PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: filenameSuffix = "ppm"; break; + case PGM_TYPE: filenameSuffix = "pgm"; break; + case PBM_TYPE: filenameSuffix = "pbm"; break; + case PAM_TYPE: filenameSuffix = "pam"; break; + default: filenameSuffix = ""; break; + } + + asprintfN(filenameFormatP, "%s_%%0%uu_%%0%uu.%s", + outstem, ndigits(nHorizSlice), ndigits(nVertSlice), + filenameSuffix); + + if (*filenameFormatP == NULL) + pm_error("Unable to allocate memory for filename format string"); +} + + + +static void +openOutStreams(struct pam const inpam, + struct pam outpam[], + unsigned int const horizSlice, + unsigned int const nHorizSlice, + unsigned int const nVertSlice, + unsigned int const sliceHeight, + unsigned int const sliceWidth, + unsigned int const rightSliceWidth, + unsigned int const hOverlap, + char const outstem[]) { +/*---------------------------------------------------------------------------- + Open the output files for a single horizontal slice (there's one file + for each vertical slice) and write the Netpbm headers to them. Also + compute the pam structures to control each. +-----------------------------------------------------------------------------*/ + const char * filenameFormat; + unsigned int vertSlice; + + computeOutputFilenameFormat(inpam.format, outstem, nHorizSlice, nVertSlice, + &filenameFormat); + + for (vertSlice = 0; vertSlice < nVertSlice; ++vertSlice) { + const char * filename; + + asprintfN(&filename, filenameFormat, horizSlice, vertSlice); + + if (filename == NULL) + pm_error("Unable to allocate memory for output filename"); + else { + outpam[vertSlice] = inpam; + outpam[vertSlice].file = pm_openw(filename); + + outpam[vertSlice].width = + vertSlice < nVertSlice-1 ? sliceWidth : rightSliceWidth; + + outpam[vertSlice].height = sliceHeight; + + pnm_writepaminit(&outpam[vertSlice]); + + strfree(filename); + } + } + strfree(filenameFormat); +} + + + +static void +closeOutFiles(struct pam pam[], unsigned int const nVertSlice) { + + unsigned int vertSlice; + + for (vertSlice = 0; vertSlice < nVertSlice; ++vertSlice) + pm_close(pam[vertSlice].file); +} + +static void +sliceRow(tuple inputRow[], + struct pam outpam[], + unsigned int const nVertSlice, + unsigned int const hOverlap) { +/*---------------------------------------------------------------------------- + Distribute the row inputRow[] across the 'nVerticalSlice' output + files described by outpam[]. Each outpam[x] tells how many columns + of inputRow[] to take and what their composition is. + + 'hOverlap', which is meaningful only when nVertSlice is greater than 1, + is the amount by which slices overlap each other. +-----------------------------------------------------------------------------*/ + tuple * outputRow; + unsigned int vertSlice; + unsigned int const sliceWidth = outpam[0].width; + unsigned int const stride = + nVertSlice > 1 ? sliceWidth - hOverlap : sliceWidth; + + for (vertSlice = 0, outputRow = inputRow; + vertSlice < nVertSlice; + outputRow += stride, ++vertSlice) { + pnm_writepamrow(&outpam[vertSlice], outputRow); + } +} + + +/*---------------------------------------------------------------------------- + The input reader. This just reads the input image row by row, except + that it lets us back up up to a predefined amount (the window size). + When we're overlapping horizontal slices, that's useful. It's not as + simple as just reading the entire image into memory at once, but uses + a lot less memory. +-----------------------------------------------------------------------------*/ + +struct inputWindow { + unsigned int windowSize; + unsigned int firstRowInWindow; + struct pam pam; + tuple ** rows; +}; + +static void +initInput(struct inputWindow * const inputWindowP, + struct pam * const pamP, + unsigned int const windowSize) { + + struct pam allocPam; /* Just for allocating the window array */ + unsigned int i; + + inputWindowP->pam = *pamP; + inputWindowP->windowSize = windowSize; + + allocPam = *pamP; + allocPam.height = windowSize; + + inputWindowP->rows = pnm_allocpamarray(&allocPam); + + inputWindowP->firstRowInWindow = 0; + + /* Fill the window with the beginning of the image */ + for (i = 0; i < windowSize && i < pamP->height; ++i) + pnm_readpamrow(&inputWindowP->pam, inputWindowP->rows[i]); +} + +static void +termInputWindow(struct inputWindow * const inputWindowP) { + + struct pam freePam; /* Just for freeing window array */ + + freePam = inputWindowP->pam; + freePam.height = inputWindowP->windowSize; + + pnm_freepamarray(inputWindowP->rows, &freePam); +} + +static tuple * +getInputRow(struct inputWindow * const inputWindowP, + unsigned int const row) { + + if (row < inputWindowP->firstRowInWindow) + pm_error("INTERNAL ERROR: attempt to back up too far with " + "getInputRow() (row %u)", row); + if (row >= inputWindowP->pam.height) + pm_error("INTERNAL ERROR: attempt to read beyond bottom of " + "input image (row %u)", row); + + while (row >= inputWindowP->firstRowInWindow + inputWindowP->windowSize) { + tuple * const oldRow0 = inputWindowP->rows[0]; + unsigned int i; + /* Slide the window down one row */ + for (i = 0; i < inputWindowP->windowSize - 1; ++i) + inputWindowP->rows[i] = inputWindowP->rows[i+1]; + ++inputWindowP->firstRowInWindow; + + /* Read in the new last row in the window */ + inputWindowP->rows[i] = oldRow0; /* Reuse the memory */ + pnm_readpamrow(&inputWindowP->pam, inputWindowP->rows[i]); + } + + return inputWindowP->rows[row - inputWindowP->firstRowInWindow]; +} + +/*----- end of input reader ----------------------------------------------*/ + + + +static void +allocOutpam(unsigned int const nVertSlice, + struct pam ** const outpamArrayP) { + + struct pam * outpamArray; + + MALLOCARRAY(outpamArray, nVertSlice); + + if (outpamArray == NULL) + pm_error("Unable to allocate array for %u output pam structures.", + nVertSlice); + + *outpamArrayP = outpamArray; +} + + + +int +main(int argc, char ** argv) { + + struct cmdlineInfo cmdline; + FILE *ifP; + struct pam inpam; + unsigned int horizSlice; + /* Number of the current horizontal slice. Slices are numbered + sequentially starting at 0. + */ + unsigned int sliceWidth; + /* Width in pam columns of each vertical slice, except + the rightmost slice, which may be narrower. If we aren't slicing + vertically, that means one slice, i.e. the slice width + is the image width. + */ + unsigned int rightSliceWidth; + /* Width in pam columns of the rightmost vertical slice. */ + unsigned int sliceHeight; + /* Height in pam rows of each horizontal slice, except + the bottom slice, which may be shorter. If we aren't slicing + horizontally, that means one slice, i.e. the slice height + is the image height. + */ + unsigned int bottomSliceHeight; + /* Height in pam rows of the bottom horizontal slice. */ + unsigned int nHorizSlice; + unsigned int nVertSlice; + struct inputWindow inputWindow; + + struct pam * outpam; + /* malloc'ed. outpam[x] is the pam structure that controls + the current horizontal slice of vertical slice x. + */ + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilespec); + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + computeSliceGeometry(cmdline, inpam, !!cmdline.verbose, + &nHorizSlice, &sliceHeight, &bottomSliceHeight, + &nVertSlice, &sliceWidth, &rightSliceWidth); + + allocOutpam(nVertSlice, &outpam); + + initInput(&inputWindow, &inpam, + nHorizSlice > 1 ? cmdline.voverlap + 1 : 1); + + for (horizSlice = 0; horizSlice < nHorizSlice; ++horizSlice) { + unsigned int const thisSliceFirstRow = + horizSlice * (sliceHeight - cmdline.voverlap); + unsigned int const thisSliceHeight = + horizSlice < nHorizSlice-1 ? sliceHeight : bottomSliceHeight; + + unsigned int row; + + openOutStreams(inpam, outpam, horizSlice, nHorizSlice, nVertSlice, + thisSliceHeight, sliceWidth, rightSliceWidth, + cmdline.hoverlap, cmdline.outstem); + + for (row = 0; row < thisSliceHeight; ++row) { + tuple * const inputRow = + getInputRow(&inputWindow, thisSliceFirstRow + row); + sliceRow(inputRow, outpam, nVertSlice, cmdline.hoverlap); + } + closeOutFiles(outpam, nVertSlice); + } + + termInputWindow(&inputWindow); + + free(outpam); + + pm_close(ifP); + + return 0; +} diff --git a/editor/pamdither.c b/editor/pamdither.c new file mode 100644 index 00000000..5eb931a6 --- /dev/null +++ b/editor/pamdither.c @@ -0,0 +1,319 @@ +/*============================================================================= + pamdither +=============================================================================== + By Bryan Henderson, July 2006. + + Contributed to the public domain. + + This is meant to replace Ppmdither by Christos Zoulas, 1991. +=============================================================================*/ + +#include "mallocvar.h" +#include "shhopt.h" +#include "pam.h" + +/* Besides having to have enough memory available, the limiting factor + in the dithering matrix power is the size of the dithering value. + We need 2*dith_power bits in an unsigned int. We also reserve + one bit to give headroom to do calculations with these numbers. +*/ +#define MAX_DITH_POWER ((sizeof(unsigned int)*8 - 1) / 2) + + +/* COLOR(): + * returns the index in the colormap for the + * r, g, b values specified. + */ +#define COLOR(r,g,b) (((r) * dith_ng + (g)) * dith_nb + (b)) + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char * inputFileName; /* File name of input file */ + const char * mapFileName; /* File name of colormap file */ + unsigned int dim; + unsigned int verbose; +}; + + + +static void +parseCommandLine (int argc, char ** argv, + struct cmdlineInfo *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 option_def_index; + + unsigned int dimSpec, mapfileSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "dim", OPT_UINT, + &cmdlineP->dim, &dimSpec, 0); + OPTENT3(0, "mapfile", OPT_STRING, + &cmdlineP->mapFilespec, &mapfileSpec, 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 */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0 ); + /* Uses and sets argc, argv, and some of *cmdline_p and others. */ + + if (!dimSpec) + cmdlineP->dim = 4; + + if (cmdlineP->dim > MAX_DITH_POWER) + pm_error("Dithering matrix power %u (-dim) is too large. " + "Must be <= %d", + dithPower, MAX_DITH_POWER); + + if (!mapfileSpec) + pm_error("You must specify the -mapfile option."); + + if (argc-1 > 1) + pm_error("Program takes at most one argument: the input file " + "specification. " + "You specified %d arguments.", argc-1); + if (argc-1 < 1) + cmdlineP->inputFilespec = "-"; + else + cmdlineP->inputFilespec = argv[1]; +} + + + +static unsigned int +dither(sample const p, + sample const maxval, + unsigned int const d, + unsigned int const ditheredMaxval, + unsigned int const ditherMatrixArea) { +/*---------------------------------------------------------------------------- + Return the dithered brightness for a component of a pixel whose real + brightness for that component is 'p' based on a maxval of 'maxval'. + The returned brightness is based on a maxval of ditheredMaxval. + + 'd' is the entry in the dithering matrix for the position of this pixel + within the dithered square. + + 'ditherMatrixArea' is the area (number of pixels in) the dithered square. +-----------------------------------------------------------------------------*/ + unsigned int const ditherSquareMaxval = ditheredMaxval * ditherMatrixArea; + /* This is the maxval for an intensity that an entire dithered + square can represent. + */ + pixval const pScaled = ditherSquareMaxval * p / maxval; + /* This is the input intensity P expressed with a maxval of + 'ditherSquareMaxval' + */ + + /* Now we scale the intensity back down to the 'ditheredMaxval', and + as that will involve rounding, we round up or down based on the position + in the dithered square, as determined by 'd' + */ + + return (pScaled + d) / ditherMatrixArea; +} + + + +static unsigned int +dithValue(unsigned int const y, + unsigned int const x, + unsigned int const dithPower) { +/*---------------------------------------------------------------------------- + Return the value of a dither matrix which is 2 ** dithPower elements + square at Row x, Column y. + [graphics gems, p. 714] +-----------------------------------------------------------------------------*/ + unsigned int d; + /* + Think of d as the density. At every iteration, d is shifted + left one and a new bit is put in the low bit based on x and y. + If x is odd and y is even, or visa versa, then a bit is shifted in. + This generates the checkerboard pattern seen in dithering. + This quantity is shifted again and the low bit of y is added in. + This whole thing interleaves a checkerboard pattern and y's bits + which is what you want. + */ + unsigned int i; + + for (i = 0, d = 0; i < dithPower; i++, x >>= 1, y >>= 1) + d = (d << 2) | (((x & 1) ^ (y & 1)) << 1) | (y & 1); + + return(d); +} + + + +static unsigned int ** +dithMatrix(unsigned int const dithPower) { +/*---------------------------------------------------------------------------- + Create the dithering matrix for dimension 'dithDim'. + + Return it in newly malloc'ed storage. + + Note that we assume 'dith_dim' is small enough that the dith_mat_sz + computed within fits in an int. Otherwise, results are undefined. +-----------------------------------------------------------------------------*/ + unsigned int const dithDim = 1 << dithPower; + + unsigned int ** dithMat; + + assert(dithPower < sizeof(unsigned int) * 8); + + { + unsigned int const dithMatSize = + (dithDim * sizeof(*dithMat)) + /* pointers */ + (dithDim * dithDim * sizeof(**dithMat)); /* data */ + + dithMat = malloc(dithMatSize); + + if (dithMat == NULL) + pm_error("Out of memory. " + "Cannot allocate %d bytes for dithering matrix.", + dithMatSize); + } + { + unsigned int * const rowStorage = (unsigned int *)&dithMat[dithDim]; + unsigned int y; + for (y = 0; y < dithDim; ++y) + dithMat[y] = &rowStorage[y * dithDim]; + } + { + unsigned int y; + for (y = 0; y < dithDim; ++y) { + unsigned int x; + for (x = 0; x < dithDim; ++x) + dithMat[y][x] = dithValue(y, x, dithPower); + } + } + return dithMat; +} + + + +static void +ditherImage(struct pam const inpam, + tuple * const colormap, + unsigned int const dithPower, + struct pam const outpam; + tuple ** const inTuples, + tuple *** const outTuplesP) { + + unsigned int const dithDim = 1 << dithPower; + unsigned int const ditherMatrixArea = SQR(dithDim); + + unsigned int const modMask = (dithDim - 1); + /* And this into N to compute N % dithDim cheaply, since we + know (though the compiler doesn't) that dithDim is a power of 2 + */ + unsigned int ** const ditherMatrix = dithMatrix(dithPower); + + tuple ** ouputTuples; + unsigned int row; + + assert(dithPower < sizeof(unsigned int) * 8); + assert(UINT_MAX / dithDim >= dithDim); + + outTuples = ppm_allocpamarray(outpam); + + for (row = 0; row < inpam.height; ++row) { + unsigned int col; + for (col = 0; col < inpam.width; ++col) { + unsigned int const d = + ditherMatrix[row & modMask][(width-col-1) & modMask]; + tuple const inputTuple = inTuples[row][col]; + unsigned int dithered[3]; + + unsigned int plane; + + assert(inpam.depth >= 3); + + for (plane = 0; plane < 3; ++plane) + dithered[plane] = + dither(inputTuple[plane], inpam.maxval, d, outpam.maxval, + ditherMatrixArea); + + outTuples[row][col] = + colormap[COLOR(dithered[RED_PLANE], + dithered[GRN_PLANE], + dithered[BLU_PLANE])]; + } + } + free(ditherMatrix); + *outTuplesP = outTuples; +} + + + +static void +getColormap(const char * const mapFileName, + tuple ** const colormapP) { + + TODO("write this"); + +} + + + +int +main(int argc, + char ** argv) { + + struct cmdlineInfo cmdline; + FILE * ifP; + tuple ** inTuples; /* Input image */ + tuple ** outTuples; /* Output image */ + tuple * colormap; + int cols, rows; + pixval maxval; /* Maxval of the input image */ + + struct pam outpamCommon; + /* Describes the output images. Width and height fields are + not meaningful, because different output images might have + different dimensions. The rest of the information is common + across all output images. + */ + + pnm_init(&argc, argv); + + parseCommandLine(&argc, &argv); + + pm_openr(cmdline.inputFileName); + + inTuples = pnm_readpam(ifP, &inpam, PAM_STRUCT_SIZE(allocation_depth)); + pm_close(ifP); + + getColormap(cmdline.mapFileName, &colormap); + + ditherImage(inpam, colormap, dithPower, inTuples, &outTuples); + + ppm_writeppm(stdout, opixels, cols, rows, outputMaxval, 0); + pm_close(stdout); + + free(colormap); + + pnm_freepamarray(inTuples, &inpam); + pnm_freepamarray(outTuples, &outpam); + + return 0; +} diff --git a/editor/pamditherbw.c b/editor/pamditherbw.c new file mode 100644 index 00000000..61c23103 --- /dev/null +++ b/editor/pamditherbw.c @@ -0,0 +1,743 @@ +/*============================================================================= + pamditherbw +=============================================================================== + Dither a grayscale PAM to a black and white PAM. + + By Bryan Henderson, San Jose CA. June 2004. + + Contributed to the public domain by its author. + + Based on ideas from Pgmtopbm by Jef Poskanzer, 1989. +=============================================================================*/ + +#include <assert.h> +#include <string.h> + +#include "pam.h" +#include "dithers.h" +#include "mallocvar.h" +#include "shhopt.h" +#include "pm_gamma.h" + +enum halftone {QT_FS, QT_THRESH, QT_DITHER8, QT_CLUSTER, QT_HILBERT}; + +enum ditherType {DT_REGULAR, DT_CLUSTER}; + + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char * inputFilespec; + enum halftone halftone; + unsigned int clumpSize; + /* Defined only for halftone == QT_HILBERT */ + unsigned int clusterRadius; + /* Defined only for halftone == QT_CLUSTER */ + float threshval; +}; + + + + +static void +parseCommandLine(int argc, char ** argv, + struct cmdlineInfo *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 floydOpt, hilbertOpt, thresholdOpt, dither8Opt, + cluster3Opt, cluster4Opt, cluster8Opt; + unsigned int valueSpec, clumpSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENT3(0, "floyd", OPT_FLAG, NULL, &floydOpt, 0); + OPTENT3(0, "fs", OPT_FLAG, NULL, &floydOpt, 0); + OPTENT3(0, "threshold", OPT_FLAG, NULL, &thresholdOpt, 0); + OPTENT3(0, "hilbert", OPT_FLAG, NULL, &hilbertOpt, 0); + OPTENT3(0, "dither8", OPT_FLAG, NULL, &dither8Opt, 0); + OPTENT3(0, "d8", OPT_FLAG, NULL, &dither8Opt, 0); + OPTENT3(0, "cluster3", OPT_FLAG, NULL, &cluster3Opt, 0); + OPTENT3(0, "c3", OPT_FLAG, NULL, &cluster3Opt, 0); + OPTENT3(0, "cluster4", OPT_FLAG, NULL, &cluster4Opt, 0); + OPTENT3(0, "c4", OPT_FLAG, NULL, &cluster4Opt, 0); + OPTENT3(0, "cluster8", OPT_FLAG, NULL, &cluster8Opt, 0); + OPTENT3(0, "c8", OPT_FLAG, NULL, &cluster8Opt, 0); + OPTENT3(0, "value", OPT_FLOAT, &cmdlineP->threshval, + &valueSpec, 0); + OPTENT3(0, "clump", OPT_UINT, &cmdlineP->clumpSize, + &clumpSpec, 0); + + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ + opt.allowNegNum = FALSE; /* We may have parms that are negative numbers */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (floydOpt + thresholdOpt + hilbertOpt + dither8Opt + + cluster3Opt + cluster4Opt + cluster8Opt == 0) + cmdlineP->halftone = QT_FS; + else if (floydOpt + thresholdOpt + dither8Opt + + cluster3Opt + cluster4Opt + cluster8Opt > 1) + pm_error("No cannot specify more than one halftoning type"); + else { + if (floydOpt) + cmdlineP->halftone = QT_FS; + else if (thresholdOpt) + cmdlineP->halftone = QT_THRESH; + else if (hilbertOpt) { + cmdlineP->halftone = QT_HILBERT; + + if (!clumpSpec) + cmdlineP->clumpSize = 5; + else { + if (cmdlineP->clumpSize < 2) + pm_error("-clump must be at least 2. You specified %u", + cmdlineP->clumpSize); + } + } else if (dither8Opt) + cmdlineP->halftone = QT_DITHER8; + else if (cluster3Opt) { + cmdlineP->halftone = QT_CLUSTER; + cmdlineP->clusterRadius = 3; + } else if (cluster4Opt) { + cmdlineP->halftone = QT_CLUSTER; + cmdlineP->clusterRadius = 4; + } else if (cluster8Opt) { + cmdlineP->halftone = QT_CLUSTER; + cmdlineP->clusterRadius = 8; + } else + pm_error("INTERNAL ERROR. No halftone option"); + } + + if (!valueSpec) + cmdlineP->threshval = 0.5; + else { + if (cmdlineP->threshval < 0.0) + pm_error("-value cannot be negative. You specified %f", + cmdlineP->threshval); + if (cmdlineP->threshval > 1.0) + pm_error("-value cannot be greater than one. You specified %f", + cmdlineP->threshval); + } + + if (clumpSpec && cmdlineP->halftone != QT_HILBERT) + pm_error("-clump is not valid without -hilbert"); + + if (argc-1 > 1) + pm_error("Too many arguments (%d). There is at most one " + "non-option argument: the file name", + argc-1); + else if (argc-1 == 1) + cmdlineP->inputFilespec = argv[1]; + else + cmdlineP->inputFilespec = "-"; +} + + +static struct pam +makeOutputPam(unsigned int const width, + unsigned int const height) { + + struct pam outpam; + + outpam.size = sizeof(outpam); + outpam.len = PAM_STRUCT_SIZE(tuple_type); + outpam.file = stdout; + outpam.format = PAM_FORMAT; + outpam.plainformat = 0; + outpam.height = height; + outpam.width = width; + outpam.depth = 1; + outpam.maxval = 1; + outpam.bytes_per_sample = 1; + strcpy(outpam.tuple_type, "BLACKANDWHITE"); + + return outpam; +} + + + +/* Hilbert curve tracer */ + +#define MAXORD 18 + +static int hil_order,hil_ord; +static int hil_turn; +static int hil_dx,hil_dy; +static int hil_x,hil_y; +static int hil_stage[MAXORD]; +static int hil_width,hil_height; + +static void +initHilbert(int const w, + int const h) { +/*---------------------------------------------------------------------------- + Initialize the Hilbert curve tracer +-----------------------------------------------------------------------------*/ + int big,ber; + hil_width = w; + hil_height = h; + big = w > h ? w : h; + for (ber = 2, hil_order = 1; ber < big; ber <<= 1, hil_order++); + if (hil_order > MAXORD) + pm_error("Sorry, hilbert order is too large"); + hil_ord = hil_order; + hil_order--; +} + + + +static int +hilbert(int * const px, int * const py) { +/*---------------------------------------------------------------------------- + Return non-zero if got another point +-----------------------------------------------------------------------------*/ + int temp; + if (hil_ord > hil_order) { + /* have to do first point */ + + hil_ord--; + hil_stage[hil_ord] = 0; + hil_turn = -1; + hil_dy = 1; + hil_dx = hil_x = hil_y = 0; + *px = *py = 0; + return 1; + } + + /* Operate the state machine */ + for(;;) { + switch (hil_stage[hil_ord]) { + case 0: + hil_turn = -hil_turn; + temp = hil_dy; + hil_dy = -hil_turn * hil_dx; + hil_dx = hil_turn * temp; + if (hil_ord > 0) { + hil_stage[hil_ord] = 1; + hil_ord--; + hil_stage[hil_ord]=0; + continue; + } + case 1: + hil_x += hil_dx; + hil_y += hil_dy; + if (hil_x < hil_width && hil_y < hil_height) { + hil_stage[hil_ord] = 2; + *px = hil_x; + *py = hil_y; + return 1; + } + case 2: + hil_turn = -hil_turn; + temp = hil_dy; + hil_dy = -hil_turn * hil_dx; + hil_dx = hil_turn * temp; + if (hil_ord > 0) { + /* recurse */ + + hil_stage[hil_ord] = 3; + hil_ord--; + hil_stage[hil_ord]=0; + continue; + } + case 3: + hil_x += hil_dx; + hil_y += hil_dy; + if (hil_x < hil_width && hil_y < hil_height) { + hil_stage[hil_ord] = 4; + *px = hil_x; + *py = hil_y; + return 1; + } + case 4: + if (hil_ord > 0) { + /* recurse */ + hil_stage[hil_ord] = 5; + hil_ord--; + hil_stage[hil_ord]=0; + continue; + } + case 5: + temp = hil_dy; + hil_dy = -hil_turn * hil_dx; + hil_dx = hil_turn * temp; + hil_turn = -hil_turn; + hil_x += hil_dx; + hil_y += hil_dy; + if (hil_x < hil_width && hil_y < hil_height) { + hil_stage[hil_ord] = 6; + *px = hil_x; + *py = hil_y; + return 1; + } + case 6: + if (hil_ord > 0) { + /* recurse */ + hil_stage[hil_ord] = 7; + hil_ord--; + hil_stage[hil_ord]=0; + continue; + } + case 7: + temp = hil_dy; + hil_dy = -hil_turn * hil_dx; + hil_dx = hil_turn * temp; + hil_turn = -hil_turn; + /* Return from a recursion */ + if (hil_ord < hil_order) + hil_ord++; + else + return 0; + } + } +} + + + +static void +doHilbert(FILE * const ifP, + unsigned int const clumpSize) { +/*---------------------------------------------------------------------------- + Use hilbert space filling curve dithering +-----------------------------------------------------------------------------*/ + /* + * This is taken from the article "Digital Halftoning with + * Space Filling Curves" by Luiz Velho, proceedings of + * SIGRAPH '91, page 81. + * + * This is not a terribly efficient or quick version of + * this algorithm, but it seems to work. - Graeme Gill. + * graeme@labtam.labtam.OZ.AU + * + */ + struct pam graypam; + struct pam bitpam; + tuple ** grays; + tuple ** bits; + + int end; + int *x,*y; + int sum; + + grays = pnm_readpam(ifP, &graypam, sizeof(graypam)); + + bitpam = makeOutputPam(graypam.width, graypam.height); + + bits = pnm_allocpamarray(&bitpam); + + MALLOCARRAY(x, clumpSize); + MALLOCARRAY(y, clumpSize); + if (x == NULL || y == NULL) + pm_error("out of memory"); + initHilbert(graypam.width, graypam.height); + + sum = 0; + end = clumpSize; + + while (end == clumpSize) { + unsigned int i; + /* compute the next cluster co-ordinates along hilbert path */ + for (i = 0; i < end; i++) { + if (hilbert(&x[i],&y[i])==0) + end = i; /* we reached the end */ + } + /* sum levels */ + for (i = 0; i < end; i++) + sum += grays[y[i]][x[i]][0]; + /* dither half and half along path */ + for (i = 0; i < end; i++) { + unsigned int const row = y[i]; + unsigned int const col = x[i]; + if (sum >= graypam.maxval) { + bits[row][col][0] = 1; + sum -= graypam.maxval; + } else + bits[row][col][0] = 0; + } + } + pnm_writepam(&bitpam, bits); + + pnm_freepamarray(bits, &bitpam); + pnm_freepamarray(grays, &graypam); +} + + + +struct converter { + void (*convertRow)(struct converter * const converterP, + unsigned int const row, + tuplen grayrow[], + tuple bitrow[]); + void (*destroy)(struct converter * const converterP); + unsigned int cols; + void * stateP; +}; + + + +struct fsState { + float * thiserr; + float * nexterr; + bool fs_forward; + samplen threshval; + /* The power value we consider to be half white */ +}; + + +static void +fsConvertRow(struct converter * const converterP, + unsigned int const row, + tuplen grayrow[], + tuple bitrow[]) { + + struct fsState * const stateP = converterP->stateP; + + samplen * const thiserr = stateP->thiserr; + samplen * const nexterr = stateP->nexterr; + + unsigned int limitcol; + unsigned int col; + + for (col = 0; col < converterP->cols + 2; ++col) + nexterr[col] = 0.0; + + if (stateP->fs_forward) { + col = 0; + limitcol = converterP->cols; + } else { + col = converterP->cols - 1; + limitcol = -1; + } + + do { + samplen sum; + + sum = pm_ungamma709(grayrow[col][0]) + thiserr[col + 1]; + if (sum >= stateP->threshval) { + /* We've accumulated enough light to justify a white output + pixel. + */ + bitrow[col][0] = PAM_BW_WHITE; + /* Remove from sum the power of the white output pixel */ + sum -= 2*stateP->threshval; + } else + bitrow[col][0] = PAM_BLACK; + + /* Forward the power from current input pixel and the power + forwarded from previous input pixels to the current pixel, + to future output pixels, but subtract out any power we put + into the current output pixel. + */ + if (stateP->fs_forward) { + thiserr[col + 2] += (sum * 7) / 16; + nexterr[col ] += (sum * 3) / 16; + nexterr[col + 1] += (sum * 5) / 16; + nexterr[col + 2] += (sum ) / 16; + + ++col; + } else { + thiserr[col ] += (sum * 7) / 16; + nexterr[col + 2] += (sum * 3) / 16; + nexterr[col + 1] += (sum * 5) / 16; + nexterr[col ] += (sum ) / 16; + + --col; + } + } while (col != limitcol); + + stateP->thiserr = nexterr; + stateP->nexterr = thiserr; + stateP->fs_forward = ! stateP->fs_forward; +} + + + +static void +fsDestroy(struct converter * const converterP) { + free(converterP->stateP); +} + + + +static struct converter +createFsConverter(struct pam * const graypamP, + float const threshFraction) { + + struct fsState * stateP; + struct converter converter; + + converter.cols = graypamP->width; + converter.convertRow = &fsConvertRow; + converter.destroy = &fsDestroy; + + MALLOCVAR_NOFAIL(stateP); + + /* Initialize Floyd-Steinberg error vectors. */ + MALLOCARRAY_NOFAIL(stateP->thiserr, graypamP->width + 2); + MALLOCARRAY_NOFAIL(stateP->nexterr, graypamP->width + 2); + srand((int)(time(NULL) ^ getpid())); + + { + /* (random errors in [-1/8 .. 1/8]) */ + unsigned int col; + for (col = 0; col < graypamP->width + 2; ++col) + stateP->thiserr[col] = ((float)rand()/RAND_MAX - 0.5) / 4; + } + + stateP->threshval = threshFraction; + + stateP->fs_forward = TRUE; + + converter.stateP = stateP; + + return converter; +} + + + +struct threshState { + samplen threshval; +}; + + +static void +threshConvertRow(struct converter * const converterP, + unsigned int const row, + tuplen grayrow[], + tuple bitrow[]) { + + struct threshState * const stateP = converterP->stateP; + + unsigned int col; + for (col = 0; col < converterP->cols; ++col) + bitrow[col][0] = + grayrow[col][0] >= stateP->threshval ? PAM_BW_WHITE : PAM_BLACK; +} + + + +static void +threshDestroy(struct converter * const converterP) { + free(converterP->stateP); +} + + + +static struct converter +createThreshConverter(struct pam * const graypamP, + float const threshFraction) { + + struct threshState * stateP; + struct converter converter; + + MALLOCVAR_NOFAIL(stateP); + + converter.cols = graypamP->width; + converter.convertRow = &threshConvertRow; + converter.destroy = &threshDestroy; + + stateP->threshval = threshFraction; + converter.stateP = stateP; + + return converter; +} + + + +struct clusterState { + unsigned int radius; + float ** clusterMatrix; +}; + + + +static void +clusterConvertRow(struct converter * const converterP, + unsigned int const row, + tuplen grayrow[], + tuple bitrow[]) { + + struct clusterState * const stateP = converterP->stateP; + unsigned int const diameter = 2 * stateP->radius; + + unsigned int col; + + for (col = 0; col < converterP->cols; ++col) { + float const threshold = + stateP->clusterMatrix[row % diameter][col % diameter]; + bitrow[col][0] = + grayrow[col][0] > threshold ? PAM_BW_WHITE : PAM_BLACK; + } +} + + + +static void +clusterDestroy(struct converter * const converterP) { + + struct clusterState * const stateP = converterP->stateP; + unsigned int const diameter = 2 * stateP->radius; + + unsigned int row; + + for (row = 0; row < diameter; ++row) + free(stateP->clusterMatrix[row]); + + free(stateP->clusterMatrix); + + free(stateP); +} + + + +static struct converter +createClusterConverter(struct pam * const graypamP, + enum ditherType const ditherType, + unsigned int const radius) { + + /* TODO: We create a floating point normalized, gamma-adjusted + dither matrix from the old integer dither matrices that were + developed for use with integer arithmetic. We really should + just change the literal values in dither.h instead of computing + the matrix from the integer literal values here. + */ + + int const clusterNormalizer = radius * radius * 2; + unsigned int const diameter = 2 * radius; + + struct converter converter; + struct clusterState * stateP; + unsigned int row; + + converter.cols = graypamP->width; + converter.convertRow = &clusterConvertRow; + converter.destroy = &clusterDestroy; + + MALLOCVAR_NOFAIL(stateP); + + stateP->radius = radius; + + MALLOCARRAY_NOFAIL(stateP->clusterMatrix, diameter); + for (row = 0; row < diameter; ++row) { + unsigned int col; + + MALLOCARRAY_NOFAIL(stateP->clusterMatrix[row], diameter); + + for (col = 0; col < diameter; ++col) { + switch (ditherType) { + case DT_REGULAR: + switch (radius) { + case 8: + stateP->clusterMatrix[row][col] = + pm_gamma709((float)dither8[row][col] / 256); + break; + default: + pm_error("INTERNAL ERROR: invalid radius"); + } + break; + case DT_CLUSTER: { + int val; + switch (radius) { + case 3: val = cluster3[row][col]; break; + case 4: val = cluster4[row][col]; break; + case 8: val = cluster8[row][col]; break; + default: + pm_error("INTERNAL ERROR: invalid radius"); + } + stateP->clusterMatrix[row][col] = + pm_gamma709((float)val / clusterNormalizer); + } + break; + } + } + } + + converter.stateP = stateP; + + return converter; +} + + + +int +main(int argc, char *argv[]) { + + struct cmdlineInfo cmdline; + FILE* ifP; + + pgm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilespec); + + if (cmdline.halftone == QT_HILBERT) + doHilbert(ifP, cmdline.clumpSize); + else { + struct converter converter; + struct pam graypam; + struct pam bitpam; + tuplen * grayrow; + tuple * bitrow; + int row; + + pnm_readpaminit(ifP, &graypam, sizeof(graypam)); + + bitpam = makeOutputPam(graypam.width, graypam.height); + + pnm_writepaminit(&bitpam); + + switch (cmdline.halftone) { + case QT_FS: + converter = createFsConverter(&graypam, cmdline.threshval); + break; + case QT_THRESH: + converter = createThreshConverter(&graypam, cmdline.threshval); + break; + case QT_DITHER8: + converter = createClusterConverter(&graypam, DT_REGULAR, 8); + break; + case QT_CLUSTER: + converter = createClusterConverter(&graypam, + DT_CLUSTER, + cmdline.clusterRadius); + break; + case QT_HILBERT: + pm_error("INTERNAL ERROR: halftone is QT_HILBERT where it " + "shouldn't be."); + break; + } + + grayrow = pnm_allocpamrown(&graypam); + bitrow = pnm_allocpamrow(&bitpam); + + for (row = 0; row < graypam.height; ++row) { + pnm_readpamrown(&graypam, grayrow); + + converter.convertRow(&converter, row, grayrow, bitrow); + + pnm_writepamrow(&bitpam, bitrow); + } + pnm_freepamrow(bitrow); + pnm_freepamrow(grayrow); + + if (converter.destroy) + converter.destroy(&converter); + } + + pm_close(ifP); + + return 0; +} diff --git a/editor/pamedge.c b/editor/pamedge.c new file mode 100644 index 00000000..e73c9d17 --- /dev/null +++ b/editor/pamedge.c @@ -0,0 +1,203 @@ +/* pnmedge.c - edge-detection +** +** Copyright (C) 1989 by Jef Poskanzer. +** modified for pnm by Peter Kirchgessner, 1995. +** +** 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. +*/ + +#include <math.h> + +#include "pm_c_util.h" +#include "pam.h" + + + +static void +writeBlackRow(struct pam * const pamP) { + + tuple * const tuplerow = pnm_allocpamrow(pamP); + + unsigned int col; + for (col = 0; col < pamP->width; ++col) { + unsigned int plane; + for (plane = 0; plane < pamP->depth; ++plane) + tuplerow[col][plane] = 0; + } + pnm_writepamrow(pamP, tuplerow); +} + + + +static void +rotateRows(tuple ** const row0P, + tuple ** const row1P, + tuple ** const row2P) { + /* Rotate rows. */ + tuple * const formerRow0 = *row0P; + *row0P = *row1P; + *row1P = *row2P; + *row2P = formerRow0; +} + + + +static long +horizGradient(tuple * const tuplerow, + unsigned int const col, + unsigned int const plane) { + + return (long)tuplerow[col+1][plane] - (long)tuplerow[col-1][plane]; +} + + + +static long +horizAvg(tuple * const tuplerow, + unsigned int const col, + unsigned int const plane) { + + return + 1 * (long)tuplerow[col-1][plane] + + 2 * (long)tuplerow[col ][plane] + + 1 * (long)tuplerow[col+1][plane]; + +} + + + +static void +computeOneRow(struct pam * const inpamP, + struct pam * const outpamP, + tuple * const row0, + tuple * const row1, + tuple * const row2, + tuple * const orow) { +/*---------------------------------------------------------------------------- + Compute an output row from 3 input rows. + + The input rows must have the same maxval as the output row. +-----------------------------------------------------------------------------*/ + unsigned int plane; + + for (plane = 0; plane < inpamP->depth; ++plane) { + unsigned int col; + + /* Left column is black */ + orow[0][plane] = 0; + + for (col = 1; col < inpamP->width - 1; ++col) { + double const grad1 = + 1 * horizGradient(row0, col, plane) + + 2 * horizGradient(row1, col, plane) + + 1 * horizGradient(row2, col, plane); + + double const grad2 = + horizAvg(row2, col, plane) - horizAvg(row0, col, plane); + + double const gradient = sqrt(SQR(grad1) + SQR(grad2)); + + /* apply arbitrary scaling factor and maxval clipping */ + orow[col][plane] = MIN(outpamP->maxval, (long)(gradient / 1.8)); + + /* Right column is black */ + orow[inpamP->width - 1][plane] = 0; + } + } +} + + + +static void +writeMiddleRows(struct pam * const inpamP, + struct pam * const outpamP) { + + tuple *row0, *row1, *row2; + tuple *orow, *irow; + unsigned int row; + + irow = pnm_allocpamrow(inpamP); + orow = pnm_allocpamrow(outpamP); + row0 = pnm_allocpamrow(outpamP); + row1 = pnm_allocpamrow(outpamP); + row2 = pnm_allocpamrow(outpamP); + + /* Read in the first two rows. */ + pnm_readpamrow(inpamP, irow); + pnm_scaletuplerow(inpamP, row0, irow, outpamP->maxval); + pnm_readpamrow(inpamP, irow); + pnm_scaletuplerow(inpamP, row1, irow, outpamP->maxval); + + pm_message("row1[0][0]=%lu", row1[0][0]); + + for (row = 1; row < inpamP->height - 1; ++row) { + /* Read in the next row and write out the current row. */ + + pnm_readpamrow(inpamP, irow); + pnm_scaletuplerow(inpamP, row2, irow, outpamP->maxval); + + computeOneRow(inpamP, outpamP, row0, row1, row2, orow); + + pnm_writepamrow(outpamP, orow); + + rotateRows(&row0, &row1, &row2); + } + pnm_freepamrow(orow); + pnm_freepamrow(row2); + pnm_freepamrow(row1); + pnm_freepamrow(row0); +} + + + +int +main(int argc, char *argv[]) { + FILE *ifP; + struct pam inpam, outpam; + + pnm_init( &argc, argv ); + + if (argc-1 == 1) + ifP = pm_openr(argv[1]); + else if (argc-1 == 0) + ifP = stdin; + else + pm_error("Too many arguments. Program takes at most 1 argument: " + "input file name"); + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + if (inpam.width < 3) + pm_error("Image is %u columns wide. It must be at least 3.", + inpam.width); + if (inpam.height < 3) + pm_error("Image is %u rows high. It must be at least 3.", + inpam.height); + + outpam = inpam; + outpam.file = stdout; + if (PAM_FORMAT_TYPE(inpam.format) == PBM_TYPE) { + outpam.format = PGM_FORMAT; + outpam.maxval = 255; + } + + pnm_writepaminit(&outpam); + + /* First row is black: */ + writeBlackRow(&outpam ); + + writeMiddleRows(&inpam, &outpam); + + pm_close(ifP); + + /* Last row is black: */ + writeBlackRow(&outpam); + + pm_close(stdout); + + return 0; +} diff --git a/editor/pamenlarge.c b/editor/pamenlarge.c new file mode 100644 index 00000000..15b91b4f --- /dev/null +++ b/editor/pamenlarge.c @@ -0,0 +1,117 @@ +/*============================================================================= + pamenlarge +=============================================================================== + By Bryan Henderson 2004.09.26. Contributed to the public domain by its + author. +=============================================================================*/ + +#include "pam.h" +#include "mallocvar.h" + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *inputFilespec; + unsigned int scaleFactor; +}; + + + +static void +parseCommandLine(int argc, 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. +-----------------------------------------------------------------------------*/ + if (argc-1 < 1) + pm_error("You must specify at least one argument: The scale factor"); + else { + cmdlineP->scaleFactor = atoi(argv[1]); + + if (cmdlineP->scaleFactor < 1) + pm_error("Scale factor must be an integer at least 1. " + "You specified '%s'", argv[1]); + + if (argc-1 >= 2) + cmdlineP->inputFilespec = argv[2]; + else + cmdlineP->inputFilespec = "-"; + } +} + + + +static void +makeOutputRowMap(tuple ** const outTupleRowP, + struct pam * const outpamP, + struct pam * const inpamP, + tuple * const inTuplerow) { +/*---------------------------------------------------------------------------- + Create a tuple *outTupleRowP which is actually a row of pointers into + inTupleRow[], so as to map input pixels to output pixels by stretching. +-----------------------------------------------------------------------------*/ + tuple * newtuplerow; + int col; + + MALLOCARRAY_NOFAIL(newtuplerow, outpamP->width); + + for (col = 0 ; col < inpamP->width; ++col) { + unsigned int const scaleFactor = outpamP->width / inpamP->width; + unsigned int subcol; + + for (subcol = 0; subcol < scaleFactor; ++subcol) + newtuplerow[col * scaleFactor + subcol] = inTuplerow[col]; + } + *outTupleRowP = newtuplerow; +} + + + +int +main(int argc, + char * argv[]) { + + struct cmdlineInfo cmdline; + FILE * ifP; + struct pam inpam; + struct pam outpam; + tuple * tuplerow; + tuple * newtuplerow; + int row; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilespec); + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + outpam = inpam; + outpam.file = stdout; + outpam.width = inpam.width * cmdline.scaleFactor; + outpam.height = inpam.height * cmdline.scaleFactor; + + pnm_writepaminit(&outpam); + + tuplerow = pnm_allocpamrow(&inpam); + + makeOutputRowMap(&newtuplerow, &outpam, &inpam, tuplerow); + + for (row = 0; row < inpam.height; ++row) { + pnm_readpamrow(&inpam, tuplerow); + pnm_writepamrowmult(&outpam, newtuplerow, cmdline.scaleFactor); + } + + free(newtuplerow); + + pnm_freepamrow(tuplerow); + + pm_close(ifP); + pm_close(stdout); + + return 0; +} + diff --git a/editor/pamenlarge.test b/editor/pamenlarge.test new file mode 100644 index 00000000..a2221d4d --- /dev/null +++ b/editor/pamenlarge.test @@ -0,0 +1,8 @@ +echo Test 1. Should print 3424505894 913236 +./pamenlarge 3 ../testimg.ppm | cksum +echo Test 2. Should print 2940246561 304422 +ppmtopgm ../testimg.ppm | ./pamenlarge 3 | cksum +echo Test 3. Should print 3342398172 297 +./pamenlarge 3 ../testgrid.pbm | cksum +echo Test 4. Should print 237488670 3133413 +./pamenlarge 3 -plain ../testimg.ppm | cksum diff --git a/editor/pamflip.c b/editor/pamflip.c new file mode 100644 index 00000000..59b60b56 --- /dev/null +++ b/editor/pamflip.c @@ -0,0 +1,910 @@ +/* pamflip.c - perform one or more flip operations on a Netpbm image +** +** Copyright (C) 1989 by Jef Poskanzer. +** +** 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. +*/ + +/* + transformGen() is the general transformation function. + + The following are enhancements for specific cases: + + transformRowByRowPbm() + transformRowsBottomTopPbm() + transformRowByRowNonPbm() + transformRowsBottomTopNonPbm() + transformPbm() + + Although we use transformGen() only when none of the enhancement + functions apply, it is capable of handling all cases. (Only that it + is slow, and uses more memory.) In the same manner, transformPbm() is + capable of handling all pbm transformations and transformRowByRowNonPbm() + transformRowsBottomTomNonPbm() are capable of handling pbm. + + + There is some fancy virtual memory management in transformGen() to avoid + page thrashing when you flip a large image in a columns-for-rows + way (e.g. -transpose). + + The page thrashing we're trying to avoid could happen because the + output of the transformation is stored in an array of tuples in + virtual memory. A tuple array is stored in column-first order, + meaning that all the columns of particular row are contiguous, the + next row is next to that, etc. If you fill up that array by + filling in Column 0 sequentially in every row from top to bottom, + you will touch a lot of different virtual memory pages, and every + one has to be paged in as you touch it. + + If the number of virtual memory pages you touch exceeds the amount + of real memory the process can get, then by the time you hit the bottom + of the tuple array, the pages that hold the top are already paged out. + So if you go back and do Column 1 from top to bottom, you will again + touch lots of pages and have to page in every one of them. Do this + for 100 columns, and you might page in every page in the array 100 times + each, putting a few bytes in the page each time. + + That is very expensive. Instead, you'd like to keep the same pages in + real memory as long as possible and fill them up as much as you can + before paging them out and working on a new set of pages. You can do + that by doing Column 0 from top to say Row 10, then Column 1 from top + to Row 10, etc. all the way across the image. Assuming 10 rows fits + in real memory, you will keep the virtual memory for the first 10 rows + of the tuple array in real memory until you've filled them in completely. + Now you go back and do Column 0 from Row 11 to Row 20, then Column 1 + from Row 11 to Row 20, and so on. + + So why are we even trying to fill in column by column instead of just + filling in row by row? Because we're reading an input image row by row + and transforming it in such a way that a row of the input becomes + a column of the output. In order to fill in a whole row of the output, + we'd have to read a whole column of the input, and then we have the same + page thrashing problem in the input. + + So the optimal procedure is to do N output rows in each pass, where + N is the largest number of rows we can fit in real memory. In each + pass, we read certain columns of every row of the input and output + every column of certain rows of the output. The output area for + the rows in the pass gets paged in once during the pass and then + never again. Note that some pages of every row of the input get + paged in once in each pass too. As each input page is referenced + only in one burst, input pages do not compete with output pages for + real memory -- the working set is the output pages, which get referenced + cyclically. + + This all worked when we used the pnm xel format, but now that we + use the pam tuple format, there's an extra memory reference that + could be causing trouble. Because tuples have varying depth, a pam + row is an array of pointers to the tuples. To access a tuple, we + access the tuple pointer, then the tuple. We could probably do better, + because the samples are normally in the memory immediately following + the tuple pointers, so we could compute where a tuple's samples live + without actually loading the tuple address from memory. I.e. the + address of the tuple for Column 5 of Row 9 of a 3-deep 100-wide + image is (void*)tuples[9] + 100 * sizeof(tuple*) + 5*(3*sizeof(sample)). +*/ + +#define _BSD_SOURCE 1 /* Make sure strdup() is in string.h */ +#define _XOPEN_SOURCE 500 /* Make sure strdup() is in string.h */ + +#include <limits.h> +#include <string.h> + +#include "pam.h" +#include "shhopt.h" +#include "mallocvar.h" +#include "nstring.h" +#include "bitreverse.h" + +enum xformType {LEFTRIGHT, TOPBOTTOM, TRANSPOSE}; + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *inputFilespec; /* Filespec of input file */ + unsigned int xformCount; + /* Number of transforms in the 'xformType' array */ + enum xformType xformList[10]; + /* Array of transforms to be applied, in order */ + unsigned int availableMemory; + unsigned int pageSize; + unsigned int verbose; +}; + + + +static void +parseXformOpt(const char * const xformOpt, + unsigned int * const xformCountP, + enum xformType * const xformList) { +/*---------------------------------------------------------------------------- + Translate the -xform option string into an array of transform types. + + Return the array as xformList[], which is preallocated for at least + 10 elements. +-----------------------------------------------------------------------------*/ + unsigned int xformCount; + char * xformOptWork; + char * cursor; + bool eol; + + xformOptWork = strdup(xformOpt); + cursor = &xformOptWork[0]; + + eol = FALSE; /* initial value */ + xformCount = 0; /* initial value */ + while (!eol && xformCount < 10) { + const char * token; + token = strsepN(&cursor, ","); + if (token) { + if (streq(token, "leftright")) + xformList[xformCount++] = LEFTRIGHT; + else if (streq(token, "topbottom")) + xformList[xformCount++] = TOPBOTTOM; + else if (streq(token, "transpose")) + xformList[xformCount++] = TRANSPOSE; + else if (streq(token, "")) + { /* ignore it */} + else + pm_error("Invalid transform type in -xform option: '%s'", + token ); + } else + eol = TRUE; + } + free(xformOptWork); + + *xformCountP = xformCount; +} + + + +static void +parseCommandLine(int argc, 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 = malloc(100*sizeof(optEntry)); + /* Instructions to OptParseOptions3 on how to parse our options. + */ + optStruct3 opt; + + unsigned int option_def_index; + + unsigned int lr, tb, xy, r90, r270, r180, null; + unsigned int memsizeSpec, pagesizeSpec, xformSpec; + unsigned int memsizeOpt; + const char *xformOpt; + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENT3(0, "lr", OPT_FLAG, NULL, &lr, 0); + OPTENT3(0, "leftright", OPT_FLAG, NULL, &lr, 0); + OPTENT3(0, "tb", OPT_FLAG, NULL, &tb, 0); + OPTENT3(0, "topbottom", OPT_FLAG, NULL, &tb, 0); + OPTENT3(0, "xy", OPT_FLAG, NULL, &xy, 0); + OPTENT3(0, "transpose", OPT_FLAG, NULL, &xy, 0); + OPTENT3(0, "r90", OPT_FLAG, NULL, &r90, 0); + OPTENT3(0, "rotate90", OPT_FLAG, NULL, &r90, 0); + OPTENT3(0, "ccw", OPT_FLAG, NULL, &r90, 0); + OPTENT3(0, "r180", OPT_FLAG, NULL, &r180, 0); + OPTENT3(0, "rotate180", OPT_FLAG, NULL, &r180, 0); + OPTENT3(0, "r270", OPT_FLAG, NULL, &r270, 0); + OPTENT3(0, "rotate270", OPT_FLAG, NULL, &r270, 0); + OPTENT3(0, "cw", OPT_FLAG, NULL, &r270, 0); + OPTENT3(0, "null", OPT_FLAG, NULL, &null, 0); + OPTENT3(0, "verbose", OPT_FLAG, NULL, &cmdlineP->verbose, 0); + OPTENT3(0, "memsize", OPT_UINT, &memsizeOpt, + &memsizeSpec, 0); + OPTENT3(0, "pagesize", OPT_UINT, &cmdlineP->pageSize, + &pagesizeSpec, 0); + OPTENT3(0, "xform", OPT_STRING, &xformOpt, + &xformSpec, 0); + + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ + opt.allowNegNum = FALSE; /* We don't parms that are negative numbers */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (lr + tb + xy + r90 + r180 + r270 + null > 1) + pm_error("You may specify only one type of flip."); + if (lr + tb + xy + r90 + r180 + r270 + null == 1) { + if (lr) { + cmdlineP->xformCount = 1; + cmdlineP->xformList[0] = LEFTRIGHT; + } else if (tb) { + cmdlineP->xformCount = 1; + cmdlineP->xformList[0] = TOPBOTTOM; + } else if (xy) { + cmdlineP->xformCount = 1; + cmdlineP->xformList[0] = TRANSPOSE; + } else if (r90) { + cmdlineP->xformCount = 2; + cmdlineP->xformList[0] = TRANSPOSE; + cmdlineP->xformList[1] = TOPBOTTOM; + } else if (r180) { + cmdlineP->xformCount = 2; + cmdlineP->xformList[0] = LEFTRIGHT; + cmdlineP->xformList[1] = TOPBOTTOM; + } else if (r270) { + cmdlineP->xformCount = 2; + cmdlineP->xformList[0] = TRANSPOSE; + cmdlineP->xformList[1] = LEFTRIGHT; + } else if (null) { + cmdlineP->xformCount = 0; + } + } else if (xformSpec) + parseXformOpt(xformOpt, &cmdlineP->xformCount, cmdlineP->xformList); + else + pm_error("You must specify an option such as -topbottom to indicate " + "what kind of flip you want."); + + if (memsizeSpec) { + if (memsizeOpt > UINT_MAX / 1024 / 1024) + pm_error("-memsize value too large: %u MiB. Maximum this program " + "can handle is %u MiB", + memsizeOpt, UINT_MAX / 1024 / 1024); + cmdlineP->availableMemory = memsizeOpt * 1024 *1024; + } else + cmdlineP->availableMemory = UINT_MAX; + + if (!pagesizeSpec) + cmdlineP->pageSize = 4*1024; + + if (argc-1 == 0) + cmdlineP->inputFilespec = "-"; + else if (argc-1 != 1) + pm_error("Program takes zero or one argument (filename). You " + "specified %d", argc-1); + else + cmdlineP->inputFilespec = argv[1]; +} + + + +struct xformMatrix { + int a; + int b; + int c; + int d; + int e; + int f; +}; + + + +static void +leftright(struct xformMatrix * const xformP) { + xformP->a = - xformP->a; + xformP->c = - xformP->c; + xformP->e = - xformP->e + 1; +} + + + +static void +topbottom(struct xformMatrix * const xformP) { + xformP->b = - xformP->b; + xformP->d = - xformP->d; + xformP->f = - xformP->f + 1; +} + + + +static void +swap(int * const xP, int * const yP) { + + int const t = *xP; + + *xP = *yP; + *yP = t; +} + + + +static void +transpose(struct xformMatrix * const xformP) { + swap(&xformP->a, &xformP->b); + swap(&xformP->c, &xformP->d); + swap(&xformP->e, &xformP->f); +} + + + +static void +computeXformMatrix(struct xformMatrix * const xformP, + unsigned int const xformCount, + enum xformType xformType[]) { + + struct xformMatrix const nullTransform = {1, 0, 0, 1, 0, 0}; + + unsigned int i; + + *xformP = nullTransform; /* initial value */ + + for (i = 0; i < xformCount; ++i) { + switch (xformType[i]) { + case LEFTRIGHT: + leftright(xformP); + break; + case TOPBOTTOM: + topbottom(xformP); + break; + case TRANSPOSE: + transpose(xformP); + break; + } + } +} + + + +static void +bitOrderReverse(unsigned char * const bitrow, + unsigned int const cols) { +/*---------------------------------------------------------------------------- + Reverse the bits in a packed pbm row (1 bit per pixel). I.e. the leftmost + bit becomes the rightmost, etc. +-----------------------------------------------------------------------------*/ + unsigned int const lastfullByteIdx = cols/8 - 1; + + if (cols == 0 || bitrow == NULL ) + pm_error("Invalid arguments passed to bitOrderReverse"); + + if (cols <= 8) + bitrow[0] = bitreverse[bitrow[0]] << (8-cols); + else if (cols % 8 == 0) { + unsigned int i, j; + for (i = 0, j = lastfullByteIdx; i <= j; ++i, --j) { + unsigned char const t = bitreverse[bitrow[j]]; + bitrow[j] = bitreverse[bitrow[i]]; + bitrow[i] = t; + } + } else { + unsigned int const m = cols % 8; + + unsigned int i, j; + /* Cursors into bitrow[]. i moves from left to center; + j moves from right to center as bits of bitrow[] are exchanged. + */ + unsigned char th, tl; /* 16 bit temp ( th << 8 | tl ) */ + tl = 0; + for (i = 0, j = lastfullByteIdx+1; i <= lastfullByteIdx/2; ++i, --j) { + th = bitreverse[bitrow[i]]; + bitrow[i] = + bitreverse[0xff & ((bitrow[j-1] << 8 | bitrow[j]) >> (8-m))]; + bitrow[j] = 0xff & ((th << 8 | tl) >> m); + tl = th; + } + if (i == j) + /* bitrow[] has an odd number of bytes (an even number of + full bytes; lastfullByteIdx is odd), so we did all but + the center byte above. We do the center byte now. + */ + bitrow[j] = 0xff & ((bitreverse[bitrow[i]] << 8 | tl) >> m); + } +} + + + +static void +transformRowByRowPbm(struct pam * const inpamP, + struct pam * const outpamP, + bool const reverse) { +/*---------------------------------------------------------------------------- + Transform a PBM image either by flipping it left for right, or just leaving + it alone, as indicated by 'reverse'. + + Process the image one row at a time and use fast packed PBM bit + reverse algorithm (where required). +-----------------------------------------------------------------------------*/ + unsigned char * bitrow; + unsigned int row; + + bitrow = pbm_allocrow_packed(outpamP->width); + + pbm_writepbminit(outpamP->file, outpamP->width, outpamP->height, 0); + + for (row = 0; row < inpamP->height; ++row) { + pbm_readpbmrow_packed(inpamP->file, bitrow, inpamP->width, + inpamP->format); + + if (reverse) + bitOrderReverse(bitrow, inpamP->width); + + pbm_writepbmrow_packed(outpamP->file, bitrow, outpamP->width, 0); + } + pbm_freerow_packed(bitrow); +} + + + +static void +transformRowByRowNonPbm(struct pam * const inpamP, + struct pam * const outpamP, + bool const reverse) { +/*---------------------------------------------------------------------------- + Flip an image left for right or leave it alone. + + Process one row at a time. + + This works on any image, but is slower and uses more memory than the + PBM-only transformRowByRowPbm(). +-----------------------------------------------------------------------------*/ + tuple * tuplerow; + tuple * newtuplerow; + /* This is not a full tuple row. It is either an array of pointers + to the tuples in 'tuplerow' (in reverse order) or just 'tuplerow' + itself. + */ + tuple * scratchTuplerow; + + unsigned int row; + + tuplerow = pnm_allocpamrow(inpamP); + + if (reverse) { + /* Set up newtuplerow[] to point to the tuples of tuplerow[] in + reverse order. + */ + unsigned int col; + + MALLOCARRAY_NOFAIL(scratchTuplerow, inpamP->width); + + for (col = 0; col < inpamP->width; ++col) + scratchTuplerow[col] = tuplerow[inpamP->width - col - 1]; + newtuplerow = scratchTuplerow; + } else { + scratchTuplerow = NULL; + newtuplerow = tuplerow; + } + pnm_writepaminit(outpamP); + + for (row = 0; row < inpamP->height ; ++row) { + pnm_readpamrow(inpamP, tuplerow); + pnm_writepamrow(outpamP, newtuplerow); + } + + if (scratchTuplerow) + free(scratchTuplerow); + pnm_freepamrow(tuplerow); +} + + + +static void +transformRowByRow(struct pam * const inpamP, + struct pam * const outpamP, + bool const reverse, + bool const verbose) { + + if (verbose) + pm_message("Transforming row by row, top to bottom"); + + switch (PNM_FORMAT_TYPE(inpamP->format)) { + case PBM_TYPE: + transformRowByRowPbm(inpamP, outpamP, reverse); + break; + default: + transformRowByRowNonPbm(inpamP, outpamP, reverse); + break; + } +} + + + +static void +transformRowsBottomTopPbm(struct pam * const inpamP, + struct pam * const outpamP, + bool const reverse) { +/*---------------------------------------------------------------------------- + Flip a PBM image top for bottom. Iff 'reverse', also flip it left for right. + + Read complete image into memory in packed PBM format; Use fast + packed PBM bit reverse algorithm (where required). +-----------------------------------------------------------------------------*/ + unsigned int const rows=inpamP->height; + + unsigned char ** bitrow; + int row; + + bitrow = pbm_allocarray_packed(outpamP->width, outpamP->height); + + for (row = 0; row < rows; ++row) + pbm_readpbmrow_packed(inpamP->file, bitrow[row], + inpamP->width, inpamP->format); + + pbm_writepbminit(outpamP->file, outpamP->width, outpamP->height, 0); + + for (row = 0; row < rows; ++row) { + if (reverse) + bitOrderReverse(bitrow[rows-row-1], inpamP->width); + + pbm_writepbmrow_packed(outpamP->file, bitrow[rows - row - 1], + outpamP->width, 0); + } + pbm_freearray_packed(bitrow, outpamP->height); +} + + + +static void +transformRowsBottomTopNonPbm(struct pam * const inpamP, + struct pam * const outpamP, + bool const reverse) { +/*---------------------------------------------------------------------------- + Read complete image into memory as a tuple array. + + This can do any transformation except a column-for-row transformation, + on any type of image, but is slower and uses more memory than the + PBM-only transformRowsBottomTopPbm(). +-----------------------------------------------------------------------------*/ + tuple** tuplerows; + tuple * scratchTuplerow; + /* This is not a full tuple row -- just an array of pointers to + the tuples in 'tuplerows'. + */ + unsigned int row; + + if (reverse) + MALLOCARRAY_NOFAIL(scratchTuplerow, inpamP->width); + else + scratchTuplerow = NULL; + + tuplerows = pnm_allocpamarray(outpamP); + + for (row = 0; row < inpamP->height ; ++row) + pnm_readpamrow(inpamP, tuplerows[row]); + + pnm_writepaminit(outpamP); + + for (row = 0; row < inpamP->height ; ++row) { + tuple * newtuplerow; + tuple * const tuplerow = tuplerows[inpamP->height - row - 1]; + if (reverse) { + unsigned int col; + newtuplerow = scratchTuplerow; + for (col = 0; col < inpamP->width; ++col) + newtuplerow[col] = tuplerow[inpamP->width - col - 1]; + } else + newtuplerow = tuplerow; + pnm_writepamrow(outpamP, newtuplerow); + } + + if (scratchTuplerow) + free(scratchTuplerow); + + pnm_freepamarray(tuplerows, outpamP); +} + + + +static void +transformRowsBottomTop(struct pam * const inpamP, + struct pam * const outpamP, + bool const reverse, + bool const verbose) { + + if (PNM_FORMAT_TYPE(inpamP->format) == PBM_TYPE) { + if (verbose) + pm_message("Transforming PBM row by row, bottom to top"); + transformRowsBottomTopPbm(inpamP, outpamP, reverse); + } else { + if (verbose) + pm_message("Transforming non-PBM row by row, bottom to top"); + transformRowsBottomTopNonPbm(inpamP, outpamP, reverse); + } +} + + + +static void __inline__ +transformPoint(int const col, + int const newcols, + int const row, + int const newrows, + struct xformMatrix const xform, + int * const newcolP, + int * const newrowP ) { +/*---------------------------------------------------------------------------- + Compute the location in the output of a pixel that is at row 'row', + column 'col' in the input. Assume the output image is 'newcols' by + 'newrows' and the transformation is as described by 'xform'. + + Return the output image location of the pixel as *newcolP and *newrowP. +-----------------------------------------------------------------------------*/ + /* The transformation is: + + [ a b 0 ] + [ x y 1 ] [ c d 0 ] = [ x2 y2 1 ] + [ e f 1 ] + */ + *newcolP = xform.a * col + xform.c * row + xform.e * (newcols - 1); + *newrowP = xform.b * col + xform.d * row + xform.f * (newrows - 1); +} + + + +static void +transformPbm(struct pam * const inpamP, + struct pam * const outpamP, + struct xformMatrix const xform) { +/*---------------------------------------------------------------------------- + This is the same as transformGen, except that it uses less + memory, since the PBM buffer format uses one bit per pixel instead + of twelve bytes + pointer space + + This can do any PBM transformation, but is slower and uses more + memory than the more restricted transformRowByRowPbm() and + transformRowsBottomTopPbm(). +-----------------------------------------------------------------------------*/ + bit* bitrow; + bit** newbits; + int row; + + bitrow = pbm_allocrow(inpamP->width); + newbits = pbm_allocarray(pbm_packed_bytes(outpamP->width), + outpamP->height); + + /* Initialize entire array to zeroes. One bits will be or'ed in later */ + for (row = 0; row < outpamP->height; ++row) { + int col; + for (col = 0; col < pbm_packed_bytes(outpamP->width); ++col) + newbits[row][col] = 0; + } + + for (row = 0; row < inpamP->height; ++row) { + int col; + pbm_readpbmrow(inpamP->file, bitrow, inpamP->width, inpamP->format); + for (col = 0; col < inpamP->width; ++col) { + int newcol, newrow; + transformPoint(col, outpamP->width, row, outpamP->height, xform, + &newcol, &newrow); + newbits[newrow][newcol/8] |= bitrow[col] << (7 - newcol % 8); + /* Use of "|=" patterned after pbm_readpbmrow_packed. */ + } + } + + pbm_writepbminit(outpamP->file, outpamP->width, outpamP->height, 0); + for (row = 0; row < outpamP->height; ++row) + pbm_writepbmrow_packed(outpamP->file, newbits[row], outpamP->width, + 0); + + pbm_freearray(newbits, outpamP->height); + pbm_freerow(bitrow); +} + + + +static unsigned int +optimalSegmentSize(struct xformMatrix const xform, + struct pam * const pamP, + unsigned int const availableMemory, + unsigned int const pageSize) { +/*---------------------------------------------------------------------------- + Compute the maximum number of columns that can be transformed, one row + at a time, without causing page thrashing. + + See comments at the top of this file for an explanation of the kind + of page thrashing using segments avoids. + + 'availableMemory' is the amount of real memory in bytes that this + process should expect to be able to use. + + 'pageSize' is the size of a page in bytes. A page means the unit that + is paged in or out. + + 'pamP' describes the storage required to represent a row of the + output array. +-----------------------------------------------------------------------------*/ + unsigned int segmentSize; + + if (xform.b == 0) + segmentSize = pamP->width; + else { + unsigned int const otherNeeds = 200*1024; + /* A wild guess at how much real memory is needed by the program + for purposes other than the output tuple array. + */ + if (otherNeeds > availableMemory) + segmentSize = pamP->width; /* Can't prevent thrashing */ + else { + unsigned int const availablePages = + (availableMemory - otherNeeds) / pageSize; + if (availablePages <= 1) + segmentSize = pamP->width; /* Can't prevent thrashing */ + else { + unsigned int const bytesPerRow = + pamP->width * pamP->depth * pamP->bytes_per_sample; + unsigned int rowsPerPage = + MAX(1, (pageSize + (pageSize/2)) / bytesPerRow); + /* This is how many consecutive rows we can touch + on average while staying within the same page. + */ + segmentSize = availablePages * rowsPerPage; + } + } + } + return segmentSize; +} + + + +static void +transformNonPbm(struct pam * const inpamP, + struct pam * const outpamP, + struct xformMatrix const xform, + unsigned int const segmentSize, + bool const verbose) { +/*---------------------------------------------------------------------------- + Do the transform using "pam" library functions, as opposed to "pbm" + ones. + + Assume input file is positioned to the raster (just after the + header). + + 'segmentSize' is the number of columns we are to process in each + pass. We do each segment going from left to right. For each + segment, we do each row, going from top to bottom. For each row of + the segment, we do each column, going from left to right. (The + reason Caller wants it done by segments is to improve virtual memory + reference locality. See comments at the top of this file). + + if 'segmentSize' is less than the whole image, ifP must be a seekable + file. + + This can do any transformation, but is slower and uses more memory + than the PBM-only transformPbm(). +-----------------------------------------------------------------------------*/ + pm_filepos imagepos; + /* The input file position of the raster. But defined only if + segment size is less than whole image. + */ + tuple* tuplerow; + tuple** newtuples; + unsigned int startCol; + + tuplerow = pnm_allocpamrow(inpamP); + newtuples = pnm_allocpamarray(outpamP); + + if (segmentSize < inpamP->width) + pm_tell2(inpamP->file, &imagepos, sizeof(imagepos)); + + for (startCol = 0; startCol < inpamP->width; startCol += segmentSize) { + /* Do one set of columns which is small enough not to cause + page thrashing. + */ + unsigned int const endCol = MIN(inpamP->width, startCol + segmentSize); + unsigned int row; + + if (verbose) + pm_message("Transforming Columns %u up to %u", + startCol, endCol); + + if (startCol > 0) + /* Go back to read from Row 0 again */ + pm_seek2(inpamP->file, &imagepos, sizeof(imagepos)); + + for (row = 0; row < inpamP->height; ++row) { + unsigned int col; + pnm_readpamrow(inpamP, tuplerow); + + for (col = startCol; col < endCol; ++col) { + int newcol, newrow; + transformPoint(col, outpamP->width, row, outpamP->height, + xform, + &newcol, &newrow); + pnm_assigntuple(inpamP, newtuples[newrow][newcol], + tuplerow[col]); + } + } + } + + pnm_writepam(outpamP, newtuples); + + pnm_freepamarray(newtuples, outpamP); + pnm_freepamrow(tuplerow); +} + + + +static void +transformGen(struct pam * const inpamP, + struct pam * const outpamP, + struct xformMatrix const xform, + unsigned int const availableMemory, + unsigned int const pageSize, + bool const verbose) { +/*---------------------------------------------------------------------------- + Produce the transformed output on Standard Output. + + Assume input file is positioned to the raster (just after the + header). + + This can transform any image in any way, but is slower and uses more + memory than the more restricted transformRowByRow() and + transformRowsBottomTop(). +-----------------------------------------------------------------------------*/ + unsigned int const segmentSize = + optimalSegmentSize(xform, outpamP, availableMemory, pageSize); + + switch (PNM_FORMAT_TYPE(inpamP->format)) { + case PBM_TYPE: + transformPbm(inpamP, outpamP, xform); + break; + default: + if (segmentSize < outpamP->width) { + if (verbose && xform.b !=0) + pm_message("Transforming %u columns of %u total at a time", + segmentSize, outpamP->width); + else + pm_message("Transforming entire image at once"); + } + transformNonPbm(inpamP, outpamP, xform, segmentSize, verbose); + break; + } +} + + + +int +main(int argc, char * argv[]) { + struct cmdlineInfo cmdline; + struct pam inpam; + struct pam outpam; + FILE* ifP; + struct xformMatrix xform; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + if (cmdline.availableMemory < UINT_MAX) + ifP = pm_openr_seekable(cmdline.inputFilespec); + else + ifP = pm_openr(cmdline.inputFilespec); + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + computeXformMatrix(&xform, cmdline.xformCount, cmdline.xformList); + + outpam = inpam; /* initial value */ + + outpam.file = stdout; + outpam.width = abs(xform.a) * inpam.width + abs(xform.c) * inpam.height; + outpam.height = abs(xform.b) * inpam.width + abs(xform.d) * inpam.height; + + if (xform.b == 0 && xform.d == 1 && xform.f == 0) + /* In this case Row N of the output is based only on Row N of + the input, so we can transform row by row and avoid + in-memory buffering altogether. + */ + transformRowByRow(&inpam, &outpam, xform.a == -1, cmdline.verbose); + else if (xform.b == 0 && xform.c == 0) + /* In this case, Row N of the output is based only on Row ~N of the + input. We need all the rows in memory, but have to pass + through them only twice, so there is no page thrashing concern. + */ + transformRowsBottomTop(&inpam, &outpam, xform.a == -1, + cmdline.verbose); + else + /* This is a colum-for-row type of transformation, which requires + complex traversal of an in-memory image. + */ + transformGen(&inpam, &outpam, xform, + cmdline.availableMemory, cmdline.pageSize, + cmdline.verbose); + + pm_close(inpam.file); + pm_close(outpam.file); + + return 0; +} diff --git a/editor/pamflip.test b/editor/pamflip.test new file mode 100644 index 00000000..96e889ea --- /dev/null +++ b/editor/pamflip.test @@ -0,0 +1,12 @@ +echo Test 1. Should print 2116496681 101484 +./pamflip -lr ../testimg.ppm | cksum +echo Test 2. Should print 217037000 101484 +./pamflip -cw ../testimg.ppm | cksum +echo Test 3. Should print 2052917888 101484 +./pamflip -tb ../testimg.ppm | cksum +echo Test 4. Should print 3375384165 41 +./pamflip -lr ../testgrid.pbm | cksum +echo Test 5. Should print 604323149 41 +./pamflip -tb ../testgrid.pbm | cksum +echo Test 6. Should print 490797850 37 +./pamflip -cw ../testgrid.pbm | cksum diff --git a/editor/pamfunc.c b/editor/pamfunc.c new file mode 100644 index 00000000..dbb1ca70 --- /dev/null +++ b/editor/pamfunc.c @@ -0,0 +1,221 @@ +/****************************************************************************** + pamfunc +******************************************************************************* + Apply one of various functions to each sample in a PAM image + + By Bryan Henderson, San Jose CA 2002.06.16. + + Contributed to the public domain + + ENHANCEMENT IDEAS: + + 1) speed up by doing integer arithmetic instead of floating point for + multiply/divide where possible. Especially when multiplying by an + integer. + + 2) For multiply/divide, give option of simply changing the maxval and + leaving the raster alone. + +******************************************************************************/ + +#include "pam.h" +#include "shhopt.h" + +enum function {FN_MULTIPLY, FN_DIVIDE, FN_ADD, FN_SUBTRACT, FN_MIN, FN_MAX}; + +/* Note that when the user specifies a minimum, that means he's requesting + a "max" function. +*/ + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *inputFilespec; /* Filespec of input file */ + enum function function; + union { + float multiplier; + float divisor; + int adder; + int subtractor; + unsigned int max; + unsigned int min; + } u; + unsigned int verbose; +}; + + +static void +parseCommandLine(int argc, 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 = malloc(100*sizeof(optEntry)); + /* Instructions to OptParseOptions2 on how to parse our options. + */ + optStruct3 opt; + + unsigned int option_def_index; + + unsigned int multiplierSpec, divisorSpec, adderSpec, subtractorSpec; + unsigned int maxSpec, minSpec; + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENT3(0, "multiplier", OPT_FLOAT, &cmdlineP->u.multiplier, + &multiplierSpec, 0); + OPTENT3(0, "divisor", OPT_FLOAT, &cmdlineP->u.divisor, + &divisorSpec, 0); + OPTENT3(0, "adder", OPT_INT, &cmdlineP->u.adder, + &adderSpec, 0); + OPTENT3(0, "subtractor", OPT_INT, &cmdlineP->u.subtractor, + &subtractorSpec, 0); + OPTENT3(0, "min", OPT_UINT, &cmdlineP->u.min, + &minSpec, 0); + OPTENT3(0, "max", OPT_UINT, &cmdlineP->u.max, + &maxSpec, 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 */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (multiplierSpec + divisorSpec + adderSpec + subtractorSpec + + minSpec + maxSpec > 1) + pm_error("You may specify at most one of -multiplier, -divisor," + "-adder, -subtractor, -min, and -max"); + + if (multiplierSpec) { + cmdlineP->function = FN_MULTIPLY; + if (cmdlineP->u.multiplier < 0) + pm_error("Multiplier must be nonnegative. You specified %f", + cmdlineP->u.multiplier); + } else if (divisorSpec) { + cmdlineP->function = FN_DIVIDE; + if (cmdlineP->u.divisor < 0) + pm_error("Divisor must be nonnegative. You specified %f", + cmdlineP->u.divisor); + } else if (adderSpec) { + cmdlineP->function = FN_ADD; + } else if (subtractorSpec) { + cmdlineP->function = FN_SUBTRACT; + } else if (minSpec) { + cmdlineP->function = FN_MAX; + } else if (maxSpec) { + cmdlineP->function = FN_MIN; + } else + pm_error("You must specify one of -multiplier, -divisor, " + "-adder, -subtractor, -min, or -max"); + + if (argc-1 > 1) + pm_error("Too many arguments (%d). File spec is the only argument.", + argc-1); + + if (argc-1 < 1) + cmdlineP->inputFilespec = "-"; + else + cmdlineP->inputFilespec = argv[1]; + +} + + + +static void +applyFunction(struct cmdlineInfo const cmdline, + struct pam const inpam, + struct pam const outpam, + tuple * const inputRow, + tuple * const outputRow) { + + float const oneOverDivisor = 1/cmdline.u.divisor; + /* In my experiments, the compiler couldn't figure out that + 1/cmdline.u.divisor is a constant and instead recomputed it + for each and every pixel. division is slower than + multiplication, so we want to multiply by + 1/cmdline.u.divisor instead of divide by cmdline.u.divisor, + so we compute that here. Note that if the function isn't + divide, both cmdline.u.divisor and oneOverDivisor are + meaningless. + */ + int col; + + for (col = 0; col < inpam.width; ++col) { + int plane; + for (plane = 0; plane < inpam.depth; ++plane) { + sample const inSample = inputRow[col][plane]; + sample outSample; /* Could be > maxval */ + + switch (cmdline.function) { + case FN_MULTIPLY: + outSample = ROUNDU(inSample * cmdline.u.multiplier); + break; + case FN_DIVIDE: + outSample = ROUNDU(inSample * oneOverDivisor); + break; + case FN_ADD: + outSample = MAX(0, (long)inSample + cmdline.u.adder); + break; + case FN_SUBTRACT: + outSample = MAX(0, (long)inSample - cmdline.u.subtractor); + break; + case FN_MAX: + outSample = MAX(inSample, cmdline.u.min); + break; + case FN_MIN: + outSample = MIN(inSample, cmdline.u.max); + break; + } + outputRow[col][plane] = MIN(outpam.maxval, outSample); + } + } +} + + + +int +main(int argc, char *argv[]) { + + FILE* ifP; + tuple* inputRow; /* Row from input image */ + tuple* outputRow; /* Row of output image */ + int row; + struct cmdlineInfo cmdline; + struct pam inpam; /* Input PAM image */ + struct pam outpam; /* Output PAM image */ + + pnm_init( &argc, argv ); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilespec); + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + inputRow = pnm_allocpamrow(&inpam); + + outpam = inpam; /* Initial value -- most fields should be same */ + outpam.file = stdout; + + pnm_writepaminit(&outpam); + + outputRow = pnm_allocpamrow(&outpam); + + for (row = 0; row < inpam.height; row++) { + pnm_readpamrow(&inpam, inputRow); + + applyFunction(cmdline, inpam, outpam, inputRow, outputRow); + + pnm_writepamrow(&outpam, outputRow); + } + pnm_freepamrow(outputRow); + pnm_freepamrow(inputRow); + pm_close(inpam.file); + pm_close(outpam.file); + + exit(0); +} + diff --git a/editor/pammasksharpen.c b/editor/pammasksharpen.c new file mode 100644 index 00000000..87b928be --- /dev/null +++ b/editor/pammasksharpen.c @@ -0,0 +1,192 @@ +#include "pam.h" +#include "shhopt.h" +#include "mallocvar.h" + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char * inputFilespec; + const char * maskFilespec; + unsigned int verbose; + float sharpness; + float threshold; +}; + + + +static void +parseCommandLine(int argc, 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 OptParseOptions2 on how to parse our options. + */ + optStruct3 opt; + + unsigned int option_def_index; + + unsigned int sharpSpec, thresholdSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENT3(0, "sharpness", OPT_FLOAT, &cmdlineP->sharpness, + &sharpSpec, 0); + OPTENT3(0, "threshold", OPT_FLOAT, &cmdlineP->threshold, + &thresholdSpec, 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 */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (sharpSpec) { + if (cmdlineP->sharpness < 0) + pm_error("-sharpness less than zero doesn't make sense. " + "You specified %f", cmdlineP->sharpness); + } else + cmdlineP->sharpness = 1.0; + + if (thresholdSpec) { + if (cmdlineP->threshold < 0) + pm_error("-threshold less than zero doesn't make sense. " + "You specified %f", cmdlineP->threshold); + if (cmdlineP->threshold > 1.0) + pm_error("-threshold greater than unity doesn't make sense. " + "You specified %f", cmdlineP->threshold); + + } else + cmdlineP->threshold = 0.0; + + if (argc-1 < 1) + pm_error("You must specify at least one argument: The name " + "of the mask image file"); + else { + cmdlineP->maskFilespec = argv[1]; + if (argc-1 < 2) + cmdlineP->inputFilespec = "-"; + else { + cmdlineP->inputFilespec = argv[2]; + + if (argc-1 > 2) + pm_error("There are at most two arguments: mask file name " + "and input file name. You specified %d", argc-1); + } + } +} + + + +static sample +sharpened(sample const inputSample, + sample const maskSample, + float const sharpness, + sample const threshold, + sample const maxval) { + + int const edgeness = inputSample - maskSample; + + sample retval; + + if (abs(edgeness) > threshold) { + float const rawResult = inputSample + edgeness * sharpness; + + retval = MIN(maxval, (unsigned)MAX(0, (int)(rawResult+0.5))); + } else + retval = inputSample; + + return retval; +} + + + +int +main(int argc, char *argv[]) { + + struct cmdlineInfo cmdline; + struct pam inpam; + struct pam maskpam; + struct pam outpam; + FILE * ifP; + FILE * maskfP; + tuple * inputTuplerow; + tuple * maskTuplerow; + tuple * outputTuplerow; + unsigned int row; + sample threshold; + /* Magnitude of difference between image and unsharp mask below + which they will be considered identical. + */ + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilespec); + maskfP = pm_openr(cmdline.maskFilespec); + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + pnm_readpaminit(maskfP, &maskpam, PAM_STRUCT_SIZE(tuple_type)); + + if (inpam.width != maskpam.width || + inpam.height != maskpam.height || + inpam.depth != maskpam.depth) + pm_error("The mask image must be the same dimensions as the " + "input image. The mask is %dx%dx%d, but the input is " + "%dx%dx%d.", + maskpam.width, maskpam.height, maskpam.depth, + inpam.width, inpam.height, inpam.depth); + if (inpam.maxval != maskpam.maxval) + pm_error("The mask image must have the same maxval as the " + "input image. The input image has maxval %u, " + "but the mask image has maxval %u", + (unsigned)inpam.maxval, (unsigned)maskpam.maxval); + + threshold = (float)cmdline.threshold / inpam.maxval; + + outpam = inpam; + outpam.file = stdout; + + inputTuplerow = pnm_allocpamrow(&inpam); + maskTuplerow = pnm_allocpamrow(&maskpam); + outputTuplerow = pnm_allocpamrow(&outpam); + + pnm_writepaminit(&outpam); + + for (row = 0; row < outpam.height; ++row) { + unsigned int col; + pnm_readpamrow(&inpam, inputTuplerow); + pnm_readpamrow(&maskpam, maskTuplerow); + + for (col = 0; col < outpam.width; ++col) { + unsigned int plane; + + for (plane = 0; plane < outpam.depth; ++plane) { + outputTuplerow[col][plane] = + sharpened(inputTuplerow[col][plane], + maskTuplerow[col][plane], + cmdline.sharpness, + threshold, + outpam.maxval); + } + } + pnm_writepamrow(&outpam, outputTuplerow); + } + + pm_close(ifP); + pm_close(maskfP); + + pnm_freepamrow(inputTuplerow); + pnm_freepamrow(maskTuplerow); + pnm_freepamrow(outputTuplerow); + + return 0; +} diff --git a/editor/pammixinterlace.c b/editor/pammixinterlace.c new file mode 100644 index 00000000..1421c7a2 --- /dev/null +++ b/editor/pammixinterlace.c @@ -0,0 +1,173 @@ +/****************************************************************************** + pammixinterlace +******************************************************************************* + De-interlace an image by merging adjacent rows. + + Copyright (C) 2005 Bruce Guenter, FutureQuest, Inc. + + 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. + +******************************************************************************/ + +#include "pam.h" +#include "shhopt.h" +#include "mallocvar.h" + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *inputFilespec; /* Filespecs of input files */ +}; + + +static void +parseCommandLine(int argc, char ** argv, + struct cmdlineInfo *cmdlineP) { +/*---------------------------------------------------------------------------- + Note that the file spec array we return is stored in the storage that + was passed to us as the argv array. +-----------------------------------------------------------------------------*/ + optStruct3 opt; /* set by OPTENT3 */ + optEntry *option_def; + unsigned int option_def_index; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + + 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 */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (argc-1 < 1) + cmdlineP->inputFilespec = "-"; + else if (argc-1 == 1) + cmdlineP->inputFilespec = argv[1]; + else + pm_error("You specified too many arguments (%d). The only " + "argument is the optional input file specification.", + argc-1); +} + + + +static void +allocateRowWindowBuffer(struct pam * const pamP, + tuple ** const tuplerow) { + + unsigned int row; + + for (row = 0; row < 3; ++row) + tuplerow[row] = pnm_allocpamrow(pamP); +} + + + +static void +freeRowWindowBuffer(tuple ** const tuplerow) { + + unsigned int row; + + for (row = 0; row < 3; ++row) + pnm_freepamrow(tuplerow[row]); + +} + + + +static void +slideWindowDown(tuple ** const tuplerow) { +/*---------------------------------------------------------------------------- + Slide the 3-line tuple row window tuplerow[] down one row by moving + pointers. + + tuplerow[2] ends up an uninitialized buffer. +-----------------------------------------------------------------------------*/ + tuple * const oldrow0 = tuplerow[0]; + tuplerow[0] = tuplerow[1]; + tuplerow[1] = tuplerow[2]; + tuplerow[2] = oldrow0; +} + + + +int +main(int argc, char *argv[]) { + + FILE * ifP; + struct cmdlineInfo cmdline; + struct pam inpam; + struct pam outpam; + tuple * tuplerow[3]; + tuple * outputrow; + + pnm_init( &argc, argv ); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilespec); + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + outpam = inpam; /* Initial value -- most fields should be same */ + outpam.file = stdout; + + pnm_writepaminit(&outpam); + + allocateRowWindowBuffer(&inpam, tuplerow); + outputrow = pnm_allocpamrow(&outpam); + + if (inpam.height < 3) { + unsigned int row; + pm_message("WARNING: Image height less than 3. No mixing done."); + for (row = 0; row < inpam.height; ++row) { + pnm_readpamrow(&inpam, tuplerow[0]); + pnm_writepamrow(&outpam, tuplerow[0]); + } + } else { + unsigned int row; + + pnm_readpamrow(&inpam, tuplerow[0]); + pnm_readpamrow(&inpam, tuplerow[1]); + + /* Pass through first row */ + pnm_writepamrow(&outpam, tuplerow[0]); + + for (row = 2; row < inpam.height; ++row) { + unsigned int col; + pnm_readpamrow(&inpam, tuplerow[2]); + for (col = 0; col < inpam.width; ++col) { + unsigned int plane; + + for (plane = 0; plane < inpam.depth; ++plane) { + outputrow[col][plane] = + (tuplerow[0][col][plane] + + tuplerow[1][col][plane] * 2 + + tuplerow[2][col][plane]) / 4; + } + } + pnm_writepamrow(&outpam, outputrow); + + slideWindowDown(tuplerow); + } + + /* Pass through last row */ + pnm_writepamrow(&outpam, tuplerow[1]); + } + + freeRowWindowBuffer(tuplerow); + pnm_freepamrow(outputrow); + pm_close(inpam.file); + pm_close(outpam.file); + + return 0; +} diff --git a/editor/pamoil.c b/editor/pamoil.c new file mode 100644 index 00000000..6cb8d3ac --- /dev/null +++ b/editor/pamoil.c @@ -0,0 +1,137 @@ +/* pgmoil.c - read a portable pixmap and turn into an oil painting +** +** Copyright (C) 1990 by Wilson Bent (whb@hoh-2.att.com) +** Shamelessly butchered into a color version by Chris Sheppard +** 2001 +** +** 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. +*/ + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include "pam.h" +#include "mallocvar.h" + +static void +convertRow(struct pam const inpam, tuple ** const tuples, + tuple * const tuplerow, int const row, int const smearFactor, + int * const hist) { + + int sample; + for (sample = 0; sample < inpam.depth; sample++) { + int col; + for (col = 0; col < inpam.width; ++col) { + int i; + int drow; + int modalval; + /* The sample value that occurs most often in the neighborhood + of the pixel being examined + */ + + /* Compute hist[] - frequencies, in the neighborhood, of each + sample value + */ + for (i = 0; i <= inpam.maxval; ++i) hist[i] = 0; + + for (drow = row - smearFactor; drow <= row + smearFactor; ++drow) { + if (drow >= 0 && drow < inpam.height) { + int dcol; + for (dcol = col - smearFactor; + dcol <= col + smearFactor; + ++dcol) { + if ( dcol >= 0 && dcol < inpam.width ) + ++hist[tuples[drow][dcol][sample]]; + } + } + } + { + /* Compute modalval */ + int sampleval; + int maxfreq; + + maxfreq = 0; + modalval = 0; + + for (sampleval = 0; sampleval <= inpam.maxval; ++sampleval) { + if (hist[sampleval] > maxfreq) { + maxfreq = hist[sampleval]; + modalval = sampleval; + } + } + } + tuplerow[col][sample] = modalval; + } + } +} + + + +int +main(int argc, char *argv[] ) { + struct pam inpam, outpam; + FILE* ifp; + tuple ** tuples; + tuple * tuplerow; + int * hist; + /* A buffer for the convertRow subroutine to use */ + int argn; + int row; + int smearFactor; + const char* const usage = "[-n <n>] [ppmfile]"; + + ppm_init( &argc, argv ); + + argn = 1; + smearFactor = 3; /* DEFAULT VALUE */ + + /* Check for options. */ + if ( argn < argc && argv[argn][0] == '-' ) { + if ( argv[argn][1] == 'n' ) { + ++argn; + if ( argn == argc || sscanf(argv[argn], "%d", &smearFactor) != 1 ) + pm_usage( usage ); + } else + pm_usage( usage ); + ++argn; + } + if ( argn < argc ) { + ifp = pm_openr( argv[argn] ); + ++argn; + } else + ifp = stdin; + + if ( argn != argc ) + pm_usage( usage ); + + tuples = pnm_readpam(ifp, &inpam, PAM_STRUCT_SIZE(tuple_type)); + pm_close(ifp); + + MALLOCARRAY(hist, inpam.maxval + 1); + if (hist == NULL) + pm_error("Unable to allocate memory for histogram."); + + outpam = inpam; outpam.file = stdout; + + pnm_writepaminit(&outpam); + + tuplerow = pnm_allocpamrow(&inpam); + + for (row = 0; row < inpam.height; ++row) { + convertRow(inpam, tuples, tuplerow, row, smearFactor, hist); + pnm_writepamrow(&outpam, tuplerow); + } + + pnm_freepamrow(tuplerow); + free(hist); + pnm_freepamarray(tuples, &inpam); + + pm_close(stdout); + exit(0); +} + diff --git a/editor/pamperspective.c b/editor/pamperspective.c new file mode 100644 index 00000000..fdf446c7 --- /dev/null +++ b/editor/pamperspective.c @@ -0,0 +1,1331 @@ +/* + pamperspective -- a reverse scanline renderer + + Copyright (C) 2004 by Mark Weyer + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +#define _BSD_SOURCE /* Make sure strdup is int string.h */ + +#include <math.h> +#include <string.h> + +#include "pam.h" +#include "shhopt.h" +#include "mallocvar.h" + +typedef double number; + +/* There was no reason for exactly this value of eps. + For compatibility it should only be decreased in future versions. +*/ + +#define eps 0.0001 + + +/* Multiple choice types for the command line */ + +typedef enum {image, pixel_u} unit; +const char *const unit_token[3] = {"image", "pixel", NULL}; + +typedef enum {lattice, pixel_s} coord_system; +const char *const system_token[3] = {"lattice", "pixel", NULL}; + +typedef enum {nearest, linear} interpolation; +const char *const interpolation_token[3] = {"nearest", "linear", NULL}; + +typedef enum {free_, fixed} proportion; +const char *const proportion_token[3] = {"free", "fixed", NULL}; + +const char *const bool_token[7] = {"yes", "true", "on", + "no", "false", "off", NULL}; +#define first_false_bool_token 3 + +/* All command line options that have float (actually number) values. + We use our own parsing technique for these, to handle width/height + ratios like 4/3 +*/ + +#define num_float_options 15 +const char *const float_option_name[num_float_options][3] = { + {"upper left x", "upper_left_x", "ulx"}, + {"upper left y", "upper_left_y", "uly"}, + {"upper right x", "upper_right_x", "urx"}, + {"upper right y", "upper_right_y", "ury"}, + {"lower left x", "lower_left_x", "llx"}, + {"lower left y", "lower_left_y", "lly"}, + {"lower right x", "lower_right_x", "lrx"}, + {"lower right y", "lower_right_y", "lry"}, + {NULL, "detail", NULL}, + {NULL, "ratio", NULL}, + {NULL, "margin", NULL}, + {NULL, "top_margin", "tmargin"}, + {NULL, "bottom_margin", "bmargin"}, + {NULL, "left_margin", "lmargin"}, + {NULL, "right_margin", "rmargin"} +}; + +/* All command line options that have multiple choice values (except bools). */ + +#define num_enum_options 5 +const char *const enum_option_name[num_enum_options] = { + "input_system", "output_system", "input_unit", "interpolation", "proportion" +}; +const char *const *const enum_option_type[num_enum_options] = { + system_token, system_token, unit_token, interpolation_token, proportion_token +}; + +/* All command line options that have bool values */ + +#define num_bool_options 1 +const char *const bool_option_name[num_bool_options] = { + "frame_include" +}; + + +/* A linked list node for --include points */ + +typedef struct include_point_tag { + + /* How the point is given on the command line (for error messages) */ + + const char* specification; + + /* coordinates */ + + number xi,yi; + + /* link */ + + struct include_point_tag* next; + +} include_point; + + +/* The collection of command line options */ + +typedef struct { + + /* float options */ + + number floats[num_float_options]; + + /* enum options */ + + int enums[num_enum_options]; + + /* bool options */ + + bool bools[num_bool_options]; + + /* flags */ + + unsigned int width_spec, height_spec, + top_margin_spec, left_margin_spec, right_margin_spec, bottom_margin_spec; + + /* Other stuff */ + + int width, height; + const char* infilename; + include_point* include_points; + +} option; + + +/* The collection of properties that correspond to the four specified + vertices +*/ + +typedef struct { + + /* 2d (image) coordinates of the 4 vertices */ + + number xi_ul, yi_ul, xi_ur, yi_ur, xi_ll, yi_ll, xi_lr, yi_lr; + + /* 3d (world) coordinates of the 4 vertices */ + + number xw_ul, yw_ul, zw_ul, xw_ur, yw_ur, zw_ur, + xw_ll, yw_ll, zw_ll, xw_lr, yw_lr, zw_lr; + + /* Originally I planned to include the possibility to move the + centre of projection, that is the pixel the camera "looks at". It + turned out, maybe surprisingly, that this does not have any + effect. So now this centre is moved to (0,0). + + Another original plan was to correct the output parameters + depending on the lengths of the paralellograms sides or its + angles. This is, however, not possible without knowing something + like the camera angle or focal length (in pixels). + */ + + /* The coefficients for the map from output to world coordinates. + + The actual mapping is + u,v -> (ax+bx*u+cx*v, ay+by*u+cy*v, az+bz*u+cz*v) + */ + number ax,bx,cx, ay,by,cy, az,bz,cz; + +} world_data; + + +/* + Internal infile buffer + + This is a cyclic in random access out buffer, just large enough + to store all input lines that are still in use. +*/ + +typedef struct { + + int num_rows, last_physical, last_logical; + tuple** rows; + const struct pam* inpam; + +} buffer; + + + +/* + The following are like MALLOCARRAY_NOFAIL and MALLOCVAR_NOFAIL, + but issue an error message instead of aborting. +*/ + +#define MALLOCARRAY_SAFE(handle,length) \ +{ \ + MALLOCARRAY(handle,length); \ + if (handle==NULL) \ + pm_error ("Out of memory."); \ +} + +#define MALLOCVAR_SAFE(handle) \ +{ \ + MALLOCVAR(handle); \ + if (handle==NULL) \ + pm_error ("Out of memory."); \ +} + + + +static void set_command_line_defaults (option *const options) +{ + options->infilename = "-"; + options->include_points = NULL; + options->floats[8] = 1.0; /* --detail */ + options->floats[9] = 1.0; /* --ratio */ + options->floats[10] = 0.0; /* --margin */ + options->floats[11] = 0.0; /* --top_margin */ + options->floats[12] = 0.0; /* --bottom_margin */ + options->floats[13] = 0.0; /* --left_margin */ + options->floats[14] = 0.0; /* --right_margin */ + options->enums[0] = lattice; /* --input_system */ + options->enums[1] = lattice; /* --output_system */ + options->enums[2] = pixel_u; /* --input_unit */ + options->enums[3] = nearest; /* --interpolation */ + options->enums[4] = free_; /* --proportion */ + options->bools[0] = TRUE; /* --frame_include */ +} + + + +static int parse_enum (const char *const text, + const char *const *const tokens, const char *const name) +/*---------------------------------------------------------------------------- + Parse an argument given to a multiple choice command line option +-----------------------------------------------------------------------------*/ +{ + bool found; + const char *const * cur_token; + char* tokenlist; + int tokenlistlen; + int value; + int num_spaces; + int i; + + /* We find out, whether ^text occurs in ^tokens */ + + found = FALSE; + value = 0; + while (tokens[value] && !found) { + if (strcmp (text, tokens[value])) + value++; + else + found = TRUE; + }; + + /* otherwise issue an error */ + + if (!found) { + /* For the error message we want to list the allowed tokens. + First we have to determine, how much memory we need for that. + */ + num_spaces = 2; + + tokenlistlen = 0; + cur_token = tokens; + while (*cur_token) { + tokenlistlen += (strlen(*cur_token) + num_spaces); + cur_token++; + }; + /* Then we create that list */ + MALLOCARRAY_SAFE(tokenlist, tokenlistlen); + *tokenlist = 0; + cur_token = tokens; + while (*cur_token) { + for (i=0; i<num_spaces; i++) + strcat (tokenlist, " "); + strcat (tokenlist, *cur_token); + cur_token++; + }; + /* Finally we issue the error */ + pm_error ("'%s' is not a valid value for --%s. " + "Valid values are: %s", text, name, tokenlist); + /* pm_error() aborts the program, so there is no memory freeing here. */ + }; + + /* If all went well, we return the value associated with the token, + which happens to be the index where we found the token + */ + return value; +} + + + +static number parse_float (char *const text) +/*---------------------------------------------------------------------------- + Parse an argument given to a float command line option. We cannot + just call strtod, because we want to parse fractions like "5/3" +-----------------------------------------------------------------------------*/ +{ + bool error; + char* end; + char* denstart; + number num,den; + + error = FALSE; + num = strtod (text, &end); /* try strtod anyway */ + switch (*end) { + case 0: /* It is a plain number */ + break; + case '/': /* It might be a fraction */ + /* (Try to) parse the numerator */ + *end = 0; + num = strtod (text, &end); + error = (*end) != 0; + if (!error) { + /* Undo the above change */ + *end = '/'; + /* (Try to) parse the denominator */ + denstart = end+1; + den = strtod (denstart, &end); + error = (fabs(den)<eps) || ((*end) != 0); + if (!error) + num /= den; + }; + break; + default: /* It is no number format we know */ + error = TRUE; + }; + if (error) + pm_error ("Invalid number format: %s", text); + + return num; +} + + + +static void parse_include_point(char * specification, + include_point ** const include_pointsP) +/*---------------------------------------------------------------------------- + Add one point to the front of the linked list of include points + headed by include_pointsP. + + The point is described by the asciiz string at 'specification'. +----------------------------------------------------------------------------*/ +{ + include_point* new_point; + char* comma_seek; + + MALLOCVAR_SAFE(new_point); + new_point->specification = specification; + new_point->next = *include_pointsP; + *include_pointsP = new_point; + + /* Now we parse the specification */ + + for (comma_seek = specification; (*comma_seek != ',') && (*comma_seek != 0); + comma_seek++); + if (*comma_seek == 0) + pm_error ("Invalid format for --include point: '%s'", specification); + *comma_seek = 0; /* separate the two parts for parsing purposes */ + new_point->xi = (number) parse_float(specification); + new_point->yi = (number) parse_float(comma_seek+1); + *comma_seek = ','; +} + + +static void parse_include_points(const char * const include_opt, + include_point ** const include_pointsP) +/*---------------------------------------------------------------------------- + Process the --include option value include_opt by making a linked list + of the points it describes (in reverse order). + + Return a pointer to the first element of that linked list as + *include_pointsP. +----------------------------------------------------------------------------*/ +{ + char * cursor; + char * optWork; + /* Same as include_opt, except we replace delimiters with nulls + as we work. + */ + + optWork = strdup(include_opt); + if (optWork == NULL) + pm_error("out of memory"); + + cursor = &optWork[0]; + while (*cursor != '\0') { + bool hit_end; + char * sem_seek; + + for (sem_seek = cursor; + (*sem_seek != ';') && (*sem_seek != 0); + sem_seek++); + + hit_end = (*sem_seek == '\0'); + + *sem_seek = '\0'; + parse_include_point(cursor, include_pointsP); + + if (hit_end) + cursor = sem_seek; + else + cursor = sem_seek+1; + } + free(optWork); +} + + +static void parse_command_line (int argc, char* argv[], option *const options) +{ + char* float_text[num_float_options]; + unsigned int float_spec[num_float_options]; + char* enum_text[num_enum_options]; + unsigned int enum_spec[num_enum_options]; + char* bool_text[num_bool_options]; + unsigned int bool_spec[num_bool_options]; + char * include_opt; + unsigned int include_spec; + int i,j; + optStruct3 opt; + unsigned int option_def_index; + optEntry* option_def; + + /* Let shhopt try its best */ + + option_def_index = 0; + MALLOCARRAY_SAFE(option_def, + (2*num_float_options + num_enum_options + num_bool_options + 3)); + for (i=0; i<num_float_options; i++) + for (j=1; j<3; j++) + if (float_option_name[i][j]) + OPTENT3(0, float_option_name[i][j], OPT_STRING, + &(float_text[i]), &(float_spec[i]), 0); + for (i=0; i<num_enum_options; i++) + OPTENT3(0, enum_option_name[i], OPT_STRING, + &(enum_text[i]), &(enum_spec[i]), 0); + for (i=0; i<num_bool_options; i++) + OPTENT3(0, bool_option_name[i], OPT_STRING, + &(bool_text[i]), &(bool_spec[i]), 0); + OPTENT3(0, "width", OPT_INT, &(options->width), &(options->width_spec), 0); + OPTENT3(0, "height", OPT_INT, &(options->height), &(options->height_spec), + 0); + OPTENT3(0, "include", OPT_STRING, &include_opt, &include_spec, 0); + opt.opt_table = option_def; + opt.short_allowed = FALSE; + opt.allowNegNum = TRUE; + optParseOptions3 (&argc, argv, opt, sizeof(opt), 0); + + /* The non-option arguments are optionally all eight coordinates + and optionally the input filename + */ + + switch (argc-1) { + case 1: + options->infilename = argv[1]; + case 0: + for (i=0; i<8; i++) + if (!float_spec[i]) + pm_error ("The %s-coordinate was not specified", + float_option_name[i][0]); + break; + case 9: + options->infilename = argv[9]; + case 8: + for (i=0; i<8; i++) { + float_text[i] = argv[i+1]; + float_spec[i] = 1; + }; + break; + default: pm_error ("Wrong (number of) command line arguments"); + }; + + if (include_spec) + parse_include_points(include_opt, &options->include_points); + + /* Parse float options -- shhopt retrieved them as strings */ + + for (i=0; i<num_float_options; i++) + if (float_spec[i]) + options->floats[i] = parse_float (float_text[i]); + + /* Parse enum options -- shhopt retrieved them as strings */ + + for (i=0; i<num_enum_options; i++) + if (enum_spec[i]) + options->enums[i] = parse_enum (enum_text[i],enum_option_type[i], + enum_option_name[i]); + + /* Parse bool options -- shhopt retrieved them as strings */ + + for (i=0; i<num_bool_options; i++) + if (bool_spec[i]) + options->bools[i] = (first_false_bool_token > + parse_enum (bool_text[i], bool_token, + bool_option_name[i])); + + /* Propagate values where neccessary */ + + if (float_spec[10]) /* --margin */ + for (i=11; i<15; i++) /* --top_margin through --right_margin */ + if (!(float_spec[i])) { + options->floats[i] = options->floats[10]; + float_spec[i]=1; + }; + options->top_margin_spec = float_spec[11]; + options->bottom_margin_spec = float_spec[12]; + options->left_margin_spec = float_spec[13]; + options->right_margin_spec = float_spec[14]; + + /* Clean up */ + + free(option_def); +} + + + +static void free_option (option *const options) +{ + include_point* current; + include_point* dispose; + + current = options->include_points; + while (current != NULL) { + dispose = current; + current = current->next; + free(dispose); + }; +} + + + +static void init_world (option *const options, + const struct pam *const inpam, world_data *const world) +{ + /* constructs xi_ul,...,yi_lr + + Internally we use a pixel coordinate system with pixel units + + This also translates the --include points' coordinates + into the internal system + */ + + number mult_x, mult_y, add_after; + int add_before; + include_point* current_include; + + switch (options->enums[0]) { /* --input_system */ + case lattice: + add_after = -0.5; + add_before = 0; + break; + case pixel_s: + add_after = 0.0; + add_before = -1; + break; + }; + switch (options->enums[2]) { /* --input_unit */ + case image: + mult_x = (number)((inpam->width) + add_before); + mult_y = (number)((inpam->height) + add_before); + break; + case pixel_u: + mult_x = 1.0; + mult_y = 1.0; + break; + }; + + world->xi_ul = ((number) options->floats[0]) * mult_x + add_after; + world->yi_ul = ((number) options->floats[1]) * mult_y + add_after; + world->xi_ur = ((number) options->floats[2]) * mult_x + add_after; + world->yi_ur = ((number) options->floats[3]) * mult_y + add_after; + world->xi_ll = ((number) options->floats[4]) * mult_x + add_after; + world->yi_ll = ((number) options->floats[5]) * mult_y + add_after; + world->xi_lr = ((number) options->floats[6]) * mult_x + add_after; + world->yi_lr = ((number) options->floats[7]) * mult_y + add_after; + + for (current_include = options->include_points; current_include != NULL; + current_include = current_include->next) { + current_include->xi = current_include->xi * mult_x + add_after; + current_include->yi = current_include->yi * mult_y + add_after; + }; +} + + + +static bool solve_3_linear_equations (number* x1, number* x2, number* x3, + number const a11, number const a12, + number const a13, number const b1, + number const a21, number const a22, + number const a23, number const b2, + number const a31, number const a32, + number const a33, number const b3) +/*---------------------------------------------------------------------------- + The three equations are + a11*x1 + a12*x2 + a13*x3 = b1 + a21*x1 + a22*x2 + a23*x3 = b2 + a31*x1 + a32*x2 + a33*x3 = b3 + The return value is wether the system is solvable +----------------------------------------------------------------------------*/ +{ + number c11,c12,d1,c21,c22,d2,e,f; + int pivot; + + /* We do Gaussian elimination. + Whenever we find the system to be unsolvable, we just return FALSE. + In this specific case it makes the code clearer. + */ + + if (fabs(a11)<fabs(a21)) + if (fabs(a21)<fabs(a31)) + pivot=3; + else + pivot=2; + else + if (fabs(a11)<fabs(a31)) + pivot=3; + else + pivot=1; + + switch (pivot) { + case 1: + if (fabs(a11)<eps) return FALSE; + c11 = a22-a12*a21/a11; + c12 = a23-a13*a21/a11; + d1 = b2- b1*a21/a11; + c21 = a32-a12*a31/a11; + c22 = a33-a13*a31/a11; + d2 = b3- b1*a31/a11; + break; + case 2: + if (fabs(a21)<eps) return FALSE; + c11 = a12-a22*a11/a21; + c12 = a13-a23*a11/a21; + d1 = b1- b2*a11/a21; + c21 = a32-a22*a31/a21; + c22 = a33-a23*a31/a21; + d2 = b3- b2*a31/a21; + break; + case 3: + if (fabs(a31)<eps) return FALSE; + c11 = a12-a32*a11/a31; + c12 = a13-a33*a11/a31; + d1 = b1- b3*a11/a31; + c21 = a22-a32*a21/a31; + c22 = a23-a33*a21/a31; + d2 = b2- b3*a21/a31; + break; + } + + /* Now we have a subsystem: + c11*x2 + c12*x3 = d1 + c21*x2 + c22*x3 = d2 + */ + + if (fabs(c11)>fabs(c21)) { + if (fabs(c11)<eps) return FALSE; + e = c22-c12*c21/c11; + f = d2- d1*c21/c11; + /* Now we have a single equation e*x3=f */ + if (fabs(e)<eps) return FALSE; + *x3 = f/e; + *x2 = (d1-c12*(*x3))/c11; + } + else { + if (fabs(c21)<eps) return FALSE; + e = c12-c22*c11/c21; + f = d1- d2*c11/c21; + /* Now we have a single equation e*x3=f */ + if (fabs(e)<eps) return FALSE; + *x3 = f/e; + *x2 = (d2-c22*(*x3))/c21; + }; + + switch (pivot) { + case 1: + *x1 = (b1-a13*(*x3)-a12*(*x2))/a11; + break; + case 2: + *x1 = (b2-a23*(*x3)-a22*(*x2))/a21; + break; + case 3: + *x1 = (b3-a33*(*x3)-a32*(*x2))/a31; + break; + }; + + return TRUE; +} + + +static void determine_world_parallelogram (world_data *const world, + const option *const options) +/*---------------------------------------------------------------------------- + constructs xw_ul,...,zw_lr from xi_ul,...,yi_lr + + Actually this is a solution of a linear equation system. + + We first solve 4 variables (the 4 z-coordinates) against 4 + equations: Each z-coordinate determines the corresponding x- and + y-coordinates in a linear fashion, where the coefficients are taken + from the image coordinates. This corresponds to the fact that a + point of an image determines a line in the world. + + 3 equations state that the 4 points form a parallelogram. The 4th + equation is for normalization and states, that the centre of the + parallelogram has a z-coordinate of 1. +-----------------------------------------------------------------------------*/ +{ + number dx1,dx2,dx3,dx4,dx5, dy1,dy2,dy3,dy4,dy5; + number det; + number xw_ul,yw_ul,zw_ul, xw_ur,yw_ur,zw_ur, + xw_ll,yw_ll,zw_ll, xw_lr,yw_lr,zw_lr; + number top_margin, left_margin, right_margin, bottom_margin; + include_point* current_include; + number include_xo, include_yo, include_zw; + bool solvable, margin_spec; + + dx1 = world->xi_lr - world->xi_ul; /* d1 is the image diagonal ul -> lr */ + dy1 = world->yi_lr - world->yi_ul; + dx2 = world->xi_ur - world->xi_ll; /* d2 is the image diagonal ll -> ur */ + dy2 = world->yi_ur - world->yi_ll; + dx3 = world->xi_ur - world->xi_ul; /* d3 is the image side ul -> ur */ + dy3 = world->yi_ur - world->yi_ul; + dx4 = world->xi_ul - world->xi_ll; /* d4 is the image side ll -> ul */ + dy4 = world->yi_ul - world->yi_ll; + dx5 = world->xi_ur - world->xi_lr; /* d5 is the image side lr -> ur */ + dy5 = world->yi_ur - world->yi_lr; + + det = dx2*dy1 - dx1*dy2; + + /* A determinant of 0 is really bad: It means that that diagonals in the + image are parallel (or of zero length) + */ + + if ((-eps<det) && (det<eps)) + pm_error ("The specified vertices are degenerated. " + "Maybe they were given in the wrong order?"); + + zw_ul = 2.0*(dx5*dy2-dx2*dy5)/det; + zw_ur = 2.0*(dx4*dy1-dx1*dy4)/det; + zw_ll = 2.0*(dx3*dy1-dx1*dy3)/det; + zw_lr = 2.0*(dx2*dy3-dx3*dy2)/det; + + /* A zero or negative value for some z means that three of the points + lie on a line in the image or that the four points do not define + a convex shape. We have to forbid this in order to prevent divisions + by zero later on + */ + + if ((zw_ul<eps) || (zw_ur<eps) || (zw_ll<eps) || (zw_lr<eps)) + pm_error ("The specified vertices are degenerated. " + "Maybe they were given in the wrong order?"); + + xw_ul = world->xi_ul * zw_ul; + yw_ul = world->yi_ul * zw_ul; + xw_ur = world->xi_ur * zw_ur; + yw_ur = world->yi_ur * zw_ur; + xw_ll = world->xi_ll * zw_ll; + yw_ll = world->yi_ll * zw_ll; + xw_lr = world->xi_lr * zw_lr; + yw_lr = world->yi_lr * zw_lr; + + /* Now we introduce the margin. There are several ways the margin can be + defined. margin_spec keeps track of wether one of them has yet been + used. As long as margin_spec==FALSE, the variables top_margin to + bottom_margin are not initialized! */ + + if (options->bools[0]) { /* --frame_include */ + top_margin = 0.0; + left_margin = 0.0; + right_margin = 0.0; + bottom_margin = 0.0; + margin_spec = TRUE; + } else + margin_spec = FALSE; + + for (current_include = options->include_points; current_include != NULL; + current_include = current_include->next) { + solvable = solve_3_linear_equations(&include_xo, &include_yo, &include_zw, + xw_ul-xw_ur, xw_ul-xw_ll, current_include->xi, xw_ul, + yw_ul-yw_ur, yw_ul-yw_ll, current_include->yi, yw_ul, + zw_ul-zw_ur, zw_ul-zw_ll, 1.0, zw_ul); + if (!solvable) + pm_error ("The --include point %s lies on the horizon.", + current_include->specification); + if (include_zw<0.0) + pm_error ("The --include point %s lies beyond the horizon.", + current_include->specification); + if (margin_spec) { + top_margin = MAX(top_margin, -include_yo); + left_margin = MAX(left_margin, -include_xo); + right_margin = MAX(right_margin, include_xo-1.0); + bottom_margin = MAX(bottom_margin, include_yo-1.0); + } else { + top_margin = -include_yo; + left_margin = -include_xo; + right_margin = include_xo-1.0; + bottom_margin = include_yo-1.0; + margin_spec = TRUE; + }; + } + + if (margin_spec) { /* the margin is there. --top_margin and such can + still enlarge it */ + if (options->top_margin_spec) + top_margin = MAX(top_margin, options->floats[11]); + if (options->left_margin_spec) + left_margin = MAX(left_margin, options->floats[13]); + if (options->right_margin_spec) + right_margin = MAX(right_margin, options->floats[14]); + if (options->bottom_margin_spec) + bottom_margin = MAX(bottom_margin, options->floats[12]); + } else /* the margin is not yet there. --top_margin and + such can remedy this only if all of them are + given */ + if ((options->top_margin_spec) && (options->left_margin_spec) && + (options->right_margin_spec) && (options->bottom_margin_spec)) { + top_margin = options->floats[11]; + left_margin = options->floats[13]; + right_margin = options->floats[14]; + bottom_margin = options->floats[12]; + } else /* the margin finally is not there */ + pm_error ("No frame specified. " + "Use --frame_include=yes or --include or --margin."); + + world->xw_ul = xw_ul + - top_margin * (xw_ll-xw_ul) + - left_margin * (xw_ur-xw_ul); + world->yw_ul = yw_ul + - top_margin * (yw_ll-yw_ul) + - left_margin * (yw_ur-yw_ul); + world->zw_ul = zw_ul + - top_margin * (zw_ll-zw_ul) + - left_margin * (zw_ur-zw_ul); + world->xw_ur = xw_ur + - top_margin * (xw_lr-xw_ur) + - right_margin * (xw_ul-xw_ur); + world->yw_ur = yw_ur + - top_margin * (yw_lr-yw_ur) + - right_margin * (yw_ul-yw_ur); + world->zw_ur = zw_ur + - top_margin * (zw_lr-zw_ur) + - right_margin * (zw_ul-zw_ur); + world->xw_ll = xw_ll + - bottom_margin * (xw_ul-xw_ll) + - left_margin * (xw_lr-xw_ll); + world->yw_ll = yw_ll + - bottom_margin * (yw_ul-yw_ll) + - left_margin * (yw_lr-yw_ll); + world->zw_ll = zw_ll + - bottom_margin * (zw_ul-zw_ll) + - left_margin * (zw_lr-zw_ll); + world->xw_lr = xw_lr + - bottom_margin * (xw_ur-xw_lr) + - right_margin * (xw_ll-xw_lr); + world->yw_lr = yw_lr + - bottom_margin * (yw_ur-yw_lr) + - right_margin * (yw_ll-yw_lr); + world->zw_lr = zw_lr + - bottom_margin * (zw_ur-zw_lr) + - right_margin * (zw_ll-zw_lr); + + /* Again we have to forbid nonpositive z */ + + if ((world->zw_ul<eps) || (world->zw_ur<eps) || + (world->zw_ll<eps) || (world->zw_lr<eps)) + pm_error ("The specified margin is too large."); + +} + + + +static int diff (int const a, int const b) +{ + return MAX (b-a, a-b); +} + + + +static number norm_vector (number const x1, number const y1, number const z1, + number const x2, number const y2, number const z2) +/*---------------------------------------------------------------------------- + Two 3D vertices p1 and p2 are given by their coordinates. + A linear movement from p1 to p2, parameterized by the interval [0,1], + is projected to the input image. The function returns the norm of + the derivative of this overall movement at time 0, that is at p1. + The norm uses the max metric. +-----------------------------------------------------------------------------*/ +{ + number dx,dy; + + dx = (x2-x1)/z1 - (z2-z1)*x1/(z1*z1); + dy = (y2-y1)/z1 - (z2-z1)*y1/(z1*z1); + + return MAX (fabs(dx), fabs(dy)); +} + + + +static number norm_side (number const x1, number const y1, number const z1, + number const x2, number const y2, number const z2) +/*---------------------------------------------------------------------------- + This is similar to norm_vector. But now the norm of the derivative + is computed at both endpoints of the movement and the maximum is + returned. + + Why do we do this? The return value n is in fact the maximum of the + norm of the derivative ALONG the movement. So we know that if we + divide the movement into at least n steps, we will encounter every + x- and every y-coordinate of the input image between the two points. + This is our notion of losslessness with --detail=1. +-----------------------------------------------------------------------------*/ +{ + return MAX (norm_vector(x1,y1,z1,x2,y2,z2), + norm_vector(x2,y2,z2,x1,y1,z1)); +} + + + +static void determine_output_width_and_height (const world_data *const world, + option *const options) +{ + number du,dv; + int xsteps,ysteps,width,height; + + /* Determine the number of steps for losslessness */ + + du = MAX (norm_side(world->xw_ul, world->yw_ul, world->zw_ul, + world->xw_ur, world->yw_ur, world->zw_ur), + norm_side(world->xw_ll, world->yw_ll, world->zw_ll, + world->xw_lr, world->yw_lr, world->zw_lr)); + dv = MAX (norm_side(world->xw_ul, world->yw_ul, world->zw_ul, + world->xw_ll, world->yw_ll, world->zw_ll), + norm_side(world->xw_ur, world->yw_ur, world->zw_ur, + world->xw_lr, world->yw_lr, world->zw_lr)); + xsteps = ceil(du*options->floats[8]); /* option->floats[8] is --detail */ + ysteps = ceil(dv*options->floats[8]); + + /* Turn the numbers of steps into width and height */ + + switch (options->enums[1]) { /* --output_system */ + case lattice: + width = xsteps; + height = ysteps; + break; + case pixel_s: + width = xsteps+1; + height = ysteps+1; + break; + }; + + /* Correct the proportion of width and height by increasing one of them */ + + switch (options->enums[4]) { /* --proportion */ + case free_: /* no correction at all */ + break; + case fixed: /* correction now */ + /* options->floats[9] is --ratio */ + width = MAX (floor(0.5 + ((number)height) * options->floats[9]), + width); + height = MAX (floor(0.5 + ((number)width) / options->floats[9]), + height); + break; + }; + + /* Override anything we have by the specified width and height */ + + if (!(options->width_spec)) + options->width=width; + if (!(options->height_spec)) + options->height=height; +} + + + +static void determine_coefficients_lattice (world_data *const world, + const option *const options) +/*---------------------------------------------------------------------------- + Constructs ax,...,cz from xw_ul,...,zw_lr + + The calculations assume lattice coordinates, that is the point ul + corresponds to the upper left corner of the pixel (0,0) and the + point lr corresponds to the lower left corner of the pixel + (width-1,height-1) +-----------------------------------------------------------------------------*/ +{ + number width,height; + + width = (number) options->width; + height = (number) options->height; + + world->bx = (world->xw_ur - world->xw_ul)/width; + world->cx = (world->xw_ll - world->xw_ul)/height; + world->by = (world->yw_ur - world->yw_ul)/width; + world->cy = (world->yw_ll - world->yw_ul)/height; + world->bz = (world->zw_ur - world->zw_ul)/width; + world->cz = (world->zw_ll - world->zw_ul)/height; + + world->ax = world->xw_ul + world->bx/2.0 + world->cx/2.0; + world->ay = world->yw_ul + world->by/2.0 + world->cy/2.0; + world->az = world->zw_ul + world->bz/2.0 + world->cz/2.0; +} + + + +static void determine_coefficients_pixel (world_data *const world, + const option *const options) +/*---------------------------------------------------------------------------- + Constructs ax,...,cz from xw_ul,...,zw_lr + + The calculations assume pixel coordinates, that is the point ul + corresponds to the centre of the pixel (0,0) and the point lr + corresponds to the centre of the pixel (width-1,height-1) +-----------------------------------------------------------------------------*/ +{ + number width,height; + + if (options->width == 1) + pm_error ("You specified 'pixel' as output coordinate model " + "and a width of 1. These things don't mix."); + if (options->height == 1) + pm_error ("You specified 'pixel' as output coordinate model " + "and a height of 1. These things don't mix."); + + width = (number) (options->width-1); + height = (number) (options->height-1); + + world->bx = (world->xw_ur - world->xw_ul)/width; + world->cx = (world->xw_ll - world->xw_ul)/height; + world->by = (world->yw_ur - world->yw_ul)/width; + world->cy = (world->yw_ll - world->yw_ul)/height; + world->bz = (world->zw_ur - world->zw_ul)/width; + world->cz = (world->zw_ll - world->zw_ul)/height; + + world->ax = world->xw_ul; + world->ay = world->yw_ul; + world->az = world->zw_ul; +} + + + +static void outpixel_to_inpixel (int const xo, int const yo, + number* const xi, number* const yi, + const world_data *const world) +{ + number xof,yof,xw,yw,zw; + + xof = (number) xo; + yof = (number) yo; + xw = world->ax + world->bx*xof + world->cx*yof; + yw = world->ay + world->by*xof + world->cy*yof; + zw = world->az + world->bz*xof + world->cz*yof; + *xi = xw/zw; + *yi = yw/zw; +} + +static int outpixel_to_iny (int xo, int yo, const world_data *const world) +{ + number xi,yi; + + outpixel_to_inpixel (xo,yo,&xi,&yi,world); + + return (int) yi; +} + +static int clean_y (int const y, const struct pam *const outpam) +{ + return MIN(MAX(0, y), outpam->height-1); +} + +static void init_buffer (buffer *const b, const world_data *const world, + const option *const options, + const struct pam *const inpam, + const struct pam *const outpam) +{ + int yul, yur, yll, ylr, y_min; + int i, num_rows; + + yul = outpixel_to_iny (0,0,world); + yur = outpixel_to_iny (outpam->width-1,0,world); + yll = outpixel_to_iny (0,outpam->height-1,world); + ylr = outpixel_to_iny (outpam->width-1,outpam->height-1,world); + + y_min = MIN (MIN (yul,yur), MIN (yll,ylr)); + num_rows = MAX (MAX (diff (yul, yur), + diff (yll, ylr)), + MAX (diff (clean_y(yul,outpam), clean_y(y_min,outpam)), + diff (clean_y(yur,outpam), clean_y(y_min,outpam)))) + + 2; + switch (options->enums[3]) { /* --interpolation */ + case nearest: + break; + case linear: + num_rows += 1; + break; + }; + if (num_rows > inpam->height) + num_rows = inpam->height; + + b->num_rows = num_rows; + MALLOCARRAY_SAFE (b->rows, num_rows); + for (i=0; i<num_rows; i++) { + b->rows[i] = pnm_allocpamrow (inpam); + pnm_readpamrow (inpam, b->rows[i]); + }; + b->last_physical = num_rows-1; + b->last_logical = num_rows-1; + b->inpam = inpam; +} + +static tuple* read_buffer (buffer *const b, int const logical_y) +{ + int y; + + while (logical_y > b->last_logical) { + b->last_physical++; + if (b->last_physical == b->num_rows) + b->last_physical = 0; + pnm_readpamrow (b->inpam, b->rows[b->last_physical]); + b->last_logical++; + } + + y = logical_y - b->last_logical + b->last_physical; + if (y<0) + y += b->num_rows; + + return b->rows[y]; +} + +static void free_buffer (buffer *const b) +{ + int i; + + for (i=0; i<b->num_rows; i++) + pnm_freepamrow (b->rows[i]); + free (b->rows); +} + + + + +/* The following variables are global for speed reasons. + In this way they do not have to be passed to each call of the + interpolation functions + + Think of this as Schönfinkeling (aka Currying). +*/ + +static tuple background; +static buffer* indata; +static int width,height,depth; + +static void init_interpolation_global_vars (buffer* const inbuffer, + const struct pam *const inpam, + const struct pam *const outpam) +{ + pnm_createBlackTuple (outpam, &background); + indata = inbuffer; + width = inpam->width; + height = inpam->height; + depth = outpam->depth; +} + + + +static void clean_interpolation_global_vars (void) +{ + free (background); +} + + + +/* These functions perform the interpolation */ + +static tuple attempt_read (int const x, int const y) +{ + if ((x<0) || (x>=width) || (y<0) || (y>=height)) + return background; + else + return read_buffer(indata, y)[x]; +} + + + +static void take_nearest (tuple const dest, number const x, number const y) +{ + int xx,yy,entry; + tuple p; + + xx = (int)floor(x+0.5); + yy = (int)floor(y+0.5); + p = attempt_read (xx, yy); + for (entry=0; entry<depth; entry++) { + dest[entry]=p[entry]; + } +} + + + +static void linear_interpolation (tuple const dest, + number const x, number const y) +{ + int xx,yy,entry; + number xf,yf,a,b,c,d; + tuple p1,p2,p3,p4; + + xx = (int)floor(x); + yy = (int)floor(y); + xf = x-(number)xx; + yf = y-(number)yy; + p1 = attempt_read (xx, yy); + p2 = attempt_read (xx+1, yy); + p3 = attempt_read (xx, yy+1); + p4 = attempt_read (xx+1, yy+1); + a = (1.0-xf)*(1.0-yf); + b = xf*(1.0-yf); + c = (1.0-xf)*yf; + d = xf*yf; + for (entry=0; entry<depth; entry++) { + dest[entry]=(sample) floor( + a*((number) p1[entry]) + + b*((number) p2[entry]) + + c*((number) p3[entry]) + + d*((number) p4[entry]) + + 0.5); + } +} + + + +int main (int argc, char* argv[]) +{ + FILE* infp; + struct pam inpam; + buffer inbuffer; + FILE* outfp; + struct pam outpam; + tuple* outrow; + option options; + world_data world; + int row,col; + number xi,yi; + void (*interpolate) (tuple, number, number); + + /* The usual initializations */ + + pnm_init (&argc, argv); + set_command_line_defaults (&options); + parse_command_line (argc, argv, &options); + infp = pm_openr (options.infilename); + pnm_readpaminit (infp, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + /* Our own initializations */ + + init_world (&options, &inpam, &world); + determine_world_parallelogram (&world, &options); + determine_output_width_and_height (&world, &options); + switch (options.enums[1]) { /* --output_system */ + case lattice: + determine_coefficients_lattice (&world, &options); + break; + case pixel_s: + determine_coefficients_pixel (&world, &options); + break; + }; + + /* Initialize outpam */ + + outfp = pm_openw ("-"); + outpam.size = sizeof (outpam); + outpam.len = PAM_STRUCT_SIZE(bytes_per_sample); + outpam.file = outfp; + outpam.format = inpam.format; + outpam.plainformat = inpam.plainformat; + outpam.height = options.height; + outpam.width = options.width; + outpam.depth = inpam.depth; + outpam.maxval = inpam.maxval; + outpam.bytes_per_sample = inpam.bytes_per_sample; + pnm_writepaminit (&outpam); + + /* Initialize the actual calculation */ + + init_buffer (&inbuffer, &world, &options, &inpam, &outpam); + outrow = pnm_allocpamrow (&outpam); + init_interpolation_global_vars (&inbuffer,&inpam,&outpam); + switch (options.enums[3]) { /* --interpolation */ + case nearest: + interpolate = take_nearest; + break; + case linear: + interpolate = linear_interpolation; + break; + }; + + /* Perform the actual calculation */ + + for (row=0; row<outpam.height; row++) { + for (col=0; col<outpam.width; col++) { + outpixel_to_inpixel (col,row,&xi,&yi,&world); + interpolate(outrow[col],xi,yi); + } + pnm_writepamrow (&outpam, outrow); + } + + /* Close everything down nicely */ + + clean_interpolation_global_vars (); + free_buffer (&inbuffer); + pnm_freepamrow (outrow); + free_option (&options); + pm_close (infp); + pm_close (outfp); + return 0; +} + + + + diff --git a/editor/pampop9.c b/editor/pampop9.c new file mode 100644 index 00000000..d6c61e4f --- /dev/null +++ b/editor/pampop9.c @@ -0,0 +1,108 @@ +/* + * + * (c) Robert Tinsley, 2003 (http://www.thepoacher.net/contact) + * + * Released under the GPL (http://www.fsf.org/licenses/gpl.txt) + * + * + * CHANGES + * + * v1.00 2003-02-28 Original version + * + * v1.10 2003-03-02 + * + changed to use pam_* routines rather than ppm_* + * + changed to use pm_* rather than fopen()/fclose() + * + renamed from ppmgrid to pampup9 + * + wrote a man-page (actually, html) + * + * Changes by Bryan Henderson for inclusion in the Netpbm package (fully + * exploiting Netpbm library). Renamed to Pampop9. March 2003. + * + */ + +#include <stdio.h> +#include <stdlib.h> /* atoi() */ + +#include "pam.h" + +static const char * const copyright = + "(c) Robert Tinsley 2003 (http://www.thepoacher.net/contact)"; + +static const char *usagestr = "pnmfile|- xtiles ytiles xdelta ydelta"; + +int main(int argc, char *argv[]) +{ + const char *filename = "-"; + int xtiles, ytiles, xdelta, ydelta; + FILE *fp; + + struct pam spam, dpam; + tuple **spix, *dpix; + int xtilesize, ytilesize, sx, sy, dx, dy, p; + + + + pnm_init(&argc, argv); + + if (argc-1 != 5) { + pm_error("Wrong number of arguments. Program requires 5 arguments; " + "you supplied %d. Usage: %s", argc-1, usagestr); + } + + filename = argv[1]; + xtiles = atoi(argv[2]); + ytiles = atoi(argv[3]); + xdelta = atoi(argv[4]); + ydelta = atoi(argv[5]); + + if (filename == NULL || *filename == '\0' + || xtiles <= 0 || ytiles <= 0 || xdelta < 0 || ydelta < 0) + pm_error("invalid argument"); + + /* read src pam */ + + fp = pm_openr(filename); + + spix = pnm_readpam(fp, &spam, PAM_STRUCT_SIZE(tuple_type)); + + pm_close(fp); + + /* init dst pam */ + + xtilesize = spam.width - (xtiles - 1) * xdelta; + ytilesize = spam.height - (ytiles - 1) * ydelta; + + if (xtilesize <= 0) + pm_error("xtilesize must be positive. You specified %d", xtilesize); + if (ytilesize <= 0) + pm_error("ytilesize must be positive. You specified %d", ytilesize); + + dpam = spam; + dpam.file = stdout; + dpam.width = xtiles * xtilesize; + dpam.height = ytiles * ytilesize; + + dpix = pnm_allocpamrow(&dpam); + + pnm_writepaminit(&dpam); + + /* generate dst pam */ + + for (dy = 0; dy < dpam.height; dy++) { + sy = ((int) (dy / ytilesize)) * ydelta + (dy % ytilesize); + for (dx = 0; dx < dpam.width; dx++) { + sx = ((int) (dx / xtilesize)) * xdelta + (dx % xtilesize); + for (p = 0; p < spam.depth; ++p) { + dpix[dx][p] = spix[sy][sx][p]; + } + } + pnm_writepamrow(&dpam, dpix); + } + + /* all done */ + + pnm_freepamarray(spix, &spam); + pnm_freepamrow(dpix); + + exit(EXIT_SUCCESS); +} diff --git a/editor/pamscale.c b/editor/pamscale.c new file mode 100644 index 00000000..229fce42 --- /dev/null +++ b/editor/pamscale.c @@ -0,0 +1,2149 @@ +/* pamscale.c - rescale (resample) a PNM image + + This program evolved out of Jef Poskanzer's program Pnmscale from + his Pbmplus package (which was derived from Poskanzer's 1989 + Ppmscale). The resampling logic was taken from Michael Reinelt's + program Pnmresample, somewhat recoded to follow Netpbm conventions. + Michael submitted that for inclusion in Netpbm in December 2003. + The frame of the program is by Bryan Henderson, and the old scaling + algorithm is based on that in Jef Poskanzer's Pnmscale, but + completely rewritten by Bryan Henderson ca. 2000. Plenty of other + people contributed code changes over the years. + + Copyright (C) 2003 by Michael Reinelt <reinelt@eunet.at> + + Copyright (C) 1989, 1991 by Jef Poskanzer. + + 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 _XOPEN_SOURCE /* get M_PI in math.h */ + +#include <stdlib.h> +#include <stdio.h> +#include <math.h> +#include <string.h> +#include <assert.h> + +#include "pam.h" +#include "shhopt.h" +#include "mallocvar.h" + + +/****************************/ +/****************************/ +/********* filters **********/ +/****************************/ +/****************************/ + +/* Most of the filters are FIR (finite impulse respone), but some +** (sinc, bessel) are IIR (infinite impulse respone). +** They should be windowed with hanning, hamming, blackman or +** kaiser window. +** For sinc and bessel the blackman window will be used per default. +*/ + +#define EPSILON 1e-7 + + +/* x^2 and x^3 helper functions */ +static __inline__ double +pow2 (double x) +{ + return x*x; +} + +static __inline__ double +pow3 (double x) +{ + return x*x*x; +} + + +/* box, pulse, Fourier window, */ +/* box function also know as rectangle function */ +/* 1st order (constant) b-spline */ + +#define radius_point (0.0) +#define radius_box (0.5) + +static double +filter_box (double x) +{ + if (x < 0.0) x = -x; + if (x <= 0.5) return 1.0; + return 0.0; +} + + +/* triangle, Bartlett window, */ +/* triangle function also known as lambda function */ +/* 2nd order (linear) b-spline */ + +#define radius_triangle (1.0) + +static double +filter_triangle (double x) +{ + if (x < 0.0) x = -x; + if (x < 1.0) return 1.0-x; + return 0.0; +} + + +/* 3rd order (quadratic) b-spline */ + +#define radius_quadratic (1.5) + +static double +filter_quadratic(double x) +{ + if (x < 0.0) x = -x; + if (x < 0.5) return 0.75-pow2(x); + if (x < 1.5) return 0.50*pow2(x-1.5); + return 0.0; +} + + +/* 4th order (cubic) b-spline */ + +#define radius_cubic (2.0) + +static double +filter_cubic(double x) +{ + if (x < 0.0) x = -x; + if (x < 1.0) return 0.5*pow3(x) - pow2(x) + 2.0/3.0; + if (x < 2.0) return pow3(2.0-x)/6.0; + return 0.0; +} + + +/* Catmull-Rom spline, Overhauser spline */ + +#define radius_catrom (2.0) + +static double +filter_catrom(double x) +{ + if (x < 0.0) x = -x; + if (x < 1.0) return 1.5*pow3(x) - 2.5*pow2(x) + 1.0; + if (x < 2.0) return -0.5*pow3(x) + 2.5*pow2(x) - 4.0*x + 2.0; + return 0.0; +} + + +/* Mitchell & Netravali's two-param cubic */ +/* see Mitchell&Netravali, */ +/* "Reconstruction Filters in Computer Graphics", SIGGRAPH 88 */ + +#define radius_mitchell (2.0) + +static double +filter_mitchell(double x) +{ + + double b = 1.0/3.0; + double c = 1.0/3.0; + + double p0 = ( 6.0 - 2.0*b ) / 6.0; + double p2 = (-18.0 + 12.0*b + 6.0*c) / 6.0; + double p3 = ( 12.0 - 9.0*b - 6.0*c) / 6.0; + double q0 = ( 8.0*b + 24.0*c) / 6.0; + double q1 = ( - 12.0*b - 48.0*c) / 6.0; + double q2 = ( 6.0*b + 30.0*c) / 6.0; + double q3 = ( - b - 6.0*c) / 6.0; + + if (x < 0.0) x = -x; + if (x < 1.0) return p3*pow3(x) + p2*pow2(x) + p0; + if (x < 2.0) return q3*pow3(x) + q2*pow2(x) + q1*x + q0; + return 0.0; +} + + +/* Gaussian filter (infinite) */ + +#define radius_gauss (1.25) + +static double +filter_gauss(double x) +{ + return exp(-2.0*pow2(x)) * sqrt(2.0/M_PI); +} + + +/* sinc, perfect lowpass filter (infinite) */ + +#define radius_sinc (4.0) + +static double +filter_sinc(double x) +{ + /* Note: Some people say sinc(x) is sin(x)/x. Others say it's + sin(PI*x)/(PI*x), a horizontal compression of the former which is + zero at integer values. We use the latter, whose Fourier transform + is a canonical rectangle function (edges at -1/2, +1/2, height 1). + */ + if (x == 0.0) return 1.0; + return sin(M_PI*x)/(M_PI*x); +} + + +/* Bessel (for circularly symm. 2-d filt, infinite) */ +/* See Pratt "Digital Image Processing" p. 97 for Bessel functions */ + +#define radius_bessel (3.2383) + +static double +filter_bessel(double x) +{ + if (x == 0.0) return M_PI/4.0; + return j1(M_PI*x)/(2.0*x); +} + + +/* Hanning window (infinite) */ + +#define radius_hanning (1.0) + +static double +filter_hanning(double x) +{ + return 0.5*cos(M_PI*x) + 0.5; +} + + +/* Hamming window (infinite) */ + +#define radius_hamming (1.0) + +static double +filter_hamming(double x) +{ + return 0.46*cos(M_PI*x) + 0.54; +} + + +/* Blackman window (infinite) */ + +#define radius_blackman (1.0) + +static double +filter_blackman(double x) +{ + return 0.5*cos(M_PI*x) + 0.08*cos(2.0*M_PI*x) + 0.42; +} + + +/* parameterized Kaiser window (infinite) */ +/* from Oppenheim & Schafer, Hamming */ + +#define radius_kaiser (1.0) + +/* modified zeroth order Bessel function of the first kind. */ +static double +bessel_i0(double x) +{ + + int i; + double sum, y, t; + + sum = 1.0; + y = pow2(x)/4.0; + t = y; + for (i=2; t>EPSILON; i++) { + sum += t; + t *= (double)y/pow2(i); + } + return sum; +} + +static double +filter_kaiser(double x) +{ + /* typically 4<a<9 */ + /* param a trades off main lobe width (sharpness) */ + /* for side lobe amplitude (ringing) */ + + double a = 6.5; + double i0a = 1.0/bessel_i0(a); + + return i0a*bessel_i0(a*sqrt(1.0-pow2(x))); +} + + +/* normal distribution (infinite) */ +/* Normal(x) = Gaussian(x/2)/2 */ + +#define radius_normal (1.0) + +static double +filter_normal(double x) +{ + return exp(-pow2(x)/2.0) / sqrt(2.0*M_PI); + return 0.0; +} + + +/* Hermite filter */ + +#define radius_hermite (1.0) + +static double +filter_hermite(double x) +{ + /* f(x) = 2|x|^3 - 3|x|^2 + 1, -1 <= x <= 1 */ + if (x < 0.0) x = -x; + if (x < 1.0) return 2.0*pow3(x) - 3.0*pow2(x) + 1.0; + return 0.0; +} + + +/* Lanczos filter */ + +#define radius_lanczos (3.0) + +static double +filter_lanczos(double x) +{ + if (x < 0.0) x = -x; + if (x < 3.0) return filter_sinc(x) * filter_sinc(x/3.0); + return(0.0); +} + + + +typedef struct { + const char *name; + double (*function)(double); + double radius; + /* This is how far from the Y axis (on either side) the + function has significant value. (You can use this to limit + how much of your domain you bother to compute the function + over). + */ + bool windowed; +} filter; + + +static filter Filters[] = { + { "point", filter_box, radius_point, FALSE }, + { "box", filter_box, radius_box, FALSE }, + { "triangle", filter_triangle, radius_triangle, FALSE }, + { "quadratic", filter_quadratic, radius_quadratic, FALSE }, + { "cubic", filter_cubic, radius_cubic, FALSE }, + { "catrom", filter_catrom, radius_catrom, FALSE }, + { "mitchell", filter_mitchell, radius_mitchell, FALSE }, + { "gauss", filter_gauss, radius_gauss, FALSE }, + { "sinc", filter_sinc, radius_sinc, TRUE }, + { "bessel", filter_bessel, radius_bessel, TRUE }, + { "hanning", filter_hanning, radius_hanning, FALSE }, + { "hamming", filter_hamming, radius_hamming, FALSE }, + { "blackman", filter_blackman, radius_blackman, FALSE }, + { "kaiser", filter_kaiser, radius_kaiser, FALSE }, + { "normal", filter_normal, radius_normal, FALSE }, + { "hermite", filter_hermite, radius_hermite, FALSE }, + { "lanczos", filter_lanczos, radius_lanczos, FALSE }, + { NULL }, +}; + + +typedef double (*basicFunction_t)(double); + + +/****************************/ +/****************************/ +/****** end of filters ******/ +/****************************/ +/****************************/ + + + +enum scaleType {SCALE_SEPARATE, SCALE_BOXFIT, SCALE_BOXFILL, SCALE_PIXELMAX}; + /* This is a way of specifying the output dimensions. + + SCALE_SEPARATE means specify the horizontal and vertical scaling + separately. One or both may be unspecified. + + SCALE_BOXFIT means specify height and width of a box, and the + image must be scaled, preserving aspect ratio, to the largest + size that will fit in the box. Some of the box may be empty. + + SCALE_BOXFILL means specify height and width of a box, and the + image must be scaled, preserving aspect ratio, to the smallest + size that completely fills the box. Some of the image may be + outside the box. + + SCALE_PIXELMAX means specify the maximum number of pixels the result + should have and scale preserving aspect ratio and maximizing image + size. + */ + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + * in a form easy for the program to use. + */ + const char * inputFileName; /* Filespec of input file */ + unsigned int nomix; + basicFunction_t filterFunction; /* NULL if not using resample method */ + basicFunction_t windowFunction; + /* Meaningful only when filterFunction != NULL */ + double filterRadius; + /* Meaningful only when filterFunction != NULL */ + enum scaleType scaleType; + /* 'xsize' and 'ysize' are numbers of pixels. Their meaning depends upon + 'scaleType'. for SCALE_BOXFIT and SCALE_BOXFILL, they are the box + dimensions. For SCALE_SEPARATE, they are the separate dimensions, or + zero to indicate unspecified. For SCALE_PIXELMAX, they are + meaningless. + */ + unsigned int xsize; + unsigned int ysize; + /* 'xscale' and 'yscale' are meaningful only for scaleType == + SCALE_SEPARATE and only where the corresponding xsize/ysize is + unspecified. 0.0 means unspecified. + */ + float xscale; + float yscale; + /* 'pixels' is meaningful only for scaleType == SCALE_PIXELMAX */ + unsigned int pixels; + unsigned int linear; + unsigned int verbose; +}; + + + +static void +lookupFilterByName(const char * const filtername, + filter * const filterP) { + + unsigned int i; + bool found; + + found = FALSE; /* initial assumption */ + + for (i=0; Filters[i].name; ++i) { + if (strcmp(filtername, Filters[i].name) == 0) { + *filterP = Filters[i]; + found = TRUE; + } + } + if (!found) { + unsigned int i; + char known_filters[1024]; + strcpy(known_filters, ""); + for (i = 0; Filters[i].name; ++i) { + const char * const name = Filters[i].name; + if (strlen(known_filters) + strlen(name) + 1 + 1 < + sizeof(known_filters)) { + strcat(known_filters, name); + strcat(known_filters, " "); + } + } + pm_error("No such filter as '%s'. Known filter names are: %s", + filtername, known_filters); + } +} + + + +static void +processFilterOptions(unsigned int const filterSpec, + const char filterOpt[], + unsigned int const windowSpec, + const char windowOpt[], + struct cmdlineInfo * const cmdlineP) { + + if (filterSpec) { + filter baseFilter; + lookupFilterByName(filterOpt, &baseFilter); + cmdlineP->filterFunction = baseFilter.function; + cmdlineP->filterRadius = baseFilter.radius; + + if (windowSpec) { + filter windowFilter; + lookupFilterByName(windowOpt, &windowFilter); + + if (cmdlineP->windowFunction == filter_box) + cmdlineP->windowFunction = NULL; + else + cmdlineP->windowFunction = windowFilter.function; + } else { + /* Default for most filters is no window. Those that _require_ + a window function get Blackman. + */ + if (baseFilter.windowed) + cmdlineP->windowFunction = filter_blackman; + else + cmdlineP->windowFunction = NULL; + } + } else + cmdlineP->filterFunction = NULL; +} + + + +static void +parseXyParms(int const argc, + char ** const argv, + struct cmdlineInfo * const cmdlineP) { + + /* parameters are box width (columns), box height (rows), and + optional filespec + */ + if (argc-1 < 2) + pm_error("You must supply at least two parameters with " + "-xyfit/xyfill/xysize: " + "x and y dimensions of the bounding box."); + else if (argc-1 > 3) + pm_error("Too many arguments. With -xyfit/xyfill/xysize, " + "you need 2 or 3 arguments."); + else { + char * endptr; + cmdlineP->xsize = strtol(argv[1], &endptr, 10); + if (strlen(argv[1]) > 0 && *endptr != '\0') + pm_error("horizontal size argument not an integer: '%s'", + argv[1]); + if (cmdlineP->xsize <= 0) + pm_error("horizontal size argument is not positive: %d", + cmdlineP->xsize); + + cmdlineP->ysize = strtol(argv[2], &endptr, 10); + if (strlen(argv[2]) > 0 && *endptr != '\0') + pm_error("vertical size argument not an integer: '%s'", + argv[2]); + if (cmdlineP->ysize <= 0) + pm_error("vertical size argument is not positive: %d", + cmdlineP->ysize); + + if (argc-1 < 3) + cmdlineP->inputFileName = "-"; + else + cmdlineP->inputFileName = argv[3]; + } +} + + + +static void +parseScaleParms(int const argc, + char ** const argv, + struct cmdlineInfo * const cmdlineP) { + + /* parameters are scale factor and optional filespec */ + if (argc-1 < 1) + pm_error("With no dimension options, you must supply at least " + "one parameter: the scale factor."); + else { + cmdlineP->xscale = cmdlineP->yscale = atof(argv[1]); + + if (cmdlineP->xscale == 0.0) + pm_error("The scale parameter %s is not a positive number.", + argv[1]); + else { + if (argc-1 < 2) + cmdlineP->inputFileName = "-"; + else + cmdlineP->inputFileName = argv[2]; + } + } +} + + + +static void +parseFilespecOnlyParms(int const argc, + char ** const argv, + struct cmdlineInfo * const cmdlineP) { + + /* Only parameter allowed is optional filespec */ + if (argc-1 < 1) + cmdlineP->inputFileName = "-"; + else + cmdlineP->inputFileName = argv[1]; +} + + +static void +parseCommandLine(int argc, + 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 option_def_index; + unsigned int xyfit, xyfill; + int xsize, ysize, pixels; + int reduce; + float xscale, yscale; + const char *filterOpt, *window; + unsigned int filterSpec, windowSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "xsize", OPT_UINT, &xsize, NULL, 0); + OPTENT3(0, "width", OPT_UINT, &xsize, NULL, 0); + OPTENT3(0, "ysize", OPT_UINT, &ysize, NULL, 0); + OPTENT3(0, "height", OPT_UINT, &ysize, NULL, 0); + OPTENT3(0, "xscale", OPT_FLOAT, &xscale, NULL, 0); + OPTENT3(0, "yscale", OPT_FLOAT, &yscale, NULL, 0); + OPTENT3(0, "pixels", OPT_UINT, &pixels, NULL, 0); + OPTENT3(0, "reduce", OPT_UINT, &reduce, NULL, 0); + OPTENT3(0, "xysize", OPT_FLAG, NULL, &xyfit, 0); + OPTENT3(0, "xyfit", OPT_FLAG, NULL, &xyfit, 0); + OPTENT3(0, "xyfill", OPT_FLAG, NULL, &xyfill, 0); + OPTENT3(0, "verbose", OPT_FLAG, NULL, &cmdlineP->verbose, 0); + OPTENT3(0, "filter", OPT_STRING, &filterOpt, &filterSpec, 0); + OPTENT3(0, "window", OPT_STRING, &window, &windowSpec, 0); + OPTENT3(0, "nomix", OPT_FLAG, NULL, &cmdlineP->nomix, 0); + OPTENT3(0, "linear", OPT_FLAG, NULL, &cmdlineP->linear, 0); + + /* Set the defaults. -1 = unspecified */ + + /* (Now that we're using ParseOptions3, we don't have to do this -1 + * nonsense, but we don't want to risk screwing these complex + * option compatibilities up, so we'll convert that later. + */ + xsize = -1; + ysize = -1; + xscale = -1.0; + yscale = -1.0; + pixels = -1; + reduce = -1; + + 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 */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0 ); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (cmdlineP->nomix && filterSpec) + pm_error("You cannot specify both -nomix and -filter."); + + processFilterOptions(filterSpec, filterOpt, windowSpec, window, + cmdlineP); + + if (xsize == 0) + pm_error("-xsize/width must be greater than zero."); + if (ysize == 0) + pm_error("-ysize/height must be greater than zero."); + if (xscale != -1.0 && xscale <= 0.0) + pm_error("-xscale must be greater than zero."); + if (yscale != -1.0 && yscale <= 0.0) + pm_error("-yscale must be greater than zero."); + if (reduce <= 0 && reduce != -1) + pm_error("-reduce must be greater than zero."); + + if (xsize != -1 && xscale != -1) + pm_error("Cannot specify both -xsize/width and -xscale."); + if (ysize != -1 && yscale != -1) + pm_error("Cannot specify both -ysize/height and -yscale."); + + if ((xyfit || xyfill) && + (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 || + reduce != -1 || pixels != -1) ) + pm_error("Cannot specify -xyfit/xyfill/xysize with other " + "dimension options."); + if (xyfit && xyfill) + pm_error("Cannot specify both -xyfit and -xyfill"); + if (pixels != -1 && + (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 || + reduce != -1) ) + pm_error("Cannot specify -pixels with other dimension options."); + if (reduce != -1 && + (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1) ) + pm_error("Cannot specify -reduce with other dimension options."); + + if (pixels == 0) + pm_error("-pixels must be greater than zero"); + + /* Get the program parameters */ + + if (xyfit || xyfill) { + cmdlineP->scaleType = xyfit ? SCALE_BOXFIT : SCALE_BOXFILL; + parseXyParms(argc, argv, cmdlineP); + } else if (reduce != -1) { + cmdlineP->scaleType = SCALE_SEPARATE; + parseFilespecOnlyParms(argc, argv, cmdlineP); + cmdlineP->xscale = cmdlineP->yscale = + ((double) 1.0) / ((double) reduce); + pm_message("reducing by %d gives scale factor of %f.", + reduce, cmdlineP->xscale); + } else if (pixels != -1) { + cmdlineP->scaleType = SCALE_PIXELMAX; + parseFilespecOnlyParms(argc, argv, cmdlineP); + cmdlineP->pixels = pixels; + } else if (xsize == -1 && xscale == -1 && ysize == -1 && yscale == -1 + && pixels == -1 && reduce == -1) { + cmdlineP->scaleType = SCALE_SEPARATE; + parseScaleParms(argc, argv, cmdlineP); + cmdlineP->xsize = cmdlineP->ysize = 0; + } else { + cmdlineP->scaleType = SCALE_SEPARATE; + parseFilespecOnlyParms(argc, argv, cmdlineP); + cmdlineP->xsize = xsize == -1 ? 0 : xsize; + cmdlineP->ysize = ysize == -1 ? 0 : ysize; + cmdlineP->xscale = xscale == -1.0 ? 0.0 : xscale; + cmdlineP->yscale = yscale == -1.0 ? 0.0 : yscale; + } +} + + + +static void +computeOutputDimensions(struct cmdlineInfo const cmdline, + int const rows, + int const cols, + int * const newrowsP, + int * const newcolsP) { + + switch(cmdline.scaleType) { + case SCALE_PIXELMAX: { + if (rows * cols <= cmdline.pixels) { + *newrowsP = rows; + *newcolsP = cols; + } else { + const double scale = + sqrt( (float) cmdline.pixels / ((float) cols * (float) rows)); + *newrowsP = rows * scale; + *newcolsP = cols * scale; + } + } break; + case SCALE_BOXFIT: + case SCALE_BOXFILL: { + double const aspect_ratio = (float) cols / (float) rows; + double const box_aspect_ratio = + (float) cmdline.xsize / (float) cmdline.ysize; + + if ((box_aspect_ratio > aspect_ratio && + cmdline.scaleType == SCALE_BOXFIT) || + (box_aspect_ratio < aspect_ratio && + cmdline.scaleType == SCALE_BOXFILL)) { + *newrowsP = cmdline.ysize; + *newcolsP = *newrowsP * aspect_ratio + 0.5; + } else { + *newcolsP = cmdline.xsize; + *newrowsP = *newcolsP / aspect_ratio + 0.5; + } + } break; + case SCALE_SEPARATE: { + if (cmdline.xsize) + *newcolsP = cmdline.xsize; + else if (cmdline.xscale) + *newcolsP = cmdline.xscale * cols + .5; + else if (cmdline.ysize) + *newcolsP = cols * ((float) cmdline.ysize/rows) +.5; + else + *newcolsP = cols; + + if (cmdline.ysize) + *newrowsP = cmdline.ysize; + else if (cmdline.yscale) + *newrowsP = cmdline.yscale * rows +.5; + else if (cmdline.xsize) + *newrowsP = rows * ((float) cmdline.xsize/cols) +.5; + else + *newrowsP = rows; + } + } + + /* If the calculations above yielded (due to rounding) a zero + * dimension, we fudge it up to 1. We do this rather than considering + * it a specification error (and dying) because it's friendlier to + * automated processes that work on arbitrary input. It saves them + * having to check their numbers to avoid catastrophe. + */ + + if (*newcolsP < 1) *newcolsP = 1; + if (*newrowsP < 1) *newrowsP = 1; +} + + + + +/****************************/ +/****************************/ +/******* resampling *********/ +/****************************/ +/****************************/ + +/* The resample code was inspired by Paul Heckbert's zoom program. +** http://www.cs.cmu.edu/~ph/zoom +*/ + +struct filterFunction { +/*---------------------------------------------------------------------------- + A function to convolve with the samples. +-----------------------------------------------------------------------------*/ + basicFunction_t basicFunction; + /* The basic shape of the function. Its horizontal scale is + designed to filter out frequencies greater than 1. + */ + basicFunction_t windowFunction; + /* A function to multiply by basicFunction(). NULL if none. */ + double windowScaler; + /* Factor by which to compress windowFunction() horizontally */ + double horizontalScaler; + /* Factor by which to compress basicFunction() * + windowFunction horizontally. Note that compressing + horizontally in the sample domain is equivalent to + expanding horizontally (and shrinking vertically) in the + frequency domain. I.e. values less than unity have the + effect of chopping out high frequencies. + */ + double radius; + /* A final filter. filterFunction(x) is zero for |x| > radius + regardless of what the rest of the members say. + + Implementation note: This is important because windowFunction(), + out of laziness, doesn't do the whole job of windowing. It is + not zero beyond the cutoff points as it should be. If not for + that, radius would only be a hint to describe what the other + members already do, so the convolver knows where to stop. + */ +}; + +typedef struct { + /* A term of the linear combination of input rows that makes up an + output row. I.e. an input row and its weight. + + Alternatively, the analogous thing for a column. + */ + int position; /* Row/column number in the input image */ + double weight; /* Weight to be given to that row/col. In [0, 1]. */ +} WEIGHT; + +typedef struct { + /* A description of the linear combination of input rows that + generates a particular output row. An output row is a weighted + average of some input rows. E.g. Row 2 of the output might be + composed of 50% of Row 2 of the input and 50% of Row 3 of the + input. + + Alternatively, the analogous thing for columns. + */ + unsigned int nWeight; + /* Number of elements in 'Weight'. They're consecutive, starting + at index 0. + */ + unsigned int allocWeight; + /* Number of allocated frames in 'Weight' */ + WEIGHT *Weight; + /* The terms of the linear combination. Has 'nWeight' elements. + The coefficients (weights) of each add up to unity. + */ +} WLIST; + +typedef struct { + /* This identifies a row of the input image. */ + int rowNumber; + /* The row number in the input image of the row. + -1 means no row. + */ + tuple *tuplerow; + /* The tuples of the row. + If rowNumber = -1, these are arbitrary, but allocated, tuples. + */ +} SCANLINE; + +typedef struct { + /* A vertical window of a raster */ + int width; /* Width of the window, in columns */ + int height; /* Height of the window, in rows */ + SCANLINE *line; + /* An array of 'height' elements, malloced. + This identifies the lines of the input image that compose the + window. The index order is NOT the order of the rows in the + image. E.g. line[0] isn't always the topmost row of the window. + Rather, the rows are arranged in a cycle and you have to know + indpendently where the topmost one is. E.g. the rows of a 5 + line window with topmost row at index 3 might be: + + line[0] = Row 24 + line[1] = Row 25 + line[2] = Row 26 + line[3] = Row 22 + line[4] = Row 23 + */ +} SCAN; + + + +static int +appendWeight(WLIST * const WList, + int const index, + double const weight) { +/*---------------------------------------------------------------------------- + Add a weighting of 'weight' for index 'index' to the weight list + 'WList'. +-----------------------------------------------------------------------------*/ + if (weight == 0.0) { + /* A weight of 0 in the list is redundant, so we don't add it. + A weight entry says "Add W fraction of the pixel at index I," + so where W is 0, it's the same as not having the entry at all. + */ + } else { + unsigned int const n = WList->nWeight; + + assert(WList->allocWeight >= n+1); + + WList->Weight[n].position = index; + WList->Weight[n].weight = weight; + ++WList->nWeight; + } + return 0; +} + + + +static sample +floatToSample(double const value, + sample const maxval) { + + /* Take care here, the conversion of any floating point value <= + -1.0 to an unsigned type is _undefined_. See ISO 9899:1999 + section 6.3.1.4. Not only is it undefined it also does the + wrong thing in actual practice, EG on Darwin PowerPC (my iBook + running OS X) negative values clamp to maxval. We get negative + values because some of the filters (EG catrom) have negative + weights. + */ + + return MIN(maxval, (sample)(MAX(0.0, (value + 0.5)))); +} + + + +static void +initWeightList(WLIST * const weightListP, + unsigned int const maxWeights) { + + weightListP->nWeight = 0; + weightListP->allocWeight = maxWeights; + MALLOCARRAY(weightListP->Weight, maxWeights); + if (weightListP->Weight == NULL) + pm_error("Out of memory allocating a %u-element weight list.", + maxWeights); +} + + + +static void +createWeightList(unsigned int const targetPos, + unsigned int const sourceSize, + double const scale, + struct filterFunction filter, + WLIST * const weightListP) { +/*---------------------------------------------------------------------------- + Create a weight list for computing target pixel number 'targetPos' from + a set of source pixels. These pixels are a line of pixels either + horizontally or vertically. The weight list is a list of weights to give + each source pixel in the set. + + The source pixel set is a window of source pixels centered on some + point. The weights are defined by the function 'filter' of + the position within the window, and normalized to add up to 1.0. + Technically, the window is infinite, but we know that the filter + function is zero beyond a certain distance from the center of the + window. + + For example, assume 'targetPos' is 5. That means we're computing weights + for either Column 5 or Row 5 of the target image. Assume it's Column 5. + Assume 'radius' is 1. That means a window of two pixels' worth of a + source row determines the color of the Column 5 pixel of a target + row. Assume 'filter' is a triangle function -- 1 at 0, sloping + down to 0 at -1 and 1. + + Now assume that the scale factor is 2 -- the target image will be + twice the size of the source image. That means the two-pixel-wide + window of the source row that affects Column 5 of the target row + (centered at target position 5.5) goes from position 1.75 to + 3.75, centered at 2.75. That means the window covers 1/4 of + Column 1, all of Column 2, and 3/4 of Column 3 of the source row. + + We want to calculate 3 weights, one to be applied to each source pixel + in computing the target pixel. Ideally, we would compute the average + height of the filter function over each source pixel region. But + that's too hard. So we approximate by assuming that the filter function + is constant within each region, at the value the function has at the + _center_ of the region. + + So for the Column 1 region, which goes from 1.75 to 2.00, centered + -.875 from the center of the window, we assume a constant function + value of triangle(-.875), which equals .125. For the 2.00-3.00 + region, we get triangle(-.25) = .75. For the 3.00-3.75 region, we + get triangle(.125) = .875. So the weights for the 3 regions, which + we get by multiplying this constant function value by the width of + the region and normalizing so they add up to 1 are: + + Source Column 1: .125*.25 / 1.4375 = .022 + Source Column 2: .75*1.00 / 1.4375 = .521 + Source Column 3: .875*.75 / 1.4375 = .457 + + These are the weights we return. Thus, if we assume that the source + pixel 1 has value 10, source pixel 2 has value 20, and source pixel 3 + has value 30, Caller would compute target pixel 5's value as + + 10*.022 + 20*.521 + 30*.457 = 24 + +-----------------------------------------------------------------------------*/ + /* 'windowCenter', is the continous position within the source of + the center of the window that will influence target pixel + 'targetPos'. 'left' and 'right' are the edges of the window. + 'leftPixel' and 'rightPixel' are the pixel positions of the + pixels at the edges of that window. Note that if we're + doing vertical weights, "left" and "right" mean top and + bottom. + */ + double const windowCenter = ((double)targetPos + 0.5) / scale; + double left = MAX(0.0, windowCenter - filter.radius - EPSILON); + unsigned int const leftPixel = floor(left); + double right = MIN((double)sourceSize - EPSILON, + windowCenter + filter.radius + EPSILON); + unsigned int const rightPixel = floor(right); + + double norm; + unsigned int j; + + initWeightList(weightListP, rightPixel - leftPixel + 1); + + /* calculate weights */ + norm = 0.0; /* initial value */ + + for (j = leftPixel; j <= rightPixel; ++j) { + /* Calculate the weight that source pixel 'j' will have in the + value of target pixel 'targetPos'. + */ + double const regionLeft = MAX(left, (double)j); + double const regionRight = MIN(right, (double)(j + 1)); + double const regionWidth = regionRight - regionLeft; + double const regionCenter = (regionRight + regionLeft) / 2; + double const dist = regionCenter - windowCenter; + double weight; + + weight = filter.basicFunction(filter.horizontalScaler * dist); + if (filter.windowFunction) + weight *= filter.windowFunction( + filter.horizontalScaler * filter.windowScaler * dist); + + assert(regionWidth <= 1.0); + weight *= regionWidth; + norm += weight; + appendWeight(weightListP, j, weight); + } + + if (norm == 0.0) + pm_error("INTERNAL ERROR: No source pixels contribute to target " + "pixel %u", targetPos); + + /* normalize the weights so they add up to 1.0 */ + if (norm != 1.0) { + unsigned int n; + for (n = 0; n < weightListP->nWeight; ++n) { + weightListP->Weight[n].weight /= norm; + } + } +} + + + +static void +createWeightListSet(unsigned int const sourceSize, + unsigned int const targetSize, + struct filterFunction const filterFunction, + WLIST ** const weightListSetP) { +/*---------------------------------------------------------------------------- + Create the set of weight lists that will effect the resample. + + This is where the actual work of resampling gets done. + + The weight list set is a bunch of factors one can multiply by the + pixels in a region to effect a resampling. Multiplying by these + factors effects all of the following transformations on the + original pixels: + + 1) Filter out any frequencies that are artifacts of the + original sampling. We assume a perfect sampling was done, + which means the original continuous dataset had a maximum + frequency of 1/2 of the original sample rate and anything + above that is an artifact of the sampling. So we filter out + anything above 1/2 of the original sample rate (sample rate + == pixel resolution). + + 2) Filter out any frequencies that are too high to be captured + by the new sampling -- i.e. frequencies above 1/2 the new + sample rate. This is the information we must lose due to low + sample rate. + + 3) Sample the result at the new sample rate. + + We do all three of these steps in a single convolution of the + original pixels. Steps (1) and (2) can be combined into a + single frequency domain rectangle function. A frequency domain + rectangle function is a pixel domain sinc function, which is + what we assume 'filterFunction' is. We get Step 3 by computing + the convolution only at the new sample points. + + I don't know what any of this means when 'filterFunction' is + not sinc. Maybe it just means some approximation or additional + filtering steps are happening. +-----------------------------------------------------------------------------*/ + double const scale = (double)targetSize / sourceSize; + WLIST *weightListSet; /* malloc'ed */ + unsigned int targetPos; + + MALLOCARRAY_NOFAIL(weightListSet, targetSize); + + for (targetPos = 0; targetPos < targetSize; ++targetPos) + createWeightList(targetPos, sourceSize, scale, filterFunction, + &weightListSet[targetPos]); + + *weightListSetP = weightListSet; +} + + + +static struct filterFunction +makeFilterFunction(double const scale, + basicFunction_t basicFunction, + double const basicRadius, + basicFunction_t windowFunction) { +/*---------------------------------------------------------------------------- + Create a function to convolve with the samples (so it isn't actually + a filter function, but the Fourier transform of a filter function. + A filter function is something you multiply by in the frequency domain) + to create a function from which one can resample. + + Convolving with this function will achieve two goals: + + 1) filter out high frequencies that are artifacts of the original + sampling (i.e. the turning of a continuous function into a staircase + function); + 2) filter out frequencies higher than half the resample rate, so that + the resample will be a perfect sampling of it, and not have aliasing. + + + To make the calculation even more efficient, we take advantage + of the fact that the weight list doesn't depend on the + particular old and new sample rates at all except -- all that's + important is their ratio (which is 'scale'). So we assume the + original sample rate is 1 and the new sample rate is 'scale'. + +-----------------------------------------------------------------------------*/ + double const freqLimit = MIN(1.0, scale); + /* We're going to cut out any frequencies above this, to accomplish + Steps (1) and (2) above. + */ + + struct filterFunction retval; + + retval.basicFunction = basicFunction; + retval.windowFunction = windowFunction; + + retval.horizontalScaler = freqLimit; + + /* Our 'windowFunction' argument is a function normalized to the + domain (-1, 1). We need to scale it horizontally to fit the + basic filter function. We assume the radius of the filter + function is the area to which the window should fit (i.e. zero + beyond the radius, nonzero inside the radius). But that's + really a misuse of radius, because radius is supposed to be + just the distance beyond which we can assume for convenience + that the filter function is zero, possibly giving up some + precision. + + But note that 'windowFunction' isn't zero outside (-1, 1), even + though the actual window function is supposed to be. Hence, + scaling the window function exactly to the radius stops our + calculations from noticing the wrong values outside (-1, 1) -- + we'll never use them. + */ + retval.windowScaler = 1/basicRadius; + + retval.radius = basicRadius / retval.horizontalScaler; + + return retval; +} + + + +static void +destroyWeightListSet(WLIST * const weightListSet, + unsigned int const size) { + + unsigned int i; + + for (i = 0; i < size; ++i) + free(weightListSet[i].Weight); + + free(weightListSet); +} + + + +static void +createScanBuf(struct pam * const pamP, + double const maxRowWeights, + bool const verbose, + SCAN * const scanbufP) { + + SCAN scanbuf; + unsigned int lineNumber; + + scanbuf.width = pamP->width; + scanbuf.height = maxRowWeights; + MALLOCARRAY_NOFAIL(scanbuf.line, scanbuf.height); + + for (lineNumber = 0; lineNumber < scanbuf.height; ++lineNumber) { + scanbuf.line[lineNumber].rowNumber = -1; + scanbuf.line[lineNumber].tuplerow = pnm_allocpamrow(pamP); + } + + if (verbose) + pm_message("scanline buffer: %d lines of %d pixels", + scanbuf.height, scanbuf.width); + + *scanbufP = scanbuf; +} + + + +static void +destroyScanbuf(SCAN const scanbuf) { + + unsigned int lineNumber; + + for (lineNumber = 0; lineNumber < scanbuf.height; ++lineNumber) + pnm_freepamrow(scanbuf.line[lineNumber].tuplerow); + + free(scanbuf.line); +} + + + +static void +resampleDimensionMessage(struct pam * const inpamP, + struct pam * const outpamP) { + + pm_message ("resampling from %d*%d to %d*%d (%f, %f)", + inpamP->width, inpamP->height, + outpamP->width, outpamP->height, + (double)outpamP->width/inpamP->width, + (double)outpamP->height/inpamP->height); +} + + + +static void +addInPixel(const struct pam * const pamP, + tuple const tuple, + float const weight, + bool const haveOpacity, + unsigned int const opacityPlane, + double * const accum) { +/*---------------------------------------------------------------------------- + Add into *accum the values from the tuple 'tuple', weighted by + 'weight'. + + Iff 'haveOpacity', Plane 'opacityPlane' of the tuple is an opacity + (alpha, transparency) plane. +-----------------------------------------------------------------------------*/ + unsigned int plane; + + for (plane = 0; plane < pamP->depth; ++plane) { + sample adjustedForOpacity; + + if (haveOpacity && plane != opacityPlane) { + float const opacity = (float)tuple[opacityPlane]/pamP->maxval; + float const unadjusted = (float)tuple[plane]/pamP->maxval; + + adjustedForOpacity = + floatToSample(unadjusted * opacity, pamP->maxval); + } else + adjustedForOpacity = tuple[plane]; + + accum[plane] += (double)adjustedForOpacity * weight; + } +} + + + +static void +generateOutputTuple(const struct pam * const pamP, + double const accum[], + bool const haveOpacity, + unsigned int const opacityPlane, + tuple * const tupleP) { +/*---------------------------------------------------------------------------- + Convert the values accum[] accumulated for a pixel by + outputOneResampledRow() to a bona fide PAM tuple as *tupleP, + as described by *pamP. +-----------------------------------------------------------------------------*/ + unsigned int plane; + + for (plane = 0; plane < pamP->depth; ++plane) { + float opacityAdjustedSample; + + if (haveOpacity && plane != opacityPlane) { + if (accum[opacityPlane] < EPSILON) { + assert(accum[plane] < EPSILON); + opacityAdjustedSample = 0.0; + } else + opacityAdjustedSample = accum[plane] / accum[opacityPlane]; + } else + opacityAdjustedSample = accum[plane]; + + (*tupleP)[plane] = floatToSample(opacityAdjustedSample, pamP->maxval); + } +} + + + +static void +outputOneResampledRow(const struct pam * const outpamP, + SCAN const scanbuf, + WLIST const YW, + const WLIST * const XWeight, + tuple * const line, + double * const accum) { +/*---------------------------------------------------------------------------- + From the data in 'scanbuf' and weights in 'YW' and 'XWeight', + generate one output row for the image described by *outpamP and + output it. + + An output pixel is a weighted average of the pixels in a certain + rectangle of the input. 'YW' and 'XWeight' describe those weights + for each column of the row we are to output. + + 'line' and 'accum' are just working space that Caller provides us + with to save us the time of allocating it. 'line' is at least big + enough to hold an output row; 'weight' is at least outpamP->depth + big. +-----------------------------------------------------------------------------*/ + unsigned int col; + + bool haveOpacity; /* There is an opacity plane */ + unsigned int opacityPlane; /* Plane number of opacity plane, if any */ + + pnm_getopacity(outpamP, &haveOpacity, &opacityPlane); + + for (col = 0; col < outpamP->width; ++col) { + WLIST const XW = XWeight[col]; + + unsigned int i; + { + unsigned int plane; + for (plane = 0; plane < outpamP->depth; ++plane) + accum[plane] = 0.0; + } + + for (i = 0; i < YW.nWeight; ++i) { + int const yp = YW.Weight[i].position; + float const yw = YW.Weight[i].weight; + int const slot = yp % scanbuf.height; + + unsigned int j; + + for (j = 0; j < XW.nWeight; ++j) { + int const xp = XW.Weight[j].position; + tuple const tuple = scanbuf.line[slot].tuplerow[xp]; + + addInPixel(outpamP, tuple, yw * XW.Weight[j].weight, + haveOpacity, opacityPlane, + accum); + } + } + generateOutputTuple(outpamP, accum, haveOpacity, opacityPlane, + &line[col]); + } + pnm_writepamrow(outpamP, line); +} + + + +static bool +scanbufContainsTheRows(SCAN const scanbuf, + WLIST const rowWeights) { +/*---------------------------------------------------------------------------- + Return TRUE iff scanbuf 'scanbuf' contains every row mentioned in + 'rowWeights'. + + It might contain additional rows besides. +-----------------------------------------------------------------------------*/ + bool missingRow; + unsigned int i; + + for (i = 0, missingRow = FALSE; + i < rowWeights.nWeight && !missingRow; + ++i) { + unsigned int const inputRow = rowWeights.Weight[i].position; + unsigned int const slot = inputRow % scanbuf.height; + /* This is the number of the slot in the scanbuf that would + have the input row in question if the scanbuf has the + row at all. + */ + if (scanbuf.line[slot].rowNumber != inputRow) { + /* Nope, this slot has some other row or no row at all. + So the row we're looking for isn't in the scanbuf. + */ + missingRow = TRUE; + } + } + return !missingRow; +} + + + +static void +createWeightLists(struct pam * const inpamP, + struct pam * const outpamP, + basicFunction_t const filterFunction, + double const filterRadius, + basicFunction_t const windowFunction, + WLIST ** const horizWeightP, + WLIST ** const vertWeightP, + unsigned int * const maxRowWeightsP) { +/*---------------------------------------------------------------------------- + This is the function that actually does the resampling. Note that it + does it without ever looking at the source or target pixels! It produces + a simple set of numbers that Caller can blindly apply to the source + pixels to get target pixels. +-----------------------------------------------------------------------------*/ + struct filterFunction horizFilter, vertFilter; + + horizFilter = makeFilterFunction( + (double)outpamP->width/inpamP->width, + filterFunction, filterRadius, windowFunction); + + createWeightListSet(inpamP->width, outpamP->width, horizFilter, + horizWeightP); + + vertFilter = makeFilterFunction( + (double)outpamP->height/inpamP->height, + filterFunction, filterRadius, windowFunction); + + createWeightListSet(inpamP->height, outpamP->height, vertFilter, + vertWeightP); + + *maxRowWeightsP = ceil(2.0*(vertFilter.radius+EPSILON) + 1 + EPSILON); +} + + + +static void +resample(struct pam * const inpamP, + struct pam * const outpamP, + basicFunction_t const filterFunction, + double const filterRadius, + basicFunction_t const windowFunction, + bool const verbose, + bool const linear) { +/*--------------------------------------------------------------------------- + Resample the image in the input file, described by *inpamP, + so as to create the image in the output file, described by *outpamP. + + Input and output differ by height, width, and maxval only. + + Use the resampling filter function 'filterFunction', applied over + radius 'filterRadius'. + + The input file is positioned past the header, to the beginning of the + raster. The output file is too. +---------------------------------------------------------------------------*/ + int inputRow, outputRow; + WLIST * horizWeight; + WLIST * vertWeight; + SCAN scanbuf; + unsigned int maxRowWeights; + + tuple * line; + /* This is just work space for outputOneSampleRow() */ + double * weight; + /* This is just work space for outputOneSampleRow() */ + + if (linear) + pm_error("You cannot use the resampling scaling method on " + "linear input."); + + createWeightLists(inpamP, outpamP, filterFunction, filterRadius, + windowFunction, &horizWeight, &vertWeight, + &maxRowWeights); + + createScanBuf(inpamP, maxRowWeights, verbose, &scanbuf); + + if (verbose) + resampleDimensionMessage(inpamP, outpamP); + + line = pnm_allocpamrow(outpamP); + MALLOCARRAY_NOFAIL(weight, outpamP->depth); + + outputRow = 0; + for (inputRow = 0; inputRow < inpamP->height; ++inputRow) { + bool needMoreInput; + /* We've output as much as we can using the rows that are in + the scanbuf; it's time to move the window. Or fill it in + the first place. + */ + unsigned int scanbufSlot; + + /* Read source row; add it to the scanbuf */ + scanbufSlot = inputRow % scanbuf.height; + scanbuf.line[scanbufSlot].rowNumber = inputRow; + pnm_readpamrow(inpamP, scanbuf.line[scanbufSlot].tuplerow); + + /* Output all the rows we can make out of the current contents of + the scanbuf. Might be none. + */ + needMoreInput = FALSE; /* initial assumption */ + while (outputRow < outpamP->height && !needMoreInput) { + WLIST const rowWeights = vertWeight[outputRow]; + /* The description of what makes up our current output row; + i.e. what fractions of which input rows combine to create + this output row. + */ + assert(rowWeights.nWeight <= scanbuf.height); + + if (scanbufContainsTheRows(scanbuf, rowWeights)) { + outputOneResampledRow(outpamP, scanbuf, rowWeights, + horizWeight, line, weight); + ++outputRow; + } else + needMoreInput = TRUE; + } + } + + if (outputRow != outpamP->height) + pm_error("INTERNAL ERROR: assembled only %u of the required %u " + "output rows.", outputRow, outpamP->height); + + pnm_freepamrow(line); + destroyScanbuf(scanbuf); + destroyWeightListSet(horizWeight, outpamP->width); + destroyWeightListSet(vertWeight, outpamP->height); +} + + +/****************************/ +/****************************/ +/**** end of resampling *****/ +/****************************/ +/****************************/ + + + +static void +zeroNewRow(struct pam * const pamP, + tuplen * const tuplenrow) { + + unsigned int col; + + for (col = 0; col < pamP->width; ++col) { + unsigned int plane; + + for (plane = 0; plane < pamP->depth; ++plane) + tuplenrow[col][plane] = 0.0; + } +} + + + +static void +accumOutputCol(struct pam * const pamP, + tuplen const intuplen, + float const fraction, + tuplen const accumulator) { +/*---------------------------------------------------------------------------- + Add fraction 'fraction' of the pixel indicated by 'intuplen' to the + pixel accumulator 'accumulator'. + + 'intuplen' and 'accumulator' are not a standard libnetpbm tuplen. + It is proportional to light intensity. The foreground color + component samples are proportional to light intensity, and have + opacity factored in. +-----------------------------------------------------------------------------*/ + unsigned int plane; + + for (plane = 0; plane < pamP->depth; ++plane) + accumulator[plane] += fraction * intuplen[plane]; +} + + + +static void +horizontalScale(tuplen * const inputtuplenrow, + tuplen * const newtuplenrow, + struct pam * const inpamP, + struct pam * const outpamP, + float const xscale, + float * const stretchP) { +/*---------------------------------------------------------------------------- + Take the input row 'inputtuplenrow', decribed by *inpamP, and scale + it by a factor of 'xscale', to create the output row 'newtuplenrow', + described by *outpamP. + + Due to arithmetic imprecision, we may have to stretch slightly the + contents of the last pixel of the output row to make a full pixel. + Return as *stretchP the fraction of a pixel by which we had to + stretch in this way. + + Assume maxval and depth of input and output are the same. +-----------------------------------------------------------------------------*/ + float fraccoltofill, fraccolleft; + unsigned int col; + unsigned int newcol; + + newcol = 0; + fraccoltofill = 1.0; /* Output column is "empty" now */ + + zeroNewRow(outpamP, newtuplenrow); + + for (col = 0; col < inpamP->width; ++col) { + /* Process one tuple from input ('inputtuplenrow') */ + fraccolleft = xscale; + /* Output all columns, if any, that can be filled using information + from this input column, in addition to what's already in the output + column. + */ + while (fraccolleft >= fraccoltofill) { + /* Generate one output pixel in 'newtuplerow'. It will + consist of anything accumulated from prior input pixels + in accumulator[], plus a fraction of the current input + pixel. + */ + assert(newcol < outpamP->width); + accumOutputCol(inpamP, inputtuplenrow[col], fraccoltofill, + newtuplenrow[newcol]); + + fraccolleft -= fraccoltofill; + + /* Set up to start filling next output column */ + ++newcol; + fraccoltofill = 1.0; + } + /* There's not enough left in the current input pixel to fill up + a whole output column, so just accumulate the remainder of the + pixel into the current output column. Due to rounding, we may + have a tiny bit of pixel left and have run out of output pixels. + In that case, we throw away what's left. + */ + if (fraccolleft > 0.0 && newcol < outpamP->width) { + accumOutputCol(inpamP, inputtuplenrow[col], fraccolleft, + newtuplenrow[newcol]); + fraccoltofill -= fraccolleft; + } + } + + if (newcol < outpamP->width-1 || newcol > outpamP->width) + pm_error("Internal error: last column filled is %d, but %d " + "is the rightmost output column.", + newcol, outpamP->width-1); + + if (newcol < outpamP->width) { + /* We were still working on the last output column when we + ran out of input columns. This would be because of rounding + down, and we should be missing only a tiny fraction of that + last output column. Just fill in the missing color with the + color of the rightmost input pixel. + */ + accumOutputCol(inpamP, inputtuplenrow[inpamP->width-1], + fraccoltofill, newtuplenrow[newcol]); + + *stretchP = fraccoltofill; + } else + *stretchP = 0.0; +} + + + +static void +zeroAccum(struct pam * const pamP, + tuplen * const accumulator) { + + unsigned int plane; + + for (plane = 0; plane < pamP->depth; ++plane) { + unsigned int col; + for (col = 0; col < pamP->width; ++col) + accumulator[col][plane] = 0.0; + } +} + + + +static void +accumOutputRow(struct pam * const pamP, + tuplen * const tuplenrow, + float const fraction, + tuplen * const accumulator) { +/*---------------------------------------------------------------------------- + Take 'fraction' times the samples in row 'tuplenrow' and add it to + 'accumulator' in the same way as accumOutputCol(). + + 'fraction' is less than 1.0. +-----------------------------------------------------------------------------*/ + unsigned int plane; + + for (plane = 0; plane < pamP->depth; ++plane) { + unsigned int col; + for (col = 0; col < pamP->width; ++col) + accumulator[col][plane] += fraction * tuplenrow[col][plane]; + } +} + + + +static void +readARow(struct pam * const pamP, + tuplen * const tuplenRow, + const pnm_transformMap * const transform) { +/*---------------------------------------------------------------------------- + Read a row from the input file described by *pamP, as values (for the + foreground color) proportional to light intensity, with opacity + included. + + By contrast, a simple libnetpbm read would give the same numbers you + find in a PAM: gamma-adjusted values for the foreground color + component and scaled as if opaque. The latter means that full red + would have a red intensity of 1.0 even if the pixel is only 75% + opaque. We, on the other hand, would return red intensity of .75 in + that case. + + The opacity plane we return is the same as a simple libnetpbm read + would return. + + We ASSUME that the transform 'transform' is that necessary to effect + the conversion to intensity-linear values and normalize. If it is + NULL, we ASSUME that they already are intensity-proportional and just + need to be normalized. +-----------------------------------------------------------------------------*/ + tuple * tupleRow; + + tupleRow = pnm_allocpamrow(pamP); + + pnm_readpamrow(pamP, tupleRow); + + pnm_normalizeRow(pamP, tupleRow, transform, tuplenRow); + + pnm_applyopacityrown(pamP, tuplenRow); + + pnm_freepamrow(tupleRow); +} + + + +static void +writeARow(struct pam * const pamP, + tuplen * const tuplenRow, + const pnm_transformMap * const transform) { +/*---------------------------------------------------------------------------- + Write a row to the output file described by *pamP, from values + proportional to light intensity with opacity included (i.e. the same + kind of number you would get form readARow()). + + We ASSUME that the transform 'transform' is that necessary to effect + the conversion to brightness-linear unnormalized values. If it is + NULL, we ASSUME that they already are brightness-proportional and just + need to be unnormalized. + + We destroy *tuplenRow in the process. +-----------------------------------------------------------------------------*/ + tuple * tupleRow; + + tupleRow = pnm_allocpamrow(pamP); + + pnm_unapplyopacityrown(pamP, tuplenRow); + + pnm_unnormalizeRow(pamP, tuplenRow, transform, tupleRow); + + pnm_writepamrow(pamP, tupleRow); + + pnm_freepamrow(tupleRow); +} + + + +static void +issueStretchWarning(bool const verbose, + double const fracrowtofill) { + + /* We need another input row to fill up this + output row, but there aren't any more. + That's because of rounding down on our + scaling arithmetic. So we go ahead with + the data from the last row we read, which + amounts to stretching out the last output + row. + */ + if (verbose) + pm_message("%f of bottom row stretched due to " + "arithmetic imprecision", + fracrowtofill); +} + + + +static void +scaleHorizontallyAndOutputRow(struct pam * const inpamP, + struct pam * const outpamP, + tuplen * const rowAccumulator, + const pnm_transformMap * const transform, + tuplen * const newtuplenrow, + float const xscale, + unsigned int const row, + bool const verbose) { +/*---------------------------------------------------------------------------- + Scale the row in 'rowAccumulator' horizontally by factor 'xscale' + and output it. + + 'newtuplenrow' is work space Caller provides us. It is at least + wide enough to hold one output row. +-----------------------------------------------------------------------------*/ + if (outpamP->width == inpamP->width) + /* shortcut X scaling */ + writeARow(outpamP, rowAccumulator, transform); + /* This destroys 'rowAccumulator' */ + else { + float stretch; + + horizontalScale(rowAccumulator, newtuplenrow, inpamP, outpamP, + xscale, &stretch); + + if (verbose && row == 0) + pm_message("%f of right column stretched due to " + "arithmetic imprecision", + stretch); + + writeARow(outpamP, newtuplenrow, transform); + /* This destroys 'newtuplenrow' */ + } +} + + + +static void +createTransforms(struct pam * const inpamP, + struct pam * const outpamP, + bool const assumeLinear, + const pnm_transformMap ** const inputTransformP, + const pnm_transformMap ** const outputTransformP) { + + if (assumeLinear) { + *inputTransformP = NULL; + *outputTransformP = NULL; + } else { + *inputTransformP = pnm_createungammatransform(inpamP); + *outputTransformP = pnm_creategammatransform(outpamP); + } +} + + + +static void +destroyTransforms(const pnm_transformMap * const inputTransform, + const pnm_transformMap * const outputTransform) { + + if (inputTransform) + free((void*)inputTransform); + + if (outputTransform) + free((void*)outputTransform); +} + + + +static void +scaleWithMixing(struct pam * const inpamP, + struct pam * const outpamP, + float const xscale, + float const yscale, + bool const assumeLinear, + bool const verbose) { +/*---------------------------------------------------------------------------- + Scale the image described by *inpamP by xscale horizontally and + yscale vertically and write the result as the image described by + *outpamP. + + The input file is positioned past the header, to the beginning of the + raster. The output file is too. + + Mix colors from input rows together in the output rows. + + 'assumeLinear' means to assume that the sample values in the input + image vary from standard PAM in that they are proportional to + intensity, (This makes the computation a lot faster, so you might + use this even if the samples are actually standard PAM, to get + approximate but fast results). + +-----------------------------------------------------------------------------*/ + /* Here's how we think of the color mixing scaling operation: + + First, I'll describe scaling in one dimension. Assume we have + a one row image. A raster row is ordinarily a sequence of + discrete pixels which have no width and no distance between + them -- only a sequence. Instead, think of the raster row as a + bunch of pixels 1 unit wide adjacent to each other. For + example, we are going to scale a 100 pixel row to a 150 pixel + row. Imagine placing the input row right above the output row + and stretching it so it is the same size as the output row. It + still contains 100 pixels, but they are 1.5 units wide each. + Our goal is to make the output row look as much as possible + like the stretched input row, while observing that a pixel can + be only one color. + + Output Pixel 0 is completely covered by Input Pixel 0, so we + make Output Pixel 0 the same color as Input Pixel 0. Output + Pixel 1 is covered half by Input Pixel 0 and half by Input + Pixel 1. So we make Output Pixel 1 a 50/50 mix of Input Pixels + 0 and 1. If you stand back far enough, input and output will + look the same. + + This works for all scale factors, both scaling up and scaling down. + + For images with an opacity plane, imagine Input Pixel 0's + foreground is fully opaque red (1,0,0,1), and Input Pixel 1 is + fully transparent (foreground irrelevant) (0,0,0,0). We make + Output Pixel 0's foreground fully opaque red as before. Output + Pixel 1 is covered half by Input Pixel 0 and half by Input + Pixel 1, so it is 50% opaque; but its foreground color is still + red: (1,0,0,0.5). The output foreground color is the opacity + and coverage weighted average of the input foreground colors, + and the output opacity is the coverage weighted average of the + input opacities. + + This program always stretches or squeezes the input row to be the + same length as the output row; The output row's pixels are always + 1 unit wide. + + The same thing works in the vertical direction. We think of + rows as stacked strips of 1 unit height. We conceptually + stretch the image vertically first (same process as above, but + in place of a single-color pixels, we have a vector of colors). + Then we take each row this vertical stretching generates and + stretch it horizontally. + */ + + tuplen * tuplenrow; /* An input row */ + tuplen * newtuplenrow; /* Working space */ + float rowsleft; + /* The number of rows of output that need to be formed from the + current input row (the one in tuplerow[]), less the number that + have already been formed (either in accumulator[] + or output to the file). This can be fractional because of the + way we define rows as having height. + */ + float fracrowtofill; + /* The fraction of the current output row (the one in vertScaledRow[]) + that hasn't yet been filled in from an input row. + */ + tuplen * rowAccumulator; + /* The red, green, and blue color intensities so far accumulated + from input rows for the current output row. The ultimate value + of this is an output row after vertical scaling, but before + horizontal scaling. + */ + int rowsread; + /* Number of rows of the input file that have been read */ + int row; + const pnm_transformMap * inputTransform; + const pnm_transformMap * outputTransform; + + tuplenrow = pnm_allocpamrown(inpamP); + rowAccumulator = pnm_allocpamrown(inpamP); + + rowsread = 0; + rowsleft = 0.0; + fracrowtofill = 1.0; + + newtuplenrow = pnm_allocpamrown(outpamP); + + createTransforms(inpamP, outpamP, assumeLinear, + &inputTransform, &outputTransform); + + for (row = 0; row < outpamP->height; ++row) { + /* First scale Y from tuplerow[] into rowAccumulator[]. */ + + zeroAccum(inpamP, rowAccumulator); + + if (outpamP->height == inpamP->height) { + /* shortcut Y scaling */ + readARow(inpamP, rowAccumulator, inputTransform); + } else { + while (fracrowtofill > 0) { + if (rowsleft <= 0.0) { + if (rowsread < inpamP->height) { + readARow(inpamP, tuplenrow, inputTransform); + ++rowsread; + } else + issueStretchWarning(verbose, fracrowtofill); + rowsleft = yscale; + } + if (rowsleft < fracrowtofill) { + accumOutputRow(inpamP, tuplenrow, rowsleft, + rowAccumulator); + fracrowtofill -= rowsleft; + rowsleft = 0.0; + } else { + accumOutputRow(inpamP, tuplenrow, fracrowtofill, + rowAccumulator); + rowsleft = rowsleft - fracrowtofill; + fracrowtofill = 0.0; + } + } + fracrowtofill = 1.0; + } + /* 'rowAccumulator' now contains the contents of a single + output row, but not yet horizontally scaled. Scale it now + horizontally and write it out. + */ + scaleHorizontallyAndOutputRow(inpamP, outpamP, rowAccumulator, + outputTransform, newtuplenrow, xscale, + row, verbose); + /* Destroys rowAccumulator */ + + } + destroyTransforms(inputTransform, outputTransform); + pnm_freepamrown(rowAccumulator); + pnm_freepamrown(newtuplenrow); + pnm_freepamrown(tuplenrow); +} + + + +static void +scaleWithoutMixing(const struct pam * const inpamP, + const struct pam * const outpamP, + float const xscale, + float const yscale) { +/*---------------------------------------------------------------------------- + Scale the image described by *inpamP by xscale horizontally and + yscale vertically and write the result as the image described by + *outpamP. + + The input file is positioned past the header, to the beginning of the + raster. The output file is too. + + Don't mix colors from different input pixels together in the output + pixels. Each output pixel is an exact copy of some corresponding + input pixel. +-----------------------------------------------------------------------------*/ + tuple * tuplerow; /* An input row */ + tuple * newtuplerow; + int row; + int rowInInput; + + tuplerow = pnm_allocpamrow(inpamP); + rowInInput = -1; + + newtuplerow = pnm_allocpamrow(outpamP); + + for (row = 0; row < outpamP->height; ++row) { + int col; + + int const inputRow = (int) (row / yscale); + + for (; rowInInput < inputRow; ++rowInInput) + pnm_readpamrow(inpamP, tuplerow); + + for (col = 0; col < outpamP->width; ++col) { + int const inputCol = (int) (col / xscale); + + pnm_assigntuple(inpamP, newtuplerow[col], tuplerow[inputCol]); + } + + pnm_writepamrow(outpamP, newtuplerow); + } + pnm_freepamrow(tuplerow); + pnm_freepamrow(newtuplerow); +} + + + +int +main(int argc, char **argv ) { + + struct cmdlineInfo cmdline; + FILE* ifP; + struct pam inpam, outpam; + float xscale, yscale; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFileName); + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + outpam = inpam; /* initial value */ + outpam.file = stdout; + + if (PNM_FORMAT_TYPE(inpam.format) == PBM_TYPE) { + outpam.format = PGM_TYPE; + outpam.maxval = PGM_MAXMAXVAL; + pm_message("promoting from PBM to PGM"); + } else { + outpam.format = inpam.format; + outpam.maxval = inpam.maxval; + } + + computeOutputDimensions(cmdline, inpam.height, inpam.width, + &outpam.height, &outpam.width); + + xscale = (float) outpam.width / inpam.width; + yscale = (float) outpam.height / inpam.height; + + if (cmdline.verbose) { + pm_message("Scaling by %f horizontally to %d columns.", + xscale, outpam.width); + pm_message("Scaling by %f vertically to %d rows.", + yscale, outpam.height); + } + + if (xscale * inpam.width < outpam.width - 1 || + yscale * inpam.height < outpam.height - 1) + pm_error("Arithmetic precision of this program is inadequate to " + "do the specified scaling. Use a smaller input image " + "or a slightly different scale factor."); + + pnm_writepaminit(&outpam); + + if (cmdline.nomix) { + if (cmdline.verbose) + pm_message("Using nomix method"); + scaleWithoutMixing(&inpam, &outpam, xscale, yscale); + } else if (!cmdline.filterFunction) { + if (cmdline.verbose) + pm_message("Using regular rescaling method"); + scaleWithMixing(&inpam, &outpam, xscale, yscale, + cmdline.linear, cmdline.verbose); + } else { + if (cmdline.verbose) + pm_message("Using general filter method"); + resample(&inpam, &outpam, + cmdline.filterFunction, cmdline.filterRadius, + cmdline.windowFunction, cmdline.verbose, + cmdline.linear); + } + pm_close(ifP); + pm_close(stdout); + + return 0; +} diff --git a/editor/pamstretch-gen b/editor/pamstretch-gen new file mode 100755 index 00000000..cd59a36b --- /dev/null +++ b/editor/pamstretch-gen @@ -0,0 +1,80 @@ +#!/bin/sh +# +# pamstretch-gen - a shell script which acts a little like a general +# form of pamstretch, by scaling up with pamstretch then scaling +# down with pamscale. +# +# it also copes with N<1, but then it just uses pamscale. :-) +# +# Formerly named 'pnminterp-gen' and 'pnmstretch-gen'. +# +# Copyright (C) 1998,2000 Russell Marks. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or (at +# your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# + + +if [ "$1" = "" ]; then + echo 'usage: pamstretch-gen N [pnmfile]' + exit 1 +fi + +tempdir="${TMPDIR-/tmp}/pamstretch-gen.$$" +mkdir $tempdir || { echo "Could not create temporary file. Exiting."; exit 1;} +chmod 700 $tempdir +tempfile=$tempdir/pnmig + +trap 'rm -rf $tempdir' 0 1 3 15 + +if ! cat $2 >$tempfile 2>/dev/null; then + echo 'pamstretch-gen: error reading file' 1>&2 + exit 1 +fi + +if ! pnmfile $tempfile 1>/dev/null 2>/dev/null; then + echo 'Not valid pnm input' + exit 1 +fi + +# we use the width as indication of how much to scale; width and +# height are being scaled equally, so this should be ok. +width=`pnmfile $tempfile 2>/dev/null|cut -d " " -f 3` + +if [ "$width" = "" ]; then + echo 'pamstretch-gen: not a PNM file' 1>&2 + exit 1 +fi + +# should really use dc for maths, but awk is less painful :-) +target_width=`awk 'BEGIN{printf("%d",'0.5+"$width"*"$1"')}'` + +# work out how far we have to scale it up with pamstretch so that the +# new width is >= the target width. +int_scale=`awk ' +BEGIN { +int_scale=1;int_width='"$width"' +while(int_width<'"$target_width"') + { + int_scale++ + int_width+='"$width"' + } +print int_scale +}'` + +if [ "$int_scale" -eq 1 ]; then + pamscale "$1" $tempfile +else + pamstretch "$int_scale" $tempfile | pnmscale -xsi "$target_width" +fi diff --git a/editor/pamstretch.c b/editor/pamstretch.c new file mode 100644 index 00000000..0e9e6abf --- /dev/null +++ b/editor/pamstretch.c @@ -0,0 +1,408 @@ +/* pamstretch - scale up portable anymap by interpolating between pixels. + * + * This program is based on 'pnminterp' by Russell Marks, rename + * pnmstretch for inclusion in Netpbm, then rewritten and renamed to + * pamstretch by Bryan Henderson in December 2001. + * + * Copyright (C) 1998,2000 Russell Marks. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <ctype.h> +#include "pam.h" +#include "shhopt.h" + +enum an_edge_mode { + EDGE_DROP, + /* drop one (source) pixel at right/bottom edges. */ + EDGE_INTERP_TO_BLACK, + /* interpolate right/bottom edge pixels to black. */ + EDGE_NON_INTERP + /* don't interpolate right/bottom edge pixels + (default, and what zgv does). */ +}; + + +struct cmdline_info { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *input_filespec; /* Filespecs of input files */ + enum an_edge_mode edge_mode; + unsigned int xscale; + unsigned int yscale; +}; + + + +tuple blackTuple; + /* A "black" tuple. Unless our input image is PBM, PGM, or PPM, we + don't really know what "black" means, so this is just something + arbitrary in that case. + */ + + +static void +parse_command_line(int argc, char ** argv, + struct cmdline_info *cmdline_p) { +/*---------------------------------------------------------------------------- + Note that the file spec array we return is stored in the storage that + was passed to us as the argv array. +-----------------------------------------------------------------------------*/ + optStruct3 opt; /* set by OPTENT3 */ + optEntry *option_def = malloc(100*sizeof(optEntry)); + unsigned int option_def_index; + + unsigned int blackedge; + unsigned int dropedge; + unsigned int xscale_spec; + unsigned int yscale_spec; + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENT3('b', "blackedge", OPT_FLAG, NULL, &blackedge, 0); + OPTENT3('d', "dropedge", OPT_FLAG, NULL, &dropedge, 0); + OPTENT3(0, "xscale", OPT_UINT, + &cmdline_p->xscale, &xscale_spec, 0); + OPTENT3(0, "yscale", OPT_UINT, + &cmdline_p->yscale, &yscale_spec, 0); + + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* We have some short (old-fashioned) options */ + opt.allowNegNum = FALSE; /* We have no parms that are negative numbers */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdline_p and others. */ + + if (blackedge && dropedge) + pm_error("Can't specify both -blackedge and -dropedge options."); + else if (blackedge) + cmdline_p->edge_mode = EDGE_INTERP_TO_BLACK; + else if (dropedge) + cmdline_p->edge_mode = EDGE_DROP; + else + cmdline_p->edge_mode = EDGE_NON_INTERP; + + if (xscale_spec && cmdline_p->xscale == 0) + pm_error("You specified zero for the X scale factor."); + if (yscale_spec && cmdline_p->yscale == 0) + pm_error("You specified zero for the Y scale factor."); + + if (xscale_spec && !yscale_spec) + cmdline_p->yscale = 1; + if (yscale_spec && !xscale_spec) + cmdline_p->xscale = 1; + + if (!(xscale_spec || yscale_spec)) { + /* scale must be specified in an argument */ + if ((argc-1) != 1 && (argc-1) != 2) + pm_error("Wrong number of arguments (%d). Without scale options, " + "you must supply 1 or 2 arguments: scale and " + "optional file specification", argc-1); + + { + char *endptr; /* ptr to 1st invalid character in scale arg */ + unsigned int scale; + + scale = strtol(argv[1], &endptr, 10); + if (*argv[1] == '\0') + pm_error("Scale argument is a null string. " + "Must be a number."); + else if (*endptr != '\0') + pm_error("Scale argument contains non-numeric character '%c'.", + *endptr); + else if (scale < 2) + pm_error("Scale argument must be at least 2. " + "You specified %d", scale); + cmdline_p->xscale = scale; + cmdline_p->yscale = scale; + } + if (argc-1 > 1) + cmdline_p->input_filespec = argv[2]; + else + cmdline_p->input_filespec = "-"; + } else { + /* No scale argument allowed */ + if ((argc-1) > 1) + pm_error("Too many arguments (%d). With a scale option, " + "the only argument is the " + "optional file specification", argc-1); + if (argc-1 > 0) + cmdline_p->input_filespec = argv[1]; + else + cmdline_p->input_filespec = "-"; + } +} + + + +static void +stretch_line(struct pam * const inpamP, + const tuple * const line, const tuple * const line_stretched, + unsigned int const scale, enum an_edge_mode const edge_mode) { +/*---------------------------------------------------------------------------- + Stretch the line of tuples 'line' into the output buffer 'line_stretched', + by factor 'scale'. +-----------------------------------------------------------------------------*/ + int scaleincr; + int sisize; + /* normalizing factor to make fractions representable as integers. + E.g. if sisize = 100, one half is represented as 50. + */ + unsigned int col; + unsigned int outcol; + + sisize=0; + while (sisize<256) + sisize += scale; + scaleincr = sisize/scale; /* (1/scale, normalized) */ + + outcol = 0; /* initial value */ + + for (col = 0; col < inpamP->width; ++col) { + unsigned int pos; + /* The fraction of the way we are from curline to nextline, + normalized by sisize. + */ + if (col >= inpamP->width-1) { + /* We're at the edge. There is no column to the right with which + to interpolate. + */ + switch(edge_mode) { + case EDGE_DROP: + /* No output column needed for this input column */ + break; + case EDGE_INTERP_TO_BLACK: { + unsigned int pos; + for (pos = 0; pos < sisize; pos += scaleincr) { + unsigned int plane; + for (plane = 0; plane < inpamP->depth; ++plane) + line_stretched[outcol][plane] = + (line[col][plane] * (sisize-pos)) / sisize; + ++outcol; + } + } + break; + case EDGE_NON_INTERP: { + unsigned int pos; + for (pos = 0; pos < sisize; pos += scaleincr) { + unsigned int plane; + for (plane = 0; plane < inpamP->depth; ++plane) + line_stretched[outcol][plane] = line[col][plane]; + ++outcol; + } + } + break; + default: + pm_error("INTERNAL ERROR: invalid value for edge_mode"); + } + } else { + /* Interpolate with the next input column to the right */ + for (pos = 0; pos < sisize; pos += scaleincr) { + unsigned int plane; + for (plane = 0; plane < inpamP->depth; ++plane) + line_stretched[outcol][plane] = + (line[col][plane] * (sisize-pos) + + line[col+1][plane] * pos) / sisize; + ++outcol; + } + } + } +} + + + +static void +write_interp_rows(struct pam * const outpamP, + const tuple * const curline, + const tuple * const nextline, + tuple * const outbuf, + int const scale) { +/*---------------------------------------------------------------------------- + Write out 'scale' rows, being 'curline' followed by rows that are + interpolated between 'curline' and 'nextline'. +-----------------------------------------------------------------------------*/ + unsigned int scaleincr; + unsigned int sisize; + unsigned int pos; + + sisize=0; + while(sisize<256) sisize+=scale; + scaleincr=sisize/scale; + + for (pos = 0; pos < sisize; pos += scaleincr) { + unsigned int col; + for (col = 0; col < outpamP->width; ++col) { + unsigned int plane; + for (plane = 0; plane < outpamP->depth; ++plane) + outbuf[col][plane] = (curline[col][plane] * (sisize-pos) + + nextline[col][plane] * pos) / sisize; + } + pnm_writepamrow(outpamP, outbuf); + } +} + + + +static void +swap_buffers(tuple ** const buffer1P, tuple ** const buffer2P) { + /* Advance "next" line to "current" line by switching + line buffers + */ + tuple *tmp; + + tmp = *buffer1P; + *buffer1P = *buffer2P; + *buffer2P = tmp; +} + + +static void +stretch(struct pam * const inpamP, struct pam * const outpamP, + int const xscale, int const yscale, + enum an_edge_mode const edge_mode) { + + tuple *linebuf1, *linebuf2; /* Input buffers for two rows at a time */ + tuple *curline, *nextline; /* Pointers to one of the two above buffers */ + /* And the stretched versions: */ + tuple *stretched_linebuf1, *stretched_linebuf2; + tuple *curline_stretched, *nextline_stretched; + + tuple *outbuf; /* One-row output buffer */ + unsigned int row; + unsigned int rowsToStretch; + + linebuf1 = pnm_allocpamrow(inpamP); + linebuf2 = pnm_allocpamrow(inpamP); + stretched_linebuf1 = pnm_allocpamrow(outpamP); + stretched_linebuf2 = pnm_allocpamrow(outpamP); + outbuf = pnm_allocpamrow(outpamP); + + curline = linebuf1; + curline_stretched = stretched_linebuf1; + nextline = linebuf2; + nextline_stretched = stretched_linebuf2; + + pnm_readpamrow(inpamP, curline); + stretch_line(inpamP, curline, curline_stretched, xscale, edge_mode); + + if (edge_mode == EDGE_DROP) + rowsToStretch = inpamP->height - 1; + else + rowsToStretch = inpamP->height; + + for (row = 0; row < rowsToStretch; row++) { + if (row == inpamP->height-1) { + /* last line is about to be output. there is no further + * `next line'. if EDGE_DROP, we stop here, with output + * of rows-1 rows. if EDGE_INTERP_TO_BLACK we make next + * line black. if EDGE_NON_INTERP (default) we make it a + * copy of the current line. + */ + switch (edge_mode) { + case EDGE_INTERP_TO_BLACK: { + int col; + for (col = 0; col < outpamP->width; col++) + nextline_stretched[col] = blackTuple; + } + break; + case EDGE_NON_INTERP: { + /* EDGE_NON_INTERP */ + int col; + for (col = 0; col < outpamP->width; col++) + nextline_stretched[col] = curline_stretched[col]; + } + break; + case EDGE_DROP: + pm_error("INTERNAL ERROR: processing last row, but " + "edge_mode is EDGE_DROP."); + } + } else { + pnm_readpamrow(inpamP, nextline); + stretch_line(inpamP, nextline, nextline_stretched, xscale, + edge_mode); + } + + /* interpolate curline towards nextline into outbuf */ + write_interp_rows(outpamP, curline_stretched, nextline_stretched, + outbuf, yscale); + + swap_buffers(&curline, &nextline); + swap_buffers(&curline_stretched, &nextline_stretched); + } + pnm_freerow(outbuf); + pnm_freerow(stretched_linebuf2); + pnm_freerow(stretched_linebuf1); + pnm_freerow(linebuf2); + pnm_freerow(linebuf1); +} + + + +int +main(int argc,char *argv[]) { + + FILE *ifp; + + struct cmdline_info cmdline; + struct pam inpam, outpam; + + pnm_init(&argc, argv); + + parse_command_line(argc, argv, &cmdline); + + ifp = pm_openr(cmdline.input_filespec); + + pnm_readpaminit(ifp, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + if (inpam.width < 2) + pm_error("Image is too narrow. Must be at least 2 columns."); + if (inpam.height < 2) + pm_error("Image is too short. Must be at least 2 lines."); + + + outpam = inpam; /* initial value */ + outpam.file = stdout; + + if (PNM_FORMAT_TYPE(inpam.format) == PBM_TYPE) { + outpam.format = PGM_TYPE; + /* usual filter message when reading PBM but writing PGM: */ + pm_message("promoting from PBM to PGM"); + } else { + outpam.format = inpam.format; + } + { + unsigned int const dropped = cmdline.edge_mode == EDGE_DROP ? 1 : 0; + + outpam.width = (inpam.width - dropped) * cmdline.xscale; + outpam.height = (inpam.height - dropped) * cmdline.yscale; + + pnm_writepaminit(&outpam); + } + + pnm_createBlackTuple(&outpam, &blackTuple); + + stretch(&inpam, &outpam, + cmdline.xscale, cmdline.yscale, cmdline.edge_mode); + + pm_close(ifp); + + exit(0); +} + + + diff --git a/editor/pamthreshold.c b/editor/pamthreshold.c new file mode 100644 index 00000000..40260e79 --- /dev/null +++ b/editor/pamthreshold.c @@ -0,0 +1,623 @@ +/* pamthreshold - convert a Netpbm image to black and white by thresholding */ + +/* This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +/* Copyright (C) 2006 Erik Auerswald + * auerswal@unix-ag.uni-kl.de */ + +#include <assert.h> +#include <math.h> +#include <stdlib.h> +#include <string.h> + +#include "mallocvar.h" +#include "nstring.h" +#include "shhopt.h" +#include "pam.h" + +#define MAX_ITERATIONS 100 /* stop after at most 100 iterations */ + +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 simple; + float threshold; + bool local; + bool dual; + float contrast; + unsigned int width, height; + /* geometry of local subimage. Defined only if 'local' or 'dual' + is true. + */ +}; + + + +struct range { + /* A range of sample values, normalized to [0, 1] */ + samplen min; + samplen max; +}; + + + +static void +initRange(struct range * const rangeP) { + + /* Initialize to "undefined" state */ + rangeP->min = 1.0; + rangeP->max = 0.0; +} + + + +static void +addToRange(struct range * const rangeP, + samplen const newSample) { + + rangeP->min = MIN(newSample, rangeP->min); + rangeP->max = MAX(newSample, rangeP->max); +} + + + +static float +spread(struct range const range) { + + assert(range.max >= range.min); + + return range.max - range.min; +} + + + +static void +parseGeometry(const char * const wxl, + unsigned int * const widthP, + unsigned int * const heightP, + const char ** const errorP) { + + char * const xPos = strchr(wxl, 'x'); + if (!xPos) + asprintfN(errorP, "There is no 'x'. It should be WIDTHxHEIGHT"); + else { + *widthP = atoi(wxl); + *heightP = atoi(xPos + 1); + + if (*widthP == 0) + asprintfN(errorP, "Width is zero."); + else if (*heightP == 0) + asprintfN(errorP, "Height is zero."); + else + *errorP = NULL; + } +} + + + +static void +parseCommandLine(int argc, + char ** argv, + struct cmdlineInfo *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. +-----------------------------------------------------------------------------*/ + /* vars for the option parser */ + optEntry * option_def; + optStruct3 opt; + unsigned int option_def_index = 0; /* incremented by OPTENT3 */ + + unsigned int thresholdSpec, localSpec, dualSpec, contrastSpec; + const char * localOpt; + const char * dualOpt; + + MALLOCARRAY_NOFAIL(option_def, 100); + + /* define the options */ + OPTENT3(0, "simple", OPT_FLAG, NULL, + &cmdlineP->simple, 0); + OPTENT3(0, "local", OPT_STRING, &localOpt, + &localSpec, 0); + OPTENT3(0, "dual", OPT_STRING, &dualOpt, + &dualSpec, 0); + OPTENT3(0, "threshold", OPT_FLOAT, &cmdlineP->threshold, + &thresholdSpec, 0); + OPTENT3(0, "contrast", OPT_FLOAT, &cmdlineP->contrast, + &contrastSpec, 0); + + /* set the defaults */ + cmdlineP->width = cmdlineP->height = 0U; + + /* set the option description for optParseOptions3 */ + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* long options only */ + opt.allowNegNum = FALSE; /* we have no numbers at all */ + + /* parse commandline, change argc, argv, and *cmdlineP */ + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + + if (cmdlineP->simple + localSpec + dualSpec > 1) + pm_error("You may specify only one of -simple, -local, and -dual"); + + if (!thresholdSpec) + cmdlineP->threshold = 0.5; + + /* 0 <= threshold <= 1 */ + if ((cmdlineP->threshold < 0.0) || (cmdlineP->threshold > 1.0)) + pm_error("threshold must be in [0,1]"); + + if (!contrastSpec) + cmdlineP->contrast = 0.05; + + /* 0 <= contrast <= 1 */ + if ((cmdlineP->contrast < 0.0) || (cmdlineP->contrast > 1.0)) + pm_error("contrast must be in [0,1]"); + + if (localSpec) { + const char * error; + cmdlineP->local = TRUE; + + parseGeometry(localOpt, &cmdlineP->width, &cmdlineP->height, &error); + + if (error) { + pm_error("Invalid -local value '%s'. %s", localOpt, error); + strfree(error); + } + } else + cmdlineP->local = FALSE; + + if (dualSpec) { + const char * error; + cmdlineP->dual = TRUE; + + parseGeometry(dualOpt, &cmdlineP->width, &cmdlineP->height, &error); + + if (error) { + pm_error("Invalid -dual value '%s'. %s", dualOpt, error); + strfree(error); + } + } else + cmdlineP->dual = FALSE; + + if (argc-1 < 1) + cmdlineP->inputFileName = "-"; + else if (argc-1 == 1) + cmdlineP->inputFileName = argv[1]; + else + pm_error("Progam takes at most 1 parameter: the file name. " + "You specified %d", argc-1); +} + + + +/* simple thresholding (the same as in pamditherbw) */ + +static void +thresholdSimple(struct pam * const inpamP, + struct pam * const outpamP, + float const threshold) { + + tuplen * inrow; /* normalized input row */ + tuple * outrow; /* raw output row */ + unsigned int row; /* number of the current row */ + + inrow = pnm_allocpamrown(inpamP); + outrow = pnm_allocpamrow(outpamP); + + /* do the simple thresholding */ + for (row = 0; row < inpamP->height; ++row) { + unsigned int col; + pnm_readpamrown(inpamP, inrow); + for (col = 0; col < inpamP->width; ++col) + outrow[col][0] = + inrow[col][0] >= threshold ? PAM_BW_WHITE : PAM_BLACK; + pnm_writepamrow(outpamP, outrow); + } + + pnm_freepamrow(inrow); + pnm_freepamrow(outrow); +} + + + +static void +analyzeDistribution(struct pam * const inpamP, + const unsigned int ** const histogramP, + struct range * const rangeP) { +/*---------------------------------------------------------------------------- + Find the distribution of the sample values -- minimum, maximum, and + how many of each value -- in input image *inpamP, whose file is + positioned to the raster. + + Return the minimum and maximum as *rangeP and the frequency + distribution as *histogramP, an array such that histogram[i] is the + number of pixels that have sample value i. + + Leave the file positioned to the raster. +-----------------------------------------------------------------------------*/ + unsigned int row; + tuple * inrow; + tuplen * inrown; + unsigned int * histogram; /* malloced array */ + unsigned int i; + + pm_filepos rasterPos; /* Position in input file of the raster */ + + pm_tell2(inpamP->file, &rasterPos, sizeof(rasterPos)); + + inrow = pnm_allocpamrow(inpamP); + inrown = pnm_allocpamrown(inpamP); + MALLOCARRAY(histogram, inpamP->maxval+1); + if (histogram == NULL) + pm_error("Unable to allocate space for %lu-entry histogram", + inpamP->maxval+1); + + /* Initialize histogram -- zero occurences of everything */ + for (i = 0; i <= inpamP->maxval; ++i) + histogram[i] = 0; + + initRange(rangeP); + + for (row = 0; row < inpamP->height; ++row) { + unsigned int col; + pnm_readpamrow(inpamP, inrow); + pnm_normalizeRow(inpamP, inrow, NULL, inrown); + for (col = 0; col < inpamP->width; ++col) { + ++histogram[inrow[col][0]]; + addToRange(rangeP, inrown[col][0]); + } + } + *histogramP = histogram; + + pnm_freepamrow(inrow); + pnm_freepamrown(inrown); + + pm_seek2(inpamP->file, &rasterPos, sizeof(rasterPos)); +} + + + +static void +getLocalThreshold(tuplen ** const inrows, + unsigned int const windowWidth, + unsigned int const x, + unsigned int const localWidth, + unsigned int const localHeight, + float const darkness, + float const minSpread, + samplen const defaultThreshold, + samplen * const thresholdP) { +/*---------------------------------------------------------------------------- + Find a suitable threshold in local area around one pixel. + + inrows[][] is a an array of 'windowWidth' pixels by 'localHeight'. + + 'x' is a column number within the window. + + We look at the rectangle consisting of the 'localWidth' columns + surrounding x, all rows. If x is near the left or right edge, we truncate + the window as needed. + + We base the threshold on the local spread (difference between minimum + and maximum sample values in the local areas) and the 'darkness' + factor. A higher 'darkness' gets a higher threshold. + + If the spread is less than 'minSpread', we return 'defaultThreshold' and + 'darkness' is irrelevant. + + 'localWidth' must be odd. +-----------------------------------------------------------------------------*/ + unsigned int const startCol = x >= localWidth/2 ? x - localWidth/2 : 0; + + unsigned int col; + struct range localRange; + + assert(localWidth % 2 == 1); /* entry condition */ + + initRange(&localRange); + + for (col = startCol; col <= x + localWidth/2 && col < windowWidth; ++col) { + unsigned int row; + + for (row = 0; row < localHeight; ++row) + addToRange(&localRange, inrows[row][col][0]); + } + + if (spread(localRange) < minSpread) + *thresholdP = defaultThreshold; + else + *thresholdP = localRange.min + darkness * spread(localRange); +} + + + +static void +thresholdLocalRow(struct pam * const inpamP, + tuplen ** const inrows, + unsigned int const localWidth, + unsigned int const windowHeight, + unsigned int const row, + struct cmdlineInfo const cmdline, + struct range const globalRange, + samplen const globalThreshold, + tuple * const outrow) { + + tuplen * const inrow = inrows[row % windowHeight]; + + float minSpread; + unsigned int col; + + if (cmdline.dual) + minSpread = cmdline.contrast * spread(globalRange); + else + minSpread = 0.0; + + for (col = 0; col < inpamP->width; ++col) { + samplen threshold; + + getLocalThreshold(inrows, inpamP->width, col, localWidth, windowHeight, + cmdline.threshold, minSpread, globalThreshold, + &threshold); + + outrow[col][0] = inrow[col][0] >= threshold ? PAM_BW_WHITE : PAM_BLACK; + } +} + + + +static void +computeGlobalThreshold(struct pam * const inpamP, + const unsigned int * const histogram, + struct range const globalRange, + float * const thresholdP) { +/*---------------------------------------------------------------------------- + Compute the proper threshold to use for the image described by + *inpamP, whose file is positioned to the raster. + + For our convenience: + + 'histogram' describes the frequency of occurence of the various sample + values in the image. + + 'globalRange' describes the range (minimum, maximum) of sample values + in the image. + + Return the threshold (scaled to [0, 1]) as *thresholdP. + + Leave the file positioned to the raster. +-----------------------------------------------------------------------------*/ + /* Found this algo in the wikipedia article "Thresholding (image + processing)" + */ + + float threshold; /* threshold is iteratively determined */ + float oldthreshold; /* stop if oldthreshold==threshold */ + unsigned int iter; /* count of done iterations */ + + /* Use middle value (halfway between min and max) as initial threshold */ + threshold = (globalRange.min + globalRange.max) / 2.0; + + oldthreshold = -1.0; /* initial value */ + iter = 0; /* initial value */ + + /* adjust threshold to image */ + while (fabs(oldthreshold - threshold) > 0.01 && iter < MAX_ITERATIONS) { + unsigned long white, black; /* number of white, black pixels */ + unsigned int row; + + ++iter; + + /* count black and white pixels */ + + for (row = 0, white = 0, black = 0; row < inpamP->height; ++row) { + unsigned int col; + + for(col = 0; col < threshold * inpamP->maxval; ++col) + black += histogram[col]; + for(; col <= inpamP->maxval; ++col) + white += histogram[col]; + } + + oldthreshold = threshold; + + /* Use the weighted average of black and white pixels to calculate new + threshold + */ + threshold = + (black * (globalRange.min + threshold) / 2.0 + + white * (threshold + globalRange.max) / 2.0) / + (black + white); + } + + *thresholdP = threshold; +} + + + +static void +thresholdLocal(struct pam * const inpamP, + struct pam * const outpamP, + struct cmdlineInfo const cmdline) { +/*---------------------------------------------------------------------------- + Threshold the image described by *inpamP, whose file is positioned to the + raster, and output the resulting raster to the image described by + *outpamP. + + Use local adaptive thresholding aka dynamic thresholding or dual + thresholding (global for low contrast areas, LAT otherwise) +-----------------------------------------------------------------------------*/ + struct range globalRange; /* Range of sample values in entire image */ + tuplen ** inrows; + /* vertical window of image containing the local area. This is + a ring of 'windowHeight' rows. Row R of the image, when it is + in the window, is inrows[R % windowHeight]. + */ + unsigned int windowHeight; /* size of 'inrows' window */ + unsigned int nextRowToRead; + /* Number of the next row to be read from the file into the inrows[] + buffer. + */ + tuple * outrow; /* raw output row */ + unsigned int row; + /* Number of the current row. The current row is normally the + one in the center of the inrows[] buffer (which has an actual + center row because it is of odd height), but when near the top + and bottom edge of the image, it is not. + */ + const unsigned int * histogram; + samplen globalThreshold; + /* This is a threshold based on the entire image, to use in areas + where the contrast is too small to use a locally-derived threshold. + */ + unsigned int oddLocalWidth; + unsigned int oddLocalHeight; + unsigned int i; + + /* use a subimage with odd width and height to have a middle pixel */ + + if (cmdline.width % 2 == 0) + oddLocalWidth = cmdline.width + 1; + else + oddLocalWidth = cmdline.width; + if (cmdline.height % 2 == 0) + oddLocalHeight = cmdline.height + 1; + else + oddLocalHeight = cmdline.height; + + windowHeight = MIN(oddLocalHeight, inpamP->height); + + analyzeDistribution(inpamP, &histogram, &globalRange); + + computeGlobalThreshold(inpamP, histogram, globalRange, &globalThreshold); + + outrow = pnm_allocpamrow(outpamP); + + MALLOCARRAY(inrows, windowHeight); + + if (inrows == NULL) + pm_error("Unable to allocate memory for a %u-row array", windowHeight); + + for (i = 0; i < windowHeight; ++i) + inrows[i] = pnm_allocpamrown(inpamP); + + /* Fill the vertical window buffer */ + nextRowToRead = 0; + + while (nextRowToRead < windowHeight) + pnm_readpamrown(inpamP, inrows[nextRowToRead++ % windowHeight]); + + for (row = 0; row < inpamP->height; ++row) { + thresholdLocalRow(inpamP, inrows, oddLocalWidth, windowHeight, row, + cmdline, globalRange, globalThreshold, outrow); + + pnm_writepamrow(outpamP, outrow); + + /* read next image line if available and necessary */ + if (row + windowHeight / 2 >= nextRowToRead && + nextRowToRead < inpamP->height) + pnm_readpamrown(inpamP, inrows[nextRowToRead++ % windowHeight]); + } + + free((void*)histogram); + for (i = 0; i < windowHeight; ++i) + pnm_freepamrow(inrows[i]); + free(inrows); + pnm_freepamrow(outrow); +} + + + +static void +thresholdIterative(struct pam * const inpamP, + struct pam * const outpamP) { + + const unsigned int * histogram; + struct range globalRange; + samplen threshold; + + analyzeDistribution(inpamP, &histogram, &globalRange); + + computeGlobalThreshold(inpamP, histogram, globalRange, &threshold); + + pm_message("using global threshold %4.2f", threshold); + + thresholdSimple(inpamP, outpamP, threshold); +} + + + +int +main(int argc, char **argv) { + + FILE * ifP; + struct cmdlineInfo cmdline; + struct pam inpam, outpam; + bool eof; /* No more images in input stream */ + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + if (cmdline.simple) + ifP = pm_openr(cmdline.inputFileName); + else + ifP = pm_openr_seekable(cmdline.inputFileName); + + /* threshold each image in the PAM file */ + eof = FALSE; + while (!eof) { + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type)); + + /* set output image parameters for a bilevel image */ + outpam.size = sizeof(outpam); + outpam.len = PAM_STRUCT_SIZE(tuple_type); + outpam.file = stdout; + outpam.format = PAM_FORMAT; + outpam.plainformat = 0; + outpam.height = inpam.height; + outpam.width = inpam.width; + outpam.depth = 1; + outpam.maxval = 1; + outpam.bytes_per_sample = 1; + strcpy(outpam.tuple_type, "BLACKANDWHITE"); + + pnm_writepaminit(&outpam); + + /* do the thresholding */ + + if (cmdline.simple) + thresholdSimple(&inpam, &outpam, cmdline.threshold); + else if (cmdline.local || cmdline.dual) + thresholdLocal(&inpam, &outpam, cmdline); + else + thresholdIterative(&inpam, &outpam); + + pnm_nextimage(ifP, &eof); + } + + pm_close(ifP); + + return 0; +} diff --git a/editor/pbmclean.c b/editor/pbmclean.c new file mode 100644 index 00000000..3ae3acfc --- /dev/null +++ b/editor/pbmclean.c @@ -0,0 +1,239 @@ +/* pbmclean.c - pixel cleaning. Remove pixel if less than n connected + * identical neighbours, n=1 default. + * AJCD 20/9/90 + * stern, Fri Oct 19 00:10:38 MET DST 2001 + * add '-white/-black' flags to restrict operation to given blobs + */ + +#include <stdio.h> +#include "pbm.h" +#include "shhopt.h" + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *inputFilespec; /* Filespecs of input files */ + bool flipWhite; + bool flipBlack; + unsigned int connect; + unsigned int verbose; +}; + +#define PBM_INVERT(p) ((p) == PBM_WHITE ? PBM_BLACK : PBM_WHITE) + +/* input bitmap size and storage */ +static bit *inrow[3] ; + +#define THISROW (1) + +enum compass_heading { + WEST=0, + NORTHWEST=1, + NORTH=2, + NORTHEAST=3, + EAST=4, + SOUTHEAST=5, + SOUTH=6, + SOUTHWEST=7 +}; +/* compass directions from west clockwise. Indexed by enum compass_heading */ +int const xd[] = { -1, -1, 0, 1, 1, 1, 0, -1 } ; +int const yd[] = { 0, -1, -1, -1, 0, 1, 1, 1 } ; + +static void +parseCommandLine(int argc, char ** argv, + struct cmdlineInfo *cmdlineP) { +/*---------------------------------------------------------------------------- + Note that the file spec array we return is stored in the storage that + was passed to us as the argv array. +-----------------------------------------------------------------------------*/ + optStruct3 opt; /* set by OPTENT3 */ + optEntry *option_def = malloc(100*sizeof(optEntry)); + unsigned int option_def_index; + + unsigned int black, white; + unsigned int minneighborsSpec; + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "verbose", OPT_FLAG, NULL, &cmdlineP->verbose, 0); + OPTENT3(0, "black", OPT_FLAG, NULL, &black, 0); + OPTENT3(0, "white", OPT_FLAG, NULL, &white, 0); + OPTENT3(0, "minneighbors", OPT_UINT, &cmdlineP->connect, + &minneighborsSpec, 0); + + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ + opt.allowNegNum = TRUE; /* We sort of allow negative numbers as parms */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (!black && !white) { + cmdlineP->flipBlack = TRUE; + cmdlineP->flipWhite = TRUE; + } else { + cmdlineP->flipBlack = !!black; + cmdlineP->flipWhite = !!white; + } + + + if (!minneighborsSpec) { + /* Now we do a sleazy tour through the parameters to see if + one is -N where N is a positive integer. That's for + backward compatibility, since Pbmclean used to have + unconventional syntax where a -N option was used instead of + the current -minneighbors option. The only reason -N didn't + get processed by pm_optParseOptions3() is that it looked + like a negative number parameter instead of an option. + If we find a -N, we make like it was a -minneighbors=N option. + */ + int i; + bool foundNegative; + + cmdlineP->connect = 1; /* default */ + foundNegative = FALSE; + + for (i = 1; i < argc; ++i) { + if (foundNegative) + argv[i-1] = argv[i]; + else { + if (atoi(argv[i]) < 0) { + cmdlineP->connect = - atoi(argv[i]); + foundNegative = TRUE; + } + } + } + if (foundNegative) + --argc; + } + + if (argc-1 < 1) + cmdlineP->inputFilespec = "-"; + else if (argc-1 == 1) + cmdlineP->inputFilespec = argv[1]; + else + pm_error("You specified too many arguments (%d). The only " + "argument is the optional input file specification.", + argc-1); +} + + + + + +static void +nextrow(FILE * const ifd, + int const row, + int const cols, + int const rows, + int const format) { +/*---------------------------------------------------------------------------- + Advance one row in the input. + + 'row' is the row number that will be the current row. +-----------------------------------------------------------------------------*/ + bit * shuffle; + + /* First, get the "next" row in inrow[2] if this is the very first + call to nextrow(). + */ + if (inrow[2] == NULL && row < rows) { + inrow[2] = pbm_allocrow(cols); + pbm_readpbmrow(ifd, inrow[2], cols, format); + } + /* Now advance the inrow[] window, rotating the buffer that now holds + the "previous" row to use it for the new "next" row. + */ + shuffle = inrow[0]; + + inrow[0] = inrow[1]; + inrow[1] = inrow[2]; + inrow[2] = shuffle ; + if (row+1 < rows) { + /* Read the "next" row in from the file. Allocate buffer if needed */ + if (inrow[2] == NULL) + inrow[2] = pbm_allocrow(cols); + pbm_readpbmrow(ifd, inrow[2], cols, format); + } else { + /* There is no next row */ + if (inrow[2]) { + pbm_freerow(inrow[2]); + inrow[2] = NULL; + } + } +} + + + +static unsigned int +likeNeighbors(bit * const inrow[3], + unsigned int const col, + unsigned int const cols) { + + int const point = inrow[THISROW][col]; + enum compass_heading heading; + int joined; + + joined = 0; /* initial value */ + for (heading = WEST; heading <= SOUTHWEST; ++heading) { + int x = col + xd[heading] ; + int y = THISROW + yd[heading] ; + if (x < 0 || x >= cols || !inrow[y]) { + if (point == PBM_WHITE) joined++; + } else if (inrow[y][x] == point) joined++ ; + } + return joined; +} + + + +int +main(int argc, char *argv[]) { + + struct cmdlineInfo cmdline; + FILE *ifp; + bit *outrow; + int cols, rows, format; + unsigned int row; + unsigned int nFlipped; /* Number of pixels we have flipped so far */ + + pbm_init( &argc, argv ); + + parseCommandLine(argc, argv, &cmdline); + + ifp = pm_openr(cmdline.inputFilespec); + + inrow[0] = inrow[1] = inrow[2] = NULL; + pbm_readpbminit(ifp, &cols, &rows, &format); + + outrow = pbm_allocrow(cols); + + pbm_writepbminit(stdout, cols, rows, 0) ; + + nFlipped = 0; /* No pixels flipped yet */ + for (row = 0; row < rows; ++row) { + unsigned int col; + nextrow(ifp, row, cols, rows, format); + for (col = 0; col < cols; ++col) { + bit const thispoint = inrow[THISROW][col]; + if ((cmdline.flipWhite && thispoint == PBM_WHITE) || + (cmdline.flipBlack && thispoint == PBM_BLACK)) { + if (likeNeighbors(inrow, col, cols) < cmdline.connect) { + outrow[col] = PBM_INVERT(thispoint); + ++nFlipped; + } else + outrow[col] = thispoint; + } else + outrow[col] = thispoint; + } + pbm_writepbmrow(stdout, outrow, cols, 0) ; + } + pbm_freerow(outrow); + pm_close(ifp); + + if (cmdline.verbose) + pm_message("%d pixels flipped", nFlipped); + + return 0; +} diff --git a/editor/pbmlife.c b/editor/pbmlife.c new file mode 100644 index 00000000..be34cc69 --- /dev/null +++ b/editor/pbmlife.c @@ -0,0 +1,114 @@ +/* pbmlife.c - read a portable bitmap and apply Conway's rules of Life to it +** +** Copyright (C) 1988,1 1991 by Jef Poskanzer. +** +** 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. +*/ + +#include "pbm.h" + +int +main( argc, argv ) +int argc; +char* argv[]; + { + FILE* ifp; + bit* prevrow; + bit* thisrow; + bit* nextrow; + bit* temprow; + register bit* newrow; + int rows, cols, row; + register int col, count; + int format; + + + pbm_init( &argc, argv ); + + if ( argc > 2 ) + pm_usage( "[pbmfile]" ); + + if ( argc == 2 ) + ifp = pm_openr( argv[1] ); + else + ifp = stdin; + + pbm_readpbminit( ifp, &cols, &rows, &format ); + prevrow = pbm_allocrow( cols ); + thisrow = pbm_allocrow( cols ); + nextrow = pbm_allocrow( cols ); + + pbm_writepbminit( stdout, cols, rows, 0 ); + newrow = pbm_allocrow( cols ); + + pbm_readpbmrow( ifp, nextrow, cols, format ); + + for ( row = 0; row < rows; ++row ) + { + temprow = prevrow; + prevrow = thisrow; + thisrow = nextrow; + nextrow = temprow; + if ( row < rows - 1 ) + pbm_readpbmrow( ifp, nextrow, cols, format ); + + for ( col = 0; col < cols; ++col ) + { + /* Check the neighborhood, with an unrolled double loop. */ + count = 0; + if ( row > 0 ) + { + /* upper left */ + if ( col > 0 && prevrow[col - 1] == PBM_WHITE ) + ++count; + /* upper center */ + if ( prevrow[col] == PBM_WHITE ) + ++count; + /* upper right */ + if ( col < cols - 1 && prevrow[col + 1] == PBM_WHITE ) + ++count; + } + /* left */ + if ( col > 0 && thisrow[col - 1] == PBM_WHITE ) + ++count; + /* right */ + if ( col < cols - 1 && thisrow[col + 1] == PBM_WHITE ) + ++count; + if ( row < rows - 1 ) + { + /* lower left */ + if ( col > 0 && nextrow[col - 1] == PBM_WHITE ) + ++count; + /* lower center */ + if ( nextrow[col] == PBM_WHITE ) + ++count; + /* lower right */ + if ( col < cols - 1 && nextrow[col + 1] == PBM_WHITE ) + ++count; + } + + /* And compute the new value. */ + if ( thisrow[col] == PBM_WHITE ) + if ( count == 2 || count == 3 ) + newrow[col] = PBM_WHITE; + else + newrow[col] = PBM_BLACK; + else + if ( count == 3 ) + newrow[col] = PBM_WHITE; + else + newrow[col] = PBM_BLACK; + } + pbm_writepbmrow( stdout, newrow, cols, 0 ); + } + + pm_close( ifp ); + pm_close( stdout ); + + exit( 0 ); + } diff --git a/editor/pbmmask.c b/editor/pbmmask.c new file mode 100644 index 00000000..21ada6b9 --- /dev/null +++ b/editor/pbmmask.c @@ -0,0 +1,222 @@ +/* pbmmask.c - create a mask bitmap from a portable bitmap +** +** Copyright (C) 1989, 1991 by Jef Poskanzer. +** +** 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. +*/ + +#include "pbm.h" +#include "mallocvar.h" + +static bit ** bits; +static bit ** mask; +static bit backcolor; +static int rows, cols; + + + +static short * fcols; +static short * frows; +static int fstacksize = 0; +static int fstackp = 0; + + + +static void +addflood(int const col, + int const row) { + + if ( bits[row][col] == backcolor && mask[row][col] == PBM_BLACK ) { + if ( fstackp >= fstacksize ) { + if ( fstacksize == 0 ) { + fstacksize = 1000; + MALLOCARRAY(fcols, fstacksize); + MALLOCARRAY(frows, fstacksize); + if ( fcols == NULL || frows == NULL ) + pm_error( "out of memory" ); + } else { + fstacksize *= 2; + fcols = (short*) realloc( + (char*) fcols, fstacksize * sizeof(short) ); + frows = (short*) realloc( + (char*) frows, fstacksize * sizeof(short) ); + if ( fcols == (short*) 0 || frows == (short*) 0 ) + pm_error( "out of memory" ); + } + } + fcols[fstackp] = col; + frows[fstackp] = row; + ++fstackp; + } +} + + + +static void +flood(void) { + + while ( fstackp > 0 ) { + int col, row; + --fstackp; + col = fcols[fstackp]; + row = frows[fstackp]; + if ( bits[row][col] == backcolor && mask[row][col] == PBM_BLACK ) { + int c; + mask[row][col] = PBM_WHITE; + if ( row - 1 >= 0 ) + addflood( col, row - 1 ); + if ( row + 1 < rows ) + addflood( col, row + 1 ); + for ( c = col + 1; c < cols; ++c ) { + if ( bits[row][c] == backcolor && mask[row][c] == PBM_BLACK ) { + mask[row][c] = PBM_WHITE; + if ( row - 1 >= 0 && + ( bits[row - 1][c - 1] != backcolor || + mask[row - 1][c - 1] != PBM_BLACK ) ) + addflood( c, row - 1 ); + if ( row + 1 < rows && + ( bits[row + 1][c - 1] != backcolor || + mask[row + 1][c - 1] != PBM_BLACK ) ) + addflood( c, row + 1 ); + } + else + break; + } + for ( c = col - 1; c >= 0; --c ) { + if ( bits[row][c] == backcolor && mask[row][c] == PBM_BLACK ) { + mask[row][c] = PBM_WHITE; + if ( row - 1 >= 0 && + ( bits[row - 1][c + 1] != backcolor || + mask[row - 1][c + 1] != PBM_BLACK ) ) + addflood( c, row - 1 ); + if ( row + 1 < rows && + ( bits[row + 1][c + 1] != backcolor || + mask[row + 1][c + 1] != PBM_BLACK ) ) + addflood( c, row + 1 ); + } else + break; + } + } + } +} + + + +int +main(int argc, char * argv[]) { + + FILE* ifp; + int argn, expand, wcount; + register int row, col; + const char* const usage = "[-expand] [pbmfile]"; + + pbm_init( &argc, argv ); + + argn = 1; + expand = 0; + + if ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' ) + { + if ( pm_keymatch( argv[argn], "-expand", 2 ) ) + expand = 1; + else if ( pm_keymatch( argv[argn], "-noexpand", 2 ) ) + expand = 0; + else + pm_usage( usage ); + ++argn; + } + + if ( argn == argc ) + ifp = stdin; + else + { + ifp = pm_openr( argv[argn] ); + ++argn; + } + + if ( argn != argc ) + pm_usage( usage ); + + bits = pbm_readpbm( ifp, &cols, &rows ); + pm_close( ifp ); + mask = pbm_allocarray( cols, rows ); + + /* Clear out the mask. */ + for ( row = 0; row < rows; ++row ) + for ( col = 0; col < cols; ++col ) + mask[row][col] = PBM_BLACK; + + /* Figure out the background color, by counting along the edge. */ + wcount = 0; + for ( row = 0; row < rows; ++row ) { + if ( bits[row][0] == PBM_WHITE ) + ++wcount; + if ( bits[row][cols - 1] == PBM_WHITE ) + ++wcount; + } + for ( col = 1; col < cols - 1; ++col ) { + if ( bits[0][col] == PBM_WHITE ) + ++wcount; + if ( bits[rows - 1][col] == PBM_WHITE ) + ++wcount; + } + if ( wcount >= rows + cols - 2 ) + backcolor = PBM_WHITE; + else + backcolor = PBM_BLACK; + + /* Flood the entire edge. Probably the first call will be enough, but + might as well be sure. + */ + for ( col = cols - 3; col >= 2; col -= 2 ) { + addflood( col, rows - 1 ); + addflood( col, 0 ); + } + for ( row = rows - 1; row >= 0; row -= 2 ) { + addflood( cols - 1, row ); + addflood( 0, row ); + } + flood( ); + + if ( ! expand ) + /* Done. */ + pbm_writepbm( stdout, mask, cols, rows, 0 ); + else { + /* Expand by one pixel. */ + int srow, scol; + unsigned int row; + bit ** emask; + + emask = pbm_allocarray( cols, rows ); + + for ( row = 0; row < rows; ++row ) { + unsigned int col; + for ( col = 0; col < cols; ++col ) + if ( mask[row][col] == PBM_BLACK ) + emask[row][col] = PBM_BLACK; + else { + emask[row][col] = PBM_WHITE; + for ( srow = row - 1; srow <= row + 1; ++srow ) + for ( scol = col - 1; scol <= col + 1; ++scol ) + if ( srow >= 0 && srow < rows && + scol >= 0 && scol < cols && + mask[srow][scol] == PBM_BLACK ) { + + emask[row][col] = PBM_BLACK; + break; + } + } + } + pbm_writepbm( stdout, emask, cols, rows, 0 ); + } + + pm_close( stdout ); + + return 0; +} + diff --git a/editor/pbmpscale.c b/editor/pbmpscale.c new file mode 100644 index 00000000..63f203ed --- /dev/null +++ b/editor/pbmpscale.c @@ -0,0 +1,199 @@ +/* pbmpscale.c - pixel scaling with jagged edge smoothing. + * AJCD 13/8/90 + */ + +#include <stdio.h> +#include "pbm.h" +#include "mallocvar.h" + +/* prototypes */ +void nextrow_pscale ARGS((FILE *ifd, int row)); +int corner ARGS((int pat)); + +/* input bitmap size and storage */ +int rows, columns, format ; +bit *inrow[3] ; + +#define thisrow (1) + +/* compass directions from west clockwise */ +int xd_pscale[] = { -1, -1, 0, 1, 1, 1, 0, -1 } ; +int yd_pscale[] = { 0, -1, -1, -1, 0, 1, 1, 1 } ; + +/* starting positions for corners */ +#define NE(f) ((f) & 3) +#define SE(f) (((f) >> 2) & 3) +#define SW(f) (((f) >> 4) & 3) +#define NW(f) (((f) >> 6) & 3) + +typedef unsigned short sixteenbits ; + +/* list of corner patterns; bit 7 is current color, bits 0-6 are squares + * around (excluding square behind), going clockwise. + * The high byte of the patterns is a mask, which determines which bits are + * not ignored. + */ + +sixteenbits patterns[] = { 0x0000, 0xd555, /* no corner */ + 0x0001, 0xffc1, 0xd514, /* normal corner */ + 0x0002, 0xd554, 0xd515, /* reduced corners */ + 0xbea2, 0xdfc0, 0xfd81, + 0xfd80, 0xdf80, + 0x0003, 0xbfa1, 0xfec2 /* reduced if > 1 */ + }; + +/* search for corner patterns, return type of corner found: + * 0 = no corner, + * 1 = normal corner, + * 2 = reduced corner, + * 3 = reduced if cutoff > 1 + */ + +int corner(pat) + int pat; +{ + register int i, r=0; + for (i = 0; i < sizeof(patterns)/sizeof(sixteenbits); i++) + if (patterns[i] < 0x100) + r = patterns[i]; + else if ((pat & (patterns[i] >> 8)) == + (patterns[i] & (patterns[i] >> 8))) + return r; + return 0; +} + +/* get a new row + */ + +void nextrow_pscale(ifd, row) + FILE *ifd; + int row; +{ + bit *shuffle = inrow[0] ; + inrow[0] = inrow[1]; + inrow[1] = inrow[2]; + inrow[2] = shuffle ; + if (row < rows) { + if (shuffle == NULL) + inrow[2] = shuffle = pbm_allocrow(columns); + pbm_readpbmrow(ifd, inrow[2], columns, format) ; + } else inrow[2] = NULL; /* discard storage */ + +} + +int +main(argc, argv) + int argc; + char *argv[]; +{ + FILE *ifd; + register bit *outrow; + register int row, col, i, k; + int scale, cutoff, ucutoff ; + unsigned char *flags; + + pbm_init( &argc, argv ); + + if (argc < 2) + pm_usage("scale [pbmfile]"); + + scale = atoi(argv[1]); + if (scale < 1) + pm_perror("bad scale (< 1)"); + + if (argc == 3) + ifd = pm_openr(argv[2]); + else + ifd = stdin ; + + inrow[0] = inrow[1] = inrow[2] = NULL; + pbm_readpbminit(ifd, &columns, &rows, &format) ; + + outrow = pbm_allocrow(columns*scale) ; + MALLOCARRAY(flags, columns); + if (flags == NULL) + pm_error("out of memory") ; + + pbm_writepbminit(stdout, columns*scale, rows*scale, 0) ; + + cutoff = scale / 2; + ucutoff = scale - 1 - cutoff; + nextrow_pscale(ifd, 0); + for (row = 0; row < rows; row++) { + nextrow_pscale(ifd, row+1); + for (col = 0; col < columns; col++) { + flags[col] = 0 ; + for (i = 0; i != 8; i += 2) { + int vec = inrow[thisrow][col] != PBM_WHITE; + for (k = 0; k < 7; k++) { + int x = col + xd_pscale[(k+i)&7] ; + int y = thisrow + yd_pscale[(k+i)&7] ; + vec <<= 1; + if (x >=0 && x < columns && inrow[y]) + vec |= (inrow[y][x] != PBM_WHITE) ; + } + flags[col] |= corner(vec)<<i ; + } + } + for (i = 0; i < scale; i++) { + bit *ptr = outrow ; + int zone = (i > ucutoff) - (i < cutoff) ; + int cut = (zone < 0) ? (cutoff - i) : + (zone > 0) ? (i - ucutoff) : 0 ; + + for (col = 0; col < columns; col++) { + int pix = inrow[thisrow][col] ; + int flag = flags[col] ; + int cutl, cutr ; + + switch (zone) { + case -1: + switch (NW(flag)) { + case 0: cutl = 0; break; + case 1: cutl = cut; break; + case 2: cutl = cut ? cut-1 : 0; break; + case 3: cutl = (cut && cutoff > 1) ? cut-1 : cut; break; + default: cutl = 0; /* Should never reach here */ + } + switch (NE(flag)) { + case 0: cutr = 0; break; + case 1: cutr = cut; break; + case 2: cutr = cut ? cut-1 : 0; break; + case 3: cutr = (cut && cutoff > 1) ? cut-1 : cut; break; + default: cutr = 0; /* Should never reach here */ + } + break; + case 0: + cutl = cutr = 0; + break ; + case 1: + switch (SW(flag)) { + case 0: cutl = 0; break; + case 1: cutl = cut; break; + case 2: cutl = cut ? cut-1 : 0; break; + case 3: cutl = (cut && cutoff > 1) ? cut-1 : cut; break; + default: cutl = 0; /* should never reach here */ + } + switch (SE(flag)) { + case 0: cutr = 0; break; + case 1: cutr = cut; break; + case 2: cutr = cut ? cut-1 : 0; break; + case 3: cutr = (cut && cutoff > 1) ? cut-1 : cut; break; + default: cutr = 0; /* should never reach here */ + } + break; + default: cutl = 0; cutr = 0; /* Should never reach here */ + } + for (k = 0; k < cutl; k++) /* left part */ + *ptr++ = !pix ; + for (k = 0; k < scale-cutl-cutr; k++) /* centre part */ + *ptr++ = pix ; + for (k = 0; k < cutr; k++) /* right part */ + *ptr++ = !pix ; + } + pbm_writepbmrow(stdout, outrow, scale*columns, 0) ; + } + } + pm_close(ifd); + exit(0); +} diff --git a/editor/pbmreduce.c b/editor/pbmreduce.c new file mode 100644 index 00000000..15ec2a1b --- /dev/null +++ b/editor/pbmreduce.c @@ -0,0 +1,208 @@ +/* pbmreduce.c - read a portable bitmap and reduce it N times +** +** Copyright (C) 1989 by Jef Poskanzer. +** +** 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. +*/ + +#include "pbm.h" +#include "mallocvar.h" + +int +main( argc, argv ) + int argc; + char* argv[]; + { + FILE* ifp; + register bit** bitslice; + register bit* newbitrow; + register bit* nbP; + int argn, n, rows, cols, format, newrows, newcols; + int row, col, limitcol, subrow, subcol, count, direction; + const char* const usage = "[-floyd|-fs | -threshold] [-value <val>] N [pbmfile]"; + int halftone; +#define QT_FS 1 +#define QT_THRESH 2 +#define SCALE 1024 +#define HALFSCALE 512 + long threshval, sum; + long* thiserr; /* used for Floyd-Steinberg stuff */ + long* nexterr; /* used for Floyd-Steinberg stuff */ + long* temperr; + + + pbm_init( &argc, argv ); + + argn = 1; + halftone = QT_FS; + threshval = HALFSCALE; + + while ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' ) + { + if ( pm_keymatch( argv[argn], "-fs", 2 ) || + pm_keymatch( argv[argn], "-floyd", 2 ) ) + halftone = QT_FS; + else if ( pm_keymatch( argv[argn], "-threshold", 2 ) ) + halftone = QT_THRESH; + else if ( pm_keymatch( argv[argn], "-value", 2 ) ) + { + float f; + + ++argn; + if ( argn == argc || sscanf( argv[argn], "%f", &f ) != 1 || + f < 0.0 || f > 1.0 ) + pm_usage( usage ); + threshval = f * SCALE; + } + else + pm_usage( usage ); + ++argn; + } + + if ( argn == argc ) + pm_usage( usage ); + if ( sscanf( argv[argn], "%d", &n ) != 1 ) + pm_usage( usage ); + if ( n < 2 ) + pm_error( "N must be greater than 1" ); + ++argn; + + if ( argn == argc ) + ifp = stdin; + else + { + ifp = pm_openr( argv[argn] ); + ++argn; + } + + if ( argn != argc ) + pm_usage( usage ); + + pbm_readpbminit( ifp, &cols, &rows, &format ); + bitslice = pbm_allocarray( cols, n ); + + newrows = rows / n; + newcols = cols / n; + pbm_writepbminit( stdout, newcols, newrows, 0 ); + newbitrow = pbm_allocrow( newcols ); + + if ( halftone == QT_FS ) { + /* Initialize Floyd-Steinberg. */ + MALLOCARRAY(thiserr, newcols + 2); + MALLOCARRAY(nexterr, newcols + 2); + if ( thiserr == NULL || nexterr == NULL ) + pm_error( "out of memory" ); + + srand( (int) ( time( 0 ) ^ getpid( ) ) ); + for ( col = 0; col < newcols + 2; ++col ) + thiserr[col] = ( rand( ) % SCALE - HALFSCALE ) / 4; + /* (random errors in [-SCALE/8 .. SCALE/8]) */ + } else { + /* These variables are meaningless in this case, and the values + should never be used. + */ + thiserr = NULL; + nexterr = NULL; + } + direction = 1; + + for ( row = 0; row < newrows; ++row ) + { + for ( subrow = 0; subrow < n; ++subrow ) + pbm_readpbmrow( ifp, bitslice[subrow], cols, format ); + + if ( halftone == QT_FS ) + for ( col = 0; col < newcols + 2; ++col ) + nexterr[col] = 0; + if ( direction ) + { + col = 0; + limitcol = newcols; + nbP = newbitrow; + } + else + { + col = newcols - 1; + limitcol = -1; + nbP = &(newbitrow[col]); + } + + do + { + sum = 0; + count = 0; + for ( subrow = 0; subrow < n; ++subrow ) + for ( subcol = 0; subcol < n; ++subcol ) + if ( row * n + subrow < rows && col * n + subcol < cols ) + { + count += 1; + if ( bitslice[subrow][col * n + subcol] == PBM_WHITE ) + sum += 1; + } + sum = ( sum * SCALE ) / count; + + if ( halftone == QT_FS ) + sum += thiserr[col + 1]; + + if ( sum >= threshval ) + { + *nbP = PBM_WHITE; + if ( halftone == QT_FS ) + sum = sum - threshval - HALFSCALE; + } + else + *nbP = PBM_BLACK; + + if ( halftone == QT_FS ) + { + if ( direction ) + { + thiserr[col + 2] += ( sum * 7 ) / 16; + nexterr[col ] += ( sum * 3 ) / 16; + nexterr[col + 1] += ( sum * 5 ) / 16; + nexterr[col + 2] += ( sum ) / 16; + } + else + { + thiserr[col ] += ( sum * 7 ) / 16; + nexterr[col + 2] += ( sum * 3 ) / 16; + nexterr[col + 1] += ( sum * 5 ) / 16; + nexterr[col ] += ( sum ) / 16; + } + } + if ( direction ) + { + ++col; + ++nbP; + } + else + { + --col; + --nbP; + } + } + while ( col != limitcol ); + + pbm_writepbmrow( stdout, newbitrow, newcols, 0 ); + + if ( halftone == QT_FS ) + { + temperr = thiserr; + thiserr = nexterr; + nexterr = temperr; + direction = ! direction; + } + } + + pm_close( ifp ); + pm_close( stdout ); + + exit( 0 ); + } + + diff --git a/editor/pgmabel.c b/editor/pgmabel.c new file mode 100644 index 00000000..4914c4be --- /dev/null +++ b/editor/pgmabel.c @@ -0,0 +1,316 @@ +/* pgmabel.c - read a portable graymap and making the deconvolution +** +** Deconvolution of an axial-symmetric image of an rotation symmetrical +** process by solving the linear equation system with y-Axis as +** symmetry-line +** +** Copyright (C) 1997-2006 by German Aerospace Research establishment +** +** Author: Volker Schmidt +** lefti@voyager.boerde.de +** +** 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. +** +** $HISTORY: +** +** 24 Jan 2002 : 001.009 : some optimzization +** 22 Jan 2002 : 001.008 : some stupid calculations changed +** 08 Aug 2001 : 001.007 : new usage (netpbm-conform) +** 27 Jul 1998 : 001.006 : First try of error correction +** 26 Mar 1998 : 001.005 : Calculating the dl's before transformation +** 06 Feb 1998 : 001.004 : Include of a logo in the upper left edge +** 26 Nov 1997 : 001.003 : Some bug fixes and reading only lines +** 25 Nov 1997 : 001.002 : include of pixsize for getting scale invariant +** 03 Sep 1997 : 001.001 : only define for PID2 +** 03 Sep 1997 : 001.000 : First public release +** 21 Aug 1997 : 000.909 : Recalculate the streching-factor +** 20 Aug 1997 : 000.908 : -left and -right for calculating only one side +** 20 Aug 1997 : 000.906 : correction of divisor, include of -factor +** 15 Aug 1997 : 000.905 : Include of -help and -axis +*/ + +static const char* const version="$VER: pgmabel 1.009 (24 Jan 2002)"; + +#include <math.h> +#include <stdlib.h> /* for calloc */ +#include "pgm.h" +#include "mallocvar.h" + +#ifndef PID2 /* PI/2 (on AMIGA always defined) */ +#define PID2 1.57079632679489661923 +#endif + +#define TRUE 1 +#define FALSE 0 + +/* some global variables */ +static double *aldl, *ardl; /* pointer for weighting factors */ + +/* ---------------------------------------------------------------------------- +** procedure for calculating the sum of the calculated surfaces with the +** weight of the surface +** n <- index of end point of the summation +** N <- width of the calculated row +** xr <- array of the calculated elements of the row +** adl <- pre-calculated surface coefficient for each segment +*/ +static double +Sum ( int n, double *xr, int N, double *adl) +{ + int k; + double result=0.0; + + if (n==0) return(0.0); /* outer ring is 0 per definition */ + for (k=0 ; k<=(n-1) ; k++) + { + result += xr[k] * ( adl[k*N+n] - adl[(k+1)*N+n]); +/* result += xr[k] * ( dr(k,n+0.5,N) - dr(k+1,n+0.5,N)); */ + } + return(result); +} + +/* ---------------------------------------------------------------------------- +** procedure for calculating the surface coefficient for the Integration +** R, N <- indizes of the coefficient +** r <- radial position of the center of the surface +*/ +static double +dr ( int R, double r, int N) +{ + double a; + double b; + a=(double) N-R ; + b=(double) N-r ; + return(sqrt(a*a-b*b)); +} + +/* ---------------------------------------------------------------------------- +** procedure for making the Abel integration for deconvolution of the image +** y <-> array with values for deconvolution and results +** N <- width of the array +** adl <- array with pre-calculated weighting factors +*/ +static void +abel ( float *y, int N, double *adl) +{ + register int n; + double *rho, *rhop; /* results and new index */ + float *yp; /* new indizes for the y-array */ + + MALLOCARRAY(rho, N); + if( !rho ) + pm_error( "out of memory" ); + rhop = rho; + yp = y; + + for (n=0 ; n<N ; n++) + { + *(rhop++) = ((*yp++) - Sum(n,rho,N,adl))/(adl[n*N+n]); +/* *(rhop++) = ((*yp++) - Sum(n,rho,N))/(dr(n,n+0.5,N)); old version */ + if ( *rhop < 0.0 ) *rhop = 0.0; /* error correction ! */ +/* if (n > 2) rhop[n-1] = (rho[n-2]+rho[n-1]+rho[n])/3.0; stabilization*/ + } + for (n=0 ; n<N ; n++) + { + if (( n>=1 )&&( n<N-1 )) + (*y++) = ((rho[n-1]*0.5+rho[n]+rho[n+1]*0.5)/2.0);/*1D median filter*/ + else (*y++) = rho[n]; + } + free(rho); +} + +/* ---------------------------------------------------------------------------- +** printing a help message if Option -h(elp) is chosen +*/ +static void +help() +{ + pm_message("-----------------------------------------------------------------"); + pm_message("| pgmabel |"); + pm_message("| make a deconvolution with vertical axis as symmetry-line |"); + pm_message("| usage: |"); + pm_message("| pgmabel [-help] [-axis N] [-factor N] [-left|-right] |"); + pm_message("| [-pixsize] [-verbose] [pgmfile] |"); + pm_message("| axis : horizontal position of the axis |"); + pm_message("| factor : user defines stretch-factor for the gray levels |"); + pm_message("| pixsize : size of one pixel in mm (default = 0.1) |"); + pm_message("| left : calculating only the left (or right) side |"); + pm_message("| verbose : output of useful data |"); + pm_message("| pgmfile : Name of a pgmfile (optional) |"); + pm_message("| |"); + pm_message("| for further information please contact the manpage |"); + pm_message("-----------------------------------------------------------------"); + pm_message("%s",version); /* telling the version */ + exit(-1); /* retur-code for no result */ +} + + + + + +/* ---------------------------------------------------------------------------- +** main program +*/ +int main( argc, argv ) + int argc; + char* argv[]; +{ + FILE* ifp; + gray maxval; /* maximum gray-level */ + gray* grayorig; + gray* grayrow; /* one line in the image */ + int argn, rows, cols, row, format; + int col, midcol=0, temp, tc; + float *trow; /* temporary row for deconvolution */ + float l_div, r_div, fac=1.0, cfac=4.0; /* factor for scaling gray-level */ + float pixsize=0.1; + /* no verbose, calculating both sides */ + int verb = FALSE, left = TRUE, right = TRUE; + int nologo = FALSE; + const char* const usage = "[-help] [-axis N] [-factor N] [-pixsize N] [-left|-right] [-verbose] [pgmfile]"; + + pgm_init( &argc, argv ); + argn = 1; + + /* Check for flags. */ + while ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' ) + { + if ( pm_keymatch( argv[argn], "-help", 1 ) ) help(); + else if ( pm_keymatch( argv[argn], "-axis", 1 ) ) + { + ++argn; + if ( argn == argc || sscanf( argv[argn], "%i", &midcol ) !=1 ) + pm_usage( usage ); + } + else if ( pm_keymatch( argv[argn], "-factor", 1 ) ) + { + ++argn; + if ( argn == argc || sscanf( argv[argn], "%f", &fac ) !=1 ) + pm_usage( usage ); + } + else if ( pm_keymatch( argv[argn], "-pixsize", 1 ) ) + { + ++argn; + if ( argn == argc || sscanf( argv[argn], "%f", &pixsize ) !=1 ) + pm_usage( usage ); + } + else if ( pm_keymatch( argv[argn], "-verbose", 1 ) ) + { + verb = TRUE; + } + else if ( pm_keymatch( argv[argn], "-left", 1 ) ) + { + if ( left ) right = FALSE; + else pm_usage( usage ); + } + else if ( pm_keymatch( argv[argn], "-right", 1 ) ) + { + if ( right ) left = FALSE; + else pm_usage( usage ); + } + else if ( pm_keymatch( argv[argn], "-nologo", 4 ) ) + { + nologo = TRUE; + } + else + pm_usage( usage ); + ++ argn; + } + if ( argn < argc ) + { + ifp = pm_openr( argv[argn] ); /* open the picture */ + ++argn; + } + else + ifp = stdin; /* or reading from STDIN */ + if ( argn != argc ) + pm_usage( usage ); + + pgm_readpgminit( ifp, &cols, &rows, &maxval, &format ); /* read picture */ + pgm_writepgminit( stdout, cols, rows, maxval, 0 ); /* write the header */ + grayorig = pgm_allocrow(cols); + grayrow = pgm_allocrow( cols ); /* allocate a row */ + + if (midcol == 0) midcol = cols/2; /* if no axis set take the center */ + if (left ) l_div = (float)(PID2*pixsize)/(cfac*fac); + else l_div=1.0; /* weighting the left side */ + if (right) r_div = (float)(PID2*pixsize)/(cfac*fac); + else r_div=1.0; /* weighting the right side */ + + if (verb) + { + pm_message("%s",version); + pm_message("Calculating a portable graymap with %i rows and %i cols",rows,cols); + pm_message(" resuming a pixelsize of %f mm",pixsize); + if ( !right ) pm_message(" only the left side!"); + if ( !left ) pm_message(" only the right side!"); + pm_message(" axis = %i, stretching factor = %f",midcol,cfac*fac); + if ( left ) pm_message(" left side weighting = %f",l_div); + if ( right ) pm_message(" right side weighting = %f",r_div); + } + + /* allocating the memory for the arrays aldl and ardl */ + aldl = calloc ( midcol*midcol, sizeof(double)); + if( !aldl ) + pm_error( "out of memory" ); + ardl = calloc ( (cols-midcol)*(cols-midcol), sizeof(double)); + if( !ardl ) + pm_error( "out of memory" ); + + MALLOCARRAY(trow, cols); + if( !trow ) + pm_error( "out of memory" ); + + /* now precalculating the weighting-factors for the abel-transformation */ + for (col = 0; col < midcol; ++col) /* factors for left side */ + { + for (tc = 0; tc < midcol; ++tc) aldl[col*midcol+tc] = dr(col,tc+0.5,midcol); + } + for (col = 0; col < (cols-midcol); ++col) /* factors for right side */ + { + for (tc = 0; tc < (cols-midcol); ++tc) + ardl[col*(cols-midcol)+tc] = dr(col,tc+0.5,cols-midcol); + } + + /* abel-transformation for each row splitted in right and left side */ + for ( row = 0; row < rows ; ++row ) + { + pgm_readpgmrow( ifp, grayorig, cols, maxval, format ); + for ( col = 0; col < midcol; ++col) /* left side */ + { + trow[col] = (float) (grayorig[col]); + } + if (left ) abel(trow, midcol, aldl); /* deconvolution */ + for ( col = 0; col < midcol; ++col) /* writing left side */ + { + temp = (int)(trow[col]/l_div); + grayrow[col] = (temp>0?temp:0); + } + for ( col = midcol; col < cols; ++col ) /* right side */ + { + trow[cols-col-1] = (float) (grayorig[col]); + } + if ( right ) abel(trow,(cols-midcol),ardl); /* deconvolution */ + for ( col = midcol; col < cols; ++col) /* writing right side */ + { + temp = (int)(trow[cols-col-1]/r_div); + temp = (temp>0?temp:0); + grayrow[col] = temp; + } + pgm_writepgmrow( stdout, grayrow, cols, maxval, 0 ); /* saving row */ + } + pm_close( ifp ); + pm_close( stdout ); /* closing output */ + free( trow ); /* deconvolution is done, clear memory */ + pgm_freerow( grayorig ); + pgm_freerow( grayrow ); + free(aldl); + free(ardl); /* all used memory freed (i hope) */ + exit( 0 ); /* end of procedure */ +} + diff --git a/editor/pgmbentley.c b/editor/pgmbentley.c new file mode 100644 index 00000000..9cc86a91 --- /dev/null +++ b/editor/pgmbentley.c @@ -0,0 +1,64 @@ +/* pgmbentley.c - read a portable graymap and smear it according to brightness +** +** Copyright (C) 1990 by Wilson Bent (whb@hoh-2.att.com) +** +** 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. +*/ + +#include <stdio.h> +#include "pgm.h" + +int +main( argc, argv ) + int argc; + char* argv[]; + { + FILE* ifp; + gray maxval; + gray** gin; + gray** gout; + int argn, rows, cols, row; + register int brow, col; + const char* const usage = "[pgmfile]"; + + + pgm_init( &argc, argv ); + + argn = 1; + + if ( argn < argc ) + { + ifp = pm_openr( argv[argn] ); + ++argn; + } + else + ifp = stdin; + + if ( argn != argc ) + pm_usage( usage ); + + gin = pgm_readpgm( ifp, &cols, &rows, &maxval ); + pm_close( ifp ); + gout = pgm_allocarray( cols, rows ); + +#define N 4 + for ( row = 0; row < rows; ++row ) + for ( col = 0; col < cols; ++col ) + { + brow = row + (int) (gin[row][col]) / N; + if ( brow >= rows ) + brow = rows - 1; + gout[brow][col] = gin[row][col]; + } + + pgm_writepgm( stdout, gout, cols, rows, maxval, 0 ); + pm_close( stdout ); + pgm_freearray( gout, rows ); + + exit( 0 ); + } diff --git a/editor/pgmdeshadow.c b/editor/pgmdeshadow.c new file mode 100644 index 00000000..482c6661 --- /dev/null +++ b/editor/pgmdeshadow.c @@ -0,0 +1,343 @@ +/*============================================================================ + pgmdeshadow +============================================================================== + Read PGM containing scanned black/white text, deshadow, write PGM. +============================================================================*/ +/* + This code is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this code; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +*/ + +/* + * Algorithm reference: Luc Vincent, "Morphological Grayscale Reruction + * in Image Analysis: Applications and Efficient Algorithms," IEEE + * Transactions on Image Processing, vol. 2, no. 2, April 1993, pp. 176-201. + * + * The algorithm used here is "fast hybrid grayscale reruction," + * described as follows on pp. 198-199: + * + * I: mask image (binary or grayscale) + * J: marker image, defined on domain D_I, J <= I. + * Reruction is determined directly in J. + * + * Scan D_I in raster order: + * Let p be the current pixel; + * J(p) <- (max{J(q),q member_of N_G_plus(p) union {p}}) ^ I(p) + * [Note that ^ here refers to "pointwise minimum.] + * + * Scan D_I in antiraster order: + * Let p be the current pixel; + * J(p) <- (max{J(q),q member_of N_G_minus(p) union {p}}) ^ I(p) + * [Note that ^ here refers to "pointwise minimum.] + * If there exists q member_of N_G_minus(p) such that J(q) < J(p) and + * J(q) < I(q), then fifo_add(p) + * + * Propagation step: + * While fifo_empty() is false + * p <- fifo_first() + * For every pixel q member_of N_G(p): + * If J(q) < J(p) and I(q) ~= J(q), then + * J(q) <- min{J(p),I(q)} + * fifo_add(q) + */ + +#include <stdio.h> + +#include "pm_c_util.h" +#include "mallocvar.h" +#include "shhopt.h" +#include "pgm.h" + + +struct cmdlineInfo { + const char * inputFileName; +}; + + + +static void +parseCommandLine(int argc, char ** 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; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ + opt.allowNegNum = FALSE; /* We may have parms that are negative numbers */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (argc-1 < 1) + cmdlineP->inputFileName = "-"; + else { + cmdlineP->inputFileName = argv[1]; + if (argc-1 > 1) + pm_error ("Too many arguments. The only argument is " + "the optional input file name"); + } +} + + + +static void +initializeDeshadowMarker(gray ** const inputPixels, + gray ** const markerPixels, + unsigned int const cols, + unsigned int const rows, + gray const maxval) { +/*---------------------------------------------------------------------------- + Fill the image with maxval and then copy 1-pixel-wide borders +-----------------------------------------------------------------------------*/ + { /* Make middle white */ + unsigned int row; + + for (row = 1; row < rows-1; ++row) { + unsigned int col; + for (col = 1; col < cols-1; ++col) + markerPixels[row][col] = maxval; + } + } + { /* Copy top edge */ + unsigned int col; + for (col = 0; col < cols; ++col) + markerPixels[0][col] = inputPixels[0][col]; + } + { /* Copy bottom edge */ + unsigned int col; + for (col = 0; col < cols; ++col) + markerPixels[rows-1][col] = inputPixels[rows-1][col]; + } + { /* Copy left edge */ + unsigned int row; + for (row = 0; row < rows; ++row) + markerPixels[row][0] = inputPixels[row][0]; + } + { /* Copy right edge */ + unsigned int row; + for (row = 0; row < rows; ++row) + markerPixels[row][cols-1] = inputPixels[row][cols-1]; + } +} + + + +static gray +min5(gray const a, + gray const b, + gray const c, + gray const d, + gray const e) { + + return MIN(a,MIN(b,MIN(c,MIN(d,e)))); +} + + + +static gray +minNortheastPixel(gray ** const pixels, + unsigned int const col, + unsigned int const row) { +/*---------------------------------------------------------------------------- + Return the minimum pixel value from among the immediate north-east + neighbors of (col, row) in pixels[][]. +-----------------------------------------------------------------------------*/ + return min5(pixels[row][col], + pixels[row][col-1], + pixels[row-1][col-1], + pixels[row-1][col], + pixels[row-1][col+1]); +} + + + +static gray +minSouthwestPixel(gray ** const pixels, + unsigned int const col, + unsigned int const row) { +/*---------------------------------------------------------------------------- + Return the minimum pixel value from among the immediate south-west + neighbors of (col, row) in pixels[][]. +-----------------------------------------------------------------------------*/ + return min5(pixels[row][col], + pixels[row][col+1], + pixels[row+1][col-1], + pixels[row+1][col], + pixels[row+1][col+1]); +} + + + +static void +estimateBackground(gray ** const inputPixels, + gray ** const markerPixels, + unsigned int const cols, + unsigned int const rows, + gray const maxval) { +/*---------------------------------------------------------------------------- + Update markerPixels[]. +-----------------------------------------------------------------------------*/ + unsigned int const passes = 2; + /* make only two passes since the image is not really complicated + (otherwise could go up to 10) + */ + + unsigned int pass; + bool stable; + + for (pass = 0, stable = FALSE; pass < passes && !stable; ++pass) { + int row; + + stable = TRUE; /* initial assumption */ + + /* scan in raster order */ + + for (row = 1; row < rows; ++row) { + unsigned int col; + for (col = 1; col < cols-1; ++col) { + gray const minpixel = + minNortheastPixel(markerPixels, col, row); + + if (minpixel > inputPixels[row][col]) { + markerPixels[row][col] = minpixel; + stable = FALSE; + } else + markerPixels[row][col] = inputPixels[row][col]; + } + } + /* scan in anti-raster order */ + + for (row = rows-2; row >= 0; --row) { + int col; + for (col = cols-2; col > 0; --col) { + gray const minpixel = + minSouthwestPixel(markerPixels, col, row); + + if (minpixel > inputPixels[row][col]) { + markerPixels[row][col] = minpixel; + stable = FALSE; + } else + markerPixels[row][col] = inputPixels[row][col]; + } + } + } +} + + + +static void +divide(gray ** const dividendPixels, + gray ** const divisorPixels, + unsigned int const cols, + unsigned int const rows, + gray const maxval) { +/*---------------------------------------------------------------------------- + Divide each pixel of dividendPixels[][] by the corresponding pixel + in divisorPixels[], replacing the dividendPixels[][] pixel with the + quotient. + + But leave a one-pixel border around dividendPixels[][] unmodified. + + Make sure the results are reasonable and not larger than maxval. +-----------------------------------------------------------------------------*/ + unsigned int row; + + for (row = 1; row < rows-1; ++row) { + unsigned int col; + for (col = 1; col < cols-1; ++col) { + gray const divisor = divisorPixels[row][col]; + gray const dividend = dividendPixels[row][col]; + + gray quotient; + + if (divisor == 0) + quotient = maxval; + else { + if (25 * divisor < 3 * maxval && 25 * dividend < 3 * maxval) + quotient = maxval; + else + quotient = + MIN(maxval, + maxval * (dividend + dividend/2) / divisor); + } + dividendPixels[row][col] = quotient; + } + } +} + + + +static void +deshadow(gray ** const inputPixels, + unsigned int const cols, + unsigned int const rows, + gray const maxval) { +/*---------------------------------------------------------------------------- + Deshadow the image described by inputPixels[], 'cols', 'rows', and + 'maxval'. (Modify inputPixels[][]). +-----------------------------------------------------------------------------*/ + gray ** markerPixels; + + markerPixels = pgm_allocarray(cols, rows); + + initializeDeshadowMarker(inputPixels, markerPixels, cols, rows, maxval); + + estimateBackground(inputPixels, markerPixels, cols, rows, maxval); + + divide(inputPixels, markerPixels, cols, rows, maxval); + + pgm_freearray(markerPixels, rows); +} + + + +int +main(int argc, char* argv[]) { + + struct cmdlineInfo cmdline; + FILE * ifP; + gray maxval; + gray ** pixels; + int cols, rows; + + pgm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFileName); + + pixels = pgm_readpgm(ifP, &cols, &rows, &maxval); + pm_close(ifP); + + deshadow(pixels, cols, rows, maxval); + + pgm_writepgm(stdout, pixels, cols, rows, maxval, 0); + + pgm_freearray(pixels, rows); + + return 0; +} diff --git a/editor/pgmenhance.c b/editor/pgmenhance.c new file mode 100644 index 00000000..83670568 --- /dev/null +++ b/editor/pgmenhance.c @@ -0,0 +1,112 @@ +/* pgmenhance.c - edge-enhance a portable graymap +** +** Copyright (C) 1989, 1991 by Jef Poskanzer. +** +** 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. +*/ + +#include "pgm.h" + +int +main(int argc, char * argv[] ) { + FILE* ifp; + gray* prevrow; + gray* thisrow; + gray* nextrow; + gray* temprow; + gray* newrow; + int argn, n, rows, cols, row, col; + float phi, omphi; + gray maxval; + int format; + const char* const usage = "[-N] [pgmfile] ( 1 <= N <= 9, default = 9 )"; + + pgm_init( &argc, argv ); + + argn = 1; + n = 9; + + if ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' ) { + if ( sscanf( &(argv[argn][1]), "%d", &n ) != 1 ) + pm_usage( usage ); + if ( n < 1 || n > 9 ) + pm_usage( usage ); + ++argn; + } + + if ( argn != argc ) { + ifp = pm_openr( argv[argn] ); + ++argn; + } else + ifp = stdin; + + if ( argn != argc ) + pm_usage( usage ); + + pgm_readpgminit( ifp, &cols, &rows, &maxval, &format ); + prevrow = pgm_allocrow( cols ); + thisrow = pgm_allocrow( cols ); + nextrow = pgm_allocrow( cols ); + + pgm_writepgminit( stdout, cols, rows, maxval, 0 ); + newrow = pgm_allocrow( cols ); + + /* The edge enhancing technique is taken from Philip R. Thompson's "xim" + ** program, which in turn took it from section 6 of "Digital Halftones by + ** Dot Diffusion", D. E. Knuth, ACM Transaction on Graphics Vol. 6, No. 4, + ** October 1987, which in turn got it from two 1976 papers by J. F. Jarvis + ** et. al. + */ + phi = n / 10.0; + omphi = 1.0 - phi; + + /* First row. */ + pgm_readpgmrow( ifp, thisrow, cols, maxval, format ); + pgm_writepgmrow( stdout, thisrow, cols, maxval, 0 ); + pgm_readpgmrow( ifp, nextrow, cols, maxval, format ); + + /* Other rows. */ + for ( row = 1; row < rows - 1; row++ ) { + temprow = prevrow; + prevrow = thisrow; + thisrow = nextrow; + nextrow = temprow; + pgm_readpgmrow( ifp, nextrow, cols, maxval, format ); + + newrow[0] = thisrow[0]; + for (col = 1; col < cols - 1; col++) { + /* Compute the sum of the neighborhood. */ + long sum, newval; + sum = + (long) prevrow[col-1] + (long) prevrow[col] + + (long) prevrow[col+1] + + (long) thisrow[col-1] + (long) thisrow[col] + + (long) thisrow[col+1] + + (long) nextrow[col-1] + (long) nextrow[col] + + (long) nextrow[col+1]; + /* Now figure new value. */ + newval = ( ( thisrow[col] - phi * sum / 9 ) / omphi + 0.5 ); + if ( newval < 0 ) + newrow[col] = 0; + else if ( newval > maxval ) + newrow[col] = maxval; + else + newrow[col] = newval; + } + newrow[cols - 1] = thisrow[cols - 1]; + pgm_writepgmrow( stdout, newrow, cols, maxval, 0 ); + } + pm_close( ifp ); + + /* Last row. */ + pgm_writepgmrow( stdout, nextrow, cols, maxval, 0 ); + + pm_close( stdout ); + + exit( 0 ); +} diff --git a/editor/pgmmedian.c b/editor/pgmmedian.c new file mode 100644 index 00000000..5878b1e7 --- /dev/null +++ b/editor/pgmmedian.c @@ -0,0 +1,462 @@ +/* +** Version 1.0 September 28, 1996 +** +** Copyright (C) 1996 by Mike Burns <burns@cac.psu.edu> +** +** Adapted to Netpbm 2005.08.10 by Bryan Henderson +** +** 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. +*/ + +/* References +** ---------- +** The select k'th value implementation is based on Algorithm 489 by +** Robert W. Floyd from the "Collected Algorithms from ACM" Volume II. +** +** The histogram sort is based is described in the paper "A Fast Two- +** Dimensional Median Filtering Algorithm" in "IEEE Transactions on +** Acoustics, Speech, and Signal Processing" Vol. ASSP-27, No. 1, February +** 1979. The algorithm I more closely followed is found in "Digital +** Image Processing Algorithms" by Ioannis Pitas. +*/ + + +#include "pgm.h" +#include "shhopt.h" +#include "mallocvar.h" +#include "nstring.h" + +enum medianMethod {MEDIAN_UNSPECIFIED, SELECT_MEDIAN, HISTOGRAM_SORT_MEDIAN}; +#define MAX_MEDIAN_TYPES 2 + +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 width; + unsigned int height; + unsigned int cutoff; + enum medianMethod type; +}; + + +/* Global variables common to each median sort routine. */ +static int const forceplain = 0; +static int format; +static gray maxval; +static gray **grays; +static gray *grayrow; +static gray **rowptr; +static int ccolso2, crowso2; +static int row; + + + +static void +parseCommandLine(int argc, char ** 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 widthSpec, heightSpec, cutoffSpec, typeSpec; + const char * type; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "width", OPT_UINT, &cmdlineP->width, + &widthSpec, 0); + OPTENT3(0, "height", OPT_UINT, &cmdlineP->height, + &heightSpec, 0); + OPTENT3(0, "cutoff", OPT_UINT, &cmdlineP->cutoff, + &cutoffSpec, 0); + OPTENT3(0, "type", OPT_STRING, &type, + &typeSpec, 0); + + + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ + opt.allowNegNum = FALSE; /* We may have parms that are negative numbers */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (!widthSpec) + cmdlineP->width = 3; + if (!heightSpec) + cmdlineP->height = 3; + if (!cutoffSpec) + cmdlineP->cutoff = 250; + + if (typeSpec) { + if (STREQ(type, "histogram_sort")) + cmdlineP->type = HISTOGRAM_SORT_MEDIAN; + else if (STREQ(type, "select")) + cmdlineP->type = SELECT_MEDIAN; + else + pm_error("Invalid value '%s' for -type. Valid values are " + "'histogram_sort' and 'select'", type); + } else + cmdlineP->type = MEDIAN_UNSPECIFIED; + + if (argc-1 < 1) + cmdlineP->inputFileName = "-"; + else { + cmdlineP->inputFileName = argv[1]; + if (argc-1 > 1) + pm_error ("Too many arguments. The only argument is " + "the optional input file name"); + } +} + + + +static void +select_489(gray * const a, + int * const parray, + int const n, + int const k) { + + gray t; + int i, j, l, r; + int ptmp, ttmp; + + l = 0; + r = n - 1; + while ( r > l ) { + t = a[parray[k]]; + ttmp = parray[k]; + i = l; + j = r; + ptmp = parray[l]; + parray[l] = parray[k]; + parray[k] = ptmp; + if ( a[parray[r]] > t ) { + ptmp = parray[r]; + parray[r] = parray[l]; + parray[l] = ptmp; + } + while ( i < j ) { + ptmp = parray[i]; + parray[i] = parray[j]; + parray[j] = ptmp; + ++i; + --j; + while ( a[parray[i]] < t ) + ++i; + while ( a[parray[j]] > t ) + --j; + } + if ( a[parray[l]] == t ) { + ptmp = parray[l]; + parray[l] = parray[j]; + parray[j] = ptmp; + } else { + ++j; + ptmp = parray[j]; + parray[j] = parray[r]; + parray[r] = ptmp; + } + if ( j <= k ) + l = j + 1; + if ( k <= j ) + r = j - 1; + } +} + + + +static void +select_median(FILE * const ifp, + int const ccols, + int const crows, + int const cols, + int const rows, + int const median) { + + int ccol, col; + int crow; + int rownum, irow, temprow; + gray *temprptr; + int i, leftcol; + int num_values; + gray *garray; + + int *parray; + int addcol; + int *subcol; + int tsum; + + /* Allocate storage for array of the current gray values. */ + garray = pgm_allocrow( crows * ccols ); + + num_values = crows * ccols; + + parray = (int *) pm_allocrow( crows * ccols, sizeof(int) ); + subcol = (int *) pm_allocrow( cols, sizeof(int) ); + + for ( i = 0; i < cols; ++i ) + subcol[i] = ( i - (ccolso2 + 1) ) % ccols; + + /* Apply median to main part of image. */ + for ( ; row < rows; ++row ) { + temprow = row % crows; + pgm_readpgmrow( ifp, grays[temprow], cols, maxval, format ); + + /* Rotate pointers to rows, so rows can be accessed in order. */ + temprow = ( row + 1 ) % crows; + rownum = 0; + for ( irow = temprow; irow < crows; ++rownum, ++irow ) + rowptr[rownum] = grays[irow]; + for ( irow = 0; irow < temprow; ++rownum, ++irow ) + rowptr[rownum] = grays[irow]; + + for ( col = 0; col < cols; ++col ) { + if ( col < ccolso2 || col >= cols - ccolso2 ) { + grayrow[col] = rowptr[crowso2][col]; + } else if ( col == ccolso2 ) { + leftcol = col - ccolso2; + i = 0; + for ( crow = 0; crow < crows; ++crow ) { + temprptr = rowptr[crow] + leftcol; + for ( ccol = 0; ccol < ccols; ++ccol ) { + garray[i] = *( temprptr + ccol ); + parray[i] = i; + ++i; + } + } + select_489( garray, parray, num_values, median ); + grayrow[col] = garray[parray[median]]; + } else { + addcol = col + ccolso2; + for (crow = 0, tsum = 0; crow < crows; ++crow, tsum += ccols) + garray[tsum + subcol[col]] = *(rowptr[crow] + addcol ); + select_489( garray, parray, num_values, median ); + grayrow[col] = garray[parray[median]]; + } + } + pgm_writepgmrow( stdout, grayrow, cols, maxval, forceplain ); + } + + /* Write out remaining unchanged rows. */ + for ( irow = crowso2 + 1; irow < crows; ++irow ) + pgm_writepgmrow( stdout, rowptr[irow], cols, maxval, forceplain ); + + pgm_freerow( garray ); + pm_freerow( (char *) parray ); + pm_freerow( (char *) subcol ); +} + + + +static void +histogram_sort_median(FILE * const ifp, + int const ccols, + int const crows, + int const cols, + int const rows, + int const median) { + + int const histmax = maxval + 1; + + int *hist; + int mdn, ltmdn; + gray *left_col, *right_col; + + hist = (int *) pm_allocrow( histmax, sizeof( int ) ); + left_col = pgm_allocrow( crows ); + right_col = pgm_allocrow( crows ); + + /* Apply median to main part of image. */ + for ( ; row < rows; ++row ) { + int col; + int temprow; + int rownum; + int irow; + int i; + /* initialize hist[] */ + for ( i = 0; i < histmax; ++i ) + hist[i] = 0; + + temprow = row % crows; + pgm_readpgmrow( ifp, grays[temprow], cols, maxval, format ); + + /* Rotate pointers to rows, so rows can be accessed in order. */ + temprow = ( row + 1 ) % crows; + rownum = 0; + for ( irow = temprow; irow < crows; ++rownum, ++irow ) + rowptr[rownum] = grays[irow]; + for ( irow = 0; irow < temprow; ++rownum, ++irow ) + rowptr[rownum] = grays[irow]; + + for ( col = 0; col < cols; ++col ) { + if ( col < ccolso2 || col >= cols - ccolso2 ) + grayrow[col] = rowptr[crowso2][col]; + else if ( col == ccolso2 ) { + int crow; + int const leftcol = col - ccolso2; + i = 0; + for ( crow = 0; crow < crows; ++crow ) { + int ccol; + gray * const temprptr = rowptr[crow] + leftcol; + for ( ccol = 0; ccol < ccols; ++ccol ) { + gray const g = *( temprptr + ccol ); + ++hist[g]; + ++i; + } + } + ltmdn = 0; + for ( mdn = 0; ltmdn <= median; ++mdn ) + ltmdn += hist[mdn]; + mdn--; + if ( ltmdn > median ) + ltmdn -= hist[mdn]; + + grayrow[col] = mdn; + } else { + int crow; + int const subcol = col - ( ccolso2 + 1 ); + int const addcol = col + ccolso2; + for ( crow = 0; crow < crows; ++crow ) { + left_col[crow] = *( rowptr[crow] + subcol ); + right_col[crow] = *( rowptr[crow] + addcol ); + } + for ( crow = 0; crow < crows; ++crow ) { + { + gray const g = left_col[crow]; + hist[(int) g]--; + if ( (int) g < mdn ) + ltmdn--; + } + { + gray const g = right_col[crow]; + hist[(int) g]++; + if ( (int) g < mdn ) + ltmdn++; + } + } + if ( ltmdn > median ) + do { + mdn--; + ltmdn -= hist[mdn]; + } while ( ltmdn > median ); + else { + /* This one change from Pitas algorithm can reduce run + ** time by up to 10%. + */ + while ( ltmdn <= median ) { + ltmdn += hist[mdn]; + mdn++; + } + mdn--; + if ( ltmdn > median ) + ltmdn -= hist[mdn]; + } + grayrow[col] = mdn; + } + } + pgm_writepgmrow( stdout, grayrow, cols, maxval, forceplain ); + } + + { + /* Write out remaining unchanged rows. */ + int irow; + for ( irow = crowso2 + 1; irow < crows; ++irow ) + pgm_writepgmrow( stdout, rowptr[irow], cols, maxval, forceplain ); + } + pm_freerow( (char *) hist ); + pgm_freerow( left_col ); + pgm_freerow( right_col ); +} + + + +int +main(int argc, + char * argv[]) { + + struct cmdlineInfo cmdline; + FILE * ifP; + int cols, rows; + int median; + enum medianMethod medianMethod; + + pgm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFileName); + + ccolso2 = cmdline.width / 2; + crowso2 = cmdline.height / 2; + + pgm_readpgminit(ifP, &cols, &rows, &maxval, &format); + pgm_writepgminit(stdout, cols, rows, maxval, forceplain); + + /* Allocate space for number of rows in mask size. */ + grays = pgm_allocarray(cols, cmdline.height); + grayrow = pgm_allocrow(cols); + + /* Allocate pointers to mask row buffer. */ + rowptr = pgm_allocarray(1, cmdline.height); + + /* Read in and write out initial rows that won't get changed. */ + for (row = 0; row < cmdline.height - 1; ++row) { + pgm_readpgmrow(ifP, grays[row], cols, maxval, format); + /* Write out the unchanged row. */ + if (row < crowso2) + pgm_writepgmrow(stdout, grays[row], cols, maxval, forceplain); + } + + median = (cmdline.height * cmdline.width) / 2; + + /* Choose which sort to run. */ + if (cmdline.type == MEDIAN_UNSPECIFIED) { + if ((maxval / ((cmdline.width * cmdline.height) - 1)) < cmdline.cutoff) + medianMethod = HISTOGRAM_SORT_MEDIAN; + else + medianMethod = SELECT_MEDIAN; + } else + medianMethod = cmdline.type; + + switch (medianMethod) { + case SELECT_MEDIAN: + select_median(ifP, cmdline.width, cmdline.height, cols, rows, median); + break; + + case HISTOGRAM_SORT_MEDIAN: + histogram_sort_median(ifP, cmdline.width, cmdline.height, + cols, rows, median); + break; + case MEDIAN_UNSPECIFIED: + pm_error("INTERNAL ERROR: median unspecified"); + } + + pm_close(ifP); + pm_close(stdout); + + pgm_freearray(grays, cmdline.height); + pgm_freerow(grayrow); + pgm_freearray(rowptr, cmdline.height); + + return 0; +} + + + + + + diff --git a/editor/pgmmorphconv.c b/editor/pgmmorphconv.c new file mode 100644 index 00000000..abc4e718 --- /dev/null +++ b/editor/pgmmorphconv.c @@ -0,0 +1,253 @@ +/* pgmmorphconv.c - morphological convolutions on a graymap: dilation and +** erosion +** +** Copyright (C) 2000 by Luuk van Dijk/Mind over Matter +** +** Based on +** pnmconvol.c - general MxN convolution on a portable anymap +** +** Copyright (C) 1989, 1991 by Jef Poskanzer. +** +** 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. +*/ + +#include "pm_c_util.h" +#include "pgm.h" + + +/************************************************************ + * Dilate + ************************************************************/ + +static int +dilate( bit** template, int trowso2, int tcolso2, + gray** in_image, gray** out_image, + int rows, int cols ){ + + int c, r, tc, tr; + int templatecount; + gray source; + + for( c=0; c<cols; ++c) + for( r=0; r<rows; ++r ) + out_image[r][c] = 0; /* only difference with erode is here and below */ + + /* + * for each non-black pixel of the template + * add in to out + */ + + templatecount=0; + + for( tr=-trowso2; tr<=trowso2; ++tr ){ + for( tc=-tcolso2; tc<=tcolso2; ++tc ){ + + if( template[trowso2+tr][tcolso2+tc] == PBM_BLACK ) continue; + + ++templatecount; + + for( r= ((tr>0)?0:-tr) ; r< ((tr>0)?(rows-tr):rows) ; ++r ){ + for( c= ((tc>0)?0:-tc) ; c< ((tc>0)?(cols-tc):cols) ; ++c ){ + source = in_image[r+tr][c+tc]; + out_image[r][c] = MAX(source, out_image[r][c]); + } /* for c */ + } /* for r */ + } /* for tr */ + } /* for tc */ + + return templatecount; + +} /* dilate */ + + + +/************************************************************ + * Erode: same as dilate except !!!! + ************************************************************/ + +static int +erode( bit** template, int trowso2, int tcolso2, + gray** in_image, gray** out_image, + int rows, int cols ){ + + int c, r, tc, tr; + int templatecount; + gray source; + + for( c=0; c<cols; ++c) + for( r=0; r<rows; ++r ) + out_image[r][c] = PGM_MAXMAXVAL; /* !!!! */ + + /* + * for each non-black pixel of the template + * add in to out + */ + + templatecount=0; + + for( tr=-trowso2; tr<=trowso2; ++tr ){ + for( tc=-tcolso2; tc<=tcolso2; ++tc ){ + + if( template[trowso2+tr][tcolso2+tc] == PBM_BLACK ) continue; + + ++templatecount; + + for( r= ((tr>0)?0:-tr) ; r< ((tr>0)?(rows-tr):rows) ; ++r ){ + for( c= ((tc>0)?0:-tc) ; c< ((tc>0)?(cols-tc):cols) ; ++c ){ + + source = in_image[r+tr][c+tc]; + out_image[r][c] = MIN(source, out_image[r][c]); + + } /* for c */ + } /* for r */ + + + + } /* for tr */ + } /* for tc */ + + return templatecount; + +} /* erode */ + + + +/************************************************************ + * Main + ************************************************************/ + + +int main( int argc, char* argv[] ){ + + int argn; + char operation; + const char* usage = "-dilate|-erode|-open|-close <templatefile> [pgmfile]"; + + FILE* tifp; /* template */ + int tcols, trows; + int tcolso2, trowso2; + bit** template; + + + FILE* ifp; /* input image */ + int cols, rows; + gray maxval; + + gray** in_image; + gray** out_image; + + int templatecount=0; + + pgm_init( &argc, argv ); + + /* + * parse arguments + */ + + ifp = stdin; + operation = 'd'; + + argn=1; + + if( argn == argc ) pm_usage( usage ); + + if( pm_keymatch( argv[argn], "-erode", 2 )) { operation='e'; argn++; } + else + if( pm_keymatch( argv[argn], "-dilate", 2 )) { operation='d'; argn++; } + else + if( pm_keymatch( argv[argn], "-open", 2 )) { operation='o'; argn++; } + else + if( pm_keymatch( argv[argn], "-close", 2 )) { operation='c'; argn++; } + + if( argn == argc ) pm_usage( usage ); + + tifp = pm_openr( argv[argn++] ); + + if( argn != argc ) ifp = pm_openr( argv[argn++] ); + + if( argn != argc ) pm_usage( usage ); + + + /* + * Read in the template matrix. + */ + + template = pbm_readpbm( tifp, &tcols, &trows ); + pm_close( tifp ); + + if( tcols % 2 != 1 || trows % 2 != 1 ) + pm_error("the template matrix must have an odd number of " + "rows and columns" ); + + /* the reason is that we want the middle pixel to be the origin */ + tcolso2 = tcols / 2; /* template coords run from -tcols/2 .. 0 .. +tcols/2 */ + trowso2 = trows / 2; + +#if 0 + fprintf(stderr, "template: %d x %d\n", trows, tcols); + fprintf(stderr, "half: %d x %d\n", trowso2, tcolso2); +#endif + + /* + * Read in the image + */ + + in_image = pgm_readpgm( ifp, &cols, &rows, &maxval); + + if( cols < tcols || rows < trows ) + pm_error("the image is smaller than the convolution matrix" ); + +#if 0 + fprintf(stderr, "image: %d x %d (%d)\n", rows, cols, maxval); +#endif + + /* + * Allocate output buffer and initialize with min or max value + */ + + out_image = pgm_allocarray( cols, rows ); + + if( operation == 'd' ){ + templatecount = dilate(template, trowso2, tcolso2, + in_image, out_image, rows, cols); + } + else if( operation == 'e' ){ + templatecount = erode(template, trowso2, tcolso2, + in_image, out_image, rows, cols); + } + else if( operation == 'o' ){ + gray ** eroded_image; + eroded_image = pgm_allocarray( cols, rows ); + templatecount = erode(template, trowso2, tcolso2, + in_image, eroded_image, rows, cols); + templatecount = dilate(template, trowso2, tcolso2, + eroded_image, out_image, rows, cols); + pgm_freearray( eroded_image, rows ); + } + else if( operation == 'c' ){ + gray ** dilated_image; + dilated_image = pgm_allocarray( cols, rows ); + templatecount = dilate(template, trowso2, tcolso2, + in_image, dilated_image, rows, cols); + templatecount = erode(template, trowso2, tcolso2, + dilated_image, out_image, rows, cols); + pgm_freearray( dilated_image, rows ); + } + + if(templatecount == 0 ) pm_error( "The template was empty!" ); + + pgm_writepgm( stdout, out_image, cols, rows, maxval, 1 ); + + pgm_freearray( out_image, rows ); + pgm_freearray( in_image, rows ); + pm_close( ifp ); + + exit( 0 ); + +} /* main */ + diff --git a/editor/pnmalias.c b/editor/pnmalias.c new file mode 100644 index 00000000..36b41ce4 --- /dev/null +++ b/editor/pnmalias.c @@ -0,0 +1,250 @@ +/* pnmmalias.c - antialias a portable anymap. +** +** Copyright (C) 1992 by Alberto Accomazzi, Smithsonian Astrophysical +** Observatory. +** +** 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. +*/ + +#include "pnm.h" + +int +main(int argc, char * argv[] ) { + FILE* ifp; + xel* xelrow[3]; + xel* newxelrow; + pixel bgcolorppm, fgcolorppm; + register xel* xpP; + register xel* xP; + register xel* xnP; + register xel* nxP; + xel bgcolor, fgcolor; + int argn, rows, cols, format, newformat, bgonly, fgonly; + int bgalias, fgalias; + int row; + double fmask[9], weight; + xelval maxval; + xelval newmaxval; + const char* const usage = "[-bgcolor <color>] [-fgcolor <color>] [-bonly] [-fonly] [-balias] [-falias] [-weight <w>] [pnmfile]"; + + pnm_init( &argc, argv ); + + bgonly = fgonly = 0; + bgalias = fgalias = 0; + weight = 1./3.; + argn = 1; + PPM_ASSIGN( bgcolorppm, 0, 0, 0); + PPM_ASSIGN( fgcolorppm, 0, 0, 0); + + while ( argn < argc && argv[argn][0] == '-' ) + { + if ( pm_keymatch( argv[argn], "-fgcolor", 3 ) ) + { + if ( ++argn >= argc ) + pm_usage( usage ); + else + fgcolorppm = ppm_parsecolor( argv[argn], PPM_MAXMAXVAL ); + } + else if ( pm_keymatch( argv[argn], "-bgcolor", 3 ) ) + { + if ( ++argn >= argc ) + pm_usage( usage ); + else + bgcolorppm = ppm_parsecolor( argv[argn], PPM_MAXMAXVAL ); + } + else if ( pm_keymatch( argv[argn], "-weight", 2 ) ) + { + if ( ++argn >= argc ) + pm_usage( usage ); + else if ( sscanf( argv[argn], "%lf", &weight ) != 1 ) + pm_usage( usage ); + else if ( weight >= 1. || weight <= 0. ) + { + pm_message( "weight factor w must be 0.0 < w < 1.0" ); + pm_usage( usage ); + } + } + else if ( pm_keymatch( argv[argn], "-bonly", 3 ) ) + bgonly = 1; + else if ( pm_keymatch( argv[argn], "-fonly", 3 ) ) + fgonly = 1; + else if ( pm_keymatch( argv[argn], "-balias", 3 ) ) + bgalias = 1; + else if ( pm_keymatch( argv[argn], "-falias", 3 ) ) + fgalias = 1; + else if ( pm_keymatch( argv[argn], "-bfalias", 3 ) ) + bgalias = fgalias = 0; + else if ( pm_keymatch( argv[argn], "-fbalias", 3 ) ) + bgalias = fgalias = 0; + else + pm_usage( usage ); + ++argn; + } + + if ( argn != argc ) + { + ifp = pm_openr( argv[argn] ); + ++argn; + } + else + ifp = stdin; + + if ( argn != argc ) + pm_usage( usage ); + + /* normalize mask elements */ + fmask[4] = weight; + fmask[0] = fmask[1] = fmask[2] = fmask[3] = ( 1.0 - weight ) / 8.0; + fmask[5] = fmask[6] = fmask[7] = fmask[8] = ( 1.0 - weight ) / 8.0; + + pnm_readpnminit( ifp, &cols, &rows, &maxval, &format ); + + xelrow[0] = pnm_allocrow( cols ); + xelrow[1] = pnm_allocrow( cols ); + xelrow[2] = pnm_allocrow( cols ); + newxelrow = pnm_allocrow( cols ); + + /* Promote PBM files to PGM. */ + if ( PNM_FORMAT_TYPE(format) == PBM_TYPE ) { + newformat = PGM_TYPE; + newmaxval = PGM_MAXMAXVAL; + pm_message( "promoting from PBM to PGM" ); + } else { + newformat = format; + newmaxval = maxval; + } + + /* Figure out foreground pixel value if none was given */ + if (PPM_GETR(fgcolorppm) == 0 && PPM_GETG(fgcolorppm) == 0 && + PPM_GETB(fgcolorppm) == 0 ) { + if ( PNM_FORMAT_TYPE(newformat) == PGM_TYPE ) + PNM_ASSIGN1( fgcolor, newmaxval ); + else + PPM_ASSIGN( fgcolor, newmaxval, newmaxval, newmaxval ); + } else { + if ( PNM_FORMAT_TYPE(newformat) == PGM_TYPE ) + PNM_ASSIGN1( fgcolor, PPM_GETR( fgcolorppm ) ); + else + fgcolor = fgcolorppm; + } + + if (PPM_GETR(bgcolorppm) != 0 || PPM_GETG(bgcolorppm) != 0 || + PPM_GETB(bgcolorppm) != 0 ) { + if ( PNM_FORMAT_TYPE(newformat) == PGM_TYPE ) + PNM_ASSIGN1( bgcolor, PPM_GETR( bgcolorppm) ); + else + bgcolor = bgcolorppm; + } else { + if ( PNM_FORMAT_TYPE(newformat) == PGM_TYPE ) + PNM_ASSIGN1( bgcolor, 0 ); + else + PPM_ASSIGN( bgcolor, 0, 0, 0 ); + } + + + pnm_readpnmrow( ifp, xelrow[0], cols, newmaxval, format ); + pnm_readpnmrow( ifp, xelrow[1], cols, newmaxval, format ); + pnm_writepnminit( stdout, cols, rows, newmaxval, newformat, 0 ); + pnm_writepnmrow( stdout, xelrow[0], cols, newmaxval, newformat, 0 ); + + for ( row = 1; row < rows - 1; ++row ) { + int col; + int value, valuer, valueg, valueb; + + pnm_readpnmrow( ifp, xelrow[(row+1)%3], cols, newmaxval, format ); + newxelrow[0] = xelrow[row%3][0]; + + for ( col = 1, xpP = (xelrow[(row-1)%3] + 1), xP = (xelrow[row%3] + 1), + xnP = (xelrow[(row+1)%3] + 1), nxP = (newxelrow+1); + col < cols - 1; ++col, ++xpP, ++xP, ++xnP, ++nxP ) { + + int fgflag, bgflag; + + /* Reset flags if anti-aliasing is to be done on foreground + * or background pixels only */ + if ( ( bgonly && PNM_EQUAL( *xP, fgcolor ) ) || + ( fgonly && PNM_EQUAL( *xP, bgcolor ) ) ) + bgflag = fgflag = 0; + else { + /* Do anti-aliasing here: see if pixel is at the border of a + * background or foreground stepwise side */ + bgflag = + (PNM_EQUAL(*xpP,bgcolor) && PNM_EQUAL(*(xP+1),bgcolor)) || + (PNM_EQUAL(*(xP+1),bgcolor) && PNM_EQUAL(*xnP,bgcolor)) || + (PNM_EQUAL(*xnP,bgcolor) && PNM_EQUAL(*(xP-1),bgcolor)) || + (PNM_EQUAL(*(xP-1),bgcolor) && PNM_EQUAL(*xpP,bgcolor)); + fgflag = + (PNM_EQUAL(*xpP,fgcolor) && PNM_EQUAL(*(xP+1),fgcolor)) || + (PNM_EQUAL(*(xP+1),fgcolor) && PNM_EQUAL(*xnP,fgcolor)) || + (PNM_EQUAL(*xnP,fgcolor) && PNM_EQUAL(*(xP-1),fgcolor)) || + (PNM_EQUAL(*(xP-1),fgcolor) && PNM_EQUAL(*xpP,fgcolor)); + } + if ( ( bgflag && bgalias ) || ( fgflag && fgalias ) || + ( bgflag && fgflag ) ) + switch( PNM_FORMAT_TYPE( newformat ) ) { + case PGM_TYPE: + value = PNM_GET1(*(xpP-1)) * fmask[0] + + PNM_GET1(*(xpP )) * fmask[1] + + PNM_GET1(*(xpP+1)) * fmask[2] + + PNM_GET1(*(xP -1)) * fmask[3] + + PNM_GET1(*(xP )) * fmask[4] + + PNM_GET1(*(xP +1)) * fmask[5] + + PNM_GET1(*(xnP-1)) * fmask[6] + + PNM_GET1(*(xnP )) * fmask[7] + + PNM_GET1(*(xnP+1)) * fmask[8] + + 0.5; + PNM_ASSIGN1( *nxP, value ); + break; + default: + valuer= PPM_GETR(*(xpP-1)) * fmask[0] + + PPM_GETR(*(xpP )) * fmask[1] + + PPM_GETR(*(xpP+1)) * fmask[2] + + PPM_GETR(*(xP -1)) * fmask[3] + + PPM_GETR(*(xP )) * fmask[4] + + PPM_GETR(*(xP +1)) * fmask[5] + + PPM_GETR(*(xnP-1)) * fmask[6] + + PPM_GETR(*(xnP )) * fmask[7] + + PPM_GETR(*(xnP+1)) * fmask[8] + + 0.5; + valueg= PPM_GETG(*(xpP-1)) * fmask[0] + + PPM_GETG(*(xpP )) * fmask[1] + + PPM_GETG(*(xpP+1)) * fmask[2] + + PPM_GETG(*(xP -1)) * fmask[3] + + PPM_GETG(*(xP )) * fmask[4] + + PPM_GETG(*(xP +1)) * fmask[5] + + PPM_GETG(*(xnP-1)) * fmask[6] + + PPM_GETG(*(xnP )) * fmask[7] + + PPM_GETG(*(xnP+1)) * fmask[8] + + 0.5; + valueb= PPM_GETB(*(xpP-1)) * fmask[0] + + PPM_GETB(*(xpP )) * fmask[1] + + PPM_GETB(*(xpP+1)) * fmask[2] + + PPM_GETB(*(xP -1)) * fmask[3] + + PPM_GETB(*(xP )) * fmask[4] + + PPM_GETB(*(xP +1)) * fmask[5] + + PPM_GETB(*(xnP-1)) * fmask[6] + + PPM_GETB(*(xnP )) * fmask[7] + + PPM_GETB(*(xnP+1)) * fmask[8] + + 0.5; + PPM_ASSIGN( *nxP, valuer, valueg, valueb ); + break; + } + else + *nxP = *xP; + } + + newxelrow[cols-1] = xelrow[row%3][cols-1]; + pnm_writepnmrow( stdout, newxelrow, cols, newmaxval, newformat, 0 ); + } + + pnm_writepnmrow( stdout, xelrow[row%3], cols, newmaxval, newformat, 0 ); + + pm_close( ifp ); + exit ( 0 ); +} + diff --git a/editor/pnmcat.c b/editor/pnmcat.c new file mode 100644 index 00000000..20dbf34d --- /dev/null +++ b/editor/pnmcat.c @@ -0,0 +1,427 @@ +/* pnmcat.c - concatenate portable anymaps +** +** Copyright (C) 1989, 1991 by Jef Poskanzer. +** +** 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. +*/ + +#include "pnm.h" +#include "mallocvar.h" +#include "shhopt.h" + + +enum backcolor {BACK_BLACK, BACK_WHITE, BACK_AUTO}; + +enum orientation {TOPBOTTOM, LEFTRIGHT}; + +enum justification {JUST_CENTER, JUST_MIN, JUST_MAX}; + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char **inputFilespec; + unsigned int nfiles; + enum backcolor backcolor; + enum orientation orientation; + enum justification justification; +}; + + + +static void +parseCommandLine(int argc, 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 = malloc(100*sizeof(optEntry)); + /* Instructions to OptParseOptions2 on how to parse our options. + */ + optStruct3 opt; + + unsigned int option_def_index; + + unsigned int leftright, topbottom, black, white, jtop, jbottom, + jleft, jright, jcenter; + + option_def_index = 0; /* incremented by OPTENTRY */ + 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); + + 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 */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + 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->backcolor = BACK_BLACK; + else if (white) + cmdlineP->backcolor = BACK_WHITE; + else + cmdlineP->backcolor = BACK_AUTO; + + if (jtop + jbottom + jleft + jright + jcenter > 1) + pm_error("You may specify onlyone 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->inputFilespec, 1); + cmdlineP->inputFilespec[0] = "-"; + cmdlineP->nfiles = 1; + } else { + unsigned int i; + + MALLOCARRAY_NOFAIL(cmdlineP->inputFilespec, argc-1); + + for (i = 0; i < argc-1; ++i) + cmdlineP->inputFilespec[i] = argv[1+i]; + cmdlineP->nfiles = argc-1; + } +} + + + +static void +computeOutputParms(unsigned int const nfiles, + enum orientation const orientation, + int cols[], + int rows[], + xelval maxval[], + int format[], + int * const newcolsP, + int * const newrowsP, + xelval * const newmaxvalP, + int * const newformatP) { + + int newcols, newrows; + int newformat; + xelval newmaxval; + + unsigned int i; + + newcols = 0; + newrows = 0; + + for (i = 0; i < nfiles; ++i) { + if (i == 0) { + newmaxval = maxval[i]; + newformat = format[i]; + } else { + if (PNM_FORMAT_TYPE(format[i]) > PNM_FORMAT_TYPE(newformat)) + newformat = format[i]; + if (maxval[i] > newmaxval) + newmaxval = maxval[i]; + } + switch (orientation) { + case LEFTRIGHT: + newcols += cols[i]; + if (rows[i] > newrows) + newrows = rows[i]; + break; + case TOPBOTTOM: + newrows += rows[i]; + if (cols[i] > newcols) + newcols = cols[i]; + break; + } + } + *newrowsP = newrows; + *newcolsP = newcols; + *newmaxvalP = newmaxval; + *newformatP = newformat; +} + + + +static void +concatenateLeftRight(FILE * const ofp, + unsigned int const nfiles, + int const newcols, + int const newrows, + xelval const newmaxval, + int const newformat, + enum justification const justification, + FILE * ifp[], + int cols[], + int rows[], + xelval maxval[], + int format[], + xel * xelrow[], + xel background[]) { + + unsigned int row; + + xel * const newxelrow = pnm_allocrow(newcols); + + for (row = 0; row < newrows; ++row) { + unsigned int new; + unsigned int i; + + new = 0; + for (i = 0; i < nfiles; ++i) { + int padtop; + + switch (justification) { + case JUST_MIN: + padtop = 0; + break; + case JUST_MAX: + padtop = newrows - rows[i]; + break; + case JUST_CENTER: + padtop = ( newrows - rows[i] ) / 2; + break; + } + if (row < padtop || row >= padtop + rows[i]) { + unsigned int col; + for (col = 0; col < cols[i]; ++col) + newxelrow[new+col] = background[i]; + } else { + if (row != padtop) { + /* first row already read */ + pnm_readpnmrow( + ifp[i], xelrow[i], cols[i], maxval[i], format[i] ); + pnm_promoteformatrow( + xelrow[i], cols[i], maxval[i], format[i], + newmaxval, newformat ); + } + { + unsigned int col; + for (col = 0; col < cols[i]; ++col) + newxelrow[new+col] = xelrow[i][col]; + } + } + new += cols[i]; + } + pnm_writepnmrow(ofp, newxelrow, newcols, newmaxval, newformat, 0); + } +} + + + +static void +concatenateTopBottom(FILE * const ofp, + unsigned int const nfiles, + int const newcols, + int const newrows, + xelval const newmaxval, + int const newformat, + enum justification const justification, + FILE * ifp[], + int cols[], + int rows[], + xelval maxval[], + int format[], + xel * xelrow[], + xel background[]) { + + int new; + xel * const newxelrow = pnm_allocrow(newcols); + int padleft; + unsigned int i; + unsigned int row; + + i = 0; + switch (justification) { + case JUST_MIN: + padleft = 0; + break; + case JUST_MAX: + padleft = newcols - cols[i]; + break; + case JUST_CENTER: + padleft = (newcols - cols[i]) / 2; + break; + } + + new = 0; + + for (row = 0; row < newrows; ++row) { + if (row - new >= rows[i]) { + new += rows[i]; + ++i; + if (i >= nfiles) + pm_error("INTERNAL ERROR: i > nfiles"); + switch (justification) { + case JUST_MIN: + padleft = 0; + break; + case JUST_MAX: + padleft = newcols - cols[i]; + break; + case JUST_CENTER: + padleft = (newcols - cols[i]) / 2; + break; + } + } + if (row - new > 0) { + pnm_readpnmrow( + ifp[i], xelrow[i], cols[i], maxval[i], format[i]); + pnm_promoteformatrow( + xelrow[i], cols[i], maxval[i], format[i], + newmaxval, newformat); + } + { + unsigned int col; + + for (col = 0; col < padleft; ++col) + newxelrow[col] = background[i]; + for (col = 0; col < cols[i]; ++col) + newxelrow[padleft+col] = xelrow[i][col]; + for (col = padleft + cols[i]; col < newcols; ++col) + newxelrow[col] = background[i]; + } + pnm_writepnmrow(ofp, + newxelrow, newcols, newmaxval, newformat, 0); + } +} + + + +int +main(int argc, char ** argv) { + + struct cmdlineInfo cmdline; + FILE** ifp; + xel** xelrow; + xel* background; + xelval* maxval; + xelval newmaxval; + int* rows; + int* cols; + int* format; + int newformat; + unsigned int i; + int newrows, newcols; + + pnm_init( &argc, argv ); + + parseCommandLine(argc, argv, &cmdline); + + MALLOCARRAY_NOFAIL(ifp, cmdline.nfiles); + MALLOCARRAY_NOFAIL(xelrow, cmdline.nfiles); + MALLOCARRAY_NOFAIL(background, cmdline.nfiles); + MALLOCARRAY_NOFAIL(maxval, cmdline.nfiles); + MALLOCARRAY_NOFAIL(rows, cmdline.nfiles); + MALLOCARRAY_NOFAIL(cols, cmdline.nfiles); + MALLOCARRAY_NOFAIL(format, cmdline.nfiles); + + for (i = 0; i < cmdline.nfiles; ++i) { + ifp[i] = pm_openr(cmdline.inputFilespec[i]); + pnm_readpnminit(ifp[i], &cols[i], &rows[i], &maxval[i], &format[i]); + xelrow[i] = pnm_allocrow(cols[i]); + } + + computeOutputParms(cmdline.nfiles, cmdline.orientation, + cols, rows, maxval, format, + &newcols, &newrows, &newmaxval, &newformat); + + for (i = 0; i < cmdline.nfiles; ++i) { + /* Read first row just to get a good guess at the background. */ + pnm_readpnmrow(ifp[i], xelrow[i], cols[i], maxval[i], format[i]); + pnm_promoteformatrow( + xelrow[i], cols[i], maxval[i], format[i], newmaxval, newformat); + switch (cmdline.backcolor) { + case BACK_AUTO: + background[i] = + pnm_backgroundxelrow( + xelrow[i], cols[i], newmaxval, newformat); + break; + case BACK_BLACK: + background[i] = pnm_blackxel(newmaxval, newformat); + break; + case BACK_WHITE: + background[i] = pnm_whitexel(newmaxval, newformat); + break; + } + } + + pnm_writepnminit(stdout, newcols, newrows, newmaxval, newformat, 0); + + switch (cmdline.orientation) { + case LEFTRIGHT: + concatenateLeftRight(stdout, cmdline.nfiles, + newcols, newrows, newmaxval, newformat, + cmdline.justification, + ifp, cols, rows, maxval, format, xelrow, + background); + break; + case TOPBOTTOM: + concatenateTopBottom(stdout, cmdline.nfiles, + newcols, newrows, newmaxval, newformat, + cmdline.justification, + ifp, cols, rows, maxval, format, xelrow, + background); + break; + } + free(cmdline.inputFilespec); + + for (i = 0; i < cmdline.nfiles; ++i) + pm_close(ifp[i]); + + pm_close(stdout); + + return 0; +} diff --git a/editor/pnmcomp.c b/editor/pnmcomp.c new file mode 100644 index 00000000..5a8b1d55 --- /dev/null +++ b/editor/pnmcomp.c @@ -0,0 +1,459 @@ +/* +-------------------------------------------------------------------+ */ +/* | Copyright 1992, David Koblas. | */ +/* | 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. | */ +/* +-------------------------------------------------------------------+ */ + +/* + + DON'T ADD NEW FUNCTION TO THIS PROGRAM. ADD IT TO pamcomp.c INSTEAD. + +*/ + + + +#define _BSD_SOURCE /* Make sure strcasecmp() is in string.h */ +#include <string.h> + +#include "pnm.h" +#include "shhopt.h" +#include "mallocvar.h" + +enum horizPos {BEYONDLEFT, LEFT, CENTER, RIGHT, BEYONDRIGHT}; +enum vertPos {ABOVE, TOP, MIDDLE, BOTTOM, BELOW}; + + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *underlyingFilespec; /* '-' if stdin */ + const char *overlayFilespec; + const char *alphaFilespec; + const char *outputFilespec; /* '-' if stdout */ + int xoff, yoff; /* value of xoff, yoff options */ + float opacity; + unsigned int alphaInvert; + enum horizPos align; + enum vertPos valign; +}; + + + + +static void +parseCommandLine(int argc, 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 option_def_index; + + char *align, *valign; + unsigned int xoffSpec, yoffSpec, alignSpec, valignSpec, opacitySpec, + alphaSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "invert", OPT_FLAG, NULL, + &cmdlineP->alphaInvert, 0 ); + OPTENT3(0, "xoff", OPT_INT, &cmdlineP->xoff, + &xoffSpec, 0 ); + OPTENT3(0, "yoff", OPT_INT, &cmdlineP->yoff, + &yoffSpec, 0 ); + OPTENT3(0, "opacity", OPT_FLOAT, &cmdlineP->opacity, + &opacitySpec, 0 ); + OPTENT3(0, "alpha", OPT_STRING, &cmdlineP->alphaFilespec, + &alphaSpec, 0 ); + OPTENT3(0, "align", OPT_STRING, &align, + &alignSpec, 0 ); + OPTENT3(0, "valign", OPT_STRING, &valign, + &valignSpec, 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 */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + + if (!xoffSpec) + cmdlineP->xoff = 0; + if (!yoffSpec) + cmdlineP->yoff = 0; + if (!alphaSpec) + cmdlineP->alphaFilespec = NULL; + + if (alignSpec) { + if (strcasecmp(align, "BEYONDLEFT") == 0) + cmdlineP->align = BEYONDLEFT; + else if (strcasecmp(align, "LEFT") == 0) + cmdlineP->align = LEFT; + else if (strcasecmp(align, "CENTER") == 0) + cmdlineP->align = CENTER; + else if (strcasecmp(align, "RIGHT") == 0) + cmdlineP->align = RIGHT; + else if (strcasecmp(align, "BEYONDRIGHT") == 0) + cmdlineP->align = BEYONDRIGHT; + else + pm_error("Invalid value for align option: '%s'. Only LEFT, " + "RIGHT, CENTER, BEYONDLEFT, and BEYONDRIGHT are valid.", + align); + } else + cmdlineP->align = LEFT; + + if (valignSpec) { + if (strcasecmp(valign, "ABOVE") == 0) + cmdlineP->valign = ABOVE; + else if (strcasecmp(valign, "TOP") == 0) + cmdlineP->valign = TOP; + else if (strcasecmp(valign, "MIDDLE") == 0) + cmdlineP->valign = MIDDLE; + else if (strcasecmp(valign, "BOTTOM") == 0) + cmdlineP->valign = BOTTOM; + else if (strcasecmp(valign, "BELOW") == 0) + cmdlineP->valign = BELOW; + else + pm_error("Invalid value for valign option: '%s'. Only TOP, " + "BOTTOM, MIDDLE, ABOVE, and BELOW are valid.", + align); + } else + cmdlineP->valign = TOP; + + if (!opacitySpec) + cmdlineP->opacity = 1.0; + + if (argc-1 < 1) + pm_error("Need at least one argument: file specification of the " + "overlay image."); + + cmdlineP->overlayFilespec = argv[1]; + + if (argc-1 >= 2) + cmdlineP->underlyingFilespec = argv[2]; + else + cmdlineP->underlyingFilespec = "-"; + + if (argc-1 >= 3) + cmdlineP->outputFilespec = argv[3]; + else + cmdlineP->outputFilespec = "-"; + + if (argc-1 > 3) + pm_error("Too many arguments. Only acceptable arguments are: " + "overlay image, underlying image, output image"); +} + + + + +static void +warnOutOfFrame( int const originLeft, + int const originTop, + int const overCols, + int const overRows, + int const underCols, + int const underRows ) { + if (originLeft >= underCols) + pm_message("WARNING: the overlay is entirely off the right edge " + "of the underlying image. " + "It will not be visible in the result. The horizontal " + "overlay position you selected is %d, " + "and the underlying image " + "is only %d pixels wide.", originLeft, underCols ); + else if (originLeft + overCols <= 0) + pm_message("WARNING: the overlay is entirely off the left edge " + "of the underlying image. " + "It will not be visible in the result. The horizontal " + "overlay position you selected is %d and the overlay is " + "only %d pixels wide.", originLeft, overCols); + else if (originTop >= underRows) + pm_message("WARNING: the overlay is entirely off the bottom edge " + "of the underlying image. " + "It will not be visible in the result. The vertical " + "overlay position you selected is %d, " + "and the underlying image " + "is only %d pixels high.", originTop, underRows ); + else if (originTop + overRows <= 0) + pm_message("WARNING: the overlay is entirely off the top edge " + "of the underlying image. " + "It will not be visible in the result. The vertical " + "overlay position you selected is %d and the overlay is " + "only %d pixels high.", originTop, overRows); +} + + + +static void +computeOverlayPosition(const int underCols, const int underRows, + const int overCols, const int overRows, + const struct cmdlineInfo cmdline, + int * const originLeftP, + int * const originTopP) { +/*---------------------------------------------------------------------------- + Determine where to overlay the overlay image, based on the options the + user specified and the realities of the image dimensions. + + The origin may be outside the underlying image (so e.g. *originLeftP may + be negative or > image width). That means not all of the overlay image + actually gets used. In fact, there may be no overlap at all. +-----------------------------------------------------------------------------*/ + int xalign, yalign; + + switch (cmdline.align) { + case BEYONDLEFT: xalign = -overCols; break; + case LEFT: xalign = 0; break; + case CENTER: xalign = (underCols-overCols)/2; break; + case RIGHT: xalign = underCols - overCols; break; + case BEYONDRIGHT: xalign = underCols; break; + } + switch (cmdline.valign) { + case ABOVE: yalign = -overRows; break; + case TOP: yalign = 0; break; + case MIDDLE: yalign = (underRows-overRows)/2; break; + case BOTTOM: yalign = underRows - overRows; break; + case BELOW: yalign = underRows; break; + } + *originLeftP = xalign + cmdline.xoff; + *originTopP = yalign + cmdline.yoff; + + warnOutOfFrame( *originLeftP, *originTopP, + overCols, overRows, underCols, underRows ); +} + + + +static pixval +composeComponents(pixval const compA, + pixval const compB, + float const distrib, + pixval const maxval) { +/*---------------------------------------------------------------------------- + Compose a single component of each of two pixels, with 'distrib' being + the fraction of 'compA' in the result, 1-distrib the fraction of 'compB'. + + Both inputs are based on a maxval of 'maxval', and so is our result. + + Note that while 'distrib' in the straightforward case is always in + [0,1], it can in fact be negative or greater than 1. We clip the + result as required to return a legal pixval. +-----------------------------------------------------------------------------*/ + return MIN(maxval, MAX(0, (int)compA * distrib + + (int)compB * (1.0 - distrib) + + 0.5 + ) + ); +} + + + +static pixel +composePixels(pixel const pixelA, + pixel const pixelB, + float const distrib, + pixval const maxval) { +/*---------------------------------------------------------------------------- + Compose two pixels 'pixelA' and 'pixelB', with 'distrib' being the + fraction of 'pixelA' in the result, 1-distrib the fraction of 'pixelB'. + + Both inputs are based on a maxval of 'maxval', and so is our result. + + Note that while 'distrib' in the straightforward case is always in + [0,1], it can in fact be negative or greater than 1. We clip the + result as required to return a legal pixval. +-----------------------------------------------------------------------------*/ + pixel retval; + + pixval const red = + composeComponents(PPM_GETR(pixelA), PPM_GETR(pixelB), distrib, maxval); + pixval const grn = + composeComponents(PPM_GETG(pixelA), PPM_GETG(pixelB), distrib, maxval); + pixval const blu = + composeComponents(PPM_GETB(pixelA), PPM_GETB(pixelB), distrib, maxval); + + PPM_ASSIGN(retval, red, grn, blu); + + return retval; +} + + + +static void +composite(int const originleft, + int const origintop, + pixel ** const overlayImage, + int const overlayCols, + int const overlayRows, + xelval const overlayMaxval, + int const overlayType, + int const cols, + int const rows, + xelval const maxval, + int const type, + gray ** const alpha, + gray const alphaMax, + bool const invertAlpha, + float const opacity, + FILE * const ifp, + FILE * const ofp) { +/*---------------------------------------------------------------------------- + Overlay the overlay image 'overlayImage' onto the underlying image + which is in file 'ifp', and output the composite to file 'ofp'. + + The underlying image file 'ifp' is positioned after its header. The + width, height, format, and maxval of the underlying image are 'cols', + 'rows', 'type', and 'maxval'. + + The width, height, format, and maxval of the overlay image are + overlayCols, overlayRows, overlayType and overlayMaxval. + + 'originleft' and 'origintop' are the coordinates in the underlying + image plane where the top left corner of the overlay image is + to go. It is not necessarily inside the underlying image (in fact, + may be negative). Only the part of the overlay that actually intersects + the underlying image, if any, gets into the output. + + Note that we modify the overlay image 'overlayImage' to change its + format and maxval to the format and maxval of the output. +-----------------------------------------------------------------------------*/ + /* otype and oxmaxv are the type and maxval for the composed (output) + image, and are derived from that of the underlying and overlay + images. + */ + int const otype = (overlayType < type) ? type : overlayType; + xelval const omaxv = pm_lcm(maxval, overlayMaxval, 1, PNM_OVERALLMAXVAL); + + int row; + xel *pixelrow; + + pixelrow = pnm_allocrow(cols); + + if (overlayType != otype || overlayMaxval != omaxv) { + pnm_promoteformat(overlayImage, overlayCols, overlayRows, + overlayMaxval, overlayType, omaxv, otype); + } + + pnm_writepnminit(ofp, cols, rows, omaxv, otype, 0); + + for (row = 0; row < rows; ++row) { + int col; + + /* Read a row and convert it to the output type */ + pnm_readpnmrow(ifp, pixelrow, cols, maxval, type); + + if (type != otype || maxval != omaxv) + pnm_promoteformatrow(pixelrow, cols, maxval, type, omaxv, otype); + + /* Now overlay the overlay with alpha (if defined) */ + for (col = 0; col < cols; ++col) { + int const ovlcol = col - originleft; + int const ovlrow = row - origintop; + + double overlayWeight; + + if (ovlcol >= 0 && ovlcol < overlayCols && + ovlrow >= 0 && ovlrow < overlayRows) { + + if (alpha == NULL) { + overlayWeight = opacity; + } else { + double alphaval; + alphaval = + (double)alpha[ovlrow][ovlcol] / (double)alphaMax; + if (invertAlpha) + alphaval = 1.0 - alphaval; + overlayWeight = alphaval * opacity; + } + + pixelrow[col] = composePixels(overlayImage[ovlrow][ovlcol], + pixelrow[col], + overlayWeight, omaxv); + } + } + pnm_writepnmrow(ofp, pixelrow, cols, omaxv, otype, 0); + } + pnm_freerow(pixelrow); +} + + + +int +main(int argc, char *argv[]) { + + FILE *ifp, *ofp; + pixel **image; + int imageCols, imageRows, imageType; + xelval imageMax; + int cols, rows, type; + xelval maxval; + gray **alpha; + int alphaCols, alphaRows; + xelval alphaMax; + struct cmdlineInfo cmdline; + int originLeft, originTop; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + { /* Read the overlay image into 'image' */ + FILE *fp; + fp = pm_openr(cmdline.overlayFilespec); + image = + pnm_readpnm(fp, &imageCols, &imageRows, &imageMax, &imageType); + pm_close(fp); + } + if (cmdline.alphaFilespec) { + /* Read the alpha mask file into 'alpha' */ + FILE *fp = pm_openr(cmdline.alphaFilespec); + alpha = pgm_readpgm(fp, &alphaCols, &alphaRows, &alphaMax); + pm_close(fp); + + if (imageCols != alphaCols || imageRows != alphaRows) + pm_error("Alpha map and overlay image are not the same size"); + } else + alpha = NULL; + + ifp = pm_openr(cmdline.underlyingFilespec); + + ofp = pm_openw(cmdline.outputFilespec); + + pnm_readpnminit(ifp, &cols, &rows, &maxval, &type); + + computeOverlayPosition(cols, rows, imageCols, imageRows, + cmdline, &originLeft, &originTop); + + composite(originLeft, originTop, + image, imageCols, imageRows, imageMax, imageType, + cols, rows, maxval, type, + alpha, alphaMax, cmdline.alphaInvert, cmdline.opacity, + ifp, ofp); + + pm_close(ifp); + pm_close(ofp); + + /* If the program failed, it previously aborted with nonzero completion + code, via various function calls. + */ + return 0; +} + + diff --git a/editor/pnmconvol.c b/editor/pnmconvol.c new file mode 100644 index 00000000..0bf44ce3 --- /dev/null +++ b/editor/pnmconvol.c @@ -0,0 +1,1989 @@ +/* pnmconvol.c - general MxN convolution on a PNM image +** +** Version 2.0.1 January 30, 1995 +** +** Major rewriting by Mike Burns +** Copyright (C) 1994, 1995 by Mike Burns (burns@chem.psu.edu) +** +** Copyright (C) 1989, 1991 by Jef Poskanzer. +** +** 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. +*/ + +/* A change history is at the bottom */ + +#include "pnm.h" +#include "shhopt.h" +#include "mallocvar.h" + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *inputFilespec; /* '-' if stdin */ + const char *kernelFilespec; + unsigned int nooffset; +}; + +static void +parseCommandLine(int argc, char ** argv, + struct cmdlineInfo *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 option_def_index; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "nooffset", OPT_FLAG, NULL, + &cmdlineP->nooffset, 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 */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (argc-1 < 1) + pm_error("Need at least one argument: file specification of the " + "convolution kernel image."); + + cmdlineP->kernelFilespec = argv[1]; + + if (argc-1 >= 2) + cmdlineP->inputFilespec = argv[2]; + else + cmdlineP->inputFilespec = "-"; + + if (argc-1 > 2) + pm_error("Too many arguments. Only acceptable arguments are: " + "convolution file name and input file name"); +} + + +/* Macros to verify that r,g,b values are within proper range */ + +#define CHECK_GRAY \ + if ( tempgsum < 0L ) g = 0; \ + else if ( tempgsum > maxval ) g = maxval; \ + else g = tempgsum; + +#define CHECK_RED \ + if ( temprsum < 0L ) r = 0; \ + else if ( temprsum > maxval ) r = maxval; \ + else r = temprsum; + +#define CHECK_GREEN \ + if ( tempgsum < 0L ) g = 0; \ + else if ( tempgsum > maxval ) g = maxval; \ + else g = tempgsum; + +#define CHECK_BLUE \ + if ( tempbsum < 0L ) b = 0; \ + else if ( tempbsum > maxval ) b = maxval; \ + else b = tempbsum; + +struct convolveType { + void (*ppmConvolver)(const float ** const rweights, + const float ** const gweights, + const float ** const bweights); + void (*pgmConvolver)(const float ** const weights); +}; + +static FILE* ifp; +static int crows, ccols, ccolso2, crowso2; +static int cols, rows; +static xelval maxval; +static int format, newformat; + + + +static void +computeWeights(xel * const * const cxels, + int const ccols, + int const crows, + int const cformat, + xelval const cmaxval, + bool const offsetPgm, + float *** const rweightsP, + float *** const gweightsP, + float *** const bweightsP) { +/*---------------------------------------------------------------------------- + Compute the convolution matrix in normalized form from the PGM + form. Each element of the output matrix is the actual weight we give an + input pixel -- i.e. the thing by which we multiple a value from the + input image. + + 'offsetPgm' means the PGM convolution matrix is defined in offset form so + that it can represent negative values. E.g. with maxval 100, 50 means + 0, 100 means 50, and 0 means -50. If 'offsetPgm' is false, 0 means 0 + and there are no negative weights. +-----------------------------------------------------------------------------*/ + double const scale = (offsetPgm ? 2.0 : 1.0) / cmaxval; + double const offset = offsetPgm ? - 1.0 : 0.0; + + float** rweights; + float** gweights; + float** bweights; + + float rsum, gsum, bsum; + + unsigned int crow; + + /* Set up the normalized weights. */ + rweights = (float**) pm_allocarray(ccols, crows, sizeof(float)); + gweights = (float**) pm_allocarray(ccols, crows, sizeof(float)); + bweights = (float**) pm_allocarray(ccols, crows, sizeof(float)); + + rsum = gsum = bsum = 0.0; /* initial value */ + + for (crow = 0; crow < crows; ++crow) { + unsigned int ccol; + for (ccol = 0; ccol < ccols; ++ccol) { + switch (PNM_FORMAT_TYPE(cformat)) { + case PPM_TYPE: + rsum += rweights[crow][ccol] = + (PPM_GETR(cxels[crow][ccol]) * scale + offset); + gsum += gweights[crow][ccol] = + (PPM_GETG(cxels[crow][ccol]) * scale + offset); + bsum += bweights[crow][ccol] = + (PPM_GETB(cxels[crow][ccol]) * scale + offset); + break; + + default: + gsum += gweights[crow][ccol] = + (PNM_GET1(cxels[crow][ccol]) * scale + offset); + break; + } + } + } + *rweightsP = rweights; + *gweightsP = gweights; + *bweightsP = bweights; + + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + if (rsum < 0.9 || rsum > 1.1 || gsum < 0.9 || gsum > 1.1 || + bsum < 0.9 || bsum > 1.1) { + pm_message("WARNING - this convolution matrix is biased. " + "red, green, and blue average weights: %f, %f, %f " + "(unbiased would be 1).", + rsum, gsum, bsum); + + if (rsum < 0 && gsum < 0 && bsum < 0) + pm_message("Maybe you want the -nooffset option?"); + } + break; + + default: + if (gsum < 0.9 || gsum > 1.1) + pm_message("WARNING - this convolution matrix is biased. " + "average weight = %f (unbiased would be 1)", + gsum); + break; + } +} + + + +/* General PGM Convolution +** +** No useful redundancy in convolution matrix. +*/ + +static void +pgm_general_convolve(const float ** const weights) { + xel** xelbuf; + xel* outputrow; + xelval g; + int row; + xel **rowptr, *temprptr; + int toprow, temprow; + int i, irow; + long tempgsum; + + /* Allocate space for one convolution-matrix's worth of rows, plus + a row output buffer. + */ + xelbuf = pnm_allocarray(cols, crows); + outputrow = pnm_allocrow(cols); + + /* Allocate array of pointers to xelbuf */ + rowptr = (xel **) pnm_allocarray(1, crows); + + pnm_writepnminit(stdout, cols, rows, maxval, newformat, 0); + + /* Read in one convolution-matrix's worth of image, less one row. */ + for (row = 0; row < crows - 1; ++row) { + pnm_readpnmrow(ifp, xelbuf[row], cols, maxval, format); + if (PNM_FORMAT_TYPE(format) != newformat) + pnm_promoteformatrow(xelbuf[row], cols, maxval, format, + maxval, newformat); + /* Write out just the part we're not going to convolve. */ + if (row < crowso2) + pnm_writepnmrow(stdout, xelbuf[row], cols, maxval, newformat, 0); + } + + /* Now the rest of the image - read in the row at the end of + xelbuf, and convolve and write out the row in the middle. + */ + for (; row < rows; ++row) { + int col; + toprow = row + 1; + temprow = row % crows; + pnm_readpnmrow(ifp, xelbuf[temprow], cols, maxval, format); + if (PNM_FORMAT_TYPE(format) != newformat) + pnm_promoteformatrow(xelbuf[temprow], cols, maxval, format, + maxval, newformat); + + /* Arrange rowptr to eliminate the use of mod function to determine + which row of xelbuf is 0...crows. Mod function can be very costly. + */ + temprow = toprow % crows; + i = 0; + for (irow = temprow; irow < crows; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + for (col = 0; col < cols; ++col) { + if (col < ccolso2 || col >= cols - ccolso2) + outputrow[col] = rowptr[crowso2][col]; + else { + int const leftcol = col - ccolso2; + int crow; + float gsum; + gsum = 0.0; + for (crow = 0; crow < crows; ++crow) { + int ccol; + temprptr = rowptr[crow] + leftcol; + for (ccol = 0; ccol < ccols; ++ccol) + gsum += PNM_GET1(*(temprptr + ccol)) + * weights[crow][ccol]; + } + tempgsum = gsum + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + } + pnm_writepnmrow(stdout, outputrow, cols, maxval, newformat, 0); + } + + /* Now write out the remaining unconvolved rows in xelbuf. */ + for (irow = crowso2 + 1; irow < crows; ++irow) + pnm_writepnmrow(stdout, rowptr[irow], cols, maxval, newformat, 0 ); +} + + + +/* PGM Mean Convolution +** +** This is the common case where you just want the target pixel replaced with +** the average value of its neighbors. This can work much faster than the +** general case because you can reduce the number of floating point operations +** that are required since all the weights are the same. You will only need +** to multiply by the weight once, not for every pixel in the convolution +** matrix. +** +** This algorithm works by creating sums for each column of crows height for +** the whole width of the image. Then add ccols column sums together to obtain +** the total sum of the neighbors and multiply that sum by the weight. As you +** move right to left to calculate the next pixel, take the total sum you just +** generated, add in the value of the next column and subtract the value of the +** leftmost column. Multiply that by the weight and that's it. As you move +** down a row, calculate new column sums by using previous sum for that column +** and adding in pixel on current row and subtracting pixel in top row. +** +*/ + + +static void +pgm_mean_convolve(const float ** const weights) { + float const gmeanweight = weights[0][0]; + + int ccol, col; + xel** xelbuf; + xel* outputrow; + xelval g; + int row, crow; + xel **rowptr, *temprptr; + int leftcol; + int i, irow; + int toprow, temprow; + int subrow, addrow; + int subcol, addcol; + long gisum; + int tempcol, crowsp1; + long tempgsum; + long *gcolumnsum; + + /* Allocate space for one convolution-matrix's worth of rows, plus + ** a row output buffer. MEAN uses an extra row. */ + xelbuf = pnm_allocarray( cols, crows + 1 ); + outputrow = pnm_allocrow( cols ); + + /* Allocate array of pointers to xelbuf. MEAN uses an extra row. */ + rowptr = (xel **) pnm_allocarray( 1, crows + 1); + + /* Allocate space for intermediate column sums */ + gcolumnsum = (long *) pm_allocrow( cols, sizeof(long) ); + for ( col = 0; col < cols; ++col ) + gcolumnsum[col] = 0L; + + pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 ); + + /* Read in one convolution-matrix's worth of image, less one row. */ + for ( row = 0; row < crows - 1; ++row ) + { + pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[row], cols, maxval, format, maxval, newformat ); + /* Write out just the part we're not going to convolve. */ + if ( row < crowso2 ) + pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 ); + } + + /* Do first real row only */ + subrow = crows; + addrow = crows - 1; + toprow = row + 1; + temprow = row % crows; + pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[temprow], cols, maxval, format, maxval, newformat ); + + temprow = toprow % crows; + i = 0; + for (irow = temprow; irow < crows; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + gisum = 0L; + for ( col = 0; col < cols; ++col ) + { + if ( col < ccolso2 || col >= cols - ccolso2 ) + outputrow[col] = rowptr[crowso2][col]; + else if ( col == ccolso2 ) + { + leftcol = col - ccolso2; + for ( crow = 0; crow < crows; ++crow ) + { + temprptr = rowptr[crow] + leftcol; + for ( ccol = 0; ccol < ccols; ++ccol ) + gcolumnsum[leftcol + ccol] += + PNM_GET1( *(temprptr + ccol) ); + } + for ( ccol = 0; ccol < ccols; ++ccol) + gisum += gcolumnsum[leftcol + ccol]; + tempgsum = (float) gisum * gmeanweight + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + else + { + /* Column numbers to subtract or add to isum */ + subcol = col - ccolso2 - 1; + addcol = col + ccolso2; + for ( crow = 0; crow < crows; ++crow ) + gcolumnsum[addcol] += PNM_GET1( rowptr[crow][addcol] ); + gisum = gisum - gcolumnsum[subcol] + gcolumnsum[addcol]; + tempgsum = (float) gisum * gmeanweight + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + } + pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 ); + + ++row; + /* For all subsequent rows do it this way as the columnsums have been + ** generated. Now we can use them to reduce further calculations. + */ + crowsp1 = crows + 1; + for ( ; row < rows; ++row ) + { + toprow = row + 1; + temprow = row % (crows + 1); + pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[temprow], cols, maxval, format, maxval, newformat ); + + /* This rearrangement using crows+1 rowptrs and xelbufs will cause + ** rowptr[0..crows-1] to always hold active xelbufs and for + ** rowptr[crows] to always hold the oldest (top most) xelbuf. + */ + temprow = (toprow + 1) % crowsp1; + i = 0; + for (irow = temprow; irow < crowsp1; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + gisum = 0L; + for ( col = 0; col < cols; ++col ) + { + if ( col < ccolso2 || col >= cols - ccolso2 ) + outputrow[col] = rowptr[crowso2][col]; + else if ( col == ccolso2 ) + { + leftcol = col - ccolso2; + for ( ccol = 0; ccol < ccols; ++ccol ) + { + tempcol = leftcol + ccol; + gcolumnsum[tempcol] = gcolumnsum[tempcol] + - PNM_GET1( rowptr[subrow][ccol] ) + + PNM_GET1( rowptr[addrow][ccol] ); + gisum += gcolumnsum[tempcol]; + } + tempgsum = (float) gisum * gmeanweight + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + else + { + /* Column numbers to subtract or add to isum */ + subcol = col - ccolso2 - 1; + addcol = col + ccolso2; + gcolumnsum[addcol] = gcolumnsum[addcol] + - PNM_GET1( rowptr[subrow][addcol] ) + + PNM_GET1( rowptr[addrow][addcol] ); + gisum = gisum - gcolumnsum[subcol] + gcolumnsum[addcol]; + tempgsum = (float) gisum * gmeanweight + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + } + pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 ); + } + + /* Now write out the remaining unconvolved rows in xelbuf. */ + for ( irow = crowso2 + 1; irow < crows; ++irow ) + pnm_writepnmrow( + stdout, rowptr[irow], cols, maxval, newformat, 0 ); + + } + + +/* PGM Horizontal Convolution +** +** Similar idea to using columnsums of the Mean and Vertical convolution, +** but uses temporary sums of row values. Need to multiply by weights crows +** number of times. Each time a new line is started, must recalculate the +** initials rowsums for the newest row only. Uses queue to still access +** previous row sums. +** +*/ + +static void +pgm_horizontal_convolve(const float ** const weights) { + int ccol, col; + xel** xelbuf; + xel* outputrow; + xelval g; + int row, crow; + xel **rowptr, *temprptr; + int leftcol; + int i, irow; + int temprow; + int subcol, addcol; + float gsum; + int addrow, subrow; + long **growsum, **growsumptr; + int crowsp1; + long tempgsum; + + /* Allocate space for one convolution-matrix's worth of rows, plus + ** a row output buffer. */ + xelbuf = pnm_allocarray( cols, crows + 1 ); + outputrow = pnm_allocrow( cols ); + + /* Allocate array of pointers to xelbuf */ + rowptr = (xel **) pnm_allocarray( 1, crows + 1); + + /* Allocate intermediate row sums. HORIZONTAL uses an extra row. */ + /* crows current rows and 1 extra for newest added row. */ + growsum = (long **) pm_allocarray( cols, crows + 1, sizeof(long) ); + growsumptr = (long **) pnm_allocarray( 1, crows + 1); + + pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 ); + + /* Read in one convolution-matrix's worth of image, less one row. */ + for ( row = 0; row < crows - 1; ++row ) + { + pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[row], cols, maxval, format, maxval, newformat ); + /* Write out just the part we're not going to convolve. */ + if ( row < crowso2 ) + pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 ); + } + + /* First row only */ + temprow = row % crows; + pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[temprow], cols, maxval, format, maxval, newformat ); + + temprow = (row + 1) % crows; + i = 0; + for (irow = temprow; irow < crows; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + for ( crow = 0; crow < crows; ++crow ) + growsumptr[crow] = growsum[crow]; + + for ( col = 0; col < cols; ++col ) + { + if ( col < ccolso2 || col >= cols - ccolso2 ) + outputrow[col] = rowptr[crowso2][col]; + else if ( col == ccolso2 ) + { + leftcol = col - ccolso2; + gsum = 0.0; + for ( crow = 0; crow < crows; ++crow ) + { + temprptr = rowptr[crow] + leftcol; + growsumptr[crow][leftcol] = 0L; + for ( ccol = 0; ccol < ccols; ++ccol ) + growsumptr[crow][leftcol] += + PNM_GET1( *(temprptr + ccol) ); + gsum += growsumptr[crow][leftcol] * weights[crow][0]; + } + tempgsum = gsum + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + else + { + gsum = 0.0; + leftcol = col - ccolso2; + subcol = col - ccolso2 - 1; + addcol = col + ccolso2; + for ( crow = 0; crow < crows; ++crow ) + { + growsumptr[crow][leftcol] = growsumptr[crow][subcol] + - PNM_GET1( rowptr[crow][subcol] ) + + PNM_GET1( rowptr[crow][addcol] ); + gsum += growsumptr[crow][leftcol] * weights[crow][0]; + } + tempgsum = gsum + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + } + pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 ); + + + /* For all subsequent rows */ + + subrow = crows; + addrow = crows - 1; + crowsp1 = crows + 1; + ++row; + for ( ; row < rows; ++row ) + { + temprow = row % crowsp1; + pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[temprow], cols, maxval, format, maxval, newformat ); + + temprow = (row + 2) % crowsp1; + i = 0; + for (irow = temprow; irow < crowsp1; ++i, ++irow) + { + rowptr[i] = xelbuf[irow]; + growsumptr[i] = growsum[irow]; + } + for (irow = 0; irow < temprow; ++irow, ++i) + { + rowptr[i] = xelbuf[irow]; + growsumptr[i] = growsum[irow]; + } + + for ( col = 0; col < cols; ++col ) + { + if ( col < ccolso2 || col >= cols - ccolso2 ) + outputrow[col] = rowptr[crowso2][col]; + else if ( col == ccolso2 ) + { + gsum = 0.0; + leftcol = col - ccolso2; + growsumptr[addrow][leftcol] = 0L; + for ( ccol = 0; ccol < ccols; ++ccol ) + growsumptr[addrow][leftcol] += + PNM_GET1( rowptr[addrow][leftcol + ccol] ); + for ( crow = 0; crow < crows; ++crow ) + gsum += growsumptr[crow][leftcol] * weights[crow][0]; + tempgsum = gsum + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + else + { + gsum = 0.0; + leftcol = col - ccolso2; + subcol = col - ccolso2 - 1; + addcol = col + ccolso2; + growsumptr[addrow][leftcol] = growsumptr[addrow][subcol] + - PNM_GET1( rowptr[addrow][subcol] ) + + PNM_GET1( rowptr[addrow][addcol] ); + for ( crow = 0; crow < crows; ++crow ) + gsum += growsumptr[crow][leftcol] * weights[crow][0]; + tempgsum = gsum + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + } + pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 ); + } + + /* Now write out the remaining unconvolved rows in xelbuf. */ + for ( irow = crowso2 + 1; irow < crows; ++irow ) + pnm_writepnmrow( + stdout, rowptr[irow], cols, maxval, newformat, 0 ); + + } + + +/* PGM Vertical Convolution +** +** Uses column sums as in Mean Convolution. +** +*/ + + +static void +pgm_vertical_convolve(const float ** const weights) { + int ccol, col; + xel** xelbuf; + xel* outputrow; + xelval g; + int row, crow; + xel **rowptr, *temprptr; + int leftcol; + int i, irow; + int toprow, temprow; + int subrow, addrow; + int tempcol; + float gsum; + long *gcolumnsum; + int crowsp1; + int addcol; + long tempgsum; + + /* Allocate space for one convolution-matrix's worth of rows, plus + ** a row output buffer. VERTICAL uses an extra row. */ + xelbuf = pnm_allocarray( cols, crows + 1 ); + outputrow = pnm_allocrow( cols ); + + /* Allocate array of pointers to xelbuf */ + rowptr = (xel **) pnm_allocarray( 1, crows + 1 ); + + /* Allocate space for intermediate column sums */ + gcolumnsum = (long *) pm_allocrow( cols, sizeof(long) ); + for ( col = 0; col < cols; ++col ) + gcolumnsum[col] = 0L; + + pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 ); + + /* Read in one convolution-matrix's worth of image, less one row. */ + for ( row = 0; row < crows - 1; ++row ) + { + pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[row], cols, maxval, format, maxval, newformat ); + /* Write out just the part we're not going to convolve. */ + if ( row < crowso2 ) + pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 ); + } + + /* Now the rest of the image - read in the row at the end of + ** xelbuf, and convolve and write out the row in the middle. + */ + /* For first row only */ + + toprow = row + 1; + temprow = row % crows; + pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[temprow], cols, maxval, format, maxval, newformat ); + + /* Arrange rowptr to eliminate the use of mod function to determine + ** which row of xelbuf is 0...crows. Mod function can be very costly. + */ + temprow = toprow % crows; + i = 0; + for (irow = temprow; irow < crows; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + for ( col = 0; col < cols; ++col ) + { + if ( col < ccolso2 || col >= cols - ccolso2 ) + outputrow[col] = rowptr[crowso2][col]; + else if ( col == ccolso2 ) + { + gsum = 0.0; + leftcol = col - ccolso2; + for ( crow = 0; crow < crows; ++crow ) + { + temprptr = rowptr[crow] + leftcol; + for ( ccol = 0; ccol < ccols; ++ccol ) + gcolumnsum[leftcol + ccol] += + PNM_GET1( *(temprptr + ccol) ); + } + for ( ccol = 0; ccol < ccols; ++ccol) + gsum += gcolumnsum[leftcol + ccol] * weights[0][ccol]; + tempgsum = gsum + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + else + { + gsum = 0.0; + leftcol = col - ccolso2; + addcol = col + ccolso2; + for ( crow = 0; crow < crows; ++crow ) + gcolumnsum[addcol] += PNM_GET1( rowptr[crow][addcol] ); + for ( ccol = 0; ccol < ccols; ++ccol ) + gsum += gcolumnsum[leftcol + ccol] * weights[0][ccol]; + tempgsum = gsum + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + } + pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 ); + + /* For all subsequent rows */ + subrow = crows; + addrow = crows - 1; + crowsp1 = crows + 1; + ++row; + for ( ; row < rows; ++row ) + { + toprow = row + 1; + temprow = row % (crows +1); + pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[temprow], cols, maxval, format, maxval, newformat ); + + /* Arrange rowptr to eliminate the use of mod function to determine + ** which row of xelbuf is 0...crows. Mod function can be very costly. + */ + temprow = (toprow + 1) % crowsp1; + i = 0; + for (irow = temprow; irow < crowsp1; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + for ( col = 0; col < cols; ++col ) + { + if ( col < ccolso2 || col >= cols - ccolso2 ) + outputrow[col] = rowptr[crowso2][col]; + else if ( col == ccolso2 ) + { + gsum = 0.0; + leftcol = col - ccolso2; + for ( ccol = 0; ccol < ccols; ++ccol ) + { + tempcol = leftcol + ccol; + gcolumnsum[tempcol] = gcolumnsum[tempcol] + - PNM_GET1( rowptr[subrow][ccol] ) + + PNM_GET1( rowptr[addrow][ccol] ); + gsum = gsum + gcolumnsum[tempcol] * weights[0][ccol]; + } + tempgsum = gsum + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + else + { + gsum = 0.0; + leftcol = col - ccolso2; + addcol = col + ccolso2; + gcolumnsum[addcol] = gcolumnsum[addcol] + - PNM_GET1( rowptr[subrow][addcol] ) + + PNM_GET1( rowptr[addrow][addcol] ); + for ( ccol = 0; ccol < ccols; ++ccol ) + gsum += gcolumnsum[leftcol + ccol] * weights[0][ccol]; + tempgsum = gsum + 0.5; + CHECK_GRAY; + PNM_ASSIGN1( outputrow[col], g ); + } + } + pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 ); + } + + /* Now write out the remaining unconvolved rows in xelbuf. */ + for ( irow = crowso2 + 1; irow < crows; ++irow ) + pnm_writepnmrow( + stdout, rowptr[irow], cols, maxval, newformat, 0 ); + + } + + + + +/* PPM General Convolution Algorithm +** +** No redundancy in convolution matrix. Just use brute force. +** See pgm_general_convolve() for more details. +*/ + +static void +ppm_general_convolve(const float ** const rweights, + const float ** const gweights, + const float ** const bweights) { + int ccol, col; + xel** xelbuf; + xel* outputrow; + xelval r, g, b; + int row, crow; + float rsum, gsum, bsum; + xel **rowptr, *temprptr; + int toprow, temprow; + int i, irow; + int leftcol; + long temprsum, tempgsum, tempbsum; + + /* Allocate space for one convolution-matrix's worth of rows, plus + ** a row output buffer. */ + xelbuf = pnm_allocarray( cols, crows ); + outputrow = pnm_allocrow( cols ); + + /* Allocate array of pointers to xelbuf */ + rowptr = (xel **) pnm_allocarray( 1, crows ); + + pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 ); + + /* Read in one convolution-matrix's worth of image, less one row. */ + for ( row = 0; row < crows - 1; ++row ) + { + pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[row], cols, maxval, format, maxval, newformat ); + /* Write out just the part we're not going to convolve. */ + if ( row < crowso2 ) + pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 ); + } + + /* Now the rest of the image - read in the row at the end of + ** xelbuf, and convolve and write out the row in the middle. + */ + for ( ; row < rows; ++row ) + { + toprow = row + 1; + temprow = row % crows; + pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[temprow], cols, maxval, format, maxval, newformat ); + + /* Arrange rowptr to eliminate the use of mod function to determine + ** which row of xelbuf is 0...crows. Mod function can be very costly. + */ + temprow = toprow % crows; + i = 0; + for (irow = temprow; irow < crows; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + for ( col = 0; col < cols; ++col ) + { + if ( col < ccolso2 || col >= cols - ccolso2 ) + outputrow[col] = rowptr[crowso2][col]; + else + { + leftcol = col - ccolso2; + rsum = gsum = bsum = 0.0; + for ( crow = 0; crow < crows; ++crow ) + { + temprptr = rowptr[crow] + leftcol; + for ( ccol = 0; ccol < ccols; ++ccol ) + { + rsum += PPM_GETR( *(temprptr + ccol) ) + * rweights[crow][ccol]; + gsum += PPM_GETG( *(temprptr + ccol) ) + * gweights[crow][ccol]; + bsum += PPM_GETB( *(temprptr + ccol) ) + * bweights[crow][ccol]; + } + } + temprsum = rsum + 0.5; + tempgsum = gsum + 0.5; + tempbsum = bsum + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN( outputrow[col], r, g, b ); + } + } + pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 ); + } + + /* Now write out the remaining unconvolved rows in xelbuf. */ + for ( irow = crowso2 + 1; irow < crows; ++irow ) + pnm_writepnmrow( + stdout, rowptr[irow], cols, maxval, newformat, 0 ); + + } + + +/* PPM Mean Convolution +** +** Same as pgm_mean_convolve() but for PPM. +** +*/ + +static void +ppm_mean_convolve(const float ** const rweights, + const float ** const gweights, + const float ** const bweights) { + /* All weights of a single color are the same so just grab any one + of them. + */ + float const rmeanweight = rweights[0][0]; + float const gmeanweight = gweights[0][0]; + float const bmeanweight = bweights[0][0]; + + int ccol, col; + xel** xelbuf; + xel* outputrow; + xelval r, g, b; + int row, crow; + xel **rowptr, *temprptr; + int leftcol; + int i, irow; + int toprow, temprow; + int subrow, addrow; + int subcol, addcol; + long risum, gisum, bisum; + long temprsum, tempgsum, tempbsum; + int tempcol, crowsp1; + long *rcolumnsum, *gcolumnsum, *bcolumnsum; + + + + /* Allocate space for one convolution-matrix's worth of rows, plus + ** a row output buffer. MEAN uses an extra row. */ + xelbuf = pnm_allocarray( cols, crows + 1 ); + outputrow = pnm_allocrow( cols ); + + /* Allocate array of pointers to xelbuf. MEAN uses an extra row. */ + rowptr = (xel **) pnm_allocarray( 1, crows + 1); + + /* Allocate space for intermediate column sums */ + rcolumnsum = (long *) pm_allocrow( cols, sizeof(long) ); + gcolumnsum = (long *) pm_allocrow( cols, sizeof(long) ); + bcolumnsum = (long *) pm_allocrow( cols, sizeof(long) ); + for ( col = 0; col < cols; ++col ) + { + rcolumnsum[col] = 0L; + gcolumnsum[col] = 0L; + bcolumnsum[col] = 0L; + } + + pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 ); + + /* Read in one convolution-matrix's worth of image, less one row. */ + for ( row = 0; row < crows - 1; ++row ) + { + pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[row], cols, maxval, format, maxval, newformat ); + /* Write out just the part we're not going to convolve. */ + if ( row < crowso2 ) + pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 ); + } + + /* Do first real row only */ + subrow = crows; + addrow = crows - 1; + toprow = row + 1; + temprow = row % crows; + pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[temprow], cols, maxval, format, maxval, newformat ); + + temprow = toprow % crows; + i = 0; + for (irow = temprow; irow < crows; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + risum = 0L; + gisum = 0L; + bisum = 0L; + for ( col = 0; col < cols; ++col ) + { + if ( col < ccolso2 || col >= cols - ccolso2 ) + outputrow[col] = rowptr[crowso2][col]; + else if ( col == ccolso2 ) + { + leftcol = col - ccolso2; + for ( crow = 0; crow < crows; ++crow ) + { + temprptr = rowptr[crow] + leftcol; + for ( ccol = 0; ccol < ccols; ++ccol ) + { + rcolumnsum[leftcol + ccol] += + PPM_GETR( *(temprptr + ccol) ); + gcolumnsum[leftcol + ccol] += + PPM_GETG( *(temprptr + ccol) ); + bcolumnsum[leftcol + ccol] += + PPM_GETB( *(temprptr + ccol) ); + } + } + for ( ccol = 0; ccol < ccols; ++ccol) + { + risum += rcolumnsum[leftcol + ccol]; + gisum += gcolumnsum[leftcol + ccol]; + bisum += bcolumnsum[leftcol + ccol]; + } + temprsum = (float) risum * rmeanweight + 0.5; + tempgsum = (float) gisum * gmeanweight + 0.5; + tempbsum = (float) bisum * bmeanweight + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN( outputrow[col], r, g, b ); + } + else + { + /* Column numbers to subtract or add to isum */ + subcol = col - ccolso2 - 1; + addcol = col + ccolso2; + for ( crow = 0; crow < crows; ++crow ) + { + rcolumnsum[addcol] += PPM_GETR( rowptr[crow][addcol] ); + gcolumnsum[addcol] += PPM_GETG( rowptr[crow][addcol] ); + bcolumnsum[addcol] += PPM_GETB( rowptr[crow][addcol] ); + } + risum = risum - rcolumnsum[subcol] + rcolumnsum[addcol]; + gisum = gisum - gcolumnsum[subcol] + gcolumnsum[addcol]; + bisum = bisum - bcolumnsum[subcol] + bcolumnsum[addcol]; + temprsum = (float) risum * rmeanweight + 0.5; + tempgsum = (float) gisum * gmeanweight + 0.5; + tempbsum = (float) bisum * bmeanweight + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN( outputrow[col], r, g, b ); + } + } + pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 ); + + ++row; + /* For all subsequent rows do it this way as the columnsums have been + ** generated. Now we can use them to reduce further calculations. + */ + crowsp1 = crows + 1; + for ( ; row < rows; ++row ) + { + toprow = row + 1; + temprow = row % (crows + 1); + pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[temprow], cols, maxval, format, maxval, newformat ); + + /* This rearrangement using crows+1 rowptrs and xelbufs will cause + ** rowptr[0..crows-1] to always hold active xelbufs and for + ** rowptr[crows] to always hold the oldest (top most) xelbuf. + */ + temprow = (toprow + 1) % crowsp1; + i = 0; + for (irow = temprow; irow < crowsp1; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + risum = 0L; + gisum = 0L; + bisum = 0L; + for ( col = 0; col < cols; ++col ) + { + if ( col < ccolso2 || col >= cols - ccolso2 ) + outputrow[col] = rowptr[crowso2][col]; + else if ( col == ccolso2 ) + { + leftcol = col - ccolso2; + for ( ccol = 0; ccol < ccols; ++ccol ) + { + tempcol = leftcol + ccol; + rcolumnsum[tempcol] = rcolumnsum[tempcol] + - PPM_GETR( rowptr[subrow][ccol] ) + + PPM_GETR( rowptr[addrow][ccol] ); + risum += rcolumnsum[tempcol]; + gcolumnsum[tempcol] = gcolumnsum[tempcol] + - PPM_GETG( rowptr[subrow][ccol] ) + + PPM_GETG( rowptr[addrow][ccol] ); + gisum += gcolumnsum[tempcol]; + bcolumnsum[tempcol] = bcolumnsum[tempcol] + - PPM_GETB( rowptr[subrow][ccol] ) + + PPM_GETB( rowptr[addrow][ccol] ); + bisum += bcolumnsum[tempcol]; + } + temprsum = (float) risum * rmeanweight + 0.5; + tempgsum = (float) gisum * gmeanweight + 0.5; + tempbsum = (float) bisum * bmeanweight + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN( outputrow[col], r, g, b ); + } + else + { + /* Column numbers to subtract or add to isum */ + subcol = col - ccolso2 - 1; + addcol = col + ccolso2; + rcolumnsum[addcol] = rcolumnsum[addcol] + - PPM_GETR( rowptr[subrow][addcol] ) + + PPM_GETR( rowptr[addrow][addcol] ); + risum = risum - rcolumnsum[subcol] + rcolumnsum[addcol]; + gcolumnsum[addcol] = gcolumnsum[addcol] + - PPM_GETG( rowptr[subrow][addcol] ) + + PPM_GETG( rowptr[addrow][addcol] ); + gisum = gisum - gcolumnsum[subcol] + gcolumnsum[addcol]; + bcolumnsum[addcol] = bcolumnsum[addcol] + - PPM_GETB( rowptr[subrow][addcol] ) + + PPM_GETB( rowptr[addrow][addcol] ); + bisum = bisum - bcolumnsum[subcol] + bcolumnsum[addcol]; + temprsum = (float) risum * rmeanweight + 0.5; + tempgsum = (float) gisum * gmeanweight + 0.5; + tempbsum = (float) bisum * bmeanweight + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN( outputrow[col], r, g, b ); + } + } + pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 ); + } + + /* Now write out the remaining unconvolved rows in xelbuf. */ + for ( irow = crowso2 + 1; irow < crows; ++irow ) + pnm_writepnmrow( + stdout, rowptr[irow], cols, maxval, newformat, 0 ); + + } + + +/* PPM Horizontal Convolution +** +** Same as pgm_horizontal_convolve() +** +**/ + +static void +ppm_horizontal_convolve(const float ** const rweights, + const float ** const gweights, + const float ** const bweights) { + int ccol, col; + xel** xelbuf; + xel* outputrow; + xelval r, g, b; + int row, crow; + xel **rowptr, *temprptr; + int leftcol; + int i, irow; + int temprow; + int subcol, addcol; + float rsum, gsum, bsum; + int addrow, subrow; + long **rrowsum, **rrowsumptr; + long **growsum, **growsumptr; + long **browsum, **browsumptr; + int crowsp1; + long temprsum, tempgsum, tempbsum; + + /* Allocate space for one convolution-matrix's worth of rows, plus + ** a row output buffer. */ + xelbuf = pnm_allocarray( cols, crows + 1 ); + outputrow = pnm_allocrow( cols ); + + /* Allocate array of pointers to xelbuf */ + rowptr = (xel **) pnm_allocarray( 1, crows + 1); + + /* Allocate intermediate row sums. HORIZONTAL uses an extra row */ + rrowsum = (long **) pm_allocarray( cols, crows + 1, sizeof(long) ); + rrowsumptr = (long **) pnm_allocarray( 1, crows + 1); + growsum = (long **) pm_allocarray( cols, crows + 1, sizeof(long) ); + growsumptr = (long **) pnm_allocarray( 1, crows + 1); + browsum = (long **) pm_allocarray( cols, crows + 1, sizeof(long) ); + browsumptr = (long **) pnm_allocarray( 1, crows + 1); + + pnm_writepnminit( stdout, cols, rows, maxval, newformat, 0 ); + + /* Read in one convolution-matrix's worth of image, less one row. */ + for ( row = 0; row < crows - 1; ++row ) + { + pnm_readpnmrow( ifp, xelbuf[row], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[row], cols, maxval, format, maxval, newformat ); + /* Write out just the part we're not going to convolve. */ + if ( row < crowso2 ) + pnm_writepnmrow( stdout, xelbuf[row], cols, maxval, newformat, 0 ); + } + + /* First row only */ + temprow = row % crows; + pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[temprow], cols, maxval, format, maxval, newformat ); + + temprow = (row + 1) % crows; + i = 0; + for (irow = temprow; irow < crows; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + for ( crow = 0; crow < crows; ++crow ) + { + rrowsumptr[crow] = rrowsum[crow]; + growsumptr[crow] = growsum[crow]; + browsumptr[crow] = browsum[crow]; + } + + for ( col = 0; col < cols; ++col ) + { + if ( col < ccolso2 || col >= cols - ccolso2 ) + outputrow[col] = rowptr[crowso2][col]; + else if ( col == ccolso2 ) + { + leftcol = col - ccolso2; + rsum = 0.0; + gsum = 0.0; + bsum = 0.0; + for ( crow = 0; crow < crows; ++crow ) + { + temprptr = rowptr[crow] + leftcol; + rrowsumptr[crow][leftcol] = 0L; + growsumptr[crow][leftcol] = 0L; + browsumptr[crow][leftcol] = 0L; + for ( ccol = 0; ccol < ccols; ++ccol ) + { + rrowsumptr[crow][leftcol] += + PPM_GETR( *(temprptr + ccol) ); + growsumptr[crow][leftcol] += + PPM_GETG( *(temprptr + ccol) ); + browsumptr[crow][leftcol] += + PPM_GETB( *(temprptr + ccol) ); + } + rsum += rrowsumptr[crow][leftcol] * rweights[crow][0]; + gsum += growsumptr[crow][leftcol] * gweights[crow][0]; + bsum += browsumptr[crow][leftcol] * bweights[crow][0]; + } + temprsum = rsum + 0.5; + tempgsum = gsum + 0.5; + tempbsum = bsum + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN( outputrow[col], r, g, b ); + } + else + { + rsum = 0.0; + gsum = 0.0; + bsum = 0.0; + leftcol = col - ccolso2; + subcol = col - ccolso2 - 1; + addcol = col + ccolso2; + for ( crow = 0; crow < crows; ++crow ) + { + rrowsumptr[crow][leftcol] = rrowsumptr[crow][subcol] + - PPM_GETR( rowptr[crow][subcol] ) + + PPM_GETR( rowptr[crow][addcol] ); + rsum += rrowsumptr[crow][leftcol] * rweights[crow][0]; + growsumptr[crow][leftcol] = growsumptr[crow][subcol] + - PPM_GETG( rowptr[crow][subcol] ) + + PPM_GETG( rowptr[crow][addcol] ); + gsum += growsumptr[crow][leftcol] * gweights[crow][0]; + browsumptr[crow][leftcol] = browsumptr[crow][subcol] + - PPM_GETB( rowptr[crow][subcol] ) + + PPM_GETB( rowptr[crow][addcol] ); + bsum += browsumptr[crow][leftcol] * bweights[crow][0]; + } + temprsum = rsum + 0.5; + tempgsum = gsum + 0.5; + tempbsum = bsum + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN( outputrow[col], r, g, b ); + } + } + pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 ); + + + /* For all subsequent rows */ + + subrow = crows; + addrow = crows - 1; + crowsp1 = crows + 1; + ++row; + for ( ; row < rows; ++row ) + { + temprow = row % crowsp1; + pnm_readpnmrow( ifp, xelbuf[temprow], cols, maxval, format ); + if ( PNM_FORMAT_TYPE(format) != newformat ) + pnm_promoteformatrow( + xelbuf[temprow], cols, maxval, format, maxval, newformat ); + + temprow = (row + 2) % crowsp1; + i = 0; + for (irow = temprow; irow < crowsp1; ++i, ++irow) + { + rowptr[i] = xelbuf[irow]; + rrowsumptr[i] = rrowsum[irow]; + growsumptr[i] = growsum[irow]; + browsumptr[i] = browsum[irow]; + } + for (irow = 0; irow < temprow; ++irow, ++i) + { + rowptr[i] = xelbuf[irow]; + rrowsumptr[i] = rrowsum[irow]; + growsumptr[i] = growsum[irow]; + browsumptr[i] = browsum[irow]; + } + + for ( col = 0; col < cols; ++col ) + { + if ( col < ccolso2 || col >= cols - ccolso2 ) + outputrow[col] = rowptr[crowso2][col]; + else if ( col == ccolso2 ) + { + rsum = 0.0; + gsum = 0.0; + bsum = 0.0; + leftcol = col - ccolso2; + rrowsumptr[addrow][leftcol] = 0L; + growsumptr[addrow][leftcol] = 0L; + browsumptr[addrow][leftcol] = 0L; + for ( ccol = 0; ccol < ccols; ++ccol ) + { + rrowsumptr[addrow][leftcol] += + PPM_GETR( rowptr[addrow][leftcol + ccol] ); + growsumptr[addrow][leftcol] += + PPM_GETG( rowptr[addrow][leftcol + ccol] ); + browsumptr[addrow][leftcol] += + PPM_GETB( rowptr[addrow][leftcol + ccol] ); + } + for ( crow = 0; crow < crows; ++crow ) + { + rsum += rrowsumptr[crow][leftcol] * rweights[crow][0]; + gsum += growsumptr[crow][leftcol] * gweights[crow][0]; + bsum += browsumptr[crow][leftcol] * bweights[crow][0]; + } + temprsum = rsum + 0.5; + tempgsum = gsum + 0.5; + tempbsum = bsum + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN( outputrow[col], r, g, b ); + } + else + { + rsum = 0.0; + gsum = 0.0; + bsum = 0.0; + leftcol = col - ccolso2; + subcol = col - ccolso2 - 1; + addcol = col + ccolso2; + rrowsumptr[addrow][leftcol] = rrowsumptr[addrow][subcol] + - PPM_GETR( rowptr[addrow][subcol] ) + + PPM_GETR( rowptr[addrow][addcol] ); + growsumptr[addrow][leftcol] = growsumptr[addrow][subcol] + - PPM_GETG( rowptr[addrow][subcol] ) + + PPM_GETG( rowptr[addrow][addcol] ); + browsumptr[addrow][leftcol] = browsumptr[addrow][subcol] + - PPM_GETB( rowptr[addrow][subcol] ) + + PPM_GETB( rowptr[addrow][addcol] ); + for ( crow = 0; crow < crows; ++crow ) + { + rsum += rrowsumptr[crow][leftcol] * rweights[crow][0]; + gsum += growsumptr[crow][leftcol] * gweights[crow][0]; + bsum += browsumptr[crow][leftcol] * bweights[crow][0]; + } + temprsum = rsum + 0.5; + tempgsum = gsum + 0.5; + tempbsum = bsum + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN( outputrow[col], r, g, b ); + } + } + pnm_writepnmrow( stdout, outputrow, cols, maxval, newformat, 0 ); + } + + /* Now write out the remaining unconvolved rows in xelbuf. */ + for ( irow = crowso2 + 1; irow < crows; ++irow ) + pnm_writepnmrow( + stdout, rowptr[irow], cols, maxval, newformat, 0 ); + + } + + +/* PPM Vertical Convolution +** +** Same as pgm_vertical_convolve() +** +*/ + +static void +ppm_vertical_convolve(const float ** const rweights, + const float ** const gweights, + const float ** const bweights) { + int ccol, col; + xel** xelbuf; + xel* outputrow; + xelval r, g, b; + int row, crow; + xel **rowptr, *temprptr; + int i, irow; + int toprow, temprow; + int subrow, addrow; + int tempcol; + long *rcolumnsum, *gcolumnsum, *bcolumnsum; + int crowsp1; + int addcol; + long temprsum, tempgsum, tempbsum; + + /* Allocate space for one convolution-matrix's worth of rows, plus + ** a row output buffer. VERTICAL uses an extra row. */ + xelbuf = pnm_allocarray(cols, crows + 1); + outputrow = pnm_allocrow(cols); + + /* Allocate array of pointers to xelbuf */ + rowptr = (xel **) pnm_allocarray(1, crows + 1); + + /* Allocate space for intermediate column sums */ + MALLOCARRAY_NOFAIL(rcolumnsum, cols); + MALLOCARRAY_NOFAIL(gcolumnsum, cols); + MALLOCARRAY_NOFAIL(bcolumnsum, cols); + + for (col = 0; col < cols; ++col) { + rcolumnsum[col] = 0L; + gcolumnsum[col] = 0L; + bcolumnsum[col] = 0L; + } + + pnm_writepnminit(stdout, cols, rows, maxval, newformat, 0); + + /* Read in one convolution-matrix's worth of image, less one row. */ + for (row = 0; row < crows - 1; ++row) { + pnm_readpnmrow(ifp, xelbuf[row], cols, maxval, format); + if (PNM_FORMAT_TYPE(format) != newformat) + pnm_promoteformatrow(xelbuf[row], cols, maxval, format, + maxval, newformat); + /* Write out just the part we're not going to convolve. */ + if (row < crowso2) + pnm_writepnmrow(stdout, xelbuf[row], cols, maxval, newformat, 0); + } + + /* Now the rest of the image - read in the row at the end of + ** xelbuf, and convolve and write out the row in the middle. + */ + /* For first row only */ + + toprow = row + 1; + temprow = row % crows; + pnm_readpnmrow(ifp, xelbuf[temprow], cols, maxval, format); + if (PNM_FORMAT_TYPE(format) != newformat) + pnm_promoteformatrow(xelbuf[temprow], cols, maxval, format, maxval, + newformat); + + /* Arrange rowptr to eliminate the use of mod function to determine + ** which row of xelbuf is 0...crows. Mod function can be very costly. + */ + temprow = toprow % crows; + i = 0; + for (irow = temprow; irow < crows; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + for (col = 0; col < cols; ++col) { + if (col < ccolso2 || col >= cols - ccolso2) + outputrow[col] = rowptr[crowso2][col]; + else if (col == ccolso2) { + int const leftcol = col - ccolso2; + float rsum, gsum, bsum; + rsum = 0.0; + gsum = 0.0; + bsum = 0.0; + for (crow = 0; crow < crows; ++crow) { + temprptr = rowptr[crow] + leftcol; + for (ccol = 0; ccol < ccols; ++ccol) { + rcolumnsum[leftcol + ccol] += + PPM_GETR(*(temprptr + ccol)); + gcolumnsum[leftcol + ccol] += + PPM_GETG(*(temprptr + ccol)); + bcolumnsum[leftcol + ccol] += + PPM_GETB(*(temprptr + ccol)); + } + } + for (ccol = 0; ccol < ccols; ++ccol) { + rsum += rcolumnsum[leftcol + ccol] * rweights[0][ccol]; + gsum += gcolumnsum[leftcol + ccol] * gweights[0][ccol]; + bsum += bcolumnsum[leftcol + ccol] * bweights[0][ccol]; + } + temprsum = rsum + 0.5; + tempgsum = gsum + 0.5; + tempbsum = bsum + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN(outputrow[col], r, g, b); + } else { + int const leftcol = col - ccolso2; + float rsum, gsum, bsum; + rsum = 0.0; + gsum = 0.0; + bsum = 0.0; + addcol = col + ccolso2; + for (crow = 0; crow < crows; ++crow) { + rcolumnsum[addcol] += PPM_GETR( rowptr[crow][addcol]); + gcolumnsum[addcol] += PPM_GETG( rowptr[crow][addcol]); + bcolumnsum[addcol] += PPM_GETB( rowptr[crow][addcol]); + } + for (ccol = 0; ccol < ccols; ++ccol) { + rsum += rcolumnsum[leftcol + ccol] * rweights[0][ccol]; + gsum += gcolumnsum[leftcol + ccol] * gweights[0][ccol]; + bsum += bcolumnsum[leftcol + ccol] * bweights[0][ccol]; + } + temprsum = rsum + 0.5; + tempgsum = gsum + 0.5; + tempbsum = bsum + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN(outputrow[col], r, g, b); + } + } + pnm_writepnmrow(stdout, outputrow, cols, maxval, newformat, 0); + + /* For all subsequent rows */ + subrow = crows; + addrow = crows - 1; + crowsp1 = crows + 1; + ++row; + for (; row < rows; ++row) { + toprow = row + 1; + temprow = row % (crows +1); + pnm_readpnmrow(ifp, xelbuf[temprow], cols, maxval, format); + if (PNM_FORMAT_TYPE(format) != newformat) + pnm_promoteformatrow(xelbuf[temprow], cols, maxval, format, + maxval, newformat); + + /* Arrange rowptr to eliminate the use of mod function to determine + ** which row of xelbuf is 0...crows. Mod function can be very costly. + */ + temprow = (toprow + 1) % crowsp1; + i = 0; + for (irow = temprow; irow < crowsp1; ++i, ++irow) + rowptr[i] = xelbuf[irow]; + for (irow = 0; irow < temprow; ++irow, ++i) + rowptr[i] = xelbuf[irow]; + + for (col = 0; col < cols; ++col) { + if (col < ccolso2 || col >= cols - ccolso2) + outputrow[col] = rowptr[crowso2][col]; + else if (col == ccolso2) { + int const leftcol = col - ccolso2; + float rsum, gsum, bsum; + rsum = 0.0; + gsum = 0.0; + bsum = 0.0; + + for (ccol = 0; ccol < ccols; ++ccol) { + tempcol = leftcol + ccol; + rcolumnsum[tempcol] = rcolumnsum[tempcol] + - PPM_GETR(rowptr[subrow][ccol]) + + PPM_GETR(rowptr[addrow][ccol]); + rsum = rsum + rcolumnsum[tempcol] * rweights[0][ccol]; + gcolumnsum[tempcol] = gcolumnsum[tempcol] + - PPM_GETG(rowptr[subrow][ccol]) + + PPM_GETG(rowptr[addrow][ccol]); + gsum = gsum + gcolumnsum[tempcol] * gweights[0][ccol]; + bcolumnsum[tempcol] = bcolumnsum[tempcol] + - PPM_GETB(rowptr[subrow][ccol]) + + PPM_GETB(rowptr[addrow][ccol]); + bsum = bsum + bcolumnsum[tempcol] * bweights[0][ccol]; + } + temprsum = rsum + 0.5; + tempgsum = gsum + 0.5; + tempbsum = bsum + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN(outputrow[col], r, g, b); + } else { + int const leftcol = col - ccolso2; + float rsum, gsum, bsum; + rsum = 0.0; + gsum = 0.0; + bsum = 0.0; + addcol = col + ccolso2; + rcolumnsum[addcol] = rcolumnsum[addcol] + - PPM_GETR(rowptr[subrow][addcol]) + + PPM_GETR(rowptr[addrow][addcol]); + gcolumnsum[addcol] = gcolumnsum[addcol] + - PPM_GETG(rowptr[subrow][addcol]) + + PPM_GETG(rowptr[addrow][addcol]); + bcolumnsum[addcol] = bcolumnsum[addcol] + - PPM_GETB(rowptr[subrow][addcol]) + + PPM_GETB(rowptr[addrow][addcol]); + for (ccol = 0; ccol < ccols; ++ccol) { + rsum += rcolumnsum[leftcol + ccol] * rweights[0][ccol]; + gsum += gcolumnsum[leftcol + ccol] * gweights[0][ccol]; + bsum += bcolumnsum[leftcol + ccol] * bweights[0][ccol]; + } + temprsum = rsum + 0.5; + tempgsum = gsum + 0.5; + tempbsum = bsum + 0.5; + CHECK_RED; + CHECK_GREEN; + CHECK_BLUE; + PPM_ASSIGN(outputrow[col], r, g, b); + } + } + pnm_writepnmrow(stdout, outputrow, cols, maxval, newformat, 0); + } + + /* Now write out the remaining unconvolved rows in xelbuf. */ + for (irow = crowso2 + 1; irow < crows; ++irow) + pnm_writepnmrow(stdout, rowptr[irow], cols, maxval, newformat, 0); + +} + + + +static void +determineConvolveType(xel * const * const cxels, + struct convolveType * const typeP) { +/*---------------------------------------------------------------------------- + Determine which form of convolution is best. The general form always + works, but with some special case convolution matrices, faster forms + of convolution are possible. + + We don't check for the case that one of the PPM colors can have + differing types. We handle only cases where all PPMs are of the same + special case. +-----------------------------------------------------------------------------*/ + int horizontal, vertical; + int tempcxel, rtempcxel, gtempcxel, btempcxel; + int crow, ccol; + + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + horizontal = TRUE; /* initial assumption */ + crow = 0; + while (horizontal && (crow < crows)) { + ccol = 1; + rtempcxel = PPM_GETR(cxels[crow][0]); + gtempcxel = PPM_GETG(cxels[crow][0]); + btempcxel = PPM_GETB(cxels[crow][0]); + while (horizontal && (ccol < ccols)) { + if ((PPM_GETR(cxels[crow][ccol]) != rtempcxel) || + (PPM_GETG(cxels[crow][ccol]) != gtempcxel) || + (PPM_GETB(cxels[crow][ccol]) != btempcxel)) + horizontal = FALSE; + ++ccol; + } + ++crow; + } + + vertical = TRUE; /* initial assumption */ + ccol = 0; + while (vertical && (ccol < ccols)) { + crow = 1; + rtempcxel = PPM_GETR(cxels[0][ccol]); + gtempcxel = PPM_GETG(cxels[0][ccol]); + btempcxel = PPM_GETB(cxels[0][ccol]); + while (vertical && (crow < crows)) { + if ((PPM_GETR(cxels[crow][ccol]) != rtempcxel) | + (PPM_GETG(cxels[crow][ccol]) != gtempcxel) | + (PPM_GETB(cxels[crow][ccol]) != btempcxel)) + vertical = FALSE; + ++crow; + } + ++ccol; + } + break; + + default: + horizontal = TRUE; /* initial assumption */ + crow = 0; + while (horizontal && (crow < crows)) { + ccol = 1; + tempcxel = PNM_GET1(cxels[crow][0]); + while (horizontal && (ccol < ccols)) { + if (PNM_GET1(cxels[crow][ccol]) != tempcxel) + horizontal = FALSE; + ++ccol; + } + ++crow; + } + + vertical = TRUE; /* initial assumption */ + ccol = 0; + while (vertical && (ccol < ccols)) { + crow = 1; + tempcxel = PNM_GET1(cxels[0][ccol]); + while (vertical && (crow < crows)) { + if (PNM_GET1(cxels[crow][ccol]) != tempcxel) + vertical = FALSE; + ++crow; + } + ++ccol; + } + break; + } + + /* Which type do we have? */ + if (horizontal && vertical) { + typeP->ppmConvolver = ppm_mean_convolve; + typeP->pgmConvolver = pgm_mean_convolve; + } else if (horizontal) { + typeP->ppmConvolver = ppm_horizontal_convolve; + typeP->pgmConvolver = pgm_horizontal_convolve; + } else if (vertical) { + typeP->ppmConvolver = ppm_vertical_convolve; + typeP->pgmConvolver = pgm_vertical_convolve; + } else { + typeP->ppmConvolver = ppm_general_convolve; + typeP->pgmConvolver = pgm_general_convolve; + } +} + + + +static void +convolveIt(int const format, + struct convolveType const convolveType, + const float** const rweights, + const float** const gweights, + const float** const bweights) { + + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + convolveType.ppmConvolver(rweights, gweights, bweights); + break; + + default: + convolveType.pgmConvolver(gweights); + } +} + + + +int +main(int argc, char * argv[]) { + + struct cmdlineInfo cmdline; + FILE* cifp; + xel** cxels; + int cformat; + xelval cmaxval; + struct convolveType convolveType; + float ** rweights; + float ** gweights; + float ** bweights; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + cifp = pm_openr(cmdline.kernelFilespec); + + /* Read in the convolution matrix. */ + cxels = pnm_readpnm(cifp, &ccols, &crows, &cmaxval, &cformat); + pm_close(cifp); + + if (ccols % 2 != 1 || crows % 2 != 1) + pm_error("the convolution matrix must have an odd number of " + "rows and columns" ); + + ccolso2 = ccols / 2; + crowso2 = crows / 2; + + ifp = pm_openr(cmdline.inputFilespec); + + pnm_readpnminit(ifp, &cols, &rows, &maxval, &format); + if (cols < ccols || rows < crows) + pm_error("the image is smaller than the convolution matrix" ); + + newformat = MAX(PNM_FORMAT_TYPE(cformat), PNM_FORMAT_TYPE(format)); + if (PNM_FORMAT_TYPE(cformat) != newformat) + pnm_promoteformat(cxels, ccols, crows, cmaxval, cformat, + cmaxval, newformat); + if (PNM_FORMAT_TYPE(format) != newformat) { + switch (PNM_FORMAT_TYPE(newformat)) { + case PPM_TYPE: + if (PNM_FORMAT_TYPE(format) != newformat) + pm_message("promoting to PPM"); + break; + case PGM_TYPE: + if (PNM_FORMAT_TYPE(format) != newformat) + pm_message("promoting to PGM"); + break; + } + } + + computeWeights(cxels, ccols, crows, newformat, cmaxval, !cmdline.nooffset, + &rweights, &gweights, &bweights); + + /* Handle certain special cases when runtime can be improved. */ + + determineConvolveType(cxels, &convolveType); + + convolveIt(format, convolveType, + (const float **)rweights, + (const float **)gweights, + (const float **)bweights); + + pm_close(stdout); + pm_close(ifp); + return 0; +} + + + +/****************************************************************************** + SOME CHANGE HISTORY +******************************************************************************* + + Version 2.0.1 Changes + --------------------- + Fixed four lines that were improperly allocated as sizeof( float ) when they + should have been sizeof( long ). + + Version 2.0 Changes + ------------------- + + Version 2.0 was written by Mike Burns (derived from Jef Poskanzer's + original) in January 1995. + + Reduce run time by general optimizations and handling special cases of + convolution matrices. Program automatically determines if convolution + matrix is one of the types it can make use of so no extra command line + arguments are necessary. + + Examples of convolution matrices for the special cases are + + Mean Horizontal Vertical + x x x x x x x y z + x x x y y y x y z + x x x z z z x y z + + I don't know if the horizontal and vertical ones are of much use, but + after working on the mean convolution, it gave me ideas for the other two. + + Some other compiler dependent optimizations + ------------------------------------------- + Created separate functions as code was getting too large to put keep both + PGM and PPM cases in same function and also because SWITCH statement in + inner loop can take progressively more time the larger the size of the + convolution matrix. GCC is affected this way. + + Removed use of MOD (%) operator from innermost loop by modifying manner in + which the current xelbuf[] is chosen. + + This is from the file pnmconvol.README, dated August 1995, extracted in + April 2000, which was in the March 1994 Netpbm release: + + ----------------------------------------------------------------------------- + This is a faster version of the pnmconvol.c program that comes with netpbm. + There are no changes to the command line arguments, so this program can be + dropped in without affecting the way you currently run it. An updated man + page is also included. + + My original intention was to improve the running time of applying a + neighborhood averaging convolution matrix to an image by using a different + algorithm, but I also improved the run time of performing the general + convolution by optimizing that code. The general convolution runs in 1/4 to + 1/2 of the original time and neighborhood averaging runs in near constant + time for the convolution masks I tested (3x3, 5x5, 7x7, 9x9). + + Sample times for two computers are below. Times are in seconds as reported + by /bin/time for a 512x512 pgm image. + + Matrix IBM RS6000 SUN IPC + Size & Type 220 + + 3x3 + original pnmconvol 6.3 18.4 + new general case 3.1 6.0 + new average case 1.8 2.6 + + 5x5 + original pnmconvol 11.9 44.4 + new general case 5.6 11.9 + new average case 1.8 2.6 + + 7x7 + original pnmconvol 20.3 82.9 + new general case 9.4 20.7 + new average case 1.8 2.6 + + 9x9 + original pnmconvol 30.9 132.4 + new general case 14.4 31.8 + new average case 1.8 2.6 + + + Send all questions/comments/bugs to me at burns@chem.psu.edu. + + - Mike + + ---------------------------------------------------------------------------- + Mike Burns System Administrator + burns@chem.psu.edu Department of Chemistry + (814) 863-2123 The Pennsylvania State University + ---------------------------------------------------------------------------- + +*/ diff --git a/editor/pnmcrop.c b/editor/pnmcrop.c new file mode 100644 index 00000000..5fafbaac --- /dev/null +++ b/editor/pnmcrop.c @@ -0,0 +1,613 @@ +/* pnmcrop.c - crop a portable anymap +** +** Copyright (C) 1988 by Jef Poskanzer. +** +** 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. +*/ + +/* IDEA FOR EFFICIENCY IMPROVEMENT: + + If we have to read the input into a regular file because it is not + seekable (a pipe), find the borders as we do the copy, so that we + do 2 passes through the file instead of 3. Also find the background + color in that pass to save yet another pass with -sides. +*/ + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <assert.h> + +#include "pnm.h" +#include "shhopt.h" +#include "mallocvar.h" + +enum bg_choice {BG_BLACK, BG_WHITE, BG_DEFAULT, BG_SIDES}; + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char * inputFilespec; + enum bg_choice background; + unsigned int left, right, top, bottom; + unsigned int verbose; + unsigned int margin; + const char * borderfile; /* NULL if none */ +}; + + + +static void +parseCommandLine(int argc, char ** argv, + struct cmdlineInfo *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 blackOpt, whiteOpt, sidesOpt; + unsigned int marginSpec, borderfileSpec; + + unsigned int option_def_index; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "black", OPT_FLAG, NULL, &blackOpt, 0); + OPTENT3(0, "white", OPT_FLAG, NULL, &whiteOpt, 0); + OPTENT3(0, "sides", OPT_FLAG, NULL, &sidesOpt, 0); + OPTENT3(0, "left", OPT_FLAG, NULL, &cmdlineP->left, 0); + OPTENT3(0, "right", OPT_FLAG, NULL, &cmdlineP->right, 0); + OPTENT3(0, "top", OPT_FLAG, NULL, &cmdlineP->top, 0); + OPTENT3(0, "bottom", OPT_FLAG, NULL, &cmdlineP->bottom, 0); + OPTENT3(0, "verbose", OPT_FLAG, NULL, &cmdlineP->verbose, 0); + OPTENT3(0, "margin", OPT_UINT, &cmdlineP->margin, + &marginSpec, 0); + OPTENT3(0, "borderfile", OPT_STRING, &cmdlineP->borderfile, + &borderfileSpec, 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 */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (argc-1 == 0) + cmdlineP->inputFilespec = "-"; /* stdin */ + else if (argc-1 == 1) + cmdlineP->inputFilespec = argv[1]; + else + pm_error("Too many arguments (%d). " + "Only need one: the input filespec", argc-1); + + if (blackOpt && whiteOpt) + pm_error("You cannot specify both -black and -white"); + else if (sidesOpt &&( blackOpt || whiteOpt )) + pm_error("You cannot specify both -sides and either -black or -white"); + else if (blackOpt) + cmdlineP->background = BG_BLACK; + else if (whiteOpt) + cmdlineP->background = BG_WHITE; + else if (sidesOpt) + cmdlineP->background = BG_SIDES; + else + cmdlineP->background = BG_DEFAULT; + + if (!cmdlineP->left && !cmdlineP->right && !cmdlineP->top + && !cmdlineP->bottom) { + cmdlineP->left = cmdlineP->right = cmdlineP->top + = cmdlineP->bottom = TRUE; + } + + if (!marginSpec) + cmdlineP->margin = 0; + + if (!borderfileSpec) + cmdlineP->borderfile = NULL; +} + + + +static xel +background3Corners(FILE * const ifP, + int const rows, + int const cols, + pixval const maxval, + int const format) { +/*---------------------------------------------------------------------------- + Read in the whole image, and check all the corners to determine the + background color. This is a quite reliable way to determine the + background color. + + Expect the file to be positioned to the start of the raster, and leave + it positioned arbitrarily. +----------------------------------------------------------------------------*/ + int row; + xel ** xels; + xel background; /* our return value */ + + xels = pnm_allocarray(cols, rows); + + for (row = 0; row < rows; ++row) + pnm_readpnmrow( ifP, xels[row], cols, maxval, format ); + + background = pnm_backgroundxel(xels, cols, rows, maxval, format); + + pnm_freearray(xels, rows); + + return background; +} + + + +static xel +background2Corners(FILE * const ifP, + int const cols, + pixval const maxval, + int const format) { +/*---------------------------------------------------------------------------- + Look at just the top row of pixels and determine the background + color from the top corners; often this is enough to accurately + determine the background color. + + Expect the file to be positioned to the start of the raster, and leave + it positioned arbitrarily. +----------------------------------------------------------------------------*/ + xel *xelrow; + xel background; /* our return value */ + + xelrow = pnm_allocrow(cols); + + pnm_readpnmrow(ifP, xelrow, cols, maxval, format); + + background = pnm_backgroundxelrow(xelrow, cols, maxval, format); + + pnm_freerow(xelrow); + + return background; +} + + + +static xel +computeBackground(FILE * const ifP, + int const cols, + int const rows, + xelval const maxval, + int const format, + enum bg_choice const backgroundChoice, + int const verbose) { +/*---------------------------------------------------------------------------- + Determine what color is the background color of the image in file + *ifP, which is described by 'cols', 'rows', 'maxval', and 'format'. + + 'backgroundChoice' is the method we are to use in determining the + background color. + + Expect the file to be positioned to the start of the raster, and leave + it positioned arbitrarily. +-----------------------------------------------------------------------------*/ + xel background; /* Our return value */ + + switch (backgroundChoice) { + case BG_WHITE: + background = pnm_whitexel(maxval, format); + break; + case BG_BLACK: + background = pnm_blackxel(maxval, format); + break; + case BG_SIDES: + background = + background3Corners(ifP, rows, cols, maxval, format); + break; + case BG_DEFAULT: + background = + background2Corners(ifP, cols, maxval, format); + break; + } + + if (verbose) + pm_message("Background color is %s", + ppm_colorname(&background, maxval, TRUE /*hexok*/)); + + return(background); +} + + + +static void +findBordersInImage(FILE * const ifP, + unsigned int const cols, + unsigned int const rows, + xelval const maxval, + int const format, + xel const backgroundColor, + bool const verbose, + bool * const hasBordersP, + unsigned int * const leftP, + unsigned int * const rightP, + unsigned int * const topP, + unsigned int * const bottomP) { +/*---------------------------------------------------------------------------- + Find the left, right, top, and bottom borders in the image 'ifP'. + Return their sizes in pixels as *leftP, *rightP, *topP, and *bottomP. + + Iff the image is all background, *hasBordersP == FALSE. + + Expect the input file to be positioned to the beginning of the + image raster and leave it positioned arbitrarily. +-----------------------------------------------------------------------------*/ + xel* xelrow; /* A row of the input image */ + int row; + bool gottop; + int left, right, bottom, top; + /* leftmost, etc. nonbackground pixel found so far; -1 for none */ + + xelrow = pnm_allocrow(cols); + + left = cols; /* initial value */ + right = -1; /* initial value */ + top = rows; /* initial value */ + bottom = -1; /* initial value */ + + gottop = FALSE; + for (row = 0; row < rows; ++row) { + int col; + int thisRowLeft; + int thisRowRight; + + pnm_readpnmrow(ifP, xelrow, cols, maxval, format); + + col = 0; + while (PNM_EQUAL(xelrow[col], backgroundColor) && col < cols) + ++col; + thisRowLeft = col; + + col = cols-1; + while (col >= thisRowLeft && PNM_EQUAL(xelrow[col], backgroundColor)) + --col; + thisRowRight = col + 1; + + if (thisRowLeft < cols) { + /* This row is not entirely background */ + + left = MIN(thisRowLeft, left); + right = MAX(thisRowRight, right); + + if (!gottop) { + gottop = TRUE; + top = row; + } + bottom = row + 1; /* New candidate */ + } + } + + if (right == -1) + *hasBordersP = FALSE; + else { + *hasBordersP = TRUE; + assert(right <= cols); assert(bottom <= rows); + *leftP = left - 0; + *rightP = cols - right; + *topP = top - 0; + *bottomP = rows - bottom; + } +} + + + +static void +findBordersInFile(const char * const borderFileName, + xel const backgroundColor, + bool const verbose, + bool * const hasBordersP, + unsigned int * const leftP, + unsigned int * const rightP, + unsigned int * const topP, + unsigned int * const bottomP) { + + FILE * borderFileP; + int cols; + int rows; + xelval maxval; + int format; + + borderFileP = pm_openr(borderFileName); + + pnm_readpnminit(borderFileP, &cols, &rows, &maxval, &format); + + findBordersInImage(borderFileP, cols, rows, maxval, format, + backgroundColor, verbose, hasBordersP, + leftP, rightP, topP, bottomP); + + pm_close(borderFileP); +} + + + +static void +reportOneEdge(unsigned int const oldBorderSize, + unsigned int const newBorderSize, + const char * const place) { + +#define ending(n) (((n) > 1) ? "s" : "") + + if (newBorderSize > oldBorderSize) + pm_message("Adding %u pixel%s to the %u-pixel %s border", + newBorderSize - oldBorderSize, + ending(newBorderSize - oldBorderSize), + oldBorderSize, place); + else if (newBorderSize < oldBorderSize) + pm_message("Cropping %u pixel%s from the %u-pixel %s border", + oldBorderSize - newBorderSize, + ending(oldBorderSize - newBorderSize), + oldBorderSize, place); + else + pm_message("Leaving %s border unchanged at %u pixel%s", + place, oldBorderSize, ending(oldBorderSize)); +} + + + +static void +reportCroppingParameters(unsigned int const oldLeftBorderSize, + unsigned int const oldRightBorderSize, + unsigned int const oldTopBorderSize, + unsigned int const oldBottomBorderSize, + unsigned int const newLeftBorderSize, + unsigned int const newRightBorderSize, + unsigned int const newTopBorderSize, + unsigned int const newBottomBorderSize) { + + if (oldLeftBorderSize == 0 && oldRightBorderSize == 0 && + oldTopBorderSize == 0 && oldBottomBorderSize == 0) + pm_message("No Border found."); + + reportOneEdge(oldLeftBorderSize, newLeftBorderSize, "left" ); + reportOneEdge(oldRightBorderSize, newRightBorderSize, "right" ); + reportOneEdge(oldTopBorderSize, newTopBorderSize, "top" ); + reportOneEdge(oldBottomBorderSize, newBottomBorderSize, "bottom" ); +} + + + + +static void +fillRow(xel * const xelrow, + unsigned int const cols, + xel const color) { + + unsigned int col; + + for (col = 0; col < cols; ++col) + xelrow[col] = color; +} + + + +static void +writeCropped(FILE * const ifP, + unsigned int const cols, + unsigned int const rows, + xelval const maxval, + int const format, + unsigned int const oldLeftBorder, + unsigned int const oldRightBorder, + unsigned int const oldTopBorder, + unsigned int const oldBottomBorder, + unsigned int const newLeftBorder, + unsigned int const newRightBorder, + unsigned int const newTopBorder, + unsigned int const newBottomBorder, + xel const backgroundColor, + FILE * const ofP) { + + /* In order to do cropping, padding or both at the same time, we have + a rather complicated row buffer: + + xelrow[] is both the input and the output buffer. So it contains + the foreground pixels, the original border pixels, and the new + border pixels. + + The foreground pixels are in the center of the + buffer, starting at Column 'foregroundLeft' and going to + 'foregroundRight'. + + There is space to the left of that for the larger of the input + left border and the output left border. + + Similarly, there is space to the right of the foreground pixels + for the larger of the input right border and the output right + border. + + We have to read an entire row, including the pixels we'll be + leaving out of the output, so we pick a starting location in + the buffer that lines up the first foreground pixel at + 'foregroundLeft'. + + When we output the row, we pick a starting location in the + buffer that includes the proper number of left border pixels + before 'foregroundLeft'. + + That's for the middle rows. For the top and bottom, we just use + the left portion of xelrow[], starting at 0. + */ + + unsigned int const foregroundCols = + cols - oldLeftBorder - oldRightBorder; + unsigned int const outputCols = + foregroundCols + newLeftBorder + newRightBorder; + unsigned int const foregroundRows = + rows - oldTopBorder - oldBottomBorder; + unsigned int const outputRows = + foregroundRows + newTopBorder + newBottomBorder; + + unsigned int const foregroundLeft = MAX(oldLeftBorder, newLeftBorder); + /* Index into xelrow[] of leftmost pixel of foreground */ + unsigned int const foregroundRight = foregroundLeft + foregroundCols; + /* Index into xelrow[] just past rightmost pixel of foreground */ + + unsigned int const allocCols = + foregroundRight + MAX(oldRightBorder, newRightBorder); + + xel *xelrow; + unsigned int i; + + assert(outputCols == newLeftBorder + foregroundCols + newRightBorder); + assert(outputRows == newTopBorder + foregroundRows + newBottomBorder); + + pnm_writepnminit(ofP, outputCols, outputRows, maxval, format, 0); + + xelrow = pnm_allocrow(allocCols); + + /* Read off existing top border */ + for (i = 0; i < oldTopBorder; ++i) + pnm_readpnmrow(ifP, xelrow, cols, maxval, format); + + + /* Output new top border */ + fillRow(xelrow, outputCols, backgroundColor); + for (i = 0; i < newTopBorder; ++i) + pnm_writepnmrow(ofP, xelrow, outputCols, maxval, format, 0); + + + /* Read and output foreground rows */ + for (i = 0; i < foregroundRows; ++i) { + /* Set left border pixels */ + fillRow(&xelrow[foregroundLeft - newLeftBorder], newLeftBorder, + backgroundColor); + + /* Read foreground pixels */ + pnm_readpnmrow(ifP, &(xelrow[foregroundLeft - oldLeftBorder]), cols, + maxval, format); + + /* Set right border pixels */ + fillRow(&xelrow[foregroundRight], newRightBorder, backgroundColor); + + pnm_writepnmrow(ofP, + &(xelrow[foregroundLeft - newLeftBorder]), outputCols, + maxval, format, 0); + } + + /* Read off existing bottom border */ + for (i = 0; i < oldBottomBorder; ++i) + pnm_readpnmrow(ifP, xelrow, cols, maxval, format); + + /* Output new bottom border */ + fillRow(xelrow, outputCols, backgroundColor); + for (i = 0; i < newBottomBorder; ++i) + pnm_writepnmrow(ofP, xelrow, outputCols, maxval, format, 0); + + pnm_freerow(xelrow); +} + + + +static void +determineNewBorders(struct cmdlineInfo const cmdline, + unsigned int const leftBorderSize, + unsigned int const rightBorderSize, + unsigned int const topBorderSize, + unsigned int const bottomBorderSize, + unsigned int * const newLeftSizeP, + unsigned int * const newRightSizeP, + unsigned int * const newTopSizeP, + unsigned int * const newBottomSizeP) { + + *newLeftSizeP = cmdline.left ? cmdline.margin : leftBorderSize ; + *newRightSizeP = cmdline.right ? cmdline.margin : rightBorderSize ; + *newTopSizeP = cmdline.top ? cmdline.margin : topBorderSize ; + *newBottomSizeP = cmdline.bottom ? cmdline.margin : bottomBorderSize ; +} + + + +int +main(int argc, char *argv[]) { + + struct cmdlineInfo cmdline; + FILE * ifP; + /* The program's regular input file. Could be a seekable copy of it + in a temporary file. + */ + + xelval maxval; + int format; + int rows, cols; /* dimensions of input image */ + bool hasBorders; + unsigned int oldLeftBorder, oldRightBorder, oldTopBorder, oldBottomBorder; + /* The sizes of the borders in the input image */ + unsigned int newLeftBorder, newRightBorder, newTopBorder, newBottomBorder; + /* The sizes of the borders in the output image */ + xel background; + pm_filepos rasterpos; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr_seekable(cmdline.inputFilespec); + + pnm_readpnminit(ifP, &cols, &rows, &maxval, &format); + + pm_tell2(ifP, &rasterpos, sizeof(rasterpos)); + + background = computeBackground(ifP, cols, rows, maxval, format, + cmdline.background, cmdline.verbose); + + if (cmdline.borderfile) { + findBordersInFile(cmdline.borderfile, + background, cmdline.verbose, &hasBorders, + &oldLeftBorder, &oldRightBorder, + &oldTopBorder, &oldBottomBorder); + } else { + pm_seek2(ifP, &rasterpos, sizeof(rasterpos)); + + findBordersInImage(ifP, cols, rows, maxval, format, + background, cmdline.verbose, &hasBorders, + &oldLeftBorder, &oldRightBorder, + &oldTopBorder, &oldBottomBorder); + } + if (!hasBorders) + pm_error("The image is entirely background; " + "there is nothing to crop."); + + determineNewBorders(cmdline, + oldLeftBorder, oldRightBorder, + oldTopBorder, oldBottomBorder, + &newLeftBorder, &newRightBorder, + &newTopBorder, &newBottomBorder); + + if (cmdline.verbose) + reportCroppingParameters(oldLeftBorder, oldRightBorder, + oldTopBorder, oldBottomBorder, + newLeftBorder, newRightBorder, + newTopBorder, newBottomBorder); + + pm_seek2(ifP, &rasterpos, sizeof(rasterpos)); + + writeCropped(ifP, cols, rows, maxval, format, + oldLeftBorder, oldRightBorder, + oldTopBorder, oldBottomBorder, + newLeftBorder, newRightBorder, + newTopBorder, newBottomBorder, + background, stdout); + + pm_close(stdout); + pm_close(ifP); + + return 0; +} diff --git a/editor/pnmcut.c b/editor/pnmcut.c new file mode 100644 index 00000000..a21fcffb --- /dev/null +++ b/editor/pnmcut.c @@ -0,0 +1,427 @@ + /* pnmcut.c - cut a rectangle out of a portable anymap +** +** Copyright (C) 1989 by Jef Poskanzer. +** +** 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. +*/ + +#include <limits.h> +#include "pnm.h" +#include "shhopt.h" + +#define UNSPEC INT_MAX + /* UNSPEC is the value we use for an argument that is not specified + by the user. Theoretically, the user could specify this value, + but we hope not. + */ + +struct cmdline_info { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *input_filespec; /* Filespecs of input files */ + + /* The following describe the rectangle the user wants to cut out. + the value UNSPEC for any of them indicates that value was not + specified. A negative value means relative to the far edge. + 'width' and 'height' are not negative. These specifications + do not necessarily describe a valid rectangle; they are just + what the user said. + */ + int left; + int right; + int top; + int bottom; + int width; + int height; + int pad; + + int verbose; +}; + + + +static xel black_xel; /* A black xel */ + + +static void +parse_command_line(int argc, char ** argv, + struct cmdline_info *cmdline_p) { +/*---------------------------------------------------------------------------- + Note that the file spec array we return is stored in the storage that + was passed to us as the argv array. +-----------------------------------------------------------------------------*/ + optStruct *option_def = malloc(100*sizeof(optStruct)); + /* Instructions to OptParseOptions2 on how to parse our options. + */ + optStruct2 opt; + + unsigned int option_def_index; + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENTRY(0, "left", OPT_INT, &cmdline_p->left, 0); + OPTENTRY(0, "right", OPT_INT, &cmdline_p->right, 0); + OPTENTRY(0, "top", OPT_INT, &cmdline_p->top, 0); + OPTENTRY(0, "bottom", OPT_INT, &cmdline_p->bottom, 0); + OPTENTRY(0, "width", OPT_INT, &cmdline_p->width, 0); + OPTENTRY(0, "height", OPT_INT, &cmdline_p->height, 0); + OPTENTRY(0, "pad", OPT_FLAG, &cmdline_p->pad, 0); + OPTENTRY(0, "verbose", OPT_FLAG, &cmdline_p->verbose, 0); + + /* Set the defaults */ + cmdline_p->left = UNSPEC; + cmdline_p->right = UNSPEC; + cmdline_p->top = UNSPEC; + cmdline_p->bottom = UNSPEC; + cmdline_p->width = UNSPEC; + cmdline_p->height = UNSPEC; + cmdline_p->pad = FALSE; + cmdline_p->verbose = FALSE; + + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ + opt.allowNegNum = TRUE; /* We may have parms that are negative numbers */ + + optParseOptions2(&argc, argv, opt, 0); + /* Uses and sets argc, argv, and some of *cmdline_p and others. */ + + if (cmdline_p->width < 0) + pm_error("-width may not be negative."); + if (cmdline_p->height < 0) + pm_error("-height may not be negative."); + + if ((argc-1) != 0 && (argc-1) != 1 && (argc-1) != 4 && (argc-1) != 5) + pm_error("Wrong number of arguments. " + "Must be 0, 1, 4, or 5 arguments."); + + switch (argc-1) { + case 0: + cmdline_p->input_filespec = "-"; + break; + case 1: + cmdline_p->input_filespec = argv[1]; + break; + case 4: + case 5: { + int warg, harg; /* The "width" and "height" command line arguments */ + + if (sscanf(argv[1], "%d", &cmdline_p->left) != 1) + pm_error("Invalid number for left column argument"); + if (sscanf(argv[2], "%d", &cmdline_p->top) != 1) + pm_error("Invalid number for top row argument"); + if (sscanf(argv[3], "%d", &warg) != 1) + pm_error("Invalid number for width argument"); + if (sscanf(argv[4], "%d", &harg) != 1) + pm_error("Invalid number for height argument"); + + if (warg > 0) { + cmdline_p->width = warg; + cmdline_p->right = UNSPEC; + } else { + cmdline_p->width = UNSPEC; + cmdline_p->right = warg -1; + } + if (harg > 0) { + cmdline_p->height = harg; + cmdline_p->bottom = UNSPEC; + } else { + cmdline_p->height = UNSPEC; + cmdline_p->bottom = harg - 1; + } + + if (argc-1 == 4) + cmdline_p->input_filespec = "-"; + else + cmdline_p->input_filespec = argv[5]; + break; + } + } +} + + + +static void +compute_cut_bounds(const int cols, const int rows, + const int leftarg, const int rightarg, + const int toparg, const int bottomarg, + const int widtharg, const int heightarg, + int * const leftcol_p, int * const rightcol_p, + int * const toprow_p, int * const bottomrow_p) { +/*---------------------------------------------------------------------------- + From the values given on the command line 'leftarg', 'rightarg', + 'toparg', 'bottomarg', 'widtharg', and 'heightarg', determine what + rectangle the user wants cut out. + + Any of these arguments may be UNSPEC to indicate "not specified". + Any except 'widtharg' and 'heightarg' may be negative to indicate + relative to the far edge. 'widtharg' and 'heightarg' are positive. + + Return the location of the rectangle as *leftcol_p, *rightcol_p, + *toprow_p, and *bottomrow_p. +-----------------------------------------------------------------------------*/ + + int leftcol, rightcol, toprow, bottomrow; + /* The left and right column numbers and top and bottom row numbers + specified by the user, except with negative values translated + into the actual values. + + Note that these may very well be negative themselves, such + as when the user says "column -10" and there are only 5 columns + in the image. + */ + + /* Translate negative column and row into real column and row */ + /* Exploit the fact that UNSPEC is a positive number */ + + if (leftarg >= 0) + leftcol = leftarg; + else + leftcol = cols + leftarg; + if (rightarg >= 0) + rightcol = rightarg; + else + rightcol = cols + rightarg; + if (toparg >= 0) + toprow = toparg; + else + toprow = rows + toparg; + if (bottomarg >= 0) + bottomrow = bottomarg; + else + bottomrow = rows + bottomarg; + + /* Sort out left, right, and width specifications */ + + if (leftcol == UNSPEC && rightcol == UNSPEC && widtharg == UNSPEC) { + *leftcol_p = 0; + *rightcol_p = cols - 1; + } + if (leftcol == UNSPEC && rightcol == UNSPEC && widtharg != UNSPEC) { + *leftcol_p = 0; + *rightcol_p = 0 + widtharg - 1; + } + if (leftcol == UNSPEC && rightcol != UNSPEC && widtharg == UNSPEC) { + *leftcol_p = 0; + *rightcol_p = rightcol; + } + if (leftcol == UNSPEC && rightcol != UNSPEC && widtharg != UNSPEC) { + *leftcol_p = rightcol - widtharg + 1; + *rightcol_p = rightcol; + } + if (leftcol != UNSPEC && rightcol == UNSPEC && widtharg == UNSPEC) { + *leftcol_p = leftcol; + *rightcol_p = cols - 1; + } + if (leftcol != UNSPEC && rightcol == UNSPEC && widtharg != UNSPEC) { + *leftcol_p = leftcol; + *rightcol_p = leftcol + widtharg - 1; + } + if (leftcol != UNSPEC && rightcol != UNSPEC && widtharg == UNSPEC) { + *leftcol_p = leftcol; + *rightcol_p = rightcol; + } + if (leftcol != UNSPEC && rightcol != UNSPEC && widtharg != UNSPEC) { + pm_error("You may not specify left, right, and width.\n" + "Choose at most two of these."); + } + + + /* Sort out top, bottom, and height specifications */ + + if (toprow == UNSPEC && bottomrow == UNSPEC && heightarg == UNSPEC) { + *toprow_p = 0; + *bottomrow_p = rows - 1; + } + if (toprow == UNSPEC && bottomrow == UNSPEC && heightarg != UNSPEC) { + *toprow_p = 0; + *bottomrow_p = 0 + heightarg - 1; + } + if (toprow == UNSPEC && bottomrow != UNSPEC && heightarg == UNSPEC) { + *toprow_p = 0; + *bottomrow_p = bottomrow; + } + if (toprow == UNSPEC && bottomrow != UNSPEC && heightarg != UNSPEC) { + *toprow_p = bottomrow - heightarg + 1; + *bottomrow_p = bottomrow; + } + if (toprow != UNSPEC && bottomrow == UNSPEC && heightarg == UNSPEC) { + *toprow_p = toprow; + *bottomrow_p = rows - 1; + } + if (toprow != UNSPEC && bottomrow == UNSPEC && heightarg != UNSPEC) { + *toprow_p = toprow; + *bottomrow_p = toprow + heightarg - 1; + } + if (toprow != UNSPEC && bottomrow != UNSPEC && heightarg == UNSPEC) { + *toprow_p = toprow; + *bottomrow_p = bottomrow; + } + if (toprow != UNSPEC && bottomrow != UNSPEC && heightarg != UNSPEC) { + pm_error("You may not specify top, bottom, and height.\n" + "Choose at most two of these."); + } + +} + + + +static void +reject_out_of_bounds(const int cols, const int rows, + const int leftcol, const int rightcol, + const int toprow, const int bottomrow) { + + /* Reject coordinates off the edge */ + + if (leftcol < 0) + pm_error("You have specified a left edge (%d) that is beyond\n" + "the left edge of the image (0)", leftcol); + if (rightcol > cols-1) + pm_error("You have specified a right edge (%d) that is beyond\n" + "the right edge of the image (%d)", rightcol, cols-1); + if (rightcol < 0) + pm_error("You have specified a right edge (%d) that is beyond\n" + "the left edge of the image (0)", rightcol); + if (rightcol > cols-1) + pm_error("You have specified a right edge (%d) that is beyond\n" + "the right edge of the image (%d)", rightcol, cols-1); + if (leftcol > rightcol) + pm_error("You have specified a left edge (%d) that is to the right\n" + "of the right edge you specified (%d)", + leftcol, rightcol); + + if (toprow < 0) + pm_error("You have specified a top edge (%d) that is above the top " + "edge of the image (0)", toprow); + if (bottomrow > rows-1) + pm_error("You have specified a bottom edge (%d) that is below the\n" + "bottom edge of the image (%d)", bottomrow, rows-1); + if (bottomrow < 0) + pm_error("You have specified a bottom edge (%d) that is above the\n" + "top edge of the image (0)", bottomrow); + if (bottomrow > rows-1) + pm_error("You have specified a bottom edge (%d) that is below the\n" + "bottom edge of the image (%d)", bottomrow, rows-1); + if (toprow > bottomrow) + pm_error("You have specified a top edge (%d) that is below\n" + "the bottom edge you specified (%d)", + toprow, bottomrow); +} + + + +static void +write_black_rows(FILE *outfile, const int rows, const int cols, + xel * const output_row, + const pixval maxval, const int format) { +/*---------------------------------------------------------------------------- + Write out to file 'outfile' 'rows' rows of 'cols' black xels each, + part of an image of format 'format' with maxval 'maxval'. + + Use *output_row as a buffer. It is at least 'cols' xels wide. +-----------------------------------------------------------------------------*/ + int row; + for (row = 0; row < rows; row++) { + int col; + for (col = 0; col < cols; col++) output_row[col] = black_xel; + pnm_writepnmrow(outfile, output_row, cols, maxval, format, 0); + } +} + + + +int +main(int argc, char *argv[]) { + + FILE* ifp; + xel* xelrow; /* Row from input image */ + xel* output_row; /* Row of output image */ + xelval maxval; + int rows, cols, format, row; + int leftcol, rightcol, toprow, bottomrow; + int output_cols; /* Width of output image */ + struct cmdline_info cmdline; + + pnm_init( &argc, argv ); + + parse_command_line(argc, argv, &cmdline); + + ifp = pm_openr(cmdline.input_filespec); + + pnm_readpnminit(ifp, &cols, &rows, &maxval, &format); + xelrow = pnm_allocrow(cols); + + black_xel = pnm_blackxel(maxval, format); + + compute_cut_bounds(cols, rows, + cmdline.left, cmdline.right, + cmdline.top, cmdline.bottom, + cmdline.width, cmdline.height, + &leftcol, &rightcol, &toprow, &bottomrow); + + if (!cmdline.pad) + reject_out_of_bounds(cols, rows, leftcol, rightcol, toprow, bottomrow); + + if (cmdline.verbose) { + pm_message("Image goes from Row 0, Column 0 through Row %d, Column %d", + rows-1, cols-1); + pm_message("Cutting from Row %d, Column %d through Row %d Column %d", + toprow, leftcol, bottomrow, rightcol); + } + + output_cols = rightcol-leftcol+1; + output_row = pnm_allocrow(output_cols); + + pnm_writepnminit(stdout, output_cols, bottomrow-toprow+1, + maxval, format, 0 ); + + /* Implementation note: If speed is ever an issue, we can probably + speed up significantly the non-padding case by writing a special + case loop here for the case cmdline.pad == FALSE. + */ + + /* Write out top padding */ + write_black_rows(stdout, 0 - toprow, output_cols, output_row, + maxval, format); + + /* Read input and write out rows extracted from it */ + for (row = 0; row < rows; row++) { + pnm_readpnmrow(ifp, xelrow, cols, maxval, format); + if (row >= toprow && row <= bottomrow) { + int col; + /* Put in left padding */ + for (col = leftcol; col < 0; col++) { + output_row[col-leftcol] = black_xel; + } + /* Put in extracted columns */ + for (col = MAX(leftcol, 0); col <= MIN(rightcol, cols-1); col++) { + output_row[col-leftcol] = xelrow[col]; + } + /* Put in right padding */ + for (col = MAX(cols, leftcol); col <= rightcol; col++) { + output_row[col-leftcol] = black_xel; + } + pnm_writepnmrow(stdout, output_row, output_cols, + maxval, format, 0); + } + } + /* Note that we may be tempted just to quit after reaching the bottom + of the extracted image, but that would cause a broken pipe problem + for the process that's feeding us the image. + */ + /* Write out bottom padding */ + write_black_rows(stdout, bottomrow - (rows-1), output_cols, output_row, + maxval, format); + + pnm_freerow(output_row); + pnm_freerow(xelrow); + pm_close(ifp); + pm_close(stdout); + + exit( 0 ); +} + diff --git a/editor/pnmflip b/editor/pnmflip new file mode 100755 index 00000000..6149aaa2 --- /dev/null +++ b/editor/pnmflip @@ -0,0 +1,80 @@ +#!/usr/bin/perl -w + +#============================================================================ +# This is a compatibility interface to Pamflip. +# +# It exists so existing programs and procedures that rely on Pnmflip +# syntax continue to work. You should not make new use of Pnmflip and +# if you modify an old use, you should upgrade it to use Pamflip. +# +# The one way that Pamflip is not backward compatible with Pnmflip is +# that with Pnmflip, you can do this: +# +# pnmflip -xy -tb +# +# and that causes pnmflip to do both transformations (i.e. the same thing +# as -r270). With Pamflip, you can't specify multiple (or zero) flip +# type options. Instead, you would use the -xform option: +# +# pamflip -xform=transpose,topbottom +# +#============================================================================ + +use strict; +use File::Basename; +use Cwd 'abs_path'; + +my $xformOpt; +my @miscOptions; +my $infile; + +$xformOpt = '-xform='; # initial value + +@miscOptions = (); + +foreach (@ARGV) { + if (/^-/) { + # It's an option + if (/^-lr$/ || /^-le.*$/) { + $xformOpt .= "leftright,"; + } elsif (/^-tb$/ || /^-to.*$/) { + $xformOpt .= "topbottom,"; + } elsif (/^-x.*$/ || /^-tr.*$/) { + $xformOpt .= "transpose,"; + } elsif (/^-r9.*$/ || /^-rotate9.*$/ || /^-cc.*$/) { + $xformOpt .= "transpose,topbottom,"; + } elsif (/^-r1.*$/ || /^-rotate1.*$/) { + $xformOpt .= "leftright,topbottom,"; + } elsif (/^-r2.*$/ || /^-rotate2.*$/ || /^-cw$/) { + $xformOpt .= "transpose,leftright,"; + } else { + # It's not a transformation option; could be a Netpbm common + # option. + push(@miscOptions, $_); + } + } else { + # It's a parameter + if (defined($infile)) { + print(STDERR + "You may specify at most one non-option parameter.\n"); + } else { + $infile = $_; + } + } +} + +# Finish off the -xform option by removing any trailing comma + +$/ = ','; +chomp($xformOpt); + +my $infileParm = defined($infile) ? $infile : "-"; + +# We want to get Pamflip from the same directory we came from if +# it's there. Frequently, the directory containing Netpbm programs is +# not in the PATH and we were invoked by absolute path. + +my $my_directory = abs_path(dirname($0)); +$ENV{"PATH"} = $my_directory . ":" . $ENV{"PATH"}; + +exec("pamflip", @miscOptions, $xformOpt, $infileParm); diff --git a/editor/pnmgamma.c b/editor/pnmgamma.c new file mode 100644 index 00000000..e13075ff --- /dev/null +++ b/editor/pnmgamma.c @@ -0,0 +1,753 @@ +/* pnmgamma.c - perform gamma correction on a PNM image +** +** Copyright (C) 1991 by Bill Davidson and Jef Poskanzer. +** +** 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. +*/ + +#include <math.h> +#include <ctype.h> + +#include "shhopt.h" +#include "mallocvar.h" +#include "pnm.h" + +enum transferFunction { + XF_EXP, + XF_EXP_INVERSE, + XF_BT709RAMP, + XF_BT709RAMP_INVERSE, + XF_SRGBRAMP, + XF_SRGBRAMP_INVERSE, + XF_BT709_TO_SRGB, + XF_SRGB_TO_BT709 +}; + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *filespec; /* '-' if stdin */ + enum transferFunction transferFunction; + float rgamma, ggamma, bgamma; + unsigned int maxval; + unsigned int makeNewMaxval; +}; + + + +static void +interpretOldArguments(int const argc, + char ** const argv, + float const defaultGamma, + struct cmdlineInfo * const cmdlineP) { + + /* Use the old syntax wherein the gamma values come from arguments. + If there is one argument, it's a gamma value for all three + components. If 3 arguments, it's separate gamma values. If + 2, it's a single gamma value plus a file name. If 4, it's + separate gamma values plus a file name. + */ + if (argc-1 == 0) { + cmdlineP->rgamma = defaultGamma; + cmdlineP->ggamma = defaultGamma; + cmdlineP->bgamma = defaultGamma; + cmdlineP->filespec = "-"; + } else if (argc-1 == 1) { + cmdlineP->rgamma = atof(argv[1]); + cmdlineP->ggamma = atof(argv[1]); + cmdlineP->bgamma = atof(argv[1]); + cmdlineP->filespec = "-"; + } else if (argc-1 == 2) { + cmdlineP->rgamma = atof(argv[1]); + cmdlineP->ggamma = atof(argv[1]); + cmdlineP->bgamma = atof(argv[1]); + cmdlineP->filespec = argv[2]; + } else if (argc-1 == 3) { + cmdlineP->rgamma = atof(argv[1]); + cmdlineP->ggamma = atof(argv[2]); + cmdlineP->bgamma = atof(argv[3]); + cmdlineP->filespec = "-"; + } else if (argc-1 == 4) { + cmdlineP->rgamma = atof(argv[1]); + cmdlineP->ggamma = atof(argv[2]); + cmdlineP->bgamma = atof(argv[3]); + cmdlineP->filespec = argv[4]; + } else + pm_error("Wrong number of arguments. " + "You may have 0, 1, or 3 gamma values " + "plus zero or one filename"); + + if (cmdlineP->rgamma <= 0.0 || + cmdlineP->ggamma <= 0.0 || + cmdlineP->bgamma <= 0.0 ) + pm_error("Invalid gamma value. Must be positive floating point " + "number."); +} + + + +static void +getGammaFromOpts(struct cmdlineInfo * const cmdlineP, + bool const gammaSpec, + float const gammaOpt, + bool const rgammaSpec, + bool const ggammaSpec, + bool const bgammaSpec, + float const defaultGamma) { + + if (gammaSpec) + if (gammaOpt < 0.0) + pm_error("Invalid gamma value. " + "Must be positive floating point number."); + + if (rgammaSpec) { + if (cmdlineP->rgamma < 0.0) + pm_error("Invalid gamma value. " + "Must be positive floating point number."); + } else { + if (gammaSpec) + cmdlineP->rgamma = gammaOpt; + else + cmdlineP->rgamma = defaultGamma; + } + if (ggammaSpec) { + if (cmdlineP->ggamma < 0.0) + pm_error("Invalid gamma value. " + "Must be positive floating point number."); + } else { + if (gammaSpec) + cmdlineP->ggamma = gammaOpt; + else + cmdlineP->ggamma = defaultGamma; + } + if (bgammaSpec) { + if (cmdlineP->bgamma < 0.0) + pm_error("Invalid gamma value. " + "Must be positive floating point number."); + } else { + if (gammaSpec) + cmdlineP->bgamma = gammaOpt; + else + cmdlineP->bgamma = defaultGamma; + } +} + + + +static void +parseCommandLine(int argc, char ** argv, + struct cmdlineInfo * const cmdlineP) { + + optEntry *option_def; + /* Instructions to optParseOptions3 on how to parse our options. + */ + optStruct3 opt; + + unsigned int bt709ramp, srgbramp, ungamma, bt709tosrgb, srgbtobt709; + unsigned int bt709tolinear, lineartobt709; + unsigned int gammaSpec, rgammaSpec, ggammaSpec, bgammaSpec; + float gammaOpt; + float defaultGamma; + unsigned int option_def_index; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "ungamma", OPT_FLAG, NULL, + &ungamma, 0); + OPTENT3(0, "bt709tolinear", OPT_FLAG, NULL, + &bt709tolinear, 0); + OPTENT3(0, "lineartobt709", OPT_FLAG, NULL, + &lineartobt709, 0); + OPTENT3(0, "bt709ramp", OPT_FLAG, NULL, + &bt709ramp, 0); + OPTENT3(0, "cieramp", OPT_FLAG, NULL, + &bt709ramp, 0); + OPTENT3(0, "srgbramp", OPT_FLAG, NULL, + &srgbramp, 0); + OPTENT3(0, "bt709tosrgb", OPT_FLAG, NULL, + &bt709tosrgb, 0); + OPTENT3(0, "srgbtobt709", OPT_FLAG, NULL, + &srgbtobt709, 0); + OPTENT3(0, "maxval", OPT_UINT, &cmdlineP->maxval, + &cmdlineP->makeNewMaxval, 0); + OPTENT3(0, "gamma", OPT_FLOAT, &gammaOpt, + &gammaSpec, 0); + OPTENT3(0, "rgamma", OPT_FLOAT, &cmdlineP->rgamma, + &rgammaSpec, 0); + OPTENT3(0, "ggamma", OPT_FLOAT, &cmdlineP->ggamma, + &ggammaSpec, 0); + OPTENT3(0, "bgamma", OPT_FLOAT, &cmdlineP->bgamma, + &bgammaSpec, 0); + + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ + opt.allowNegNum = TRUE; + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdline_p and others. */ + + if (bt709tolinear + lineartobt709 + bt709ramp + srgbramp + + bt709tosrgb + srgbtobt709 > 1) + pm_error("You may specify only one function option"); + else { + if (bt709tolinear) { + if (ungamma) + pm_error("You cannot specify -ungamma with -bt709tolinear"); + else + cmdlineP->transferFunction = XF_BT709RAMP_INVERSE; + } else if (lineartobt709) { + if (ungamma) + pm_error("You cannot specify -ungamma with -lineartobt709"); + else + cmdlineP->transferFunction = XF_BT709RAMP; + } else if (bt709tosrgb) { + if (ungamma) + pm_error("You cannot specify -ungamma with -bt709tosrgb"); + else + cmdlineP->transferFunction = XF_BT709_TO_SRGB; + } else if (srgbtobt709) { + if (ungamma) + pm_error("You cannot specify -ungamma with -srgbtobt709"); + else + cmdlineP->transferFunction = XF_SRGB_TO_BT709; + } else if (bt709ramp) { + if (ungamma) + cmdlineP->transferFunction = XF_BT709RAMP_INVERSE; + else + cmdlineP->transferFunction = XF_BT709RAMP; + } else if (srgbramp) { + if (ungamma) + cmdlineP->transferFunction = XF_SRGBRAMP_INVERSE; + else + cmdlineP->transferFunction = XF_SRGBRAMP; + } else { + if (ungamma) + cmdlineP->transferFunction = XF_EXP_INVERSE; + else + cmdlineP->transferFunction = XF_EXP; + } + } + + if (cmdlineP->makeNewMaxval) { + if (cmdlineP->maxval > PNM_OVERALLMAXVAL) + pm_error("Largest possible maxval is %u. You specified %u", + PNM_OVERALLMAXVAL, cmdlineP->maxval); + } + + switch (cmdlineP->transferFunction) { + case XF_BT709RAMP: + case XF_BT709RAMP_INVERSE: + case XF_SRGB_TO_BT709: + defaultGamma = 1.0/0.45; + break; + case XF_SRGBRAMP: + case XF_SRGBRAMP_INVERSE: + case XF_BT709_TO_SRGB: + /* The whole function is often approximated with + exponent 2.2 and no linear piece. We do the linear + piece, so we use the real exponent of 2.4. + */ + defaultGamma = 2.4; + break; + case XF_EXP: + case XF_EXP_INVERSE: + defaultGamma = 2.2; + break; + } + + if (bt709tolinear || lineartobt709 || bt709tosrgb || srgbtobt709) { + /* Use the new syntax wherein the gamma values come from options, + not arguments. So if there's an argument, it's a file name. + */ + getGammaFromOpts(cmdlineP, gammaSpec, gammaOpt, + rgammaSpec, ggammaSpec, bgammaSpec, defaultGamma); + + if (argc-1 < 1) + cmdlineP->filespec = "-"; + else { + cmdlineP->filespec = argv[1]; + if (argc-1 > 1) + pm_error("Too many arguments (%u). With this function, there " + "is at most one argument: the file name", argc-1); + } + } else { + if (gammaSpec || rgammaSpec || ggammaSpec || bgammaSpec) + pm_error("With this function, you specify the gamma values in " + "arguments, not with the -gamma, etc."); + interpretOldArguments(argc, argv, defaultGamma, cmdlineP); + } +} + + + +static void +buildPowGamma(xelval table[], + xelval const maxval, + xelval const newMaxval, + double const gamma) { +/*---------------------------------------------------------------------------- + Build a gamma table of size maxval+1 for the given gamma value. + + This function depends on pow(3m). If you don't have it, you can + simulate it with '#define pow(x,y) exp((y)*log(x))' provided that + you have the exponential function exp(3m) and the natural logarithm + function log(3m). I can't believe I actually remembered my log + identities. +-----------------------------------------------------------------------------*/ + xelval i; + double const oneOverGamma = 1.0 / gamma; + + for (i = 0 ; i <= maxval; ++i) { + double const normalized = ((double) i) / maxval; + /* Xel sample value normalized to 0..1 */ + double const v = pow(normalized, oneOverGamma); + table[i] = MIN((xelval)(v * newMaxval + 0.5), newMaxval); + /* denormalize, round and clip */ + } +} + + + +static void +buildBt709Gamma(xelval table[], + xelval const maxval, + xelval const newMaxval, + double const gamma) { +/*---------------------------------------------------------------------------- + Build a gamma table of size maxval+1 for the ITU Recommendation + BT.709 gamma transfer function. + + 'gamma' must be 1/0.45 for true Rec. 709. +-----------------------------------------------------------------------------*/ + double const oneOverGamma = 1.0 / gamma; + xelval i; + + /* This transfer function is linear for sample values 0 + .. maxval*.018 and an exponential for larger sample values. + The exponential is slightly stretched and translated, though, + unlike the popular pure exponential gamma transfer function. + */ + xelval const linearCutoff = (xelval) (maxval * 0.018 + 0.5); + double const linearExpansion = + (1.099 * pow(0.018, oneOverGamma) - 0.099) / 0.018; + double const maxvalScaler = (double)newMaxval/maxval; + + for (i = 0; i <= linearCutoff; ++i) + table[i] = i * linearExpansion * maxvalScaler + 0.5; + for (; i <= maxval; ++i) { + double const normalized = ((double) i) / maxval; + /* Xel sample value normalized to 0..1 */ + double const v = 1.099 * pow(normalized, oneOverGamma) - 0.099; + table[i] = MIN((xelval)(v * newMaxval + 0.5), newMaxval); + /* denormalize, round, and clip */ + } +} + + + +static void +buildBt709GammaInverse(xelval table[], + xelval const maxval, + xelval const newMaxval, + double const gamma) { +/*---------------------------------------------------------------------------- + Build a gamma table of size maxval+1 for the Inverse of the ITU + Rec. BT.709 gamma transfer function. + + 'gamma' must be 1/0.45 for true Rec. 709. +-----------------------------------------------------------------------------*/ + double const oneOverGamma = 1.0 / gamma; + xelval i; + + /* This transfer function is linear for sample values 0 + .. maxval*.018 and an exponential for larger sample values. + The exponential is slightly stretched and translated, though, + unlike the popular pure exponential gamma transfer function. + */ + + xelval const linearCutoff = (xelval) (maxval * 0.018 + 0.5); + double const linearCompression = + 0.018 / (1.099 * pow(0.018, oneOverGamma) - 0.099); + double const maxvalScaler = (double)newMaxval/maxval; + + for (i = 0; i <= linearCutoff / linearCompression; ++i) + table[i] = i * linearCompression * maxvalScaler + 0.5; + + for (; i <= maxval; ++i) { + double const normalized = ((double) i) / maxval; + /* Xel sample value normalized to 0..1 */ + double const v = pow((normalized + 0.099) / 1.099, gamma); + table[i] = MIN((xelval)(v * newMaxval + 0.5), newMaxval); + /* denormalize, round, and clip */ + } +} + + + +static void +buildSrgbGamma(xelval table[], + xelval const maxval, + xelval const newMaxval, + double const gamma) { +/*---------------------------------------------------------------------------- + Build a gamma table of size maxval+1 for the IEC SRGB gamma + transfer function (Standard IEC 61966-2-1). + + 'gamma' must be 2.4 for true SRGB +-----------------------------------------------------------------------------*/ + double const oneOverGamma = 1.0 / gamma; + xelval i; + + /* This transfer function is linear for sample values 0 + .. maxval*.040405 and an exponential for larger sample values. + The exponential is slightly stretched and translated, though, + unlike the popular pure exponential gamma transfer function. + */ + xelval const linearCutoff = (xelval) maxval * 0.0031308 + 0.5; + double const linearExpansion = + (1.055 * pow(0.0031308, oneOverGamma) - 0.055) / 0.0031308; + double const maxvalScaler = (double)newMaxval/maxval; + + for (i = 0; i <= linearCutoff; ++i) + table[i] = i * linearExpansion * maxvalScaler + 0.5; + for (; i <= maxval; ++i) { + double const normalized = ((double) i) / maxval; + /* Xel sample value normalized to 0..1 */ + double const v = 1.055 * pow(normalized, oneOverGamma) - 0.055; + table[i] = MIN((xelval)(v * newMaxval + 0.5), newMaxval); + /* denormalize, round, and clip */ + } +} + + + +static void +buildSrgbGammaInverse(xelval table[], + xelval const maxval, + xelval const newMaxval, + double const gamma) { +/*---------------------------------------------------------------------------- + Build a gamma table of size maxval+1 for the Inverse of the IEC SRGB gamma + transfer function (Standard IEC 61966-2-1). + + 'gamma' must be 2.4 for true SRGB +-----------------------------------------------------------------------------*/ + double const oneOverGamma = 1.0 / gamma; + xelval i; + + /* This transfer function is linear for sample values 0 + .. maxval*.040405 and an exponential for larger sample values. + The exponential is slightly stretched and translated, though, + unlike the popular pure exponential gamma transfer function. + */ + xelval const linearCutoff = (xelval) maxval * 0.0031308 + 0.5; + double const linearCompression = + 0.0031308 / (1.055 * pow(0.0031308, oneOverGamma) - 0.055); + double const maxvalScaler = (double)newMaxval/maxval; + + for (i = 0; i <= linearCutoff / linearCompression; ++i) + table[i] = i * linearCompression * maxvalScaler + 0.5; + for (; i <= maxval; ++i) { + double const normalized = ((double) i) / maxval; + /* Xel sample value normalized to 0..1 */ + double const v = pow((normalized + 0.055) / 1.055, gamma); + table[i] = MIN((xelval)(v * newMaxval + 0.5), newMaxval); + /* denormalize, round, and clip */ + } +} + + + +static void +buildBt709ToSrgbGamma(xelval table[], + xelval const maxval, + xelval const newMaxval, + double const gammaSrgb) { +/*---------------------------------------------------------------------------- + Build a gamma table of size maxval+1 for the combination of the + inverse of ITU Rec BT.709 and the forward SRGB gamma transfer + functions. I.e. this converts from Rec 709 to SRGB. + + 'gammaSrgb' must be 2.4 for true SRGB. +-----------------------------------------------------------------------------*/ + double const oneOverGamma709 = 0.45; + double const gamma709 = 1.0 / oneOverGamma709; + double const oneOverGammaSrgb = 1.0 / gammaSrgb; + double const normalizer = 1.0 / maxval; + + /* This transfer function is linear for sample values 0 + .. maxval*.018 and an exponential for larger sample values. + The exponential is slightly stretched and translated, though, + unlike the popular pure exponential gamma transfer function. + */ + + xelval const linearCutoff709 = (xelval) (maxval * 0.018 + 0.5); + double const linearCompression709 = + 0.018 / (1.099 * pow(0.018, oneOverGamma709) - 0.099); + + double const linearCutoffSrgb = 0.0031308; + double const linearExpansionSrgb = + (1.055 * pow(0.0031308, oneOverGammaSrgb) - 0.055) / 0.0031308; + + xelval i; + + for (i = 0; i <= maxval; ++i) { + double const normalized = i * normalizer; + /* Xel sample value normalized to 0..1 */ + double radiance; + double srgb; + + if (i < linearCutoff709 / linearCompression709) + radiance = normalized * linearCompression709; + else + radiance = pow((normalized + 0.099) / 1.099, gamma709); + + if (radiance < linearCutoffSrgb) + srgb = radiance * linearExpansionSrgb; + else + srgb = 1.055 * pow(normalized, oneOverGammaSrgb) - 0.055; + + table[i] = srgb * newMaxval + 0.5; + } +} + + + +static void +buildSrgbToBt709Gamma(xelval table[], + xelval const maxval, + xelval const newMaxval, + double const gamma709) { +/*---------------------------------------------------------------------------- + Build a gamma table of size maxval+1 for the combination of the + inverse of SRGB and the forward ITU Rec BT.709 gamma transfer + functions. I.e. this converts from SRGB to Rec 709. + + 'gamma709' must be 1/0.45 for true Rec. 709. +-----------------------------------------------------------------------------*/ + double const oneOverGamma709 = 1.0 / gamma709; + double const gammaSrgb = 2.4; + double const oneOverGammaSrgb = 1.0 / gammaSrgb; + double const normalizer = 1.0 / maxval; + + /* This transfer function is linear for sample values 0 + .. maxval*.040405 and an exponential for larger sample values. + The exponential is slightly stretched and translated, though, + unlike the popular pure exponential gamma transfer function. + */ + xelval const linearCutoffSrgb = (xelval) maxval * 0.0031308 + 0.5; + double const linearCompressionSrgb = + 0.0031308 / (1.055 * pow(0.0031308, oneOverGammaSrgb) - 0.055); + + xelval const linearCutoff709 = (xelval) (maxval * 0.018 + 0.5); + double const linearExpansion709 = + (1.099 * pow(0.018, oneOverGamma709) - 0.099) / 0.018; + + xelval i; + + for (i = 0; i <= maxval; ++i) { + double const normalized = i * normalizer; + /* Xel sample value normalized to 0..1 */ + double radiance; + double bt709; + + if (i < linearCutoffSrgb / linearCompressionSrgb) + radiance = normalized * linearCompressionSrgb; + else + radiance = pow((normalized + 0.099) / 1.099, gammaSrgb); + + if (radiance < linearCutoff709) + bt709 = radiance * linearExpansion709; + else + bt709 = 1.055 * pow(normalized, oneOverGamma709) - 0.055; + + table[i] = bt709 * newMaxval + 0.5; + } +} + + + +static void +createGammaTables(enum transferFunction const transferFunction, + xelval const maxval, + xelval const newMaxval, + double const rgamma, + double const ggamma, + double const bgamma, + xelval ** const rtableP, + xelval ** const gtableP, + xelval ** const btableP) { + + /* Allocate space for the tables. */ + MALLOCARRAY(*rtableP, maxval+1); + MALLOCARRAY(*gtableP, maxval+1); + MALLOCARRAY(*btableP, maxval+1); + if (*rtableP == NULL || *gtableP == NULL || *btableP == NULL) + pm_error("Can't get memory to make gamma transfer tables"); + + /* Build the gamma corection tables. */ + switch (transferFunction) { + case XF_BT709RAMP: { + buildBt709Gamma(*rtableP, maxval, newMaxval, rgamma); + buildBt709Gamma(*gtableP, maxval, newMaxval, ggamma); + buildBt709Gamma(*btableP, maxval, newMaxval, bgamma); + } break; + + case XF_BT709RAMP_INVERSE: { + buildBt709GammaInverse(*rtableP, maxval, newMaxval, rgamma); + buildBt709GammaInverse(*gtableP, maxval, newMaxval, ggamma); + buildBt709GammaInverse(*btableP, maxval, newMaxval, bgamma); + } break; + + case XF_SRGBRAMP: { + buildSrgbGamma(*rtableP, maxval, newMaxval, rgamma); + buildSrgbGamma(*gtableP, maxval, newMaxval, ggamma); + buildSrgbGamma(*btableP, maxval, newMaxval, bgamma); + } break; + + case XF_SRGBRAMP_INVERSE: { + buildSrgbGammaInverse(*rtableP, maxval, newMaxval, rgamma); + buildSrgbGammaInverse(*gtableP, maxval, newMaxval, ggamma); + buildSrgbGammaInverse(*btableP, maxval, newMaxval, bgamma); + } break; + + case XF_EXP: { + buildPowGamma(*rtableP, maxval, newMaxval, rgamma); + buildPowGamma(*gtableP, maxval, newMaxval, ggamma); + buildPowGamma(*btableP, maxval, newMaxval, bgamma); + } break; + + case XF_EXP_INVERSE: { + buildPowGamma(*rtableP, maxval, newMaxval, 1.0/rgamma); + buildPowGamma(*gtableP, maxval, newMaxval, 1.0/ggamma); + buildPowGamma(*btableP, maxval, newMaxval, 1.0/bgamma); + } break; + + case XF_BT709_TO_SRGB: { + buildBt709ToSrgbGamma(*rtableP, maxval, newMaxval, rgamma); + buildBt709ToSrgbGamma(*gtableP, maxval, newMaxval, ggamma); + buildBt709ToSrgbGamma(*btableP, maxval, newMaxval, bgamma); + } break; + + case XF_SRGB_TO_BT709: { + buildSrgbToBt709Gamma(*rtableP, maxval, newMaxval, rgamma); + buildSrgbToBt709Gamma(*gtableP, maxval, newMaxval, ggamma); + buildSrgbToBt709Gamma(*btableP, maxval, newMaxval, bgamma); + } break; + } +} + + + +static void +convertRaster(FILE * const ifP, + FILE * const ofP, + int const cols, + int const rows, + xelval const maxval, + int const format, + xelval const outputMaxval, + int const outputFormat, + xelval * const rtable, + xelval * const gtable, + xelval * const btable) { + + xel * xelrow; + unsigned int row; + + xelrow = pnm_allocrow(cols); + + for (row = 0; row < rows; ++row) { + pnm_readpnmrow(ifP, xelrow, cols, maxval, format); + + pnm_promoteformatrow(xelrow, cols, maxval, format, + maxval, outputFormat); + + switch (PNM_FORMAT_TYPE(outputFormat)) { + case PPM_TYPE: { + unsigned int col; + for (col = 0; col < cols; ++col) { + xelval const r = PPM_GETR(xelrow[col]); + xelval const g = PPM_GETG(xelrow[col]); + xelval const b = PPM_GETB(xelrow[col]); + PPM_ASSIGN(xelrow[col], rtable[r], gtable[g], btable[b]); + } + } break; + + case PGM_TYPE: { + unsigned int col; + for (col = 0; col < cols; ++col) { + xelval const xel = PNM_GET1(xelrow[col]); + PNM_ASSIGN1(xelrow[col], gtable[xel]); + } + } break; + default: + pm_error("Internal error. Impossible format type"); + } + pnm_writepnmrow(ofP, xelrow, cols, outputMaxval, outputFormat, 0); + } + pnm_freerow(xelrow); +} + + + +int +main(int argc, char *argv[]) { + struct cmdlineInfo cmdline; + FILE * ifP; + xelval maxval; + int rows, cols, format; + xelval outputMaxval; + int outputFormat; + xelval * rtable; + xelval * gtable; + xelval * btable; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.filespec); + + pnm_readpnminit(ifP, &cols, &rows, &maxval, &format); + + if (PNM_FORMAT_TYPE(format) == PPM_TYPE) + outputFormat = PPM_TYPE; + else if (cmdline.rgamma != cmdline.ggamma + || cmdline.ggamma != cmdline.bgamma) + outputFormat = PPM_TYPE; + else + outputFormat = PGM_TYPE; + + if (PNM_FORMAT_TYPE(format) != outputFormat) { + if (outputFormat == PPM_TYPE) + pm_message("Promoting to PPM"); + if (outputFormat == PGM_TYPE) + pm_message("Promoting to PGM"); + } + + outputMaxval = cmdline.makeNewMaxval ? cmdline.maxval : maxval; + + createGammaTables(cmdline.transferFunction, maxval, + outputMaxval, + cmdline.rgamma, cmdline.ggamma, cmdline.bgamma, + &rtable, >able, &btable); + + pnm_writepnminit(stdout, cols, rows, outputMaxval, outputFormat, 0); + + convertRaster(ifP, stdout, cols, rows, maxval, format, + outputMaxval, outputFormat, + rtable, gtable, btable); + + pm_close(ifP); + pm_close(stdout); + + return 0; +} diff --git a/editor/pnmhisteq.c b/editor/pnmhisteq.c new file mode 100644 index 00000000..2c6893bd --- /dev/null +++ b/editor/pnmhisteq.c @@ -0,0 +1,416 @@ +/* + pnmhisteq.c + + Equalize histogram for a PNM image + + By Bryan Henderson 2005.09.10, based on ideas from the program of + the same name by John Walker (kelvin@fourmilab.ch) -- March MVM. + WWW home page: http://www.fourmilab.ch/ in 1995. + + This program is contributed to the public domain by its author. +*/ + +#include <string.h> + +#include "pnm.h" +#include "shhopt.h" +#include "mallocvar.h" + + +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 gray; + const char * wmap; + const char * rmap; + unsigned int verbose; +}; + + + +static void +parseCommandLine(int argc, char ** 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 rmapSpec, wmapSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "rmap", OPT_STRING, &cmdlineP->rmap, + &rmapSpec, 0); + OPTENT3(0, "wmap", OPT_STRING, &cmdlineP->wmap, + &wmapSpec, 0); + OPTENT3(0, "gray", OPT_FLAG, NULL, + &cmdlineP->gray, 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 may have parms that are negative numbers */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + + if (!wmapSpec) + cmdlineP->wmap = NULL; + if (!rmapSpec) + cmdlineP->rmap = NULL; + + if (argc-1 < 1) + cmdlineP->inputFileName = "-"; + else { + cmdlineP->inputFileName = argv[1]; + if (argc-1 > 1) + pm_error("Too many arguments (%d). The only argument is the " + "input file name.", argc-1); + } +} + + + +static void +computeLuminosityHistogram(xel * const * const xels, + unsigned int const rows, + unsigned int const cols, + xelval const maxval, + int const format, + bool const monoOnly, + unsigned int ** const lumahistP, + xelval * const lminP, + xelval * const lmaxP, + unsigned int * const pixelCountP) { +/*---------------------------------------------------------------------------- + Scan the image and build the luminosity histogram. If the input is + a PPM, we calculate the luminosity of each pixel from its RGB + components. +-----------------------------------------------------------------------------*/ + xelval lmin, lmax; + unsigned int pixelCount; + unsigned int * lumahist; + + MALLOCARRAY(lumahist, maxval + 1); + if (lumahist == NULL) + pm_error("Out of storage allocating array for %u histogram elements", + maxval + 1); + + { + unsigned int i; + /* Initialize histogram to zeroes everywhere */ + for (i = 0; i <= maxval; ++i) + lumahist[i] = 0; + } + lmin = maxval; /* initial value */ + lmax = 0; /* initial value */ + + switch (PNM_FORMAT_TYPE(format)) { + case PGM_TYPE: + case PBM_TYPE: { + /* Compute intensity histogram */ + + unsigned int row; + + pixelCount = rows * cols; + for (row = 0; row < rows; ++row) { + unsigned int col; + for (col = 0; col < cols; ++col) { + xelval const l = PNM_GET1(xels[row][col]); + lmin = MIN(lmin, l); + lmax = MAX(lmax, l); + ++lumahist[l]; + } + } + } + break; + case PPM_TYPE: { + unsigned int row; + + for (row = 0, pixelCount = 0; row < rows; ++row) { + unsigned int col; + for (col = 0; col < cols; ++col) { + xel const thisXel = xels[row][col]; + if (!monoOnly || PPM_ISGRAY(thisXel)) { + xelval const l = PPM_LUMIN(thisXel); + + lmin = MIN(lmin, l); + lmax = MAX(lmax, l); + + ++lumahist[l]; + ++pixelCount; + } + } + } + } + break; + default: + pm_error("invalid input format format"); + } + + *lumahistP = lumahist; + *pixelCountP = pixelCount; + *lminP = lmin; + *lmaxP = lmax; +} + + + +static void +findMaxLuma(const xelval * const lumahist, + xelval const maxval, + xelval * const maxLumaP) { + + xelval maxluma; + unsigned int i; + + for (i = 0, maxluma = 0; i <= maxval; ++i) + if (lumahist[i] > 0) + maxluma = i; + + *maxLumaP = maxluma; +} + + + +static void +readMapFile(const char * const rmapFileName, + xelval const maxval, + gray * const lumamap) { + + int rmcols, rmrows; + gray rmmaxv; + int rmformat; + FILE * rmapfP; + + rmapfP = pm_openr(rmapFileName); + pgm_readpgminit(rmapfP, &rmcols, &rmrows, &rmmaxv, &rmformat); + + if (rmmaxv != maxval) + pm_error("maxval in map file (%u) different from input (%u)", + rmmaxv, maxval); + + if (rmrows != 1) + pm_error("Map must have 1 row. Yours has %u", rmrows); + + if (rmcols != maxval + 1) + pm_error("Map must have maxval + 1 (%u) columns. Yours has %u", + maxval + 1, rmcols); + + pgm_readpgmrow(rmapfP, lumamap, maxval+1, rmmaxv, rmformat); + + pm_close(rmapfP); +} + + + +static void +computeMap(const unsigned int * const lumahist, + xelval const maxval, + unsigned int const pixelCount, + gray * const lumamap) { + + /* Calculate initial histogram equalization curve. */ + + unsigned int i; + unsigned int pixsum; + xelval maxluma; + + for (i = 0, pixsum = 0; i <= maxval; ++i) { + + /* With 16 bit grays, the following calculation can + overflow a 32 bit long. So, we do it in floating + point. + */ + + lumamap[i] = ROUNDU((((double) pixsum * maxval)) / pixelCount); + + pixsum += lumahist[i]; + } + + findMaxLuma(lumahist, maxval, &maxluma); + + { + double const lscale = (double)maxval / + ((lumahist[maxluma] > 0) ? + (double) lumamap[maxluma] : (double) maxval); + + unsigned int i; + + /* Normalize so that the brightest pixels are set to maxval. */ + + for (i = 0; i <= maxval; ++i) + lumamap[i] = MIN(maxval, ROUNDU(lumamap[i] * lscale)); + } +} + + + +static void +getMapping(const char * const rmapFileName, + const unsigned int * const lumahist, + xelval const maxval, + unsigned int const pixelCount, + gray ** const lumamapP) { +/*---------------------------------------------------------------------------- + Calculate the luminosity mapping table which gives the + histogram-equalized luminosity for each original luminosity. +-----------------------------------------------------------------------------*/ + gray * lumamap; + + lumamap = pgm_allocrow(maxval+1); + + if (rmapFileName) + readMapFile(rmapFileName, maxval, lumamap); + else + computeMap(lumahist, maxval, pixelCount, lumamap); + + *lumamapP = lumamap; +} + + + +static void +reportMap(const unsigned int * const lumahist, + xelval const maxval, + const gray * const lumamap) { + + unsigned int i; + + fprintf(stderr, " Luminosity map Number of\n"); + fprintf(stderr, " Original New Pixels \n"); + + for (i = 0; i <= maxval; ++i) { + if (lumahist[i] > 0) { + fprintf(stderr,"%6d -> %6d %8u\n", i, + lumamap[i], lumahist[i]); + } + } +} + + + +static void +remap(xel ** const xels, + unsigned int const cols, + unsigned int const rows, + xelval const maxval, + int const format, + bool const monoOnly, + const gray * const lumamap) { +/*---------------------------------------------------------------------------- + Update the array 'xels' to have the new intensities. +-----------------------------------------------------------------------------*/ + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: { + unsigned int row; + for (row = 0; row < rows; ++row) { + unsigned int col; + for (col = 0; col < cols; ++col) { + xel const thisXel = xels[row][col]; + if (monoOnly && PPM_ISGRAY(thisXel)) { + /* Leave this pixel alone */ + } else { + struct hsv hsv; + xelval iv; + + hsv = ppm_hsv_from_color(thisXel, maxval); + iv = MIN(maxval, ROUNDU(hsv.v * maxval)); + + hsv.v = MIN(1.0, + ((double) lumamap[iv]) / ((double) maxval)); + + xels[row][col] = ppm_color_from_hsv(hsv, maxval); + } + } + } + } + break; + + case PBM_TYPE: + case PGM_TYPE: { + unsigned int row; + for (row = 0; row < rows; ++row) { + unsigned int col; + for (col = 0; col < cols; ++col) + PNM_ASSIGN1(xels[row][col], + lumamap[PNM_GET1(xels[row][col])]); + } + } + break; + } +} + + + +static void +writeMap(const char * const wmapFileName, + const gray * const lumamap, + xelval const maxval) { + + FILE * const wmapfP = pm_openw(wmapFileName); + + pgm_writepgminit(wmapfP, maxval+1, 1, maxval, 0); + + pgm_writepgmrow(wmapfP, lumamap, maxval+1, maxval, 0); + + pm_close(wmapfP); +} + + + +int +main(int argc, char * argv[]) { + + struct cmdlineInfo cmdline; + FILE * ifP; + xelval lmin, lmax; + gray * lumamap; /* Luminosity map */ + unsigned int * lumahist; /* Histogram of luminosity values */ + int rows, cols; /* Rows, columns of input image */ + xelval maxval; /* Maxval of input image */ + int format; /* Format indicator (PBM/PGM/PPM) */ + xel ** xels; /* Pixel array */ + unsigned int pixelCount; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFileName); + + xels = pnm_readpnm(ifP, &cols, &rows, &maxval, &format); + + pm_close(ifP); + + computeLuminosityHistogram(xels, rows, cols, maxval, format, + cmdline.gray, &lumahist, &lmin, &lmax, + &pixelCount); + + getMapping(cmdline.rmap, lumahist, maxval, pixelCount, &lumamap); + + if (cmdline.verbose) + reportMap(lumahist, maxval, lumamap); + + remap(xels, cols, rows, maxval, format, !!cmdline.gray, lumamap); + + pnm_writepnm(stdout, xels, cols, rows, maxval, format, 0); + + if (cmdline.wmap) + writeMap(cmdline.wmap, lumamap, maxval); + + pgm_freerow(lumamap); + + return 0; +} diff --git a/editor/pnmindex.c b/editor/pnmindex.c new file mode 100644 index 00000000..cb7d3702 --- /dev/null +++ b/editor/pnmindex.c @@ -0,0 +1,638 @@ +/*============================================================================ + pnmindex +============================================================================== + + build a visual index of a bunch of PNM images + + This used to be a C shell program, and then a BASH program. Neither + were portable enough, and the program is too complex for either of + those languages anyway, so now it's in C. + + By Bryan Henderson 2005.04.24. + + Contributed to the public domain by its author. + +============================================================================*/ + +#define _BSD_SOURCE /* Make sure strdup is in string.h */ + +#include <assert.h> +#include <unistd.h> +#include <stdarg.h> +#include <errno.h> +#include <sys/stat.h> + + +#include "pnm.h" +#include "shhopt.h" +#include "mallocvar.h" +#include "nstring.h" + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + unsigned int inputFileCount; + const char ** inputFileName; + unsigned int size; + unsigned int across; + unsigned int colors; + unsigned int black; + unsigned int noquant; + const char * title; + unsigned int verbose; +}; + +static bool verbose; + + + +static void PM_GNU_PRINTF_ATTR(1,2) +systemf(const char * const fmt, + ...) { + + va_list varargs; + + size_t dryRunLen; + + va_start(varargs, fmt); + + vsnprintfN(NULL, 0, fmt, varargs, &dryRunLen); + + va_end(varargs); + + if (dryRunLen + 1 < dryRunLen) + /* arithmetic overflow */ + pm_error("Command way too long"); + else { + size_t const allocSize = dryRunLen + 1; + char * shellCommand; + shellCommand = malloc(allocSize); + if (shellCommand == NULL) + pm_error("Can't get storage for %u-character command", + allocSize); + else { + va_list varargs; + size_t realLen; + int rc; + + va_start(varargs, fmt); + + vsnprintfN(shellCommand, allocSize, fmt, varargs, &realLen); + + assert(realLen == dryRunLen); + va_end(varargs); + + if (verbose) + pm_message("shell cmd: %s", shellCommand); + + rc = system(shellCommand); + if (rc != 0) + pm_error("shell command '%s' failed. rc %d", + shellCommand, rc); + + strfree(shellCommand); + } + } +} + + + +static void +parseCommandLine(int argc, char ** argv, + struct cmdlineInfo * const cmdlineP) { + + unsigned int option_def_index; + optEntry *option_def; + /* Instructions to optParseOptions3 on how to parse our options. + */ + optStruct3 opt; + + unsigned int quant; + unsigned int sizeSpec, colorsSpec, acrossSpec, titleSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "black", OPT_FLAG, NULL, + &cmdlineP->black, 0); + OPTENT3(0, "noquant", OPT_FLAG, NULL, + &cmdlineP->noquant, 0); + OPTENT3(0, "quant", OPT_FLAG, NULL, + &quant, 0); + OPTENT3(0, "verbose", OPT_FLAG, NULL, + &cmdlineP->verbose, 0); + OPTENT3(0, "size", OPT_UINT, &cmdlineP->size, + &sizeSpec, 0); + OPTENT3(0, "colors", OPT_UINT, &cmdlineP->colors, + &colorsSpec, 0); + OPTENT3(0, "across", OPT_UINT, &cmdlineP->across, + &acrossSpec, 0); + OPTENT3(0, "title", OPT_STRING, &cmdlineP->title, + &titleSpec, 0); + + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ + opt.allowNegNum = FALSE; + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdline_p and others. */ + + if (quant && cmdlineP->noquant) + pm_error("You can't specify both -quant and -noquat"); + + if (!colorsSpec) + cmdlineP->colors = 256; + + if (!sizeSpec) + cmdlineP->size = 100; + + if (!acrossSpec) + cmdlineP->across = 6; + + if (!titleSpec) + cmdlineP->title = NULL; + + if (colorsSpec && cmdlineP->noquant) + pm_error("-colors doesn't make any sense with -noquant"); + + if (argc-1 < 1) + pm_error("You must name at least one file that contains an image " + "to go into the index"); + + cmdlineP->inputFileCount = argc-1; + + MALLOCARRAY_NOFAIL(cmdlineP->inputFileName, cmdlineP->inputFileCount); + + { + unsigned int i; + for (i = 0; i < cmdlineP->inputFileCount; ++i) { + cmdlineP->inputFileName[i] = strdup(argv[i+1]); + if (cmdlineP->inputFileName[i] == NULL) + pm_error("Unable to allocate memory for a file name"); + } + } +} + + + +static void +freeCmdline(struct cmdlineInfo const cmdline) { + + unsigned int i; + + for (i = 0; i < cmdline.inputFileCount; ++i) + strfree(cmdline.inputFileName[i]); + + free(cmdline.inputFileName); + +} + + + +static void +makeTempDir(const char ** const tempDirP) { + + const char * const tmpdir = getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp"; + + const char * mytmpdir; + int rc; + + asprintfN(&mytmpdir, "%s/pnmindex_%d", tmpdir, getpid()); + + rc = mkdir(mytmpdir, 0700); + if (rc != 0) + pm_error("Unable to create temporary file directory '%s'. mkdir() " + "fails with errno %d (%s)", + mytmpdir, errno, strerror(errno)); + + *tempDirP = mytmpdir; +} + + + +static void +removeTempDir(const char * const tempDir) { + + int rc; + + rc = rmdir(tempDir); + if (rc != 0) + pm_error("Failed to remove temporary file directory '%s'. " + "rmdir() fails with errno %d (%s)", + tempDir, errno, strerror(errno)); +} + + +static const char * +rowFileName(const char * const dirName, + unsigned int const row) { + + const char * fileName; + + asprintfN(&fileName, "%s/pi.%u", dirName, row); + + return fileName; +} + + + +static void +makeTitle(const char * const title, + unsigned int const rowNumber, + bool const blackBackground, + const char * const tempDir) { + + const char * const invertStage = blackBackground ? "| pnminvert " : ""; + + const char * fileName; + + fileName = rowFileName(tempDir, rowNumber); + + /* This quoting is not adequate. We really should do this without + a shell at all. + */ + systemf("pbmtext \"%s\" " + "%s" + "> %s", + title, invertStage, fileName); + + strfree(fileName); +} + + + +static void +copyImage(const char * const inputFileName, + const char * const outputFileName) { + + systemf("cat %s > %s", inputFileName, outputFileName); +} + + + +static void +copyScaleQuantImage(const char * const inputFileName, + const char * const outputFileName, + int const format, + unsigned int const size, + unsigned int const quant, + unsigned int const colors) { + + const char * scaleCommand; + + switch (PNM_FORMAT_TYPE(format)) { + case PBM_TYPE: + asprintfN(&scaleCommand, + "pamscale -quiet -xysize %u %u %s " + "| pgmtopbm > %s", + size, size, inputFileName, outputFileName); + break; + + case PGM_TYPE: + asprintfN(&scaleCommand, + "pamscale -quiet -xysize %u %u %s >%s", + size, size, inputFileName, outputFileName); + break; + + case PPM_TYPE: + if (quant) + asprintfN(&scaleCommand, + "pamscale -quiet -xysize %u %u %s " + "| pnmquant -quiet %u > %s", + size, size, inputFileName, colors, outputFileName); + else + asprintfN(&scaleCommand, + "pamscale -quiet -xysize %u %u %s >%s", + size, size, inputFileName, outputFileName); + break; + default: + pm_error("Unrecognized Netpbm format: %d", format); + } + + systemf("%s", scaleCommand); + + strfree(scaleCommand); +} + + + +static int +formatTypeMax(int const typeA, + int const typeB) { + + if (typeA == PPM_TYPE || typeB == PPM_TYPE) + return PPM_TYPE; + else if (typeA == PGM_TYPE || typeB == PGM_TYPE) + return PGM_TYPE; + else + return PBM_TYPE; +} + + + +static const char * +thumbnailFileName(const char * const dirName, + unsigned int const row, + unsigned int const col) { + + const char * fileName; + + asprintfN(&fileName, "%s/pi.%u.%u", dirName, row, col); + + return fileName; +} + + + +static const char * +thumbnailFileList(const char * const dirName, + unsigned int const row, + unsigned int const cols) { + + unsigned int const maxListSize = 4096; + + char * list; + unsigned int col; + + list = malloc(maxListSize); + if (list == NULL) + pm_error("Unable to allocate %u bytes for file list", maxListSize); + + list[0] = '\0'; + + for (col = 0; col < cols; ++col) { + const char * const fileName = thumbnailFileName(dirName, row, col); + + if (strlen(list) + strlen(fileName) + 1 > maxListSize - 1) + pm_error("File name list too long for this program to handle."); + else { + strcat(list, " "); + strcat(list, fileName); + } + strfree(fileName); + } + + return list; +} + + + +static void +makeImageFile(const char * const thumbnailFileName, + const char * const inputFileName, + bool const blackBackground, + const char * const outputFileName) { + + const char * const blackWhiteOpt = blackBackground ? "-black" : "-white"; + const char * const invertStage = blackBackground ? "| pnminvert " : ""; + + systemf("pbmtext \"%s\" " + "%s" + "| pnmcat %s -topbottom %s - " + "> %s", + inputFileName, invertStage, blackWhiteOpt, + thumbnailFileName, outputFileName); +} + + + +static void +makeThumbnail(const char * const inputFileName, + unsigned int const size, + bool const black, + bool const quant, + unsigned int const colors, + const char * const tempDir, + unsigned int const row, + unsigned int const col, + int * const formatP) { + + FILE * ifP; + int imageCols, imageRows, format; + xelval maxval; + const char * tmpfile; + const char * fileName; + + ifP = pm_openr(inputFileName); + pnm_readpnminit(ifP, &imageCols, &imageRows, &maxval, &format); + pm_close(ifP); + + asprintfN(&tmpfile, "%s/pi.tmp", tempDir); + + if (imageCols < size && imageRows < size) + copyImage(inputFileName, tmpfile); + else + copyScaleQuantImage(inputFileName, tmpfile, format, + size, quant, colors); + + fileName = thumbnailFileName(tempDir, row, col); + + makeImageFile(tmpfile, inputFileName, black, fileName); + + unlink(tmpfile); + + strfree(fileName); + strfree(tmpfile); + + *formatP = format; +} + + + +static void +unlinkThumbnailFiles(const char * const dirName, + unsigned int const row, + unsigned int const cols) { + + unsigned int col; + + for (col = 0; col < cols; ++col) { + const char * const fileName = thumbnailFileName(dirName, row, col); + + unlink(fileName); + + strfree(fileName); + } +} + + + +static void +unlinkRowFiles(const char * const dirName, + unsigned int const rows) { + + unsigned int row; + + for (row = 0; row < rows; ++row) { + const char * const fileName = rowFileName(dirName, row); + + unlink(fileName); + + strfree(fileName); + } +} + + + +static void +combineIntoRowAndDelete(unsigned int const row, + unsigned int const cols, + int const maxFormatType, + bool const blackBackground, + bool const quant, + unsigned int const colors, + const char * const tempDir) { + + const char * const blackWhiteOpt = blackBackground ? "-black" : "-white"; + + const char * fileName; + const char * quantStage; + const char * fileList; + + fileName = rowFileName(tempDir, row); + + unlink(fileName); + + if (maxFormatType == PPM_TYPE && quant) + asprintfN(&quantStage, "| pnmquant -quiet %u ", colors); + else + quantStage = strdup(""); + + fileList = thumbnailFileList(tempDir, row, cols); + + systemf("pnmcat %s -leftright -jbottom %s " + "%s" + ">%s", + blackWhiteOpt, fileList, quantStage, fileName); + + strfree(fileList); + strfree(quantStage); + strfree(fileName); + + unlinkThumbnailFiles(tempDir, row, cols); +} + + + +static const char * +rowFileList(const char * const dirName, + unsigned int const rows) { + + unsigned int const maxListSize = 4096; + + unsigned int row; + char * list; + + list = malloc(maxListSize); + if (list == NULL) + pm_error("Unable to allocate %u bytes for file list", maxListSize); + + list[0] = '\0'; + + for (row = 0; row < rows; ++row) { + const char * const fileName = rowFileName(dirName, row); + + if (strlen(list) + strlen(fileName) + 1 > maxListSize - 1) + pm_error("File name list too long for this program to handle."); + + else { + strcat(list, " "); + strcat(list, fileName); + } + strfree(fileName); + } + + return list; +} + + + +static void +writeRowsAndDelete(unsigned int const rows, + int const maxFormatType, + bool const blackBackground, + bool const quant, + unsigned int const colors, + const char * const tempDir) { + + const char * const blackWhiteOpt = blackBackground ? "-black" : "-white"; + + const char * quantStage; + const char * fileList; + + if (maxFormatType == PPM_TYPE && quant) + asprintfN(&quantStage, "| pnmquant -quiet %u ", colors); + else + quantStage = strdup(""); + + fileList = rowFileList(tempDir, rows); + + systemf("pnmcat %s -topbottom %s %s", + blackWhiteOpt, fileList, quantStage); + + strfree(fileList); + strfree(quantStage); + + unlinkRowFiles(tempDir, rows); +} + + + +int +main(int argc, char *argv[]) { + struct cmdlineInfo cmdline; + const char * tempDir; + int maxFormatType; + unsigned int colsInRow; + unsigned int rowsDone; + unsigned int i; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + verbose = cmdline.verbose; + + makeTempDir(&tempDir); + + maxFormatType = PBM_TYPE; + colsInRow = 0; + rowsDone = 0; + + if (cmdline.title) + makeTitle(cmdline.title, rowsDone++, cmdline.black, tempDir); + + for (i = 0; i < cmdline.inputFileCount; ++i) { + const char * const inputFileName = cmdline.inputFileName[i]; + + int format; + + makeThumbnail(inputFileName, cmdline.size, cmdline.black, + !cmdline.noquant, cmdline.colors, tempDir, + rowsDone, colsInRow, &format); + + maxFormatType = formatTypeMax(maxFormatType, PNM_FORMAT_TYPE(format)); + + ++colsInRow; + if (colsInRow >= cmdline.across || i == cmdline.inputFileCount-1) { + combineIntoRowAndDelete( + rowsDone, colsInRow, maxFormatType, + cmdline.black, !cmdline.noquant, cmdline.colors, + tempDir); + ++rowsDone; + colsInRow = 0; + } + } + + writeRowsAndDelete(rowsDone, maxFormatType, cmdline.black, + !cmdline.noquant, cmdline.colors, tempDir); + + removeTempDir(tempDir); + + freeCmdline(cmdline); + + pm_close(stdout); + + return 0; +} diff --git a/editor/pnmindex.csh b/editor/pnmindex.csh new file mode 100755 index 00000000..c6f1e844 --- /dev/null +++ b/editor/pnmindex.csh @@ -0,0 +1,189 @@ +#!/bin/csh -f +# +# pnmindex - build a visual index of a bunch of anymaps +# +# Copyright (C) 1991 by Jef Poskanzer. +# +# 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. + +# -title and -quant added by John Heidemann 13-Sep-00. + +set size=100 # make the images about this big +set across=6 # show this many images per row +set colors=256 # quantize results to this many colors +set back="-white" # default background color +set doquant=true # quantize or not +set title="" # default title (none) + +while ( 1 ) + switch ( "$1" ) + + case -s*: + if ( $#argv < 2 ) goto usage + set size="$2" + shift + shift + breaksw + + case -a*: + if ( $#argv < 2 ) goto usage + set across="$2" + shift + shift + breaksw + + case -t*: + if ( $#argv < 2 ) goto usage + set title="$2" + shift + shift + breaksw + + case -c*: + set colors="$2" + shift + shift + breaksw + + case -noq*: + set doquant=false + shift + breaksw + + case -q*: + set doquant=true + shift + breaksw + + case -b*: + set back="-black" + shift + breaksw + + case -w*: + set back="-white" + shift + breaksw + + case -*: + goto usage + breaksw + + default: + break + breaksw + + endsw +end + +if ( $#argv == 0 ) then + goto usage +endif + +set tmpfile=/tmp/pi.tmp.$$ +rm -f $tmpfile +set maxformat=PBM + +set rowfiles=() +set imagefiles=() +@ row = 1 +@ col = 1 + +if ( "$title" != "" ) then + set rowfile=/tmp/pi.${row}.$$ + rm -f $rowfile + pbmtext "$title" > $rowfile + set rowfiles=( $rowfiles $rowfile ) + @ row += 1 +endif + +foreach i ( $argv ) + + set description=`pnmfile $i` + if ( $description[4] <= $size && $description[6] <= $size ) then + cat $i > $tmpfile + else + switch ( $description[2] ) + case PBM: + pnmscale -quiet -xysize $size $size $i | pgmtopbm > $tmpfile + breaksw + + case PGM: + pnmscale -quiet -xysize $size $size $i > $tmpfile + if ( $maxformat == PBM ) then + set maxformat=PGM + endif + breaksw + + default: + if ( $doquant == false ) then + pnmscale -quiet -xysize $size $size $i > $tmpfile + else + pnmscale -quiet -xysize $size $size $i | ppmquant -quiet $colors > $tmpfile + endif + set maxformat=PPM + breaksw + endsw + endif + set imagefile=/tmp/pi.${row}.${col}.$$ + rm -f $imagefile + if ( "$back" == "-white" ) then + pbmtext "$i" | pnmcat $back -tb $tmpfile - > $imagefile + else + pbmtext "$i" | pnminvert | pnmcat $back -tb $tmpfile - > $imagefile + endif + rm -f $tmpfile + set imagefiles=( $imagefiles $imagefile ) + + if ( $col >= $across ) then + set rowfile=/tmp/pi.${row}.$$ + rm -f $rowfile + if ( $maxformat != PPM || $doquant == false ) then + pnmcat $back -lr -jbottom $imagefiles > $rowfile + else + pnmcat $back -lr -jbottom $imagefiles | ppmquant -quiet $colors > $rowfile + endif + rm -f $imagefiles + set imagefiles=() + set rowfiles=( $rowfiles $rowfile ) + @ col = 1 + @ row += 1 + else + @ col += 1 + endif + +end + +if ( $#imagefiles > 0 ) then + set rowfile=/tmp/pi.${row}.$$ + rm -f $rowfile + if ( $maxformat != PPM || $doquant == false ) then + pnmcat $back -lr -jbottom $imagefiles > $rowfile + else + pnmcat $back -lr -jbottom $imagefiles | ppmquant -quiet $colors > $rowfile + endif + rm -f $imagefiles + set rowfiles=( $rowfiles $rowfile ) +endif + +if ( $#rowfiles == 1 ) then + cat $rowfiles +else + if ( $maxformat != PPM || $doquant == false ) then + pnmcat $back -tb $rowfiles + else + pnmcat $back -tb $rowfiles | ppmquant -quiet $colors + endif +endif +rm -f $rowfiles + +exit 0 + +usage: +echo "usage: $0 [-size N] [-across N] [-colors N] [-black] pnmfile ..." +exit 1 diff --git a/editor/pnmindex.sh b/editor/pnmindex.sh new file mode 100755 index 00000000..15ba1abd --- /dev/null +++ b/editor/pnmindex.sh @@ -0,0 +1,215 @@ +#!/bin/sh +# +# pnmindex - build a visual index of a bunch of PNM images +# +# Copyright (C) 1991 by Jef Poskanzer. +# +# 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. + +size=100 # make the images about this big +across=6 # show this many images per row +colors=256 # quantize results to this many colors +back="-white" # default background color +doquant=true # quantize or not +title="" # default title (none) + +usage () +{ + echo "usage: $0 [-size N] [-across N] [-colors N] [-black] pnmfile ..." + exit 1 +} + +while :; do + case "$1" in + + -s*) + if [ $# -lt 2 ]; then usage; fi + size="$2" + shift + shift + ;; + + -a*) + if [ $# -lt 2 ]; then usage; fi + across="$2" + shift + shift + ;; + + -t*) + if [ $# -lt 2 ]; then usage; fi + title="$2" + shift + shift + ;; + + -c*) + if [ $# -lt 2 ]; then usage; fi + colors="$2" + shift + shift + ;; + + -b*) + back="-black" + shift + ;; + + -w*) + back="-white" + shift + ;; + + -noq*) + doquant=false + shift + ;; + + -q*) + doquant=true + shift + ;; + + -*) + usage + ;; + + *) + break + ;; + esac +done + +if [ $# -eq 0 ]; then + usage +fi + +tempdir="${TMPDIR-/tmp}/pnmindex.$$" +mkdir $tempdir || { echo "Could not create temporary file. Exiting."; exit 1;} +chmod 700 $tempdir + +trap 'rm -rf $tempdir' 0 1 3 15 + +tmpfile=$tempdir/pi.tmp +maxformat=PBM + +rowfiles=() +imagefiles=() +row=1 +col=1 + +if [ "$title"x != ""x ] ; then +# rowfile=`tempfile -p pirow -m 600` + rowfile=$tempdir/pi.${row} + pbmtext "$title" > $rowfile + rowfiles=(${rowfiles[*]} $rowfile ) + row=$(($row + 1)) +fi + +for i in "$@"; do + + description=(`pnmfile $i`) + + format=${description[1]} + width=${description[3]} + height=${description[5]} + + if [ $? -ne 0 ]; then + echo pnmfile returned an error + exit $? + fi + + if [ $width -le $size ] && \ + [ $height -le $size ]; then + cat $i > $tmpfile + else + case $format in + + PBM) + pamscale -quiet -xysize $size $size $i | pgmtopbm > $tmpfile + ;; + + PGM) + pamscale -quiet -xysize $size $size $i > $tmpfile + if [ $maxformat = PBM ]; then + maxformat=PGM + fi + ;; + + *) + if [ "$doquant" = "true" ] ; then + pamscale -quiet -xysize $size $size $i | \ + pnmquant -quiet $colors > $tmpfile + else + pamscale -quiet -xysize $size $size $i > $tmpfile + fi + maxformat=PPM + ;; + esac + fi + + imagefile=$tempdir/pi.${row}.${col} + rm -f $imagefile + if [ "$back" = "-white" ]; then + pbmtext "$i" | pnmcat $back -tb $tmpfile - > $imagefile + else + pbmtext "$i" | pnminvert | pnmcat $back -tb $tmpfile - > $imagefile + fi + imagefiles=( ${imagefiles[*]} $imagefile ) + + if [ $col -ge $across ]; then + rowfile=$tempdir/pi.${row} + rm -f $rowfile + + if [ $maxformat != PPM -o "$doquant" = "false" ]; then + pnmcat $back -lr -jbottom ${imagefiles[*]} > $rowfile + else + pnmcat $back -lr -jbottom ${imagefiles[*]} | \ + pnmquant -quiet $colors > $rowfile + fi + + rm -f ${imagefiles[*]} + unset imagefiles + imagefiles=() + rowfiles=( ${rowfiles[*]} $rowfile ) + col=1 + row=$(($row + 1)) + else + col=$(($col + 1)) + fi +done + +# All the full rows have been put in row files. +# Now put the final partial row in its row file. + +if [ ${#imagefiles[*]} -gt 0 ]; then + rowfile=$tempdir/pi.${row} + rm -f $rowfile + if [ $maxformat != PPM -o "$doquant" = "false" ]; then + pnmcat $back -lr -jbottom ${imagefiles[*]} > $rowfile + else + pnmcat $back -lr -jbottom ${imagefiles[*]} | \ + pnmquant -quiet $colors > $rowfile + fi + rm -f ${imagefiles[*]} + rowfiles=( ${rowfiles[*]} $rowfile ) +fi + +if [ ${#rowfiles[*]} -eq 1 ]; then + cat $rowfiles +else + if [ $maxformat != PPM -o "$doquant" = "false" ]; then + pnmcat $back -tb ${rowfiles[*]} + else + pnmcat $back -tb ${rowfiles[*]} | pnmquant -quiet $colors + fi +fi +rm -f ${rowfiles[*]} + +exit 0 + diff --git a/editor/pnminvert.c b/editor/pnminvert.c new file mode 100644 index 00000000..40fee9be --- /dev/null +++ b/editor/pnminvert.c @@ -0,0 +1,115 @@ +/* pnminvert.c - read a portable anymap and invert it +** +** Copyright (C) 1989 by Jef Poskanzer. +** +** 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. +*/ + +#include "pnm.h" + +#define CHARBITS (sizeof(unsigned char)*8) + + + +static void +invertPbm(FILE * const ifP, + FILE * const ofP, + int const cols, + int const rows, + int const format) { +/*---------------------------------------------------------------------------- + Invert a PBM image. Use the "packed" PBM functions for speed. +-----------------------------------------------------------------------------*/ + /* We could make this faster by inverting whole words at a time, + using libnetpbm's wordaccess.h facility. + */ + int const colChars = pbm_packed_bytes(cols); + + unsigned char * bitrow; + unsigned int row; + + bitrow = pbm_allocrow_packed(cols); + + for (row = 0; row < rows; ++row) { + unsigned int colChar; + + pbm_readpbmrow_packed(ifP, bitrow, cols, format); + for (colChar = 0; colChar < colChars; ++colChar) + bitrow[colChar] = ~ bitrow[colChar]; + + /* Clean off remainder of fractional last character */ + if (cols % CHARBITS > 0) { + bitrow[colChars-1] >>= CHARBITS - cols % CHARBITS; + bitrow[colChars-1] <<= CHARBITS - cols % CHARBITS; + } + pbm_writepbmrow_packed(ofP, bitrow, cols, 0); + } + pbm_freerow_packed(bitrow); +} + + + +static void +invertPnm(FILE * const ifP, + FILE * const ofP, + int const cols, + int const rows, + xelval const maxval, + int const format) { + + xel * xelrow; + unsigned int row; + + xelrow = pnm_allocrow(cols); + + for (row = 0; row < rows; ++row) { + unsigned int col; + pnm_readpnmrow(ifP, xelrow, cols, maxval, format); + for (col = 0; col < cols; ++col) + pnm_invertxel(&xelrow[col], maxval, format); + + pnm_writepnmrow(ofP, xelrow, cols, maxval, format, 0); + } + pnm_freerow(xelrow); +} + + + +int +main(int argc, char * argv[]) { + FILE* ifP; + xelval maxval; + int rows, cols, format; + + pnm_init(&argc, argv); + + if (argc-1 > 1) + pm_error("There is at most 1 argument - the input file name. " + "You specified %d", argc-1); + if (argc-1 == 1) + ifP = pm_openr(argv[1]); + else + ifP = stdin; + + pnm_readpnminit(ifP, &cols, &rows, &maxval, &format); + pnm_writepnminit(stdout, cols, rows, maxval, format, 0); + + if (PNM_FORMAT_TYPE(format) == PBM_TYPE) + /* Take fast path */ + invertPbm(ifP, stdout, cols, rows, format); + else + /* PPM , PGM (logic also works for PBM) */ + invertPnm(ifP, stdout, cols, rows, maxval, format); + + pm_close(ifP); + pm_close(stdout); + + return 0; +} + + diff --git a/editor/pnminvert.test b/editor/pnminvert.test new file mode 100644 index 00000000..606e4e5c --- /dev/null +++ b/editor/pnminvert.test @@ -0,0 +1,15 @@ +echo Test 1. Should print 1240379484 41 +./pnminvert ../testgrid.pbm | cksum +echo Test 2. Should print 1416115901 101484 +./pnminvert ../testimg.ppm | cksum +echo Test 3. Should print 4215652354 33838 +ppmtopgm ../testimg.ppm | ./pnminvert | cksum +echo Test 4. Should print 2595564405 14 +pbmmake -w 7 7 | ./pnminvert | cksum +echo Test 5. Should print 2595564405 14 +pbmmake -b 7 7 | cksum +echo Test 6. Should print 2595564405 14 +pbmmake -b 7 7 | ./pnminvert | ./pnminvert | cksum +echo Test 7. Should print 2896726098 15 +pbmmake -g 8 8 | ./pnminvert | cksum + diff --git a/editor/pnmmargin b/editor/pnmmargin new file mode 100755 index 00000000..31420f99 --- /dev/null +++ b/editor/pnmmargin @@ -0,0 +1,88 @@ +#!/bin/sh +# +# ppmmargin - add a margin to a PNM image +# +# Copyright (C) 1991 by Jef Poskanzer. +# +# 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. + +tempdir="${TMPDIR-/tmp}/pnmmargin.$$" +mkdir $tempdir || { echo "Could not create temporary file. Exiting."; exit 1;} +chmod 700 $tempdir + +trap 'rm -rf $tempdir' 0 1 3 15 + +tmp1=$tempdir/pnmm1 +tmp2=$tempdir/pnmm2 +tmp3=$tempdir/pnmm3 +tmp4=$tempdir/pnmm4 + +color="-gofigure" + +# Parse args. +while true ; do + case "$1" in + -w* ) + color="-white" + shift + ;; + -b* ) + color="-black" + shift + ;; + -c* ) + shift + if [ ! ${1-""} ] ; then + echo "usage: $0 [-white|-black|-color <colorspec>] <size> [pnmfile]" 1>&2 + exit 1 + fi + color="$1" + shift + ;; + -* ) + echo "usage: $0 [-white|-black|-color <colorspec>] <size> [pnmfile]" 1>&2 + exit 1 + ;; + * ) + break + ;; + esac +done + +if [ ! ${1-""} ] ; then + echo "usage: $0 [-white|-black|-color <colorspec>] <size> [pnmfile]" 1>&2 + exit 1 +fi +size="$1" +shift + +if [ ${2-""} ] ; then + echo "usage: $0 [-white|-black|-color <colorspec>] <size> [pnmfile]" 1>&2 + exit 1 +fi + +# Capture input file in a tmp file, in case it's a pipe. +cat $@ > $tmp1 + +# Construct spacer files. +case "$color" in + -gofigure ) + pnmcut 0 0 1 1 $tmp1 | pnmtile $size 1 > $tmp2 + ;; + -white | -black ) + pbmmake $color $size 1 > $tmp2 + ;; + * ) + ppmmake $color $size 1 > $tmp2 + ;; +esac +pamflip -rotate90 $tmp2 > $tmp3 + +# Cat things together. +pnmcat -lr $tmp2 $tmp1 $tmp2 > $tmp4 +pnmcat -tb $tmp3 $tmp4 $tmp3 diff --git a/editor/pnmmontage.c b/editor/pnmmontage.c new file mode 100644 index 00000000..9eb2d7be --- /dev/null +++ b/editor/pnmmontage.c @@ -0,0 +1,439 @@ +/* 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. + */ + +#include <limits.h> +#include <string.h> + +#include "pam.h" +#include "shhopt.h" +#include "nstring.h" +#include "mallocvar.h" + +typedef struct { int f[sizeof(int) * 8 + 1]; } factorset; +typedef struct { int x; int y; } coord; + +static int qfactor = 200; +static int quality = 5; + +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 __inline__ int imax(int n, int m) { return (n > m ? n : m); } + +static int +checkcollision(coord *locs, coord *szs, coord *cloc, coord *csz, int n) +{ + int i; + for (i = 0; i < n; ++i) + { + if ((locs[i].x < cloc->x + csz->x) && + (locs[i].y < cloc->y + csz->y) && + (locs[i].x + szs[i].x > cloc->x) && + (locs[i].y + szs[i].y > cloc->y)) + return (1); + } + return (0); +} + +static void +recursefindpack(coord *current, coord currentsz, coord *set, + coord *best, int minarea, int *maxarea, + int depth, int n, int xinc, int yinc) +{ + coord c; + if (depth == n) + { + if (currentsz.x * currentsz.y < *maxarea) + { + memcpy(best, current, sizeof(coord) * n); + *maxarea = currentsz.x * currentsz.y; + } + return; + } + + for (current[depth].x = 0; + imax(current[depth].x + set[depth].x, currentsz.x) * + imax(currentsz.y, set[depth].y) < *maxarea; + current[depth].x += xinc) + { + for (current[depth].y = 0; + imax(current[depth].x + set[depth].x, currentsz.x) * + imax(currentsz.y, current[depth].y + set[depth].y) < *maxarea; + current[depth].y += yinc) + { + c.x = imax(current[depth].x + set[depth].x, currentsz.x); + c.y = imax(current[depth].y + set[depth].y, currentsz.y); + if (!checkcollision(current, set, ¤t[depth], &set[depth], depth)) + { + recursefindpack(current, c, set, best, minarea, maxarea, + depth + 1, n, xinc, yinc); + if (*maxarea <= minarea) + return; + } + } + } +} + +static void +findpack(struct pam *imgs, int n, coord *coords) +{ + int minarea; + int i; + int rdiv; + int cdiv; + int minx = -1; + int miny = -1; + coord *current; + coord *set; + int z = INT_MAX; + coord c = { 0, 0 }; + + if (quality > 1) + { + for (minarea = i = 0; i < n; ++i) + minarea += imgs[i].height * imgs[i].width, + minx = imax(minx, imgs[i].width), + miny = imax(miny, imgs[i].height); + + minarea = minarea * 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); + MALLOCARRAY(set, n); + for (i = 0; i < n; ++i) + set[i].x = imgs[i].width, + set[i].y = imgs[i].height; + recursefindpack(current, c, set, coords, minarea, &z, 0, n, cdiv, rdiv); +} + + + +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); +} + + + +int +main(int argc, char **argv) +{ + struct pam *imgs; + struct pam outimg; + struct pam p; + int nfiles; + int i, j; + unsigned int q[10]; + coord *coords; + const char *headfname = NULL; + const char *datafname = NULL; + const char *prefix = ""; + FILE *header; + FILE *data; + char **names; + char *c; + + optEntry *option_def = malloc(100*sizeof(optEntry)); + /* Instructions to OptParseOptions3 on how to parse our options. + */ + optStruct3 opt; + + unsigned int option_def_index; + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENT3( 0, "data", OPT_STRING, &datafname, NULL, 0); + OPTENT3( 0, "header", OPT_STRING, &headfname, NULL, 0); + OPTENT3('q', "quality", OPT_UINT, &qfactor, NULL, 0); + OPTENT3('p', "prefix", OPT_STRING, &prefix, NULL, 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; + + pnm_init(&argc, argv); + + /* Check for flags. */ + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + + if (headfname) + header = pm_openw(headfname); + + if (datafname) + data = pm_openw(datafname); + + for (i = 0; i < 10; ++i) + { + if (q[i]) + { + quality = i; + switch (quality) + { + case 0: case 1: break; + case 2: case 3: case 4: case 5: case 6: + qfactor = 100 * (8 - quality); + break; + case 7: qfactor = 150; break; + case 8: qfactor = 125; break; + case 9: qfactor = 100; break; + } + } + } + + if (1 < argc) + nfiles = argc - 1; + else + nfiles = 1; + + MALLOCARRAY(imgs, nfiles); + MALLOCARRAY(coords, nfiles); + MALLOCARRAY(names, nfiles); + + if (!imgs || !coords || !names) + pm_error("out of memory"); + + if (1 < argc) + { + for (i = 0; i < nfiles; ++i) + { + if (strchr(argv[i+1], ':')) + { + imgs[i].file = pm_openr(strchr(argv[i+1], ':') + 1); + *strchr(argv[i+1], ':') = 0; + names[i] = argv[i+1]; + } + else + { + imgs[i].file = pm_openr(argv[i+1]); + names[i] = argv[i+1]; + } + } + } + else + { + imgs[0].file = stdin; + } + + pnm_readpaminit(imgs[0].file, &imgs[0], PAM_STRUCT_SIZE(tuple_type)); + outimg.maxval = imgs[0].maxval; + outimg.format = imgs[0].format; + memcpy(outimg.tuple_type, imgs[0].tuple_type, sizeof(imgs[0].tuple_type)); + outimg.depth = imgs[0].depth; + + for (i = 1; i < nfiles; ++i) + { + pnm_readpaminit(imgs[i].file, &imgs[i], PAM_STRUCT_SIZE(tuple_type)); + if (PAM_FORMAT_TYPE(imgs[i].format) > PAM_FORMAT_TYPE(outimg.format)) + outimg.format = imgs[i].format, + memcpy(outimg.tuple_type, imgs[i].tuple_type, + sizeof(imgs[i].tuple_type)); + outimg.maxval = imax(imgs[i].maxval, outimg.maxval); + outimg.depth = imax(imgs[i].depth, outimg.depth); + } + + for (i = 0; i < nfiles - 1; ++i) + for (j = i + 1; j < nfiles; ++j) + if (imgs[j].width * imgs[j].height > imgs[i].width * imgs[i].height) + p = imgs[i], imgs[i] = imgs[j], imgs[j] = p, + c = names[i], names[i] = names[j], names[j] = c; + + findpack(imgs, nfiles, coords); + + outimg.height = outimg.width = 0; + for (i = 0; i < nfiles; ++i) + { + outimg.width = imax(outimg.width, imgs[i].width + coords[i].x); + outimg.height = imax(outimg.height, imgs[i].height + coords[i].y); + } + + 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 (datafname) + { + fprintf(data, ":0:0:%u:%u\n", outimg.width, outimg.height); + + for (i = 0; i < nfiles; ++i) + { + fprintf(data, "%s:%u:%u:%u:%u\n", names[i], coords[i].x, + coords[i].y, imgs[i].width, imgs[i].height); + } + } + + if (headfname) + { + fprintf(header, "#define %sOVERALLX %u\n" + "#define %sOVERALLY %u\n" + "\n", + prefix, outimg.width, + prefix, outimg.height); + + for (i = 0; i < nfiles; ++i) + { + *strchr(names[i], '.') = 0; + for (j = 0; names[i][j]; ++j) + { + if (ISLOWER(names[i][j])) + names[i][j] = TOUPPER(names[i][j]); + } + fprintf(header, "#define %s%sX %u\n" + "#define %s%sY %u\n" + "#define %s%sSZX %u\n" + "#define %s%sSZY %u\n" + "\n", + prefix, names[i], coords[i].x, + prefix, names[i], coords[i].y, + prefix, names[i], imgs[i].width, + prefix, names[i], imgs[i].height); + } + } + + for (i = 0; i < nfiles; ++i) + pm_close(imgs[i].file); + pm_close(stdout); + + if (headfname) + pm_close(header); + + if (datafname) + pm_close(data); + + return 0; +} diff --git a/editor/pnmnlfilt.c b/editor/pnmnlfilt.c new file mode 100644 index 00000000..20705f82 --- /dev/null +++ b/editor/pnmnlfilt.c @@ -0,0 +1,1028 @@ +/* pnmnlfilt.c - 4 in 1 (2 non-linear) filter +** - smooth an anyimage +** - do alpha trimmed mean filtering on an anyimage +** - do optimal estimation smoothing on an anyimage +** - do edge enhancement on an anyimage +** +** Version 1.0 +** +** The implementation of an alpha-trimmed mean filter +** is based on the description in IEEE CG&A May 1990 +** Page 23 by Mark E. Lee and Richard A. Redner. +** +** The paper recommends using a hexagon sampling region around each +** pixel being processed, allowing an effective sub pixel radius to be +** specified. The hexagon values are sythesized by area sampling the +** rectangular pixels with a hexagon grid. The seven hexagon values +** obtained from the 3x3 pixel grid are used to compute the alpha +** trimmed mean. Note that an alpha value of 0.0 gives a conventional +** mean filter (where the radius controls the contribution of +** surrounding pixels), while a value of 0.5 gives a median filter. +** Although there are only seven values to trim from before finding +** the mean, the algorithm has been extended from that described in +** CG&A by using interpolation, to allow a continuous selection of +** alpha value between and including 0.0 to 0.5 The useful values +** for radius are between 0.3333333 (where the filter will have no +** effect because only one pixel is sampled), to 1.0, where all +** pixels in the 3x3 grid are sampled. +** +** The optimal estimation filter is taken from an article "Converting Dithered +** Images Back to Gray Scale" by Allen Stenger, Dr Dobb's Journal, November +** 1992, and this article references "Digital Image Enhancement andNoise Filtering by +** Use of Local Statistics", Jong-Sen Lee, IEEE Transactions on Pattern Analysis and +** Machine Intelligence, March 1980. +** +** Also borrow the technique used in pgmenhance(1) to allow edge +** enhancement if the alpha value is negative. +** +** Author: +** Graeme W. Gill, 30th Jan 1993 +** graeme@labtam.oz.au +** +** 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. +*/ + +#include <math.h> + +#include "pm_c_util.h" +#include "pnm.h" + +/* MXIVAL is the maximum input sample value we can handle. + It is limited by our willingness to allocate storage in various arrays + that are indexed by sample values. + + We use PPM_MAXMAXVAL because that used to be the maximum possible + sample value in the format, and most images still limit themselves to + this value. +*/ + +#define MXIVAL PPM_MAXMAXVAL + +xelval omaxval; + /* global so that pixel processing code can get at it quickly */ +int noisevariance; + /* global so that pixel processing code can get at it quickly */ + +/* + * Declared static here rather than passing a jillion options in the call to + * do_one_frame(). Also it makes a huge amount of sense to only malloc the + * row buffers once instead of for each frame (with the corresponding free'ing + * of course). +*/ +static xel *irows[3]; +static xel *irow0, *irow1, *irow2, *orow; +static double radius=0.0,alpha= -1.0; +static int rows, cols, format, oformat, row, col; +static int (*atfunc)(int *); +static xelval maxval; + +#define NOIVAL (MXIVAL + 1) /* number of possible input values */ + +#define SCALEB 8 /* scale bits */ +#define SCALE (1 << SCALEB) /* scale factor */ +#define MXSVAL (MXIVAL * SCALE) /* maximum scaled values */ + +#define CSCALEB 2 /* coarse scale bits */ +#define CSCALE (1 << CSCALEB) /* coarse scale factor */ +#define MXCSVAL (MXIVAL * CSCALE) /* maximum coarse scaled values */ +#define NOCSVAL (MXCSVAL + 1) /* number of coarse scaled values */ +#define SCTOCSC(x) ((x) >> (SCALEB - CSCALEB)) /* scaled to coarse scaled */ +#define CSCTOSC(x) ((x) << (SCALEB - CSCALEB)) /* course scaled to scaled */ + +#ifndef MAXINT +# define MAXINT 0x7fffffff /* assume this is a 32 bit machine */ +#endif + +/* round and scale floating point to scaled integer */ +#define ROUNDSCALE(x) ((int)(((x) * (double)SCALE) + 0.5)) +/* round and un-scale scaled integer value */ +#define RUNSCALE(x) (((x) + (1 << (SCALEB-1))) >> SCALEB) +/* rounded un-scale */ +#define UNSCALE(x) ((x) >> SCALEB) + +static double +sqr(double const arg) { + return arg * arg; +} + + + +/* We restrict radius to the values: 0.333333 <= radius <= 1.0 */ +/* so that no fewer and no more than a 3x3 grid of pixels around */ +/* the pixel in question needs to be read. Given this, we only */ +/* need 3 or 4 weightings per hexagon, as follows: */ +/* _ _ */ +/* Vertical hex: |_|_| 1 2 */ +/* |X|_| 0 3 */ +/* _ */ +/* _ _|_| 1 */ +/* Middle hex: |_| 1 Horizontal hex: |X|_| 0 2 */ +/* |X| 0 |_| 3 */ +/* |_| 2 */ + +/* all filters */ +int V0[NOIVAL],V1[NOIVAL],V2[NOIVAL],V3[NOIVAL]; /* vertical hex */ +int M0[NOIVAL],M1[NOIVAL],M2[NOIVAL]; /* middle hex */ +int H0[NOIVAL],H1[NOIVAL],H2[NOIVAL],H3[NOIVAL]; /* horizontal hex */ + +/* alpha trimmed and edge enhancement only */ +int ALFRAC[NOIVAL * 8]; /* fractional alpha divider table */ + +/* optimal estimation only */ +int AVEDIV[7 * NOCSVAL]; /* divide by 7 to give average value */ +int SQUARE[2 * NOCSVAL]; /* scaled square lookup table */ + +/* ************************************************** * + Hexagon intersecting square area functions + Compute the area of the intersection of a triangle + and a rectangle + ************************************************** */ + +/* Triangle orientation is per geometric axes (not graphical axies) */ + +#define NW 0 /* North west triangle /| */ +#define NE 1 /* North east triangle |\ */ +#define SW 2 /* South west triangle \| */ +#define SE 3 /* South east triangle |/ */ +#define STH 2 +#define EST 1 + +#define SWAPI(a,b) (t = a, a = -b, b = -t) + +static double +triang_area(double rx0, double ry0, double rx1, double ry1, + double tx0, double ty0, double tx1, double ty1, + int tt) { +/* rx0,ry0,rx1,ry1: rectangle boundaries */ +/* tx0,ty0,tx1,ty1: triangle boundaries */ +/* tt: triangle type */ + + double a,b,c,d; + double lx0,ly0,lx1,ly1; + + /* Convert everything to a NW triangle */ + if (tt & STH) { + double t; + SWAPI(ry0,ry1); + SWAPI(ty0,ty1); + } + if (tt & EST) { + double t; + SWAPI(rx0,rx1); + SWAPI(tx0,tx1); + } + /* Compute overlapping box */ + if (tx0 > rx0) + rx0 = tx0; + if (ty0 > ry0) + ry0 = ty0; + if (tx1 < rx1) + rx1 = tx1; + if (ty1 < ry1) + ry1 = ty1; + if (rx1 <= rx0 || ry1 <= ry0) + return 0.0; + + /* Need to compute diagonal line intersection with the box */ + /* First compute co-efficients to formulas x = a + by and y = c + dx */ + b = (tx1 - tx0)/(ty1 - ty0); + a = tx0 - b * ty0; + d = (ty1 - ty0)/(tx1 - tx0); + c = ty0 - d * tx0; + + /* compute top or right intersection */ + tt = 0; + ly1 = ry1; + lx1 = a + b * ly1; + if (lx1 <= rx0) + return (rx1 - rx0) * (ry1 - ry0); + else if (lx1 > rx1) { + /* could be right hand side */ + lx1 = rx1; + ly1 = c + d * lx1; + if (ly1 <= ry0) + return (rx1 - rx0) * (ry1 - ry0); + tt = 1; /* right hand side intersection */ + } + /* compute left or bottom intersection */ + lx0 = rx0; + ly0 = c + d * lx0; + if (ly0 >= ry1) + return (rx1 - rx0) * (ry1 - ry0); + else if (ly0 < ry0) { + /* could be right hand side */ + ly0 = ry0; + lx0 = a + b * ly0; + if (lx0 >= rx1) + return (rx1 - rx0) * (ry1 - ry0); + tt |= 2; /* bottom intersection */ + } + + if (tt == 0) { + /* top and left intersection */ + /* rectangle minus triangle */ + return ((rx1 - rx0) * (ry1 - ry0)) + - (0.5 * (lx1 - rx0) * (ry1 - ly0)); + } else if (tt == 1) { + /* right and left intersection */ + return ((rx1 - rx0) * (ly0 - ry0)) + + (0.5 * (rx1 - rx0) * (ly1 - ly0)); + } else if (tt == 2) { + /* top and bottom intersection */ + return ((rx1 - lx1) * (ry1 - ry0)) + + (0.5 * (lx1 - lx0) * (ry1 - ry0)); + } else { + /* tt == 3 */ + /* right and bottom intersection */ + /* triangle */ + return (0.5 * (rx1 - lx0) * (ly1 - ry0)); + } +} + + + +static double +rectang_area(double rx0, double ry0, double rx1, double ry1, + double tx0, double ty0, double tx1, double ty1) { +/* Compute rectangle area */ +/* rx0,ry0,rx1,ry1: rectangle boundaries */ +/* tx0,ty0,tx1,ty1: rectangle boundaries */ + + /* Compute overlapping box */ + if (tx0 > rx0) + rx0 = tx0; + if (ty0 > ry0) + ry0 = ty0; + if (tx1 < rx1) + rx1 = tx1; + if (ty1 < ry1) + ry1 = ty1; + if (rx1 <= rx0 || ry1 <= ry0) + return 0.0; + return (rx1 - rx0) * (ry1 - ry0); +} + + + + +static double +hex_area(double sx, double sy, double hx, double hy, double d) { +/* compute the area of overlap of a hexagon diameter d, */ +/* centered at hx,hy, with a unit square of center sx,sy. */ +/* sx,sy: square center */ +/* hx,hy,d: hexagon center and diameter */ + + double hx0,hx1,hx2,hy0,hy1,hy2,hy3; + double sx0,sx1,sy0,sy1; + + /* compute square co-ordinates */ + sx0 = sx - 0.5; + sy0 = sy - 0.5; + sx1 = sx + 0.5; + sy1 = sy + 0.5; + + /* compute hexagon co-ordinates */ + hx0 = hx - d/2.0; + hx1 = hx; + hx2 = hx + d/2.0; + hy0 = hy - 0.5773502692 * d; /* d / sqrt(3) */ + hy1 = hy - 0.2886751346 * d; /* d / sqrt(12) */ + hy2 = hy + 0.2886751346 * d; /* d / sqrt(12) */ + hy3 = hy + 0.5773502692 * d; /* d / sqrt(3) */ + + return + triang_area(sx0,sy0,sx1,sy1,hx0,hy2,hx1,hy3,NW) + + triang_area(sx0,sy0,sx1,sy1,hx1,hy2,hx2,hy3,NE) + + rectang_area(sx0,sy0,sx1,sy1,hx0,hy1,hx2,hy2) + + triang_area(sx0,sy0,sx1,sy1,hx0,hy0,hx1,hy1,SW) + + triang_area(sx0,sy0,sx1,sy1,hx1,hy0,hx2,hy1,SE); +} + + + + +static void +setupAvediv(void) { + + unsigned int i; + + for (i=0; i < (7 * NOCSVAL); ++i) { + /* divide scaled value by 7 lookup */ + AVEDIV[i] = CSCTOSC(i)/7; /* scaled divide by 7 */ + } + +} + + + + +static void +setupSquare(void) { + + unsigned int i; + + for (i=0; i < (2 * NOCSVAL); ++i) { + /* compute square and rescale by (val >> (2 * SCALEB + 2)) table */ + int const val = CSCTOSC(i - NOCSVAL); + /* NOCSVAL offset to cope with -ve input values */ + SQUARE[i] = (val * val) >> (2 * SCALEB + 2); + } +} + + + + +static void +setup1(double const alpha, + double const radius, + double const maxscale, + int * const alpharangeP, + double * const meanscaleP, + double * const mmeanscaleP, + double * const alphafractionP, + int * const noisevarianceP) { + + + setupAvediv(); + setupSquare(); + + if (alpha >= 0.0 && alpha <= 0.5) { + /* alpha trimmed mean */ + double const noinmean = ((0.5 - alpha) * 12.0) + 1.0; + /* number of elements (out of a possible 7) used in the mean */ + + *mmeanscaleP = *meanscaleP = maxscale/noinmean; + if (alpha == 0.0) { + /* mean filter */ + *alpharangeP = 0; + *alphafractionP = 0.0; /* not used */ + } else if (alpha < (1.0/6.0)) { + /* mean of 5 to 7 middle values */ + *alpharangeP = 1; + *alphafractionP = (7.0 - noinmean)/2.0; + } else if (alpha < (1.0/3.0)) { + /* mean of 3 to 5 middle values */ + *alpharangeP = 2; + *alphafractionP = (5.0 - noinmean)/2.0; + } else { + /* mean of 1 to 3 middle values */ + /* alpha == 0.5 == median filter */ + *alpharangeP = 3; + *alphafractionP = (3.0 - noinmean)/2.0; + } + } else if (alpha >= 1.0 && alpha <= 2.0) { + /* optimal estimation - alpha controls noise variance threshold. */ + double const alphaNormalized = alpha - 1.0; + /* normalize it to 0.0 -> 1.0 */ + double const noinmean = 7.0; + *alpharangeP = 5; /* edge enhancement function */ + *mmeanscaleP = *meanscaleP = maxscale; /* compute scaled hex values */ + *alphafractionP = 1.0/noinmean; + /* Set up 1:1 division lookup - not used */ + *noisevarianceP = sqr(alphaNormalized * omaxval) / 8.0; + /* estimate of noise variance */ + } else if (alpha >= -0.9 && alpha <= -0.1) { + /* edge enhancement function */ + double const posAlpha = -alpha; + /* positive alpha value */ + *alpharangeP = 4; /* edge enhancement function */ + *meanscaleP = maxscale * (-posAlpha/((1.0 - posAlpha) * 7.0)); + /* mean of 7 and scaled by -posAlpha/(1-posAlpha) */ + *mmeanscaleP = maxscale * (1.0/(1.0 - posAlpha) + *meanscaleP); + /* middle pixel has 1/(1-posAlpha) as well */ + *alphafractionP = 0.0; /* not used */ + } else { + /* An entry condition on 'alpha' makes this impossible */ + pm_error("INTERNAL ERROR: impossible alpha value: %f", alpha); + } +} + + + + +static void +setupAlfrac(double const alphafraction) { + /* set up alpha fraction lookup table used on big/small */ + + unsigned int i; + + for (i=0; i < (NOIVAL * 8); ++i) { + ALFRAC[i] = ROUNDSCALE(i * alphafraction); + } +} + + + + +static void +setupPixelWeightingTables(double const radius, + double const meanscale, + double const mmeanscale) { + + /* Setup pixel weighting tables - note we pre-compute mean + division here too. + */ + double const hexhoff = radius/2; + /* horizontal offset of vertical hex centers */ + double const hexvoff = 3.0 * radius/sqrt(12.0); + /* vertical offset of vertical hex centers */ + + double const tabscale = meanscale / (radius * hexvoff); + double const mtabscale = mmeanscale / (radius * hexvoff); + + /* scale tables to normalize by hexagon area, and number of + hexes used in mean + */ + double const v0 = + hex_area(0.0, 0.0, hexhoff, hexvoff, radius) * tabscale; + double const v1 = + hex_area(0.0, 1.0, hexhoff, hexvoff, radius) * tabscale; + double const v2 = + hex_area(1.0, 1.0, hexhoff, hexvoff, radius) * tabscale; + double const v3 = + hex_area(1.0, 0.0, hexhoff, hexvoff, radius) * tabscale; + double const m0 = + hex_area(0.0, 0.0, 0.0, 0.0, radius) * mtabscale; + double const m1 = + hex_area(0.0, 1.0, 0.0, 0.0, radius) * mtabscale; + double const m2 = + hex_area(0.0, -1.0, 0.0, 0.0, radius) * mtabscale; + double const h0 = + hex_area(0.0, 0.0, radius, 0.0, radius) * tabscale; + double const h1 = + hex_area(1.0, 1.0, radius, 0.0, radius) * tabscale; + double const h2 = + hex_area(1.0, 0.0, radius, 0.0, radius) * tabscale; + double const h3 = + hex_area(1.0, -1.0, radius, 0.0, radius) * tabscale; + + unsigned int i; + + for (i=0; i <= MXIVAL; ++i) { + V0[i] = ROUNDSCALE(i * v0); + V1[i] = ROUNDSCALE(i * v1); + V2[i] = ROUNDSCALE(i * v2); + V3[i] = ROUNDSCALE(i * v3); + M0[i] = ROUNDSCALE(i * m0); + M1[i] = ROUNDSCALE(i * m1); + M2[i] = ROUNDSCALE(i * m2); + H0[i] = ROUNDSCALE(i * h0); + H1[i] = ROUNDSCALE(i * h1); + H2[i] = ROUNDSCALE(i * h2); + H3[i] = ROUNDSCALE(i * h3); + } +} + + + + +/* Table initialization function - return alpha range */ +static int +atfilt_setup(double const alpha, + double const radius, + double const maxscale) { + + int alpharange; /* alpha range value 0 - 5 */ + double meanscale; /* scale for finding mean */ + double mmeanscale; /* scale for finding mean - midle hex */ + double alphafraction; + /* fraction of next largest/smallest to subtract from sum */ + + setup1(alpha, radius, maxscale, + &alpharange, &meanscale, &mmeanscale, &alphafraction, + &noisevariance); + + setupAlfrac(alphafraction); + + setupPixelWeightingTables(radius, meanscale, mmeanscale); + + return alpharange; +} + + + +static int +atfilt0(int * p) { +/* Core pixel processing function - hand it 3x3 pixels and return result. */ +/* Mean filter */ + /* 'p' is 9 pixel values from 3x3 neighbors */ + int retv; + /* map to scaled hexagon values */ + retv = M0[p[0]] + M1[p[3]] + M2[p[7]]; + retv += H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]]; + retv += V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]]; + retv += V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]]; + retv += H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]]; + retv += V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]]; + retv += V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]]; + return UNSCALE(retv); +} + +#define CHECK(xx) {\ + h0 += xx; \ + if (xx > big) \ + big = xx; \ + else if (xx < small) \ + small = xx; } + +static int +atfilt1(int * p) { +/* Mean of 5 - 7 middle values */ +/* 'p' is 9 pixel values from 3x3 neighbors */ + + int h0,h1,h2,h3,h4,h5,h6; /* hexagon values 2 3 */ + /* 1 0 4 */ + /* 6 5 */ + int big,small; + /* map to scaled hexagon values */ + h0 = M0[p[0]] + M1[p[3]] + M2[p[7]]; + h1 = H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]]; + h2 = V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]]; + h3 = V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]]; + h4 = H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]]; + h5 = V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]]; + h6 = V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]]; + /* sum values and also discover the largest and smallest */ + big = small = h0; + CHECK(h1); + CHECK(h2); + CHECK(h3); + CHECK(h4); + CHECK(h5); + CHECK(h6); + /* Compute mean of middle 5-7 values */ + return UNSCALE(h0 -ALFRAC[(big + small)>>SCALEB]); +} +#undef CHECK + +#define CHECK(xx) {\ + h0 += xx; \ + if (xx > big1) {\ + if (xx > big0) {\ + big1 = big0; \ + big0 = xx; \ + } else \ + big1 = xx; \ + } \ + if (xx < small1) {\ + if (xx < small0) {\ + small1 = small0; \ + small0 = xx; \ + } else \ + small1 = xx; \ + }\ + } + + +static int +atfilt2(int *p) { +/* Mean of 3 - 5 middle values */ +/* 'p' is 9 pixel values from 3x3 neighbors */ + int h0,h1,h2,h3,h4,h5,h6; /* hexagon values 2 3 */ + /* 1 0 4 */ + /* 6 5 */ + int big0,big1,small0,small1; + /* map to scaled hexagon values */ + h0 = M0[p[0]] + M1[p[3]] + M2[p[7]]; + h1 = H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]]; + h2 = V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]]; + h3 = V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]]; + h4 = H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]]; + h5 = V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]]; + h6 = V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]]; + /* sum values and also discover the 2 largest and 2 smallest */ + big0 = small0 = h0; + small1 = MAXINT; + big1 = 0; + CHECK(h1); + CHECK(h2); + CHECK(h3); + CHECK(h4); + CHECK(h5); + CHECK(h6); + /* Compute mean of middle 3-5 values */ + return UNSCALE(h0 -big0 -small0 -ALFRAC[(big1 + small1)>>SCALEB]); +} + +#undef CHECK + +#define CHECK(xx) {\ + h0 += xx; \ + if (xx > big2) \ + { \ + if (xx > big1) \ + { \ + if (xx > big0) \ + { \ + big2 = big1; \ + big1 = big0; \ + big0 = xx; \ + } \ + else \ + { \ + big2 = big1; \ + big1 = xx; \ + } \ + } \ + else \ + big2 = xx; \ + } \ + if (xx < small2) \ + { \ + if (xx < small1) \ + { \ + if (xx < small0) \ + { \ + small2 = small1; \ + small1 = small0; \ + small0 = xx; \ + } \ + else \ + { \ + small2 = small1; \ + small1 = xx; \ + } \ + } \ + else \ + small2 = xx; \ + }} + +static int +atfilt3(int *p) { +/* Mean of 1 - 3 middle values. If only 1 value, then this is a median + filter. +*/ +/* 'p' is pixel values from 3x3 neighbors */ + int h0,h1,h2,h3,h4,h5,h6; /* hexagon values 2 3 */ + /* 1 0 4 */ + /* 6 5 */ + int big0,big1,big2,small0,small1,small2; + /* map to scaled hexagon values */ + h0 = M0[p[0]] + M1[p[3]] + M2[p[7]]; + h1 = H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]]; + h2 = V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]]; + h3 = V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]]; + h4 = H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]]; + h5 = V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]]; + h6 = V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]]; + /* sum values and also discover the 3 largest and 3 smallest */ + big0 = small0 = h0; + small1 = small2 = MAXINT; + big1 = big2 = 0; + CHECK(h1); + CHECK(h2); + CHECK(h3); + CHECK(h4); + CHECK(h5); + CHECK(h6); + /* Compute mean of middle 1-3 values */ + return UNSCALE(h0 -big0 -big1 -small0 -small1 + -ALFRAC[(big2 + small2)>>SCALEB]); +} +#undef CHECK + +static int +atfilt4(int *p) { +/* Edge enhancement */ +/* notice we use the global omaxval */ +/* 'p' is 9 pixel values from 3x3 neighbors */ + + int hav; + /* map to scaled hexagon values and compute enhance value */ + hav = M0[p[0]] + M1[p[3]] + M2[p[7]]; + hav += H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]]; + hav += V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]]; + hav += V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]]; + hav += H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]]; + hav += V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]]; + hav += V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]]; + if (hav < 0) + hav = 0; + hav = UNSCALE(hav); + if (hav > omaxval) + hav = omaxval; + return hav; +} + +static int +atfilt5(int *p) { +/* Optimal estimation - do smoothing in inverse proportion */ +/* to the local variance. */ +/* notice we use the globals noisevariance and omaxval*/ +/* 'p' is 9 pixel values from 3x3 neighbors */ + + int mean,variance,temp; + int h0,h1,h2,h3,h4,h5,h6; /* hexagon values 2 3 */ + /* 1 0 4 */ + /* 6 5 */ + /* map to scaled hexagon values */ + h0 = M0[p[0]] + M1[p[3]] + M2[p[7]]; + h1 = H0[p[0]] + H1[p[2]] + H2[p[1]] + H3[p[8]]; + h2 = V0[p[0]] + V1[p[3]] + V2[p[2]] + V3[p[1]]; + h3 = V0[p[0]] + V1[p[3]] + V2[p[4]] + V3[p[5]]; + h4 = H0[p[0]] + H1[p[4]] + H2[p[5]] + H3[p[6]]; + h5 = V0[p[0]] + V1[p[7]] + V2[p[6]] + V3[p[5]]; + h6 = V0[p[0]] + V1[p[7]] + V2[p[8]] + V3[p[1]]; + mean = h0 + h1 + h2 + h3 + h4 + h5 + h6; + mean = AVEDIV[SCTOCSC(mean)]; /* compute scaled mean by dividing by 7 */ + temp = (h1 - mean); variance = SQUARE[NOCSVAL + SCTOCSC(temp)]; + /* compute scaled variance */ + temp = (h2 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)]; + /* and rescale to keep */ + temp = (h3 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)]; + /* within 32 bit limits */ + temp = (h4 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)]; + temp = (h5 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)]; + temp = (h6 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)]; + temp = (h0 - mean); variance += SQUARE[NOCSVAL + SCTOCSC(temp)]; + /* (temp = h0 - mean) */ + if (variance != 0) /* avoid possible divide by 0 */ + temp = mean + (variance * temp) / (variance + noisevariance); + /* optimal estimate */ + else temp = h0; + if (temp < 0) + temp = 0; + temp = RUNSCALE(temp); + if (temp > omaxval) + temp = omaxval; + return temp; +} + + + +static void +do_one_frame(FILE *ifp) { + + pnm_writepnminit( stdout, cols, rows, omaxval, oformat, 0 ); + + if ( PNM_FORMAT_TYPE(oformat) == PPM_TYPE ) { + int pr[9],pg[9],pb[9]; /* 3x3 neighbor pixel values */ + int r,g,b; + + for ( row = 0; row < rows; row++ ) { + int po,no; /* offsets for left and right colums in 3x3 */ + xel *ip0, *ip1, *ip2, *op; + + if (row == 0) { + irow0 = irow1; + pnm_readpnmrow( ifp, irow1, cols, maxval, format ); + } + if (row == (rows-1)) + irow2 = irow1; + else + pnm_readpnmrow( ifp, irow2, cols, maxval, format ); + + for (col = cols-1,po= col>0?1:0,no=0, + ip0=irow0,ip1=irow1,ip2=irow2,op=orow; + col >= 0; + col--,ip0++,ip1++,ip2++,op++, no |= 1,po = col!= 0 ? po : 0) { + /* grab 3x3 pixel values */ + pr[0] = PPM_GETR( *ip1 ); + pg[0] = PPM_GETG( *ip1 ); + pb[0] = PPM_GETB( *ip1 ); + pr[1] = PPM_GETR( *(ip1-no) ); + pg[1] = PPM_GETG( *(ip1-no) ); + pb[1] = PPM_GETB( *(ip1-no) ); + pr[5] = PPM_GETR( *(ip1+po) ); + pg[5] = PPM_GETG( *(ip1+po) ); + pb[5] = PPM_GETB( *(ip1+po) ); + pr[3] = PPM_GETR( *(ip2) ); + pg[3] = PPM_GETG( *(ip2) ); + pb[3] = PPM_GETB( *(ip2) ); + pr[2] = PPM_GETR( *(ip2-no) ); + pg[2] = PPM_GETG( *(ip2-no) ); + pb[2] = PPM_GETB( *(ip2-no) ); + pr[4] = PPM_GETR( *(ip2+po) ); + pg[4] = PPM_GETG( *(ip2+po) ); + pb[4] = PPM_GETB( *(ip2+po) ); + pr[6] = PPM_GETR( *(ip0+po) ); + pg[6] = PPM_GETG( *(ip0+po) ); + pb[6] = PPM_GETB( *(ip0+po) ); + pr[8] = PPM_GETR( *(ip0-no) ); + pg[8] = PPM_GETG( *(ip0-no) ); + pb[8] = PPM_GETB( *(ip0-no) ); + pr[7] = PPM_GETR( *(ip0) ); + pg[7] = PPM_GETG( *(ip0) ); + pb[7] = PPM_GETB( *(ip0) ); + r = (*atfunc)(pr); + g = (*atfunc)(pg); + b = (*atfunc)(pb); + PPM_ASSIGN( *op, r, g, b ); + } + pnm_writepnmrow( stdout, orow, cols, omaxval, oformat, 0 ); + if (irow1 == irows[2]) { + irow1 = irows[0]; + irow2 = irows[1]; + irow0 = irows[2]; + } else if (irow1 == irows[1]) { + irow2 = irows[0]; + irow0 = irows[1]; + irow1 = irows[2]; + } + else /* must be at irows[0] */ + { + irow0 = irows[0]; + irow1 = irows[1]; + irow2 = irows[2]; + } + } + } else { + /* Else must be PGM */ + int p[9]; /* 3x3 neighbor pixel values */ + int pv; + int promote; + + /* we scale maxval to omaxval */ + promote = ( PNM_FORMAT_TYPE(format) != PNM_FORMAT_TYPE(oformat) ); + + for ( row = 0; row < rows; row++ ) { + int po,no; /* offsets for left and right colums in 3x3 */ + xel *ip0, *ip1, *ip2, *op; + + if (row == 0) { + irow0 = irow1; + pnm_readpnmrow( ifp, irow1, cols, maxval, format ); + if ( promote ) + pnm_promoteformatrow( irow1, cols, maxval, + format, maxval, oformat ); + } + if (row == (rows-1)) + irow2 = irow1; + else { + pnm_readpnmrow( ifp, irow2, cols, maxval, format ); + if ( promote ) + pnm_promoteformatrow( irow2, cols, maxval, + format, maxval, oformat ); + } + + for (col = cols-1,po= col>0?1:0,no=0, + ip0=irow0,ip1=irow1,ip2=irow2,op=orow; + col >= 0; + col--,ip0++,ip1++,ip2++,op++, no |= 1,po = col!= 0 ? po : 0) { + /* grab 3x3 pixel values */ + p[0] = PNM_GET1( *ip1 ); + p[1] = PNM_GET1( *(ip1-no) ); + p[5] = PNM_GET1( *(ip1+po) ); + p[3] = PNM_GET1( *(ip2) ); + p[2] = PNM_GET1( *(ip2-no) ); + p[4] = PNM_GET1( *(ip2+po) ); + p[6] = PNM_GET1( *(ip0+po) ); + p[8] = PNM_GET1( *(ip0-no) ); + p[7] = PNM_GET1( *(ip0) ); + pv = (*atfunc)(p); + PNM_ASSIGN1( *op, pv ); + } + pnm_writepnmrow( stdout, orow, cols, omaxval, oformat, 0 ); + if (irow1 == irows[2]) { + irow1 = irows[0]; + irow2 = irows[1]; + irow0 = irows[2]; + } else if (irow1 == irows[1]) { + irow2 = irows[0]; + irow0 = irows[1]; + irow1 = irows[2]; + } else { + /* must be at irows[0] */ + irow0 = irows[0]; + irow1 = irows[1]; + irow2 = irows[2]; + } + } + } +} + + + +static void +verifySame(unsigned int const imageSeq, + int const imageCols, int const imageRows, + xelval const imageMaxval, int const imageFormat, + int const cols, int const rows, + xelval const maxval, int const format) { +/*---------------------------------------------------------------------------- + Issue error message and exit the program if the imageXXX arguments don't + match the XXX arguments. +-----------------------------------------------------------------------------*/ + if (imageCols != cols) + pm_error("Width of Image %u (%d) is not the same as Image 0 (%d)", + imageSeq, imageCols, cols); + if (imageRows != rows) + pm_error("Height of Image %u (%d) is not the same as Image 0 (%d)", + imageSeq, imageRows, rows); + if (imageMaxval != maxval) + pm_error("Maxval of Image %u (%u) is not the same as Image 0 (%u)", + imageSeq, imageMaxval, maxval); + if (imageFormat != format) + pm_error("Format of Image %u is not the same as Image 0", + imageSeq); +} + + + +int (*atfuncs[6]) (int *) = {atfilt0,atfilt1,atfilt2,atfilt3,atfilt4,atfilt5}; + + + + +int +main(int argc, char *argv[]) { + + FILE * ifp; + bool eof; /* We've hit the end of the input stream */ + unsigned int imageSeq; /* Sequence number of image, starting from 0 */ + + const char* const usage = "alpha radius pnmfile\n" + "0.0 <= alpha <= 0.5 for alpha trimmed mean -or- \n" + "1.0 <= alpha <= 2.0 for optimal estimation -or- \n" + "-0.1 >= alpha >= -0.9 for edge enhancement\n" + "0.3333 <= radius <= 1.0 specify effective radius\n"; + + pnm_init( &argc, argv ); + + if ( argc < 3 || argc > 4 ) + pm_usage( usage ); + + if ( sscanf( argv[1], "%lf", &alpha ) != 1 ) + pm_usage( usage ); + if ( sscanf( argv[2], "%lf", &radius ) != 1 ) + pm_usage( usage ); + + if ((alpha > -0.1 && alpha < 0.0) || (alpha > 0.5 && alpha < 1.0)) + pm_error( "Alpha must be in range 0.0 <= alpha <= 0.5 " + "for alpha trimmed mean" ); + if (alpha > 2.0) + pm_error( "Alpha must be in range 1.0 <= alpha <= 2.0 " + "for optimal estimation" ); + if (alpha < -0.9 || (alpha > -0.1 && alpha < 0.0)) + pm_error( "Alpha must be in range -0.9 <= alpha <= -0.1 " + "for edge enhancement" ); + if (radius < 0.333 || radius > 1.0) + pm_error( "Radius must be in range 0.333333333 <= radius <= 1.0" ); + + if ( argc == 4 ) + ifp = pm_openr( argv[3] ); + else + ifp = stdin; + + pnm_readpnminit( ifp, &cols, &rows, &maxval, &format ); + + if (maxval > MXIVAL) + pm_error("The maxval of the input image (%d) is too large.\n" + "This program's limit is %d.", + maxval, MXIVAL); + + oformat = PNM_FORMAT_TYPE(format); + /* force output to max precision without forcing new 2-byte format */ + omaxval = MIN(maxval, PPM_MAXMAXVAL); + + atfunc = atfuncs[atfilt_setup(alpha, radius, + (double)omaxval/(double)maxval)]; + + if ( oformat < PGM_TYPE ) { + oformat = RPGM_FORMAT; + pm_message( "promoting file to PGM" ); + } + + orow = pnm_allocrow(cols); + irows[0] = pnm_allocrow(cols); + irows[1] = pnm_allocrow(cols); + irows[2] = pnm_allocrow(cols); + irow0 = irows[0]; + irow1 = irows[1]; + irow2 = irows[2]; + + eof = FALSE; /* We're already in the middle of the first image */ + imageSeq = 0; + while (!eof) { + do_one_frame(ifp); + pm_nextimage(ifp, &eof); + if (!eof) { + /* Read and validate header of next image */ + int imageCols, imageRows; + xelval imageMaxval; + int imageFormat; + + ++imageSeq; + pnm_readpnminit(ifp, &imageCols, &imageRows, + &imageMaxval, &imageFormat); + verifySame(imageSeq, + imageCols, imageRows, imageMaxval, imageFormat, + cols, rows, maxval, format); + } + } + + pnm_freerow(irow0); + pnm_freerow(irow1); + pnm_freerow(irow2); + pnm_freerow(orow); + pm_close(ifp); + + return 0; +} + + diff --git a/editor/pnmnorm.c b/editor/pnmnorm.c new file mode 100644 index 00000000..51d954a8 --- /dev/null +++ b/editor/pnmnorm.c @@ -0,0 +1,620 @@ +/****************************************************************************** + Pnmnorm +******************************************************************************* + + This program normalizes the contrast in a Netpbm image. + + by Bryan Henderson bryanh@giraffe-data.com San Jose CA March 2002. + Adapted from Ppmnorm. + + Ppmnorm is by Wilson H. Bent, Jr. (whb@usc.edu) + Extensively hacked from pgmnorm.c, which carries the following note: + + Copyright (C) 1989, 1991 by Jef Poskanzer. + + 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. + + (End of note from pgmnorm.c) + + Pgmnorm's man page also said: + + Partially based on the fbnorm filter in Michael Mauldin's "Fuzzy Pixmap" + package. +*****************************************************************************/ + +#include <assert.h> + +#include "pnm.h" +#include "shhopt.h" +#include "mallocvar.h" + +enum brightMethod {BRIGHT_LUMINOSITY, BRIGHT_COLORVALUE, BRIGHT_SATURATION}; + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *inputFilespec; /* Filespec of input file */ + unsigned int bvalueSpec; + xelval bvalue; + unsigned int bpercentSpec; + float bpercent; + unsigned int wvalueSpec; + xelval wvalue; + unsigned int wpercentSpec; + float wpercent; + enum brightMethod brightMethod; + unsigned int keephues; + float maxExpansion; + /* The maximum allowed expansion factor for expansion specified + by per centile. This is a factor, not a per cent increase. + E.g. 50% increase means a factor of 1.50. + */ +}; + + + +static void +parseCommandLine (int argc, char ** argv, + struct cmdlineInfo *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 luminosity, colorvalue, saturation; + unsigned int maxexpandSpec; + float maxexpand; + + unsigned int option_def_index; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "bpercent", OPT_FLOAT, + &cmdlineP->bpercent, &cmdlineP->bpercentSpec, 0); + OPTENT3(0, "wpercent", OPT_FLOAT, + &cmdlineP->wpercent, &cmdlineP->wpercentSpec, 0); + OPTENT3(0, "bvalue", OPT_UINT, + &cmdlineP->bvalue, &cmdlineP->bvalueSpec, 0); + OPTENT3(0, "wvalue", OPT_UINT, + &cmdlineP->wvalue, &cmdlineP->wvalueSpec, 0); + OPTENT3(0, "maxexpand", OPT_FLOAT, + &maxexpand, &maxexpandSpec, 0); + OPTENT3(0, "keephues", OPT_FLAG, + NULL, &cmdlineP->keephues, 0); + OPTENT3(0, "luminosity", OPT_FLAG, + NULL, &luminosity, 0); + OPTENT3(0, "colorvalue", OPT_FLAG, + NULL, &colorvalue, 0); + OPTENT3(0, "saturation", OPT_FLAG, + NULL, &saturation, 0); + OPTENT3(0, "brightmax", OPT_FLAG, + NULL, &colorvalue, 0); + + /* Note: -brightmax was documented and accepted long before it was + actually implemented. By the time we implemented it, we + decided -colorvalue was a better name for it. + */ + + 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 */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0 ); + /* Uses and sets argc, argv, and some of *cmdline_p and others. */ + + if (!cmdlineP->wpercentSpec) + cmdlineP->wpercent = 1.0; + if (!cmdlineP->bpercentSpec) + cmdlineP->bpercent = 2.0; + + if (cmdlineP->wpercent < 0.0) + pm_error("You specified a negative value for wpercent: %f", + cmdlineP->wpercent); + if (cmdlineP->bpercent < 0.0) + pm_error("You specified a negative value for bpercent: %f", + cmdlineP->bpercent); + if (cmdlineP->wpercent > 100.0) + pm_error("You specified a per centage > 100 for wpercent: %f", + cmdlineP->wpercent); + if (cmdlineP->bpercent > 100.0) + pm_error("You specified a per centage > 100 for bpercent: %f", + cmdlineP->bpercent); + + if (luminosity + colorvalue + saturation > 1) + pm_error("You can specify only one of " + "-luminosity, -colorvalue, and -saturation"); + else { + if (colorvalue) + cmdlineP->brightMethod = BRIGHT_COLORVALUE; + else if (saturation) + cmdlineP->brightMethod = BRIGHT_SATURATION; + else + cmdlineP->brightMethod = BRIGHT_LUMINOSITY; + } + if (maxexpandSpec) { + if (maxexpand < 0) + pm_error("-maxexpand must be positive. You specified %f", + maxexpand); + else + cmdlineP->maxExpansion = 1 + (float)maxexpand/100; + } else + cmdlineP->maxExpansion = 1e6; /* essentially infinite */ + + if (argc-1 > 1) + pm_error("Program takes at most one argument: the input file " + "specification. " + "You specified %d arguments.", argc-1); + if (argc-1 < 1) + cmdlineP->inputFilespec = "-"; + else + cmdlineP->inputFilespec = argv[1]; +} + + + +static void +buildHistogram(FILE * const ifp, + int const cols, + int const rows, + xelval const maxval, + int const format, + unsigned int hist[], + enum brightMethod const brightMethod) { +/*---------------------------------------------------------------------------- + Build the histogram of brightness values for the image that is in file + 'ifp', which is positioned just after the header (at the raster). + + The histogram is the array hist[] such that hist[x] is the number + of xels in the image that have brightness x. That brightness is + either the color value (intensity of most intense component) of the + xel or it is the luminosity of the xel, depending on + 'brightMethod'. In either case, it is based on the same maxval as + the image, which is 'maxval'. The image is 'cols' columns wide by + 'rows' rows high. + + Leave the file positioned arbitrarily. +-----------------------------------------------------------------------------*/ + int row; + xel * xelrow; + + xelrow = pnm_allocrow(cols); + + { + unsigned int i; + for (i = 0; i <= maxval; ++i) + hist[i] = 0; + } + for (row = 0; row < rows; ++row) { + int col; + pnm_readpnmrow(ifp, xelrow, cols, maxval, format); + for (col = 0; col < cols; ++col) { + xelval brightness; + xel const p = xelrow[col]; + if (PNM_FORMAT_TYPE(format) == PPM_TYPE) { + switch(brightMethod) { + case BRIGHT_LUMINOSITY: + brightness = PPM_LUMIN(p); + break; + case BRIGHT_COLORVALUE: + brightness = ppm_colorvalue(p); + break; + case BRIGHT_SATURATION: + brightness = ppm_saturation(p, maxval); + break; + } + } else + brightness = PNM_GET1(p); + ++hist[brightness]; + } + } + pnm_freerow(xelrow); +} + + + +static void +computeBottomPercentile(unsigned int hist[], + unsigned int const highest, + unsigned int const total, + float const percent, + unsigned int * const percentileP) { +/*---------------------------------------------------------------------------- + Compute the lowest index of hist[] such that the sum of the hist[] + values with that index and lower represent at least 'percent' per cent of + 'n' (which is assumed to be the sum of all the values in hist[], + given to us to save us the time of computing it). +-----------------------------------------------------------------------------*/ + unsigned int cutoff = total * percent / 100.0; + unsigned int count; + unsigned int percentile; + + percentile = 0; /* initial value */ + count = hist[0]; /* initial value */ + + while (count < cutoff) { + if (percentile == highest) + pm_error("Internal error: computeBottomPercentile() received" + "a 'total' value greater than the sum of the hist[]" + "values"); + ++percentile; + count += hist[percentile]; + } + *percentileP = percentile; +} + + + +static void +computeTopPercentile(unsigned int hist[], + unsigned int const highest, + unsigned int const total, + float const percent, + unsigned int * const percentileP) { +/*---------------------------------------------------------------------------- + Compute the highest index of hist[] such that the sum of the hist[] + values with that index and higher represent 'percent' per cent of + 'n' (which is assumed to be the sum of all the values in hist[], + given to us to save us the time of computing it). +-----------------------------------------------------------------------------*/ + unsigned int cutoff = total * percent / 100.0; + unsigned int count; + unsigned int percentile; + + percentile = highest; /* initial value */ + count = hist[highest]; + + while (count < cutoff) { + --percentile; + count += hist[percentile]; + } + *percentileP = percentile; +} + + + +static void +computeAdjustmentForExpansionLimit(xelval const maxval, + xelval const unlBvalue, + xelval const unlWvalue, + float const maxExpansion, + xelval * const bLowerP, + xelval * const wRaiseP) { +/*---------------------------------------------------------------------------- + Assuming 'unlBvalue' and 'unlWvalue' are the appropriate bvalue and + wvalue to normalize the image to 0 .. maxval, compute the amount + by which the bvalue must be raised and the wvalue lowered from that + in order to cap the expansion factor at 'maxExpansion'. + + E.g. if 'maxval' is 100, 'unlBvalue' is 20 and 'unlWvalue' is 70, that + implies an expansion factor of 100/50 (because the range goes from + 70-20, which is 50, to 100 - 0, which is 100). If 'maxEpansion' is + 1.333, these values are unacceptable. To get down to the desired 1.333 + factor, we need the span of bvalue to wvalue to be 75, not 50. So + we need to raise the bvalue and lower the wvalue by a total of 25. + We apportion that adjustment to bvalue and wvalue in proportion to + how close each is already to it's end (which we call the margin). + 'unlBvalue' is 20 from its end, while 'unlWvalue' is 30 from its end, + so we want to lower the bvalue by 10 and raise the wvalue by 15. + Ergo we return *bLowerP = 10 and *wRaise = 15. +-----------------------------------------------------------------------------*/ + unsigned int const newRange = maxval - 0; + /* The range of sample values after normalization, if we used + the unlimited bvalue and wvalue + */ + unsigned int const oldRange = unlWvalue - unlBvalue; + /* The range of sample values in the original image that normalize + to 0 .. maxval, if we used the unlimited bvalue and wvalue + */ + float const unlExpansion = (float)newRange/oldRange; + + if (unlExpansion <= maxExpansion) { + /* No capping is necessary. Unlimited values are already within + range. + */ + *bLowerP = 0; + *wRaiseP = 0; + } else { + unsigned int const totalWidening = newRange/maxExpansion - oldRange; + /* Amount by which the (bvalue, wvalue) range must be widened + to limit expansion to 'maxExpansion' + */ + unsigned int const bMargin = unlBvalue - 0; + unsigned int const wMargin = maxval - unlWvalue; + + /* Apportion 'totalWidening' between the black and and the + white end + */ + *bLowerP = + ROUNDU((float)bMargin / (bMargin + wMargin) * totalWidening); + *wRaiseP = + ROUNDU((float)wMargin / (bMargin + wMargin) * totalWidening); + + pm_message("limiting expansion of %.1f%% to %.1f%%", + (unlExpansion - 1) * 100, (maxExpansion -1) * 100); + } +} + + + +static void +computeEndValues(FILE * const ifp, + int const cols, + int const rows, + xelval const maxval, + int const format, + struct cmdlineInfo const cmdline, + xelval * const bvalueP, + xelval * const wvalueP) { +/*---------------------------------------------------------------------------- + Figure out what original values will be translated to full white and + full black -- thus defining to what all the other values get translated. + + This may involve looking at the image. The image is in the file + 'ifp', which is positioned just past the header (at the raster). + Leave it positioned arbitrarily. +-----------------------------------------------------------------------------*/ + unsigned int * hist; /* malloc'ed */ + + MALLOCARRAY(hist, PNM_OVERALLMAXVAL+1); + + if (hist == NULL) + pm_error("Unable to allocate storage for intensity histogram."); + else { + xelval unlimitedBvalue, unlimitedWvalue; + unsigned int bLower, wRaise; + + buildHistogram(ifp, cols, rows, maxval, format, hist, + cmdline.brightMethod); + + if (cmdline.bvalueSpec && !cmdline.bpercentSpec) { + unlimitedBvalue = cmdline.bvalue; + } else { + xelval percentBvalue; + computeBottomPercentile(hist, maxval, cols*rows, cmdline.bpercent, + &percentBvalue); + if (cmdline.bvalueSpec) + unlimitedBvalue = MIN(percentBvalue, cmdline.bvalue); + else + unlimitedBvalue = percentBvalue; + } + + if (cmdline.wvalueSpec && !cmdline.wpercentSpec) { + unlimitedWvalue = cmdline.wvalue; + } else { + xelval percentWvalue; + computeTopPercentile(hist, maxval, cols*rows, cmdline.wpercent, + &percentWvalue); + if (cmdline.wvalueSpec) + unlimitedWvalue = MIN(percentWvalue, cmdline.wvalue); + else + unlimitedWvalue = percentWvalue; + } + + computeAdjustmentForExpansionLimit( + maxval, unlimitedBvalue, unlimitedWvalue, cmdline.maxExpansion, + &bLower, &wRaise); + + *bvalueP = unlimitedBvalue - bLower; + *wvalueP = unlimitedWvalue + wRaise; + + free(hist); + } +} + + + +static void +computeTransferFunction(xelval const bvalue, + xelval const wvalue, + xelval const maxval, + xelval ** const newBrightnessP) { +/*---------------------------------------------------------------------------- + Compute the transfer function, i.e. the array *newBrightnessP such that + (*newBrightnessP)[x] is the brightness of the xel that should replace a + xel with brightness x. Brightness in this case means either luminosity + or color value (and it doesn't matter to us which). + + 'bvalue' is the highest brightness that should map to zero brightness; + 'wvalue' is the lowest brightness that should map to full brightness. + brightnesses in between should be stretched linearly. (That stretching + could conceivably result in more brightnesses mapping to zero and full + brightness, due to rounding). + + Define function only for values 0..maxval. +-----------------------------------------------------------------------------*/ + xelval * newBrightness; + xelval i; + + MALLOCARRAY(newBrightness, maxval+1); + + if (newBrightness == NULL) + pm_error("Unable to allocate memory for transfer function."); + + /* Clip the lowest brightnesses to zero */ + if (bvalue > 0) + for (i = 0; i < bvalue; ++i) + newBrightness[i] = 0; + + /* Map the middle brightnesses linearly onto 0..maxval */ + { + unsigned int const range = wvalue - bvalue; + unsigned int val; + /* The following for loop is a hand optimization of this one: + for (i = bvalue; i <= wvalue; ++i) + newBrightness[i] = (i-bvalue)*maxval/range); + (with proper rounding) + */ + for (i = bvalue, val = range/2; + i <= wvalue; + ++i, val += maxval) + newBrightness[i] = MIN(val / range, maxval); + + assert(newBrightness[bvalue] == 0); + assert(newBrightness[wvalue] == maxval); + } + + /* Clip the highest brightnesses to maxval */ + for (i = wvalue+1; i <= maxval; ++i) + newBrightness[i] = maxval; + + *newBrightnessP = newBrightness; +} + + + +static float +brightScaler(xel const p, + pixval const maxval, + xelval const newBrightness[], + enum brightMethod const brightMethod) { +/*---------------------------------------------------------------------------- + Return the multiple by which the brightness pixel of color 'p' (based + on maxval 'maxval') should be changed according to the transfer + function newBrightness[], using the 'brightMethod' measure of + brightness. + + For example, if 'brightMethod' is BRIGHT_LUMINOSITY, p is has + luminosity 50, and newBrightness[50] is 75, we would return 1.5. +-----------------------------------------------------------------------------*/ + xelval oldBrightness; + float scaler; + + switch (brightMethod) { + case BRIGHT_LUMINOSITY: + oldBrightness = PPM_LUMIN(p); + break; + case BRIGHT_COLORVALUE: + oldBrightness = ppm_colorvalue(p); + break; + case BRIGHT_SATURATION: + oldBrightness = ppm_saturation(p, maxval); + break; + } + if (oldBrightness == 0) { + assert(newBrightness[oldBrightness] == 0); + /* Doesn't matter what we scale by. zero times anything is zero. */ + scaler = 1.0; + } else + scaler = (float)newBrightness[oldBrightness]/oldBrightness; + + return scaler; +} + + + +static void +writeRowNormalized(xel * const xelrow, + int const cols, + xelval const maxval, + int const format, + enum brightMethod const brightMethod, + bool const keephues, + xelval const newBrightness[], + xel * const rowbuf) { +/*---------------------------------------------------------------------------- + Write to Standard Output a normalized version of the xel row + 'xelrow'. Normalize it via the transfer function newBrightness[]. + + Use 'rowbuf' as a work buffer. It is at least 'cols' columns wide. +-----------------------------------------------------------------------------*/ + xel * const outrow = rowbuf; + + unsigned int col; + for (col = 0; col < cols; ++col) { + xel const p = xelrow[col]; + + if (PPM_FORMAT_TYPE(format) == PPM_TYPE) { + if (keephues) { + float const scaler = + brightScaler(p, maxval, newBrightness, brightMethod); + + xelval const r = MIN((int)(PPM_GETR(p)*scaler+0.5), maxval); + xelval const g = MIN((int)(PPM_GETG(p)*scaler+0.5), maxval); + xelval const b = MIN((int)(PPM_GETB(p)*scaler+0.5), maxval); + PNM_ASSIGN(outrow[col], r, g, b); + } else + PNM_ASSIGN(outrow[col], + newBrightness[PPM_GETR(p)], + newBrightness[PPM_GETG(p)], + newBrightness[PPM_GETB(p)]); + } else + PNM_ASSIGN1(outrow[col], newBrightness[PNM_GET1(p)]); + } + pnm_writepnmrow(stdout, outrow, cols, maxval, format, 0); +} + + + +int +main(int argc, char *argv[]) { + + struct cmdlineInfo cmdline; + FILE *ifP; + pm_filepos imagePos; + xelval maxval; + int rows, cols, format; + xelval bvalue, wvalue; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr_seekable(cmdline.inputFilespec); + + /* Rescale so that bvalue maps to 0, wvalue maps to maxval. */ + pnm_readpnminit(ifP, &cols, &rows, &maxval, &format); + pm_tell2(ifP, &imagePos, sizeof(imagePos)); + + computeEndValues(ifP, cols, rows, maxval, format, cmdline, + &bvalue, &wvalue); + + if (wvalue <= bvalue) + pm_error("The colors which become black would overlap the " + "colors which become white."); + else { + xelval * newBrightness; + int row; + xel * xelrow; + xel * rowbuf; + + xelrow = pnm_allocrow(cols); + + pm_message("remapping %d..%d to %d..%d", bvalue, wvalue, 0, maxval); + + computeTransferFunction(bvalue, wvalue, maxval, &newBrightness); + + pm_seek2(ifP, &imagePos, sizeof(imagePos)); + pnm_writepnminit(stdout, cols, rows, maxval, format, 0); + + rowbuf = pnm_allocrow(cols); + + for (row = 0; row < rows; ++row) { + pnm_readpnmrow(ifP, xelrow, cols, maxval, format); + writeRowNormalized(xelrow, cols, maxval, format, + cmdline.keephues, cmdline.brightMethod, + newBrightness, rowbuf); + } + free(newBrightness); + pnm_freerow(rowbuf); + pnm_freerow(xelrow); + } + pm_close(ifP); + return 0; +} diff --git a/editor/pnmpad.c b/editor/pnmpad.c new file mode 100644 index 00000000..e1fbdaec --- /dev/null +++ b/editor/pnmpad.c @@ -0,0 +1,387 @@ +/* pnmpad.c - add border to sides of a portable anymap + ** AJCD 4/9/90 + */ + +/* + * Changelog + * + * 2002/01/25 - Rewrote options parsing code. + * Added pad-to-width and pad-to-height with custom + * alignment. MVB. + */ + +#include <string.h> +#include <stdio.h> + +#include "pnm.h" +#include "shhopt.h" +#include "mallocvar.h" + + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *input_filespec; /* Filespecs of input files */ + unsigned int xsize; + unsigned int xsizeSpec; + unsigned int ysize; + unsigned int ysizeSpec; + unsigned int left; + unsigned int right; + unsigned int top; + unsigned int bottom; + unsigned int leftSpec; + unsigned int rightSpec; + unsigned int topSpec; + unsigned int bottomSpec; + float xalign; + float yalign; + unsigned int white; /* >0: pad white; 0: pad black */ + unsigned int verbose; +}; + + + +static void +parseCommandLine(int argc, char ** 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 blackOpt; + unsigned int xalignSpec, yalignSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "xsize", OPT_UINT, &cmdlineP->xsize, + &cmdlineP->xsizeSpec, 0); + OPTENT3(0, "width", OPT_UINT, &cmdlineP->xsize, + &cmdlineP->xsizeSpec, 0); + OPTENT3(0, "ysize", OPT_UINT, &cmdlineP->ysize, + &cmdlineP->ysizeSpec, 0); + OPTENT3(0, "height", OPT_UINT, &cmdlineP->ysize, + &cmdlineP->ysizeSpec, 0); + OPTENT3(0, "left", OPT_UINT, &cmdlineP->left, + &cmdlineP->leftSpec, 0); + OPTENT3(0, "right", OPT_UINT, &cmdlineP->right, + &cmdlineP->rightSpec, 0); + OPTENT3(0, "top", OPT_UINT, &cmdlineP->top, + &cmdlineP->topSpec, 0); + OPTENT3(0, "bottom", OPT_UINT, &cmdlineP->bottom, + &cmdlineP->bottomSpec, 0); + OPTENT3(0, "xalign", OPT_FLOAT, &cmdlineP->xalign, + &xalignSpec, 0); + OPTENT3(0, "halign", OPT_FLOAT, &cmdlineP->xalign, + &xalignSpec, 0); + OPTENT3(0, "yalign", OPT_FLOAT, &cmdlineP->yalign, + &yalignSpec, 0); + OPTENT3(0, "valign", OPT_FLOAT, &cmdlineP->yalign, + &yalignSpec, 0); + OPTENT3(0, "black", OPT_FLAG, NULL, + &blackOpt, 0); + OPTENT3(0, "white", OPT_FLAG, NULL, + &cmdlineP->white, 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 */ + + optParseOptions3(&argc, argv, opt, sizeof opt, 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (blackOpt && cmdlineP->white) + pm_error("You cannot specify both -black and -white"); + + if (xalignSpec && (cmdlineP->leftSpec || cmdlineP->rightSpec)) + pm_error("You cannot specify both -xalign and -left or -right"); + + if (yalignSpec && (cmdlineP->topSpec || cmdlineP->bottomSpec)) + pm_error("You cannot specify both -yalign and -top or -bottom"); + + if (xalignSpec && !cmdlineP->xsizeSpec) + pm_error("-xalign is meaningless without -width"); + + if (yalignSpec && !cmdlineP->ysizeSpec) + pm_error("-yalign is meaningless without -height"); + + if (xalignSpec) { + if (cmdlineP->xalign < 0) + pm_error("You have specified a negative -halign value (%f)", + cmdlineP->xalign); + if (cmdlineP->xalign > 1) + pm_error("You have specified a -halign value (%f) greater than 1", + cmdlineP->xalign); + } else + cmdlineP->xalign = 0.5; + + if (yalignSpec) { + if (cmdlineP->yalign < 0) + pm_error("You have specified a negative -halign value (%f)", + cmdlineP->yalign); + if (cmdlineP->yalign > 1) + pm_error("You have specified a -valign value (%f) greater than 1", + cmdlineP->yalign); + } else + cmdlineP->yalign = 0.5; + + /* get optional input filename */ + if (argc-1 > 1) + pm_error("This program takes at most 1 parameter. You specified %d", + argc-1); + else if (argc-1 == 1) + cmdlineP->input_filespec = argv[1]; + else + cmdlineP->input_filespec = "-"; +} + + + +static void +parseCommandLineOld(int argc, char ** argv, + struct cmdlineInfo * const cmdlineP) { + + /* This syntax was abandonned in February 2002. */ + pm_message("Warning: old style options are deprecated!"); + + cmdlineP->xsize = cmdlineP->ysize = 0; + cmdlineP->left = cmdlineP->right = cmdlineP->top = cmdlineP->bottom = 0; + cmdlineP->xalign = cmdlineP->yalign = 0.5; + cmdlineP->white = cmdlineP->verbose = FALSE; + + while (argc >= 2 && argv[1][0] == '-') { + if (strcmp(argv[1]+1,"black") == 0) cmdlineP->white = FALSE; + else if (strcmp(argv[1]+1,"white") == 0) cmdlineP->white = TRUE; + else switch (argv[1][1]) { + case 'l': + if (atoi(argv[1]+2) < 0) + pm_error("left border too small"); + else + cmdlineP->left = atoi(argv[1]+2); + break; + case 'r': + if (atoi(argv[1]+2) < 0) + pm_error("right border too small"); + else + cmdlineP->right = atoi(argv[1]+2); + break; + case 'b': + if (atoi(argv[1]+2) < 0) + pm_error("bottom border too small"); + else + cmdlineP->bottom = atoi(argv[1]+2); + break; + case 't': + if (atoi(argv[1]+2) < 0) + pm_error("top border too small"); + else + cmdlineP->top = atoi(argv[1]+2); + break; + default: + pm_usage("[-white|-black] [-l#] [-r#] [-t#] [-b#] [pnmfile]"); + } + argc--, argv++; + } + + cmdlineP->xsizeSpec = (cmdlineP->xsize > 0); + cmdlineP->ysizeSpec = (cmdlineP->ysize > 0); + + if (argc > 2) + pm_usage("[-white|-black] [-l#] [-r#] [-t#] [-b#] [pnmfile]"); + + if (argc == 2) + cmdlineP->input_filespec = argv[1]; + else + cmdlineP->input_filespec = "-"; +} + + + +static void +computeHorizontalPadSizes(struct cmdlineInfo const cmdline, + int const cols, + unsigned int * const lpadP, + unsigned int * const rpadP) { + + if (cmdline.xsizeSpec) { + if (cmdline.leftSpec && cmdline.rightSpec) { + if (cmdline.left + cols + cmdline.right < cmdline.xsize) { + pm_error("Left padding (%u), and right " + "padding (%u) are insufficient to bring the " + "image width of %d up to %u.", + cmdline.left, cmdline.right, cols, cmdline.xsize); + } else { + *lpadP = cmdline.left; + *rpadP = cmdline.right; + } + } else if (cmdline.leftSpec) { + *lpadP = cmdline.left; + *rpadP = MAX(cmdline.xsize, cmdline.left + cols) - + (cmdline.left + cols); + } else if (cmdline.rightSpec) { + *rpadP = cmdline.right; + *lpadP = MAX(cmdline.xsize, cols + cmdline.right) - + (cols + cmdline.right); + } else { + if (cmdline.xsize > cols) { + *lpadP = ROUNDU((cmdline.xsize - cols) * cmdline.xalign); + *rpadP = cmdline.xsize - cols - *lpadP; + } else { + *lpadP = 0; + *rpadP = 0; + } + } + } else { + *lpadP = cmdline.leftSpec ? cmdline.left : 0; + *rpadP = cmdline.rightSpec ? cmdline.right : 0; + } +} + + + +static void +computeVerticalPadSizes(struct cmdlineInfo const cmdline, + int const rows, + unsigned int * const tpadP, + unsigned int * const bpadP) { + + if (cmdline.ysizeSpec) { + if (cmdline.topSpec && cmdline.bottomSpec) { + if (cmdline.bottom + rows + cmdline.top < cmdline.ysize) { + pm_error("Top padding (%u), and bottom " + "padding (%u) are insufficient to bring the " + "image height of %d up to %u.", + cmdline.top, cmdline.bottom, rows, cmdline.ysize); + } else { + *tpadP = cmdline.top; + *bpadP = cmdline.bottom; + } + } else if (cmdline.topSpec) { + *tpadP = cmdline.top; + *bpadP = MAX(cmdline.ysize, cmdline.top + rows) - + (cmdline.top + rows); + } else if (cmdline.bottomSpec) { + *bpadP = cmdline.bottom; + *tpadP = MAX(cmdline.ysize, rows + cmdline.bottom) - + (rows + cmdline.bottom); + } else { + if (cmdline.ysize > rows) { + *bpadP = ROUNDU((cmdline.ysize - rows) * cmdline.yalign); + *tpadP = cmdline.ysize - rows - *bpadP; + } else { + *bpadP = 0; + *tpadP = 0; + } + } + } else { + *bpadP = cmdline.bottomSpec ? cmdline.bottom : 0; + *tpadP = cmdline.topSpec ? cmdline.top : 0; + } +} + + + +static void +computePadSizes(struct cmdlineInfo const cmdline, + int const cols, + int const rows, + unsigned int * const lpadP, + unsigned int * const rpadP, + unsigned int * const tpadP, + unsigned int * const bpadP) { + + computeHorizontalPadSizes(cmdline, cols, lpadP, rpadP); + + computeVerticalPadSizes(cmdline, rows, tpadP, bpadP); + + if (cmdline.verbose) + pm_message("Padding: left: %u; right: %u; top: %u; bottom: %u", + *lpadP, *rpadP, *tpadP, *bpadP); +} + + + +int +main(int argc, char ** argv) { + + struct cmdlineInfo cmdline; + FILE *ifP; + xel *xelrow, *bgrow, background; + xelval maxval; + int rows, cols, newcols, row, col, format; + bool depr_cmd; /* use deprecated commandline interface */ + unsigned int lpad, rpad, tpad, bpad; + + pnm_init( &argc, argv ); + + /* detect deprecated options */ + depr_cmd = FALSE; /* initial assumption */ + if (argc > 1 && argv[1][0] == '-') { + if (argv[1][1] == 't' || argv[1][1] == 'b' + || argv[1][1] == 'l' || argv[1][1] == 'r') { + if (argv[1][2] >= '0' && argv[1][2] <= '9') + depr_cmd = TRUE; + } + } + if (argc > 2 && argv[2][0] == '-') { + if (argv[2][1] == 't' || argv[2][1] == 'b' + || argv[2][1] == 'l' || argv[2][1] == 'r') { + if (argv[2][2] >= '0' && argv[2][2] <= '9') + depr_cmd = TRUE; + } + } + + if (depr_cmd) + parseCommandLineOld(argc, argv, &cmdline); + else + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.input_filespec); + + pnm_readpnminit(ifP, &cols, &rows, &maxval, &format); + if (cmdline.white) + background = pnm_whitexel(maxval, format); + else + background = pnm_blackxel(maxval, format); + + if (cmdline.verbose) pm_message("image WxH = %dx%d", cols, rows); + + computePadSizes(cmdline, cols, rows, &lpad, &rpad, &tpad, &bpad); + + newcols = cols + lpad + rpad; + xelrow = pnm_allocrow(newcols); + bgrow = pnm_allocrow(newcols); + + for (col = 0; col < newcols; col++) + xelrow[col] = bgrow[col] = background; + + pnm_writepnminit(stdout, newcols, rows + tpad + bpad, maxval, format, 0); + + for (row = 0; row < tpad; row++) + pnm_writepnmrow(stdout, bgrow, newcols, maxval, format, 0); + + for (row = 0; row < rows; row++) { + pnm_readpnmrow(ifP, &xelrow[lpad], cols, maxval, format); + pnm_writepnmrow(stdout, xelrow, newcols, maxval, format, 0); + } + + for (row = 0; row < bpad; row++) + pnm_writepnmrow(stdout, bgrow, newcols, maxval, format, 0); + + pnm_freerow(xelrow); + pnm_freerow(bgrow); + + pm_close(ifP); + + return 0; +} diff --git a/editor/pnmpaste.c b/editor/pnmpaste.c new file mode 100644 index 00000000..38b316c6 --- /dev/null +++ b/editor/pnmpaste.c @@ -0,0 +1,185 @@ +/* pnmpaste.c - paste a rectangle into a portable anymap +** +** Copyright (C) 1989 by Jef Poskanzer. +** +** 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. +*/ + +#include "pm_c_util.h" +#include "pnm.h" + +int +main( argc, argv ) + int argc; + char* argv[]; + { + FILE* ifp1; + FILE* ifp2; + register xel* xelrow1; + register xel* xelrow2; + register xel* x1P; + register xel* x2P; + xelval maxval1, maxval2, newmaxval; + int argn, rows1, cols1, format1, x, y; + int rows2, cols2, format2, newformat, row; + register int col; + char function; + const char* const usage = "[-replace|-or|-and|-xor] frompnmfile x y [intopnmfile]"; + + pnm_init( &argc, argv ); + + argn = 1; + function = 'r'; + + /* Check for flags. */ + if ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' ) + { + if ( pm_keymatch( argv[argn], "-replace", 2 ) ) + function = 'r'; + else if ( pm_keymatch( argv[argn], "-or", 2 ) ) + function = 'o'; + else if ( pm_keymatch( argv[argn], "-and", 2 ) ) + function = 'a'; + else if ( pm_keymatch( argv[argn], "-xor", 2 ) ) + function = 'x'; + else + pm_usage( usage ); + ++argn; + } + + if ( argn == argc ) + pm_usage( usage ); + ifp1 = pm_openr( argv[argn] ); + ++argn; + + if ( argn == argc ) + pm_usage( usage ); + if ( sscanf( argv[argn], "%d", &x ) != 1 ) + pm_usage( usage ); + ++argn; + if ( argn == argc ) + pm_usage( usage ); + if ( sscanf( argv[argn], "%d", &y ) != 1 ) + pm_usage( usage ); + ++argn; + + if ( argn != argc ) + { + ifp2 = pm_openr( argv[argn] ); + ++argn; + } + else + ifp2 = stdin; + + if ( argn != argc ) + pm_usage( usage ); + + pnm_readpnminit( ifp1, &cols1, &rows1, &maxval1, &format1 ); + xelrow1 = pnm_allocrow(cols1); + pnm_readpnminit( ifp2, &cols2, &rows2, &maxval2, &format2 ); + xelrow2 = pnm_allocrow(cols2); + + if ( x <= -cols2 ) + pm_error( + "x is too negative -- the second anymap has only %d cols", + cols2 ); + else if ( x >= cols2 ) + pm_error( + "x is too large -- the second anymap has only %d cols", + cols2 ); + if ( y <= -rows2 ) + pm_error( + "y is too negative -- the second anymap has only %d rows", + rows2 ); + else if ( y >= rows2 ) + pm_error( + "y is too large -- the second anymap has only %d rows", + rows2 ); + + if ( x < 0 ) + x += cols2; + if ( y < 0 ) + y += rows2; + + if ( x + cols1 > cols2 ) + pm_error( "x + width is too large by %d pixels", x + cols1 - cols2 ); + if ( y + rows1 > rows2 ) + pm_error( "y + height is too large by %d pixels", y + rows1 - rows2 ); + + newformat = MAX( PNM_FORMAT_TYPE(format1), PNM_FORMAT_TYPE(format2) ); + newmaxval = MAX( maxval1, maxval2 ); + + if ( function != 'r' && newformat != PBM_TYPE ) + pm_error( "no logical operations allowed for non-bitmaps" ); + + pnm_writepnminit( stdout, cols2, rows2, newmaxval, newformat, 0 ); + + for ( row = 0; row < rows2; ++row ) + { + pnm_readpnmrow( ifp2, xelrow2, cols2, maxval2, format2 ); + pnm_promoteformatrow( xelrow2, cols2, maxval2, format2, + newmaxval, newformat ); + + if ( row >= y && row < y + rows1 ) + { + pnm_readpnmrow( ifp1, xelrow1, cols1, maxval1, format1 ); + pnm_promoteformatrow( xelrow1, cols1, maxval1, format1, + newmaxval, newformat ); + for ( col = 0, x1P = xelrow1, x2P = &(xelrow2[x]); + col < cols1; ++col, ++x1P, ++x2P ) + { + register xelval b1, b2; + + switch ( function ) + { + case 'r': + *x2P = *x1P; + break; + + case 'o': + b1 = PNM_GET1( *x1P ); + b2 = PNM_GET1( *x2P ); + if ( b1 != 0 || b2 != 0 ) + PNM_ASSIGN1( *x2P, newmaxval ); + else + PNM_ASSIGN1( *x2P, 0 ); + break; + + case 'a': + b1 = PNM_GET1( *x1P ); + b2 = PNM_GET1( *x2P ); + if ( b1 != 0 && b2 != 0 ) + PNM_ASSIGN1( *x2P, newmaxval ); + else + PNM_ASSIGN1( *x2P, 0 ); + break; + + case 'x': + b1 = PNM_GET1( *x1P ); + b2 = PNM_GET1( *x2P ); + if ( ( b1 != 0 && b2 == 0 ) || ( b1 == 0 && b2 != 0 ) ) + PNM_ASSIGN1( *x2P, newmaxval ); + else + PNM_ASSIGN1( *x2P, 0 ); + break; + + default: + pm_error( "can't happen" ); + } + } + } + + pnm_writepnmrow( stdout, xelrow2, cols2, newmaxval, newformat, 0 ); + } + + pm_close( ifp1 ); + pm_close( ifp2 ); + pm_close( stdout ); + + exit( 0 ); + } diff --git a/editor/pnmquant b/editor/pnmquant new file mode 100755 index 00000000..175f6906 --- /dev/null +++ b/editor/pnmquant @@ -0,0 +1,275 @@ +#!/usr/bin/perl -w + +############################################################################## +# pnmquant +############################################################################## +# By Bryan Henderson, San Jose CA; December 2001. +# +# Contributed to the public domain by its author. +############################################################################## + +use strict; +use English; +use Getopt::Long; +#use File::Temp "tempfile"; # not available before Perl 5.6.1 +use File::Spec; +#use Fcntl ":seek"; # not available in Perl 5.00503 +use Fcntl; # gets open flags + +my ($TRUE, $FALSE) = (1,0); + +my ($SEEK_SET, $SEEK_CUR, $SEEK_END) = (0, 1, 2); + +sub tempFile($) { + +# Here's what we'd do if we could expect Perl 5.6.1 or later, instead +# of calling this subroutine: +# my ($file, $filename) = tempfile("pnmquant_XXXX", +# SUFFIX=>".pnm", +# DIR=>File::Spec->tmpdir()) +# UNLINK=>$TRUE); + my ($suffix) = @_; + my $fileName; + local *file; # For some inexplicable reason, must be local, not my + my $i; + $i = 0; + do { + $fileName = File::Spec->tmpdir() . "/pnmquant_" . $i++ . $suffix; + } until sysopen(*file, $fileName, O_RDWR|O_CREAT|O_EXCL); + + return(*file, $fileName); +} + + + +sub parseCommandLine(@) { + + local @ARGV = @_; # GetOptions takes input from @ARGV only + + my %cmdline; + + my $validOptions = GetOptions(\%cmdline, + "center", + "meancolor", + "meanpixel", + "spreadbrightness", + "spreadluminosity", + "floyd|fs!", + "quiet", + "plain"); + + if (!$validOptions) { + print(STDERR "Invalid option syntax.\n"); + exit(1); + } + if (@ARGV > 2) { + print(STDERR "This program takes at most 2 arguments. You specified ", + scalar(@ARGV), "\n"); + exit(1); + } + if (@ARGV < 1) { + print(STDERR + "You must specify the number of colors as an argument.\n"); + exit(1); + } + my $infile; + $cmdline{ncolors} = $ARGV[0]; + + if (!($cmdline{ncolors} =~ m{ ^[[:digit:]]+$ }x ) || + $cmdline{ncolors} == 0) { + print(STDERR + "Number of colors argument '$cmdline{ncolors}' " . + "is not a positive integer.\n"); + exit(1); + } + + if (@ARGV > 1) { + $cmdline{infile} = $ARGV[1]; + } else { + $cmdline{infile} = "-"; + } + + return(\%cmdline); +} + + + +sub setAutoflush($) { + my ($fh) = @_; + + # Better would be $fh->autoflush() (with use IO:Handle), but older Perl + # doesn't have it. + + my $oldFh = select($fh); + $OUTPUT_AUTOFLUSH = $TRUE; + select ($oldFh); +} + + + +sub openSeekableAsStdin($) { + my ($infile) = @_; + + # Open the input file $infile and connect it to Standard Input + # (assuming Standard Input is now open as the Perl file handle STDIN). + # If $infile is "-", that means just leave Standard Input alone. But if + # Standard Input is not seekable, copy it to a temporary regular + # file and return a file handle for that. I.e. the file handle we + # return is guaranteed to be seekable. + + # Note: this all works only because STDIN is already set up on file + # descriptor 0. Otherwise, open(STDIN, ...) would just create a brand + # new file handle named STDIN on some arbitrary file descriptor. + + if ($infile eq "-") { + my $seekWorked = sysseek(STDIN, 0, $SEEK_SET); + if ($seekWorked) { + # STDIN is already as we need it. + } else { + # It isn't seekable, so we must copy it to a temporary regular file + # and return that as the input file. + + my ($inFh, $inFilename) = tempFile(".pnm"); + if (!defined($inFh)) { + die("Unable to create temporary file. Errno=$ERRNO"); + } + unlink($inFilename) or + die("Unable to unlink temporary file. Errno=$ERRNO"); + + setAutoflush($inFh); + + while (<STDIN>) { + print($inFh $_); + } + sysseek($inFh, 0, $SEEK_SET) + or die("Seek of temporary input file failed! " . + "Errno = $ERRNO"); + *INFH = *$inFh; # Because open() rejects '<&$inFh' + open(STDIN, "<&INFH"); + tell(INFH); # Avoids bogus "INFH is not referenced" warning + } + } else { + open(STDIN, "<", $infile) + or die("Unable to open input file '$infile'. Errno=$ERRNO"); + } +} + + + +sub makeColormap($$$$$) { + + my ($ncolors, $opt_meanpixel, $opt_meancolor, $opt_spreadluminosity, + $opt_quiet) = @_; + + # Make a colormap of $ncolors colors from the image on Standard Input. + # Put it in a temporary file and return its name. + + my ($mapfileFh, $mapfileSpec) = tempFile(".pnm"); + + if (!defined($mapfileFh)) { + print(STDERR "Unable to create temporary file for colormap. " . + "errno = $ERRNO\n"); + exit(1); + } + + my $averageOpt; + if (defined($opt_meanpixel)) { + $averageOpt = "-meanpixel"; + } elsif (defined($opt_meancolor)) { + $averageOpt = "-meancolor"; + } else { + $averageOpt = "-center"; + } + + my $spreadOpt; + if (defined($opt_spreadluminosity)) { + $spreadOpt = "-spreadluminosity"; + } else { + $spreadOpt = "-spreadbrightness"; + } + + my @options; + @options = ($averageOpt, $spreadOpt); + if (defined($opt_quiet)) { + push(@options, '-quiet'); + } + + open(STDOUT, ">", $mapfileSpec); + + my $maprc = system("pnmcolormap", $ncolors, @options); + + if ($maprc != 0) { + print(STDERR "pnmcolormap failed, rc=$maprc\n"); + exit(1); + } + return $mapfileSpec; +} + + + +sub remap($$$$) { + + my ($mapfileSpec, $opt_floyd, $opt_plain, $opt_quiet) = @_; + + # Remap the image on Standard Input to Standard Output, using the colors + # from the colormap file named $mapfileSpec. + + my @options; + @options = (); # initial value + + if ($opt_floyd) { + push(@options, "-floyd"); + } + if ($opt_plain) { + push(@options, "-plain"); + } + if ($opt_quiet) { + push(@options, "-quiet"); + } + + my $remaprc = system("pnmremap", "-mapfile=$mapfileSpec", @options); + + if ($remaprc != 0) { + print(STDERR "pnmremap failed, rc=$remaprc\n"); + exit(1); + } +} + + + +############################################################################## +# MAIN PROGRAM +############################################################################## + +my $cmdlineR = parseCommandLine(@ARGV); + +openSeekableAsStdin($cmdlineR->{infile}); + +# Save Standard Output for our eventual output +open(OLDOUT, ">&STDOUT"); +select(OLDOUT); # avoids Perl bug where it says we never use STDOUT + + +my $mapfileSpec = makeColormap($cmdlineR->{ncolors}, + $cmdlineR->{meanpixel}, + $cmdlineR->{meancolor}, + $cmdlineR->{spreadluminosity}, + $cmdlineR->{quiet}); + +# Note that we use sysseek() instead of seek(), because we're positioning +# the file to be read by our non-Perl child process, rather than for reading +# through the Perl I/O layer. + +sysseek(STDIN, 0, $SEEK_SET) + or die("seek back to zero on input file failed."); + + +open(STDOUT, ">&OLDOUT"); + +remap($mapfileSpec, + $cmdlineR->{floyd}, + $cmdlineR->{plain}, + $cmdlineR->{quiet}); + +unlink($mapfileSpec) or + die("Unable to unlink map file. Errno=$ERRNO"); diff --git a/editor/pnmremap.c b/editor/pnmremap.c new file mode 100644 index 00000000..2102fe63 --- /dev/null +++ b/editor/pnmremap.c @@ -0,0 +1,872 @@ +/****************************************************************************** + pnmremap.c +******************************************************************************* + + Replace colors in an input image with colors from a given colormap image. + + For PGM input, do the equivalent. + + By Bryan Henderson, San Jose, CA 2001.12.17 + + Derived from ppmquant, originally by Jef Poskanzer. + + Copyright (C) 1989, 1991 by Jef Poskanzer. + Copyright (C) 2001 by Bryan Henderson. + + 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. +******************************************************************************/ + +#include <limits.h> +#include <math.h> +#include <assert.h> + +#include "pm_c_util.h" +#include "mallocvar.h" +#include "nstring.h" +#include "shhopt.h" +#include "pam.h" +#include "pammap.h" + +#define MAXCOLORS 32767u + +enum missingMethod { + MISSING_FIRST, + MISSING_SPECIFIED, + MISSING_CLOSE +}; + +#define FS_SCALE 1024 + +struct fserr { + long** thiserr; + long** nexterr; + bool fsForward; +}; + + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char * inputFilespec; /* Filespec of input file */ + const char * mapFilespec; /* Filespec of colormap file */ + unsigned int floyd; /* Boolean: -floyd/-fs option */ + enum missingMethod missingMethod; + char * missingcolor; + /* -missingcolor value. Null if not specified */ + unsigned int verbose; +}; + + + +static void +parseCommandLine (int argc, char ** argv, + struct cmdlineInfo *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 option_def_index; + + unsigned int nofloyd, firstisdefault; + unsigned int missingSpec, mapfileSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "floyd", OPT_FLAG, + NULL, &cmdlineP->floyd, 0); + OPTENT3(0, "fs", OPT_FLAG, + NULL, &cmdlineP->floyd, 0); + OPTENT3(0, "nofloyd", OPT_FLAG, + NULL, &nofloyd, 0); + OPTENT3(0, "nofs", OPT_FLAG, + NULL, &nofloyd, 0); + OPTENT3(0, "firstisdefault", OPT_FLAG, + NULL, &firstisdefault, 0); + OPTENT3(0, "mapfile", OPT_STRING, + &cmdlineP->mapFilespec, &mapfileSpec, 0); + OPTENT3(0, "missingcolor", OPT_STRING, + &cmdlineP->missingcolor, &missingSpec, 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 */ + + cmdlineP->missingcolor = NULL; /* default value */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0 ); + /* Uses and sets argc, argv, and some of *cmdline_p and others. */ + + if (cmdlineP->floyd && nofloyd) + pm_error("You cannot specify both -floyd and -nofloyd options."); + + if (firstisdefault && missingSpec) + pm_error("You cannot specify both -missing and -firstisdefault."); + + if (firstisdefault) + cmdlineP->missingMethod = MISSING_FIRST; + else if (missingSpec) + cmdlineP->missingMethod = MISSING_SPECIFIED; + else + cmdlineP->missingMethod = MISSING_CLOSE; + + if (!mapfileSpec) + pm_error("You must specify the -mapfile option."); + + if (argc-1 > 1) + pm_error("Program takes at most one argument: the input file " + "specification. " + "You specified %d arguments.", argc-1); + if (argc-1 < 1) + cmdlineP->inputFilespec = "-"; + else + cmdlineP->inputFilespec = argv[1]; +} + + + +static void +rgbToDepth1(const struct pam * const pamP, + tuple * const tupleRow) { + + unsigned int col; + + for (col = 0; col < pamP->width; ++col) { + unsigned int plane; + double grayvalue; + grayvalue = 0.0; /* initial value */ + for (plane = 0; plane < pamP->depth; ++plane) + grayvalue += pnm_lumin_factor[plane] * tupleRow[col][plane]; + tupleRow[col][0] = (sample) (grayvalue + 0.5); + } +} + + + +static void +grayscaleToDepth3(const struct pam * const pamP, + tuple * const tupleRow) { + + unsigned int col; + + assert(pamP->allocation_depth >= 3); + + for (col = 0; col < pamP->width; ++col) { + tupleRow[col][1] = tupleRow[col][0]; + tupleRow[col][2] = tupleRow[col][0]; + } +} + + + +static void +adjustDepth(const struct pam * const pamP, + tuple * const tupleRow, + unsigned int const newDepth) { +/*---------------------------------------------------------------------------- + Change the depth of the raster row tupleRow[] of the image + described by 'pamP' to newDepth. + + We don't change the memory allocation; tupleRow[] must already have + space allocated for at least 'newDepth' planes. When we're done, + all but the first 'newDepth' planes are meaningless, but the space is + still there. + + The only depth changes we know how to do are: + + - from tuple type RGB, depth 3 to depth 1 + + We change it to grayscale or black and white. + + - from tuple type GRAYSCALE or BLACKANDWHITE depth 1 to depth 3. + + We change it to RGB. + + For any other depth change request, we issue an error message and abort + the program. +-----------------------------------------------------------------------------*/ + if (newDepth != pamP->depth) { + + if (stripeq(pamP->tuple_type, "RGB")) { + if (newDepth != 1) { + pm_error("Map image depth of %u differs from input image " + "depth of %u, and the tuple type is RGB. " + "The only depth to which I know how to convert " + "an RGB tuple is 1.", + newDepth, pamP->depth); + } else + rgbToDepth1(pamP, tupleRow); + } else if (stripeq(pamP->tuple_type, "GRAYSCALE") || + stripeq(pamP->tuple_type, "BLACKANDWHITE")) { + if (newDepth != 3) { + pm_error("Map image depth of %u differs from input image " + "depth of %u, and the tuple type is GRAYSCALE " + "or BLACKANDWHITE. " + "The only depth to which I know how to convert " + "a GRAYSCALE or BLACKANDWHITE tuple is 3.", + newDepth, pamP->depth); + } else + grayscaleToDepth3(pamP, tupleRow); + } else { + pm_error("Map image depth of %u differs from input image depth " + "of %u, and the input image does not have a tuple type " + "that I know how to convert to the map depth. " + "I can convert RGB, GRAYSCALE, and BLACKANDWHITE. " + "The input image is '%.*s'.", + newDepth, pamP->depth, + (int)sizeof(pamP->tuple_type), pamP->tuple_type); + } + } +} + + + + +static void +computeColorMapFromMap(struct pam * const mappamP, + tuple ** const maptuples, + tupletable * const colormapP, + unsigned int * const newcolorsP) { +/*---------------------------------------------------------------------------- + Produce a colormap containing the colors that we will use in the output. + + Make it include exactly those colors that are in the image + described by *mappamP and maptuples[][]. + + Return the number of colors in the returned colormap as *newcolorsP. +-----------------------------------------------------------------------------*/ + unsigned int colors; + + if (mappamP->width == 0 || mappamP->height == 0) + pm_error("colormap file contains no pixels"); + + *colormapP = + pnm_computetuplefreqtable(mappamP, maptuples, MAXCOLORS, &colors); + if (*colormapP == NULL) + pm_error("too many colors in colormap!"); + pm_message("%d colors found in colormap", colors); + *newcolorsP = colors; +} + + + +static void +initFserr(struct pam * const pamP, + struct fserr * const fserrP) { +/*---------------------------------------------------------------------------- + Initialize the Floyd-Steinberg error vectors +-----------------------------------------------------------------------------*/ + unsigned int plane; + + unsigned int const fserrSize = pamP->width + 2; + + MALLOCARRAY(fserrP->thiserr, pamP->depth); + if (fserrP->thiserr == NULL) + pm_error("Out of memory allocating Floyd-Steinberg structures " + "for depth %u", pamP->depth); + MALLOCARRAY(fserrP->nexterr, pamP->depth); + if (fserrP->nexterr == NULL) + pm_error("Out of memory allocating Floyd-Steinberg structures " + "for depth %u", pamP->depth); + + for (plane = 0; plane < pamP->depth; ++plane) { + MALLOCARRAY(fserrP->thiserr[plane], fserrSize); + if (fserrP->thiserr[plane] == NULL) + pm_error("Out of memory allocating Floyd-Steinberg structures " + "for Plane %u, size %u", plane, fserrSize); + MALLOCARRAY(fserrP->nexterr[plane], fserrSize); + if (fserrP->nexterr[plane] == NULL) + pm_error("Out of memory allocating Floyd-Steinberg structures " + "for Plane %u, size %u", plane, fserrSize); + } + + srand((int)(time(0) ^ getpid())); + + { + int col; + + for (col = 0; col < fserrSize; ++col) { + unsigned int plane; + for (plane = 0; plane < pamP->depth; ++plane) + fserrP->thiserr[plane][col] = + rand() % (FS_SCALE * 2) - FS_SCALE; + /* (random errors in [-1 .. 1]) */ + } + } + fserrP->fsForward = TRUE; +} + + + +static void +floydInitRow(struct pam * const pamP, struct fserr * const fserrP) { + + int col; + + for (col = 0; col < pamP->width + 2; ++col) { + unsigned int plane; + for (plane = 0; plane < pamP->depth; ++plane) + fserrP->nexterr[plane][col] = 0; + } +} + + + +static void +floydAdjustColor(struct pam * const pamP, + tuple const tuple, + struct fserr * const fserrP, + int const col) { +/*---------------------------------------------------------------------------- + Use Floyd-Steinberg errors to adjust actual color. +-----------------------------------------------------------------------------*/ + unsigned int plane; + + for (plane = 0; plane < pamP->depth; ++plane) { + long int const s = + tuple[plane] + fserrP->thiserr[plane][col+1] / FS_SCALE; + tuple[plane] = MIN(pamP->maxval, MAX(0,s)); + } +} + + + +static void +floydPropagateErr(struct pam * const pamP, + struct fserr * const fserrP, + int const col, + tuple const oldtuple, + tuple const newtuple) { +/*---------------------------------------------------------------------------- + Propagate Floyd-Steinberg error terms. + + The error is due to substituting the tuple value 'newtuple' for the + tuple value 'oldtuple' (both described by *pamP). The error terms + are meant to be used to introduce a compensating error into the + future selection of tuples nearby in the image. +-----------------------------------------------------------------------------*/ + unsigned int plane; + for (plane = 0; plane < pamP->depth; ++plane) { + long const newSample = newtuple[plane]; + long const oldSample = oldtuple[plane]; + long const err = (oldSample - newSample) * FS_SCALE; + + if (fserrP->fsForward) { + fserrP->thiserr[plane][col + 2] += ( err * 7 ) / 16; + fserrP->nexterr[plane][col ] += ( err * 3 ) / 16; + fserrP->nexterr[plane][col + 1] += ( err * 5 ) / 16; + fserrP->nexterr[plane][col + 2] += ( err ) / 16; + } else { + fserrP->thiserr[plane][col ] += ( err * 7 ) / 16; + fserrP->nexterr[plane][col + 2] += ( err * 3 ) / 16; + fserrP->nexterr[plane][col + 1] += ( err * 5 ) / 16; + fserrP->nexterr[plane][col ] += ( err ) / 16; + } + } +} + + + +static void +floydSwitchDir(struct pam * const pamP, struct fserr * const fserrP) { + + unsigned int plane; + + for (plane = 0; plane < pamP->depth; ++plane) { + long * const temperr = fserrP->thiserr[plane]; + fserrP->thiserr[plane] = fserrP->nexterr[plane]; + fserrP->nexterr[plane] = temperr; + } + fserrP->fsForward = ! fserrP->fsForward; +} + + + +struct colormapFinder { +/*---------------------------------------------------------------------------- + This is an object that finds a color in a colormap. The methods + 'searchColormapClose' and 'searchColormapExact' belong to it. + + This object ought to encompass the hash table as well some day and + possibly encapsulate the color map altogether and just be an object + that opaquely maps input colors to output colors. +-----------------------------------------------------------------------------*/ + tupletable colormap; + unsigned int colors; + /* Number of colors in 'colormap'. At least 1 */ + unsigned int distanceDivider; + /* The value by which our intermediate distance calculations + have to be divided to make sure we don't overflow our + unsigned int data structure. + + To the extent 'distanceDivider' is greater than 1, closest + color results will be approximate -- there could + conceivably be a closer one that we miss. + */ +}; + + + +static void +createColormapFinder(struct pam * const pamP, + tupletable const colormap, + unsigned int const colors, + struct colormapFinder ** const colormapFinderPP) { + + struct colormapFinder * colormapFinderP; + + MALLOCVAR_NOFAIL(colormapFinderP); + + colormapFinderP->colormap = colormap; + colormapFinderP->colors = colors; + + { + unsigned int const maxHandleableSqrDiff = + (unsigned int)UINT_MAX / pamP->depth; + + if (SQR(pamP->maxval) > maxHandleableSqrDiff) + colormapFinderP->distanceDivider = (unsigned int) + (SQR(pamP->maxval) / maxHandleableSqrDiff + 0.1 + 1.0); + /* The 0.1 is a fudge factor to keep us out of rounding + trouble. The 1.0 effects a round-up. + */ + else + colormapFinderP->distanceDivider = 1; + } + *colormapFinderPP = colormapFinderP; +} + + + +static void +destroyColormapFinder(struct colormapFinder * const colormapFinderP) { + + free(colormapFinderP); +} + + + +static void +searchColormapClose(struct pam * const pamP, + tuple const tuple, + struct colormapFinder * const colorFinderP, + int * const colormapIndexP) { +/*---------------------------------------------------------------------------- + Search the colormap indicated by *colorFinderP for the color closest to + that of tuple 'tuple'. Return its index as *colormapIndexP. + + *pamP describes the tuple 'tuple' and *colorFinderP has to be + compatible with it (i.e. the tuples in the color map must also be + described by *pamP). + + We compute distance between colors simply as the cartesian distance + between them in the RGB space. An alternative would be to look at + the chromaticities and luminosities of the colors. In experiments + in 2003, we found that this was actually worse in many cases. One + might think that two colors are closer if they have similar hues + than when they are simply different brightnesses of the same hue. + Human subjects asked to compare two colors normally say so. But + when replacing the color of a pixel in an image, the luminosity is + much more important, because you need to retain the luminosity + relationship between adjacent pixels. If you replace a pixel with + one that has the same chromaticity as the original, but much + darker, it may stand out among its neighbors in a way the original + pixel did not. In fact, on an image with blurred edges, we saw + ugly effects at the edges when we substituted colors using a + chromaticity-first color closeness formula. +-----------------------------------------------------------------------------*/ + unsigned int i; + unsigned int dist; + /* The closest distance we've found so far between the value of + tuple 'tuple' and a tuple in the colormap. This is measured as + the square of the cartesian distance between the tuples, except + that it's divided by 'distanceDivider' to make sure it will fit + in an unsigned int. + */ + + dist = UINT_MAX; /* initial value */ + + assert(colorFinderP->colors > 0); + + for (i = 0; i < colorFinderP->colors; ++i) { + unsigned int newdist; /* candidate for new 'dist' value */ + unsigned int plane; + + newdist = 0; + + for (plane=0; plane < pamP->depth; ++plane) { + newdist += + SQR(tuple[plane] - colorFinderP->colormap[i]->tuple[plane]) + / colorFinderP->distanceDivider; + } + if (newdist < dist) { + *colormapIndexP = i; + dist = newdist; + } + } +} + + + +static void +searchColormapExact(struct pam * const pamP, + struct colormapFinder * const colorFinderP, + tuple const tuple, + int * const colormapIndexP, + bool * const foundP) { +/*---------------------------------------------------------------------------- + Search the colormap indicated by *colorFinderP for the color of + tuple 'tuple'. If it's in the map, return its index as + *colormapIndexP and return *foundP == TRUE. Otherwise, return + *foundP = FALSE. + + *pamP describes the tuple 'tuple' and *colorFinderP has to be + compatible with it (i.e. the tuples in the color map must also be + described by *pamP). +-----------------------------------------------------------------------------*/ + unsigned int i; + bool found; + + found = FALSE; /* initial value */ + for (i = 0; i < colorFinderP->colors && !found; ++i) { + unsigned int plane; + found = TRUE; /* initial assumption */ + for (plane=0; plane < pamP->depth; ++plane) + if (tuple[plane] != colorFinderP->colormap[i]->tuple[plane]) + found = FALSE; + if (found) + *colormapIndexP = i; + } + *foundP = found; +} + + + +static void +lookupThroughHash(struct pam * const pamP, + tuple const tuple, + bool const needExactMatch, + struct colormapFinder * const colorFinderP, + tuplehash const colorhash, + int * const colormapIndexP, + bool * const usehashP) { +/*---------------------------------------------------------------------------- + Look up the color of tuple 'tuple' in the color map indicated by + 'colorFinderP' and, if it's in there, return its index as + *colormapIndexP. If not, return *colormapIndexP == -1. + + Both the tuple 'tuple' and the colors in color map 'colormap' are + described by *pamP. + + If 'needExactMatch' isn't true, we find the closest color in the color map, + and never return *colormapIndex == -1. + + lookaside at the hash table 'colorhash' to possibly avoid the cost of + a full lookup. If we do a full lookup, we add the result to 'colorhash' + unless *usehashP is false, and if that makes 'colorhash' full, we set + *usehashP false. +-----------------------------------------------------------------------------*/ + int found; + + /* Check hash table to see if we have already matched this color. */ + pnm_lookuptuple(pamP, colorhash, tuple, &found, colormapIndexP); + if (!found) { + /* No, have to do a full lookup */ + if (needExactMatch) { + bool found; + + searchColormapExact(pamP, colorFinderP, tuple, + colormapIndexP, &found); + if (!found) + *colormapIndexP = -1; + } else + searchColormapClose(pamP, tuple, colorFinderP, colormapIndexP); + if (*usehashP) { + bool fits; + pnm_addtotuplehash(pamP, colorhash, tuple, *colormapIndexP, + &fits); + if (!fits) { + pm_message("out of memory adding to hash table; " + "proceeding without it"); + *usehashP = FALSE; + } + } + } +} + + + +static void +convertRow(struct pam * const pamP, + tuple tuplerow[], + tupletable const colormap, + struct colormapFinder * const colorFinderP, + tuplehash const colorhash, + bool * const usehashP, + bool const floyd, + enum missingMethod const missingMethod, + tuple const defaultColor, + struct fserr * const fserrP, + unsigned int * const missingCountP) { +/*---------------------------------------------------------------------------- + Replace the colors in row tuplerow[] (described by *pamP) with the + new colors. + + Use and update the Floyd-Steinberg state *fserrP. + + Return the number of pixels that were not matched in the color map as + *missingCountP. + + *colorFinderP is a color finder based on 'colormap' -- it tells us what + index of 'colormap' corresponds to a certain color. +-----------------------------------------------------------------------------*/ + int col; + int limitcol; + /* The column at which to stop processing the row. If we're scanning + forwards, this is the rightmost column. If we're scanning + backward, this is the leftmost column. + */ + + if (floyd) + floydInitRow(pamP, fserrP); + + *missingCountP = 0; /* initial value */ + + if ((!floyd) || fserrP->fsForward) { + col = 0; + limitcol = pamP->width; + } else { + col = pamP->width - 1; + limitcol = -1; + } + do { + int colormapIndex; + /* Index into the colormap of the replacement color, or -1 if + there is no usable color in the color map. + */ + + if (floyd) + floydAdjustColor(pamP, tuplerow[col], fserrP, col); + + lookupThroughHash(pamP, tuplerow[col], + missingMethod != MISSING_CLOSE, colorFinderP, + colorhash, &colormapIndex, usehashP); + if (floyd) + floydPropagateErr(pamP, fserrP, col, tuplerow[col], + colormap[colormapIndex]->tuple); + + if (colormapIndex == -1) { + ++*missingCountP; + switch (missingMethod) { + case MISSING_SPECIFIED: + pnm_assigntuple(pamP, tuplerow[col], defaultColor); + break; + case MISSING_FIRST: + pnm_assigntuple(pamP, tuplerow[col], colormap[0]->tuple); + break; + default: + pm_error("Internal error: invalid value of missingMethod"); + } + } else + pnm_assigntuple(pamP, tuplerow[col], + colormap[colormapIndex]->tuple); + + if (floyd && !fserrP->fsForward) + --col; + else + ++col; + } while (col != limitcol); + + if (floyd) + floydSwitchDir(pamP, fserrP); +} + + + +static void +copyRaster(struct pam * const inpamP, + struct pam * const outpamP, + tupletable const colormap, + unsigned int const colormapSize, + bool const floyd, + enum missingMethod const missingMethod, + tuple const defaultColor, + unsigned int * const missingCountP) { + + tuplehash const colorhash = pnm_createtuplehash(); + struct colormapFinder * colorFinderP; + bool usehash; + struct fserr fserr; + tuple * tuplerow = pnm_allocpamrow(inpamP); + int row; + + if (outpamP->maxval != inpamP->maxval && missingMethod != MISSING_CLOSE) + pm_error("The maxval of the colormap (%u) is not equal to the " + "maxval of the input image (%u). This is allowable only " + "if you are doing an approximate mapping (i.e. you don't " + "specify -firstisdefault or -missingcolor)", + (unsigned int)outpamP->maxval, (unsigned int)inpamP->maxval); + + usehash = TRUE; + + createColormapFinder(outpamP, colormap, colormapSize, &colorFinderP); + + if (floyd) + initFserr(inpamP, &fserr); + + *missingCountP = 0; /* initial value */ + + for (row = 0; row < inpamP->height; ++row) { + unsigned int missingCount; + + pnm_readpamrow(inpamP, tuplerow); + + /* The following modify tuplerow, to make it consistent with + *outpamP instead of *inpamP. + */ + adjustDepth(inpamP, tuplerow, outpamP->depth); + pnm_scaletuplerow(inpamP, tuplerow, tuplerow, outpamP->maxval); + + /* The following both consults and adds to 'colorhash' and + its associated 'usehash'. It modifies 'tuplerow' too. + */ + convertRow(outpamP, tuplerow, colormap, colorFinderP, colorhash, + &usehash, + floyd, missingMethod, defaultColor, &fserr, &missingCount); + + *missingCountP += missingCount; + + pnm_writepamrow(outpamP, tuplerow); + } + destroyColormapFinder(colorFinderP); + pnm_freepamrow(tuplerow); + pnm_destroytuplehash(colorhash); +} + + + + +static void +remap(FILE * const ifP, + const struct pam * const outpamCommonP, + tupletable const colormap, + unsigned int const colormapSize, + bool const floyd, + enum missingMethod const missingMethod, + tuple const defaultColor, + bool const verbose) { + + bool eof; + eof = FALSE; + while (!eof) { + struct pam inpam, outpam; + unsigned int missingCount; + /* Number of pixels that were not matched in the color map (where + missingMethod is MISSING_CLOSE, this is always zero). + */ + + pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(allocation_depth)); + + outpam = *outpamCommonP; + outpam.width = inpam.width; + outpam.height = inpam.height; + + pnm_writepaminit(&outpam); + + /* Set up so input buffers have extra space as needed to + convert the input to the output depth. + */ + pnm_setminallocationdepth(&inpam, outpam.depth); + + copyRaster(&inpam, &outpam, colormap, colormapSize, floyd, + missingMethod, defaultColor, &missingCount); + + if (verbose) + pm_message("%d pixels not matched in color map", missingCount); + + pnm_nextimage(ifP, &eof); + } +} + + + + +int +main(int argc, char * argv[] ) { + + struct cmdlineInfo cmdline; + FILE * ifP; + struct pam outpamCommon; + /* Describes the output images. Width and height fields are + not meaningful, because different output images might have + different dimensions. The rest of the information is common + across all output images. + */ + tupletable colormap; + unsigned int colormapSize; + tuple defaultColor; + /* A tuple of the color that should replace any input color that is + not in the colormap, if we're doing MISSING_SPECIFIED. + */ + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilespec); + { + FILE * mapfile; + struct pam mappam; + tuple ** maptuples; + + mapfile = pm_openr(cmdline.mapFilespec); + maptuples = pnm_readpam(mapfile, &mappam, PAM_STRUCT_SIZE(tuple_type)); + pm_close(mapfile); + + computeColorMapFromMap(&mappam, maptuples, &colormap, &colormapSize); + pnm_freepamarray(maptuples, &mappam); + + outpamCommon = mappam; + outpamCommon.file = stdout; + } + + defaultColor = pnm_allocpamtuple(&outpamCommon); + if (cmdline.missingcolor && outpamCommon.depth == 3) { + pixel const color = + ppm_parsecolor(cmdline.missingcolor, outpamCommon.maxval); + defaultColor[PAM_RED_PLANE] = PPM_GETR(color); + defaultColor[PAM_GRN_PLANE] = PPM_GETG(color); + defaultColor[PAM_BLU_PLANE] = PPM_GETB(color); + } + + remap(ifP, &outpamCommon, colormap, colormapSize, + cmdline.floyd, cmdline.missingMethod, defaultColor, + cmdline.verbose); + + pnm_freepamtuple(defaultColor); + + pm_close(stdout); + + pm_close(ifP); + + return 0; +} diff --git a/editor/pnmrotate.c b/editor/pnmrotate.c new file mode 100644 index 00000000..64c69e2a --- /dev/null +++ b/editor/pnmrotate.c @@ -0,0 +1,808 @@ +/* pnmrotate.c - read a portable anymap and rotate it by some angle +** +** Copyright (C) 1989, 1991 by Jef Poskanzer. +** +** 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 _XOPEN_SOURCE /* get M_PI in math.h */ + +#include <math.h> +#include <assert.h> + +#include "pnm.h" +#include "shhopt.h" +#include "mallocvar.h" + +#define SCALE 4096 +#define HALFSCALE 2048 + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char * inputFilespec; /* Filespecs of input file */ + float angle; /* Angle to rotate, in radians */ + unsigned int noantialias; + const char * background; /* NULL if none */ + unsigned int keeptemp; /* For debugging */ + unsigned int verbose; +}; + + +enum rotationDirection {CLOCKWISE, COUNTERCLOCKWISE}; + +struct shearParm { + /* These numbers tell how to shear a pixel, but I haven't figured out + yet exactly what each means. + */ + long fracnew0; + long omfracnew0; + unsigned int shiftWhole; + unsigned int shiftUnits; +}; + + + +static void +parseCommandLine(int argc, 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 = malloc(100*sizeof(optEntry)); + /* Instructions to OptParseOptions3 on how to parse our options. + */ + optStruct3 opt; + + unsigned int backgroundSpec; + unsigned int option_def_index; + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENT3(0, "background", OPT_STRING, &cmdlineP->background, + &backgroundSpec, 0); + OPTENT3(0, "noantialias", OPT_FLAG, NULL, + &cmdlineP->noantialias, 0); + OPTENT3(0, "keeptemp", OPT_FLAG, NULL, + &cmdlineP->keeptemp, 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 = TRUE; /* We may have parms that are negative numbers */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (!backgroundSpec) + cmdlineP->background = NULL; + + if (argc-1 < 1) + pm_error("You must specify at least one argument: the angle " + "to rotate."); + else { + int rc; + float angleArg; + + rc = sscanf(argv[1], "%f", &angleArg); + + if (rc != 1) + pm_error("Invalid angle argument: '%s'. Must be a floating point " + "number of degrees.", argv[1]); + else if (angleArg < -90.0 || angleArg > 90.0) + pm_error("angle must be between -90 and 90, inclusive. " + "You specified %f. " + "Use 'pamflip' for other rotations.", angleArg); + else { + /* Convert to radians */ + cmdlineP->angle = angleArg * M_PI / 180.0; + + if (argc-1 < 2) + cmdlineP->inputFilespec = "-"; + else { + cmdlineP->inputFilespec = argv[2]; + + if (argc-1 > 2) + pm_error("Program takes at most two arguments " + "(angle and filename). You specified %d", + argc-1); + } + } + } +} + + + +static void +storeImage(const char * const fileName, + xel ** const xels, + unsigned int const cols, + unsigned int const rows, + xelval const maxval, + int const format) { + + FILE * ofP; + + ofP = pm_openw(fileName); + + pnm_writepnm(ofP, xels, cols, rows, maxval, format, 0); + + pm_close(ofP); +} + + + +static void +computeNewFormat(bool const antialias, + int const format, + xelval const maxval, + int * const newformatP, + xelval * const newmaxvalP) { + + if (antialias && PNM_FORMAT_TYPE(format) == PBM_TYPE) { + *newformatP = PGM_TYPE; + *newmaxvalP = PGM_MAXMAXVAL; + pm_message("promoting from PBM to PGM - " + "use -noantialias to avoid this"); + } else { + *newformatP = format; + *newmaxvalP = maxval; + } +} + + + +static xel +backgroundColor(const char * const backgroundColorName, + xel * const topRow, + int const cols, + xelval const maxval, + int const format) { + + xel retval; + + if (backgroundColorName) { + retval = ppm_parsecolor(backgroundColorName, maxval); + + switch(PNM_FORMAT_TYPE(format)) { + case PGM_TYPE: + if (!PPM_ISGRAY(retval)) + pm_error("Image is PGM (grayscale), " + "but you specified a non-gray " + "background color '%s'", backgroundColorName); + + break; + case PBM_TYPE: + if (!PNM_EQUAL(retval, pnm_whitexel(maxval, format)) && + !PNM_EQUAL(retval, pnm_blackxel(maxval, format))) + pm_error("Image is PBM (black and white), " + "but you specified '%s', which is neither black " + "nor white, as background color", + backgroundColorName); + break; + } + } else + retval = pnm_backgroundxelrow(topRow, cols, maxval, format); + + return retval; +} + + + +static void +reportBackground(xel const bgColor) { + + pm_message("Background color %u/%u/%u", + PPM_GETR(bgColor), PPM_GETG(bgColor), PPM_GETB(bgColor)); +} + + + +static void +shearX(xel * const inRow, + xel * const outRow, + int const cols, + int const format, + xel const bgxel, + bool const antialias, + float const shiftAmount, + int const newcols) { +/*---------------------------------------------------------------------------- + Shift a the row inRow[] right by 'shiftAmount' pixels and return the + result as outRow[]. + + The input row is 'cols' columns wide, whereas the output row is + 'newcols'. + + The format of the input row is 'format'. + + We shift the row on a background of color 'bgxel'. + + The output row has the same format and maxval as the input. + + 'shiftAmount' may not be negative. + + 'shiftAmount' can be fractional, so we either just go by the + nearest integer value or mix pixels to achieve the shift, depending + on 'antialias'. +-----------------------------------------------------------------------------*/ + assert(shiftAmount >= 0.0); + + if (antialias) { + unsigned int const shiftWhole = (unsigned int) shiftAmount; + long const fracShift = (shiftAmount - shiftWhole) * SCALE; + long const omfracShift = SCALE - fracShift; + + unsigned int col; + xel * nxP; + xel prevxel; + + for (col = 0; col < newcols; ++col) + outRow[col] = bgxel; + + prevxel = bgxel; + for (col = 0, nxP = &(outRow[shiftWhole]); + col < cols; ++col, ++nxP) { + + xel const p = inRow[col]; + + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + PPM_ASSIGN(*nxP, + (fracShift * PPM_GETR(prevxel) + + omfracShift * PPM_GETR(p) + + HALFSCALE) / SCALE, + (fracShift * PPM_GETG(prevxel) + + omfracShift * PPM_GETG(p) + + HALFSCALE) / SCALE, + (fracShift * PPM_GETB(prevxel) + + omfracShift * PPM_GETB(p) + + HALFSCALE) / SCALE ); + break; + + default: + PNM_ASSIGN1(*nxP, + (fracShift * PNM_GET1(prevxel) + + omfracShift * PNM_GET1(p) + + HALFSCALE) / SCALE ); + break; + } + prevxel = p; + } + if (fracShift> 0 && shiftWhole + cols < newcols) { + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + PPM_ASSIGN(*nxP, + (fracShift * PPM_GETR(prevxel) + + omfracShift * PPM_GETR(bgxel) + + HALFSCALE) / SCALE, + (fracShift * PPM_GETG(prevxel) + + omfracShift * PPM_GETG(bgxel) + + HALFSCALE) / SCALE, + (fracShift * PPM_GETB(prevxel) + + omfracShift * PPM_GETB(bgxel) + + HALFSCALE) / SCALE ); + break; + + default: + PNM_ASSIGN1(*nxP, + (fracShift * PNM_GET1(prevxel) + + omfracShift * PNM_GET1(bgxel) + + HALFSCALE) / SCALE ); + break; + } + } + } else { + unsigned int const shiftCols = (unsigned int) (shiftAmount + 0.5); + unsigned int col; + unsigned int outcol; + + outcol = 0; /* initial value */ + + for (col = 0; col < shiftCols; ++col) + outRow[outcol++] = bgxel; + for (col = 0; col < cols; ++col) + outRow[outcol++] = inRow[col]; + for (col = shiftCols + cols; col < newcols; ++col) + outRow[outcol++] = bgxel; + + assert(outcol == newcols); + } +} + + + +static void +shearXFromInputFile(FILE * const ifP, + unsigned int const cols, + unsigned int const rows, + xelval const maxval, + int const format, + enum rotationDirection const direction, + float const xshearfac, + xelval const newmaxval, + int const newformat, + bool const antialias, + const char * const background, + xel *** const shearedXelsP, + unsigned int * const newcolsP, + xel * const bgColorP) { +/*---------------------------------------------------------------------------- + Shear X from input file into newly malloced xel array. Return that + array as *shearedColsP, and its width as *tempColsP. Everything else + about the sheared image is the same as for the input image. + + The input image on file 'ifP' is described by 'cols', 'rows', + 'maxval', and 'format'. + + Along the way, figure out what the background color of the output should + be based on the contents of the file and the user's directive + 'background' and return that as *bgColorP. +-----------------------------------------------------------------------------*/ + unsigned int const maxShear = (rows - 0.5) * xshearfac + 0.5; + unsigned int const newcols = cols + maxShear; + + xel ** shearedXels; + xel * xelrow; + xel bgColor; + unsigned int row; + + shearedXels = pnm_allocarray(newcols, rows); + + xelrow = pnm_allocrow(cols); + + for (row = 0; row < rows; ++row) { + /* The shear factor is designed to shear over the entire width + from the left edge of of the left pixel to the right edge of + the right pixel. We use the distance of the center of this + pixel from the relevant edge to compute shift amount: + */ + float const xDistance = + (direction == COUNTERCLOCKWISE ? row + 0.5 : (rows-0.5 - row)); + float const shiftAmount = xshearfac * xDistance; + + pnm_readpnmrow(ifP, xelrow, cols, maxval, format); + + pnm_promoteformatrow(xelrow, cols, maxval, format, + newmaxval, newformat); + + if (row == 0) + bgColor = + backgroundColor(background, xelrow, cols, newmaxval, format); + + shearX(xelrow, shearedXels[row], cols, newformat, bgColor, + antialias, shiftAmount, newcols); + } + pnm_freerow(xelrow); + + *shearedXelsP = shearedXels; + *newcolsP = newcols; + + assert(rows >= 1); /* Ergo, bgColor is defined */ + *bgColorP = bgColor; +} + + + +static void +shearYNoAntialias(xel ** const inxels, + xel ** const outxels, + int const cols, + int const inrows, + int const outrows, + int const format, + xel const bgColor, + struct shearParm const shearParm[]) { +/*---------------------------------------------------------------------------- + Shear the image in 'inxels' ('cols' x 'inrows') vertically into + 'outxels' ('cols' x 'outrows'), both format 'format'. shearParm[X] + tells how much to shear pixels in Column X (clipped to Rows 0 + through 'outrow' -1) and 'bgColor' is what to use for background + where there is none of the input in the output. + + We do not do any antialiasing. We simply move whole pixels. + + We go row by row instead of column by column to save real memory. Going + row by row, the working set is only a few pages, whereas going column by + column, it would be one page per output row plus one page per input row. +-----------------------------------------------------------------------------*/ + unsigned int inrow; + unsigned int outrow; + + /* Fill the output with background */ + for (outrow = 0; outrow < outrows; ++outrow) { + unsigned int col; + for (col = 0; col < cols; ++col) + outxels[outrow][col] = bgColor; + } + + /* Overlay that background with sheared image */ + for (inrow = 0; inrow < inrows; ++inrow) { + unsigned int col; + for (col = 0; col < cols; ++col) { + int const outrow = inrow + shearParm[col].shiftUnits; + if (outrow >= 0 && outrow < outrows) + outxels[outrow][col] = inxels[inrow][col]; + } + } +} + + + +static void +shearYColAntialias(xel ** const inxels, + xel ** const outxels, + int const col, + int const inrows, + int const outrows, + int const format, + xel const bgxel, + struct shearParm shearParm[]) { +/*----------------------------------------------------------------------------- + Shear a column vertically. +-----------------------------------------------------------------------------*/ + long const fracnew0 = shearParm[col].fracnew0; + long const omfracnew0 = shearParm[col].omfracnew0; + int const shiftWhole = shearParm[col].shiftWhole; + + int outrow; + + xel prevxel; + int inrow; + + /* Initialize everything to background color */ + for (outrow = 0; outrow < outrows; ++outrow) + outxels[outrow][col] = bgxel; + + prevxel = bgxel; + for (inrow = 0; inrow < inrows; ++inrow) { + int const outrow = inrow + shiftWhole; + + if (outrow >= 0 && outrow < outrows) { + xel * const nxP = &(outxels[outrow][col]); + xel const x = inxels[inrow][col]; + switch ( PNM_FORMAT_TYPE(format) ) { + case PPM_TYPE: + PPM_ASSIGN(*nxP, + (fracnew0 * PPM_GETR(prevxel) + + omfracnew0 * PPM_GETR(x) + + HALFSCALE) / SCALE, + (fracnew0 * PPM_GETG(prevxel) + + omfracnew0 * PPM_GETG(x) + + HALFSCALE) / SCALE, + (fracnew0 * PPM_GETB(prevxel) + + omfracnew0 * PPM_GETB(x) + + HALFSCALE) / SCALE ); + break; + + default: + PNM_ASSIGN1(*nxP, + (fracnew0 * PNM_GET1(prevxel) + + omfracnew0 * PNM_GET1(x) + + HALFSCALE) / SCALE ); + break; + } + prevxel = x; + } + } + if (fracnew0 > 0 && shiftWhole + inrows < outrows) { + xel * const nxP = &(outxels[shiftWhole + inrows][col]); + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + PPM_ASSIGN(*nxP, + (fracnew0 * PPM_GETR(prevxel) + + omfracnew0 * PPM_GETR(bgxel) + + HALFSCALE) / SCALE, + (fracnew0 * PPM_GETG(prevxel) + + omfracnew0 * PPM_GETG(bgxel) + + HALFSCALE) / SCALE, + (fracnew0 * PPM_GETB(prevxel) + + omfracnew0 * PPM_GETB(bgxel) + + HALFSCALE) / SCALE); + break; + + default: + PNM_ASSIGN1(*nxP, + (fracnew0 * PNM_GET1(prevxel) + + omfracnew0 * PNM_GET1(bgxel) + + HALFSCALE) / SCALE); + break; + } + } +} + + + +static void +shearImageY(xel ** const inxels, + int const cols, + int const inrows, + int const format, + xel const bgxel, + bool const antialias, + enum rotationDirection const direction, + float const yshearfac, + int const yshearjunk, + xel *** const outxelsP, + unsigned int * const outrowsP) { + + unsigned int const maxShear = (cols - 0.5) * yshearfac + 0.5; + unsigned int const outrows = inrows + maxShear - 2 * yshearjunk; + + struct shearParm * shearParm; /* malloc'ed */ + int col; + xel ** outxels; + + outxels = pnm_allocarray(cols, outrows); + + MALLOCARRAY(shearParm, cols); + if (shearParm == NULL) + pm_error("Unable to allocate memory for shearParm"); + + for (col = 0; col < cols; ++col) { + /* The shear factor is designed to shear over the entire height + from the top edge of of the top pixel to the bottom edge of + the bottom pixel. We use the distance of the center of this + pixel from the relevant edge to compute shift amount: + */ + float const yDistance = + (direction == CLOCKWISE ? col + 0.5 : (cols-0.5 - col)); + float const shiftAmount = yshearfac * yDistance; + + shearParm[col].fracnew0 = (shiftAmount - (int)shiftAmount) * SCALE; + shearParm[col].omfracnew0 = SCALE - shearParm[col].fracnew0; + shearParm[col].shiftWhole = (int)shiftAmount - yshearjunk; + shearParm[col].shiftUnits = (int)(shiftAmount + 0.5) - yshearjunk; + } + if (!antialias) + shearYNoAntialias(inxels, outxels, cols, inrows, outrows, format, + bgxel, shearParm); + else { + /* TODO: do this row-by-row, same as for noantialias, to save + real memory. + */ + for (col = 0; col < cols; ++col) + shearYColAntialias(inxels, outxels, col, inrows, outrows, format, + bgxel, shearParm); + } + free(shearParm); + + *outxelsP = outxels; + *outrowsP = outrows; +} + + + +static void +shearFinal(xel * const inRow, + xel * const outRow, + int const incols, + int const outcols, + int const format, + xel const bgxel, + bool const antialias, + float const shiftAmount, + int const x2shearjunk) { + + + assert(shiftAmount >= 0.0); + + { + unsigned int col; + for (col = 0; col < outcols; ++col) + outRow[col] = bgxel; + } + + if (antialias) { + long const fracnew0 = (shiftAmount - (int) shiftAmount) * SCALE; + long const omfracnew0 = SCALE - fracnew0; + unsigned int const shiftWhole = (int)shiftAmount - x2shearjunk; + + xel prevxel; + unsigned int col; + + prevxel = bgxel; + for (col = 0; col < incols; ++col) { + int const new = shiftWhole + col; + if (new >= 0 && new < outcols) { + xel * const nxP = &(outRow[new]); + xel const x = inRow[col]; + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + PPM_ASSIGN(*nxP, + (fracnew0 * PPM_GETR(prevxel) + + omfracnew0 * PPM_GETR(x) + + HALFSCALE) / SCALE, + (fracnew0 * PPM_GETG(prevxel) + + omfracnew0 * PPM_GETG(x) + + HALFSCALE) / SCALE, + (fracnew0 * PPM_GETB(prevxel) + + omfracnew0 * PPM_GETB(x) + + HALFSCALE) / SCALE); + break; + + default: + PNM_ASSIGN1(*nxP, + (fracnew0 * PNM_GET1(prevxel) + + omfracnew0 * PNM_GET1(x) + + HALFSCALE) / SCALE ); + break; + } + prevxel = x; + } + } + if (fracnew0 > 0 && shiftWhole + incols < outcols) { + xel * const nxP = &(outRow[shiftWhole + incols]); + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + PPM_ASSIGN(*nxP, + (fracnew0 * PPM_GETR(prevxel) + + omfracnew0 * PPM_GETR(bgxel) + + HALFSCALE) / SCALE, + (fracnew0 * PPM_GETG(prevxel) + + omfracnew0 * PPM_GETG(bgxel) + + HALFSCALE) / SCALE, + (fracnew0 * PPM_GETB(prevxel) + + omfracnew0 * PPM_GETB(bgxel) + + HALFSCALE) / SCALE); + break; + + default: + PNM_ASSIGN1(*nxP, + (fracnew0 * PNM_GET1(prevxel) + + omfracnew0 * PNM_GET1(bgxel) + + HALFSCALE) / SCALE ); + break; + } + } + } else { + unsigned int const shiftCols = + (unsigned int)(shiftAmount + 0.5) - x2shearjunk; + + unsigned int col; + for (col = 0; col < incols; ++col) { + unsigned int const outcol = shiftCols + col; + if (outcol >= 0 && outcol < outcols) + outRow[outcol] = inRow[col]; + } + } +} + + + +static void +shearXToOutputFile(FILE * const ofP, + xel ** const xels, + unsigned int const cols, + unsigned int const rows, + xelval const maxval, + int const format, + enum rotationDirection const direction, + float const xshearfac, + int const x2shearjunk, + xel const bgColor, + bool const antialias) { +/*---------------------------------------------------------------------------- + Shear horizontally the image in 'xels' and write the result to file + 'ofP'. 'cols', 'rows', 'maxval', and 'format' describe the image in + 'xels'. They also describe the output image, except that it will be + wider as dictated by the shearing parameters. + + Shear over background color 'bgColor'. + + Do a smooth pixel-mixing shear iff 'antialias' is true. +-----------------------------------------------------------------------------*/ + unsigned int const maxShear = (rows - 0.5) * xshearfac + 0.5; + unsigned int const newcols = cols + maxShear - 2 * x2shearjunk; + + unsigned int row; + xel * xelrow; + + pnm_writepnminit(stdout, newcols, rows, maxval, format, 0); + + xelrow = pnm_allocrow(newcols); + + for (row = 0; row < rows; ++row) { + /* The shear factor is designed to shear over the entire width + from the left edge of of the left pixel to the right edge of + the right pixel. We use the distance of the center of this + pixel from the relevant edge to compute shift amount: + */ + float const xDistance = + (direction == COUNTERCLOCKWISE ? row + 0.5 : (rows-0.5 - row)); + float const shiftAmount = xshearfac * xDistance; + + shearFinal(xels[row], xelrow, cols, newcols, format, + bgColor, antialias, shiftAmount, x2shearjunk); + + pnm_writepnmrow(stdout, xelrow, newcols, maxval, format, 0); + } + pnm_freerow(xelrow); +} + + + +int +main(int argc, char *argv[]) { + + struct cmdlineInfo cmdline; + FILE * ifP; + xel ** shear1xels; + xel ** shear2xels; + xel bgColor; + int rows, cols, format; + int newformat; + unsigned int newrows; + int newRowsWithJunk; + unsigned int shear1Cols; + int yshearjunk, x2shearjunk; + xelval maxval, newmaxval; + float xshearfac, yshearfac; + enum rotationDirection direction; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilespec); + + pnm_readpnminit(ifP, &cols, &rows, &maxval, &format); + + computeNewFormat(!cmdline.noantialias, format, maxval, + &newformat, &newmaxval); + + xshearfac = fabs(tan(cmdline.angle / 2.0)); + yshearfac = fabs(sin(cmdline.angle)); + direction = cmdline.angle > 0 ? COUNTERCLOCKWISE : CLOCKWISE; + + /* The algorithm we use, for maximum speed, is 3 simple shears: + A horizontal, a vertical, and another horizontal. + */ + + shearXFromInputFile(ifP, cols, rows, maxval, format, + direction, xshearfac, + newmaxval, newformat, + !cmdline.noantialias, cmdline.background, + &shear1xels, &shear1Cols, &bgColor); + + pm_close(ifP); + + if (cmdline.verbose) + reportBackground(bgColor); + + if (cmdline.keeptemp) + storeImage("pnmrotate_stage1.pnm", shear1xels, shear1Cols, rows, + newmaxval, newformat); + + yshearjunk = (shear1Cols - cols) * yshearfac; + newRowsWithJunk = (shear1Cols - 1) * yshearfac + rows + 0.999999; + x2shearjunk = (newRowsWithJunk - rows - yshearjunk - 1) * xshearfac; + + shearImageY(shear1xels, shear1Cols, rows, newformat, + bgColor, !cmdline.noantialias, direction, + yshearfac, yshearjunk, + &shear2xels, &newrows); + + pnm_freearray(shear1xels, rows); + + if (cmdline.keeptemp) + storeImage("pnmrotate_stage2.pnm", shear2xels, shear1Cols, newrows, + newmaxval, newformat); + + shearXToOutputFile(stdout, shear2xels, shear1Cols, newrows, + newmaxval, newformat, + direction, xshearfac, x2shearjunk, + bgColor, !cmdline.noantialias); + + pnm_freearray(shear2xels, newrows); + pm_close(stdout); + + return 0; +} diff --git a/editor/pnmscale.c b/editor/pnmscale.c new file mode 100644 index 00000000..f75f440c --- /dev/null +++ b/editor/pnmscale.c @@ -0,0 +1,748 @@ +/* pnmscale.c - read a portable anymap and scale it +** +** Copyright (C) 1989, 1991 by Jef Poskanzer. +** +** 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. +** +*/ + +/* + + DON'T ADD NEW FUNCTION TO THIS PROGRAM. ADD IT TO pamscale.c + INSTEAD. + +*/ + + +#include <math.h> +#include <string.h> + +#include "pnm.h" +#include "shhopt.h" + +/* The pnm library allows us to code this program without branching cases + for PGM and PPM, but we do the branch anyway to speed up processing of + PGM images. +*/ + + +struct cmdline_info { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *input_filespec; /* Filespecs of input files */ + unsigned int xsize; + unsigned int ysize; + float xscale; + float yscale; + unsigned int xbox; + unsigned int ybox; + unsigned int pixels; + unsigned int verbose; + unsigned int nomix; +}; + + +static void +parse_command_line(int argc, char ** argv, + struct cmdline_info *cmdline_p) { +/*---------------------------------------------------------------------------- + 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 = malloc( 100*sizeof( optEntry ) ); + /* Instructions to optParseOptions3 on how to parse our options. + */ + optStruct3 opt; + + unsigned int option_def_index; + unsigned int xysize; + int xsize, ysize, pixels; + int reduce; + float xscale, yscale, scale_parm; + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENT3(0, "xsize", OPT_UINT, &xsize, NULL, 0); + OPTENT3(0, "width", OPT_UINT, &xsize, NULL, 0); + OPTENT3(0, "ysize", OPT_UINT, &ysize, NULL, 0); + OPTENT3(0, "height", OPT_UINT, &ysize, NULL, 0); + OPTENT3(0, "xscale", OPT_FLOAT, &xscale, NULL, 0); + OPTENT3(0, "yscale", OPT_FLOAT, &yscale, NULL, 0); + OPTENT3(0, "pixels", OPT_UINT, &pixels, NULL, 0); + OPTENT3(0, "reduce", OPT_UINT, &reduce, NULL, 0); + OPTENT3(0, "xysize", OPT_FLAG, NULL, &xysize, 0); + OPTENT3(0, "verbose", OPT_FLAG, NULL, &cmdline_p->verbose, 0); + OPTENT3(0, "nomix", OPT_FLAG, NULL, &cmdline_p->nomix, 0); + + /* Set the defaults. -1 = unspecified */ + /* (Now that we're using ParseOptions3, we don't have to do this -1 + nonsense, but we don't want to risk screwing these complex + option compatibilities up, so we'll convert that later. + */ + xsize = -1; + ysize = -1; + xscale = -1.0; + yscale = -1.0; + pixels = -1; + reduce = -1; + + 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 */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0 ); + /* Uses and sets argc, argv, and some of *cmdline_p and others. */ + + if (xsize == 0) + pm_error("-xsize/width must be greater than zero."); + if (ysize == 0) + pm_error("-ysize/height must be greater than zero."); + if (xscale != -1.0 && xscale <= 0.0) + pm_error("-xscale must be greater than zero."); + if (yscale != -1.0 && yscale <= 0.0) + pm_error("-yscale must be greater than zero."); + if (reduce <= 0 && reduce != -1) + pm_error("-reduce must be greater than zero."); + + if (xsize != -1 && xscale != -1) + pm_error("Cannot specify both -xsize/width and -xscale."); + if (ysize != -1 && yscale != -1) + pm_error("Cannot specify both -ysize/height and -yscale."); + + if (xysize && + (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 || + reduce != -1 || pixels != -1) ) + pm_error("Cannot specify -xysize with other dimension options."); + if (pixels != -1 && + (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 || + reduce != -1) ) + pm_error("Cannot specify -pixels with other dimension options."); + if (reduce != -1 && + (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1) ) + pm_error("Cannot specify -reduce with other dimension options."); + + if (pixels == 0) + pm_error("-pixels must be greater than zero"); + + /* Get the program parameters */ + + if (xysize) { + /* parameters are xbox, ybox, and optional filespec */ + scale_parm = 0.0; + if (argc-1 < 2) + pm_error("You must supply at least two parameters with -xysize:\n " + "x and y dimensions of the bounding box."); + else if (argc-1 > 3) + pm_error("Too many arguments. With -xysize, you need 2 or 3 " + "arguments."); + else { + char * endptr; + cmdline_p->xbox = strtol(argv[1], &endptr, 10); + if (strlen(argv[1]) > 0 && *endptr != '\0') + pm_error("horizontal xysize not an integer: '%s'", argv[1]); + if (cmdline_p->xbox <= 0) + pm_error("horizontal size is not positive: %d", + cmdline_p->xbox); + + cmdline_p->ybox = strtol(argv[2], &endptr, 10); + if (strlen(argv[2]) > 0 && *endptr != '\0') + pm_error("vertical xysize not an integer: '%s'", argv[2]); + if (cmdline_p->ybox <= 0) + pm_error("vertical size is not positive: %d", + cmdline_p->ybox); + + if (argc-1 < 3) + cmdline_p->input_filespec = "-"; + else + cmdline_p->input_filespec = argv[3]; + } + } else { + cmdline_p->xbox = 0; + cmdline_p->ybox = 0; + + if (xsize == -1 && xscale == -1 && ysize == -1 && yscale == -1 + && pixels == -1 && reduce == -1) { + /* parameters are scale factor and optional filespec */ + if (argc-1 < 1) + pm_error("With no dimension options, you must supply at least " + "one parameter: \nthe scale factor."); + else { + scale_parm = atof(argv[1]); + + if (scale_parm == 0.0) + pm_error("The scale parameter %s is not " + "a positive number.", + argv[1]); + else { + if (argc-1 < 2) + cmdline_p->input_filespec = "-"; + else + cmdline_p->input_filespec = argv[2]; + } + } + } else { + /* Only parameter allowed is optional filespec */ + if (argc-1 < 1) + cmdline_p->input_filespec = "-"; + else + cmdline_p->input_filespec = argv[1]; + + if (reduce != -1) { + scale_parm = ((double) 1.0) / ((double) reduce); + pm_message("reducing by %d gives scale factor of %f.", + reduce, scale_parm); + } else + scale_parm = 0.0; + } + } + + cmdline_p->xsize = xsize == -1 ? 0 : xsize; + cmdline_p->ysize = ysize == -1 ? 0 : ysize; + cmdline_p->pixels = pixels == -1 ? 0 : pixels; + + if (scale_parm) { + cmdline_p->xscale = scale_parm; + cmdline_p->yscale = scale_parm; + } else { + cmdline_p->xscale = xscale == -1.0 ? 0.0 : xscale; + cmdline_p->yscale = yscale == -1.0 ? 0.0 : yscale; + } +} + + + +static void +compute_output_dimensions(const struct cmdline_info cmdline, + const int rows, const int cols, + int * newrowsP, int * newcolsP) { + + if (cmdline.pixels) { + if (rows * cols <= cmdline.pixels) { + *newrowsP = rows; + *newcolsP = cols; + } else { + const double scale = + sqrt( (float) cmdline.pixels / ((float) cols * (float) rows)); + *newrowsP = rows * scale; + *newcolsP = cols * scale; + } + } else if (cmdline.xbox) { + const double aspect_ratio = (float) cols / (float) rows; + const double box_aspect_ratio = + (float) cmdline.xbox / (float) cmdline.ybox; + + if (box_aspect_ratio > aspect_ratio) { + *newrowsP = cmdline.ybox; + *newcolsP = *newrowsP * aspect_ratio + 0.5; + } else { + *newcolsP = cmdline.xbox; + *newrowsP = *newcolsP / aspect_ratio + 0.5; + } + } else { + if (cmdline.xsize) + *newcolsP = cmdline.xsize; + else if (cmdline.xscale) + *newcolsP = cmdline.xscale * cols + .5; + else if (cmdline.ysize) + *newcolsP = cols * ((float) cmdline.ysize/rows) +.5; + else + *newcolsP = cols; + + if (cmdline.ysize) + *newrowsP = cmdline.ysize; + else if (cmdline.yscale) + *newrowsP = cmdline.yscale * rows +.5; + else if (cmdline.xsize) + *newrowsP = rows * ((float) cmdline.xsize/cols) +.5; + else + *newrowsP = rows; + } + + /* If the calculations above yielded (due to rounding) a zero + dimension, we fudge it up to 1. We do this rather than considering + it a specification error (and dying) because it's friendlier to + automated processes that work on arbitrary input. It saves them + having to check their numbers to avoid catastrophe. + */ + + if (*newcolsP < 1) *newcolsP = 1; + if (*newrowsP < 1) *newrowsP = 1; +} + + + +static void +horizontal_scale(const xel inputxelrow[], xel newxelrow[], + const int cols, const int newcols, const float xscale, + const int format, const xelval maxval, + float * const stretchP) { +/*---------------------------------------------------------------------------- + Take the input row inputxelrow[], which is 'cols' columns wide, and + scale it by a factor of 'xscale', to create + the output row newxelrow[], which is 'newcols' columns wide. + + 'format' and 'maxval' describe the Netpbm format of the both input and + output rows. +-----------------------------------------------------------------------------*/ + float r, g, b; + float fraccoltofill, fraccolleft; + unsigned int col; + unsigned int newcol; + + newcol = 0; + fraccoltofill = 1.0; /* Output column is "empty" now */ + r = g = b = 0; /* initial value */ + for (col = 0; col < cols; ++col) { + /* Process one pixel from input ('inputxelrow') */ + fraccolleft = xscale; + /* Output all columns, if any, that can be filled using information + from this input column, in addition to what's already in the output + column. + */ + while (fraccolleft >= fraccoltofill) { + /* Generate one output pixel in 'newxelrow'. It will consist + of anything accumulated from prior input pixels in 'r','g', + and 'b', plus a fraction of the current input pixel. + */ + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + r += fraccoltofill * PPM_GETR(inputxelrow[col]); + g += fraccoltofill * PPM_GETG(inputxelrow[col]); + b += fraccoltofill * PPM_GETB(inputxelrow[col]); + PPM_ASSIGN( newxelrow[newcol], + MIN(maxval, (int) (r + 0.5)), + MIN(maxval, (int) (g + 0.5)), + MIN(maxval, (int) (b + 0.5)) + ); + break; + + default: + g += fraccoltofill * PNM_GET1(inputxelrow[col]); + PNM_ASSIGN1( newxelrow[newcol], MIN(maxval, (int) (g + 0.5))); + break; + } + fraccolleft -= fraccoltofill; + /* Set up to start filling next output column */ + newcol++; + fraccoltofill = 1.0; + r = g = b = 0.0; + } + /* There's not enough left in the current input pixel to fill up + a whole output column, so just accumulate the remainder of the + pixel into the current output column. + */ + if (fraccolleft > 0.0) { + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + r += fraccolleft * PPM_GETR(inputxelrow[col]); + g += fraccolleft * PPM_GETG(inputxelrow[col]); + b += fraccolleft * PPM_GETB(inputxelrow[col]); + break; + + default: + g += fraccolleft * PNM_GET1(inputxelrow[col]); + break; + } + fraccoltofill -= fraccolleft; + } + } + + if (newcol < newcols-1 || newcol > newcols) + pm_error("Internal error: last column filled is %d, but %d " + "is the rightmost output column.", + newcol, newcols-1); + + if (newcol < newcols ) { + /* We were still working on the last output column when we + ran out of input columns. This would be because of rounding + down, and we should be missing only a tiny fraction of that + last output column. + */ + + *stretchP = fraccoltofill; + + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + r += fraccoltofill * PPM_GETR(inputxelrow[cols-1]); + g += fraccoltofill * PPM_GETG(inputxelrow[cols-1]); + b += fraccoltofill * PPM_GETB(inputxelrow[cols-1]); + + PPM_ASSIGN(newxelrow[newcol], + MIN(maxval, (int) (r + 0.5)), + MIN(maxval, (int) (g + 0.5)), + MIN(maxval, (int) (b + 0.5)) + ); + break; + + default: + g += fraccoltofill * PNM_GET1(inputxelrow[cols-1]); + PNM_ASSIGN1( newxelrow[newcol], MIN(maxval, (int) (g + 0.5))); + break; + } + } else + *stretchP = 0; +} + + + +static void +zeroAccum(int const cols, int const format, + float rs[], float gs[], float bs[]) { + + int col; + + for ( col = 0; col < cols; ++col ) + rs[col] = gs[col] = bs[col] = 0.0; +} + + + +static void +accumOutputRow(xel * const xelrow, float const fraction, + float rs[], float gs[], float bs[], + int const cols, int const format) { +/*---------------------------------------------------------------------------- + Take 'fraction' times the color in row xelrow and add it to + rs/gs/bs. 'fraction' is less than 1.0. +-----------------------------------------------------------------------------*/ + int col; + + switch ( PNM_FORMAT_TYPE(format) ) { + case PPM_TYPE: + for ( col = 0; col < cols; ++col ) { + rs[col] += fraction * PPM_GETR(xelrow[col]); + gs[col] += fraction * PPM_GETG(xelrow[col]); + bs[col] += fraction * PPM_GETB(xelrow[col]); + } + break; + + default: + for ( col = 0; col < cols; ++col) + gs[col] += fraction * PNM_GET1(xelrow[col]); + break; + } +} + + + +static void +makeRow(xel * const xelrow, float rs[], float gs[], float bs[], + int const cols, xelval const maxval, int const format) { +/*---------------------------------------------------------------------------- + Make an xel row at 'xelrow' with format 'format' and + maxval 'maxval' out of the color values in + rs[], gs[], and bs[]. +-----------------------------------------------------------------------------*/ + int col; + + switch ( PNM_FORMAT_TYPE(format) ) { + case PPM_TYPE: + for ( col = 0; col < cols; ++col) { + PPM_ASSIGN(xelrow[col], + MIN(maxval, (int) (rs[col] + 0.5)), + MIN(maxval, (int) (gs[col] + 0.5)), + MIN(maxval, (int) (bs[col] + 0.5)) + ); + } + break; + + default: + for ( col = 0; col < cols; ++col ) { + PNM_ASSIGN1(xelrow[col], + MIN(maxval, (int) (gs[col] + 0.5))); + } + break; + } +} + + + +static void +scaleWithMixing(FILE * const ifP, + int const cols, int const rows, + xelval const maxval, int const format, + int const newcols, int const newrows, + xelval const newmaxval, int const newformat, + float const xscale, float const yscale, + bool const verbose) { +/*---------------------------------------------------------------------------- + Scale the image on input file 'ifP' (which is described by + 'cols', 'rows', 'format', and 'maxval') by xscale horizontally and + yscale vertically and write the result to standard output as format + 'newformat' and with maxval 'newmaxval'. + + The input file is positioned past the header, to the beginning of the + raster. The output file is too. + + Mix colors from input rows together in the output rows. +-----------------------------------------------------------------------------*/ + /* Here's how we think of the color mixing scaling operation: + + First, I'll describe scaling in one dimension. Assume we have + a one row image. A raster row is ordinarily a sequence of + discrete pixels which have no width and no distance between + them -- only a sequence. Instead, think of the raster row as a + bunch of pixels 1 unit wide adjacent to each other. For + example, we are going to scale a 100 pixel row to a 150 pixel + row. Imagine placing the input row right above the output row + and stretching it so it is the same size as the output row. It + still contains 100 pixels, but they are 1.5 units wide each. + Our goal is to make the output row look as much as possible + like the input row, while observing that a pixel can be only + one color. + + Output Pixel 0 is completely covered by Input Pixel 0, so we + make Output Pixel 0 the same color as Input Pixel 0. Output + Pixel 1 is covered half by Input Pixel 0 and half by Input + Pixel 1. So we make Output Pixel 1 a 50/50 mix of Input Pixels + 0 and 1. If you stand back far enough, input and output will + look the same. + + This works for all scale factors, both scaling up and scaling down. + + This program always stretches or squeezes the input row to be the + same length as the output row; The output row's pixels are always + 1 unit wide. + + The same thing works in the vertical direction. We think of + rows as stacked strips of 1 unit height. We conceptually + stretch the image vertically first (same process as above, but + in place of a single-color pixels, we have a vector of colors). + Then we take each row this vertical stretching generates and + stretch it horizontally. + */ + + xel* xelrow; /* An input row */ + xel* vertScaledRow; + /* An output row after vertical scaling, but before horizontal + scaling + */ + xel* newxelrow; + float rowsleft; + /* The number of rows of output that need to be formed from the + current input row (the one in xelrow[]), less the number that + have already been formed (either in the rs/gs/bs accumulators + or output to the file). This can be fractional because of the + way we define rows as having height. + */ + float fracrowtofill; + /* The fraction of the current output row (the one in vertScaledRow[]) + that hasn't yet been filled in from an input row. + */ + float *rs, *gs, *bs; + /* The red, green, and blue color intensities so far accumulated + from input rows for the current output row. + */ + int rowsread; + /* Number of rows of the input file that have been read */ + int row; + + xelrow = pnm_allocrow(cols); + vertScaledRow = pnm_allocrow(cols); + rs = (float*) pm_allocrow( cols, sizeof(float) ); + gs = (float*) pm_allocrow( cols, sizeof(float) ); + bs = (float*) pm_allocrow( cols, sizeof(float) ); + rowsread = 0; + rowsleft = 0.0; + zeroAccum(cols, format, rs, gs, bs); + fracrowtofill = 1.0; + + newxelrow = pnm_allocrow( newcols ); + + for ( row = 0; row < newrows; ++row ) { + /* First scale Y from xelrow[] into vertScaledRow[]. */ + + if ( newrows == rows ) { /* shortcut Y scaling if possible */ + pnm_readpnmrow( ifP, vertScaledRow, cols, newmaxval, format ); + } else { + while (fracrowtofill > 0) { + if (rowsleft <= 0.0) { + if (rowsread < rows) { + pnm_readpnmrow(ifP, xelrow, cols, newmaxval, format); + ++rowsread; + } else { + /* We need another input row to fill up this + output row, but there aren't any more. + That's because of rounding down on our + scaling arithmetic. So we go ahead with + the data from the last row we read, which + amounts to stretching out the last output + row. + */ + if (verbose) + pm_message("%f of bottom row stretched due to " + "arithmetic imprecision", + fracrowtofill); + } + rowsleft = yscale; + } + if (rowsleft < fracrowtofill) { + accumOutputRow(xelrow, rowsleft, rs, gs, bs, + cols, format); + fracrowtofill -= rowsleft; + rowsleft = 0.0; + } else { + accumOutputRow(xelrow, fracrowtofill, rs, gs, bs, + cols, format); + rowsleft = rowsleft - fracrowtofill; + fracrowtofill = 0.0; + } + } + makeRow(vertScaledRow, rs, gs, bs, cols, newmaxval, format); + zeroAccum(cols, format, rs, gs, bs); + fracrowtofill = 1.0; + } + + /* Now scale vertScaledRow horizontally into newxelrow and write + it out. + */ + + if (newcols == cols) /* shortcut X scaling if possible */ + pnm_writepnmrow(stdout, vertScaledRow, newcols, + newmaxval, newformat, 0); + else { + float stretch; + + horizontal_scale(vertScaledRow, newxelrow, cols, newcols, xscale, + format, newmaxval, &stretch); + + if (verbose && row == 0) + pm_message("%f of right column stretched due to " + "arithmetic imprecision", + stretch); + + pnm_writepnmrow(stdout, newxelrow, newcols, + newmaxval, newformat, 0 ); + } + } + pnm_freerow(newxelrow); + pnm_freerow(xelrow); + pnm_freerow(vertScaledRow); +} + + + +static void +scaleWithoutMixing(FILE * const ifP, + int const cols, int const rows, + xelval const maxval, int const format, + int const newcols, int const newrows, + xelval const newmaxval, int const newformat, + float const xscale, float const yscale) { +/*---------------------------------------------------------------------------- + Scale the image on input file 'ifP' (which is described by + 'cols', 'rows', 'format', and 'maxval') by xscale horizontally and + yscale vertically and write the result to standard output as format + 'newformat' and with maxval 'newmaxval'. + + The input file is positioned past the header, to the beginning of the + raster. The output file is too. + + Don't mix colors from different input pixels together in the output + pixels. Each output pixel is an exact copy of some corresponding + input pixel. +-----------------------------------------------------------------------------*/ + xel* xelrow; /* An input row */ + xel* newxelrow; + int row; + int rowInXelrow; + + xelrow = pnm_allocrow(cols); + rowInXelrow = -1; + + newxelrow = pnm_allocrow(newcols); + + for (row = 0; row < newrows; ++row) { + int col; + + int const inputRow = (int) (row / yscale); + + for (; rowInXelrow < inputRow; ++rowInXelrow) + pnm_readpnmrow(ifP, xelrow, cols, newmaxval, format); + + + for (col = 0; col < newcols; ++col) { + int const inputCol = (int) (col / xscale); + + newxelrow[col] = xelrow[inputCol]; + } + + pnm_writepnmrow(stdout, newxelrow, newcols, + newmaxval, newformat, 0 ); + } + pnm_freerow(xelrow); + pnm_freerow(newxelrow); +} + + + +int +main(int argc, char **argv ) { + + struct cmdline_info cmdline; + FILE* ifP; + int rows, cols, format, newformat, newrows, newcols; + xelval maxval, newmaxval; + float xscale, yscale; + + pnm_init( &argc, argv ); + + parse_command_line(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.input_filespec); + + pnm_readpnminit( ifP, &cols, &rows, &maxval, &format ); + + /* Promote PBM files to PGM. */ + if ( PNM_FORMAT_TYPE(format) == PBM_TYPE ) { + newformat = PGM_TYPE; + newmaxval = PGM_MAXMAXVAL; + pm_message( "promoting from PBM to PGM" ); + } else { + newformat = format; + newmaxval = maxval; + } + compute_output_dimensions(cmdline, rows, cols, &newrows, &newcols); + + /* We round the scale factor down so that we never fill up the + output while (a fractional pixel of) input remains unused. Instead, + we will run out of input while (a fractional pixel of) output is + unfilled -- which is easier for our algorithm to handle. + */ + xscale = (float) newcols / cols; + yscale = (float) newrows / rows; + + if (cmdline.verbose) { + pm_message("Scaling by %f horizontally to %d columns.", + xscale, newcols ); + pm_message("Scaling by %f vertically to %d rows.", + yscale, newrows); + } + + if (xscale * cols < newcols - 1 || + yscale * rows < newrows - 1) + pm_error("Arithmetic precision of this program is inadequate to " + "do the specified scaling. Use a smaller input image " + "or a slightly different scale factor."); + + pnm_writepnminit(stdout, newcols, newrows, newmaxval, newformat, 0); + + if (cmdline.nomix) + scaleWithoutMixing(ifP, cols, rows, maxval, format, + newcols, newrows, newmaxval, newformat, + xscale, yscale); + else + scaleWithMixing(ifP, cols, rows, maxval, format, + newcols, newrows, newmaxval, newformat, + xscale, yscale, cmdline.verbose); + + pm_close(ifP); + pm_close(stdout); + + exit(0); +} diff --git a/editor/pnmscalefixed.c b/editor/pnmscalefixed.c new file mode 100644 index 00000000..d562c670 --- /dev/null +++ b/editor/pnmscalefixed.c @@ -0,0 +1,590 @@ +/* pnmscale.c - read a portable anymap and scale it +** +** Copyright (C) 1989, 1991 by Jef Poskanzer. +** +** 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. +** +** Modified: +** +** June 6, 2001: Christopher W. Boyd <cboyd@pobox.com> +** - added -reduce N to allow scaling by integer value +** in this case, scale_comp becomes 1/N and x/yscale +** get set as they should +** +** +*/ + +#include <math.h> +#include "pnm.h" +#include "shhopt.h" + +/* The pnm library allows us to code this program without branching cases + for PGM and PPM, but we do the branch anyway to speed up processing of + PGM images. +*/ + +/* We do all our arithmetic in integers. In order not to get killed by the + rounding, we scale every number up by the factor SCALE, do the + arithmetic, then scale it back down. + */ +#define SCALE 4096 +#define HALFSCALE 2048 + + +struct cmdline_info { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *input_filespec; /* Filespecs of input files */ + unsigned int xsize; + unsigned int ysize; + float xscale; + float yscale; + unsigned int xbox; + unsigned int ybox; + unsigned int pixels; + unsigned int verbose; +}; + + +static void +parse_command_line(int argc, char ** argv, + struct cmdline_info *cmdline_p) { +/*---------------------------------------------------------------------------- + Note that the file spec array we return is stored in the storage that + was passed to us as the argv array. +-----------------------------------------------------------------------------*/ + optStruct *option_def = malloc(100*sizeof(optStruct)); + /* Instructions to OptParseOptions2 on how to parse our options. + */ + optStruct2 opt; + + unsigned int option_def_index; + int xysize, xsize, ysize, pixels; + int reduce; + float xscale, yscale, scale_parm; + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENTRY(0, "xsize", OPT_UINT, &xsize, 0); + OPTENTRY(0, "width", OPT_UINT, &xsize, 0); + OPTENTRY(0, "ysize", OPT_UINT, &ysize, 0); + OPTENTRY(0, "height", OPT_UINT, &ysize, 0); + OPTENTRY(0, "xscale", OPT_FLOAT, &xscale, 0); + OPTENTRY(0, "yscale", OPT_FLOAT, &yscale, 0); + OPTENTRY(0, "pixels", OPT_UINT, &pixels, 0); + OPTENTRY(0, "xysize", OPT_FLAG, &xysize, 0); + OPTENTRY(0, "verbose", OPT_FLAG, &cmdline_p->verbose, 0); + OPTENTRY(0, "reduce", OPT_UINT, &reduce, 0); + + /* Set the defaults. -1 = unspecified */ + xsize = -1; + ysize = -1; + xscale = -1.0; + yscale = -1.0; + pixels = -1; + xysize = 0; + reduce = -1; + cmdline_p->verbose = FALSE; + + 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 */ + + optParseOptions2(&argc, argv, opt, 0); + /* Uses and sets argc, argv, and some of *cmdline_p and others. */ + + if (xsize == 0) + pm_error("-xsize/width must be greater than zero."); + if (ysize == 0) + pm_error("-ysize/height must be greater than zero."); + if (xscale != -1.0 && xscale <= 0.0) + pm_error("-xscale must be greater than zero."); + if (yscale != -1.0 && yscale <= 0.0) + pm_error("-yscale must be greater than zero."); + if (reduce <= 0 && reduce != -1) + pm_error("-reduce must be greater than zero."); + + if (xsize != -1 && xscale != -1) + pm_error("Cannot specify both -xsize/width and -xscale."); + if (ysize != -1 && yscale != -1) + pm_error("Cannot specify both -ysize/height and -yscale."); + + if (xysize && + (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 || + reduce != -1 || pixels != -1) ) + pm_error("Cannot specify -xysize with other dimension options."); + if (pixels != -1 && + (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1 || + reduce != -1) ) + pm_error("Cannot specify -pixels with other dimension options."); + if (reduce != -1 && + (xsize != -1 || xscale != -1 || ysize != -1 || yscale != -1) ) + pm_error("Cannot specify -reduce with other dimension options."); + + if (pixels == 0) + pm_error("-pixels must be greater than zero"); + + /* Get the program parameters */ + + if (xysize) { + /* parameters are xbox, ybox, and optional filespec */ + scale_parm = 0.0; + if (argc-1 < 2) + pm_error("You must supply at least two parameters with -xysize:\n " + "x and y dimensions of the bounding box."); + else if (argc-1 > 3) + pm_error("Too many arguments. With -xysize, you need 2 or 3 " + "arguments."); + else { + cmdline_p->xbox = atoi(argv[1]); + cmdline_p->ybox = atoi(argv[2]); + + if (argc-1 < 3) + cmdline_p->input_filespec = "-"; + else + cmdline_p->input_filespec = argv[3]; + } + } else { + cmdline_p->xbox = 0; + cmdline_p->ybox = 0; + + if (xsize == -1 && xscale == -1 && ysize == -1 && yscale == -1 + && pixels == -1 && reduce == -1) { + /* parameters are scale factor and optional filespec */ + if (argc-1 < 1) + pm_error("With no dimension options, you must supply at least " + "one parameter: \nthe scale factor."); + else { + scale_parm = atof(argv[1]); + + if (scale_parm == 0.0) + pm_error("The scale parameter %s is not " + "a positive number.", + argv[1]); + else { + if (argc-1 < 2) + cmdline_p->input_filespec = "-"; + else + cmdline_p->input_filespec = argv[2]; + } + } + } else { + /* Only parameter allowed is optional filespec */ + if (argc-1 < 1) + cmdline_p->input_filespec = "-"; + else + cmdline_p->input_filespec = argv[1]; + + if (reduce != -1) { + scale_parm = ((double) 1.0) / ((double) reduce); + pm_message("reducing by %d gives scale factor of %f.", + reduce, scale_parm); + } else + scale_parm = 0.0; + } + } + + cmdline_p->xsize = xsize == -1 ? 0 : xsize; + cmdline_p->ysize = ysize == -1 ? 0 : ysize; + cmdline_p->pixels = pixels == -1 ? 0 : pixels; + + if (scale_parm) { + cmdline_p->xscale = scale_parm; + cmdline_p->yscale = scale_parm; + } else { + cmdline_p->xscale = xscale == -1.0 ? 0.0 : xscale; + cmdline_p->yscale = yscale == -1.0 ? 0.0 : yscale; + } +} + + + +static void +compute_output_dimensions(const struct cmdline_info cmdline, + const int rows, const int cols, + int * newrowsP, int * newcolsP) { + + if (cmdline.pixels) { + if (rows * cols <= cmdline.pixels) { + *newrowsP = rows; + *newcolsP = cols; + } else { + const double scale = + sqrt( (float) cmdline.pixels / ((float) cols * (float) rows)); + *newrowsP = rows * scale; + *newcolsP = cols * scale; + } + } else if (cmdline.xbox) { + const double aspect_ratio = (float) cols / (float) rows; + const double box_aspect_ratio = + (float) cmdline.xbox / (float) cmdline.ybox; + + if (box_aspect_ratio > aspect_ratio) { + *newrowsP = cmdline.ybox; + *newcolsP = *newrowsP * aspect_ratio + 0.5; + } else { + *newcolsP = cmdline.xbox; + *newrowsP = *newcolsP / aspect_ratio + 0.5; + } + } else { + if (cmdline.xsize) + *newcolsP = cmdline.xsize; + else if (cmdline.xscale) + *newcolsP = cmdline.xscale * cols + .5; + else if (cmdline.ysize) + *newcolsP = cols * ((float) cmdline.ysize/rows) +.5; + else + *newcolsP = cols; + + if (cmdline.ysize) + *newrowsP = cmdline.ysize; + else if (cmdline.yscale) + *newrowsP = cmdline.yscale * rows +.5; + else if (cmdline.xsize) + *newrowsP = rows * ((float) cmdline.xsize/cols) +.5; + else + *newrowsP = rows; + } + + /* If the calculations above yielded (due to rounding) a zero + dimension, we fudge it up to 1. We do this rather than considering + it a specification error (and dying) because it's friendlier to + automated processes that work on arbitrary input. It saves them + having to check their numbers to avoid catastrophe. + */ + + if (*newcolsP < 1) *newcolsP = 1; + if (*newrowsP < 1) *newrowsP = 1; +} + + + +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) { +/*---------------------------------------------------------------------------- + Take the input row inputxelrow[], which is 'cols' columns wide, and + scale it by a factor of 'sxcale', which is in SCALEths to create + the output row newxelrow[], which is 'newcols' columns wide. + + 'format' and 'maxval' describe the Netpbm format of the both input and + output rows. + + *stretchP is the number of columns (could be fractional) on the right + that we had to fill by stretching due to rounding problems. +-----------------------------------------------------------------------------*/ + long r, g, b; + long fraccoltofill, fraccolleft; + unsigned int col; + unsigned int newcol; + + newcol = 0; + fraccoltofill = SCALE; /* Output column is "empty" now */ + r = g = b = 0; /* initial value */ + for (col = 0; col < cols; ++col) { + /* Process one pixel from input ('inputxelrow') */ + fraccolleft = sxscale; + /* Output all columns, if any, that can be filled using information + from this input column, in addition what's already in the output + column. + */ + while (fraccolleft >= fraccoltofill) { + /* Generate one output pixel in 'newxelrow'. It will consist + of anything accumulated from prior input pixels in 'r','g', + and 'b', plus a fraction of the current input pixel. + */ + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + r += fraccoltofill * PPM_GETR(inputxelrow[col]); + g += fraccoltofill * PPM_GETG(inputxelrow[col]); + b += fraccoltofill * PPM_GETB(inputxelrow[col]); + r /= SCALE; + if ( r > maxval ) r = maxval; + g /= SCALE; + if ( g > maxval ) g = maxval; + b /= SCALE; + if ( b > maxval ) b = maxval; + PPM_ASSIGN( newxelrow[newcol], r, g, b ); + break; + + default: + g += fraccoltofill * PNM_GET1(inputxelrow[col]); + g /= SCALE; + if ( g > maxval ) g = maxval; + PNM_ASSIGN1( newxelrow[newcol], g ); + break; + } + fraccolleft -= fraccoltofill; + /* Set up to start filling next output column */ + newcol++; + fraccoltofill = SCALE; + r = g = b = 0; + } + /* There's not enough left in the current input pixel to fill up + a whole output column, so just accumulate the remainder of the + pixel into the current output column. + */ + if (fraccolleft > 0) { + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + r += fraccolleft * PPM_GETR(inputxelrow[col]); + g += fraccolleft * PPM_GETG(inputxelrow[col]); + b += fraccolleft * PPM_GETB(inputxelrow[col]); + break; + + default: + g += fraccolleft * PNM_GET1(inputxelrow[col]); + break; + } + fraccoltofill -= fraccolleft; + } + } + + *stretchP = 0; /* initial value */ + while (newcol < newcols) { + /* We ran out of input columns before we filled up the output + columns. This would be because of rounding down. For small + images, we're probably missing only a tiny fraction of a column, + but for large images, it could be multiple columns. + + So we fake the remaining output columns by copying the rightmost + legitimate pixel. We call this stretching. + */ + + *stretchP += fraccoltofill; + + switch (PNM_FORMAT_TYPE(format)) { + case PPM_TYPE: + r += fraccoltofill * PPM_GETR(inputxelrow[cols-1]); + g += fraccoltofill * PPM_GETG(inputxelrow[cols-1]); + b += fraccoltofill * PPM_GETB(inputxelrow[cols-1]); + + r += HALFSCALE; /* for rounding */ + r /= SCALE; + if ( r > maxval ) r = maxval; + g += HALFSCALE; /* for rounding */ + g /= SCALE; + if ( g > maxval ) g = maxval; + b += HALFSCALE; /* for rounding */ + b /= SCALE; + if ( b > maxval ) b = maxval; + PPM_ASSIGN(newxelrow[newcol], r, g, b ); + break; + + default: + g += fraccoltofill * PNM_GET1(inputxelrow[cols-1]); + g += HALFSCALE; /* for rounding */ + g /= SCALE; + if ( g > maxval ) g = maxval; + PNM_ASSIGN1(newxelrow[newcol], g ); + break; + } + newcol++; + fraccoltofill = SCALE; + } +} + + +int +main(int argc, char **argv ) { + + struct cmdline_info cmdline; + FILE* ifp; + xel* xelrow; + xel* tempxelrow; + xel* newxelrow; + xel* xP; + xel* nxP; + int rows, cols, format, newformat, rowsread, newrows, newcols; + int row, col, needtoreadrow; + xelval maxval, newmaxval; + long sxscale, syscale; + long fracrowtofill, fracrowleft; + long* rs; + long* gs; + long* bs; + int vertical_stretch; + /* The number of rows we had to fill by stretching because of + rounding error, which made us run out of input rows before we + had filled up the output rows. + */ + + pnm_init( &argc, argv ); + + parse_command_line(argc, argv, &cmdline); + + ifp = pm_openr(cmdline.input_filespec); + + pnm_readpnminit( ifp, &cols, &rows, &maxval, &format ); + + /* Promote PBM files to PGM. */ + if ( PNM_FORMAT_TYPE(format) == PBM_TYPE ) { + newformat = PGM_TYPE; + newmaxval = PGM_MAXMAXVAL; + pm_message( "promoting from PBM to PGM" ); + } else { + newformat = format; + newmaxval = maxval; + } + compute_output_dimensions(cmdline, rows, cols, &newrows, &newcols); + + /* We round the scale factor down so that we never fill up the + output while (a fractional pixel of) input remains unused. + Instead, we will run out of input while some of the output is + unfilled. We can address that by stretching, whereas the other + case would require throwing away some of the input. + */ + sxscale = SCALE * newcols / cols; + syscale = SCALE * newrows / rows; + + if (cmdline.verbose) { + pm_message("Scaling by %ld/%d = %f horizontally to %d columns.", + sxscale, SCALE, (float) sxscale/SCALE, newcols ); + pm_message("Scaling by %ld/%d = %f vertically to %d rows.", + syscale, SCALE, (float) syscale/SCALE, newrows); + } + + xelrow = pnm_allocrow(cols); + if (newrows == rows) /* shortcut Y scaling if possible */ + tempxelrow = xelrow; + else + tempxelrow = pnm_allocrow( cols ); + rs = (long*) pm_allocrow( cols, sizeof(long) ); + gs = (long*) pm_allocrow( cols, sizeof(long) ); + bs = (long*) pm_allocrow( cols, sizeof(long) ); + rowsread = 0; + fracrowleft = syscale; + needtoreadrow = 1; + for ( col = 0; col < cols; ++col ) + rs[col] = gs[col] = bs[col] = HALFSCALE; + fracrowtofill = SCALE; + vertical_stretch = 0; + + pnm_writepnminit( stdout, newcols, newrows, newmaxval, newformat, 0 ); + newxelrow = pnm_allocrow( newcols ); + + for ( row = 0; row < newrows; ++row ) { + /* First scale vertically from xelrow into tempxelrow. */ + if ( newrows == rows ) { /* shortcut vertical scaling if possible */ + pnm_readpnmrow( ifp, xelrow, cols, newmaxval, format ); + } else { + while ( fracrowleft < fracrowtofill ) { + if ( needtoreadrow ) + if ( rowsread < rows ) { + pnm_readpnmrow( ifp, xelrow, cols, newmaxval, format ); + ++rowsread; + } + switch ( PNM_FORMAT_TYPE(format) ) { + case PPM_TYPE: + for ( col = 0, xP = xelrow; col < cols; ++col, ++xP ) { + rs[col] += fracrowleft * PPM_GETR( *xP ); + gs[col] += fracrowleft * PPM_GETG( *xP ); + bs[col] += fracrowleft * PPM_GETB( *xP ); + } + break; + + default: + for ( col = 0, xP = xelrow; col < cols; ++col, ++xP ) + gs[col] += fracrowleft * PNM_GET1( *xP ); + break; + } + fracrowtofill -= fracrowleft; + fracrowleft = syscale; + needtoreadrow = 1; + } + /* Now fracrowleft is >= fracrowtofill, so we can produce a row. */ + if ( needtoreadrow ) { + if ( rowsread < rows ) { + pnm_readpnmrow( ifp, xelrow, cols, newmaxval, format ); + ++rowsread; + needtoreadrow = 0; + } else { + /* We need another input row to fill up this output row, + but there aren't any more. That's because of rounding + down on our scaling arithmetic. So we go ahead with + the data from the last row we read, which amounts to + stretching out the last output row. + */ + vertical_stretch += fracrowtofill; + } + } + switch ( PNM_FORMAT_TYPE(format) ) { + case PPM_TYPE: + for ( col = 0, xP = xelrow, nxP = tempxelrow; + col < cols; ++col, ++xP, ++nxP ) { + register long r, g, b; + + r = rs[col] + fracrowtofill * PPM_GETR( *xP ); + g = gs[col] + fracrowtofill * PPM_GETG( *xP ); + b = bs[col] + fracrowtofill * PPM_GETB( *xP ); + r /= SCALE; + if ( r > newmaxval ) r = newmaxval; + g /= SCALE; + if ( g > newmaxval ) g = newmaxval; + b /= SCALE; + if ( b > newmaxval ) b = newmaxval; + PPM_ASSIGN( *nxP, r, g, b ); + rs[col] = gs[col] = bs[col] = HALFSCALE; + } + break; + + default: + for ( col = 0, xP = xelrow, nxP = tempxelrow; + col < cols; ++col, ++xP, ++nxP ) { + register long g; + + g = gs[col] + fracrowtofill * PNM_GET1( *xP ); + g /= SCALE; + if ( g > newmaxval ) g = newmaxval; + PNM_ASSIGN1( *nxP, g ); + gs[col] = HALFSCALE; + } + break; + } + fracrowleft -= fracrowtofill; + if ( fracrowleft == 0 ) { + fracrowleft = syscale; + needtoreadrow = 1; + } + fracrowtofill = SCALE; + } + + /* Now scale tempxelrow horizontally into newxelrow & write it out. */ + + if (newcols == cols) /* shortcut X scaling if possible */ + pnm_writepnmrow(stdout, tempxelrow, newcols, + newmaxval, newformat, 0); + else { + int stretch; + + horizontal_scale(tempxelrow, newxelrow, cols, newcols, sxscale, + format, newmaxval, &stretch); + + if (cmdline.verbose && row == 0 && stretch != 0) + pm_message("%d/%d = %f right columns filled by stretching " + "due to arithmetic imprecision", + stretch, SCALE, (float) stretch/SCALE); + + pnm_writepnmrow(stdout, newxelrow, newcols, + newmaxval, newformat, 0 ); + } + } + + if (cmdline.verbose && vertical_stretch != 0) + pm_message("%d/%d = %f bottom rows filled by stretching due to " + "arithmetic imprecision", + vertical_stretch, SCALE, + (float) vertical_stretch/SCALE); + + pm_close( ifp ); + pm_close( stdout ); + + exit( 0 ); +} diff --git a/editor/pnmshear.c b/editor/pnmshear.c new file mode 100644 index 00000000..1b2d36a8 --- /dev/null +++ b/editor/pnmshear.c @@ -0,0 +1,227 @@ +/* pnmshear.c - read a portable anymap and shear it by some angle +** +** Copyright (C) 1989, 1991 by Jef Poskanzer. +** +** 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 _XOPEN_SOURCE /* get M_PI in math.h */ + +#include <math.h> +#include <string.h> + +#include "pnm.h" +#include "shhopt.h" + +#define SCALE 4096 +#define HALFSCALE 2048 + +struct cmdline_info { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char * input_filespec; /* Filespec of input file */ + double angle; /* requested shear angle, in radians */ + unsigned int noantialias; /* -noantialias option */ +}; + + + +static void +parse_command_line(int argc, char ** argv, + struct cmdline_info *cmdlineP) { + + optStruct3 opt; + unsigned int option_def_index = 0; + optEntry *option_def = malloc(100*sizeof(optEntry)); + + OPTENT3(0, "noantialias", OPT_FLAG, NULL, &cmdlineP->noantialias, 0); + + opt.opt_table = option_def; + opt.short_allowed = FALSE; + opt.allowNegNum = TRUE; + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + + if (argc-1 < 1) + pm_error("Need an argument: the shear angle.\n"); + else { + char *endptr; + cmdlineP->angle = strtod(argv[1], &endptr) * M_PI / 180; + if (*endptr != '\0' || strlen(argv[1]) == 0) + pm_error("Angle argument is not a valid floating point number: " + "'%s'", argv[1]); + if (argc-1 < 2) + cmdlineP->input_filespec = "-"; + else { + cmdlineP->input_filespec = argv[2]; + if (argc-1 > 2) + pm_error("too many arguments (%d). " + "The only arguments are shear angle and filespec.", + argc-1); + } + } +} + + +static void +makeNewXel(xel * const outputXelP, xel const curXel, xel const prevXel, + double const fracnew0, double const omfracnew0, int const format) { +/*---------------------------------------------------------------------------- + Create an output xel as *outputXel, which is part curXel and part + prevXel, the part given by the fractions omfracnew0 and fracnew0, + respectively. These fraction values are the numerator of a fraction + whose denominator is SCALE. + + The format of the pixel is 'format'. +-----------------------------------------------------------------------------*/ + + switch ( PNM_FORMAT_TYPE(format) ) { + case PPM_TYPE: + PPM_ASSIGN( *outputXelP, + ( fracnew0 * PPM_GETR(prevXel) + + omfracnew0 * PPM_GETR(curXel) + + HALFSCALE ) / SCALE, + ( fracnew0 * PPM_GETG(prevXel) + + omfracnew0 * PPM_GETG(curXel) + + HALFSCALE ) / SCALE, + ( fracnew0 * PPM_GETB(prevXel) + + omfracnew0 * PPM_GETB(curXel) + + HALFSCALE ) / SCALE ); + break; + + default: + PNM_ASSIGN1( *outputXelP, + ( fracnew0 * PNM_GET1(prevXel) + + omfracnew0 * PNM_GET1(curXel) + + HALFSCALE ) / SCALE ); + break; + } +} + + +static void +shear_row(xel * const xelrow, int const cols, + xel * const newxelrow, int const newcols, + double const shearCols, + int const format, xel const bgxel, bool const antialias) { +/*---------------------------------------------------------------------------- + Shear the row 'xelrow' by 'shearCols' columns, and return the result as + 'newxelrow'. They are 'cols' and 'newcols' columns wide, respectively. + + Fill in the part of the output row that doesn't contain image data with + 'bgxel'. + + Use antialiasing iff 'antialias'. + + The format of the input xels (which implies something about the + output xels too) is 'format'. +-----------------------------------------------------------------------------*/ + int const intShearCols = (int) shearCols; + + if ( antialias ) { + const long fracnew0 = ( shearCols - intShearCols ) * SCALE; + const long omfracnew0 = SCALE - fracnew0; + + int col; + xel prevXel; + + for ( col = 0; col < newcols; ++col ) + newxelrow[col] = bgxel; + + prevXel = bgxel; + for ( col = 0; col < cols; ++col){ + makeNewXel(&newxelrow[intShearCols + col], + xelrow[col], prevXel, fracnew0, omfracnew0, + format); + prevXel = xelrow[col]; + } + if ( fracnew0 > 0 ) + /* Need to add a column for what's left over */ + makeNewXel(&newxelrow[intShearCols + cols], + bgxel, prevXel, fracnew0, omfracnew0, format); + } else { + int col; + for ( col = 0; col < intShearCols; ++col ) + newxelrow[col] = bgxel; + for ( col = 0; col < cols; ++col ) + newxelrow[intShearCols+col] = xelrow[col]; + for ( col = intShearCols + cols; col < newcols; ++col ) + newxelrow[col] = bgxel; + } +} + + + +int +main(int argc, char * argv[]) { + FILE* ifp; + xel* xelrow; + xel* newxelrow; + xel bgxel; + int rows, cols, format; + int newformat, newcols; + int row; + xelval maxval, newmaxval; + double shearfac; + + struct cmdline_info cmdline; + + pnm_init( &argc, argv ); + + parse_command_line( argc, argv, &cmdline ); + + ifp = pm_openr( cmdline.input_filespec ); + + pnm_readpnminit( ifp, &cols, &rows, &maxval, &format ); + xelrow = pnm_allocrow( cols ); + + /* Promote PBM files to PGM. */ + if ( !cmdline.noantialias && PNM_FORMAT_TYPE(format) == PBM_TYPE ) { + newformat = PGM_TYPE; + newmaxval = PGM_MAXMAXVAL; + pm_message( "promoting from PBM to PGM - " + "use -noantialias to avoid this" ); + } else { + newformat = format; + newmaxval = maxval; + } + + shearfac = tan( cmdline.angle ); + if ( shearfac < 0.0 ) + shearfac = -shearfac; + + newcols = rows * shearfac + cols + 0.999999; + + pnm_writepnminit( stdout, newcols, rows, newmaxval, newformat, 0 ); + newxelrow = pnm_allocrow( newcols ); + + bgxel = pnm_backgroundxelrow( xelrow, cols, newmaxval, format ); + + for ( row = 0; row < rows; ++row ) { + double shearCols; + + pnm_readpnmrow( ifp, xelrow, cols, newmaxval, format ); + + if ( cmdline.angle > 0.0 ) + shearCols = row * shearfac; + else + shearCols = ( rows - row ) * shearfac; + + shear_row(xelrow, cols, newxelrow, newcols, + shearCols, format, bgxel, !cmdline.noantialias); + + pnm_writepnmrow( stdout, newxelrow, newcols, newmaxval, newformat, 0 ); + } + + pm_close( ifp ); + pm_close( stdout ); + + exit( 0 ); +} + diff --git a/editor/pnmsmooth.README b/editor/pnmsmooth.README new file mode 100644 index 00000000..fc5329bd --- /dev/null +++ b/editor/pnmsmooth.README @@ -0,0 +1,21 @@ +README for pnmsmooth.c 2.0 + +This is a replacement for the pnmsmooth script that is distributed with +pbmplus/netpbm. This version of pnmsmooth is written as a C program rather +than a shell script. It accepts command line arguments to specify a size +other than 3x3 for the convolution matrix and an argument for dumping the +resultant convolution matrix as a PGM file. By default it uses the same 3x3 +matrix size as the pnmsmooth script and can be used as a direct replacement. + +Also included are an updated man page and a patch file to update the Imakefile +in the pnm directory. You may want to apply the patches by hand if you have +already modified the Imakefile to add other new programs. You will then +have to remake the Makefiles and then type 'make all' in the pnm directory. + +- Mike + +---------------------------------------------------------------------------- +Mike Burns System Administrator +burns@chem.psu.edu Department of Chemistry +(814) 863-2123 The Pennsylvania State University + diff --git a/editor/pnmsmooth.c b/editor/pnmsmooth.c new file mode 100644 index 00000000..a18511c7 --- /dev/null +++ b/editor/pnmsmooth.c @@ -0,0 +1,241 @@ +/* pnmsmooth.c - smooth out an image by replacing each pixel with the +** average of its width x height neighbors. +** +** Version 2.0 December 5, 1994 +** +** Copyright (C) 1994 by Mike Burns (burns@chem.psu.edu) +** +** 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. +*/ + +/* + Written by Mike Burns December 5, 1994 and called Version 2.0. + Based on ideas from a shell script by Jef Poskanzer, 1989, 1991. + The shell script had no options. +*/ + + +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <sys/wait.h> + +#include "pm_c_util.h" +#include "mallocvar.h" +#include "shhopt.h" +#include "nstring.h" +#include "pnm.h" + + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *inputFilespec; /* Filespec of input file */ + unsigned int width; + unsigned int height; + const char * dump; +}; + + + +static void +parseCommandLine (int argc, char ** argv, + struct cmdlineInfo *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 option_def_index; + + unsigned int widthSpec, heightSpec, dumpSpec, sizeSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "dump", OPT_STRING, + &cmdlineP->dump, &dumpSpec, 0); + OPTENT3(0, "width", OPT_UINT, + &cmdlineP->width, &widthSpec, 0); + OPTENT3(0, "height", OPT_UINT, + &cmdlineP->height, &heightSpec, 0); + OPTENT3(0, "size", OPT_FLAG, + NULL, &sizeSpec, 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 */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0 ); + /* Uses and sets argc, argv, and some of *cmdline_p and others. */ + + if (!widthSpec) + cmdlineP->width = 3; + + if (!heightSpec) + cmdlineP->height = 3; + + if (!dumpSpec) + cmdlineP->dump = NULL; + + if (sizeSpec) { + /* -size is strictly for backward compatibility. This program + used to use a different command line processor and had + irregular syntax in which the -size option had two values, + e.g. "-size <width> <height>" And the options had to go + before the arguments. So an old pnmsmooth command looks to us + like a command with the -size flag option and the first two + arguments being the width and height. + */ + + if (widthSpec || heightSpec) + pm_error("-size is obsolete. Use -width and -height instead"); + + if (argc-1 > 3) + pm_error("Too many arguments. With -size, there are at most " + "3 arguments."); + else if (argc-1 < 2) + pm_error("Not enough arguments. With -size, the first two " + "arguments are width and height"); + else { + cmdlineP->width = atoi(argv[1]); + cmdlineP->height = atoi(argv[2]); + + if (argc-1 < 3) + cmdlineP->inputFilespec = "-"; + else + cmdlineP->inputFilespec = argv[3]; + } + } else { + if (argc-1 > 1) + pm_error("Program takes at most one argument: the input file " + "specification. " + "You specified %d arguments.", argc-1); + if (argc-1 < 1) + cmdlineP->inputFilespec = "-"; + else + cmdlineP->inputFilespec = argv[1]; + } + if (cmdlineP->width % 2 != 1) + pm_error("The convolution matrix must have an odd number of columns. " + "You specified %u", cmdlineP->width); + + if (cmdlineP->height % 2 != 1) + pm_error("The convolution matrix must have an odd number of rows. " + "You specified %u", cmdlineP->height); +} + + + +static void +writeConvolutionImage(FILE * const cofp, + unsigned int const cols, + unsigned int const rows, + int const format) { + + xelval const convmaxval = rows * cols * 2; + /* normalizing factor for our convolution matrix */ + xelval const g = rows * cols + 1; + /* weight of all pixels in our convolution matrix */ + int row; + xel *outputrow; + + if (convmaxval > PNM_OVERALLMAXVAL) + pm_error("The convolution matrix is too large. " + "Width x Height x 2\n" + "must not exceed %d and it is %d.", + PNM_OVERALLMAXVAL, convmaxval); + + pnm_writepnminit(cofp, cols, rows, convmaxval, format, 0); + outputrow = pnm_allocrow(cols); + + for (row = 0; row < rows; ++row) { + unsigned int col; + for (col = 0; col < cols; ++col) + PNM_ASSIGN1(outputrow[col], g); + pnm_writepnmrow(cofp, outputrow, cols, convmaxval, format, 0); + } + pnm_freerow(outputrow); +} + + + +static void +runPnmconvol(const char * const inputFilespec, + const char * const convolutionImageFilespec) { + + /* fork a Pnmconvol process */ + pid_t rc; + + rc = fork(); + if (rc < 0) + pm_error("fork() failed. errno=%d (%s)", errno, strerror(errno)); + else if (rc == 0) { + /* child process executes following code */ + + execlp("pnmconvol", + "pnmconvol", convolutionImageFilespec, inputFilespec, + NULL); + + pm_error("error executing pnmconvol command. errno=%d (%s)", + errno, strerror(errno)); + } else { + /* This is the parent */ + pid_t const childPid = rc; + + int status; + + /* wait for child to finish */ + while (wait(&status) != childPid); + } +} + + + +int +main(int argc, char ** argv) { + + struct cmdlineInfo cmdline; + FILE * convFileP; + const char * tempfileName; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + if (cmdline.dump) + convFileP = pm_openw(cmdline.dump); + else + pm_make_tmpfile(&convFileP, &tempfileName); + + writeConvolutionImage(convFileP, cmdline.width, cmdline.height, + PGM_FORMAT); + + pm_close(convFileP); + + if (cmdline.dump) { + /* We're done. Convolution image is in user's file */ + } else { + runPnmconvol(cmdline.inputFilespec, tempfileName); + + unlink(tempfileName); + strfree(tempfileName); + } + return 0; +} diff --git a/editor/pnmstitch.c b/editor/pnmstitch.c new file mode 100644 index 00000000..61f02a04 --- /dev/null +++ b/editor/pnmstitch.c @@ -0,0 +1,2408 @@ +/* + * Copyright (c) 2002 Mark Salyzyn + * All rights reserved. + * + * TERMS AND CONDITIONS OF USE + * + * Redistribution and use in source form, with or without modification, are + * permitted provided that redistributions of source code must retain the + * above copyright notice, this list of conditions and the following + * disclaimer. + * + * This software is provided `as is' by Mark Salyzyn and any express or implied + * warranties, including, but not limited to, the implied warranties of + * merchantability and fitness for a particular purpose, are disclaimed. In no + * event shall Mark Salyzyn be liable for any direct, indirect, incidental, + * special, exemplary or consequential damages (including, but not limited to, + * procurement of substitute goods or services; loss of use, data, or profits; + * or business interruptions) however caused and on any theory of liability, + * whether in contract, strict liability, or tort (including negligence or + * otherwise) arising in any way out of the use of this software, even if + * advised of the possibility of such damage. + * + * Any restrictions or encumberances added to this source code or derivitives, + * is prohibited. + * + * Name: pnmstitch.c + * Description: Automated panoramic stitcher. + * Many digital Cameras have a panorama mode where they hold on to + * the right hand side of an image, shifted to the left hand side of their + * view screen for subsequent pictures, facilitating the manual stitching + * up of a panoramic shot. These same cameras are shipped with software + * to manually or automatically stitch images together into the composite + * image. However, these programs are dedicated for a specific OS. + * In the pnmstitch program, it analyzes the match between the images, + * generates a transform, processes the transform on the images, and + * blends the overlapping regions. In addition, there is an output filter + * to process automatic cropping of the resultant image. + * The stitching software here works by constraining the right + * hand side of the right hand image as `fixed' per-se, after offset + * evaluation and only the left hand side of the right hand image is + * mangled. Thus, the algorithm is optimized for stitching a right hand + * image to the right hand half (half being a loose term) of the left hand + * image. + * Author: Mark Salyzyn <mark@bohica.net> June 2002 + * Version: 0.0.4 + * + * Modifications: 0.0.4 July 31 2002 Mark Salyzyn <mark@bohica.net> + * & Bryan Henderson <bryanh@giraffe-data.com> + * - FreeBSD port. + * - merge changes to incorporate into netpbm tree. + * Modifications: 0.0.3 July 27 2002 Mark Salyzyn <mark@bohica.net> + * & "George M. Sipe" <geo@sipe.org> + * - Deal with subtle differences between BSD and GNU getopt + * facilitating the Linux port. + * Modifications: 0.0.2 July 25 2002 Mark Salyzyn <mark@bohica.net> + * - RotateSliver needs to use higher resolution match. + * - RotateCrop code interfered with StraightThrough code + * resulting in an incorrect pnm image output. + * Modifications: 0.0.1 July 18 2002 Mark Salyzyn <mark@bohica.net> + * - Added BiLinearSliver, RotateSliver and HorizontalCrop + * + * ToDo: + * - Split this into multiple files ... nah, keep it in one to + * keep it from polluting the netpbm tree. + * - Add and refine the videorbits algorithm. One piece of public + * domain software that is pnm aware is called videorbits. It + * needs considerably more overlap than the Digital Cameras set + * up (of course, anyone can to a panorama with more overlap + * with our without the feature) as it uses what is called video + * flow to generate the match. It is designed more for a series + * of images from a video camera. videorbits has three programs, + * one generates the transform, the next processes the + * transform, and the final blends the images together much as + * this one piece program does. + * - speedups in matching algorithm + * - refinement in accuracy of matching algorithm + * - Add RotateCrop filter algorithm (in-memory copy of image, + * detect least loss horizontal crop on a rotated image). + * - pnmstitch should be generalized to handle transformation + * occuring on the left image, currently it blends assuming + * that there is no transformation effects on the left image. + * - user selectable blending algorithms? + */ + +#define _BSD_SOURCE 1 /* Make sure strdup() is in string.h */ +#define _XOPEN_SOURCE 500 /* Make sure strdup() is in string.h */ + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <limits.h> +#include <math.h> + +#include "pm_c_util.h" +#include "shhopt.h" +#include "nstring.h" +#include "mallocvar.h" +#include "pam.h" + +/* + * Structures + */ + +/* + * Image structure + */ +typedef struct { + const char * name; /* File Name */ + struct pam pam; /* netpbm image description */ + tuple ** tuple; /* in-memory copy of image */ +} Image; + +/* + * Output class + * The following methods and data allocations are used for output filter. + */ +typedef struct output { + /* name */ + const char * Name; + /* methods */ + bool (* Alloc)(struct output * me, const char * file, + unsigned int width, unsigned int height, + struct pam * prototype); + void (* DeAlloc)(struct output * me); + tuple *(* Row)(struct output * me, unsigned row); + void (* FlushRow)(struct output * me, unsigned row); + void (* FlushImage)(struct output * me); + /* data */ + Image * image; + void * extra; +} Output; + +extern Output OutputMethods[]; + +/* + * Stitching class + * The following methods and data allocations are used for operations + * surrounding stitching of an image. + */ +typedef struct stitcher { + /* name */ + const char * Name; + /* methods */ + bool (* Alloc)(struct stitcher *me); + void (* DeAlloc)(struct stitcher *me); + void (* Constrain)(struct stitcher *me, int x, int y, + int width, int height); + /* Set transformation parameter constraints. This affects the + function of a future 'Match' method execution. + */ + bool (* Match)(struct stitcher *me, Image * Left, Image * Right); + /* Determine the transformation parameters for the stitching. + I.e. determine the parameters that affect future invocations + of the transformation methods below. You must execute a + 'Match' before executing any of the transformation methods. + */ + /*----------------------------------------------------------------------- + The transformation methods answer the question, "Which pixel in the left + image and which pixel in the right image contribute to the pixel at + Column X, Column Y of the output? + + If there is no pixel in the left image that contributes to the output + pixel in question, the methods return column or row numbers outside + the bounds of the left image (possibly negative). Likewise for the + right image. + */ + float (* XLeft)(struct stitcher *me, int x, int y); + /* column number of the pixel from the left image */ + float (* YLeft)(struct stitcher *me, int x, int y); + /* row number of the pixel from the left image */ + float (* XRight)(struct stitcher *me, int x, int y); + /* column number of the pixel from the right image */ + float (* YRight)(struct stitcher *me, int x, int y); + /* row number of the pixel from the left image */ + /*----------------------------------------------------------------------*/ + /* Output methods */ + void (* Output)(struct stitcher *me, FILE * fp); + /* private data */ + int x, y, width, height; + /* For a Linear Sliver stitcher, 'x' and 'y' are simply the offset you + add to an output location to get the location in the right image of the + pixel that corresponds to that output pixel. + */ + float * parms; +} Stitcher; + +extern Stitcher StitcherMethods[]; + +/* + * Prototypes + */ +static int pnmstitch(const char * const left, + const char * const right, + const char * const out, + int const x, + int const y, + int const width, + int const height, + const char * const stitcher, + const char * const filter); + +struct cmdlineInfo { + /* + * All the information the user supplied in the command line, + * in a form easy for the program to use. + */ + const char * leftFilespec; /* '-' if stdin */ + const char * rightFilespec; /* '-' if stdin */ + const char * outputFilespec; /* '-' if stdout */ + const char * stitcher; + const char * filter; + int width; + int height; + int xrightpos; + int yrightpos; + unsigned int verbose; +}; + +static char minus[] = "-"; + +static void +parseCommandLine ( int argc, char ** argv, + struct cmdlineInfo *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 = malloc( 100*sizeof( optEntry ) ); + /* Instructions to optParseOptions3 on how to parse our options. + */ + optStruct3 opt; + + unsigned int option_def_index; + + char *outputOpt; + unsigned int widthSpec, heightSpec, outputSpec, + xrightposSpec, yrightposSpec, stitcherSpec, filterSpec; + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "width", OPT_UINT, &cmdlineP->width, + &widthSpec, 0); + OPTENT3(0, "height", OPT_UINT, &cmdlineP->height, + &heightSpec, 0); + OPTENT3(0, "verbose", OPT_FLAG, NULL, + &cmdlineP->verbose, 0 ); + OPTENT3(0, "output", OPT_STRING, &outputOpt, + &outputSpec, 0); + OPTENT3(0, "xrightpos", OPT_UINT, &cmdlineP->xrightpos, + &xrightposSpec, 0); + OPTENT3(0, "yrightpos", OPT_UINT, &cmdlineP->yrightpos, + &yrightposSpec, 0); + OPTENT3(0, "stitcher", OPT_STRING, &cmdlineP->stitcher, + &stitcherSpec, 0); + OPTENT3(0, "filter", OPT_STRING, &cmdlineP->filter, + &filterSpec, 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 */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (!widthSpec) { + cmdlineP->width = INT_MAX; + } + if (!heightSpec) { + cmdlineP->height = INT_MAX; + } + if (!xrightposSpec) { + cmdlineP->xrightpos = INT_MAX; + } + if (!yrightposSpec) { + cmdlineP->yrightpos = INT_MAX; + } + if (!stitcherSpec) { + cmdlineP->stitcher = "BiLinearSliver"; + } + if (!filterSpec) { + cmdlineP->filter = "StraightThrough"; + } + + if (argc-1 > 3) { + pm_error("Program takes at most three arguments: left, right, and " + "output file specifications. You specified %d", argc-1); + /* NOTREACHED */ + } else { + if (argc-1 == 0) { + cmdlineP->leftFilespec = minus; + cmdlineP->rightFilespec = minus; + } else if (argc-1 == 1) { + cmdlineP->leftFilespec = minus; + cmdlineP->rightFilespec = argv[1]; + } else { + cmdlineP->leftFilespec = argv[1]; + cmdlineP->rightFilespec = argv[2]; + } + if (argc-1 == 3 && outputSpec) { + pm_error("You cannot specify --output and also name the " + "output file with the 3rd argument."); + /* NOTREACHED */ + } else if (argc-1 == 3) { + cmdlineP->outputFilespec = argv[3]; + } else if (outputSpec) { + cmdlineP->outputFilespec = outputOpt; + } else { + cmdlineP->outputFilespec = minus; + } + } +} /* parseCommandLine() - end */ + +static int verbose; + +/* + * Parse the command line, call pnmstitch to perform work. + */ +int +main (int argc, char **argv) +{ + struct cmdlineInfo cmdline; + + pnm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + verbose = cmdline.verbose; + + return pnmstitch (cmdline.leftFilespec, + cmdline.rightFilespec, + cmdline.outputFilespec, + cmdline.xrightpos, + cmdline.yrightpos, + cmdline.width, + cmdline.height, + cmdline.stitcher, + cmdline.filter); +} /* main() - end */ + + + +/* + * allocate a clear image structure. + */ +static Image * +allocate_image(void) +{ + Image * retVal; + + MALLOCVAR(retVal); + + if (retVal != NULL) + memset (retVal, 0, (unsigned)sizeof(Image)); + + return retVal; +} + + + +/* + * free an image structure. + */ +static void +free_image(Image * image) +{ + if (image->name) { + strfree(image->name); + image->name = NULL; + } + if (image->tuple) { + pnm_freepamarray(image->tuple, &image->pam); + image->tuple = NULL; + } + if (image->pam.file) { + fclose (image->pam.file); + image->pam.file = NULL; + } + free (image); +} + + + +static void +openWithPossibleExtension(const char * const baseName, + FILE ** const ifPP, + const char ** const filenameP) { + + /* list of possible extensions for input file */ + const char * const extlist[] = { + "", ".pnm", ".pam", ".pgm", ".pbm", ".ppm" + }; + + FILE * ifP; + unsigned int extIndex; + + ifP = NULL; /* initial value -- no file opened yet */ + + for (extIndex = 0; extIndex < ARRAY_SIZE(extlist) && !ifP; ++extIndex) { + + const char * trialName; + + asprintfN(&trialName, "%s%s", baseName, extlist[extIndex]); + + ifP = fopen(trialName, "rb"); + + if (ifP) + *filenameP = trialName; + else + strfree(trialName); + } + if (!ifP) + pm_error ("Failed to open input file named '%s' " + "or '%s' with one of various common extensions.", + baseName, baseName); + + *ifPP = ifP; +} + + + +/* + * Create an image object for the PNM/PAM image in the file name 'name' + * This includes reading the entire image. + */ +static Image * +readinit(const char * const name) +{ + Image * const image = allocate_image(); + Image * retVal; + + if (image == NULL) + retVal = NULL; + else { + FILE * ifP; + + if (strcmp(name,minus) == 0) { + ifP = stdin; + image->name = strdup("<stdin>"); + } else { + openWithPossibleExtension(name, &ifP, &image->name); + } + image->tuple = pnm_readpam(ifP, &(image->pam), + PAM_STRUCT_SIZE(tuple_type)); + fclose (ifP); + image->pam.file = NULL; + + if (image->tuple == NULL) { + free_image(image); + retVal = NULL; + } else + retVal = image; + } + return retVal; +} /* readinit() - end */ + +/* + * Prepare an image to be output. + * Too bad we can't help them and add a .pnm on the filename, + * since this would be `bad'. on readinit we can check if the .pnm + * needs to be added ... + */ +static bool +writeinit(Image * image) +{ + if (strcmp(image->name,minus) == 0) { + image->pam.file = stdout; + strfree(image->name); + image->name = strdup("<stdout>"); + } else { + image->pam.file = pm_openw(image->name); + } + return TRUE; +} /* writeinit() - end */ + +/* + * Compare a subimage to an image. + * The most time consuming actions surround this subroutine. + * Return the magnitude of the difference between the specified region + * in image 'left' and the specified region in image 'right'. + * The magnitude is defined as the sum of the squares of the differences + * between intensities of each of the 3 colors over all the pixels. + * + * The region in the left image is the rectangle with top left corner at + * Column 'lx', Row 'ly', with dimensions 'width' columns by 'height' + * rows. The region in the right image is a rectangle the same dimensions + * with upper left corner at Column 'rx', Row 'ry'. + * + * Caller must ensure that the regions indicated are entirely within the + * their respective images. + */ +static unsigned long +regionDifference(Image * left, + int lx, + int ly, + Image * right, + int rx, + int ry, + int width, + int height) +{ + unsigned long total; + unsigned row; + + total = 0; /* initial value */ + + for (row = 0; row < height; ++row) { + unsigned column; + + for (column = 0; column < width; ++column) { + unsigned plane; + + for (plane = 0; plane < left->pam.depth; ++plane) { + sample const leftSample = left->tuple[row][column][plane]; + sample const rightSample = right->tuple[row][column][plane]; + total += SQR(leftSample - rightSample); + } + } + } + return total; +} + + + +/* + * Generate a recent sorted histogram of best matches. + */ +typedef struct { + unsigned long total; + int x; + int y; +} Best; +/* Arbitrary, except 9 points surrounding a hot spot plus one layer more */ +#define NUM_BEST 9 + +/* + * Allocate the Best structure. + */ +static Best * +allocate_best(void) +{ + Best * retVal; + + MALLOCARRAY(retVal, NUM_BEST); + + if (retVal != NULL) { + unsigned int i; + for (i = 0; i < NUM_BEST; ++i) { + retVal[i].total = ULONG_MAX; + retVal[i].x = INT_MAX; + retVal[i].y = INT_MAX; + } + } + return retVal; +} + +/* + * Free the Best structure + */ +#define free_best(best) free(best) + +/* + * Placement helper for the Best structure. + */ +static void +update_best(Best * best, unsigned long total, int x, int y) +{ + int i; + if (best[NUM_BEST-1].total <= total) { + return; + } + for (i = NUM_BEST - 1; i > 0; --i) { + if (best[i-1].total < total) { + break; + } + best[i] = best[i-1]; + } + best[i].total = total; + best[i].x = x; + best[i].y = y; +} + +/* + * Print helper for the Best structure. + */ +static void +pr_best(Best * const best) +{ + int i; + if (best != (Best *)NULL) + for (i = 0; i < NUM_BEST; ++i) { + fprintf (stderr, " (%d,%d)%lu", + best[i].x, best[i].y, best[i].total); + } +} /* pr_best() - end */ + +static void * +findObject(const char * const name, void * start, unsigned size) +{ + char ** object; + char ** best; + unsigned length; + + if (name == (char *)NULL) { + return start; + } + for (length = 0, best = (char **)NULL, object = (char **)start; + *object != (char *)NULL; + object = (char **)(((char *)object) + size)) { + const char * np = name; + char * op = *object; + unsigned matched = 0; + + /* case insensitive match */ + while ((*np != '\0') && (*op != '\0') + && ((*np == *op) || (tolower(*np) == tolower(*op)))) { + ++np; + ++op; + ++matched; + } + if ((*np == '\0') && (*op == '\0')) { + break; + } + if ((matched >= length) && (*np == '\0')) { + if (matched == length) { + best = (char **)NULL; + } else { + best = object; + } + length = matched; + } + } + if (*object == (char *)NULL) { + object = best; + } + if (object == (char **)NULL) { + fprintf (stderr, + "Unknown driver \"%s\". available drivers are:\n", name); + for (object = (char **)start; + *object != (char *)NULL; + object = (char **)(((char *)object) + size)) { + fprintf (stderr, "\t%s%s\n", *object, + (object == (char **)start) ? " (default)" : ""); + } + } + return object; +} + +/* + * The general wrapper for both the Output and the Stitcher algorithms. + */ + +/* Determine the mask corners for both Left and Right images */ +static void +determineMaskCorners(Stitcher * Stitch, + Image * Left, + Image * Right, + int xp[], + int yp[]) +{ + int i; + + xp[0] = xp[1] = xp[4] = xp[5] = 0; + yp[0] = yp[2] = yp[4] = yp[6] = 0; + xp[2] = xp[3] = Left->pam.width; + yp[1] = yp[3] = Left->pam.height; + xp[6] = xp[7] = Right->pam.width; + yp[5] = yp[7] = Right->pam.height; + for (i = 0; i < 8; ++i) { + int x, y, xx, yy, count = 65536; /* max iterations */ + float (*X)(Stitcher *me, int x, int y); + float (*Y)(Stitcher *me, int x, int y); + + if (i < 4) { + X = Stitch->XLeft; + Y = Stitch->YLeft; + } else { + X = Stitch->XRight; + Y = Stitch->YRight; + } + x = xp[i]; + y = yp[i]; + /* will not work if rotated 90o or if gain > 10 */ + do { + xx = ((*X)(Stitch, xp[i], yp[i]) + 0.5) - x; + if (xx < 0) { + if (xx > -100) { + ++xp[i]; + } else { + xp[i] -= xx / 10; + } + } else if (xx > 0) { + if (xx < 100) { + --xp[i]; + } else { + xp[i] -= xx / 10; + } + } + yy = ((*Y)(Stitch, xp[i], yp[i]) + 0.5) - y; + if (yy < 0) { + if (yy > -100) { + ++yp[i]; + } else { + yp[i] -= yy / 10; + } + } else if (yy > 0) { + if (yy < 100) { + --yp[i]; + } else { + yp[i] -= yy / 10; + } + } + } while (((xx != 0) || (yy != 0)) && (--count != 0)); + } + if (verbose) { + (*(Stitch->Output))(Stitch, stderr); + if (verbose > 2) { + static char quotes[] = "'\0\0\"\0\0'\"\0\"\""; + fprintf (stderr, " Left:"); + for (i = 0; i < 8; ++i) { + if (i == 4) { + fprintf (stderr, "\n Right:"); + } + fprintf (stderr, " x%s,y%s=%d,%d", + "es[(i%4)*3], "es[(i%4)*3], + xp[i], yp[i]); + } + } + } +} /* determineMaskCorners() - end */ + +static void +calculateXyWidthHeight(int xp[], + int yp[], + int * const xP, + int * const yP, + int * const widthP, + int * const heightP) +{ + int x, y, width, height, i; + + /* Calculate generic x,y left top corner, and the width and height */ + x = xp[0]; + y = yp[0]; + width = height = 0; + for (i = 1; i < 8; ++i) { + if (xp[i] < x) { + width += x - xp[i]; + x = xp[i]; + } else if ((x + width) < xp[i]) { + width = xp[i] - x; + } + if (yp[i] < y) { + height += y - yp[i]; + y = yp[i]; + } else if ((y + height) < yp[i]) { + height = yp[i] - y; + } + } + *xP = x; *yP = y; + *widthP = width; *heightP = height; +} /* calculateXyWidthHeight() - end */ + +static void +printPlan(int xp[], int yp[], Image * Left, Image * Right) +{ + /* Calculate Left image transformed bounds */ + int X, Y, W, H, i; + + X = xp[0]; + Y = yp[0]; + W = H = 0; + for (i = 1; i < 4; ++i) { + if (xp[i] < X) { + W += X - xp[i]; + X = xp[i]; + } else if ((X + W) < xp[i]) { + W = xp[i] - X; + } + if (yp[i] < Y) { + H += Y - yp[i]; + Y = yp[i]; + } else if ((Y + H) < yp[i]) { + H = yp[i] - Y; + } + } + fprintf (stderr, + "%s[%u,%u=>%d,%d](%d,%d)", + Left->name, Left->pam.width, Left->pam.height, + W, H, X, Y); + X = xp[i]; + Y = yp[i]; + W = H = 0; + for (++i; i < 8; ++i) { + if (xp[i] < X) { + W += X - xp[i]; + X = xp[i]; + } else if ((X + W) < xp[i]) { + W = xp[i] - X; + } + if (yp[i] < Y) { + H += Y - yp[i]; + Y = yp[i]; + } else if ((Y + H) < yp[i]) { + H = yp[i] - Y; + } + } + fprintf (stderr, + "+%s[%u,%u=>%d,%d](%d,%d)", + Right->name, Right->pam.width, Right->pam.height, + W, H, X, Y); +} /* printPlan() - end */ + + + +static void +stitchOnePixel(Image * const Left, + Image * const Right, + struct pam const outpam, + int const row, + int const column, + int const y, + int const right_row, + int const right_column, + unsigned * const firstRightP, + tuple const outPixel) { + + unsigned plane; + + for (plane = 0; plane < outpam.depth; ++plane) { + sample leftPixel, rightPixel; + /* Left `mix' is easy to find */ + leftPixel = (column < Left->pam.width) + ? (y < 0) + ? ((row < -y) || (row >= (Left->pam.height - y))) + ? 0 + : Left->tuple[row + y][column][plane] + : (row < Left->pam.height) + ? Left->tuple[row][column][plane] + : 0 + : 0; + rightPixel = 0; + if (right_column >= 0) { + rightPixel = Right->tuple[right_row][right_column][plane]; + if ((rightPixel > 0) && (*firstRightP == 0)) + *firstRightP = column; + } + if (leftPixel == 0) { + leftPixel = rightPixel; + } else if ((*firstRightP <= column) + && (column < Left->pam.width) + && (rightPixel > 0)) { + /* blend 7/8 over half of stitch */ + int const w = Left->pam.width - *firstRightP; + if (column < (*firstRightP + w/2)) { + int const v = (w * 4) / 7; + leftPixel = (sample)( + ((leftPixel + * (unsigned long)(*firstRightP + v - column)) + + (rightPixel + * (unsigned long)(column - *firstRightP))) + / (unsigned long)v); + } else { + int const v = w * 4; + leftPixel = (sample)( + ((leftPixel + * (unsigned long)(Left->pam.width - column)) + + (rightPixel + * (unsigned long)(column - Left->pam.width + v))) + / (unsigned long)v); + } + } + outPixel[plane] = leftPixel; + } +} + + + +static void +stitchOneRow(Image * const Left, + Image * const Right, + Output * const Out, + Stitcher * const Stitch, + int const row, + int const y) { + + /* + * We scale the overlap of the left and right images, we need to + * discover and hold on to the left edge of the right image to + * determine the rate at which we blend. Most (7/8) of the blending + * occurs in the first half of the overlap to reduce the occurences + * of blending artifacts. If there is no overlap, the image present + * has no blending activity, this is determined by the black + * background and is not through an alpha layer to help reduce + * storage needs. The algorithm below is complicated most by + * the blending determinations, overlapping a left untransformed + * image with a right transformed image with a black background is + * all that remains. + */ + /* + * Normalize transformation against origin, the + * transformation algorithm was in reference to the right + * hand side of the left hand image before. + */ + tuple * const Row = (*(Out->Row))(Out,row); + + unsigned column, firstRight; + + firstRight = 0; /* initial value */ + + for (column = 0; column < Out->image->pam.width; ++column) { + int right_row, right_column; + + right_row = -1; + right_column = (*(Stitch->XRight))(Stitch, column, + (y < 0) ? (row + y) : row) + 0.5; + if ((0 <= right_column) + && (right_column < Right->pam.width)) { + right_row = (*(Stitch->YRight))(Stitch, column, + (y < 0) ? (row + y) : row) + 0.5; + if ((right_row < 0) + || (Right->pam.height <= right_row)) { + right_column = -1; + right_row = -1; + } + } else + right_column = -1; + + /* Create the pixel at column 'column' of row 'row' of the + output 'Out': Row[column]. + */ + stitchOnePixel(Left, Right, Out->image->pam, row, column, y, + right_row, right_column, &firstRight, Row[column]); + } +} + + + +static void +stitchit(Image * const Left, + Image * const Right, + const char * const outfilename, + const char * const filter, + Stitcher * const Stitch, + int * const retvalP) { + + Output * const Out = findObject(filter, &OutputMethods[0], + sizeof(OutputMethods[0])); + unsigned row; + int xp[8], yp[8], x, y, width, height; + + if ((Out == (Output *)NULL) || (Out->Name == (char *)NULL)) + *retvalP = -2; + else { + if (verbose) + fprintf (stderr, "Selected %s output filter algorithm\n", + Out->Name); + + /* Determine the mask corners for both Left and Right images */ + determineMaskCorners(Stitch, Left, Right, xp, yp); + + /* Output the combined images */ + + /* Calculate generic x,y left top corner, and the width and height */ + calculateXyWidthHeight(xp, yp, &x, &y, &width, &height); + + if (verbose) + printPlan(xp, yp, Left, Right); + + if (!(*(Out->Alloc))(Out, outfilename, width, height, &Left->pam)) + *retvalP = -9; + else { + if (verbose) { + fprintf (stderr, + "=%s[%u,%u=>%d,%d](%d,%d)\n", + Out->image->name, Out->image->pam.width, + Out->image->pam.height, width, height, x, y); + } + for (row = 0; row < Out->image->pam.height; row++) { + /* Generate row number 'row' of the output image 'Out' */ + stitchOneRow(Left, Right, Out, Stitch, row, y); + (*(Out->FlushRow))(Out,row); + } + (*(Out->FlushImage))(Out); + (*(Out->DeAlloc))(Out); + + *retvalP = 0; + } + } +} + + + +static int +pnmstitch(const char * const leftfilename, + const char * const rightfilename, + const char * const outfilename, + int const reqx, + int const reqy, + int const reqWidth, + int const reqHeight, + const char * const stitcher, + const char * const filter) +{ + Stitcher * const Stitch = findObject(stitcher, &StitcherMethods[0], + sizeof(StitcherMethods[0])); + Image * Left; + Image * Right; + int retval; + + if ((Stitch == (Stitcher *)NULL) || (Stitch->Name == (char *)NULL)) + retval = -1; + else { + if (verbose) + fprintf (stderr, "Selected %s stitcher algorithm\n", + Stitch->Name); + + /* Left hand image read into memory */ + Left = readinit(leftfilename); + if (Left == NULL) + retval = -3; + else { + /* Right hand image read into memory */ + Right = readinit(rightfilename); + if (Right == NULL) + retval = -4; + else { + if (Left->pam.depth != Right->pam.depth) { + fprintf(stderr, "Images should have matching depth. " + "The left image has depth %d, " + "while the right has depth %d.", + Left->pam.depth, Right->pam.depth); + retval = -5; + } else if (Left->pam.maxval != Right->pam.maxval) { + fprintf (stderr, + "Images should have matching maxval. " + "The left image has maxval %u, " + "while the right has maxval %u.", + (unsigned)Left->pam.maxval, + (unsigned)Right->pam.maxval); + retval = -6; + } else if ((*(Stitch->Alloc))(Stitch) == FALSE) + retval = -7; + else { + (*(Stitch->Constrain))(Stitch, reqx, reqy, + reqWidth, reqHeight); + + if ((*(Stitch->Match))(Stitch, Left, Right) == FALSE) + retval = -8; + else + stitchit(Left, Right, outfilename, filter, Stitch, + &retval); + } + free_image(Right); + } + free_image(Left); + } + } + return retval; +} + + + +/* Output Methods */ + +/* Helper methods */ + +static void +OutputDeAlloc(Output * me) +{ + if (me->image != (Image *)NULL) { + /* Free up resources */ + free_image (me->image); + me->image = (Image *)NULL; + } + if (me->extra != (void *)NULL) { + free (me->extra); + me->extra = (void *)NULL; + } +} /* OutputDeAlloc() - end */ + +static bool +OutputAlloc(Output * const me, + const char * const file, + unsigned int const width, + unsigned int const height, + struct pam * const prototype) +{ + /* Output the combined images */ + me->extra = (void *)NULL; + me->image = allocate_image(); + if (me->image == (Image *)NULL) { + return FALSE; + } + me->image->pam = *prototype; + me->image->pam.width = width; + me->image->pam.height = height; + /* Give the output a name */ + me->image->name = strdup(file); + /* Initialize output arrays */ + if (writeinit(me->image) == FALSE) { + OutputDeAlloc(me); + return FALSE; + } + return TRUE; +} /* OutputAlloc() - end */ + +/* StraightThrough output method */ + +static void +StraightThroughDeAlloc(Output * me) +{ + /* Trick the proper freeing of resouces on the Output Image */ + me->image->pam.height = 1; + OutputDeAlloc(me); +} /* StraightThroughDeAlloc() - end */ + +static bool +StraightThroughAlloc(Output * const me, + const char * const file, + unsigned int const width, + unsigned int const height, + struct pam * const prototype) +{ + if (OutputAlloc(me, file, width, height, prototype) == FALSE) { + StraightThroughDeAlloc(me); + } + /* Trick the proper allocation of resouces on the Output Image */ + me->image->pam.height = 1; + me->image->tuple = pnm_allocpamarray(&me->image->pam); + if (me->image->tuple == (tuple **)NULL) { + StraightThroughDeAlloc(me); + return FALSE; + } + me->image->pam.height = height; + pnm_writepaminit(&me->image->pam); + return TRUE; +} /* StraightThroughAlloc() - end */ + +static tuple * +StraightThroughRow(Output * me, unsigned row) +{ + UNREFERENCED_PARAMETER(row); + return me->image->tuple[0]; +} /* StraightThroughRow() - end */ + +static void +StraightThroughFlushRow(Output * me, unsigned row) +{ + UNREFERENCED_PARAMETER(row); + if (me->image != (Image *)NULL) { + pnm_writepamrow(&me->image->pam, me->image->tuple[0]); + } +} /* StraightThroughFlushRow() - end */ + +static void +StraightThroughFlushImage(Output * me) +{ + UNREFERENCED_PARAMETER(me); +} /* StraightThroughFlushImage() - end */ + +/* Horizontal Crop output method */ + +#define HorizontalCropDeAlloc StraightThroughDeAlloc + +typedef struct { + int state; + int lostInSpace; +} HorizontalCropExtra; + +static bool +HorizontalCropAlloc(Output * const me, + const char * const file, + unsigned int const width, + unsigned int const height, + struct pam * const prototype) +{ + unsigned long pos; + + if (StraightThroughAlloc(me, file, width, height, prototype) == FALSE) { + return FALSE; + } + me->extra = (void *)malloc(sizeof(HorizontalCropExtra)); + if (me->extra == (void *)NULL) { + HorizontalCropDeAlloc(me); + return FALSE; + } + memset (me->extra, 0, sizeof(HorizontalCropExtra)); + /* Test if we can seek, important since we rewrite the header */ + pos = ftell(me->image->pam.file); + if ((fseek(me->image->pam.file, 1L, SEEK_SET) != 0) + || (ftell(me->image->pam.file) != 1L)) { + fprintf (stderr, "%s needs to output to a seekable entity\n", + me->Name); + } + (void)fseek(me->image->pam.file, pos, SEEK_SET); + return TRUE; +} /* HorizontalCropAlloc() - end */ + +#define HorizontalCropRow StraightThroughRow + +static void +HorizontalCropFlushRow(Output * me, unsigned row) +{ + unsigned column; + unsigned threshold; +# define HorizontalCropThreshold 4 + UNREFERENCED_PARAMETER(row); + + if (me->image == (Image *)NULL) { + return; + } + if (((HorizontalCropExtra *)(me->extra))->state == 2) { + ((HorizontalCropExtra *)(me->extra))->lostInSpace++; + return; + } + /* Any pitch black pixels? */ + threshold = HorizontalCropThreshold; + for (column = 0; column < me->image->pam.width; ++column) { + unsigned plane = 0; + while (me->image->tuple[0][column][plane] == (sample)0) { + if (++plane >= me->image->pam.depth) { + if (--threshold == 0) { + if (((HorizontalCropExtra *)(me->extra))->state == 1) { + ((HorizontalCropExtra *)(me->extra))->state = 2; + } + ((HorizontalCropExtra *)(me->extra))->lostInSpace++; + return; + } + } + } + if (plane < me->image->pam.depth) { + threshold = HorizontalCropThreshold; + } + } + ((HorizontalCropExtra *)(me->extra))->state = 1; + pnm_writepamrow(&me->image->pam, me->image->tuple[0]); +} /* HorizontalCropFlushRow() - end */ + +static void +HorizontalCropFlushImage(Output * me) +{ + me->image->pam.height -= ((HorizontalCropExtra *)(me->extra))->lostInSpace; + if (verbose) { + fprintf (stderr, "%s has set image size to %d x %d\n", + me->Name, me->image->pam.width, me->image->pam.height); + } + if (fseek(me->image->pam.file, 0L, SEEK_SET) == 0) { + pnm_writepaminit(&me->image->pam); + } else { + fprintf (stderr, + "%s failed to seek to beginning to rewrite the header\n", + me->Name); + } +} /* HorizontalCropFlushImage() - end */ + +/* Rotate Crop output method */ + +#define RotateCropDeAlloc OutputDeAlloc + +static bool +RotateCropAlloc(Output * const me, + const char * const file, + unsigned int const width, + unsigned int const height, + struct pam * const prototype) +{ + if (OutputAlloc(me, file, width, height, prototype) == FALSE) { + RotateCropDeAlloc(me); + } + me->image->tuple = pnm_allocpamarray(&me->image->pam); + if (me->image->tuple == (tuple **)NULL) { + RotateCropDeAlloc(me); + return FALSE; + } + return TRUE; +} /* RotateCropAlloc() - end */ + +static tuple * +RotateCropRow(Output * me, unsigned row) +{ + return me->image->tuple[row]; +} /* RotateCropRow() - end */ + +static void +RotateCropFlushRow(Output * me, unsigned row) +{ + UNREFERENCED_PARAMETER(me); + UNREFERENCED_PARAMETER(row); +} /* RotateCropFlushRow() - end */ + +/* + * Algorithm under construction. + * + */ +static void +RotateCropFlushImage(Output * me) +{ + /* Cop Out for now ... */ + pnm_writepam(&me->image->pam, me->image->tuple); +} /* RotateCropFlushImage() - end */ + +/* Output Method Table */ + +Output OutputMethods[] = { + { "StraightThrough", StraightThroughAlloc, StraightThroughDeAlloc, + StraightThroughRow, StraightThroughFlushRow, + StraightThroughFlushImage }, + { "HorizontalCrop", HorizontalCropAlloc, HorizontalCropDeAlloc, + HorizontalCropRow, HorizontalCropFlushRow, HorizontalCropFlushImage }, + { "RotateCrop (unimplemented)", RotateCropAlloc, RotateCropDeAlloc, + RotateCropRow, RotateCropFlushRow, RotateCropFlushImage }, + { (char *)NULL } +}; + +/* Stitcher Methods */ + +/* These names are for the 8 parameters of a stitch, in any of the 3 + methods this program presently implements. Each is a subscript in + the parms[] array for the Stitcher object that represents a linear + stitching method. + + There are also other sets of names for the 8 parameters, such as + Rotate_a. I don't know why. Maybe historical. +*/ + +#define Sliver_A 0 +#define Sliver_B 1 +#define Sliver_C 2 +#define Sliver_D 3 +#define Sliver_xp 4 +#define Sliver_yp 5 +#define Sliver_xpp 6 +#define Sliver_ypp 7 + +/* Linear Stitcher Methods */ + +static void +LinearDeAlloc(Stitcher * me) +{ + if (me->parms != (float *)NULL) { + free (me->parms); + me->parms = (float *)NULL; + } +} /* LinearDeAlloc() - end */ + +static bool +LinearAlloc(Stitcher * me) +{ + bool retval; + + MALLOCARRAY(me->parms, 8); + if (me->parms == NULL) + retval = FALSE; + else { + /* Constraints unset */ + me->x = INT_MAX; + me->y = INT_MAX; + me->width = INT_MAX; + me->height = INT_MAX; + /* Unity transform matrix */ + me->parms[Sliver_A] = 1.0; + me->parms[Sliver_B] = 0.0; + me->parms[Sliver_C] = 0.0; + me->parms[Sliver_D] = 0.0; + me->parms[Sliver_xp] = 0.0; + me->parms[Sliver_yp] = 1.0; + me->parms[Sliver_xpp] = 0.0; + me->parms[Sliver_ypp] = 0.0; + retval = TRUE; + } + return retval; +} + + + +static void +LinearConstrain(Stitcher * me, int x, int y, int width, int height) +{ + me->x = x; + me->y = y; + me->width = width; + me->height = height; +} /* LinearConstrain() - end */ + +/* + * First pass is to find an approximate match. To do so, we take a + * width sliver of the left hand side of the right image and compare + * the sample to the left hand image. Accuracy is honored over speed. + * The image overlap is expected between 7/16 to 1/16 in the horizontal + * position, and a minumum of 5/8 in the vertical dimension. + * + * Blind alleys: + * - reduced resolution can match in totally wrong regions, + * as such it can not be used to improve the speed by + * getting close, then fine tuning at full resolution. + * - vector (color) average of sample matched to running + * vector average on left image in an attempt to improve + * positional accuracy of a reduced resolution image + * produced even more artifacts. + * - A complete boxed sliver did not find a minima, as for + * too large or too small of a square sample. heuristics + * show that it works between 1/128 to 1/16 of the total + * image dimension. Smaller, of course, improves speed, + * but has the possibility of less accuracy. + * + * Transformation parameters + * x=x.+a + * y=y'+b + * Where x,y represents the original point, and x.,y. + * represents the transformed point. Thus: + * + * Transformed image: + * ((x'+x")/2,(y'+y"-H)/2) ((x'+x"+2W)/2,(y'+y"-H)/2) + * ((x'+x")/2,(y'+y"+H)/2) ((x'+x"+2W)/2,(y'+y"+H)/2 + * + * Corresponding to Original (dot) image: + * (0,0) (Right->pam.width,0) + * (0,Right->pam.height) (Right->pam.width,Right->pam.height) + * + * Our matching data points are centered on x.=width/2, and + * scan for transformation results with a variety of y. values: + * x=a*width/2+by.+c*width*y./2+d + * y=e*width/2+fy.+g*width*y./2+h + * we set: + * A=b+c*width/2 + * B=a*width/2+d + * C=f+g*width/2 + * D=e*width/2+h + * thus simplifying to: + * x=Ay.+B + * y=Cy.+D + * adding in a weighting factor of w, the error equation is: + * 2 2 2 + * E(A,B,C,D)=w * ((Ay.+B-x) + (Cy.+D-y)) + * thus + * 2 + * dE(A)=2wy.(Ay.+B-x) => 0=A{wy.y. + B{wy. - {wy.x + * 2 + * dE(B)=2w(Ay.+B-x) => 0=A{wy. + B{w - {wx + * A=({wy.x{w-{wx{wy.)/({wy.y.{w-{wy.{wy.) + * B=({wx-A{wy.)/{w + * and + * 2 + * dE(C)=2wy.(Cy.+D-y) => 0=C{wy.y. + D{wy. - {wy.y + * 2 + * dE(D)=2w(Cy.+D-y) => 0=C{wy. + D{w - {wy + * C=({wy.y{w-{wy{wy.)/({wy.y.{w-{wy.{wy.) + * D=({wy-C{wy.)/{w + * requiring us to collect: + * {wy.x=sumydotx + * {wx=sumx + * {wy.=sumydot + * {wy.y.=sumydotydot + * {w=sum + * {wy.y=sumydoty + * {wy=sumy + * Once we have A, B, C and D, we can calculate the x',y' and x",y" + * values as follows (based on geometric interpolation from the above + * constraints): + * x'=AH/2+B-AHW/(2W-width) + * y'=CH/2+D-CHW/(2W-width) + * x"=AH/2+B+AHW/(2W-width) + * y"=CH/2+D+CHW/(2W-width) + * These two points can be used either in the Linear or the BiLinear to + * establish a transform. + */ + +#define IMAGE_PORTION 64 +#define SKIP_SLIVER 1 + +/* Following global variables are for use by SliverMatch() */ +static unsigned long starPeriod; + /* The number of events between printing of a "*" progress + indicator. + */ +static unsigned long starCount; + /* The number of events until the next * progress indicator needs to be + printed. + */ + +static void +starEvent() { + + if (--starCount == 0) { + starCount = starPeriod; + fprintf (stderr, "*"); + } +} + + +static void +starInit(unsigned long const period) { + starPeriod = period; + starCount = period; +} + + +static void +starResetPeriod(unsigned long const period) { + starPeriod = period; + if (starCount > period) + starCount = period; +} + +static void +findBestMatches(Image * const Left, + Image * const Right, + int const x, + int const y, + int const width, + int const height, + int const offY, + unsigned const Xmin, + unsigned const Xmax, + int const Ymin, + int const Ymax, + Best best[NUM_BEST]) { +/*---------------------------------------------------------------------------- + Compare the rectangle 'width' columns by 'height' rows with upper + left corner at Column 'x', Row y+offY in image 'Right' to a bunch of + rectangles of the same size in image 'Left' and generate a list of the + rectangles in 'Left' that best match the one in 'Right'. + + The specific rectangles in 'Left' we examine are those with upper left + corner (X,Y+offY) where X is in [Xmin, Xmax) and Y is in [Ymin, Ymax). + + We return the ordered list of best matches as best[]. + + Caller must ensure that each of the rectangles in question is fully + contained with its respective image. +-----------------------------------------------------------------------------*/ + unsigned X, Y; + /* Exhaustively find the best match */ + for (X = Xmin; X < Xmax; ++X) { + int const widthOfOverlap = X - Left->pam.width; + for (Y = Ymin; Y < Ymax; ++Y) { + unsigned long difference = regionDifference( + Left, X, Y + offY, + Right, x, y + offY, + width, height); + update_best(best, difference, widthOfOverlap, Y + offY); + starEvent(); + } + } +} + + + +static void +allocate_best_array(Best *** const bestP, unsigned const bestSize) { + + Best ** best; + unsigned int i; + + MALLOCARRAY(best, bestSize); + if (best == NULL) + pm_error("No memory for Best array"); + + for (i = 0; i < bestSize; ++i) + best[i] = allocate_best(); + *bestP = best; +} + + + +static void determineXYRange(Stitcher * const me, + Image * const Left, + Image * const Right, + unsigned * const XminP, + unsigned * const XmaxP, + int * const YminP, + int * const YmaxP) { + + if (me->x == INT_MAX) { + *XmaxP = Left->pam.width - me->width; + /* I can't bring myself to go half way */ + *XminP = Left->pam.width - (7 * Right->pam.width / 16); + } else { + *XminP = me->x; + *XmaxP = me->x + 1; + } + if (me->y == INT_MAX) { + /* Middle 1/4 */ + *YminP = Left->pam.height * 3 / 8; + *YmaxP = Left->pam.height - (*YminP) - me->height; + } else { + *YminP = me->y; + *YmaxP = me->y + 1; + } + if (verbose) + pm_message("Test %d<x<%d %d<y<%d", *XminP, *XmaxP, *YminP, *YmaxP); +} + + + +/* + * Find the weighted best line fit using the left hand margin of the + * right hand image. + */ +static bool +SliverMatch(Stitcher * me, Image * Left, Image * Right, + unsigned image_portion, unsigned skip_sliver) +{ + /* up/down 3/10, make sure has an odd number of members */ + unsigned const bestSize = + 1 + 2 * ((image_portion * 3) / (10 * skip_sliver)); + Best ** best; /* malloc'ed array of Best * */ + float sumydotx, sumx, sumydot, sum; + float sumydoty, sumy, sumydotydot; + int yDiff; + unsigned X, Xmin, Xmax, num, xmin, xmax; + int x, y, Y, Ymin, Ymax, in, ymin, ymax; + + /* Harry Sticks Geeses */ + if (me->width == INT_MAX) { + me->width = Right->pam.width / image_portion; + } + if ((me->width > (Right->pam.width/2)) + || (me->width > (Left->pam.width/2))) { + pm_error ("stitch sample too wide %d\n", me->width); + /* NOTREACHED */ + } + if (me->height == INT_MAX) { + me->height = Right->pam.height / image_portion; + } + if ((me->height > Right->pam.height) + || (me->height > Left->pam.height)) { + pm_error ("stitch sample too high %d\n", me->height); + /* NOTREACHED */ + } + yDiff = (Right->pam.height * skip_sliver) / image_portion; + starInit((unsigned long)-1L); + + allocate_best_array(&best, bestSize); + + determineXYRange(me, Left, Right, &Xmin, &Xmax, &Ymin, &Ymax); + + /* Find the best */ + if ((verbose == 1) || (verbose == 2)) { + fprintf (stderr, "%79s|\r|", ""); + starInit((unsigned long) + ( + (unsigned long)(Xmax - Xmin) + * (unsigned long)(Ymax - Ymin) + ) * (unsigned long)bestSize + / 78L); + } + + /* A point in the middle of the right image */ + x = 0; + y = (Right->pam.height - me->height) / 2; + /* + * Exhaustively search for the best match, improvements + * in the algorithm here, if any, would give us the best + * bang for the buck when it comes to improving performance. + */ + /* + * First pass through the right hand images to determine + * which are good candidate (top 90 percentile) for content of + * features that we may have a chance of testing with. + */ + { + float minf, maxf; + float * features; + + MALLOCARRAY(features, bestSize); + minf = maxf = 0.0; + for (in = 0; in < bestSize; ++in) { + int const offY = yDiff * (in - (bestSize/2)); + float SUM[3], SUMSQ[3]; + int plane; + for (plane = 0; plane < MIN(Right->pam.depth,3); ++plane) { + SUM[plane] = SUMSQ[plane] = 0.0; + } + for (X = x; X < (x + me->width); ++X) { + for (Y = y + offY; Y < (y + offY + me->height); ++Y) { + for (plane = 0; + plane < MIN(Right->pam.depth,3); + ++plane) { + sample point = Right->tuple[Y][X][plane]; + SUM[plane] += point; + SUMSQ[plane] += point * point; + } + } + } + /* How many features */ + features[in] = 0.0; + for (plane = 0; plane < MIN(Right->pam.depth,3); ++plane) { + features[in] += SUMSQ[plane] - + (SUM[plane]*SUM[plane]/(float)(me->width*me->height)); + } + if ((minf == 0.0) || (features[in] < minf)) { + minf = features[in]; + } + if ((maxf == 0.0) || (features[in] > maxf)) { + maxf = features[in]; + } + } + /* Select 90% in the contrast range */ + minf = (minf + maxf) / 10; + for (in = 0; in < bestSize; ++in) { + if (features[in] < minf) { + free_best(best[in]); + best[in] = (Best *)NULL; + } + } + } + /* Loop through the constraints to find the best match */ + sumydotx=sumx=sumydot=sumydotydot=sum=sumydoty=sumy=0.0; + xmin = UINT_MAX; + xmax = 0; + ymin = INT_MAX; + ymax = INT_MIN; + in = num = 0; + for (;;) { + float w; + int offY = yDiff * (in - (bestSize/2)); + /* See if this one to be skipped because of too few features */ + if (best[in] == (Best *)NULL) { + if (in > (bestSize/2)) { + in = bestSize - in; + } else if (in < (bestSize/2)) { + in = (bestSize-1) - in; + } else { + break; + } + if ((verbose == 1) || (verbose == 2)) + for (X = Xmin; X < Xmax; ++X) { + for (Y = Ymin; Y < Ymax; ++Y) + starEvent(); + } + continue; + } + findBestMatches(Left, Right, x, y, me->width, me->height, + offY, Xmin, Xmax, Ymin, Ymax, + best[in]); + /* slop (noise in NUM_BEST) */ + { + float SUMx, SUMxx, SUMy, SUMyy, SUMw; + unsigned i; + SUMx = SUMxx = SUMy = SUMyy = SUMw = 0.0; + for (i = 0; i < NUM_BEST; ++i) { + /* best[in][i] describes the ith closest region in the right + image to the region in the left image whose top corner is + at (x, y+offY). + */ + float const w2 = (best[in][i].total > 0) + ? (1.0 / (float)best[in][i].total) + : 1.0; + SUMx += w2 * best[in][i].x; + SUMy += w2 * best[in][i].y; + SUMxx += w2 * (best[in][i].x * best[in][i].x); + SUMyy += w2 * (best[in][i].y * best[in][i].y); + SUMw += w2; + } + /* Find our weighted error */ + w = SUMw + / ((SUMxx - (SUMx*SUMx)/SUMw) + (SUMyy - (SUMy*SUMy)/SUMw)); + } + /* magnify slop */ + w *= w; + Y = y + offY; + sumy += w * best[in][0].y; + sumx += w * best[in][0].x; + sum += w; + sumydot += w * Y; + sumydotydot += w * Y * Y; + sumydoty += w * Y * best[in][0].y; + sumydotx += w * Y * best[in][0].x; + /* Calculate the best fit line for these matches */ + me->parms[Sliver_C] = ((sumydotydot * sum) + - (sumydot * sumydot)); + if (me->parms[Sliver_C] == 0.0) { + me->parms[Sliver_A] = 0.0; + } else { + me->parms[Sliver_A] = ((sumydotx * sum) + - (sumx * sumydot)) + / me->parms[Sliver_C]; + me->parms[Sliver_C] = ((sumydoty * sum) + - (sumy * sumydot)) + / me->parms[Sliver_C]; + } + if (sum == 0.0) { + me->parms[Sliver_B] = me->parms[Sliver_D] = 0; + } else { + me->parms[Sliver_B] = (sumx + - (me->parms[Sliver_A] * sumydot)) + / sum; + me->parms[Sliver_D] = (sumy + - (me->parms[Sliver_C] * sumydot)) + / sum; + } + if (verbose > 2) { + fprintf (stderr, "%.4g*(%d,%d)@(%d,%d)\n", + w, best[in][0].x + Left->pam.width, best[in][0].y, + me->width / 2, Y); + } + /* Record history of limits */ + if ((best[in][0].x + Left->pam.width) < xmin) { + xmin = best[in][0].x + Left->pam.width; + } + if (xmax < (best[in][0].x + Left->pam.width)) { + xmax = best[in][0].x + Left->pam.width; + } + if ((best[in][0].y - offY) < ymin) { + ymin = best[in][0].y - offY; + } + if (ymax < (best[in][0].y - offY)) { + ymax = best[in][0].y - offY; + } + /* Lets restrict the search a bit now */ + if (++num > 1) { + if (me->x == INT_MAX) { + int newXmin, newXmax, hold; + /* Use the formula to determine the bounds */ + newXmin = (int)(me->parms[Sliver_B] + 0.5) + + Left->pam.width; + newXmax = (int)((me->parms[Sliver_A] + * (float)Left->pam.height) + + me->parms[Sliver_B] + 0.5) + + Left->pam.width; + if (newXmax < newXmin) { + hold = newXmin; + newXmin = newXmax; + newXmax = hold; + } + /* Trust little ... */ + hold = (3 * newXmin - newXmax) / 2; + newXmax = (3 * newXmax - newXmin) / 2; + newXmin = hold; + /* Don't go inside history */ + if (xmin < newXmin) { + newXmin = xmin + Left->pam.width; + } + if (newXmax < xmax) { + newXmax = xmax + Left->pam.width; + } + /* If it is `wacky' drop it */ + if ((newXmax - Xmax) < (Right->pam.width / 3)) { + /* Now upgrade new minimum and maximum */ + hold = Xmin; + if ((Xmin < newXmin) && (newXmin < Xmax)) { + hold = newXmin; + } + if ((Xmin < newXmax) && (newXmax < Xmax)) { + Xmax = newXmax; + } + Xmin = hold; + } + } + if (me->y == INT_MAX) { + int newYmin, newYmax, hold; + float tmp; + /* Use the formula to determine the bounds */ + newYmin = tmp = me->parms[Sliver_D] + + ((float)(Left->pam.height + 1)) + / 2; + newYmax = (int)((me->parms[Sliver_C] + * (float)Left->pam.height) + + tmp) - Left->pam.height; + if (newYmax < newYmin) { + hold = newYmin; + newYmin = newYmax; + newYmax = hold; + } + /* Trust little ... */ + hold = (3 * newYmin - newYmax) / 2; + newYmax = (3 * newYmax - newYmin) / 2; + newYmin = hold; + /* Don't go inside history */ + if (ymin < newYmin) { + newYmin = ymin; + } + if (newYmax < ymax) { + newYmax = ymax; + } + /* Now upgrade new minimum and maximum */ + hold = Ymin; + if ((Ymin < newYmin) && (newYmin < Ymax)) { + hold = newYmin; + } + if ((Ymin < newYmax) && (newYmax < Ymax)) { + Ymax = newYmax; + } + Ymin = hold; + } + if ((verbose == 1) || (verbose == 2)) { + starResetPeriod((unsigned long)( + ((unsigned long)(Xmax - Xmin) + * (unsigned long)(Ymax - Ymin)) + * (unsigned long)bestSize + / 78L)); + } + } + if (in > (bestSize/2)) { + in = bestSize - in; + } else if (in < (bestSize/2)) { + in = (bestSize-1) - in; + } else { + break; + } + } + if ((verbose == 1) || (verbose == 2)) { + fprintf (stderr, "\n"); + } + if (verbose > 2) { + fprintf (stderr, "Up "); + pr_best(best[bestSize-1]); + fprintf (stderr, "\nMid "); + pr_best(best[bestSize/2]); + fprintf (stderr, "\nDown"); + pr_best(best[0]); + fprintf (stderr, "\n"); + } + + if (verbose) { + if (verbose > 1) { + fprintf (stderr, + "[y=%.4g [x=%.4g [=%.4g [y.=%.4g " + "[y.y.=%.4g [y.y=%.4g [y.x=%.4g\n", + sumy, sumx, sum, sumydot, sumydotydot, + sumydoty, sumydotx); + } + fprintf (stderr, "x=%.4gY%+.4g\ny=%.4gY%+.4g\n", + me->parms[Sliver_A], me->parms[Sliver_B], + me->parms[Sliver_C], me->parms[Sliver_D]); + } + /* + * Free up resources + */ + for (in = 0; in < bestSize; ++in) { + if (best[in] != (Best *)NULL) { + free_best (best[in]); + best[in] = (Best *)NULL; + } + } + free(best); + + /* Calculate x',y' and x",y" from best fit line formula */ + sum = (float)(Right->pam.width * Right->pam.height) + / (float)(2 * Right->pam.width - me->width); + me->parms[Sliver_xpp] = me->parms[Sliver_A] * sum; + me->parms[Sliver_ypp] = me->parms[Sliver_C] * sum; + sumx = me->parms[Sliver_A] * (Right->pam.height / 2) + + me->parms[Sliver_B]; + sumx += Left->pam.width; + sumy = me->parms[Sliver_C] * (Right->pam.height / 2) + + me->parms[Sliver_D]; + me->parms[Sliver_xp] = sumx - me->parms[Sliver_xpp]; + me->parms[Sliver_yp] = sumy - me->parms[Sliver_ypp]; + me->parms[Sliver_xpp] += sumx; + me->parms[Sliver_ypp] += sumy; + if (verbose > 1) { + fprintf (stderr, "x',y'=%.4g,%.4g x\",y\"=%.4g,%.4g\n", + me->parms[Sliver_xp], me->parms[Sliver_yp], + me->parms[Sliver_xpp], me->parms[Sliver_ypp]); + } + return TRUE; +} /* SliverMatch() - end */ + +/* These are not used. Perhaps they are forerunners of the more + expressive Sliver_A, etc. names. +*/ +#define Linear_a 0 +#define Linear_b 1 +#define Linear_c 2 +#define Linear_d 3 +#define Linear_e 4 +#define Linear_f 5 +#define Linear_g 6 +#define Linear_h 7 + +static bool +LinearMatch(Stitcher * me, Image * Left, Image * Right) +{ + if (SliverMatch(me, Left, Right, IMAGE_PORTION, SKIP_SLIVER * 8) + == FALSE) { + return FALSE; + } + + me->x = - (me->parms[Sliver_xp] + me->parms[Sliver_xpp] + 1) / 2; + me->y = - (me->parms[Sliver_yp] + me->parms[Sliver_ypp] + + (1 - Left->pam.height)) / 2; + + if (verbose) + pm_message("LinearMatch translation parameters are (%d,%d)", + me->x, me->y); + + return TRUE; +} /* LinearMatch() - end */ + +/* + * Transformation parameters + * left x' = x + * left y' = y + * right x' = x + me->x + * right y' = y + me->y + */ +static float +LinearXLeft(Stitcher * me, int x, int y) +{ + UNREFERENCED_PARAMETER(y); + return x; +} /* LinearXLeft() - end */ + +static float +LinearYLeft(Stitcher * me, int x, int y) +{ + UNREFERENCED_PARAMETER(x); + return y; +} /* LinearYLeft() - end */ + +static float +LinearXRight(Stitcher * me, int x, int y) +{ + UNREFERENCED_PARAMETER(y); + return (x + me->x); +} /* LinearXRight() - end */ + +static float +LinearYRight(Stitcher * me, int x, int y) +{ + UNREFERENCED_PARAMETER(x); + return (y + me->y); +} /* LinearYRight() - end */ + +static void +LinearOutput(Stitcher * me, FILE * fp) +{ + fprintf (fp, "x'=x%+d\ny'=y%+d\n", me->x, me->y); +} /* LinearOutput() - end */ + +/* BiLinear Stitcher Methods */ + +static void +BiLinearDeAlloc(Stitcher * me) +{ + LinearDeAlloc(me); +} + + + +static bool +BiLinearAlloc(Stitcher * me) +{ + return LinearAlloc(me); +} + + + +static void +BiLinearConstrain(Stitcher * me, int x, int y, int width, int height) +{ + LinearConstrain(me, x, y, width, height); + if (x != INT_MAX) { + me->parms[3] -= x; + } + if (y != INT_MAX) { + me->parms[7] -= y; + } +} /* BiLinearConstrain() - end */ + +/* + * First pass is to find an approximate match. To do so, we take a + * width sliver of the left hand side of the right image and compare + * the sample to the left hand image. Accuracy is honored over speed. + * The image overlap is expected between 7/16 to 1/16 in the horizontal + * position, and a minumum of 5/8 in the vertical dimension. + * + * Blind alleys: + * - Tried a simpler constraint for right side to be `back' + * to image, twisted too much sometimes: + * . . . + * W=aW+bH+cWH+d + * H=eW+fH+gWH+h + * W=aW+d + * 0=eW+h + * Solve the equations resulted in: + * a = W/(W-x") - cy" + * b = -Wc + * c = W/((x"-W)(y'-y")) + * d = (1-a)W + * e = y'(y"x'+W-Hx'-x"y")/(x'y"x"-Wx'y"-Wy'x"-WWy'+x'y'x"-Wx'y'-W) + * f = 1 - Wg + * g = (e + (H-y")/(x"-W))/y" + * h = -We + * Results left here for historical reasons. + * + * Transformation parameters + * x=ax.+by.+cx.y.+d + * y=ex.+fy.+gx.y.+h + * Where x,y represents the original point, and x.,y. + * represents the transformed point. Thus: + * + * Transformed image: + * (x',y') (x'",y'") + * (x",y") (x"",y"") + * + * Corresponding to Original (dot) image: + * (0,0) (Right->pam.width,0) + * (0,Right->pam.height) (Right->pam.width,Right->pam.height) + * + * Define: + * H=Right->pam.height + * W=Right->pam.width + * Given that I want a flat presentation that both reduces the distortion + * necessary on an image, reduces the cropping losses, and flattens out the + * spherical or orbit distortions; it was chosen to constrain the right side + * in the middle horizontal, and pivot the left side in that middle (hopefully + * minimally) and to allow the image only vertical and horizontal location + * placement. Rotating the entire image could increase cropping losses + * especially if the focus was not down the center of the image on a + * graduated field causing the distortion to accumulate in subsequent + * images. Trapezoidal would cause the distortion to accumulate in subsequent + * images as well, resetting to `square' gradually towards the right would + * allow the next image to restart a match placing the distortions mainly + * in the stitching zone where averaging and the slight expectation of + * artifacts would minimize the effects. These constraints can be explained + * mathematically as the following: + * x'" + x"" - 2W = x' + x" + * y'" + y"" = y' + y" + * x'" = x"" + * y'" + H = y"" + * resulting in the right side of the image being completely explained by the + * placement of the left hand side: + * x'"=(x'+x"+2W)/2 + * y'"=(y'+y"-H)/2 + * x""=(x'+x"+2W)/2 + * y""=(y'+y"+H)/2 + * + * Describing the `X' polygon using geometry and ratios: + * X=A(y-(y'+y")/2)(x-(x'+x"+2W)/2) + (x - (x'+x")/2)) + * A=2(x"-x')/((y'-y")(x'-x"-2W)) + * a=2(y'(x'-x")-W(y'-y"))/((y'-y")(x'-x"-2W)) + * b=(x'-x")(x'+x"+2W)/((y'-y")(x'-x"-2W)) + * c=2(x"-x')/((y'-y")(x'-x"-2W)) + * d=(2W(x"y'-x'y")+y'(x"x"-x'x'))/((y'-y")(x'-x"-2W)) + * + * Describing the `Y' polygon using geometry and ratios (note use of X rather + * than x, this has the effect of linearalizing the polygon). + * Y=((y'-y"+H)/W(y'-y"))(y-(y'+y")/2)(X-HW/(y'-y"+H)) + H/2 + * e=(y'+y")(y'-y"+H)/2W(y"-y') + * f=H/(y"-y') + * g=(y'-y"+H)/W(y'-y") + * h=Hy'/(y'-y") + * + * FYI: Reverse transform using the same formula style is: + * a=(x"-x'+2W)/2W + * b=(x"-x')/H + * c=(x'-x")/WH + * d=x' + * e=(y"-y'-H)/2W + * f=(y"-y')/H + * g=(y'-y"+H)/WH + * h=y' + */ + +#define BiLinear_a 0 +#define BiLinear_b 1 +#define BiLinear_c 2 +#define BiLinear_d 3 +#define BiLinear_e 4 +#define BiLinear_f 5 +#define BiLinear_g 6 +#define BiLinear_h 7 + +static bool +BiLinearMatch(Stitcher * me, Image * Left, Image * Right) +{ + float xp, yp, xpp, ypp; + + if (SliverMatch(me, Left, Right, IMAGE_PORTION, SKIP_SLIVER) == FALSE) { + return FALSE; + } + /* If too wacky, flatten out */ + xp = me->parms[Sliver_xp]; + yp = me->parms[Sliver_yp]; + xpp = me->parms[Sliver_xpp]; + ypp = me->parms[Sliver_ypp]; + if ((me->parms[Sliver_A] < -0.3) + || (0.3 < me->parms[Sliver_A])) { + xp = xpp = (xp + xpp) / 2; + } + if ((me->parms[Sliver_C] < 0.6) + || (1.5 < me->parms[Sliver_D])) { + yp = (yp + ypp - (float)Right->pam.height) / 2; + ypp = yp + Right->pam.height; + } + + /* + * Calculate any necessary transformations on the + * right image to improve the stitching match. We have Done a + * weighted best fit line on the points we have collected + * thus far, now translate this to the constrained + * transformation equations. + */ + /* a = y"-y' */ + me->parms[BiLinear_a] = ypp-yp; + /* c = x'-x" */ + me->parms[BiLinear_c] = xp-xpp; + /* d = (y"-y')(x"-x'+2W) = (y'-y")(x'-x"-2W) */ + me->parms[BiLinear_d] = me->parms[BiLinear_a] + * ((float) + (2*Right->pam.width)-me->parms[BiLinear_c]); + /* a = 2(y'(x'-x")+W(y"-y'))/((y'-y")(x'-x"-2W)) */ + me->parms[BiLinear_a] = 2*(yp*me->parms[BiLinear_c] + + me->parms[BiLinear_a]*(float)(Right->pam.width)) + / me->parms[BiLinear_d]; + /* b = (x'-x")(x'+x"+2W)/((y'-y")(x'-x"-2W)) */ + me->parms[BiLinear_b] = me->parms[BiLinear_c] + * (xp+xpp+(float)(2*Right->pam.width)) + / me->parms[BiLinear_d]; + /* c = -2(x'-x")/((y'-y")(x'-x"-2W)) */ + me->parms[BiLinear_c]*= -2/me->parms[BiLinear_d]; + /* d = (2W(x"y'-x'y")+y'(x"x"-x'x'))/((y'-y")(x'-x"-2W)) */ + me->parms[BiLinear_d] = ((xpp*yp-xp*ypp)*(float)(2*Right->pam.width) + + yp*(xpp*xpp-xp*xp)) + / me->parms[BiLinear_d]; + + /* f = y"-y' */ + me->parms[BiLinear_f] = ypp-yp; + /* g = (y"-y'-H)/W(y"-y') */ + me->parms[BiLinear_g] = (me->parms[BiLinear_f]-(float)Right->pam.height) + / me->parms[BiLinear_f] + / (float)Right->pam.width; + /* e = (y'+y")(y'-y"+H)/2W(y"-y') = -g(y'+y")/2 */ + me->parms[BiLinear_e] = (yp+ypp)*me->parms[BiLinear_g]/-2; + /* f=H/(y"-y') */ + me->parms[BiLinear_f] = ((float)Right->pam.height) + / me->parms[BiLinear_f]; + /* h = Hy'/(y'-y") = -fy' */ + me->parms[BiLinear_h] = -yp*me->parms[BiLinear_f]; + + return TRUE; +} /* BiLinearMatch() - end */ + +/* + * Transformation parameters + * x`=x + * y`=y + */ +#define BiLinearXLeft LinearXLeft +#define BiLinearYLeft LinearYLeft + +/* + * Transformation parameters + * x`=ax+by+cxy+d + * y`=ex`+fy+gx`y+h + */ +static float +BiLinearXRight(Stitcher * me, int x, int y) +{ + return (me->parms[BiLinear_a] * x) + (me->parms[BiLinear_b] * y) + + (me->parms[BiLinear_c] * (x * y)) + me->parms[BiLinear_d]; +} /* BiLinearXRight() - end */ + +static float +BiLinearYRight(Stitcher * me, int x, int y) +{ + /* A little trick I learned from a biker */ + float X = BiLinearXRight(me, x, y); + return (me->parms[BiLinear_e] * X) + (me->parms[BiLinear_f] * y) + + (me->parms[BiLinear_g] * (X * y)) + me->parms[BiLinear_h]; +} /* BiLinearYRight() - end */ + +static void +BiLinearOutput(Stitcher * me, FILE * fp) +{ + fprintf (fp, + "x'=%.6gx%+.6gy%+.6gxy%+.6g\ny'=%.6gx'%+.6gy%+.6gx'y%+.6g\n", + me->parms[BiLinear_a], me->parms[BiLinear_b], me->parms[BiLinear_c], + me->parms[BiLinear_d], me->parms[BiLinear_e], me->parms[BiLinear_f], + me->parms[BiLinear_g], me->parms[BiLinear_h]); +} /* BiLinearOutput() - end */ + +/* Rotate Stitcher Methods */ + +#define RotateDeAlloc BiLinearDeAlloc + +#define RotateAlloc BiLinearAlloc + +#define RotateConstrain BiLinearConstrain + +/* + * First pass is to utilize the SliverMatch. + * + * Transformation parameters + * x=ax.+by.+d + * y=ex.+fy.+h + * Where x,y represents the original point, and x.,y. + * represents the transformed point. Thus: + * + * Transformed image: + * (x',y') (x'",y'") + * (x",y") (x"",y"") + * + * Corresponding to Original (dot) image: + * (0,0) (Right->pam.width,0) + * (0,Right->pam.height) (Right->pam.width,Right->pam.height) + * + * Define: + * H=Right->pam.height + * W=Right->pam.width + * + */ + +#define Rotate_a 0 +#define Rotate_b 1 +#define Rotate_c 2 +#define Rotate_d 3 +#define Rotate_e 4 +#define Rotate_f 5 +#define Rotate_g 6 +#define Rotate_h 7 + +static bool +RotateMatch(Stitcher * me, Image * Left, Image * Right) +{ + float xp, yp, xpp, ypp; + + if (SliverMatch(me, Left, Right, IMAGE_PORTION, SKIP_SLIVER) == FALSE) { + return FALSE; + } + xp = me->parms[Sliver_xp]; + yp = me->parms[Sliver_yp]; + xpp = me->parms[Sliver_xpp]; + ypp = me->parms[Sliver_ypp]; + + me->parms[Rotate_c] = (xp - xpp); + me->parms[Rotate_c]*= me->parms[Rotate_c]; + me->parms[Rotate_g] = (yp - ypp); + me->parms[Rotate_g]*= me->parms[Rotate_g]; + me->parms[Rotate_a] = me->parms[Rotate_f] = sqrt(me->parms[Rotate_g] + / (me->parms[Rotate_c] + + me->parms[Rotate_g])); + me->parms[Rotate_b] = me->parms[Rotate_e] = sqrt(me->parms[Rotate_c] + / (me->parms[Rotate_c] + + me->parms[Rotate_g])); + if (xp < xpp) { + me->parms[Rotate_b] = -me->parms[Rotate_b]; + } else { + me->parms[Rotate_e] = -me->parms[Rotate_e]; + } + /* negative (for reverse transform below) xp & yp set for unity gain */ + xp = ((me->parms[Rotate_b] * (float)Right->pam.height) + xp + xpp) / -2; + yp = ((me->parms[Rotate_f] * (float)Right->pam.height) - yp - ypp) / 2; + me->parms[Rotate_d] = xp * me->parms[Rotate_a] + yp * me->parms[Rotate_b]; + me->parms[Rotate_h] = xp * me->parms[Rotate_e] + yp * me->parms[Rotate_f]; + return TRUE; +} /* RotateMatch() - end */ + +/* + * Transformation parameters + * x`=x + * y`=y + */ +#define RotateXLeft BiLinearXLeft +#define RotateYLeft BiLinearYLeft + +/* + * Transformation parameters + * x`=ax+by+d + * y`=ex+fy+h + */ + +static float +RotateXRight(Stitcher * me, int x, int y) +{ + return (me->parms[Rotate_a] * x) + (me->parms[Rotate_b] * y) + + me->parms[Rotate_d]; +} /* RotateXRight() - end */ + +static float +RotateYRight(Stitcher * me, int x, int y) +{ + return (me->parms[Rotate_e] * x) + (me->parms[Rotate_f] * y) + + me->parms[Rotate_h]; +} /* RotateYRight() - end */ + +static void +RotateOutput(Stitcher * me, FILE * fp) +{ + fprintf (fp, + "x'=%.6gx%+.6gy%+.6g\ny'=%.6gx%+.6gy%+.6g\n", + me->parms[Rotate_a], me->parms[Rotate_b], me->parms[Rotate_d], + me->parms[Rotate_e], me->parms[Rotate_f], me->parms[Rotate_h]); +} /* RotateOutput() - end */ + +/* Stitcher Method Table */ + +Stitcher StitcherMethods[] = { + { "RotateSliver", RotateAlloc, RotateDeAlloc, RotateConstrain, + RotateMatch, RotateXLeft, RotateYLeft, RotateXRight, + RotateYRight, RotateOutput }, + { "BiLinearSliver", BiLinearAlloc, BiLinearDeAlloc, BiLinearConstrain, + BiLinearMatch, BiLinearXLeft, BiLinearYLeft, BiLinearXRight, + BiLinearYRight, BiLinearOutput }, + { "LinearSliver", LinearAlloc, LinearDeAlloc, LinearConstrain, + LinearMatch, LinearXLeft, LinearYLeft, LinearXRight, + LinearYRight, LinearOutput }, + { (char *)NULL } +}; diff --git a/editor/pnmtile.c b/editor/pnmtile.c new file mode 100644 index 00000000..96bf658d --- /dev/null +++ b/editor/pnmtile.c @@ -0,0 +1,63 @@ +/* pnmtile.c - replicate a portable anymap into a specified size +** +** Copyright (C) 1989 by Jef Poskanzer. +** +** 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. +*/ + +#include "pnm.h" + +int +main( argc, argv ) + int argc; + char* argv[]; + { + FILE* ifp; + xel** xels; + register xel* xelrow; + xelval maxval; + int rows, cols, format, width, height, row, col; + const char* const usage = "width height [pnmfile]"; + + pnm_init( &argc, argv ); + + if ( argc < 3 || argc > 4 ) + pm_usage( usage ); + + if ( sscanf( argv[1], "%d", &width ) != 1 ) + pm_usage( usage ); + if ( sscanf( argv[2], "%d", &height ) != 1 ) + pm_usage( usage ); + + if ( width < 1 ) + pm_error( "width is less than 1" ); + if ( height < 1 ) + pm_error( "height is less than 1" ); + + if ( argc == 4 ) + ifp = pm_openr( argv[3] ); + else + ifp = stdin; + + xels = pnm_readpnm( ifp, &cols, &rows, &maxval, &format ); + pm_close( ifp ); + + xelrow = pnm_allocrow( width ); + + pnm_writepnminit( stdout, width, height, maxval, format, 0 ); + for ( row = 0; row < height; ++row ) + { + for ( col = 0; col < width; ++col ) + xelrow[col] = xels[row % rows][col % cols]; + pnm_writepnmrow( stdout, xelrow, width, maxval, format, 0 ); + } + + pm_close( stdout ); + + exit( 0 ); + } diff --git a/editor/ppm3d.c b/editor/ppm3d.c new file mode 100644 index 00000000..c37ceeb1 --- /dev/null +++ b/editor/ppm3d.c @@ -0,0 +1,138 @@ +/* ppmto3d.c - convert a portable pixmap to a portable graymap +** +** Copyright (C) 1989 by Jef Poskanzer. +** +** 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. +*/ + +#include "ppm.h" +#include "lum.h" + +static void +computeGrayscaleRow(const pixel * const inputRow, + gray * const outputRow, + pixval const maxval, + unsigned int const cols) { + + if (maxval <= 255) { + unsigned int col; + /* Use fast approximation to 0.299 r + 0.587 g + 0.114 b. */ + for (col = 0; col < cols; ++col) + outputRow[col] = ppm_fastlumin(inputRow[col]); + } else { + unsigned int col; + /* Can't use fast approximation, so fall back on floats. */ + for (col = 0; col < cols; ++col) + outputRow[col] = PPM_LUMIN(inputRow[col]) + 0.5; + } +} + + + +int +main (int argc, char *argv[]) { + + int offset; + int cols, rows, row; + pixel* pixelrow; + pixval maxval; + + FILE* Lifp; + pixel* Lpixelrow; + gray* Lgrayrow; + int Lrows, Lcols, Lformat; + pixval Lmaxval; + + FILE* Rifp; + pixel* Rpixelrow; + gray* Rgrayrow; + int Rrows, Rcols, Rformat; + pixval Rmaxval; + + ppm_init (&argc, argv); + + if (argc-1 > 3 || argc-1 < 2) + pm_error("Wrong number of arguments (%d). Arguments are " + "leftppmfile rightppmfile [horizontal_offset]", argc-1); + + Lifp = pm_openr (argv[1]); + Rifp = pm_openr (argv[2]); + + if (argc-1 >= 3) + offset = atoi (argv[3]); + else + offset = 30; + + ppm_readppminit (Lifp, &Lcols, &Lrows, &Lmaxval, &Lformat); + ppm_readppminit (Rifp, &Rcols, &Rrows, &Rmaxval, &Rformat); + + if ((Lcols != Rcols) || (Lrows != Rrows) || + (Lmaxval != Rmaxval) || + (PPM_FORMAT_TYPE(Lformat) != PPM_FORMAT_TYPE(Rformat))) + pm_error ("Pictures are not of same size and format"); + + cols = Lcols; + rows = Lrows; + maxval = Lmaxval; + + ppm_writeppminit (stdout, cols, rows, maxval, 0); + Lpixelrow = ppm_allocrow (cols); + Lgrayrow = pgm_allocrow (cols); + Rpixelrow = ppm_allocrow (cols); + Rgrayrow = pgm_allocrow (cols); + pixelrow = ppm_allocrow (cols); + + for (row = 0; row < rows; ++row) { + ppm_readppmrow(Lifp, Lpixelrow, cols, maxval, Lformat); + ppm_readppmrow(Rifp, Rpixelrow, cols, maxval, Rformat); + + computeGrayscaleRow(Lpixelrow, Lgrayrow, maxval, cols); + computeGrayscaleRow(Rpixelrow, Rgrayrow, maxval, cols); + { + int col; + gray* LgP; + gray* RgP; + pixel* pP; + for (col = 0, pP = pixelrow, LgP = Lgrayrow, RgP = Rgrayrow; + col < cols + offset; + ++col) { + + if (col < offset/2) + ++LgP; + else if (col >= offset/2 && col < offset) { + const pixval Blue = (pixval) (float) *LgP; + const pixval Red = (pixval) 0; + PPM_ASSIGN (*pP, Red, Blue, Blue); + ++LgP; + ++pP; + } else if (col >= offset && col < cols) { + const pixval Red = (pixval) (float) *RgP; + const pixval Blue = (pixval) (float) *LgP; + PPM_ASSIGN (*pP, Red, Blue, Blue); + ++LgP; + ++RgP; + ++pP; + } else if (col >= cols && col < cols + offset/2) { + const pixval Blue = (pixval) 0; + const pixval Red = (pixval) (float) *RgP; + PPM_ASSIGN (*pP, Red, Blue, Blue); + ++RgP; + ++pP; + } else + ++RgP; + } + } + ppm_writeppmrow(stdout, pixelrow, cols, maxval, 0); + } + + pm_close(Lifp); + pm_close(Rifp); + pm_close(stdout); + + return 0; +} diff --git a/editor/ppmbrighten.c b/editor/ppmbrighten.c new file mode 100644 index 00000000..93649082 --- /dev/null +++ b/editor/ppmbrighten.c @@ -0,0 +1,337 @@ +/* ppmbrighten.c - allow user control over Value and Saturation of PPM file +** +** Copyright (C) 1989 by Jef Poskanzer. +** Copyright (C) 1990 by Brian Moffet. +** +** 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. +*/ + +#include "ppm.h" +#include "shhopt.h" +#include "mallocvar.h" + +#define MULTI 1000 + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *inputFilespec; /* '-' if stdin */ + float saturation; + float value; + unsigned int normalize; +}; + + + + +static void +parseCommandLine (int argc, char ** argv, + struct cmdlineInfo *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 option_def_index; + + unsigned int saturationSpec, valueSpec; + int saturationOpt, valueOpt; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "saturation", OPT_INT, &saturationOpt, + &saturationSpec, 0 ); + OPTENT3(0, "value", OPT_INT, &valueOpt, + &valueSpec, 0 ); + OPTENT3(0, "normalize", OPT_FLAG, NULL, + &cmdlineP->normalize, 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 */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (saturationSpec) { + if (saturationOpt < -100) + pm_error("Saturation reduction cannot be more than 100%%. " + "You specified %d", saturationOpt); + else + cmdlineP->saturation = 1.0 + (float)saturationOpt / 100; + } else + cmdlineP->saturation = 1.0; + + if (valueSpec) { + if (valueOpt < -100) + pm_error("Value reduction cannot be more than 100%%. " + "You specified %d", valueOpt); + else + cmdlineP->value = 1.0 + (float)valueOpt / 100; + } else + cmdlineP->value = 1.0; + + if (argc-1 < 1) + cmdlineP->inputFilespec = "-"; + else if (argc-1 == 1) + cmdlineP->inputFilespec = argv[1]; + else + pm_error("Program takes at most one argument: file specification"); +} + + + +static __inline__ unsigned int +mod(int const dividend, unsigned int const divisor) { + + int remainder = dividend % divisor; + + if (remainder < 0) + return divisor + remainder; + else + return (unsigned int) remainder; +} + + + +static void +RGBtoHSV(pixel const color, + pixval const maxval, + unsigned int * const hP, + unsigned int * const sP, + unsigned int * const vP) { + + unsigned int const R = (MULTI * PPM_GETR(color) + maxval - 1) / maxval; + unsigned int const G = (MULTI * PPM_GETG(color) + maxval - 1) / maxval; + unsigned int const B = (MULTI * PPM_GETB(color) + maxval - 1) / maxval; + + unsigned int s, v; + unsigned int t; + unsigned int sector; + + v = MAX(R, MAX(G, B)); + + t = MIN(R, MIN(G, B)); + + if (v == 0) + s = 0; + else + s = ((v - t)*MULTI)/v; + + if (s == 0) + sector = 0; + else { + unsigned int const cr = (MULTI * (v - R))/(v - t); + unsigned int const cg = (MULTI * (v - G))/(v - t); + unsigned int const cb = (MULTI * (v - B))/(v - t); + + if (R == v) + sector = mod((int)(cb - cg), 6*MULTI); + else if (G == v) + sector = mod((int)((2*MULTI) + cr - cb), 6*MULTI); + else if (B == v) + sector = mod((int)((4*MULTI) + cg - cr), 6*MULTI); + else + pm_error("Internal error: neither r, g, nor b is maximum"); + } + + *hP = sector * 60; + *sP = s; + *vP = v; +} + + + +static void +HSVtoRGB(unsigned int const h, + unsigned int const s, + unsigned int const v, + pixval const maxval, + pixel * const colorP) { + + unsigned int R, G, B; + + if (s == 0) { + R = v; + G = v; + B = v; + } else { + unsigned int const sectorSize = 60 * MULTI; + /* Color wheel is divided into six 60 degree sectors. */ + unsigned int const sector = (h/sectorSize); + /* The sector in which our color resides. Value is in 0..5 */ + unsigned int const f = (h - sector*sectorSize)/60; + /* The fraction of the way the color is from one side of + our sector to the other side, going clockwise. Value is + in [0, MULTI). + */ + unsigned int const m = (v * (MULTI - s)) / MULTI; + unsigned int const n = (v * (MULTI - (s * f)/MULTI)) / MULTI; + unsigned int const k = (v * (MULTI - (s * (MULTI - f))/MULTI)) / MULTI; + + switch (sector) { + case 0: + R = v; + G = k; + B = m; + break; + case 1: + R = n; + G = v; + B = m; + break; + case 2: + R = m; + G = v; + B = k; + break; + case 3: + R = m; + G = n; + B = v; + break; + case 4: + R = k; + G = m; + B = v; + break; + case 5: + R = v; + G = m; + B = n; + break; + default: + pm_error("Invalid H value passed to HSVtoRGB: %u/%u", h, MULTI); + } + } + PPM_ASSIGN(*colorP, + (R * maxval) / MULTI, + (G * maxval) / MULTI, + (B * maxval) / MULTI); +} + + + +static void +getMinMax(FILE * const ifP, + int const cols, + int const rows, + pixval const maxval, + int const format, + unsigned int * const minValueP, + unsigned int * const maxValueP) { + + pixel * pixelrow; + unsigned int minValue, maxValue; + int row; + + pixelrow = ppm_allocrow(cols); + + maxValue = 0; + minValue = MULTI; + for (row = 0; row < rows; ++row) { + unsigned int col; + ppm_readppmrow(ifP, pixelrow, cols, maxval, format); + for (col = 0; col < cols; ++col) { + unsigned int H, S, V; + + RGBtoHSV(pixelrow[col], maxval, &H, &S, &V); + maxValue = MAX(maxValue, V); + minValue = MIN(minValue, V); + } + } + ppm_freerow(pixelrow); + + *minValueP = minValue; + *maxValueP = maxValue; +} + + + +int +main(int argc, char * argv[]) { + + struct cmdlineInfo cmdline; + FILE *ifP; + pixval minValue, maxValue; + pixel *pixelrow; + pixval maxval; + int rows, cols, format, row; + + ppm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + if (cmdline.normalize) + ifP = pm_openr_seekable(cmdline.inputFilespec); + else + ifP = pm_openr(cmdline.inputFilespec); + + ppm_readppminit(ifP, &cols, &rows, &maxval, &format); + + if (cmdline.normalize) { + pm_filepos rasterPos; + pm_tell2(ifP, &rasterPos, sizeof(rasterPos)); + getMinMax(ifP, cols, rows, maxval, format, &minValue, &maxValue); + pm_seek2(ifP, &rasterPos, sizeof(rasterPos)); + pm_message("Minimum value %u%% of full intensity " + "being remapped to zero.", + (minValue*100+MULTI/2)/MULTI); + pm_message("Maximum value %u%% of full intensity " + "being remapped to full.", + (maxValue*100+MULTI/2)/MULTI); + } + + pixelrow = ppm_allocrow(cols); + + ppm_writeppminit(stdout, cols, rows, maxval, 0); + + for (row = 0; row < rows; ++row) { + unsigned int col; + ppm_readppmrow(ifP, pixelrow, cols, maxval, format); + for (col = 0; col < cols; ++col) { + unsigned int H, S, V; + + RGBtoHSV(pixelrow[col], maxval, &H, &S, &V); + + if (cmdline.normalize) { + V -= minValue; + V = (V * MULTI) / + (MULTI - (minValue+MULTI-maxValue)); + } + + S = MIN(MULTI, (unsigned int) (S * cmdline.saturation + 0.5)); + V = MIN(MULTI, (unsigned int) (V * cmdline.value + 0.5)); + + HSVtoRGB(H, S, V, maxval, &pixelrow[col]); + } + + ppm_writeppmrow(stdout, pixelrow, cols, maxval, 0); + } + ppm_freerow(pixelrow); + + pm_close(ifP); + + /* If the program failed, it previously aborted with nonzero completion + code, via various function calls. + */ + return 0; +} diff --git a/editor/ppmchange.c b/editor/ppmchange.c new file mode 100644 index 00000000..92d55527 --- /dev/null +++ b/editor/ppmchange.c @@ -0,0 +1,232 @@ +/* ppmchange.c - change a given color to another +** +** Copyright (C) 1991 by Wilson H. Bent, Jr. +** +** 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. +** +** Modified by Alberto Accomazzi (alberto@cfa.harvard.edu). +** 28 Jan 94 - Added multiple color substitution function. +*/ + +#include "ppm.h" +#include "shhopt.h" +#include "mallocvar.h" + +#define TCOLS 256 +#define SQRT3 1.73205080756887729352 + /* The square root of 3 */ + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char *input_filespec; /* Filespecs of input files */ + int ncolors; /* Number of valid entries in color0[], color1[] */ + char * oldcolorname[TCOLS]; /* colors user wants replaced */ + char * newcolorname[TCOLS]; /* colors with which he wants them replaced */ + int closeness; + /* -closeness option value. Zero if no -closeness option */ + char * remainder_colorname; + /* Color user specified for -remainder. Null pointer if he didn't + specify -remainder. + */ + unsigned int closeok; +}; + + + +static void +parseCommandLine(int argc, char ** 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 closenessSpec, remainderSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENT3(0, "closeness", OPT_UINT, + &cmdlineP->closeness, &closenessSpec, 0); + OPTENT3(0, "remainder", OPT_STRING, + &cmdlineP->remainder_colorname, &remainderSpec, 0); + OPTENT3(0, "closeok", OPT_FLAG, + NULL, &cmdlineP->closeok, 0); + + opt.opt_table = option_def; + opt.short_allowed = FALSE; /* We have no short (old-fashioned) options */ + opt.allowNegNum = FALSE; /* We may have parms that are negative numbers */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (!closenessSpec) + cmdlineP->remainder_colorname = NULL; + + if (!closenessSpec) + cmdlineP->closeness = 0; + + if ((argc-1) % 2 == 0) + cmdlineP->input_filespec = "-"; + else + cmdlineP->input_filespec = argv[argc-1]; + + { + int argn; + cmdlineP->ncolors = 0; /* initial value */ + for (argn = 1; + argn+1 < argc && cmdlineP->ncolors < TCOLS; + argn += 2) { + cmdlineP->oldcolorname[cmdlineP->ncolors] = argv[argn]; + cmdlineP->newcolorname[cmdlineP->ncolors] = argv[argn+1]; + cmdlineP->ncolors++; + } + } +} + + + +static double +sqrf(float const F) { + return F*F; +} + + + +static int +colormatch(pixel const comparand, + pixel const comparator, + float const closeness) { +/*---------------------------------------------------------------------------- + Return true iff 'comparand' matches 'comparator' in color within the + fuzz factor 'closeness'. +-----------------------------------------------------------------------------*/ + /* Fast path for usual case */ + if (closeness == 0) + return PPM_EQUAL(comparand, comparator); + + return PPM_DISTANCE(comparand, comparator) <= sqrf(closeness); +} + + + +static void +changeRow(const pixel * const inrow, + pixel * const outrow, + int const cols, + int const ncolors, + const pixel colorfrom[], + const pixel colorto[], + bool const remainder_specified, + pixel const remainder_color, + float const closeness) { +/*---------------------------------------------------------------------------- + Replace the colors in a single row. There are 'ncolors' colors to + replace. The to-replace colors are in the array colorfrom[], and the + replace-with colors are in corresponding elements of colorto[]. + Iff 'remainder_specified' is true, replace all colors not mentioned + in colorfrom[] with 'remainder_color'. Use the closeness factor + 'closeness' in determining if something in the input row matches + a color in colorfrom[]. + + The input row is 'inrow'. The output is returned as 'outrow', in + storage which must be already allocated. Both are 'cols' columns wide. +-----------------------------------------------------------------------------*/ + int col; + + for (col = 0; col < cols; ++col) { + int i; + int have_match; /* logical: It's a color user said to change */ + pixel newcolor; + /* Color to which we must change current pixel. Undefined unless + 'have_match' is true. + */ + + have_match = FALSE; /* haven't found a match yet */ + for (i = 0; i < ncolors && !have_match; ++i) { + have_match = colormatch(inrow[col], colorfrom[i], closeness); + newcolor = colorto[i]; + } + if (have_match) + outrow[col] = newcolor; + else if (remainder_specified) + outrow[col] = remainder_color; + else + outrow[col] = inrow[col]; + } +} + + + +int +main(int argc, char *argv[]) { + struct cmdlineInfo cmdline; + FILE * ifP; + int format; + int rows, cols; + pixval maxval; + float closeness; + int row; + pixel* inrow; + pixel* outrow; + + pixel oldcolor[TCOLS]; /* colors user wants replaced */ + pixel newcolor[TCOLS]; /* colors with which he wants them replaced */ + pixel remainder_color; + /* Color user specified for -remainder. Undefined if he didn't + specify -remainder. + */ + + ppm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.input_filespec); + + ppm_readppminit(ifP, &cols, &rows, &maxval, &format); + + if (cmdline.remainder_colorname) + remainder_color = ppm_parsecolor2(cmdline.remainder_colorname, maxval, + cmdline.closeok); + { + int i; + for (i = 0; i < cmdline.ncolors; ++i) { + oldcolor[i] = ppm_parsecolor2(cmdline.oldcolorname[i], maxval, + cmdline.closeok); + newcolor[i] = ppm_parsecolor2(cmdline.newcolorname[i], maxval, + cmdline.closeok); + } + } + closeness = SQRT3 * maxval * cmdline.closeness/100; + + ppm_writeppminit( stdout, cols, rows, maxval, 0 ); + inrow = ppm_allocrow(cols); + outrow = ppm_allocrow(cols); + + /* Scan for the desired color */ + for (row = 0; row < rows; row++) { + ppm_readppmrow(ifP, inrow, cols, maxval, format); + + changeRow(inrow, outrow, cols, cmdline.ncolors, oldcolor, newcolor, + cmdline.remainder_colorname != NULL, + remainder_color, closeness); + + ppm_writeppmrow(stdout, outrow, cols, maxval, 0); + } + + pm_close(ifP); + + return 0; +} diff --git a/editor/ppmcolormask.c b/editor/ppmcolormask.c new file mode 100644 index 00000000..57e5c825 --- /dev/null +++ b/editor/ppmcolormask.c @@ -0,0 +1,245 @@ +/*========================================================================= + ppmcolormask +=========================================================================== + + This program produces a PBM mask of areas containing a certain color. + + By Bryan Henderson, Olympia WA; April 2000. + + Contributed to the public domain by its author. +=========================================================================*/ + +#include <assert.h> +#include <string.h> + +#include "pm_c_util.h" +#include "shhopt.h" +#include "mallocvar.h" +#include "nstring.h" +#include "ppm.h" +#include "pbm.h" + +enum matchType { + MATCH_EXACT, + MATCH_BK +}; + +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 colorCount; + struct { + enum matchType matchType; + union { + pixel color; /* matchType == MATCH_EXACT */ + bk_color bkColor; /* matchType == MATCH_BK */ + } u; + } maskColor[16]; + unsigned int verbose; +}; + + + +static void +parseColorOpt(const char * const colorOpt, + struct cmdlineInfo * const cmdlineP) { + + unsigned int colorCount; + char * colorOptWork; + char * cursor; + bool eol; + + colorOptWork = strdup(colorOpt); + cursor = &colorOptWork[0]; + + eol = FALSE; /* initial value */ + colorCount = 0; /* initial value */ + while (!eol && colorCount < ARRAY_SIZE(cmdlineP->maskColor)) { + const char * token; + token = strsepN(&cursor, ","); + if (token) { + if (STRNEQ(token, "bk:", 3)) { + cmdlineP->maskColor[colorCount].matchType = MATCH_BK; + cmdlineP->maskColor[colorCount].u.bkColor = + ppm_bk_color_from_name(&token[3]); + } else { + cmdlineP->maskColor[colorCount].matchType = MATCH_EXACT; + cmdlineP->maskColor[colorCount].u.color = + ppm_parsecolor(token, PPM_MAXMAXVAL); + } + ++colorCount; + } else + eol = TRUE; + } + free(colorOptWork); + + cmdlineP->colorCount = colorCount; +} + + + +static void +parseCommandLine(int argc, char ** argv, + struct cmdlineInfo *cmdlineP) { +/*---------------------------------------------------------------------------- + Note that many of the strings that this function returns in the + *cmdlineP structure are actually in the supplied argv array. And + sometimes, one of these strings is actually just a suffix of an entry + in argv! +-----------------------------------------------------------------------------*/ + optEntry * option_def; + /* Instructions to OptParseOptions3 on how to parse our options. */ + optStruct3 opt; + + unsigned int option_def_index; + const char * colorOpt; + unsigned int colorSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "color", OPT_STRING, &colorOpt, &colorSpec, 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 may have parms that are negative numbers */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and all of *cmdlineP. */ + + if (colorSpec) + parseColorOpt(colorOpt, cmdlineP); + + if (colorSpec) { + if (argc-1 < 1) + cmdlineP->inputFilename = "-"; /* he wants stdin */ + else if (argc-1 == 1) + cmdlineP->inputFilename = argv[1]; + else + pm_error("Too many arguments. When you specify -color, " + "the only argument accepted is the optional input " + "file name."); + } else { + if (argc-1 < 1) + pm_error("You must specify the -color option."); + else { + cmdlineP->colorCount = 1; + cmdlineP->maskColor[0].matchType = MATCH_EXACT; + cmdlineP->maskColor[0].u.color = + ppm_parsecolor(argv[1], PPM_MAXMAXVAL); + + if (argc - 1 < 2) + cmdlineP->inputFilename = "-"; /* he wants stdin */ + else if (argc-1 == 2) + cmdlineP->inputFilename = argv[2]; + else + pm_error("Too many arguments. The only arguments accepted " + "are the mask color and optional input file name"); + } + } +} + + + +static bool +isBkColor(pixel const comparator, + pixval const maxval, + bk_color const comparand) { + + /* TODO: keep a cache of the bk color for each color in + a colorhash_table. + */ + + bk_color const comparatorBk = ppm_bk_color_from_color(comparator, maxval); + + return comparatorBk == comparand; +} + + + +static bool +colorIsInSet(pixel const color, + pixval const maxval, + struct cmdlineInfo const cmdline) { + + bool isInSet; + unsigned int i; + + for (i = 0, isInSet = FALSE; + i < cmdline.colorCount && !isInSet; ++i) { + + assert(i < ARRAY_SIZE(cmdline.maskColor)); + + switch(cmdline.maskColor[i].matchType) { + case MATCH_EXACT: + if (PPM_EQUAL(color, cmdline.maskColor[i].u.color)) + isInSet = TRUE; + break; + case MATCH_BK: + if (isBkColor(color, maxval, cmdline.maskColor[i].u.bkColor)) + isInSet = TRUE; + break; + } + } + return isInSet; +} + + + +int +main(int argc, char *argv[]) { + + struct cmdlineInfo cmdline; + + FILE * ifP; + + /* Parameters of input image: */ + int rows, cols; + pixval maxval; + int format; + + ppm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilename); + + ppm_readppminit(ifP, &cols, &rows, &maxval, &format); + pbm_writepbminit(stdout, cols, rows, 0); + { + pixel * const inputRow = ppm_allocrow(cols); + bit * const maskRow = pbm_allocrow(cols); + + unsigned int numPixelsMasked; + + unsigned int row; + for (row = 0, numPixelsMasked = 0; row < rows; ++row) { + int col; + ppm_readppmrow(ifP, inputRow, cols, maxval, format); + for (col = 0; col < cols; ++col) { + if (colorIsInSet(inputRow[col], maxval, cmdline)) { + maskRow[col] = PBM_BLACK; + ++numPixelsMasked; + } else + maskRow[col] = PBM_WHITE; + } + pbm_writepbmrow(stdout, maskRow, cols, 0); + } + + if (cmdline.verbose) + pm_message("%u pixels found matching %u requested colors", + numPixelsMasked, cmdline.colorCount); + + pbm_freerow(maskRow); + ppm_freerow(inputRow); + } + pm_close(ifP); + + return 0; +} + + + diff --git a/editor/ppmdim.c b/editor/ppmdim.c new file mode 100644 index 00000000..4e64965a --- /dev/null +++ b/editor/ppmdim.c @@ -0,0 +1,112 @@ + +/*********************************************************************/ +/* ppmdim - dim a picture down to total blackness */ +/* Frank Neumann, October 1993 */ +/* V1.4 16.11.1993 */ +/* */ +/* version history: */ +/* V1.0 ~ 15.August 1993 first version */ +/* V1.1 03.09.1993 uses ppm libs & header files */ +/* V1.2 03.09.1993 integer arithmetics instead of float */ +/* (gains about 50 % speed up) */ +/* V1.3 10.10.1993 reads only one line at a time - this */ +/* saves LOTS of memory on big pictures */ +/* V1.4 16.11.1993 Rewritten to be NetPBM.programming con- */ +/* forming */ +/*********************************************************************/ + +#include "ppm.h" + +/* global variables */ +#ifdef AMIGA +static char *version = "$VER: ppmdim 1.4 (16.11.93)"; /* Amiga version identification */ +#endif + +/**************************/ +/* start of main function */ +/**************************/ +int main(argc, argv) +int argc; +char *argv[]; +{ + FILE* ifp; + int argn, rows, cols, format, i = 0, j = 0; + pixel *srcrow, *destrow; + pixel *pP = NULL, *pP2 = NULL; + pixval maxval; + double dimfactor; + long longfactor; + const char * const usage = "dimfactor [ppmfile]\n dimfactor: 0.0 = total blackness, 1.0 = original picture\n"; + + /* parse in 'default' parameters */ + ppm_init(&argc, argv); + + argn = 1; + + /* parse in dim factor */ + if (argn == argc) + pm_usage(usage); + if (sscanf(argv[argn], "%lf", &dimfactor) != 1) + pm_usage(usage); + if (dimfactor < 0.0 || dimfactor > 1.0) + pm_error("dim factor must be in the range from 0.0 to 1.0 "); + ++argn; + + /* parse in filename (if present, stdin otherwise) */ + if (argn != argc) + { + ifp = pm_openr(argv[argn]); + ++argn; + } + else + ifp = stdin; + + if (argn != argc) + pm_usage(usage); + + /* read first data from file */ + ppm_readppminit(ifp, &cols, &rows, &maxval, &format); + + /* no error checking required here, ppmlib does it all for us */ + srcrow = ppm_allocrow(cols); + + longfactor = (long)(dimfactor * 65536); + + /* allocate a row of pixel data for the new pixels */ + destrow = ppm_allocrow(cols); + + ppm_writeppminit(stdout, cols, rows, maxval, 0); + + /** now do the dim'ing **/ + /* the 'float' parameter for dimming is sort of faked - in fact, we */ + /* convert it to a range from 0 to 65536 for integer math. Shouldn't */ + /* be something you'll have to worry about, though. */ + + for (i = 0; i < rows; i++) + { + ppm_readppmrow(ifp, srcrow, cols, maxval, format); + + pP = srcrow; + pP2 = destrow; + + for (j = 0; j < cols; j++) + { + PPM_ASSIGN(*pP2, (PPM_GETR(*pP) * longfactor) >> 16, + (PPM_GETG(*pP) * longfactor) >> 16, + (PPM_GETB(*pP) * longfactor) >> 16); + + pP++; + pP2++; + } + + /* write out one line of graphic data */ + ppm_writeppmrow(stdout, destrow, cols, maxval, 0); + } + + pm_close(ifp); + ppm_freerow(srcrow); + ppm_freerow(destrow); + + exit(0); +} + diff --git a/editor/ppmdist.c b/editor/ppmdist.c new file mode 100644 index 00000000..90c2e3d3 --- /dev/null +++ b/editor/ppmdist.c @@ -0,0 +1,170 @@ +#include "ppm.h" +#include "mallocvar.h" + + +/* + * Yep, it's a very simple algorithm, but it was something I wanted to have + * available. + */ + +struct colorToGrayEntry { + pixel color; + gray gray; + int frequency; +}; + +/* + * BUG: This number was chosen pretty arbitrarily. The program is * probably + * only useful for a very small numbers of colors - and that's * not only + * because of the O(n) search that's used. The idea lends * itself primarily + * to low color (read: simple, machine generated) images. + */ +#define MAXCOLORS 255 + + +static gray +newGrayValue(pixel *pix, struct colorToGrayEntry *colorToGrayMap, int colors) { + + int color; + /* + * Allowing this to be O(n), since the program is intended for small + * n. Later, perhaps sort by color (r, then g, then b) and bsearch. + */ + for (color = 0; color < colors; color++) { + if (PPM_EQUAL(*pix, colorToGrayMap[color].color)) + return colorToGrayMap[color].gray; + } + pm_error("This should never happen - contact the maintainer"); + return (-1); +} + + + +static int +cmpColorToGrayEntryByIntensity(const void *entry1, const void *entry2) { + + return ((struct colorToGrayEntry *) entry1)->gray - + ((struct colorToGrayEntry *) entry2)->gray; +} + + + +static int +cmpColorToGrayEntryByFrequency(const void * entry1, const void * entry2) { + + return ((struct colorToGrayEntry *) entry1)->frequency - + ((struct colorToGrayEntry *) entry2)->frequency; +} + + + +int +main(int argc, char *argv[]) { + + FILE *ifp; + int col, cols, row, rows, color, colors, argn; + int frequency; + pixval maxval; + pixel **pixels; + pixel *pP; + colorhist_vector hist; + gray *grayrow; + gray *gP; + struct colorToGrayEntry *colorToGrayMap; + + + ppm_init(&argc, argv); + + argn = 1; + /* Default is to sort colors by intensity */ + frequency = 0; + + while (argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0') { + if (pm_keymatch(argv[argn], "-frequency", 2)) + frequency = 1; + else if (pm_keymatch(argv[argn], "-intensity", 2)) + frequency = 0; + else + pm_usage( "[-frequency|-intensity] [ppmfile]" ); + ++argn; + } + + if (argn < argc) { + ifp = pm_openr(argv[argn]); + ++argn; + } else + ifp = stdin; + + pixels = ppm_readppm(ifp, &cols, &rows, &maxval); + pm_close(ifp); + /* all done with the input file - it's entirely in memory */ + + /* + * Compute a histogram of the colors in the input. This is good for + * both frequency, and indirectly the intensity, of a color. + */ + hist = ppm_computecolorhist(pixels, cols, rows, MAXCOLORS, &colors); + + if (hist == (colorhist_vector) 0) + /* + * BUG: This perhaps should use an exponential backoff, in + * the number of colors, until success - cf pnmcolormap's + * approach. The results are then more what's expected, but + * not necessarily very useful. + */ + pm_error("Too many colors - Try reducing with pnmquant"); + + /* copy the colors into another structure for sorting */ + MALLOCARRAY(colorToGrayMap, colors); + for (color = 0; color < colors; color++) { + colorToGrayMap[color].color = hist[color].color; + colorToGrayMap[color].frequency = hist[color].value; + /* + * This next is derivable, of course, but it's far faster to + * store it precomputed. This can be skipped, when sorting + * by frequency - but again, for a small number of colors + * it's a small matter. + */ + colorToGrayMap[color].gray = PPM_LUMIN(hist[color].color); + } + + /* + * sort by intensity - sorting by frequency (in the histogram) is + * worth considering as a future addition. + */ + if (frequency) + qsort(colorToGrayMap, colors, sizeof(struct colorToGrayEntry), + &cmpColorToGrayEntryByFrequency); + else + qsort(colorToGrayMap, colors, sizeof(struct colorToGrayEntry), + &cmpColorToGrayEntryByIntensity); + + /* + * create mapping between the n colors in input, to n evenly spaced + * grayscale intensities. This is done by overwriting the neatly + * formed gray values corresponding to the input-colors, with a new + * set of evenly spaced gray values. Since maxval can be changed on + * a lark, we just use gray levels 0..colors-1, and adjust maxval + * accordingly + */ + maxval = colors - 1; + for (color = 0; color < colors; color++) + colorToGrayMap[color].gray = color; + + /* write pgm file, mapping colors to intensities */ + pgm_writepgminit(stdout, cols, rows, maxval, 0); + + grayrow = pgm_allocrow(cols); + + for (row = 0; row < rows; row++) { + for (col = 0, pP = pixels[row], gP = grayrow; col < cols; + col++, pP++, gP++) + *gP = newGrayValue(pP, colorToGrayMap, colors); + pgm_writepgmrow(stdout, grayrow, cols, maxval, 0); + } + + pm_close(stdout); + + exit(0); +} + diff --git a/editor/ppmdither.c b/editor/ppmdither.c new file mode 100644 index 00000000..beb45e2f --- /dev/null +++ b/editor/ppmdither.c @@ -0,0 +1,309 @@ +/* ppmdither.c - Ordered dithering of a color ppm file to a specified number +** of primary shades. +** +** Copyright (C) 1991 by Christos Zoulas. +** +** 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. +*/ + +#include "ppm.h" +#include "mallocvar.h" + +/* Besides having to have enough memory available, the limiting factor + in the dithering matrix power is the size of the dithering value. + We need 2*dith_power bits in an unsigned int. We also reserve + one bit to give headroom to do calculations with these numbers. +*/ +#define MAX_DITH_POWER ((sizeof(unsigned int)*8 - 1) / 2) + +typedef unsigned char ubyte; + +static unsigned int dith_power; /* base 2 log of dither matrix dimension */ +static unsigned int dith_dim; /* dimension of the dither matrix */ +static unsigned int dith_dm2; /* dith_dim squared */ +static unsigned int **dith_mat; /* the dithering matrix */ +static int debug; + +/* COLOR(): + * returns the index in the colormap for the + * r, g, b values specified. + */ +#define COLOR(r,g,b) (((r) * dith_ng + (g)) * dith_nb + (b)) + + + +static unsigned int +dither(pixval const p, + pixval const maxval, + unsigned int const d, + unsigned int const ditheredMaxval) { +/*---------------------------------------------------------------------------- + Return the dithered intensity for a component of a pixel whose real + intensity for that component is 'p' based on a maxval of 'maxval'. + The returned intensity is based on a maxval of ditheredMaxval. + + 'd' is the entry in the dithering matrix for the position of this pixel + within the dithered square. +-----------------------------------------------------------------------------*/ + unsigned int const ditherSquareMaxval = ditheredMaxval * dith_dm2; + /* This is the maxval for an intensity that an entire dithered + square can represent. + */ + pixval const pScaled = ditherSquareMaxval * p / maxval; + /* This is the input intensity P expressed with a maxval of + 'ditherSquareMaxval' + */ + + /* Now we scale the intensity back down to the 'ditheredMaxval', and + as that will involve rounding, we round up or down based on the position + in the dithered square, as determined by 'd' + */ + + return (pScaled + d) / dith_dm2; +} + + +/* + * Return the value of a dither matrix which is 2**dith_power elements + * square at Row x, Column y. + * [graphics gems, p. 714] + */ +static unsigned int +dith_value(unsigned int y, unsigned int x, const unsigned int dith_power) { + + unsigned int d; + + /* + * Think of d as the density. At every iteration, d is shifted + * left one and a new bit is put in the low bit based on x and y. + * If x is odd and y is even, or visa versa, then a bit is shifted in. + * This generates the checkerboard pattern seen in dithering. + * This quantity is shifted again and the low bit of y is added in. + * This whole thing interleaves a checkerboard pattern and y's bits + * which is what you want. + */ + int i; + for (i = 0, d = 0; i < dith_power; i++, x >>= 1, y >>= 1) + d = (d << 2) | (((x & 1) ^ (y & 1)) << 1) | (y & 1); + return(d); +} /* end dith_value */ + + + +static unsigned int ** +dith_matrix(unsigned int const dith_dim) { +/*---------------------------------------------------------------------------- + Create the dithering matrix for dimension 'dith_dim'. + + Return it in newly malloc'ed storage. + + Note that we assume 'dith_dim' is small enough that the dith_mat_sz + computed within fits in an int. Otherwise, results are undefined. +-----------------------------------------------------------------------------*/ + unsigned int ** dith_mat; + { + unsigned int const dith_mat_sz = + (dith_dim * sizeof(int *)) + /* pointers */ + (dith_dim * dith_dim * sizeof(int)); /* data */ + + dith_mat = (unsigned int **) malloc(dith_mat_sz); + + if (dith_mat == NULL) + pm_error("Out of memory. " + "Cannot allocate %d bytes for dithering matrix.", + dith_mat_sz); + } + { + unsigned int * const dat = (unsigned int *) &dith_mat[dith_dim]; + unsigned int y; + for (y = 0; y < dith_dim; y++) + dith_mat[y] = &dat[y * dith_dim]; + } + { + unsigned int y; + for (y = 0; y < dith_dim; y++) { + unsigned int x; + for (x = 0; x < dith_dim; x++) { + dith_mat[y][x] = dith_value(y, x, dith_power); + if (debug) + (void) fprintf(stderr, "%4d ", dith_mat[y][x]); + } + if (debug) + (void) fprintf(stderr, "\n"); + } + } + return dith_mat; +} + + + +static void +dith_setup(const unsigned int dith_power, + const unsigned int dith_nr, + const unsigned int dith_ng, + const unsigned int dith_nb, + const pixval output_maxval, + pixel ** const colormapP) { +/*---------------------------------------------------------------------------- + Set up the dithering parameters, color map (lookup table) and + dithering matrix. + + Return the colormap in newly malloc'ed storage and return its address + as *colormapP. +-----------------------------------------------------------------------------*/ + unsigned int r, g, b; + + if (dith_nr < 2) + pm_error("too few shades for red, minimum of 2"); + if (dith_ng < 2) + pm_error("too few shades for green, minimum of 2"); + if (dith_nb < 2) + pm_error("too few shades for blue, minimum of 2"); + + MALLOCARRAY(*colormapP, dith_nr * dith_ng * dith_nb); + if (*colormapP == NULL) + pm_error("Unable to allocate space for the color lookup table " + "(%d by %d by %d pixels).", dith_nr, dith_ng, dith_nb); + + for (r = 0; r < dith_nr; r++) + for (g = 0; g < dith_ng; g++) + for (b = 0; b < dith_nb; b++) { + PPM_ASSIGN((*colormapP)[COLOR(r,g,b)], + (r * output_maxval / (dith_nr - 1)), + (g * output_maxval / (dith_ng - 1)), + (b * output_maxval / (dith_nb - 1))); + } + + if (dith_power > MAX_DITH_POWER) { + pm_error("Dithering matrix power %d is too large. Must be <= %d", + dith_power, MAX_DITH_POWER); + } else { + dith_dim = (1 << dith_power); + dith_dm2 = dith_dim * dith_dim; + } + + dith_mat = dith_matrix(dith_dim); +} /* end dith_setup */ + + +/* + * Dither whole image + */ +static void +dith_dither(const unsigned int width, const unsigned int height, + const pixval maxval, + const pixel * const colormap, + pixel ** const input, pixel ** const output, + const unsigned int dith_nr, + const unsigned int dith_ng, + const unsigned int dith_nb, + const pixval output_maxval + ) { + + const unsigned int dm = (dith_dim - 1); /* A mask */ + unsigned int row, col; + + for (row = 0; row < height; row++) + for (col = 0; col < width; col++) { + unsigned int const d = dith_mat[row & dm][(width-col-1) & dm]; + pixel const input_pixel = input[row][col]; + unsigned int const dithered_r = + dither(PPM_GETR(input_pixel), maxval, d, dith_nr-1); + unsigned int const dithered_g = + dither(PPM_GETG(input_pixel), maxval, d, dith_ng-1); + unsigned int const dithered_b = + dither(PPM_GETB(input_pixel), maxval, d, dith_nb-1); + output[row][col] = + colormap[COLOR(dithered_r, dithered_g, dithered_b)]; + } +} + + +int +main( argc, argv ) + int argc; + char* argv[]; + { + FILE* ifp; + pixel *colormap; /* malloc'd */ + pixel **ipixels; /* Input image */ + pixel **opixels; /* Output image */ + int cols, rows; + pixval maxval; /* Maxval of the input image */ + pixval output_maxval; /* Maxval in the dithered output image */ + unsigned int argn; + const char* const usage = + "[-dim <num>] [-red <num>] [-green <num>] [-blue <num>] [ppmfile]"; + unsigned int dith_nr; /* number of red shades in output */ + unsigned int dith_ng; /* number of green shades in output */ + unsigned int dith_nb; /* number of blue shades in output */ + + + ppm_init( &argc, argv ); + + dith_nr = 5; /* default */ + dith_ng = 9; /* default */ + dith_nb = 5; /* default */ + + dith_power = 4; /* default */ + debug = 0; /* default */ + argn = 1; + + while ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' ) + { + if ( pm_keymatch( argv[argn], "-dim", 1) && argn + 1 < argc ) { + argn++; + if (sscanf(argv[argn], "%u", &dith_power) != 1) + pm_usage( usage ); + } + else if ( pm_keymatch( argv[argn], "-red", 1 ) && argn + 1 < argc ) { + argn++; + if (sscanf(argv[argn], "%u", &dith_nr) != 1) + pm_usage( usage ); + } + else if ( pm_keymatch( argv[argn], "-green", 1 ) && argn + 1 < argc ) { + argn++; + if (sscanf(argv[argn], "%u", &dith_ng) != 1) + pm_usage( usage ); + } + else if ( pm_keymatch( argv[argn], "-blue", 1 ) && argn + 1 < argc ) { + argn++; + if (sscanf(argv[argn], "%u", &dith_nb) != 1) + pm_usage( usage ); + } + else if ( pm_keymatch( argv[argn], "-debug", 6 )) { + debug = 1; + } + else + pm_usage( usage ); + ++argn; + } + + if ( argn != argc ) + { + ifp = pm_openr( argv[argn] ); + ++argn; + } + else + ifp = stdin; + + if ( argn != argc ) + pm_usage( usage ); + + ipixels = ppm_readppm( ifp, &cols, &rows, &maxval ); + pm_close( ifp ); + opixels = ppm_allocarray(cols, rows); + output_maxval = pm_lcm(dith_nr-1, dith_ng-1, dith_nb-1, PPM_MAXMAXVAL); + dith_setup(dith_power, dith_nr, dith_ng, dith_nb, output_maxval, + &colormap); + dith_dither(cols, rows, maxval, colormap, ipixels, opixels, + dith_nr, dith_ng, dith_nb, output_maxval); + ppm_writeppm(stdout, opixels, cols, rows, output_maxval, 0); + pm_close(stdout); + exit(0); +} diff --git a/editor/ppmdraw.c b/editor/ppmdraw.c new file mode 100644 index 00000000..5a4be96b --- /dev/null +++ b/editor/ppmdraw.c @@ -0,0 +1,885 @@ +#define _XOPEN_SOURCE /* Make sure M_PI is in math.h */ +#define _BSD_SOURCE /* Make sure strdup is in string.h */ + +#include <string.h> +#include <ctype.h> +#include <assert.h> +#include <math.h> + +#include "pm_c_util.h" +#include "shhopt.h" +#include "mallocvar.h" +#include "nstring.h" +#include "ppm.h" +#include "ppmdraw.h" +#include "ppmdfont.h" + +static bool verbose; + + +static double +sindeg(double const angle) { + + return sin((double)angle / 360 * 2 * M_PI); +} + + +static double +cosdeg(double const angle) { + + return cos((double)angle / 360 * 2 * M_PI); +} + + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char * inputFilename; /* '-' if stdin */ + const char * scriptfile; /* NULL means none. '-' means stdin */ + const char * script; /* NULL means none */ + unsigned int verbose; +}; + + + +static void +parseCommandLine (int argc, 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 option_def_index; + + unsigned int scriptSpec, scriptfileSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "script", OPT_STRING, &cmdlineP->script, + &scriptSpec, 0); + OPTENT3(0, "scriptfile", OPT_STRING, &cmdlineP->scriptfile, + &scriptfileSpec, 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 */ + + optParseOptions3( &argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (!scriptSpec && !scriptfileSpec) + pm_error("You must specify either -script or -scriptfile"); + + if (scriptSpec && scriptfileSpec) + pm_error("You may not specify both -script and -scriptfile"); + + if (!scriptSpec) + cmdlineP->script = NULL; + if (!scriptfileSpec) + cmdlineP->scriptfile = NULL; + + if (argc-1 < 1) { + if (cmdlineP->scriptfile && strcmp(cmdlineP->scriptfile, "-") == 0) + pm_error("You can't specify Standard Input for both the " + "input image and the script file"); + else + cmdlineP->inputFilename = "-"; + } + else if (argc-1 == 1) + cmdlineP->inputFilename = argv[1]; + else + pm_error("Program takes at most one argument: input file name"); +} + + +struct pos { + unsigned int x; + unsigned int y; +}; + +struct drawState { + struct pos currentPos; + pixel color; +}; + + + +static void +initDrawState(struct drawState * const drawStateP, + pixval const maxval) { + + drawStateP->currentPos.x = 0; + drawStateP->currentPos.y = 0; + PPM_ASSIGN(drawStateP->color, maxval, maxval, maxval); +} + + + +static void +readScriptFile(const char * const scriptFileName, + const char ** const scriptP) { + + FILE * scriptFileP; + char * script; + size_t scriptAllocation; + size_t bytesReadSoFar; + + scriptAllocation = 4096; + + MALLOCARRAY(script, scriptAllocation); + + if (script == NULL) + pm_error("out of memory reading script from file"); + + scriptFileP = pm_openr(scriptFileName); + + bytesReadSoFar = 0; + while (!feof(scriptFileP)) { + size_t bytesRead; + + if (scriptAllocation - bytesReadSoFar < 2) { + scriptAllocation += 4096; + REALLOCARRAY(script, scriptAllocation); + if (script == NULL) + pm_error("out of memory reading script from file"); + } + bytesRead = fread(script + bytesReadSoFar, 1, + scriptAllocation - bytesReadSoFar - 1, scriptFileP); + bytesReadSoFar += bytesRead; + } + pm_close(scriptFileP); + + { + unsigned int i; + for (i = 0; i < bytesReadSoFar; ++i) + if (!isprint(script[i]) && !isspace(script[i])) + pm_error("Script contains byte that is not printable ASCII " + "character: 0x%02x", script[i]); + } + script[bytesReadSoFar] = '\0'; /* terminating NUL */ + + *scriptP = script; +} + + + +enum drawVerb { + VERB_SETPOS, + VERB_SETLINETYPE, + VERB_SETLINECLIP, + VERB_SETCOLOR, + VERB_SETFONT, + VERB_LINE, + VERB_LINE_HERE, + VERB_SPLINE3, + VERB_CIRCLE, + VERB_FILLEDRECTANGLE, + VERB_TEXT, + VERB_TEXT_HERE +}; + +struct setposArg { + int x; + int y; +}; + +struct setlinetypeArg { + int type; +}; + +struct setlineclipArg { + unsigned int clip; +}; + +struct setcolorArg { + const char * colorName; +}; + +struct setfontArg { + const char * fontFileName; +}; + +struct lineArg { + int x0; + int y0; + int x1; + int y1; +}; + +struct lineHereArg { + int right; + int down; +}; + +struct spline3Arg { + int x0; + int y0; + int x1; + int y1; + int x2; + int y2; +}; + +struct circleArg { + int cx; + int cy; + unsigned int radius; +}; + +struct filledrectangleArg { + int x; + int y; + unsigned int width; + unsigned int height; +}; + +struct textArg { + int xpos; + int ypos; + unsigned int height; + int angle; + const char * text; +}; + + +struct drawCommand { + enum drawVerb verb; + union { + struct setposArg setposArg; + struct setlinetypeArg setlinetypeArg; + struct setlineclipArg setlineclipArg; + struct setcolorArg setcolorArg; + struct setfontArg setfontArg; + struct lineArg lineArg; + struct lineHereArg lineHereArg; + struct spline3Arg spline3Arg; + struct circleArg circleArg; + struct filledrectangleArg filledrectangleArg; + struct textArg textArg; + } u; +}; + + + +static void +freeDrawCommand(const struct drawCommand * const commandP) { + + switch (commandP->verb) { + case VERB_SETPOS: + break; + case VERB_SETLINETYPE: + break; + case VERB_SETLINECLIP: + break; + case VERB_SETCOLOR: + strfree(commandP->u.setcolorArg.colorName); + break; + case VERB_SETFONT: + strfree(commandP->u.setfontArg.fontFileName); + break; + case VERB_LINE: + break; + case VERB_LINE_HERE: + break; + case VERB_SPLINE3: + break; + case VERB_CIRCLE: + break; + case VERB_FILLEDRECTANGLE: + break; + case VERB_TEXT: + case VERB_TEXT_HERE: + strfree(commandP->u.textArg.text); + break; + } + + + free((void *) commandP); +} + + + +struct commandListElt { + struct commandListElt * nextP; + const struct drawCommand * commandP; +}; + + +struct script { + struct commandListElt * commandListHeadP; + struct commandListElt * commandListTailP; +}; + + + +static void +freeScript(struct script * const scriptP) { + + struct commandListElt * p; + + for (p = scriptP->commandListHeadP; p; p = p->nextP) { + freeDrawCommand(p->commandP); + free(p); + } + + free(scriptP); +} + + + +static void +doTextHere(pixel ** const pixels, + unsigned int const cols, + unsigned int const rows, + pixval const maxval, + const struct drawCommand * const commandP, + struct drawState * const drawStateP) { + + ppmd_text(pixels, cols, rows, maxval, + drawStateP->currentPos.x, + drawStateP->currentPos.y, + commandP->u.textArg.height, + commandP->u.textArg.angle, + commandP->u.textArg.text, + PPMD_NULLDRAWPROC, + &drawStateP->color); + + { + int left, top, right, bottom; + + ppmd_text_box(commandP->u.textArg.height, 0, + commandP->u.textArg.text, + &left, &top, &right, &bottom); + + + drawStateP->currentPos.x += + ROUND((right-left) * cosdeg(commandP->u.textArg.angle)); + drawStateP->currentPos.y -= + ROUND((right-left) * sindeg(commandP->u.textArg.angle)); + } +} + + + +static void +executeScript(struct script * const scriptP, + pixel ** const pixels, + unsigned int const cols, + unsigned int const rows, + pixval const maxval) { + + struct drawState drawState; + unsigned int seq; + /* Sequence number of current command (0 = first, etc.) */ + struct commandListElt * p; + /* Pointer to current element in command list */ + + initDrawState(&drawState, maxval); + + for (p = scriptP->commandListHeadP, seq = 0; p; p = p->nextP, ++seq) { + const struct drawCommand * const commandP = p->commandP; + + if (verbose) + pm_message("Command %u: %u", seq, commandP->verb); + + switch (commandP->verb) { + case VERB_SETPOS: + drawState.currentPos.x = commandP->u.setposArg.x; + drawState.currentPos.y = commandP->u.setposArg.y; + break; + case VERB_SETLINETYPE: + ppmd_setlinetype(commandP->u.setlinetypeArg.type); + break; + case VERB_SETLINECLIP: + ppmd_setlineclip(commandP->u.setlineclipArg.clip); + break; + case VERB_SETCOLOR: + drawState.color = + ppm_parsecolor2(commandP->u.setcolorArg.colorName, + maxval, TRUE); + break; + case VERB_SETFONT: { + FILE * ifP; + const struct ppmd_font * fontP; + ifP = pm_openr(commandP->u.setfontArg.fontFileName); + ppmd_read_font(ifP, &fontP); + ppmd_set_font(fontP); + pm_close(ifP); + } break; + case VERB_LINE: + ppmd_line(pixels, cols, rows, maxval, + commandP->u.lineArg.x0, commandP->u.lineArg.y0, + commandP->u.lineArg.x1, commandP->u.lineArg.y1, + PPMD_NULLDRAWPROC, + &drawState.color); + break; + case VERB_LINE_HERE: { + struct pos endPos; + + endPos.x = drawState.currentPos.x + commandP->u.lineHereArg.right; + endPos.y = drawState.currentPos.y + commandP->u.lineHereArg.down; + + ppmd_line(pixels, cols, rows, maxval, + drawState.currentPos.x, drawState.currentPos.y, + endPos.x, endPos.y, + PPMD_NULLDRAWPROC, + &drawState.color); + drawState.currentPos = endPos; + } break; + case VERB_SPLINE3: + ppmd_spline3(pixels, cols, rows, maxval, + commandP->u.spline3Arg.x0, + commandP->u.spline3Arg.y0, + commandP->u.spline3Arg.x1, + commandP->u.spline3Arg.y1, + commandP->u.spline3Arg.x2, + commandP->u.spline3Arg.y2, + PPMD_NULLDRAWPROC, + &drawState.color); + break; + case VERB_CIRCLE: + ppmd_circle(pixels, cols, rows, maxval, + commandP->u.circleArg.cx, + commandP->u.circleArg.cy, + commandP->u.circleArg.radius, + PPMD_NULLDRAWPROC, + &drawState.color); + break; + case VERB_FILLEDRECTANGLE: + ppmd_filledrectangle(pixels, cols, rows, maxval, + commandP->u.filledrectangleArg.x, + commandP->u.filledrectangleArg.y, + commandP->u.filledrectangleArg.width, + commandP->u.filledrectangleArg.height, + PPMD_NULLDRAWPROC, + &drawState.color); + break; + case VERB_TEXT: + ppmd_text(pixels, cols, rows, maxval, + commandP->u.textArg.xpos, + commandP->u.textArg.ypos, + commandP->u.textArg.height, + commandP->u.textArg.angle, + commandP->u.textArg.text, + PPMD_NULLDRAWPROC, + &drawState.color); + break; + case VERB_TEXT_HERE: + doTextHere(pixels, cols, rows, maxval, commandP, &drawState); + break; + } + } +} + + + +struct tokenSet { + + const char * token[10]; + unsigned int count; + +}; + + + +static void +parseDrawCommand(struct tokenSet const commandTokens, + const struct drawCommand ** const drawCommandPP) { + + struct drawCommand * drawCommandP; + + if (commandTokens.count < 1) + pm_error("No tokens in command."); + else { + const char * const verb = commandTokens.token[0]; + + MALLOCVAR(drawCommandP); + if (drawCommandP == NULL) + pm_error("Out of memory to parse '%s' command", verb); + + if (STREQ(verb, "setpos")) { + drawCommandP->verb = VERB_SETPOS; + if (commandTokens.count < 3) + pm_error("Not enough tokens for a 'setpos' command. " + "Need %u. Got %u", 3, commandTokens.count); + else { + drawCommandP->u.setposArg.x = atoi(commandTokens.token[1]); + drawCommandP->u.setposArg.y = atoi(commandTokens.token[2]); + } + } else if (STREQ(verb, "setlinetype")) { + drawCommandP->verb = VERB_SETLINETYPE; + if (commandTokens.count < 2) + pm_error("Not enough tokens for a 'setlinetype' command. " + "Need %u. Got %u", 2, commandTokens.count); + else { + const char * const typeArg = commandTokens.token[1]; + if (STREQ(typeArg, "normal")) + drawCommandP->u.setlinetypeArg.type = PPMD_LINETYPE_NORMAL; + else if (STREQ(typeArg, "normal")) + drawCommandP->u.setlinetypeArg.type = + PPMD_LINETYPE_NODIAGS; + else + pm_error("Invalid type"); + } + } else if (STREQ(verb, "setlineclip")) { + drawCommandP->verb = VERB_SETLINECLIP; + if (commandTokens.count < 2) + pm_error("Not enough tokens for a 'setlineclip' command. " + "Need %u. Got %u", 2, commandTokens.count); + else + drawCommandP->u.setlineclipArg.clip = + atoi(commandTokens.token[1]); + } else if (STREQ(verb, "setcolor")) { + drawCommandP->verb = VERB_SETCOLOR; + if (commandTokens.count < 2) + pm_error("Not enough tokens for a 'setcolor' command. " + "Need %u. Got %u", 2, commandTokens.count); + else + drawCommandP->u.setcolorArg.colorName = + strdup(commandTokens.token[1]); + } else if (STREQ(verb, "setfont")) { + drawCommandP->verb = VERB_SETFONT; + if (commandTokens.count < 2) + pm_error("Not enough tokens for a 'setfont' command. " + "Need %u. Got %u", 2, commandTokens.count); + else + drawCommandP->u.setfontArg.fontFileName = + strdup(commandTokens.token[1]); + } else if (STREQ(verb, "line")) { + drawCommandP->verb = VERB_LINE; + if (commandTokens.count < 5) + pm_error("Not enough tokens for a 'line' command. " + "Need %u. Got %u", 5, commandTokens.count); + else { + drawCommandP->u.lineArg.x0 = atoi(commandTokens.token[1]); + drawCommandP->u.lineArg.y0 = atoi(commandTokens.token[2]); + drawCommandP->u.lineArg.x1 = atoi(commandTokens.token[3]); + drawCommandP->u.lineArg.y1 = atoi(commandTokens.token[4]); + } + } else if (STREQ(verb, "line_here")) { + drawCommandP->verb = VERB_LINE_HERE; + if (commandTokens.count < 3) + pm_error("Not enough tokens for a 'line_here' command. " + "Need %u. Got %u", 3, commandTokens.count); + else { + struct lineHereArg * const argP = + &drawCommandP->u.lineHereArg; + argP->right = atoi(commandTokens.token[1]); + argP->down = atoi(commandTokens.token[2]); + } + } else if (STREQ(verb, "spline3")) { + drawCommandP->verb = VERB_SPLINE3; + if (commandTokens.count < 7) + pm_error("Not enough tokens for a 'spline3' command. " + "Need %u. Got %u", 7, commandTokens.count); + else { + struct spline3Arg * const argP = + &drawCommandP->u.spline3Arg; + argP->x0 = atoi(commandTokens.token[1]); + argP->y0 = atoi(commandTokens.token[2]); + argP->x1 = atoi(commandTokens.token[3]); + argP->y1 = atoi(commandTokens.token[4]); + argP->x2 = atoi(commandTokens.token[5]); + argP->y2 = atoi(commandTokens.token[6]); + } + } else if (STREQ(verb, "circle")) { + drawCommandP->verb = VERB_CIRCLE; + if (commandTokens.count < 4) + pm_error("Not enough tokens for a 'circle' command. " + "Need %u. Got %u", 4, commandTokens.count); + else { + struct circleArg * const argP = &drawCommandP->u.circleArg; + argP->cx = atoi(commandTokens.token[1]); + argP->cy = atoi(commandTokens.token[2]); + argP->radius = atoi(commandTokens.token[3]); + } + } else if (STREQ(verb, "filledrectangle")) { + drawCommandP->verb = VERB_FILLEDRECTANGLE; + if (commandTokens.count < 5) + pm_error("Not enough tokens for a 'filledrectangle' command. " + "Need %u. Got %u", 4, commandTokens.count); + else { + struct filledrectangleArg * const argP = + &drawCommandP->u.filledrectangleArg; + argP->x = atoi(commandTokens.token[1]); + argP->y = atoi(commandTokens.token[2]); + argP->width = atoi(commandTokens.token[3]); + argP->height = atoi(commandTokens.token[4]); + } + } else if (STREQ(verb, "text")) { + drawCommandP->verb = VERB_TEXT; + if (commandTokens.count < 6) + pm_error("Not enough tokens for a 'text' command. " + "Need %u. Got %u", 6, commandTokens.count); + else { + drawCommandP->u.textArg.xpos = atoi(commandTokens.token[1]); + drawCommandP->u.textArg.ypos = atoi(commandTokens.token[2]); + drawCommandP->u.textArg.height= atoi(commandTokens.token[3]); + drawCommandP->u.textArg.angle = atoi(commandTokens.token[4]); + drawCommandP->u.textArg.text = strdup(commandTokens.token[5]); + if (drawCommandP->u.textArg.text == NULL) + pm_error("Out of storage parsing 'text' command"); + } + } else if (STREQ(verb, "text_here")) { + drawCommandP->verb = VERB_TEXT_HERE; + if (commandTokens.count < 4) + pm_error("Not enough tokens for a 'text_here' command. " + "Need %u. Got %u", 4, commandTokens.count); + else { + drawCommandP->u.textArg.height= atoi(commandTokens.token[1]); + drawCommandP->u.textArg.angle = atoi(commandTokens.token[2]); + drawCommandP->u.textArg.text = strdup(commandTokens.token[3]); + if (drawCommandP->u.textArg.text == NULL) + pm_error("Out of storage parsing 'text_here' command"); + } + } else + pm_error("Unrecognized verb '%s'", verb); + } + *drawCommandPP = drawCommandP; +} + + + +static void +disposeOfCommandTokens(struct tokenSet * const tokenSetP, + struct script * const scriptP) { + + /* We've got a whole command in 'tokenSet'. Parse it into *scriptP + and reset tokenSet to empty. + */ + + struct commandListElt * commandListEltP; + + MALLOCVAR(commandListEltP); + if (commandListEltP == NULL) + pm_error("Out of memory allocating command list element frame"); + + parseDrawCommand(*tokenSetP, &commandListEltP->commandP); + + { + unsigned int i; + for (i = 0; i < tokenSetP->count; ++i) + strfree(tokenSetP->token[i]); + tokenSetP->count = 0; + } + /* Put the list element for this command at the tail of the list */ + commandListEltP->nextP = NULL; + if (scriptP->commandListTailP) + scriptP->commandListTailP->nextP = commandListEltP; + else + scriptP->commandListHeadP = commandListEltP; + + scriptP->commandListTailP = commandListEltP; +} + + + +static void +processToken(const char * const scriptText, + unsigned int const cursor, + unsigned int const tokenStart, + struct script * const scriptP, + struct tokenSet * const tokenSetP) { + + char * token; + unsigned int const tokenLength = cursor - tokenStart; + MALLOCARRAY_NOFAIL(token, tokenLength + 1); + memcpy(token, &scriptText[tokenStart], tokenLength); + token[tokenLength] = '\0'; + + if (STREQ(token, ";")) { + disposeOfCommandTokens(tokenSetP, scriptP); + free(token); + } else { + if (tokenSetP->count >= ARRAY_SIZE(tokenSetP->token)) + pm_error("too many tokens"); + else + tokenSetP->token[tokenSetP->count++] = token; + } +} + + + +static void +parseScript(const char * const scriptText, + struct script ** const scriptPP) { + + struct script * scriptP; + unsigned int cursor; /* cursor in scriptText[] */ + bool intoken; /* Cursor is inside token */ + unsigned int tokenStart; + /* Position in 'scriptText' where current token starts. + Meaningless if 'intoken' is false. + */ + bool quotedToken; + /* Current token is a quoted string. Meaningless if 'intoken' + is false + */ + struct tokenSet tokenSet; + + MALLOCVAR_NOFAIL(scriptP); + + scriptP->commandListHeadP = NULL; + scriptP->commandListTailP = NULL; + + /* A token begins with a non-whitespace character. A token ends before + a whitespace character or semicolon or end of script, except that if + the token starts with a double quote, whitespace and semicolon don't + end it and another double quote does. + + Semicolon (unquoted) is a token by itself. + */ + + tokenSet.count = 0; + intoken = FALSE; + tokenStart = 0; + cursor = 0; + + while (scriptText[cursor] != '\0') { + char const scriptChar = scriptText[cursor]; + + if (intoken) { + if ((quotedToken && scriptChar == '"') || + (!quotedToken && (isspace(scriptChar) || scriptChar == ';'))) { + /* We've passed a token. */ + + processToken(scriptText, cursor, tokenStart, scriptP, + &tokenSet); + + intoken = FALSE; + if (scriptChar != ';') + ++cursor; + } else + ++cursor; + } else { + if (!isspace(scriptChar)) { + /* A token starts here */ + + if (scriptChar == ';') + /* It ends here too -- semicolon is token by itself */ + processToken(scriptText, cursor+1, cursor, scriptP, + &tokenSet); + else { + intoken = TRUE; + quotedToken = (scriptChar == '"'); + if (quotedToken) + tokenStart = cursor + 1; + else + tokenStart = cursor; + } + } + ++cursor; + } + } + + if (intoken) { + /* Parse the last token, which was terminated by end of string */ + if (quotedToken) + pm_error("Script ends in the middle of a quoted string"); + processToken(scriptText, cursor, tokenStart, scriptP, &tokenSet); + } + + if (tokenSet.count > 0) { + /* Parse the last command, which was not terminated with a semicolon. + */ + disposeOfCommandTokens(&tokenSet, scriptP); + } + + *scriptPP = scriptP; +} + + + +static void +getScript(struct cmdlineInfo const cmdline, + struct script ** const scriptPP) { + + const char * scriptText; + + if (cmdline.script) { + scriptText = strdup(cmdline.script); + if (scriptText == NULL) + pm_error("Out of memory creating script"); + } else if (cmdline.scriptfile) + readScriptFile(cmdline.scriptfile, &scriptText); + else + pm_error("INTERNAL ERROR: no script"); + + if (verbose) + pm_message("Executing script '%s'", scriptText); + + parseScript(scriptText, scriptPP); + + strfree(scriptText); +} + + + +static void +doOneImage(FILE * const ifP, + struct script * const scriptP) { + + pixel ** pixels; + pixval maxval; + int rows, cols; + + pixels = ppm_readppm(ifP, &cols, &rows, &maxval); + + executeScript(scriptP, pixels, cols, rows, maxval); + + ppm_writeppm(stdout, pixels, cols, rows, maxval, 0); + + ppm_freearray(pixels, rows); +} + + + +int +main(int argc, char * argv[]) { + + struct cmdlineInfo cmdline; + FILE * ifP; + struct script * scriptP; + bool eof; + + ppm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + verbose = cmdline.verbose; + + ifP = pm_openr(cmdline.inputFilename); + + getScript(cmdline, &scriptP); + + eof = FALSE; + while (!eof) { + doOneImage(ifP, scriptP); + ppm_nextimage(ifP, &eof); + } + + freeScript(scriptP); + + pm_close(ifP); + + /* If the program failed, it previously aborted with nonzero completion + code, via various function calls. + */ + return 0; +} diff --git a/editor/ppmfade b/editor/ppmfade new file mode 100755 index 00000000..2507eaf2 --- /dev/null +++ b/editor/ppmfade @@ -0,0 +1,309 @@ +#!/usr/bin/perl -w +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +# +# This program creates a fade (a sequence of frames) between two images. +# +# By Bryan Henderson, Olympia WA; March 2000 +# +# Contributed to the public domain by its author. +# +# Inspired by the program Pbmfade by Wesley C. Barris of AHPCRC, +# Minnesota Supercomputer Center, Inc. January 7, 1994. Pbmfade does +# much the same thing, but handles non-Netpbm formats too, and is +# implemented in a more primitive language. +# +#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +use strict; + +my $SPREAD = 1; +my $SHIFT = 2; +my $RELIEF = 3; +my $OIL = 4; +my $EDGE = 5; +my $BENTLEY = 6; +my $BLOCK = 7; +my $MIX = 8; +# +# Set some defaults. +# +my $nframes = 30; # total number of files created (1 sec) +my $first_file = "undefined"; +my $last_file = "undefined"; +my $base_name = "fade"; # default base name of output files +my $image = "ppm"; # default output storage format +my $mode = $SPREAD; # default fading mode +# +# Check those command line args. +# +if (@ARGV == 0) { + usage(); +} + +my $n; # argument number + +for ($n = 0; $n < @ARGV; $n++) { + if ("$ARGV[$n]" eq "-f") { + $n++; + $first_file = $ARGV[$n]; + if (-e $first_file) { + } else { + print "I can't find $first_file\n"; + exit 20; + } + } elsif ($ARGV[$n] eq "-l") { + $n++; + $last_file = $ARGV[$n]; + if (-e $last_file) { + } else { + print "I can't find $last_file\n"; + exit 20; + } + } elsif ($ARGV[$n] eq "-base") { + $n++; + $base_name = $ARGV[$n]; + } elsif ($ARGV[$n] eq "-spread") { + $mode = $SPREAD; + } elsif ($ARGV[$n] eq "-shift") { + $mode = $SHIFT; + } elsif ($ARGV[$n] eq "-relief") { + $mode = $RELIEF; + } elsif ($ARGV[$n] eq "-oil") { + $mode = $OIL; + } elsif ("$ARGV[$n]" eq "-edge") { + $mode = $EDGE; + } elsif ("$ARGV[$n]" eq "-bentley") { + $mode = $BENTLEY; + } elsif ("$ARGV[$n]" eq "-block") { + $mode = $BLOCK; + } elsif ("$ARGV[$n]" eq "-mix") { + $mode = $MIX; + } elsif ($ARGV[$n] eq "-help" || $ARGV[$n] eq "-h") { + usage(); + } else { + print "Unknown argument: $ARGV[$n]\n"; + exit 100; + } +} +# +# Define a couple linear ramps. +# +# We don't use element 0 of these arrays. +my @spline10 = (0, 0, 0.11, 0.22, 0.33, 0.44, 0.55, 0.66, 0.77, 0.88, 1.0); +my @spline20 = (0, 0, 0.05, 0.11, 0.16, 0.21, 0.26, 0.32, 0.37, 0.42, 0.47, + 0.53, 0.58, 0.63, 0.69, 0.74, 0.79, 0.84, 0.89, 0.95, 1.0); +# +# Just what are we supposed to do? +# +my ($height, $width); # width and height of our frames +if ($first_file ne "undefined") { + if ((`pnmfile $first_file` =~ m{\b(\d+)\sby\s(\d+)} )) { + $width = $1; $height = $2; + } else { + print("Unrecognized results from pnmfile on $first_file.\n"); + exit(50); + } +} elsif ($last_file ne "undefined") { + if ((`pnmfile $last_file` =~ m{\b(\d+)\sby\s(\d+)} )) { + $width = $1; $height = $2; + } else { + print("Unrecognized results from pnmfile on $first_file.\n"); + exit(50); + } +} else { + print("ppmfade: You must specify -f or -l (or both)\n"); + exit(90); +} + +print("Frames are " . $width . "W x " . $height . "H\n"); + +if ($first_file eq "undefined") { + print "Fading from black to "; + system("ppmmake \\#000 $width $height >junk1$$.ppm"); +} else { + print "Fading from $first_file to "; + system("cp", $first_file, "junk1$$.ppm"); +} + +if ($last_file eq "undefined") { + print "black.\n"; + system("ppmmake \\#000 $width $height >junk2$$.ppm"); +} else { + print "$last_file\n"; + system("cp", $last_file, "junk2$$.ppm"); +} + +# +# Perform the fade. +# + +# Here's what our temporary files are: +# junk1$$.ppm: The original (fade-from) image +# junk2$$.ppm: The target (fade-from) image +# junk3$$.ppm: The frame of the fade for the current iteration of the +# the for loop. +# junk1a$$.ppm: If the fade involves a ppmmix sequence from one intermediate +# image to another, this is the first frame of that +# sequence. +# junk2a$$.ppm: This is the last frame of the above-mentioned ppmmix sequence + +my $i; # Frame number +for ($i = 1; $i <= $nframes; $i++) { + print("Creating $i of $nframes...\n"); + if ($mode eq $SPREAD) { + if ($i <= 10) { + my $n = $spline20[$i] * 100; + system("ppmspread $n junk1$$.ppm >junk3$$.ppm"); + } elsif ($i <= 20) { + my $n; + $n = $spline20[$i] * 100; + system("ppmspread $n junk1$$.ppm >junk1a$$.ppm"); + $n = (1-$spline20[$i-10]) * 100; + system("ppmspread $n junk2$$.ppm >junk2a$$.ppm"); + $n = $spline10[$i-10]; + system("ppmmix $n junk1a$$.ppm junk2a$$.ppm >junk3$$.ppm"); + } else { + my $n = (1-$spline20[$i-10])*100; + system("ppmspread $n junk2$$.ppm >junk3$$.ppm"); + } + } elsif ($mode eq $SHIFT) { + if ($i <= 10) { + my $n = $spline20[$i] * 100; + system("ppmshift $n junk1$$.ppm >junk3$$.ppm"); + } elsif ($i <= 20) { + my $n; + $n = $spline20[$i] * 100; + system("ppmshift $n junk1$$.ppm >junk1a$$.ppm"); + $n = (1-$spline20[$i-10])*100; + system("ppmshift $n junk2$$.ppm >junk2a$$.ppm"); + $n = $spline10[$i-10]; + system("ppmmix $n junk1a$$.ppm junk2a$$.ppm >junk3$$.ppm"); + } else { + my $n = (1-$spline20[$i-10]) * 100; + system("ppmshift $n junk2$$.ppm >junk3$$.ppm"); + } + } elsif ($mode eq $RELIEF) { + if ($i == 1) { + system("ppmrelief junk1$$.ppm >junk1r$$.ppm"); + } + if ($i <= 10) { + my $n = $spline10[$i]; + system("ppmmix $n junk1$$.ppm junk1r$$.ppm >junk3$$.ppm"); + } elsif ($i <= 20) { + my $n = $spline10[$i-10]; + system("ppmmix $n junk1r$$.ppm junk2r$$.ppm >junk3$$.ppm"); + } else { + my $n = $spline10[$i-20]; + system("ppmmix $n junk2r$$.ppm junk2$$.ppm >junk3$$.ppm"); + } + if ($i == 10) { + system("ppmrelief junk2$$.ppm >junk2r$$.ppm"); + } + } elsif ($mode eq $OIL) { + if ($i == 1) { + system("ppmtopgm junk1$$.ppm | pgmoil >junko$$.ppm"); + system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " . + ">junk1o$$.ppm"); + } + if ($i <= 10) { + my $n = $spline10[$i]; + system("ppmmix $n junk1$$.ppm junk1o$$.ppm >junk3$$.ppm"); + } elsif ($i <= 20) { + my $n = $spline10[$i-10]; + system("ppmmix $n junk1o$$.ppm junk2o$$.ppm >junk3$$.ppm"); + } else { + my $n = $spline10[$i-20]; + system("ppmmix $n junk2o$$.ppm junk2$$.ppm >junk3$$.ppm"); + } + if ($i == 10) { + system("ppmtopgm junk2$$.ppm | pgmoil >junko$$.ppm"); + system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " . + ">junk2o$$.ppm"); + } + } elsif ($mode eq $EDGE) { + if ($i == 1) { + system("ppmtopgm junk1$$.ppm | pgmedge >junko$$.ppm"); + system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " . + ">junk1o$$.ppm"); + } + if ($i <= 10) { + my $n = $spline10[$i]; + system("ppmmix $n junk1$$.ppm junk1o$$.ppm >junk3$$.ppm"); + } elsif ($i <= 20) { + my $n = $spline10[$i-10]; + system("ppmmix $n junk1o$$.ppm junk2o$$.ppm >junk3$$.ppm"); + } else { + my $n = $spline10[$i-20]; + system("ppmmix $n junk2o$$.ppm junk2$$.ppm >junk3$$.ppm"); + } + if ($i == 10) { + system("ppmtopgm junk2$$.ppm | pgmedge >junko$$.ppm"); + system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " . + ">junk2o$$.ppm"); + } + } elsif ($mode eq $BENTLEY) { + if ($i == 1) { + system("ppmtopgm junk1$$.ppm | pgmbentley >junko$$.ppm"); + system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " . + ">junk1o$$.ppm"); + } + if ($i <= 10) { + my $n = $spline10[$i]; + system("ppmmix $n junk1$$.ppm junk1o$$.ppm >junk3$$.ppm"); + } elsif ($i <= 20) { + my $n = $spline10[$i-10]; + system("ppmmix $n junk1o$$.ppm junk2o$$.ppm >junk3$$.ppm"); + } else { + my $n = $spline10[$i-20]; + system("ppmmix $n junk2o$$.ppm junk2$$.ppm >junk3$$.ppm"); + } + if ($i == 10) { + system("ppmtopgm junk2$$.ppm | pgmbentley >junko$$.ppm"); + system("rgb3toppm junko$$.ppm junko$$.ppm junko$$.ppm " . + ">junk2o$$.ppm"); + } + } elsif ($mode eq $BLOCK) { + if ($i <= 10) { + my $n = 1 - 1.9*$spline20[$i]; + system("pamscale $n junk1$$.ppm | " . + "pamscale -width $width -height $height >junk3$$.ppm"); + } elsif ($i <= 20) { + my $n = $spline10[$i-10]; + system("ppmmix $n junk1a$$.ppm junk2a$$.ppm >junk3$$.ppm"); + } else { + my $n = 1 - 1.9*$spline20[31-$i]; + system("pamscale $n junk2$$.ppm | " . + "pamscale -width $width -height $height >junk3$$.ppm"); + } + if ($i == 10) { + system("cp", "junk3$$.ppm", "junk1a$$.ppm"); + system("pamscale $n junk2$$.ppm | " . + "pamscale -width $width -height $height >junk2a$$.ppm"); + } + } elsif ($mode eq $MIX) { + my $fade_factor = sqrt(1/($nframes-$i+1)); + system("ppmmix $fade_factor junk1$$.ppm junk2$$.ppm >junk3$$.ppm"); + } else { + print("Internal error: impossible mode value '$mode'\n"); + } + + my $outfile = sprintf("%s.%04d.ppm", $base_name, $i); + system("cp", "junk3$$.ppm", $outfile); +} + +# +# Clean up shop. +# +system("rm junk*$$.ppm"); + +exit(0); + + + +sub usage() { + print "Usage: ppmfade [-f first_file] [-l last_file]\n"; + print " [-spread|-relief|-oil|-edge|-bentley|-block]\n"; + print " [-base basename]\n"; + print "Notes: Default base: fade\n"; + print " The resulting image files will be named fade.NNNN.ppm.\n"; + exit(100); +} diff --git a/editor/ppmflash.c b/editor/ppmflash.c new file mode 100644 index 00000000..d1d048df --- /dev/null +++ b/editor/ppmflash.c @@ -0,0 +1,114 @@ + +/*********************************************************************/ +/* ppmflash - brighten a picture up to total whiteout */ +/* Frank Neumann, August 1993 */ +/* V1.4 16.11.1993 */ +/* */ +/* version history: */ +/* V1.0 ~ 15.August 1993 first version */ +/* V1.1 03.09.1993 uses ppm libs & header files */ +/* V1.2 03.09.1993 integer arithmetics instead of float */ +/* (gains about 50 % speed up) */ +/* V1.3 11.10.1993 reads only one line at a time - this */ +/* saves LOTS of memory on big picturs */ +/* V1.4 16.11.1993 Rewritten to be NetPBM.programming con- */ +/* forming */ +/*********************************************************************/ + +#include "ppm.h" + +/* global variables */ +#ifdef AMIGA +static char *version = "$VER: ppmflash 1.4 (16.11.93)"; +#endif + +/**************************/ +/* start of main function */ +/**************************/ +int main(argc, argv) + int argc; + char *argv[]; +{ + FILE* ifp; + int argn, rows, cols, i, j, format; + pixel *srcrow, *destrow; + pixel *pP, *pP2; + pixval maxval; + double flashfactor; + long longfactor; + const char* const usage = "flashfactor [ppmfile]\n flashfactor: 0.0 = original picture, 1.0 = total whiteout\n"; + + /* parse in 'default' parameters */ + ppm_init( &argc, argv ); + + argn = 1; + + /* parse in flash factor */ + if (argn == argc) + pm_usage(usage); + if (sscanf(argv[argn], "%lf", &flashfactor) != 1) + pm_usage(usage); + if (flashfactor < 0.0 || flashfactor > 1.0) + pm_error("flash factor must be in the range from 0.0 to 1.0 "); + ++argn; + + /* parse in filename (if present, stdin otherwise) */ + if (argn != argc) + { + ifp = pm_openr(argv[argn]); + ++argn; + } + else + ifp = stdin; + + if (argn != argc) + pm_usage(usage); + + /* read first data from file */ + ppm_readppminit(ifp, &cols, &rows, &maxval, &format); + + /* no error checking required here, ppmlib does it all for us */ + srcrow = ppm_allocrow(cols); + + longfactor = (long)(flashfactor * 65536); + + /* allocate a row of pixel data for the new pixels */ + destrow = ppm_allocrow(cols); + + ppm_writeppminit(stdout, cols, rows, maxval, 0); + + /** now do the flashing **/ + /* the 'float' parameter for flashing is sort of faked - in fact, we */ + /* convert it to a range from 0 to 65536 for integer math. Shouldn't */ + /* be something you'll have to worry about, though. */ + + for (i = 0; i < rows; i++) { + ppm_readppmrow(ifp, srcrow, cols, maxval, format); + + pP = srcrow; + pP2 = destrow; + + for (j = 0; j < cols; j++) { + PPM_ASSIGN(*pP2, + PPM_GETR(*pP) + + (((maxval - PPM_GETR(*pP)) * longfactor) >> 16), + PPM_GETG(*pP) + + (((maxval - PPM_GETG(*pP)) * longfactor) >> 16), + PPM_GETB(*pP) + + (((maxval - PPM_GETB(*pP)) * longfactor) >> 16)); + + pP++; + pP2++; + } + + /* write out one line of graphic data */ + ppm_writeppmrow(stdout, destrow, cols, maxval, 0); + } + + pm_close(ifp); + ppm_freerow(srcrow); + ppm_freerow(destrow); + + exit(0); +} + diff --git a/editor/ppmglobe.c b/editor/ppmglobe.c new file mode 100644 index 00000000..ee1a57c3 --- /dev/null +++ b/editor/ppmglobe.c @@ -0,0 +1,172 @@ +/* + * This code written 2003 + * by Max Gensthaler <Max@Gensthaler.de> + * Distributed under the Gnu Public License (GPL) + * + * Gensthaler called it 'ppmglobemap'. + * + * Translations of comments and C dialect by Bryan Henderson May 2003. + */ + + +#define _XOPEN_SOURCE /* get M_PI in math.h */ +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <math.h> + +#include "ppm.h" +#include "colorname.h" +#include "shhopt.h" +#include "mallocvar.h" + + +struct cmdlineInfo { + /* All the information the user supplied in the command line, + in a form easy for the program to use. + */ + const char * inputFileName; /* Filename of input files */ + unsigned int stripcount; + const char * background; + unsigned int closeok; +}; + + + +static void +parseCommandLine(int argc, char ** 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 backgroundSpec; + + MALLOCARRAY_NOFAIL(option_def, 100); + + option_def_index = 0; /* incremented by OPTENT3 */ + OPTENT3(0, "background", OPT_STRING, &cmdlineP->background, + &backgroundSpec, 0); + OPTENT3(0, "closeok", OPT_FLAG, NULL, + &cmdlineP->closeok, 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 */ + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + /* Uses and sets argc, argv, and some of *cmdlineP and others. */ + + if (!backgroundSpec) + cmdlineP->background = NULL; + + if (argc - 1 < 1) + pm_error("You must specify at least one argument: the strip count"); + else { + int const stripcount = atoi(argv[1]); + if (stripcount <= 0) + pm_error("The strip count must be positive. You specified %d", + stripcount); + + cmdlineP->stripcount = stripcount; + + if (argc-1 < 2) + cmdlineP->inputFileName = "-"; + else + cmdlineP->inputFileName = argv[2]; + + if (argc - 1 > 2) + pm_error("There are at most two arguments: strip count " + "and input file name. " + "You specified %u", argc-1); + } +} + + + +int +main(int argc, char *argv[]) { + + struct cmdlineInfo cmdline; + FILE * ifP; + pixel ** srcPixels; + pixel ** dstPixels; + int srcCols, srcRows; + unsigned int dstCols, dstRows; + pixval srcMaxval, dstMaxval; + unsigned int stripWidth; + /* Width in pixels of each strip. In the output image, this means + the rectangular strip in which the lens-shaped foreground strip + is placed.. + */ + unsigned int row; + pixel backgroundColor; + + ppm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFileName); + + srcPixels = ppm_readppm(ifP, &srcCols, &srcRows, &srcMaxval); + + pm_close(ifP); + + stripWidth = srcCols / cmdline.stripcount; + + if (stripWidth < 1) + pm_error("You asked for %u strips, but the image is only " + "%u pixels wide, so that is impossible.", + cmdline.stripcount, srcCols); + + dstCols = stripWidth * cmdline.stripcount; + dstRows = srcRows; + dstMaxval = srcMaxval; + + if (cmdline.background == NULL) + PPM_ASSIGN(backgroundColor, 0, 0, 0); + else + pm_parse_dictionary_name(cmdline.background, + dstMaxval, cmdline.closeok, + &backgroundColor); + + dstPixels = ppm_allocarray(dstCols, dstRows); + + for (row = 0; row < dstRows; ++row) { + double const factor = sin(M_PI * row / dstRows); + /* Amount by which we squeeze the foreground image of each + strip in this row. + */ + int const stripBorder = (int)((stripWidth*(1.0-factor)/2.0) + 0.5); + /* Distance from the edge (either one) of a strip to the + foreground image within that strip -- i.e. number of pixels + of background color, which User will cut out with scissors + after he prints the image. + */ + unsigned int dstCol; + + for (dstCol = 0; dstCol < dstCols; ++dstCol) { + if (dstCol % stripWidth < stripBorder + || dstCol % stripWidth >= stripWidth - stripBorder) + dstPixels[row][dstCol] = backgroundColor; + else { + unsigned int const leftEdge = + (dstCol / stripWidth) * stripWidth; + unsigned int const srcCol = leftEdge + + (int)((dstCol % stripWidth - stripBorder) / factor + 0.5); + dstPixels[row][dstCol] = srcPixels[row][srcCol]; + } + } + } + + ppm_writeppm(stdout, dstPixels, dstCols, dstRows, dstMaxval, 0); + + return 0; +} diff --git a/editor/ppmlabel.c b/editor/ppmlabel.c new file mode 100644 index 00000000..885d7d36 --- /dev/null +++ b/editor/ppmlabel.c @@ -0,0 +1,212 @@ +/* + + Add text labels to a PPM image + + by John Walker -- kelvin@fourmilab.ch + WWW home page: http://www.fourmilab.ch/ + June 1995 +*/ + +#define _XOPEN_SOURCE /* get M_PI in math.h */ + +#include <math.h> +#include <string.h> + +#include "pm_c_util.h" +#include "ppm.h" +#include "ppmdraw.h" + +#define dtr(x) (((x) * M_PI) / 180.0) + +static int argn, rows, cols, x, y, size, angle, transparent; +static pixel **pixels; +static pixval maxval; +static pixel rgbcolor, backcolor; + +/* DRAWTEXT -- Draw text at current location and advance to + start of next line. */ + +static void +drawtext(const char * const text) { + + if (!transparent && strlen(text) > 0) { + struct fillobj * handle; + + int left, top, right, bottom; + int lx, ly; + int p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y; + double sina, cosa; + + handle = ppmd_fill_create(); + + ppmd_text_box(size, 0, text, &left, &top, &right, &bottom); + + /* Displacement vector */ + + lx = right; + ly = -(top - bottom); + + /* Sine and cosine */ + + sina = sin(dtr(angle)); + cosa = cos(dtr(angle)); + + /* Rotated extent box corners */ + + p1x = (int) ((x + left * cosa + bottom * sina) + 0.5); + p1y = (int) ((y + bottom * cosa + -left * sina) + 0.5); + +#define WERF ppmd_fill_drawproc, handle + + p2x = (int) (p1x - sina * ly + 0.5); + p2y = (int) ((p1y - cosa * ly) + 0.5); + + p3x = (int) (p1x + cosa * lx + -sina * ly + 0.5); + p3y = (int) ((p1y - (cosa * ly + sina * lx)) + 0.5); + + p4x = (int) (p1x + cosa * lx + 0.5); + p4y = (int) ((p1y - sina * lx) + 0.5); + + ppmd_line(pixels, cols, rows, maxval, + p1x, p1y, p2x, p2y, + WERF); + ppmd_line(pixels, cols, rows, maxval, + p2x, p2y, p3x, p3y, + WERF); + ppmd_line(pixels, cols, rows, maxval, + p3x, p3y, p4x, p4y, + WERF); + ppmd_line(pixels, cols, rows, maxval, + p4x, p4y, p1x, p1y, + WERF); + + + ppmd_fill(pixels, cols, rows, maxval, + handle, PPMD_NULLDRAWPROC, (char *) &backcolor); + + ppmd_fill_destroy(handle); + } + ppmd_text(pixels, cols, rows, maxval, + x, y, size, angle, text, + PPMD_NULLDRAWPROC, (char *) &rgbcolor); + + /* For convenience, simulate a carriage return to the next line. + This allows multiple "-text" specifications or multiple lines + in a -file input to write consecutive lines of text in a + generally reasonable fashion. + */ + + x += (int) ((cos(dtr(angle + 270)) * size * 1.75) + 0.5); + y -= (int) ((sin(dtr(angle + 270)) * size * 1.75) + 0.5); +} + + + +int +main(int argc, char *argv[]) { + + FILE *ifP; + + /* Process standard command line arguments */ + + ppm_init(&argc, argv); + + argn = 1; + + /* Check for explicit input file specification, Note that + we count on the fact that every command line switch + takes a single argument. If this becomes untrue due + to a change in the future, you'll have to make this + test smarter. + */ + + if ((argn != argc) && (argc == 2 || argv[argc - 2][0] != '-')) { + ifP = pm_openr(argv[argc - 1]); + argc--; + } else + ifP = stdin; + + /* Load input image */ + + pixels = ppm_readppm(ifP, &cols, &rows, &maxval); + pm_close(ifP); + + /* Set initial defaults */ + + x = 0; + y = rows / 2; + size = 12; + angle = 0; + PPM_ASSIGN(rgbcolor, maxval, maxval, maxval); + PPM_ASSIGN(backcolor, 0, 0, 0); + transparent = TRUE; + + while (argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0') { + + if (pm_keymatch(argv[argn], "-angle", 1)) { + argn++; + if ((argn == argc) || (sscanf(argv[argn], "%d", &angle) != 1)) + pm_error("-angle doesn't have a value"); + + } else if (pm_keymatch(argv[argn], "-background", 1)) { + argn++; + if (strcmp(argv[argn], "transparent") == 0) { + transparent = TRUE; + } else { + transparent = FALSE; + backcolor = ppm_parsecolor(argv[argn], maxval); + } + + } else if (pm_keymatch(argv[argn], "-color", 1) + || pm_keymatch(argv[argn], "-colour", 1)) { + argn++; + rgbcolor = ppm_parsecolor(argv[argn], maxval); + + } else if (pm_keymatch(argv[argn], "-file", 1)) { + char s[512]; + + argn++; + ifP = pm_openr(argv[argn]); + while (fgets(s, sizeof s, ifP) != NULL) { + while (s[0] != 0 && s[strlen(s) - 1] < ' ') { + s[strlen(s) - 1] = 0; + } + drawtext(s); + } + pm_close(ifP); + + } else if (pm_keymatch(argv[argn], "-size", 1)) { + argn++; + if ((argn == argc) || (sscanf(argv[argn], "%d", &size) != 1)) + pm_error("-size doesn't have a value"); + } else if (pm_keymatch(argv[argn], "-text", 1)) { + argn++; + drawtext(argv[argn]); + + } else if (pm_keymatch(argv[argn], "-u", 1)) { + pm_error("-u doesn't have a value"); + + } else if (pm_keymatch(argv[argn], "-x", 1)) { + argn++; + if ((argn == argc) || (sscanf(argv[argn], "%d", &x) != 1)) + pm_error("-x doesn't have a value"); + + } else if (pm_keymatch(argv[argn], "-y", 1)) { + argn++; + if ((argn == argc) || (sscanf(argv[argn], "%d", &y) != 1)) + pm_error("-y doesn't have a value"); + + } else + pm_error("Unrecognized option: '%s'", argv[argn]); + argn++; + } + + if (argn != argc) + pm_error("Extraneous arguments"); + + ppm_writeppm(stdout, pixels, cols, rows, maxval, 0); + + ppm_freearray(pixels, rows); + + return 0; +} diff --git a/editor/ppmmix.c b/editor/ppmmix.c new file mode 100644 index 00000000..5306d1cf --- /dev/null +++ b/editor/ppmmix.c @@ -0,0 +1,131 @@ + +/*********************************************************************/ +/* ppmmix - mix together two pictures like with a fader */ +/* Frank Neumann, October 1993 */ +/* V1.2 16.11.1993 */ +/* */ +/* version history: */ +/* V1.0 Aug 1993 first version */ +/* V1.1 12.10.1993 uses ppm libs&headers, integer math, cleanups */ +/* V1.2 16.11.1993 Rewritten to be NetPBM.programming conforming */ +/*********************************************************************/ + +#include "ppm.h" + +/* global variables */ +#ifdef AMIGA +static char *version = "$VER: ppmmix 1.2 (16.11.93)"; /* Amiga version identification */ +#endif + +/**************************/ +/* start of main function */ +/**************************/ +int main(argc, argv) +int argc; +char *argv[]; +{ + FILE *ifp1, *ifp2; + int argn, rows, cols, format, i = 0, j = 0; + int rows2, cols2, format2; + pixel *srcrow1, *srcrow2, *destrow; + pixel *pP1, *pP2, *pP3; + pixval maxval, maxval2; + pixval r1, r2, r3, g1, g2, g3, b1, b2, b3; + double fadefactor; + long longfactor; + const char * const usage = "fadefactor ppmfile1 ppmfile2\n fadefactor: 0.0 = only ppmfile1, 1.0 = only ppmfile2\n"; + + /* parse in 'default' parameters */ + ppm_init(&argc, argv); + + argn = 1; + + /* parse in dim factor */ + if (argn == argc) + pm_usage(usage); + if (sscanf(argv[argn], "%lf", &fadefactor) != 1) + pm_usage(usage); + if (fadefactor < 0.0 || fadefactor > 1.0) + pm_error("fade factor must be in the range from 0.0 to 1.0 "); + ++argn; + + /* parse in filenames and open files (cannot be stdin-filters, sorry..) */ + if (argn == argc-2) + { + ifp1 = pm_openr(argv[argn]); + ++argn; + ifp2 = pm_openr(argv[argn]); + } + else + pm_usage(usage); + + /* read first data from both files and compare sizes etc. */ + ppm_readppminit(ifp1, &cols, &rows, &maxval, &format); + ppm_readppminit(ifp2, &cols2, &rows2, &maxval2, &format2); + + if ( (cols != cols2) || (rows != rows2) ) + pm_error("image sizes are different!"); + + if ( maxval != maxval2) + pm_error("images have different maxvalues"); + + if (format != format2) + { + pm_error("images have different PxM types"); + } + + /* no error checking required here, ppmlib does it all for us */ + srcrow1 = ppm_allocrow(cols); + srcrow2 = ppm_allocrow(cols); + + longfactor = (long)(fadefactor * 65536); + + /* allocate a row of pixel data for the new pixels */ + destrow = ppm_allocrow(cols); + + ppm_writeppminit(stdout, cols, rows, maxval, 0); + + for (i = 0; i < rows; i++) + { + ppm_readppmrow(ifp1, srcrow1, cols, maxval, format); + ppm_readppmrow(ifp2, srcrow2, cols, maxval, format); + + pP1 = srcrow1; + pP2 = srcrow2; + pP3 = destrow; + + for (j = 0; j < cols; j++) + { + r1 = PPM_GETR(*pP1); + g1 = PPM_GETG(*pP1); + b1 = PPM_GETB(*pP1); + + r2 = PPM_GETR(*pP2); + g2 = PPM_GETG(*pP2); + b2 = PPM_GETB(*pP2); + + r3 = r1 + (((r2 - r1) * longfactor) >> 16); + g3 = g1 + (((g2 - g1) * longfactor) >> 16); + b3 = b1 + (((b2 - b1) * longfactor) >> 16); + + + PPM_ASSIGN(*pP3, r3, g3, b3); + + pP1++; + pP2++; + pP3++; + } + + /* write out one line of graphic data */ + ppm_writeppmrow(stdout, destrow, cols, maxval, 0); + } + + pm_close(ifp1); + pm_close(ifp2); + ppm_freerow(srcrow1); + ppm_freerow(srcrow2); + ppm_freerow(destrow); + + exit(0); +} + diff --git a/editor/ppmntsc.c b/editor/ppmntsc.c new file mode 100644 index 00000000..b9f2ac2f --- /dev/null +++ b/editor/ppmntsc.c @@ -0,0 +1,499 @@ +/* This is ppmntsc.c, a program to adjust saturation values in an image + so they are legal for NTSC or PAL. + + It is derived from the program rlelegal.c, dated June 5, 1995, + which is described below and propagates that program's copyright. + The derivation was done by Bryan Henderson on 2000.04.21 to convert + it from operating on the RLE format to operating on the PPM format + and to rewrite it in a cleaner style, taking advantage of modern C + compiler technology. +*/ + + +/* + * This software is copyrighted as noted below. It may be freely copied, + * modified, and redistributed, provided that the copyright notice is + * preserved on all copies. + * + * There is no warranty or other guarantee of fitness for this software, + * it is provided solely "as is". Bug reports or fixes may be sent + * to the author, who may or may not act on them as he desires. + * + * You may not include this software in a program or other software product + * without supplying the source, or without informing the end-user that the + * source is available for no extra charge. + * + * If you modify this software, you should include a notice giving the + * name of the person performing the modification, the date of modification, + * and the reason for such modification. + */ + +/* + * rlelegal.c - Make RGB colors legal in the YIQ or YUV color systems. + * + * Author: Wes Barris + * Minnesota Supercomputer Center, Inc. + * Date: Fri Oct 15, 1993 + * @Copyright, Research Equipment Inc., d/b/a Minnesota Supercomputer + * Center, Inc., 1993 + + */ + +#define _BSD_SOURCE 1 /* Make sure strdup() is in string.h */ +#define _XOPEN_SOURCE 500 /* Make sure strdup() is in string.h */ + +#include <stdio.h> +#include <math.h> +#include <string.h> +#include "ppm.h" +#include "mallocvar.h" +#include "shhopt.h" + +#define TRUE 1 +#define FALSE 0 + +enum legalize {RAISE_SAT, LOWER_SAT, ALREADY_LEGAL}; + /* The actions that make a legal pixel */ + +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 verbose; + unsigned int debug; + unsigned int pal; + enum {ALL, LEGAL_ONLY, ILLEGAL_ONLY, CORRECTED_ONLY} output; +}; + + + + +static void +rgbtoyiq(const int r, const int g, const int b, + double * const y_p, + double * const i_p, + double * const q_p) { + + *y_p = .299*(r/255.0) + .587*(g/255.0) + .114*(b/255.0); + *i_p = .596*(r/255.0) - .274*(g/255.0) - .322*(b/255.0); + *q_p = .211*(r/255.0) - .523*(g/255.0) + .312*(b/255.0); +} + + + +static void +yiqtorgb(const double y, const double i, const double q, + int * const r_p, int * const g_p, int * const b_p) { + *r_p = 255.0*(1.00*y + .9562*i + .6214*q); + *g_p = 255.0*(1.00*y - .2727*i - .6468*q); + *b_p = 255.0*(1.00*y -1.1037*i +1.7006*q); +} + + + +static void +rgbtoyuv(const int r, const int g, const int b, + double * const y_p, + double * const u_p, + double * const v_p) { + *y_p = .299*(r/255.0) + .587*(g/255.0) + .114*(b/255.0); + *u_p = -.147*(r/255.0) - .289*(g/255.0) + .437*(b/255.0); + *v_p = .615*(r/255.0) - .515*(g/255.0) - .100*(b/255.0); +} + + + +static void +yuvtorgb(const double y, const double u, const double v, + int * const r_p, int * const g_p, int * const b_p) { + + *r_p = 255.0*(1.00*y + .0000*u +1.1398*v); + *g_p = 255.0*(1.00*y - .3938*u - .5805*v); + *b_p = 255.0*(1.00*y +2.0279*u + .0000*v); +} + + + +static void +make_legal_yiq(const double y, const double i, const double q, + double * const y_new_p, + double * const i_new_p, + double * const q_new_p, + enum legalize * const action_p + ) { + + double sat_old, sat_new; + /* + * I and Q are legs of a right triangle. Saturation is the hypotenuse. + */ + sat_old = sqrt(i*i + q*q); + if (y+sat_old > 1.0) { + const double diff = 0.5*((y+sat_old) - 1.0); + *y_new_p = y - diff; + sat_new = 1.0 - *y_new_p; + *i_new_p = i*(sat_new/sat_old); + *q_new_p = q*(sat_new/sat_old); + *action_p = LOWER_SAT; + } else if (y-sat_old <= -0.251) { + const double diff = 0.5*((sat_old-y) - 0.251); + *y_new_p = y + diff; + sat_new = 0.250 + *y_new_p; + *i_new_p = i*(sat_new/sat_old); + *q_new_p = q*(sat_new/sat_old); + *action_p = RAISE_SAT; + } else { + *y_new_p = y; + *i_new_p = i; + *q_new_p = q; + *action_p = ALREADY_LEGAL; + } + return; +} + + + +static void +make_legal_yuv(const double y, const double u, const double v, + double * const y_new_p, + double * const u_new_p, + double * const v_new_p, + enum legalize * const action_p + ) { + + double sat_old, sat_new; + /* + * U and V are legs of a right triangle. Saturation is the hypotenuse. + */ + sat_old = sqrt(u*u + v*v); + if (y+sat_old >= 1.334) { + const double diff = 0.5*((y+sat_old) - 1.334); + *y_new_p = y - diff; + sat_new = 1.333 - *y_new_p; + *u_new_p = u*(sat_new/sat_old); + *v_new_p = v*(sat_new/sat_old); + *action_p = LOWER_SAT; + } else if (y-sat_old <= -0.339) { + const double diff = 0.5*((sat_old-y) - 0.339); + *y_new_p = y + diff; + sat_new = 0.338 + *y_new_p; + *u_new_p = u*(sat_new/sat_old); + *v_new_p = v*(sat_new/sat_old); + *action_p = RAISE_SAT; + } else { + *u_new_p = u; + *v_new_p = v; + *action_p = ALREADY_LEGAL; + } + return; +} + + + +static void +make_legal_yiq_i(const int r_in, const int g_in, const int b_in, + int * const r_out_p, + int * const g_out_p, + int * const b_out_p, + enum legalize * const action_p + ) { + + double y, i, q; + double y_new, i_new, q_new; + /* + * Convert to YIQ and compute the new saturation. + */ + rgbtoyiq(r_in, g_in, b_in, &y, &i, &q); + make_legal_yiq(y, i, q, &y_new, &i_new, &q_new, action_p); + if (*action_p != ALREADY_LEGAL) + /* + * Given the new I and Q, compute new RGB values. + */ + yiqtorgb(y_new, i_new, q_new, r_out_p, g_out_p, b_out_p); + else { + *r_out_p = r_in; + *g_out_p = g_in; + *b_out_p = b_in; + } + return; +} + + + +static void +make_legal_yuv_i(const int r_in, const int g_in, const int b_in, + int * const r_out_p, + int * const g_out_p, + int * const b_out_p, + enum legalize * const action_p + ){ + + double y, u, v; + double y_new, u_new, v_new; + /* + * Convert to YUV and compute the new saturation. + */ + rgbtoyuv(r_in, g_in, b_in, &y, &u, &v); + make_legal_yuv(y, u, v, &y_new, &u_new, &v_new, action_p); + if (*action_p != ALREADY_LEGAL) + /* + * Given the new U and V, compute new RGB values. + */ + yuvtorgb(y_new, u_new, v_new, r_out_p, g_out_p, b_out_p); + else { + *r_out_p = r_in; + *g_out_p = g_in; + *b_out_p = b_in; + } + return; +} + + + +static void +make_legal_yiq_b(const pixel input, + pixel * const output_p, + enum legalize * const action_p) { + + + int ir_in, ig_in, ib_in; + int ir_out, ig_out, ib_out; + + ir_in = (int)PPM_GETR(input); + ig_in = (int)PPM_GETG(input); + ib_in = (int)PPM_GETB(input); + + make_legal_yiq_i(ir_in, ig_in, ib_in, &ir_out, &ig_out, &ib_out, action_p); + + PPM_ASSIGN(*output_p, ir_out, ig_out, ib_out); + + return; +} + + + +static void +make_legal_yuv_b(const pixel input, + pixel * const output_p, + enum legalize * const action_p) { + + int ir_in, ig_in, ib_in; + int ir_out, ig_out, ib_out; + + ir_in = (int)PPM_GETR(input); + ig_in = (int)PPM_GETG(input); + ib_in = (int)PPM_GETB(input); + make_legal_yuv_i(ir_in, ig_in, ib_in, &ir_out, &ig_out, &ib_out, action_p); + + PPM_ASSIGN(*output_p, ir_out, ig_out, ib_out); + + return; +} + + + +static void +report_mapping(const pixel old_pixel, const pixel new_pixel) { +/*---------------------------------------------------------------------------- + Assuming old_pixel and new_pixel are input and output pixels, + tell the user that we changed a pixel to make it legal, if in fact we + did and it isn't the same change that we just reported. +-----------------------------------------------------------------------------*/ + static pixel last_changed_pixel; + static int first_time = TRUE; + + if (!PPM_EQUAL(old_pixel, new_pixel) && + (first_time || PPM_EQUAL(old_pixel, last_changed_pixel))) { + pm_message("Mapping %d %d %d -> %d %d %d\n", + PPM_GETR(old_pixel), + PPM_GETG(old_pixel), + PPM_GETB(old_pixel), + PPM_GETR(new_pixel), + PPM_GETG(new_pixel), + PPM_GETB(new_pixel) + ); + + last_changed_pixel = old_pixel; + first_time = FALSE; + } +} + + + +static void +convert_one_image(FILE * const ifp, struct cmdlineInfo const cmdline, + bool * const eofP, + int * const hicountP, int * const locountP) { + + /* Parameters of input image: */ + int rows, cols; + pixval maxval; + int format; + + ppm_readppminit(ifp, &cols, &rows, &maxval, &format); + ppm_writeppminit(stdout, cols, rows, maxval, FALSE); + { + pixel* const input_row = ppm_allocrow(cols); + pixel* const output_row = ppm_allocrow(cols); + pixel last_illegal_pixel; + /* Value of the illegal pixel we most recently processed */ + pixel black; + /* A constant - black pixel */ + + PPM_ASSIGN(black, 0, 0, 0); + + PPM_ASSIGN(last_illegal_pixel, 0, 0, 0); /* initial value */ + { + int row; + + *hicountP = 0; *locountP = 0; /* initial values */ + + for (row = 0; row < rows; ++row) { + int col; + ppm_readppmrow(ifp, input_row, cols, maxval, format); + for (col = 0; col < cols; ++col) { + pixel corrected; + /* Corrected or would-be corrected value for pixel */ + enum legalize action; + /* What action was used to make pixel legal */ + if (cmdline.pal) + make_legal_yuv_b(input_row[col], + &corrected, + &action); + else + make_legal_yiq_b(input_row[col], + &corrected, + &action); + + if (action == LOWER_SAT) + (*hicountP)++; + if (action == RAISE_SAT) + (*locountP)++; + if (cmdline.debug) report_mapping(input_row[col], + corrected); + switch (cmdline.output) { + case ALL: + output_row[col] = corrected; + break; + case LEGAL_ONLY: + output_row[col] = (action == ALREADY_LEGAL) ? + input_row[col] : black; + break; + case ILLEGAL_ONLY: + output_row[col] = (action != ALREADY_LEGAL) ? + input_row[col] : black; + break; + case CORRECTED_ONLY: + output_row[col] = (action != ALREADY_LEGAL) ? + corrected : black; + break; + } + } + ppm_writeppmrow(stdout, output_row, cols, maxval, FALSE); + } + } + ppm_freerow(output_row); + ppm_freerow(input_row); + } +} + + +static void +parseCommandLine(int argc, char ** argv, + struct cmdlineInfo * const cmdlineP) { +/*---------------------------------------------------------------------------- + Note that many of the strings that this function returns in the + *cmdlineP structure are actually in the supplied argv array. And + sometimes, one of these strings is actually just a suffix of an entry + in argv! +-----------------------------------------------------------------------------*/ + optStruct3 opt; + optEntry *option_def; + /* Instructions to OptParseOptions on how to parse our options. + */ + unsigned int option_def_index; + unsigned int legalonly, illegalonly, correctedonly; + + MALLOCARRAY(option_def, 100); + + option_def_index = 0; /* incremented by OPTENTRY */ + OPTENT3('v', "verbose", OPT_FLAG, NULL, &cmdlineP->verbose, 0); + OPTENT3('V', "debug", OPT_FLAG, NULL, &cmdlineP->debug, 0); + OPTENT3('p', "pal", OPT_FLAG, NULL, &cmdlineP->pal, 0); + OPTENT3('l', "legalonly", OPT_FLAG, NULL, &legalonly, 0); + OPTENT3('i', "illegalonly", OPT_FLAG, NULL, &illegalonly, 0); + OPTENT3('c', "correctedonly", OPT_FLAG, NULL, &correctedonly, 0); + + opt.opt_table = option_def; + opt.short_allowed = TRUE; + opt.allowNegNum = FALSE; + + optParseOptions3(&argc, argv, opt, sizeof(opt), 0); + + if (argc - 1 == 0) + cmdlineP->inputFilename = "-"; /* he wants stdin */ + else if (argc - 1 == 1) + cmdlineP->inputFilename = argv[1]; + else + pm_error("Too many arguments. The only arguments accepted " + "are the mask color and optional input file specification"); + + if (legalonly + illegalonly + correctedonly > 1) + pm_error("--legalonly, --illegalonly, and --correctedonly are " + "conflicting options. Specify at most one of these."); + + if (legalonly) + cmdlineP->output = LEGAL_ONLY; + else if (illegalonly) + cmdlineP->output = ILLEGAL_ONLY; + else if (correctedonly) + cmdlineP->output = CORRECTED_ONLY; + else + cmdlineP->output = ALL; +} + + + +int +main(int argc, char **argv) { + + struct cmdlineInfo cmdline; + FILE * ifP; + int total_hicount, total_locount; + int image_count; + + bool eof; + + ppm_init(&argc, argv); + + parseCommandLine(argc, argv, &cmdline); + + ifP = pm_openr(cmdline.inputFilename); + + image_count = 0; /* initial value */ + total_hicount = 0; /* initial value */ + total_locount = 0; /* initial value */ + + eof = FALSE; + while (!eof) { + int hicount, locount; + convert_one_image(ifP, cmdline, &eof, &hicount, &locount); + image_count++; + total_hicount += hicount; + total_locount += locount; + ppm_nextimage(ifP, &eof); + } + + + if (cmdline.verbose) { + pm_message("%d images processed.", image_count); + pm_message("%d pixels were above the saturation limit.", + total_hicount); + pm_message("%d pixels were below the saturation limit.", + total_locount); + } + + pm_close(ifP); + + return 0; +} diff --git a/editor/ppmquant b/editor/ppmquant new file mode 100755 index 00000000..11bce6d2 --- /dev/null +++ b/editor/ppmquant @@ -0,0 +1,30 @@ +#!/usr/bin/perl -w +############################################################################## +# This is nothing but a compatibility interface for Pnmquant. +# An old program coded to call Ppmquant will continue working because +# this interface exists. All new (or newly modified) programs should +# call Pnmquant or Pnmremap instead. +# +# In days past, Pnmquant and Pnmremap did not exist. Ppmquant did +# the job of both Pnmremap and Pnmquant, but only on PPM images. +############################################################################## + +use strict; + +use Getopt::Long; + +my $TRUE=1; my $FALSE = 0; + +my @ppmquantArgv = @ARGV; + +Getopt::Long::Configure('pass_through'); + +my $validOptions = GetOptions('mapfile' => \my $mapfileopt); + +my $mapfileOptionPresent = ($validOptions && $mapfileopt); + +if ($mapfileOptionPresent) { + system('pnmremap', @ppmquantArgv); +} else { + system('pnmquant', @ppmquantArgv); +} diff --git a/editor/ppmquantall b/editor/ppmquantall new file mode 100755 index 00000000..af1ce22c --- /dev/null +++ b/editor/ppmquantall @@ -0,0 +1,97 @@ +#!/bin/sh +# +# ppmquantall - run ppmquant on a bunch of files all at once, so they share +# a common colormap +# +# WARNING: overwrites the source files with the results!!! +# +# Verbose explanation: Let's say you've got a dozen pixmaps that you want +# to display on the screen all at the same time. Your screen can only +# display 256 different colors, but the pixmaps have a total of a thousand +# or so different colors. For a single pixmap you solve this problem with +# pnmquant; this script solves it for multiple pixmaps. All it does is +# concatenate them together into one big pixmap, run pnmquant on that, and +# then split it up into little pixmaps again. +# +# IMPLEMENTATION NOTE: Now that Pnmcolormap can compute a single colormap +# for a whole stream of images, this program could be implemented more +# simply. Today, it concatenates a bunch of images into one image, uses +# Pnmquant to quantize that, then splits the result back into multiple +# images. It could instead just run Pnmcolormap over all the images, +# then run Pnmremap on each input image using the one colormap for all. + +usage() +{ + echo "usage: $0 [-ext extension] <newcolors> <ppmfile> ..." + exit 1 +} + +ext= + +while :; do + + case "$1" in + -ext*) + if [ $# -lt 2 ]; then + usage + fi + ext=".$2" + shift + shift + ;; + + *) + break + ;; + + esac +done + +if [ $# -lt 2 ]; then + usage +fi + +newcolors=$1 +shift +nfiles=$# +files=($@) + +# Extract the width and height of each of the images. +# Here, we make the assumption that the width and height are on the +# second line, even though the PPM format doesn't require that. +# To be robust, we need to use Pnmfile to get that information, or +# Put this program in C and use ppm_readppminit(). + +set widths=() +set heights=() + +for i in ${files[@]}; do + widths=(${widths[*]} `grep -v '^#' $i | sed '1d; s/ .*//; 2q'`) + heights=(${heights[*]} `grep -v '^#' $i | sed '1d; s/.* //; 2q'`) +done + +tempdir="${TMPDIR-/tmp}/ppmquantall.$$" +mkdir $tempdir || { echo "Could not create temporary file. Exiting."; exit 1;} +chmod 700 $tempdir + +trap 'rm -rf $tempdir' 0 1 3 15 + +all=$tempdir/pqa.all.$$ + +pnmcat -topbottom -jleft -white ${files[@]} | pnmquant $newcolors > $all +if [ $? != 0 ]; then + exit $? +fi + +y=0 +i=0 + +while [ $i -lt $nfiles ]; do + pamcut -left 0 -top $y -width ${widths[$i]} -height ${heights[$i]} $all \ + > ${files[$i]}$ext + if [ $? != 0 ]; then + exit $? + fi + y=$(($y + ${heights[$i]})) + i=$(($i + 1)) +done diff --git a/editor/ppmquantall.csh b/editor/ppmquantall.csh new file mode 100644 index 00000000..9a89bca0 --- /dev/null +++ b/editor/ppmquantall.csh @@ -0,0 +1,57 @@ +#!/bin/csh -f +# +# ppmquantall - run ppmquant on a bunch of files all at once, so they share +# a common colormap +# +# WARNING: overwrites the source files with the results!!! +# +# Verbose explanation: Let's say you've got a dozen pixmaps that you want +# to display on the screen all at the same time. Your screen can only +# display 256 different colors, but the pixmaps have a total of a thousand +# or so different colors. For a single pixmap you solve this problem with +# ppmquant; this script solves it for multiple pixmaps. All it does is +# concatenate them together into one big pixmap, run ppmquant on that, and +# then split it up into little pixmaps again. + +if ( $#argv < 3 ) then + echo "usage: ppmquantall <newcolors> <ppmfile> <ppmfile> ..." + exit 1 +endif + +set newcolors=$argv[1] +set files=( $argv[2-] ) + +# Extract the width and height of each of the images. +# Here, we make the assumption that the width and height are on the +# second line, even though the PPM format doesn't require that. +# To be robust, we need to use Pnmfile to get that information, or +# Put this program in C and use ppm_readppminit(). + +set widths=() +set heights=() +foreach i ( $files ) + set widths=( $widths `sed '1d; s/ .*//; 2q' $i` ) + set heights=( $heights `sed '1d; s/.* //; 2q' $i` ) +end + +set all=/tmp/pqa.all.$$ +rm -f $all +pnmcat -topbottom -jleft -white $files | ppmquant -quiet $newcolors > $all +if ( $status != 0 ) exit $status + +@ y = 0 +@ i = 1 +while ( $i <= $#files ) + pnmcut -left 0 -top $y -width $widths[$i] -height $heights[$i] $all \ + > $files[$i] + if ( $status != 0 ) exit $status + @ y = $y + $heights[$i] + @ i++ +end + +rm -f $all + + + + + diff --git a/editor/ppmrelief.c b/editor/ppmrelief.c new file mode 100644 index 00000000..5e0669c3 --- /dev/null +++ b/editor/ppmrelief.c @@ -0,0 +1,90 @@ +/* ppmrelief.c - generate a relief map of a portable pixmap +** +** Copyright (C) 1990 by Wilson H. Bent, Jr. +** +** 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. +*/ + +#include <stdio.h> +#include "ppm.h" + +int +main(int argc, char * argv[]) { + + FILE* ifp; + pixel** inputbuf; + pixel* outputrow; + int argn, rows, cols, format, row; + register int col; + pixval maxval, mv2; + const char* const usage = "[ppmfile]"; + + ppm_init( &argc, argv ); + + argn = 1; + + if ( argn != argc ) { + ifp = pm_openr( argv[argn] ); + ++argn; + } else + ifp = stdin; + + if ( argn != argc ) + pm_usage( usage ); + + ppm_readppminit( ifp, &cols, &rows, &maxval, &format ); + mv2 = maxval / 2; + + /* Allocate space for 3 input rows, plus an output row. */ + inputbuf = ppm_allocarray( cols, 3 ); + outputrow = ppm_allocrow( cols ); + + ppm_writeppminit( stdout, cols, rows, maxval, 0 ); + + /* Read in the first two rows. */ + ppm_readppmrow( ifp, inputbuf[0], cols, maxval, format ); + ppm_readppmrow( ifp, inputbuf[1], cols, maxval, format ); + + /* Write out the first row, all zeros. */ + for ( col = 0; col < cols; ++col ) + PPM_ASSIGN( outputrow[col], 0, 0, 0 ); + ppm_writeppmrow( stdout, outputrow, cols, maxval, 0 ); + + /* Now the rest of the image - read in the 3rd row of inputbuf, + ** and convolve with the first row into the output buffer. + */ + for ( row = 2 ; row < rows; ++row ) { + pixval r, g, b; + int rowa, rowb; + + rowa = row % 3; + rowb = (row + 2) % 3; + ppm_readppmrow( ifp, inputbuf[rowa], cols, maxval, format ); + + for ( col = 0; col < cols - 2; ++col ) { + r = PPM_GETR( inputbuf[rowa][col] ) + + ( mv2 - PPM_GETR( inputbuf[rowb][col + 2] ) ); + g = PPM_GETG( inputbuf[rowa][col] ) + + ( mv2 - PPM_GETG( inputbuf[rowb][col + 2] ) ); + b = PPM_GETB( inputbuf[rowa][col] ) + + ( mv2 - PPM_GETB( inputbuf[rowb][col + 2] ) ); + PPM_ASSIGN( outputrow[col + 1], r, g, b ); + } + ppm_writeppmrow( stdout, outputrow, cols, maxval, 0 ); + } + + /* And write the last row, zeros again. */ + for ( col = 0; col < cols; ++col ) + PPM_ASSIGN( outputrow[col], 0, 0, 0 ); + ppm_writeppmrow( stdout, outputrow, cols, maxval, 0 ); + + pm_close( ifp ); + pm_close( stdout ); + + exit( 0 ); +} diff --git a/editor/ppmshadow b/editor/ppmshadow new file mode 100755 index 00000000..2a32fca0 --- /dev/null +++ b/editor/ppmshadow @@ -0,0 +1,273 @@ +#!/usr/bin/perl -w + +# P P M S H A D O W + +# by John Walker -- http://www.fourmilab.ch/ +# version = 1.2; +# --> with minor changes by Bryan Henderson to adapt to Netbpm. +# See above web site for the real John Walker work, named pnmshadow. + +# Bryan Henderson later made some major style changes (use strict, etc) and +# eliminated most use of shells. See Netbpm HISTORY file. + +# Pnmshadow is a brutal sledgehammer implemented in Perl which +# adds attractive shadows to images, as often seen in titles +# of World-Wide Web pages. This program does not actually +# *do* any image processing--it simply invokes components of +# Jef Poskanzer's PBMplus package (which must be present on +# the path when this script is run) to bludgeon the source +# image into a plausible result. +# +# This program is in the public domain. +# +# + +use strict; +require 5.0; +# The good open() syntax, with the mode separate from the file name, +# came after 5.0. So did mkdir() with default mode. + +my $true=1; my $false=0; + + +sub getDimensions($) { + my ($fileName) = @_; +#----------------------------------------------------------------------------- +# Return the dimensions of the Netpbm image in the named file +#----------------------------------------------------------------------------- + my ($width, $height); + my $pamfileOutput = `pamfile $fileName`; + if ($pamfileOutput =~ m/.*\sP[BGP]M\s.*,\s*(\d*)\sby\s(\d*)/) { + ($width, $height) = ($1, $2); + } else { + die("Unrecognized output from 'pamfile' shell command"); + } + return ($width, $height); +} + + +sub makeConvolutionKernel($$) { + my ($convkernelfile, $ckern) = @_; + + # Create convolution kernel file to generate shadow + + open(OF, ">$convkernelfile") or die(); + printf(OF "P2\n$ckern $ckern\n%d\n", $ckern * $ckern * 2); + my $a = ($ckern * $ckern) + 1; + my $i; + for ($i = 0; $i < $ckern; $i++) { + my $j; + for ($j = 0; $j < $ckern; $j++) { + printf(OF "%d%s", $a, ($j < ($ckern - 1)) ? " " : "\n"); + } + } + close(OF); +} + + + +############################################################################## +# MAINLINE +############################################################################## + + +my $tmpdir = $ENV{TMPDIR} || "/tmp"; +my $ourtmp = "$tmpdir/ppmshadow$$"; +mkdir($ourtmp, 0777) or + die("Unable to create directory for temporary files '$ourtmp"); + +# Process command line options + + +my $ifile; # Input file name +my ($xoffset, $yoffset); + +my $convolve = 11; # Default blur convolution kernel size +my $keeptemp = $false; # Don't preserve intermediate files +my $translucent = $false; # Default not translucent + +while (@ARGV) { + my $arg = shift; + if ((substr($arg, 0, 1) eq '-') && (length($arg) > 1)) { + my $opt; + $opt = substr($arg, 1, 1); + $opt =~ tr/A-Z/a-z/; + if ($opt eq 'b') { # -B n -- Blur size + if (!defined($convolve = shift)) { + die("Argument missing after -b option\n"); + } + if (($convolve < 11) && (($convolve & 1) == 0)) { + $convolve++; # Round up even kernel specification + } + } elsif ($opt eq 'k') { # -K -- Keep temporary files + $keeptemp = $true; + } elsif ($opt eq 't') { # -T -- Translucent image + $translucent = $true; + } elsif ($opt eq 'x') { # -X n -- X offset + if (!defined($xoffset = shift)) { + die("Argument missing after -x option\n"); + } + if ($xoffset < 0) { + $xoffset = -$xoffset; + } + } elsif ($opt eq 'y') { # -Y n -- Y offset + if (!defined($yoffset = shift)) { + die("Argument missing after -x option\n"); + } + if ($yoffset < 0) { + $yoffset = -$xoffset; + } + } + } else { + if (defined $ifile) { + die("Duplicate input file specification."); + } + $ifile = $arg; + } +} + +# Apply defaults for arguments not specified + +if (!(defined $xoffset)) { + # Xoffset defaults to half the blur distance + $xoffset = int($convolve / 2); +} + +if (!(defined $yoffset)) { + # Yoffset defaults to Xoffset, however specified + $yoffset = $xoffset; +} + +# Save the Standard Output open instance so we can use the STDOUT +# file descriptor to pass files to our children. +open(OLDOUT, ">&STDOUT"); +select(OLDOUT); # avoids Perl bug where it says we never use STDOUT + +my $infile = "$ourtmp/infile.ppm"; + +if (defined($ifile) && $ifile ne "-") { + open(STDIN, "<$ifile") or die(); +} +open(STDOUT, ">$infile") or die("Unable to open '$infile' as STDOUT"); +system("ppmtoppm"); + +# You would think we could and should close stdin and stdout now, but if +# we do that, system() pipelines later on fail mysteriously. They don't +# seem to be able to open stdin and stdout pipes properly if stdin and +# stdout didn't already exist. 2002.09.07 BJH + +my ($sourceImageWidth, $sourceImageHeight) = getDimensions($infile); + +# Create an all-background-color image (same size as original image) + +my $backgroundfile = "$ourtmp/background.ppm"; +system("pamcut -left=0 -top=0 -width=1 -height=1 $infile | " . + "pamscale -xsize=$sourceImageWidth " . + "-ysize=$sourceImageHeight >$backgroundfile"); + +# Create mask file for background. It is white wherever there is background +# image in the input. + +my $bgmaskfile = "$ourtmp/bgmask.pbm"; +system("pamarith -difference $infile $backgroundfile | pnminvert | ppmtopgm " . + "| pgmtopbm -thresh -value 1.0 >$bgmaskfile"); + +my $ckern = $convolve <= 11 ? $convolve : 11; + +my $convkernelfile = "$ourtmp/convkernel.pgm"; + +makeConvolutionKernel($convkernelfile, $ckern); + +if ($translucent) { + + # Convolve the input color image with the kernel + # to create a translucent shadow image. + + system("pnmconvol $convkernelfile $infile >$ourtmp/blurred.ppm"); + unlink("$convkernelfile") unless $keeptemp; + while ($ckern < $convolve) { + system("pnmsmooth $ourtmp/blurred.ppm >$ourtmp/convolvedx.ppm"); + rename("$ourtmp/convolvedx.ppm", "$ourtmp/blurred.ppm"); + ++$ckern; + } +} else { + + # Convolve the positive mask with the kernel to create shadow + + my $blurredblackshadfile = "$ourtmp/blurredblackshad.pgm"; + system("pamdepth -quiet 255 $bgmaskfile | " . + "pnmconvol $convkernelfile >$blurredblackshadfile"); + unlink($convkernelfile) unless $keeptemp; + + while ($ckern < $convolve) { + my $smoothedfile = "$ourtmp/smoothed.pgm"; + system("pnmsmooth $blurredblackshadfile >$smoothedfile"); + rename($smoothedfile, $blurredblackshadfile); + ++$ckern; + } + + # Multiply the shadow by the background color + + system("pamarith -multiply $blurredblackshadfile $backgroundfile " . + ">$ourtmp/blurred.ppm"); + unlink($blurredblackshadfile) unless $keeptemp; +} + +# Cut shadow image down to size of our frame. + +my $shadowfile = "$ourtmp/shadow.ppm"; +{ + my $width = $sourceImageWidth - $xoffset; + my $height = $sourceImageHeight - $yoffset; + open(STDIN, "<$ourtmp/blurred.ppm") or die(); + open(STDOUT, ">$shadowfile") or die(); + system("pamcut", "-left=0", "-top=0", + "-width=$width", "-height=$height"); +} +unlink("$ourtmp/blurred.ppm") unless $keeptemp; + +# Make mask for foreground + +my $fgmaskfile = "$ourtmp/fgmask.pbm"; +open(STDIN, "<$bgmaskfile") or die(); +open(STDOUT, ">$fgmaskfile") or die(); +system("pnminvert"); + +# Make image which is just foreground; rest is black. + +my $justfgfile = "$ourtmp/justfg.ppm"; +open(STDOUT, ">$justfgfile") or die(); +system("pamarith", "-multiply", $infile, $fgmaskfile); + +unlink($fgmaskfile) unless $keeptemp; +unlink($infile) unless $keeptemp; + +# Paste shadow onto background. + +my $shadbackfile = "$ourtmp/shadback.ppm"; +open(STDOUT, ">$shadbackfile") or die(); +system("pnmpaste", "-replace", $shadowfile, $xoffset, $yoffset, + $backgroundfile); +unlink($shadowfile) unless $keeptemp; +unlink($backgroundfile) unless $keeptemp; + +# Knock out (make black) foreground area + +my $allbutfgfile = "$ourtmp/allbutfg.ppm"; +open(STDOUT, ">$allbutfgfile") or die(); +system("pamarith", "-multiply", $shadbackfile, $bgmaskfile); + +unlink($shadbackfile) unless $keeptemp; +unlink($bgmaskfile) unless $keeptemp; + +# Place foreground in blacked out area, send to original Standard Output. + +open(STDOUT, ">&OLDOUT"); + +system("pamarith", "-add", $justfgfile, $allbutfgfile); +unlink($justfgfile) unless $keeptemp; +unlink($allbutfgfile) unless $keeptemp; + +if (!$keeptemp) { + rmdir($ourtmp) or die ("Unable to remove temporary directory '$ourtmp'"); +} diff --git a/editor/ppmshadow.doc b/editor/ppmshadow.doc new file mode 100644 index 00000000..1539c708 --- /dev/null +++ b/editor/ppmshadow.doc @@ -0,0 +1,627 @@ +<html> +<head> +<title>pnmshadow: How it Works</title> + +</head> + +<body> + +<center> +<h1><img src="figures/how_title.jpg" width=417 height=116 alt="pnmshadow: How it Works"></h1> +</center> + +<hr> +<p> + +This document describes the process, including PBMplus commands +and the intermediate images they create, by +which <b><a href="./">pnmshadow</a></b> +adds black shadows to source images. +A <a href="how-t.html">companion document</a> +describes how translucent shadows are created when the +<b>-t</b> option is specified. + +<h3>The Starting Point</h3> + +Let's start with the following source image, 536 pixels wide and 141 +pixels high. We convert the image from whatever form in which +it was originally created (GIF, JPEG, etc.) to a PPM file before +processing it with <b>pnmshadow</b>. + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadin.gif" width=536 height=141 alt="Input image"> +</table> +</center> + +<h3>The Blank Background</h3> + +We start by determining the size of the input image with +<b>pnmfile</b> and then constructing an image with the same +size as the input image consisting entirely of the background +color, which is defined as the color of the pixel at the upper +left corner of the source image. This is performed by the +command: + +<p> +<pre> + pnmcut 0 0 1 1 <em>ifile</em> | pnmscale -xsize <em>xsize</em> -ysize <em>ysize</em> ><em>fname</em>-5.ppm +</pre> +<p> + +yielding the image: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt5.gif" width=536 height=141 alt="Blank background image"> +</table> +</center> + +<h3>The Positive Mask</h3> + +A positive mask image is created in which all pixels of the background +color are set to white and all other pixels are black. This is accomplished +by subtracting the blank background image from the input (using the +<tt>-difference</tt> option on <b>pnmarith</b> to avoid clipping at +zero or the maximum pixel value), then inverting the result and +thresholding it to a monochrome bitmap. + +<p> +<pre> + pnmarith -difference <em>ifile</em> <em>fname</em>-5.ppm | pnminvert | ppmtopgm | pgmtopbm -thresh -value 1.0 ><em>fname</em>-1.ppm +</pre> +<p> + +This produces the following mask image. + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt1.gif" width=536 height=141 alt="Positive mask image"> +</table> +</center> + +<h3>The Blurred Image</h3> + +Since we wish to simulate a shadow from a nearby extended +light source rather than a sharp shadow as cast by the +Sun, we need to prepare a blurred version of the original +image. If the <b>-t</b> option is not specified on +<b>pnmshadow</b> the shadow cast by an object of any color +is always black, so the positive mask serves as the source image +when preparing the shadow. A convolution kernel which averages +the number of pixels specified by the <b>-b</b> option +(default 11), written into the temporary file <tt><em>fname</em>-2.ppm</tt> +in ASCII PGM format, and then the blurred image is created with +the command: + +<p> +<pre> + pnmconvol <em>fname</em>-2.ppm <em>fname</em>-1.ppm ><em>fname</em>-3.ppm +</pre> +<p> + +With the default blur setting of 11 pixels, the blurred image +below is generated. + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt3.jpg" width=536 height=141 alt="Blurred shadow"> +</table> +</center> + +<h3>Shadow on Background Color</h3> + +Having generated the blurred shadow from the monochrome mask image, +it will consist of pixels ranging from white to black on a white +background. In order to preserve the background color in the +original image, we multiply the shadow by the blank background color +image created previously. White pixels take on the background color +and pixels belonging to the shadow are scaled to be relative to the +background. + +<p> +<pre> + pnmarith -multiply <em>fname</em>-3.ppm <em>fname</em>-5.ppm ><em>fname</em>-10.ppm +</pre> +<p> + +This yields the following shadow, with the background of the +original image. + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt10.jpg" width=536 height=141 alt="Shadow with background color"> +</table> +</center> + +<h3>Offset Shadow Clip</h3> + +Shadows, even bogus ones like we're generating, usually look best when +cast by a light source diagonally displaced from the centre of the +shadow-casting object. To achieve this effect, we first cut a +rectangle from the blurred shadow image reduced in size by the +the number of pixels specified by the <b>-b</b> option +which default to half the blur (<b>-b</b>) setting. The +<em>xsize</em> and <em>ysize</em> arguments in the following +command are the size of the input image in pixels less the shadow +displacement in the respective axis. + +<p> +<pre> + pnmcut 0 0 <em>xsize</em> <em>ysize</em> <em>fname</em>-10.ppm ><em>fname</em>-4.ppm +</pre> +<p> + +The shadow clip is the identical to the shadow on background color, but +smaller by the offset in each direction. + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt4.jpg" width=531 height=136 alt="Offset shadow clip"> +</table> +</center> + +<h3>Offset Shadow</h3> + +Now we're ready to assemble the shadow offset by the specified number +of pixels. We do this by pasting the image cut in the previous step +into the blank background, yielding an image the same size as the +source image with the blurred shadow displaced to the right and +down. + +<p> +<pre> + pnmpaste -replace <em>fname</em>-4.ppm <em>xoffset</em> <em>yoffset</em> <em>fname</em>-5.ppm ><em>fname</em>-6.ppm +</pre> +<p> + +This gives the following result: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt6.jpg" width=536 height=141 alt="Offset shadow"> +</table> +</center> + +<h3>Inverse Mask</h3> + +In order to stitch everything together, we need an inverse of the +mask prepared earlier--one where black pixels represent the background +and all other material is white. This is easily accomplished by +running the positive mask through <b>pnminvert</b>: + +<p> +<pre> + pnminvert <em>fname</em>-1.ppm ><em>fname</em>-7.ppm +</pre> +<p> + +yielding: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt7.gif" width=536 height=141 alt="Inverse mask"> +</table> +</center> + +<h3>Masked Input Image</h3> + +Now we use the inverse mask prepared in the previous step to create +an image containing all non-background pixels from the source image, +with background pixels set to black. We simply multiply the +inverse mask by the source image: + +<p> +<pre> + pnmarith -multiply <em>ifile</em> <em>fname</em>-7.ppm ><em>fname</em>-8.ppm +</pre> +<p> + +<em>et voilą:</em> + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt8.gif" width=536 height=141 alt="Masked Input Image"> +</table> +</center> + +<h3>Shadow with Source Masked</h3> + +Our last intermediate step before joining the image with its +shadow is preparing a shadow image with all non-background pixels +in the source image set to black. This ensures that when we add +the image and the shadow, the shadow will not override any pixel +in the source image. + +<p> +<pre> + pnmarith -multiply <em>fname</em>-6.ppm <em>fname</em>-1.ppm ><em>fname</em>-9.ppm +</pre> +<p> + +This is accomplished by multiplying the shadow by the positive +mask image, which sets all non-background pixels in the source +image to black: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt9.jpg" width=536 height=141 alt="Shadow with Source Masked"> +</table> +</center> + +<h3>The Final Product</h3> + +At long last, we're ready to put together the pieces and deliver +the result to our ever-patient user. This amounts simply to +adding the masked input image (consisting solely of non-background +pixels from the original image) to the shadow with source masked +(in which all source pixels are black): + +<p> +<pre> + pnmarith -add <em>fname</em>-8.ppm <em>fname</em>-9.ppm +</pre> +<p> + +The resulting image, with shadow, is as follows: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadout.jpg" width=536 height=141 alt="Output: Image with shadow"> +</table> +</center> + +<h3>Smooth Operator</h3> + +Since many computer graphics programs create sharp edges on +text, it's often best to create an image at a greater resolution +than that used for presentation, then scale it to the final +resolution with a tool which resamples the image, thus +minimising jagged edges by averaging adjacent +pixels. Using the output of <b>pnmshadow</b> as the starting +point and scaling to half size with <b>pnmscale</b>, we arrive at +the following smoothed image, with shadow, ready to adorn a +Web page: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadout2.jpg" width=268 height=71 alt="Half scale image with shadow"> +</table> +</center> + +<h4><a href="how-t.html">How it works with translucent shadows</a></h4> +<h4><a href="./"><b>pnmshadow</b> main page</a></h4> + +<p> +<hr> +<p> +<address> +by <a href="/">John Walker</a><br> +August 8th, 1997 +</address> + +</body> +</html> +<html> +<head> +<title>pnmshadow: How it Works in Translucent Mode</title> + +</head> + +<body> + +<center> +<h1><img src="figures/how_title-t.jpg" width=421 height=159 alt="pnmshadow: How it Works in Translucent Mode"></h1> +</center> + +<hr> + +<p> + +This document describes the process, including PBMplus commands +and the intermediate images they create, by +which <b><a href="./">pnmshadow</a></b> +adds translucent shadows when the <b>-t</b> command line +option is specified. A <a href="how.html">companion document</a> +describes how the default black shadows are generated. + +<h3>The Starting Point</h3> + +Let's start with the following source image, 536 pixels wide and 141 +pixels high. We convert the image from whatever form in which +it was originally created (GIF, JPEG, etc.) to a PPM file before +processing it with <b>pnmshadow</b>. + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadin.gif" width=536 height=141 alt="Input image"> +</table> +</center> + +<h3>The Blank Background</h3> + +We start by determining the size of the input image with +<b>pnmfile</b> and then constructing an image with the same +size as the input image consisting entirely of the background +color, which is defined as the color of the pixel at the upper +left corner of the source image. This is performed by the +command: + +<p> +<pre> + pnmcut 0 0 1 1 <em>ifile</em> | pnmscale -xsize <em>xsize</em> -ysize <em>ysize</em> ><em>fname</em>-5.ppm +</pre> +<p> + +yielding the image: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt5.gif" width=536 height=141 alt="Blank background image"> +</table> +</center> + +<h3>The Positive Mask</h3> + +A positive mask image is created in which all pixels of the background +color are set to white and all other pixels are black. This is accomplished +by subtracting the blank background image from the input (using the +<tt>-difference</tt> option on <b>pnmarith</b> to avoid clipping at +zero or the maximum pixel value), then inverting the result and +thresholding it to a monochrome bitmap. + +<p> +<pre> + pnmarith -difference <em>ifile</em> <em>fname</em>-5.ppm | pnminvert | ppmtopgm | pgmtopbm -thresh -value 1.0 ><em>fname</em>-1.ppm +</pre> +<p> + +This produces the following mask image. + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt1.gif" width=536 height=141 alt="Positive mask image"> +</table> +</center> + +<h3>The Blurred Image</h3> + +Since we wish to simulate a shadow from a nearby extended +light source rather than a sharp shadow as cast by the +Sun, we need to prepare a blurred version of the original +image. +A convolution kernel which averages +the number of pixels specified by the <b>-b</b> option +(default 11), written into the temporary file <tt><em>fname</em>-2.ppm</tt> +in ASCII PGM format, and then the blurred image is created with +the command: + +<p> +<pre> + pnmconvol <em>fname</em>-2.ppm <em>ifile</em> ><em>fname</em>-10.ppm +</pre> +<p> + +With the default blur setting of 11 pixels, the blurred image +below is generated. + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt10-t.jpg" width=536 height=141 alt="Blurred shadow"> +</table> +</center> + +<h3>Offset Shadow Clip</h3> + +Shadows, even bogus ones like we're generating, usually look best when +cast by a light source diagonally displaced from the centre of the +shadow-casting object. To achieve this effect, we first cut a +rectangle from the blurred shadow image reduced in size by the +the number of pixels specified by the <b>-b</b> option +which default to half the blur (<b>-b</b>) setting. The +<em>xsize</em> and <em>ysize</em> arguments in the following +command are the size of the input image in pixels less the shadow +displacement in the respective axis. + +<p> +<pre> + pnmcut 0 0 <em>xsize</em> <em>ysize</em> <em>fname</em>-10.ppm ><em>fname</em>-4.ppm +</pre> +<p> + +The shadow clip is the identical to the shadow on background color, but +smaller by the offset in each direction. + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt4-t.jpg" width=531 height=136 alt="Offset shadow clip"> +</table> +</center> + +<h3>Offset Shadow</h3> + +Now we're ready to assemble the shadow offset by the specified number +of pixels. We do this by pasting the image cut in the previous step +into the blank background, yielding an image the same size as the +source image with the blurred shadow displaced to the right and +down. + +<p> +<pre> + pnmpaste -replace <em>fname</em>-4.ppm <em>xoffset</em> <em>yoffset</em> <em>fname</em>-5.ppm ><em>fname</em>-6.ppm +</pre> +<p> + +This gives the following result: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt6-t.jpg" width=536 height=141 alt="Offset shadow"> +</table> +</center> + +<h3>Inverse Mask</h3> + +In order to stitch everything together, we need an inverse of the +mask prepared earlier--one where black pixels represent the background +and all other material is white. This is easily accomplished by +running the positive mask through <b>pnminvert</b>: + +<p> +<pre> + pnminvert <em>fname</em>-1.ppm ><em>fname</em>-7.ppm +</pre> +<p> + +yielding: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt7.gif" width=536 height=141 alt="Inverse mask"> +</table> +</center> + +<h3>Masked Input Image</h3> + +Now we use the inverse mask prepared in the previous step to create +an image containing all non-background pixels from the source image, +with background pixels set to black. We simply multiply the +inverse mask by the source image: + +<p> +<pre> + pnmarith -multiply <em>ifile</em> <em>fname</em>-7.ppm ><em>fname</em>-8.ppm +</pre> +<p> + +<em>et voilą:</em> + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt8.gif" width=536 height=141 alt="Masked Input Image"> +</table> +</center> + +<h3>Shadow with Source Masked</h3> + +Our last intermediate step before joining the image with its +shadow is preparing a shadow image with all non-background pixels +in the source image set to black. This ensures that when we add +the image and the shadow, the shadow will not override any pixel +in the source image. + +<p> +<pre> + pnmarith -multiply <em>fname</em>-6.ppm <em>fname</em>-1.ppm ><em>fname</em>-9.ppm +</pre> +<p> + +This is accomplished by multiplying the shadow by the positive +mask image, which sets all non-background pixels in the source +image to black: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadt9-t.jpg" width=536 height=141 alt="Shadow with Source Masked"> +</table> +</center> + +<h3>The Final Product</h3> + +At long last, we're ready to put together the pieces and deliver +the image to our ever-patient user. This amounts simply to +adding the masked input image (consisting solely of non-background +pixels from the original image) to the shadow with source masked +(in which all source pixels are black): + +<p> +<pre> + pnmarith -add <em>fname</em>-8.ppm <em>fname</em>-9.ppm +</pre> +<p> + +The resulting image, with shadow, is as follows: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadout-t.jpg" width=536 height=141 alt="Output: Image with translucent shadow"> +</table> +</center> + +<h3>Smooth Operator</h3> + +Since many computer graphics programs create sharp edges on +text, it's often best to create an image at a greater resolution +than that used for presentation, then scale it to the final +resolution with a tool which resamples the image, thus +minimising jagged edges by averaging adjacent +pixels. Using the output of <b>pnmshadow</b> as the starting +point and scaling to half size with <b>pnmscale</b>, we arrive at +the following smoothed image, with shadow, ready to adorn a +Web page: + +<p> +<center> +<table border=5> +<tr><td> +<img src="figures/shadout2-t.jpg" width=268 height=71 alt="Half scale image with translucent shadow"> +</table> +</center> + +<h4><a href="how.html">How it works with black shadows</a></h4> +<h4><a href="./"><b>pnmshadow</b> main page</a></h4> + +<p> +<hr> +<p> +<address> +by <a href="/">John Walker</a><br> +August 8th, 1997 +</address> + +</body> +</html> diff --git a/editor/ppmshift.c b/editor/ppmshift.c new file mode 100644 index 00000000..1f8a599b --- /dev/null +++ b/editor/ppmshift.c @@ -0,0 +1,137 @@ + +/*********************************************************************/ +/* ppmshift - shift lines of a picture left or right by x pixels */ +/* Frank Neumann, October 1993 */ +/* V1.1 16.11.1993 */ +/* */ +/* version history: */ +/* V1.0 11.10.1993 first version */ +/* V1.1 16.11.1993 Rewritten to be NetPBM.programming conforming */ +/*********************************************************************/ + +#include "ppm.h" + +/* global variables */ +#ifdef AMIGA +static char *version = "$VER: ppmshift 1.1 (16.11.93)"; /* Amiga version identification */ +#endif + +/**************************/ +/* start of main function */ +/**************************/ +int main(argc, argv) +int argc; +char *argv[]; +{ + FILE* ifp; + time_t timenow; + int argn, rows, cols, format, i = 0, j = 0; + pixel *srcrow, *destrow; + pixel *pP = NULL, *pP2 = NULL; + pixval maxval; + int shift, nowshift; + const char * const usage = "shift [ppmfile]\n shift: maximum number of pixels to shift a line by\n"; + + /* parse in 'default' parameters */ + ppm_init(&argc, argv); + + argn = 1; + + /* parse in shift number */ + if (argn == argc) + pm_usage(usage); + if (sscanf(argv[argn], "%d", &shift) != 1) + pm_usage(usage); + if (shift < 0) + pm_error("shift factor must be 0 or more"); + ++argn; + + /* parse in filename (if present, stdin otherwise) */ + if (argn != argc) + { + ifp = pm_openr(argv[argn]); + ++argn; + } + else + ifp = stdin; + + if (argn != argc) + pm_usage(usage); + + /* read first data from file */ + ppm_readppminit(ifp, &cols, &rows, &maxval, &format); + + if (shift > cols) + { + shift = cols; + pm_message("shift amount is larger than picture width - reset to %d", shift); + } + + /* no error checking required here, ppmlib does it all for us */ + srcrow = ppm_allocrow(cols); + + /* allocate a row of pixel data for the new pixels */ + destrow = ppm_allocrow(cols); + + ppm_writeppminit(stdout, cols, rows, maxval, 0); + + /* get time of day to feed the random number generator */ + timenow = time(NULL); + srand(timenow); + + /** now do the shifting **/ + /* the range by which a line is shifted lays in the range from */ + /* -shift/2 .. +shift/2 pixels; however, within this range it is */ + /* randomly chosen */ + for (i = 0; i < rows; i++) + { + if (shift != 0) + nowshift = (rand() % (shift+1)) - ((shift+1) / 2); + else + nowshift = 0; + + ppm_readppmrow(ifp, srcrow, cols, maxval, format); + + pP = srcrow; + pP2 = destrow; + + /* if the shift value is less than zero, we take the original pixel line and */ + /* copy it into the destination line translated to the left by x pixels. The */ + /* empty pixels on the right end of the destination line are filled up with */ + /* the pixel that is the right-most in the original pixel line. */ + if (nowshift < 0) + { + pP+= abs(nowshift); + for (j = 0; j < cols; j++) + { + PPM_ASSIGN(*pP2, PPM_GETR(*pP), PPM_GETG(*pP), PPM_GETB(*pP)); + pP2++; + if (j < (cols+nowshift)-1) + pP++; + } + } + /* if the shift value is 0 or positive, the first <nowshift> pixels of the */ + /* destination line are filled with the first pixel from the source line, */ + /* and the rest of the source line is copied to the dest line */ + else + { + for (j = 0; j < cols; j++) + { + PPM_ASSIGN(*pP2, PPM_GETR(*pP), PPM_GETG(*pP), PPM_GETB(*pP)); + pP2++; + if (j >= nowshift) + pP++; + } + } + + /* write out one line of graphic data */ + ppm_writeppmrow(stdout, destrow, cols, maxval, 0); + } + + pm_close(ifp); + ppm_freerow(srcrow); + ppm_freerow(destrow); + + exit(0); +} + diff --git a/editor/ppmspread.c b/editor/ppmspread.c new file mode 100644 index 00000000..569d1266 --- /dev/null +++ b/editor/ppmspread.c @@ -0,0 +1,127 @@ +/*********************************************************************/ +/* ppmspread - randomly displace a PPM's pixels by a certain amount */ +/* Frank Neumann, October 1993 */ +/* V1.1 16.11.1993 */ +/* */ +/* version history: */ +/* V1.0 12.10.1993 first version */ +/* V1.1 16.11.1993 Rewritten to be NetPBM.programming conforming */ +/*********************************************************************/ + +#include <string.h> +#include "ppm.h" + +/* global variables */ +#ifdef AMIGA +static char *version = "$VER: ppmspread 1.1 (16.11.93)"; /* Amiga version identification */ +#endif + +/**************************/ +/* start of main function */ +/**************************/ +int main(argc, argv) +int argc; +char *argv[]; +{ + FILE* ifp; + int argn, rows, cols, i, j; + int xdis, ydis, xnew, ynew; + pixel **destarray, **srcarray; + pixel *pP, *pP2; + pixval maxval; + pixval r1, g1, b1; + int amount; + time_t timenow; + const char * const usage = "amount [ppmfile]\n amount: # of pixels to displace a pixel by at most\n"; + + /* parse in 'default' parameters */ + ppm_init(&argc, argv); + + argn = 1; + + /* parse in amount & seed */ + if (argn == argc) + pm_usage(usage); + if (sscanf(argv[argn], "%d", &amount) != 1) + pm_usage(usage); + if (amount < 0) + pm_error("amount should be a positive number"); + ++argn; + + /* parse in filename (if present, stdin otherwise) */ + if (argn != argc) + { + ifp = pm_openr(argv[argn]); + ++argn; + } + else + ifp = stdin; + + if (argn != argc) + pm_usage(usage); + + /* read entire picture into buffer */ + srcarray = ppm_readppm(ifp, &cols, &rows, &maxval); + + /* allocate an entire picture buffer for dest picture */ + destarray = ppm_allocarray(cols, rows); + + /* clear out the buffer */ + for (i=0; i < rows; i++) + memset(destarray[i], 0, cols * sizeof(pixel)); + + /* set seed for random number generator */ + /* get time of day to feed the random number generator */ + timenow = time(NULL); + srand(timenow); + + /* start displacing pixels */ + for (i = 0; i < rows; i++) + { + pP = srcarray[i]; + + for (j = 0; j < cols; j++) + { + xdis = (rand() % (amount+1)) - ((amount+1) / 2); + ydis = (rand() % (amount+1)) - ((amount+1) / 2); + + xnew = j + xdis; + ynew = i + ydis; + + /* only set the displaced pixel if it's within the bounds of the image */ + if (xnew >= 0 && xnew < cols && ynew >= 0 && ynew < rows) + { + /* displacing a pixel is accomplished by swapping it with another */ + /* pixel in its vicinity - so, first store other pixel's RGB */ + pP2 = srcarray[ynew] + xnew; + r1 = PPM_GETR(*pP2); + g1 = PPM_GETG(*pP2); + b1 = PPM_GETB(*pP2); + /* set second pixel to new value */ + pP2 = destarray[ynew] + xnew; + PPM_ASSIGN(*pP2, PPM_GETR(*pP), PPM_GETG(*pP), PPM_GETB(*pP)); + + /* now, set first pixel to (old) value of second */ + pP2 = destarray[i] + j; + PPM_ASSIGN(*pP2, r1, g1, b1); + } + else + { + /* displaced pixel is out of bounds; leave the old pixel there */ + pP2 = destarray[i] + j; + PPM_ASSIGN(*pP2, PPM_GETR(*pP), PPM_GETG(*pP), PPM_GETB(*pP)); + } + pP++; + } + } + + /* write out entire dest picture in one go */ + ppm_writeppm(stdout, destarray, cols, rows, maxval, 0); + + pm_close(ifp); + ppm_freearray(srcarray, rows); + ppm_freearray(destarray, rows); + + exit(0); +} + diff --git a/editor/ppmtv.c b/editor/ppmtv.c new file mode 100644 index 00000000..da25102a --- /dev/null +++ b/editor/ppmtv.c @@ -0,0 +1,105 @@ + +/*********************************************************************/ +/* ppmtv - make a 'look-alike ntsc' picture from a PPM file */ +/* Frank Neumann, October 1993 */ +/* V1.1 16.11.1993 */ +/* */ +/* version history: */ +/* V1.0 12.10.1993 first version */ +/* V1.1 16.11.1993 Rewritten to be NetPBM.programming conforming */ +/*********************************************************************/ + +#include "ppm.h" + +/**************************/ +/* start of main function */ +/**************************/ +int main(argc, argv) +int argc; +char *argv[]; +{ + FILE* ifp; + int argn, rows, cols, format, i = 0, j = 0; + pixel *srcrow, *destrow; + pixel *pP = NULL, *pP2 = NULL; + pixval maxval; + double dimfactor; + long longfactor; + const char * const usage = "dimfactor [ppmfile]\n dimfactor: 0.0 = total blackness, 1.0 = original picture\n"; + + /* parse in 'default' parameters */ + ppm_init(&argc, argv); + + argn = 1; + + /* parse in dim factor */ + if (argn == argc) + pm_usage(usage); + if (sscanf(argv[argn], "%lf", &dimfactor) != 1) + pm_usage(usage); + if (dimfactor < 0.0 || dimfactor > 1.0) + pm_error("dim factor must be in the range from 0.0 to 1.0 "); + ++argn; + + /* parse in filename (if present, stdin otherwise) */ + if (argn != argc) + { + ifp = pm_openr(argv[argn]); + ++argn; + } + else + ifp = stdin; + + if (argn != argc) + pm_usage(usage); + + /* read first data from file */ + ppm_readppminit(ifp, &cols, &rows, &maxval, &format); + + /* no error checking required here, ppmlib does it all for us */ + srcrow = ppm_allocrow(cols); + + longfactor = (long)(dimfactor * 65536); + + /* allocate a row of pixel data for the new pixels */ + destrow = ppm_allocrow(cols); + + ppm_writeppminit(stdout, cols, rows, maxval, 0); + + /** now do the ntsc'ing (actually very similar to ppmdim) **/ + for (i = 0; i < rows; i++) + { + ppm_readppmrow(ifp, srcrow, cols, maxval, format); + + pP = srcrow; + pP2 = destrow; + + for (j = 0; j < cols; j++) + { + /* every alternating row is left in unchanged condition */ + if (i & 1) + { + PPM_ASSIGN(*pP2, PPM_GETR(*pP), PPM_GETG(*pP), PPM_GETB(*pP)); + } + /* and the other lines are dimmed to the specified factor */ + else + { + PPM_ASSIGN(*pP2, (PPM_GETR(*pP) * longfactor) >> 16, + (PPM_GETG(*pP) * longfactor) >> 16, + (PPM_GETB(*pP) * longfactor) >> 16); + } + pP++; + pP2++; + } + + /* write out one line of graphic data */ + ppm_writeppmrow(stdout, destrow, cols, maxval, 0); + } + + pm_close(ifp); + ppm_freerow(srcrow); + ppm_freerow(destrow); + + exit(0); +} + |