Path: ccsf.homeunix.org!ccsf.homeunix.org!news1.wakwak.com!nf1.xephion.ne.jp!onion.ish.org!onodera-news!newsfeed.media.kyoto-u.ac.jp!news.tains.tohoku.ac.jp!news.iwate-pu.ac.jp!g236y004 From: Newsgroups: fj.sources,fj.comp.image Subject: ppmtogif-noLZW.c v4.9 Followup-To: fj.sources.d,fj.comp.image Date: Tue, 13 Jan 2004 12:48:19 JST Organization: messy enough to have no footspace Lines: 1723 Message-ID: <2504113200412.480112@sra-tohoku.co.jp.msgid> References: <472621200304.540631@sra-tohoku.co.jp.msgid> <3c024137$0$202$25292c14@hirose.net.tohoku.ac.jp> NNTP-Posting-Host: nwuxe001.iwate-pu.ac.jp X-Newsreader: mnews [version 1.22PL5] 2002-11-27(Wed) Xref: ccsf.homeunix.org fj.sources:34 fj.comp.image:32 Archive-name: ppmtogif-noLZW.c Version: $Revision: 4.9 $ Last-Modified: $Date: 2004-01-08 17:41:43+09 $ かべ。 とりあえず現状放出。 ・ppmを食って ・ランレングスGIFを吐く Cプログラムです。 前回(1.28)からの変更点: ・高速化 ・外部カラーマップ (-map ppmfile) ・動画GIF出力対応(!) ・ランレングスに加え、無圧縮出力もサポート 最新ブラウザだけ相手にする&&ベタ塗り絵ならPNGのほうが ずっと優れてますが、ビンテージブラウザを常用してると そうもいきませんで… 参考:他のツールの状況 ・無圧縮 (1ピクセル1コード) ImageMagick djpeg netpbm の ppmtogif -nolzw (コードはdjpegと同じ) ・ランレングス これ gifsicle (アルゴリズム的にちょっとLZWとかぶっている?) ・独自 whirlgif (作者曰くB-Treeを使っており特許問題なし) なんと、無圧縮とランレングスの両方吐けるツールは このppmtogif-noLZWが初めてのようです(ほんとかよ) ランレングスにするとだいたいLZWの4倍くらいの大きさにふくれます。 ブラウザが対応してるならBMPや生PPMをgzipしたほうが小さいです。 GIF出力をさらに圧縮する場合は、コード長を4bitか8bitにしたほうがいいので、 入力は8色か128色に減色したほうがよいでしょう。 -- kabe #if shell : ${CDEBUGFLAGS:=-g -O -Wall -Wstrict-prototypes} set -x ${CC:=gcc} ${CFLAGS:=${CDEBUGFLAGS}} $0 exit #endif /* * Archive-name: ppmtogif-noLZW.c * Version: $Revision: 4.9 $ * Last-Modified: $Date: 2004-01-08 17:41:43+09 $ * * non-LZW,runlength GIF encoder * * eats PPM, outs GIF to stdout * * Author: kabe$sra-tohoku.co.jp * * Changes from 1.20: * faster (cache colormap lookup) * terser (use -v to get diagnostic) * can build animated GIF * -map refcolors.ppm now works * add "-norle" to enable nocompress mode * don't rely on EOF for pixel reads * * TODO: * -codelen # * netpbm-ppmtogif commandline compatibility * canonicalize/clear "pixel" "index" (esp.IRT netpbm) * immediate error on invalid colorspec * "-sort" sort colormap * (use "struct pixel" IRT libppm)(no benefit?) * use local colormap? (esp anim) * builtin 6x6x6+16=226 cmap cube * output diff pixels for anim inter-frame * do something with peek_v * * redef initcodesize +1 * build runlength on read? * (win, as full image don't have to be read into memory) * * debug options: * -DNO_TWIRL * -DDUMP_CMAP * -DPROFILE_CMAP_HASH=1 * * License: * * Modification and redistribution is allowed. * * No gurantee and no liability by author for anything * including but not limited to * infringing patents; use at your own risk. */ static const char _rcsid[] = "$Id: ppmtogif-noLZW.c,v 4.9 2004-01-08 17:41:43+09 kabe Exp kabe $"; #include #include /*strcmp, memset*/ #include /*toupper*/ #include /*malloc, abs, atoi*/ #include /*errno, EINVAL*/ #include /*va_list*/ /* bbstream.h { */ typedef struct { FILE *fd; unsigned int dangle; /* dangling bits not enough for an octet; LSB filled */ int danglebits; /* current data bits in dangle */ int buflen; /* current data length of buf */ unsigned char buf[260]; /* 256+4(possible overpush) */ } bbstream_t; bbstream_t *bbstream_open(FILE *outfd); int bbstream_out(bbstream_t *bb, int data, int bits); void bbstream_close(bbstream_t *bb); /* } bbstream.h */ /*****************************************************************/ /* libppm-ish routines { */ /*static*/ struct { char *progname; int verbose; } G = { .progname = "ppmtogif", .verbose = 0 }; void pm_init( int *argcP, char *argv[] ) { /* init progname */ G.progname = strrchr( argv[0], '/' ); if (!G.progname) { G.progname = argv[0]; } else { G.progname++; /* skip '/' */ } #if defined(O_BINARY) && HAVE_SETMODE /* DOS binary mode; this only matters for cygwin. * cf. netpbm pbm/libpbm1.c:pm_init() */ if (!isatty(fileno(stdin))) setmode(fileno(stdin) ,O_BINARY); if (!isatty(fileno(stdout))) setmode(fileno(stdout),O_BINARY); #endif } /* printf to stderr with program name prefixed */ int pm_message(const char fmt[], ... ) { int ret; va_list va; va_start(va, fmt); fputs(G.progname, stderr); fputs(": ", stderr); ret = vfprintf(stderr, fmt, va); va_end(va); return ret; } /* * void ppm_readppminit( FILE* fp, int* colsP, int* rowsP, pixval* maxvalP, int* formatP ) *formatP is ('P'<<8) + char */ void ppm_readppminit( FILE* ppmfd, int* colsP, int* rowsP, int* maxvalP, int* formatP) { if ((*formatP = fgetc(ppmfd)) != 'P') { pm_message("input not PNM\n"); exit(1); } *formatP = (*formatP << 8) + fgetc(ppmfd); /* '3' '6' */ fscanf(ppmfd, "%d %d %d", colsP, rowsP, maxvalP); fgetc(ppmfd); /* discard terminating newline */ } /* xtol("fedc",3) returns 0xfed */ static unsigned long xtol( hex, len ) const char *hex; /* string to parse */ size_t len; /* length to parse in */ { static const char * const x = "0123456789ABCDEF"; unsigned long ret = 0; while (*hex && len) { char *p = strchr(x, toupper(*hex)); if (!p) { errno = EINVAL; return 0; } ret = (ret<<4) + p-x; hex++; len--; } return ret; } /* * pixel ppm_parsecolor( char* colorname, pixval maxval ) * colorname: * "#rgb" "#rrggbb" "#rrrgggbbb" "#rrrrggggbbbb" * maxval: full-scale pixel value to normalize the return RGB values */ void ppm_parsecolor(const char *cname, unsigned int maxval, unsigned *rP,unsigned *gP,unsigned *bP) { unsigned parsedmaxval; unsigned long r,g,b; if (cname[0]=='#') { int s; switch(s=strlen(cname+1)) { int cstep; case 3: /* #rgb */ case 6: /* #rrggbb */ case 9: /* #rrrgggbbb */ case 12: /* #rrrrggggbbbb */ cstep=s/3; /* 1,2,3,4 */ parsedmaxval = (1<<(cstep*4))-1; /*0xf,0xff,0xfff,0xffff*/ r = xtol(cname+1 , cstep); g = xtol(cname+1+cstep , cstep); b = xtol(cname+1+cstep*2, cstep); break; default: goto parsefail; } }/*transrgb="#rgb"*/ else { goto parsefail; } /* redepth */ if (parsedmaxval == maxval) { /* match; do nothing */ } else if (parsedmaxval < maxval) { /* extend */ int /*double*/ ratio = maxval / parsedmaxval; r = r * ratio; g = g * ratio; b = b * ratio; } else if (parsedmaxval > maxval) { /* shrink */ int /*double*/ ratio = parsedmaxval / maxval; r = r / ratio; g = g / ratio; b = b / ratio; } *rP = r; *gP = g; *bP = b; return; parsefail: pm_message("cannot parse colorname <%s>\n", cname); exit(1); } /* * pnm_readpixel * no libpnm equivalent * read a single pixel from pnm file, after ppm_readppminit() * pnmfd: FILE* for read * pnmtype: 'P3' 'P6' * maxval: of the pnm file; 255 || 65535 * rP,gP,bP: returned pixel value * return: 0 if success, EOF if eof */ int pnm_readpixel(pnmfd, pnmtype, maxval, rP,gP,bP) register FILE * const pnmfd; const int pnmtype; const unsigned maxval; register unsigned * const rP, * const gP, * const bP; { /* read one pixel */ switch (pnmtype) { case ('P'<<8)+'3': /* ascii PPM# */ if (feof(pnmfd)) return EOF; if ( 3 == fscanf(pnmfd, "%d %d %d", rP,gP,bP)) { break; } return EOF; case ('P'<<8)+'6': /* raw PPM */ /* read fullscale value here */ if (maxval < 256) { /* 8 bit */ *rP = getc(pnmfd) & 255; *gP = getc(pnmfd) & 255; *bP = getc(pnmfd) & 255; } else { register unsigned m; /* 16 bit (MSByte first) */ m = getc(pnmfd); *rP = ((m<<8)+getc(pnmfd))&65535; m = getc(pnmfd); *gP = ((m<<8)+getc(pnmfd))&65535; m = getc(pnmfd); *bP = ((m<<8)+getc(pnmfd))&65535; } if (feof(pnmfd)) return EOF; break; default: pm_message("PNM type <%c%c> not supported now\n", pnmtype>>8,pnmtype&255); /* should bail immediately, not neat */ exit(1); return EOF; }/*esac pnmtype*/ return 0; } /* } libppm-ish routines */ /*****************************************************************/ /* cmap.c { */ struct cmap_t { int cmaplen; /*valid cmap[] entries*/ struct { unsigned /*char*/ r,g,b; /* 8 bit is enough */ } cmap[256]; /* used for fast rgb->index translation in cmap_findent()*/ struct cmap_hent_t { int i; unsigned r,g,b; } hash[256]; /* 256 entries == 4kB */ #if PROFILE_CMAP_HASH int finds, hits, clashes; #endif }; #define HASHENT(r,g,b) (((r)+(g)+(b))&255) /* clear the cmap */ void cmap_init(struct cmap_t * const cmap) { int i; cmap->cmaplen = 0; /* for (i=0; i<256; i++) cmap->cmap[i].r=cmap->cmap[i].g=cmap->cmap[i].b=0; */ memset(cmap->cmap, 0, sizeof(cmap->cmap)); for (i=0;i<256;i++) cmap->hash[i].i = -1; #if PROFILE_CMAP_HASH cmap->finds = 0; cmap->hits = 0; cmap->clashes = 0; #endif } /* return index of cmap.cmap[], -1 if not found */ /* this routine is the bottleneck */ int cmap_findent(cmap, r,g,b, matchmode) register struct cmap_t * const cmap; const unsigned r,g,b; const int matchmode; /* '=':exact '~':best */ { register int i; int best_i; struct cmap_hent_t *hent; long best_d = 0x7fffffff; /*LONG_MAX*/ #if PROFILE_CMAP_HASH cmap->finds++; #endif /* simple optimize: use 256 entry hash for recent queries */ hent = &cmap->hash[HASHENT(r,g,b)]; if (hent->i>=0 && hent->r==r && hent->g==g && hent->b==b) { #if PROFILE_CMAP_HASH cmap->hits++; #endif i = hent->i; return i; } /* TODO: linear search, which is VERY slow */ /* TODO: pack RGB into single integer for fast compare */ best_i = -1; for (i=0; icmaplen; i++) { if (cmap->cmap[i].r==r && cmap->cmap[i].g==g && cmap->cmap[i].b==b) { /* found */ hent->i = i; hent->r = r; hent->g = g; hent->b = b; return i; } if (matchmode == '~') { #if USE_EUCLID_DISTANCE long x1,x2,x3,d; x1 = (long)cmap->cmap[i].r - (long)r; x2 = (long)cmap->cmap[i].g - (long)g; x3 = (long)cmap->cmap[i].b - (long)b; d = (x1*x1)+(x2*x2)+(x3*x3); #else long d; /* use manhattan distance */ d = labs((long)cmap->cmap[i].r - (long)r) + labs((long)cmap->cmap[i].g - (long)g) + labs((long)cmap->cmap[i].b - (long)b); #endif if (d < best_d) { best_i = i; best_d = d; } } }/*next i*/ /* not found. pickup best */ i = best_i; /* always -1 if matchmode='=' */ if (i>=0) { #if PROFILE_CMAP_HASH if (hent->i >=0) cmap->clashes++; #endif hent->i = i; hent->r = r; hent->g = g; hent->b = b; } return i; } /* return index of added color into cmap.cmap[], -1 for error */ /* NOTE: doesn't check for duplicate entry */ int cmap_add(cmap, r,g,b) register /*nonconst*/ struct cmap_t * const cmap; const unsigned r,g,b; { int i; if (cmap->cmaplen==256) { /* already full */ /* pm_message("Color Palette overflow, use ppmquant 256 to reduce colors\n"); */ return -1; } i = cmap->cmaplen; cmap->cmap[i].r=r; cmap->cmap[i].g=g; cmap->cmap[i].b=b; cmap->cmaplen++; return i; } /* cmap.c } */ #define fput_LEs(x,fd) fputc((x)&255,(fd));fputc(((x)>>8)&255,(fd)) typedef unsigned char gifPixel; /* GIF pixel index */ /* * return: pixel index for transrgb parameter; -1 if not found * transrgb: transparency spec * NULL | "none" * "upperleft" * "background" | "bg" most used color * "#rrggbb" RGB spec */ int get_transparentindex(gifPixel raster[], size_t rasterlen, const struct cmap_t * const cmap, const char * const transrgb) { int transindex = -1; int i; if (transrgb == NULL || transrgb[0]=='\0') return -1; if (!strcmp(transrgb, "none")) { /* do nothing */ ; } else if (!strncmp(transrgb, "background", 1)) { /* count the pixel freq and select the most-used-color */ unsigned int xfreq[256]; /* pixel frequency counter */ size_t x; memset(xfreq, 0, sizeof(xfreq)); for (x=0; xcmaplen; i++) { if (xfreq[transindex] < xfreq[i]) { transindex = i; } } } else if (!strncmp(transrgb, "upperleft", 1)) { transindex = raster[0]; } else { unsigned r,g,b; ppm_parsecolor(transrgb, 255, &r,&g,&b); i = cmap_findent(cmap, r,g,b, '~'); /* find best match */ if (i >= 0) { transindex = i; } else { pm_message("Warning: color {%d %d %d} not found, no transparency set\n", r,g,b); /*transindex = -1;*/ } }/*transrgb*/ /* now, transindex is set if transparency is there */ return transindex; } /* * transindex: pixel index to use for transparency, -1 if undefined */ void gifout_GCE( FILE *outfd, int transindex, int delay ) { int i; /* graphic control extension (transparency,delay) */ if (transindex < 0 && delay == 0) return; /* do nothing */ /* output GCE */ fputc('!', outfd); fputc(249, outfd); fputc(4, outfd); /* sizeof(this block) */ i = (0 /*:3 reserved*/ <<5) | (0 /*:3 disposal method*/ <<2) | /* none,keep,bg,prev */ (0 /*:1 user input flag*/<<1) | ((transindex >= 0)&1) /*:1 transparent*/; fputc(i, outfd); fput_LEs(delay, outfd); /* delay 1/100s */ if (transindex<0) transindex=0; /* clamp if no spec */ fputc(transindex, outfd); /*transparency index*/ fputc(0, outfd); /*terminator*/ } /* * Read a PPM file from head, and * build a indexed raster and cmap * * Calling convention: * FILE *ppmfd; * int width, height; * gifPixel *raster; * size_t rasterlen; * struct cmap_t cmap; * * ppmfd = fopen("file.ppm", "rb"); * cmap_init(&cmap); * ppm_readraster(ppmfd, &raster,&rasterlen, &width,&height, &cmap, '+'); * * * raster will be allocated; free(raster) afterwards * * rasterlen, width, height will be assigned a scalar value * * cmap is updated to contain new colors if cmap_mode=='+' * * Passing NULL for raster will just aquire the cmap * * pnm_readpnm() is similar, but has vast different call convention */ int ppm_readraster(register FILE *ppmfd, gifPixel *raster_r[], /* allocated raster, must be free()ed afterward */ size_t *rasterlen_r, /* will be width*height */ int *width_r, int *height_r, /* return */ struct cmap_t *cmap_r, /* pass in cmap_init()ed cmap */ int cmap_mode /* '+':append '.':don't modify cmap;use best match */ ) { int width, height, depth, pnmtype; int i; size_t rasterlen; size_t pixct; /* for loop */ gifPixel *raster = NULL; ppm_readppminit(ppmfd, &width, &height, &depth, &pnmtype); if (G.verbose >= 1) { pm_message("PNM type: %c%c\n", (pnmtype>>8),pnmtype&255); pm_message("size: %d x %d = %lu\n", width, height, (unsigned long)width*height); pm_message("depth: %d\n", depth); } if (width_r) *width_r = width; if (height_r) *height_r = height; /* read PNM body */ /** normalize to 0-255, create colormap, create indexed raster stream */ if (G.verbose >= 1) { pm_message("Reading PPM"); fprintf(stderr, ", normalize"); if (cmap_mode!='.') fprintf(stderr, ", getting colormap"); if (cmap_mode=='.' && raster_r) fprintf(stderr, ", selecting nearest color"); if (raster_r) fprintf(stderr, ", build index array"); fprintf(stderr, "...\n"); } /* preallocate raster array, as we already know the total size */ rasterlen = width * height; if (raster_r == NULL) { /* caller just wants to update colormap */ raster = NULL; ; } else { raster = malloc(rasterlen*sizeof(raster[0])); if (!raster) { fprintf(stderr, "malloc() failed for gifRaster.raster!!\n"); exit(1); } } /* pixel counts to read are known beforehand; don't rely on EOF */ for (pixct=0; pixct < rasterlen; pixct++) { unsigned int r,g,b; /* read one pixel */ if (EOF == pnm_readpixel(ppmfd, pnmtype, depth, &r,&g,&b)) { fprintf(stderr, "Warning: short read %lu/%lu\n", (unsigned long)pixct, (unsigned long)rasterlen); break; /*goto ppmeof*/ } /* normalize to 0-255 */ if (depth == 255) { /* do nothing */ ; } else if (depth == 65535) { /* shift 8 bits */ r >>= 8; /* r /= 257 is more accurate */ g >>= 8; b >>= 8; } else { /* calculate it */ r = r*255/depth; g = g*255/depth; b = b*255/depth; } /* set x255 [list $r $g $b]*/ /* If cmap is to be kept (cmap_mode='.'), * find for approximate color ('~'), * otherwise find exact color ('=') and add it if not found. */ if (cmap_mode == '.' && !raster) { /* skip cmap_findent if not needed */ } else { i = cmap_findent(cmap_r, r,g,b, (cmap_mode=='.')?'~':'='); if (i < 0) { /* not found. add it*/ i = cmap_add(cmap_r, r,g,b); if (i < 0) { /* already full */ pm_message("Color Palette overflow, use \"ppmquant 256\" to reduce colors\n"); return 1; } } /* now cmap.cmap[i] points to the pixel */ /* add the new pixel index to gifRaster */ /*expand shouldn't happen, as we already allocated it*/ if (raster) raster[pixct] = i; } #ifndef NO_TWIRL /* twirl counter */ if (G.verbose >= 0 && (pixct & 0x1fff) == 0) { /* puts -nonewline stderr "." */ fprintf(stderr, "\rRead %d%% %c\010", pixct*100 / rasterlen, "-\\|/"[(pixct/0x2000)%4] ); /*CT*/ } #endif }/*next pixct*/ /*ppmeof: ;*/ #ifndef NO_TWIRL if (G.verbose>=0) fprintf(stderr, "\rRead 100%% \n"); /*CT*/ #endif if (raster_r) { *raster_r = raster; raster = NULL; /* memory ownership to caller */ } if (rasterlen_r) *rasterlen_r = pixct; if (G.verbose>=1) if (cmap_mode == '+') pm_message("Got %d colormap entries\n", cmap_r->cmaplen); #if PROFILE_CMAP_HASH pm_message("cmap profile: hash hits %d clash %d / %d\n", cmap_r->hits, cmap_r->clashes, cmap_r->finds); #endif return 0; } /* add signature of this program */ void gifout_addprogsig(FILE *outfd) { static const char rev[] = "$Revision: 4.9 $"; char comment[80]; strcpy(comment, "$" "Rem: non-LZW, runlength GIF Encoder (impl C ver "); strcat(comment, rev+11); strcpy(comment+strlen(comment)-2, ") $"); fputc('!', outfd); fputc(254, outfd); fputc(strlen(comment), outfd); fputs(comment, outfd); fputc('\0', outfd); } /* * * ppmtogif: Convert PPM stream to GIF stream * * ppmfd: filehandle for PPM stream input * outfd: filehandle for GIF stream output * given_cmap: -map colormap to use. If NULL, build a new one from input. * transrgb: transparency spec * "none" * "upperleft" * "background" most used color * "#rrggbb" RGB spec * compressmode: 'r':RLE 'n':raw (no compression) * * Most straightforward invocation is * ppmtogif(stdin, stdout, NULL, NULL, 'r'); * */ int ppmtogif(FILE * const ppmfd, FILE * const outfd, struct cmap_t *given_cmap, const char *transrgb, int compressmode) { int width,height; struct { int theLen; gifPixel *raster; } gifRaster = {0,NULL}; struct cmap_t *cmap; int pixbits; /* bits needed for this GIF */ int i; int gifraster_runlength_dc(FILE *outfd, gifPixel indexraster[],size_t indexrasterlen, int pixbits); int gifraster_nocompress(FILE *, gifPixel [], size_t, int); /* select colormap */ if (given_cmap == NULL) { cmap = malloc(sizeof(struct cmap_t)); cmap_init(cmap); } else { cmap = given_cmap; if (G.verbose>=1) pm_message("Using external cmap with %d entries\n", given_cmap->cmaplen); // for (i=0;icmaplen;i++) { fprintf(stderr, " %3d: %3d %3d %3d\n", i,cmap->cmap[i].r,cmap->cmap[i].g,cmap->cmap[i].b); } #if PROFILE_CMAP_HASH cmap->hits=cmap->finds=cmap->clashes = 0; #endif } /* read PNM */ i = ppm_readraster(ppmfd, &gifRaster.raster,&gifRaster.theLen, &width, &height, cmap, (given_cmap==NULL)?'+':'.'); if (i!=0) return i; /* ## values set so far: ## width,height ## gifRaster.raster[] {pixel pixel ...} ## cmap.cmap[].{r,g,b} {r g b} {r g b} ... */ /* TODO: -sort colormap (what is it used for?)*/ #if DUMP_CMAP fprintf(stderr, "Colormap:\n"); for (i=0; icmaplen; i++) {fprintf(stderr, "%3d:{%d %d %d}\n", i, cmap->cmap[i].r, cmap->cmap[i].g, cmap->cmap[i].b);} #endif /* get bitlength needed for this pixel values */ for (pixbits=1; (1<cmaplen; pixbits++) ; if (G.verbose>=1) pm_message("Needed Pixel bits: %d\n", pixbits); if (pixbits > 8) { pm_message("Too many colors (%d bits)\n", pixbits); return 1; } /* output GIF header */ fprintf(outfd, transrgb?"GIF89a":"GIF87a"); /*XXX should be "89a" if using transparency*/ /* logical screen descriptor */ fput_LEs(width, outfd); fput_LEs(height, outfd); i = (1 /*:1 Global colormap follows*/<<7) | (7 /*:3 bits per color intensity*/ <<4) | (0 /*:1 Global colormap is sorted*/ <<3) | (pixbits-1 /*:3 (bits/pixel)-1 */); fputc(i, outfd); fputc(0, outfd); /* background */ fputc(0, outfd); /* aspect ratio */ /* Global color map */ for (i=0; icmaplen; i++) { fputc(cmap->cmap[i].r, outfd); fputc(cmap->cmap[i].g, outfd); fputc(cmap->cmap[i].b, outfd); } /* pad colormap to 2**pixbits */ for ((void) i; i<(1<=1) if (i>=0) pm_message("Using index %d (%d %d %d) for transparency\n", i, cmap->cmap[i].r,cmap->cmap[i].g,cmap->cmap[i].b); /* Graphic Control Extention: transparency, delay */ gifout_GCE(outfd, i, 0/*delay*/); /* Image Descriptor */ fputc(',', outfd); fput_LEs(0, outfd); fput_LEs(0, outfd); /*left,top*/ fput_LEs(width, outfd); fput_LEs(height, outfd); i = (0 /*:1 0:use global colormap 1:has local colormap*/ <<7) | (0 /*:1 0:sequential 1:interlace*/ <<6) | (0 /*:1 sorted cmap*/ << 5) | (0 /*:2 reserved*/ <<3) | 0 /*3: (local cmap bits/pixel)-1*/; fputc(i, outfd); /* ### select algorithm ## These should output from to last null block #gifraster_nocompress $outfd $gifRaster $pixbits #gifraster_runlength $outfd $gifRaster $pixbits #gifraster_runlength_vc $outfd $gifRaster $pixbits */ switch(compressmode) { default: case 'r': gifraster_runlength_dc(outfd, gifRaster.raster,gifRaster.theLen, pixbits); break; case 'n': gifraster_nocompress(outfd, gifRaster.raster,gifRaster.theLen, pixbits); break; } /* ====== } */ /* include comment in trailer */ gifout_addprogsig(outfd); /* Image terminator */ fputc(';', outfd); free(gifRaster.raster); if (given_cmap == NULL) free(cmap); return 0; } /* no compression (reset on every table overflow) */ int gifraster_nocompress(FILE *outfd, gifPixel indexraster[],size_t indexrasterlen, int pixbits) { int initcodesize; /* initial code length after clear */ int codesize; /* current code bit length */ int clearcode; int inittablevacants; /* empty "table" slots after clear */ int tablevacants; /* current empty "table" slots */ bbstream_t *bbid; /* output byteblock stream */ size_t theindex; /* indexraster[theindex] */ initcodesize = pixbits + 1; /* Minimal 3bit basecode required to * accomodate Clear and Stop signal */ if (initcodesize <= 2) initcodesize = 3; /* TODO: initcodesize = 4 for efficient post-compress */ clearcode = (1<<(initcodesize-1)); /* ### non-compressing GIF stream ## Insert clear code before "table" cause a codesize extend. ## Initial code length will be 2**initcodesize ## Initial ("clear"ed) table will be filled with ## ## 0-(2**(initcodesize-1) -1) raw pixels ## (2**(initcodesize-1) +0,+1) clear,stop ## (2**(initcodesize-1) +2 ... 2**(initcodesize)-1) table vacant ## ## so we can push on by (2**(initcodesize)) - (2**(initcodesize-1) +2) ## entries == (2**(initcodesize-1) - 2). ## (actually 1 less, if not to extend codelen) ## ## ex. initcodesize=4 (pixbits=3) ## "table" = 01234567CExxxxxx ## <----> tablevacants = 2**(4-1)-2 = 6 ## ## initcodesize is the "code size" value +1 in the file. ## codesize is the current code size */ codesize = initcodesize; /* How much can we push on before table overflow (code extends)? */ inittablevacants = (1<<(initcodesize-1)) - 2; tablevacants = inittablevacants; /* output initial code size */ fputc(initcodesize-1, outfd); bbid = bbstream_open(outfd); #ifdef CLEAR #undef CLEAR #endif #define CLEAR() \ bbstream_out(bbid, clearcode, codesize); \ tablevacants = inittablevacants; \ codesize = initcodesize /* clear code first */ CLEAR(); for (theindex=0; theindex < indexrasterlen; theindex++) { bbstream_out(bbid, indexraster[theindex], codesize); /* initial output after clear doesn't extend the "table", * but to push for 1 less to not extend codelen, * leave this to decrement even for initial out */ tablevacants--; if (tablevacants == 0) { /* "table" is full-1. clear */ /* does not extend codelen now */ CLEAR(); } } /* end */ bbstream_out(bbid, clearcode + 1 /*end code*/, codesize); bbstream_close(bbid); return 0; } #if 0 /*{*/ ////# fixed codelen runlength //proc gifraster_runlength {outfd gifRaster pixbits} \ //{ // set initcodesize $pixbits // if {$initcodesize <= 1} {set initcodesize 2} ;# min 2bit basecode // // # make the real width == initial initcodesize+1 // set codesize [expr $initcodesize + 1] // set clearcode [expr (1 << $initcodesize)] // // ### Runlength-coding // ## 1. Reset on every pixel different from previous. // ## 2. For length-running pixels, fill the "vacant" table // ## with same pixels. // ## Do not extend the code size; just repeat the // ## last "table entry". // // set inittablevacants [expr (1 << $initcodesize) - 2] // // puts -nonewline $outfd [binary format {c} $initcodesize] // dputs "Initial codesize: $initcodesize" // // set bbid [bbstream_open $outfd] // // while {[llength $gifRaster]} { // // set theindex [lindex $gifRaster 0] // ## retrieve the runlength // for {set r 1} {[lindex $gifRaster $r] == $theindex} {incr r} {} //dputs "runlength $theindex x $r" // set gifRaster [lrange $gifRaster $r end] ;# heavy // // while {$r} { // ## reset on new pixel // bbstream_out $bbid $clearcode $codesize // set tabextent 0 ;# valid "table" entries past clear/end // set codesize [expr $initcodesize + 1] ;# (redundant) // // ## initial 1 // bbstream_out $bbid $theindex $codesize // incr r -1 // // ## output by runlength-like. // ## Progress with initial vacant "table", // ## then fill the "table" with "aa" "aaa" ... entries. // ## // ## for codelen=3, // ## "0" 4(clr), 0 ("table" = 0,1,2,3,C,E) // ## "00" 4,0,0 (0,1,2,3,C,E,00) // ## "000" 4,0,6 (0,1,2,3,C,E,00) // ## "0000" 4,0,6,0 (0,1,2,3,C,E,00,000)(full,codelen bump) // ## you have to "clear" for any further pixels, // ## as "table" is always growing. // ## Actually, you can't fill the table in full because // ## once the table is full the codelen is bumped. // ## // ## codelen=4 (inittablevacants=6) // ## "0" 8,0 (0,1,2,3,4,5,6,7,C,E) // ## "00" 8,0,0 (0,1,2,3,4,5,6,7,C,E,00) // ## "000" 8,0,10 (0,1,2,3,4,5,6,7,C,E,00) // ## "0000" 8,0,10,0 (0,1,2,3,4,5,6,7,C,E,00,000) // ## "00000" 8,0,10,10 (0,1,2,3,4,5,6,7,C,E,00,000,0000) // ## "000000" 8,0,10,11 (0,1,2,3,4,5,6,7,C,E,00,000,0000,00000) // ## "0000000" 8,0,10,12 (0,1,2,3,4,5,6,7,C,E,00,000,0000,00000,000000) // ## "00000000" 8,0,10,13 (0,1,2,3,4,5,6,7,C,E,00,000,0000,00000,000000,0000000)(full,codelen bump) // // ## max code able to output is $clearcode + 2 + $tabextent // ## max length solvable is $tabextent + 2 // ////#puts stderr "tabextent=$tabextent / $inittablevacants" // while {$tabextent < [expr $inittablevacants - 1]} { // ## Pack it until the table is almost (inittablevacants-1) // ## filled. You can't fill up the table; the // ## codelen will be bumped if so. // if {$r == 0} { // # next iterate // } elseif {$r == 1} { // bbstream_out $bbid $theindex $codesize // incr r -1 // } elseif {$r <= [expr $tabextent + 2]} { // ## solvable // ## r==2: out=$clearcode + 2 // ## r==3: out=$clearcode + 3 // bbstream_out $bbid [expr $clearcode + $r] $codesize // incr r -$r // } else { // ## not yet solvable. out the longest // bbstream_out $bbid [expr $clearcode + 2 + $tabextent] $codesize // incr r -2; incr r -$tabextent // } // incr tabextent // } ;# while tabextent // // # table is full; reset // // } ;#while $r // } ;# while gifRaster // // # end // bbstream_out $bbid [expr $clearcode + 1] $codesize // // bbstream_close $bbid //} // ////## variable codelength runlength ////## this algoritm seems most feasible //proc gifraster_runlength_vc {outfd gifRaster pixbits} \ //{ // set initcodesize $pixbits // if {$initcodesize <= 1} {set initcodesize 2} ;# min 2bit basecode // // # make the real width == initial initcodesize+1 // set codesize [expr $initcodesize + 1] // set clearcode [expr (1 << $initcodesize)] // // ### Runlength-coding with variable codelen // ## 1. Reset on every pixel different from previous. // ## 2. For length-running pixels, fill the "vacant" table // ## with same pixels. // ## Extend the codelen as needed. // // ## Things that doesn't change: // ## initcodesize // ## clearcode // ## inittablevacants (tablevacants reset to this only on clear) // ## Things to change on clear: // ## tabextent // ## tablevacants // ## codesize // ## Things to change on codelen change: // ## codesize // ## tablevacants // // set inittablevacants [expr (1 << $initcodesize) - 2] // // puts -nonewline $outfd [binary format {c} $initcodesize] // dputs "Initial codesize: $initcodesize" // // set bbid [bbstream_open $outfd] // // while {[llength $gifRaster]} { // // set theindex [lindex $gifRaster 0] // ## retrieve the runlength // for {set r 1} {[lindex $gifRaster $r] == $theindex} {incr r} {} //dputs "runlength $theindex x $r" // set gifRaster [lrange $gifRaster $r end] ;# heavy // // while {$r} { // ## reset on new pixel // bbstream_out $bbid $clearcode $codesize // set tabextent 0 // set codesize [expr $initcodesize + 1] // set tablevacants $inittablevacants // // ## initial 1 // bbstream_out $bbid $theindex $codesize // incr r -1 // // ## max code able to output is $clearcode + 2 + $tabextent // ## max length solvable is $tabextent + 2 // // while {$r} { //dputs "tabextent=$tabextent / $tablevacants" // if {$r == 0} { // error "NOTREACHED" ;# because of while // } elseif {$r == 1} { // bbstream_out $bbid $theindex $codesize // incr r -1 // } elseif {$r <= [expr $tabextent + 2]} { // ## solvable // ## r==2: out=$clearcode + 2 // ## r==3: out=$clearcode + 3 // bbstream_out $bbid [expr $clearcode + $r] $codesize // incr r -$r // } else { // ## not yet solvable. out the longest // bbstream_out $bbid [expr $clearcode + 2 + $tabextent] $codesize // incr r -2; incr r -$tabextent // } // incr tabextent // // ## expand the "table" as needed. // if {$tabextent == $tablevacants} { // ## The decoder has already bumped the codesize // ## (thus tablesize) now. // ## // ## New table: // ## 0-2**(initcodesize)-1 pixel // ## 2**(initcodesize)+0,+1 clear,stop // ## 2**(initcodesize)+2 - 2**(codesize)-1 enlarged // ## New is thus // ## 2**(codesize) - (2**(initcodesize)+2) // ## == 2**(codesize) - 2**(initcodesize) - 2 // incr tablevacants [expr (1 << $codesize)] // incr codesize // // if {$codesize > 12} { // #table overflow; reset! // break ;# tabextent // } // } // // } ;# while tabextent $r // // # end runlength or table full // // } ;#while $r // } ;# while gifRaster // // # end // bbstream_out $bbid [expr $clearcode + 1] $codesize // // bbstream_close $bbid //} #endif /*}*/ /* runlength, adaptive codelen, ondemand clear */ int gifraster_runlength_dc(FILE *outfd, gifPixel indexraster[],size_t indexrasterlen, int pixbits) { int codesize; /*current (real) codesize */ int clearcode; /*constant determined from initial code size*/ int inittableremain; /*vacant slots in the "table" after clear*/ int tableremain; /*current vacant slots; decremented on output, reset to inittableremain on clear*/ int runindex; /*current vacant top of the "table"*/ int runhead; /*index into "table" of the second pixel of current runlength */ /*(first pixel is in range 0 - ((pixbits**2)-1) )*/ bbstream_t *bbid; /* output byteblock stream */ /* memmove() is really heavy, so optimize! */ int theraster; /* current(next) runlength start as indexraster[theraster] */ /* non-changing values */ int initcodesize = pixbits + 1; /* +1 to accomodate CLEAR,STOP */ if (initcodesize <= 2) initcodesize=3; /* min 3bit basecode */ /*(initcodesize now refers to real codesize for this routine)*/ codesize = initcodesize; clearcode = 1 << (initcodesize - 1); /* {init,}codesize is the actual bit width * ( written in the file is 1 less) */ /* ## Initial "table" size is 2**initcodesize, filled with ## 0 -- 2**(initcodesize-1==usually pixbits)-1 raw pixel values ## 2**(initcodesize-1)+0,+1 clear,stop ## 2**(initcodesize-1)+2 -- 2**(initcodesize)-1 vacant ## ex. for initcodesize=4 (depth 3bit) ## table={01234567CS------} ## ## The vacant "table" size is ## (2**initcodesize)-(2**(initcodesize-1)+2) ## == (2**initcodesize) - (clearcode + 2) ## We can push this far(actually 1 less) without ## extending the codelen */ inittableremain = (1< $codesize ## incr r -1 ## incr tableremain -1 ## incr runindex ## ## (if tableremain==0 the code size has extended) ##dothisbefore? ## if {!$tableremain} { ## incr tableremain [expr 1 << $codesize] ## incr codesize ## if {$codesize > 12} { ## set codesize 12 ## ##codesize too long; reset now! ## error ## } ## } */ #ifdef CLEAR #undef CLEAR #endif #define CLEAR() \ bbstream_out(bbid, clearcode, codesize); \ codesize = initcodesize; \ tableremain = inittableremain; \ runindex = clearcode + 2 \ /*set runhead $runindex*/ theraster = 0; /* initial clear. */ #ifndef MAKE_XLI_BARF CLEAR(); #endif /* main pixels loop */ while (theraster < indexrasterlen) { /* find runlength */ int theindex; /* one pixel value of indexraster[] */ int r; /* runlength counter */ theindex = indexraster[theraster]; for (r=1; theraster+r < indexrasterlen && indexraster[theraster+r]==theindex; r++) ; theraster += r; /* * Clear code emission timing: * * - We always want the first pixel in the shortest code * if (codesize != initcodesize) * * - Also calculate if clear is better for current r, that is * if codelen expands beyond initcodesize during the run * {111...CSxxxx------------} * 23456789ABCD will-be-filled "table" runlength * * thus, maximum runlength accomodable by current tableremain * will be 1(initial) + 2+3+4+...+D * == 1+((3+tableremain)*tableremain)/2 * thus * if (r > 1+((3+tableremain)*tableremain)/2) * * Deployment result: * ... output shinks a bit, but doesn't gain much * compared to additional calculation for each cycle */ firstpixel: if (codesize != initcodesize || r > 1+((3+tableremain)*tableremain)/2) { CLEAR(); } /*## output the first pixel. {*/ bbstream_out(bbid, theindex, codesize); r--; runhead = runindex; /* mark "table" index for 2pixel */ if (0==tableremain) { /* runindex == (1< 12) { /* 12bit overflow; clear immediately */ codesize=12; CLEAR(); runhead = runindex; // continue; /* skip runindex update */ /* XXX: MUST goto generating a first pixel. */ goto firstpixel; } } tableremain--; runindex++; }/*wend r (runlength)*/ }/*wend indexrasterlen*/ bbstream_out(bbid, clearcode + 1/*end*/, codesize); bbstream_close(bbid); return 0; } /* bbstream.c {*/ /* ByteBlock Stream routines * * bbstream: {bytecount(1-254):8 { variable_length_bits_data ... }}[] 0:8 * * data bytes are LSB packed, thus (0123)(4567)(89ab) will be * 45670123 xxxx89ab */ bbstream_t * bbstream_open(FILE *outfd) { bbstream_t *bb = malloc(sizeof(bbstream_t)); bb->fd = outfd; bb->dangle = 0; bb->danglebits = 0; bb->buflen = 0; return bb; } int bbstream_out(bbstream_t *bb, int data, int bits) { /*dputs "out: $data:$bits"*/ /* stack the data onto MSB */ bb->dangle = (data<danglebits) | bb->dangle; bb->danglebits += bits; /* chop off dangle from LSB byte to buffer */ while (bb->danglebits >= 8) { bb->buf[bb->buflen++] = bb->dangle & 255; bb->danglebits -= 8; bb->dangle >>= 8; if (bb->buflen>260) {fprintf(stderr,"ERR\n"); exit(1);} } /* put it out in 254 bytes block */ while (bb->buflen >= 254) { fputc(254, bb->fd); fwrite(bb->buf, 1, 254, bb->fd); bb->buflen -= 254; memmove(bb->buf, bb->buf+254, bb->buflen); /* TODO: memmove can cost, so ring buffering may be better (low priority) */ } return 0; } void bbstream_close(bbstream_t *bb) { /* flush remaining dangle */ bb->buf[bb->buflen++] = bb->dangle; while (bb->buflen) { size_t thislen = (bb->buflen > 254) ? 254 : bb->buflen; fputc(thislen, bb->fd); fwrite(bb->buf, 1, thislen, bb->fd); bb->buflen -= thislen; memmove(bb->buf, bb->buf+thislen, bb->buflen); } fputc('\0', bb->fd); /* terminator count=0 */ free(bb); } /* bbstream.c }*/ /* return stdin for "-" */ FILE * fopen_(const char *filename, const char *mode) { if (filename[0]=='-' && filename[1]=='\0') { return stdin; } else { return fopen(filename, mode); } /* NOTREACHED */ } /* * Considerations for animated gif: * * - Always use local colormap, or try to collect global colormap? * - Render full frame, or output only different pixels from previous frame? * (needs holding full-color previous frame) * * TODO: * do something with implicit stdin logic flow * cleanup indent * local cmap switch ("animate" defaults to local, "+map" to aggregate) * * "animate" ImageMagick tool thinks the initial frame is the whole size * (not the size in the initial header); so it needs the biggest * image for initial frame... (Netscape properly renders it) * */ int anim_main(int argc, char *argv[], FILE *outfd) { int optind; int force_filename = 0; /* bool; 1 if "--" */ struct cmap_t _global_cmap; struct cmap_t *cmap = &_global_cmap; int cmap_given = 0; /* bool; 1 if -map */ int max_width = 0, max_height = 0; int filecount = 0; int pixbits; int i; char *transrgb = NULL; int compressmode = 'r'; int delay = 0; int add_loop_ext = 0; /*bool*/ int loops = 0; int gifheader_done = 0; /*bool*/ int pass; int peek_v = 0; /* G.verbose level floor */ cmap_init(cmap); /* sneak peek -v option */ if (!strcmp("-v", argv[1])) { peek_v++; for (i=1; i=1) fprintf(stderr,"=== PASS1: collect colormap and maximum size\n"); /*always supressed...*/ break; case 2: if (G.verbose>=1) { fprintf(stderr,"=== PASS2: output data\n"); fprintf(stderr, "max width x height = %d x %d\n", max_width, max_height); } break; }/*esac pass*/ filecount = 0; for (optind = 1; optind < argc; optind++) { FILE *ppmfile; int status; int width, height; gifPixel *raster = NULL; size_t raster_len; if (!force_filename && argv[optind][0]=='-' && argv[optind][1] != '\0') { /* -option */ char *opt = argv[optind]; if (!strcmp("-v", opt)) { G.verbose++; continue; } else if (!strcmp("+v", opt) || !strcmp("-q", opt)) { G.verbose--; continue; } else if (!strncmp("-map", opt, 2)) { optind++; if (pass == 1) { FILE *mapf; /* is there a need to read mapfile from stdin? */ mapf = fopen_(argv[optind], "rb"); if (mapf == NULL) { pm_message("Can't open file %s\n", argv[optind]); return 1; } if (G.verbose>=0) pm_message("Reading colormap sample from <%s>\n", (mapf==stdin)?"(stdin)":argv[optind]); ppm_readraster(mapf, NULL,NULL, &width,&height, cmap, '+'); if (mapf!=stdin) fclose(mapf); cmap_given = 1; }/*pass1*/ continue; }/*-map*/ else if (!strncmp("-loop", opt, 3)) { add_loop_ext = 1; loops = atoi(argv[++optind]); continue; } else if (!strncmp("-delay", opt, 2)) { delay = atoi(argv[++optind]); continue; } else if (!strncmp("-transparent", opt, 3)) { transrgb = argv[++optind]; continue; } else if (!strncmp("-rle", opt, 3) || !strncmp(opt, "-nolzw", 4) /*ppmtogif opt*/ ) { compressmode = 'r'; /*default RLE*/ continue; } else if (!strncmp("-raw", opt, 3) || !strncmp(opt, "-norle", 5)) { compressmode = 'n'; /*no compression*/ continue; } else if (!strcmp("--", opt)) { force_filename = 1; continue; } else { /* other options */ goto usage; continue; } usage: fprintf(stderr, "usage: %s [-v][-q] [-map colormap.ppm] [-transparent ] [-loop loopcount] [-delay delay-in-10msecs] [-norle] [ppmfile ...]\n", G.progname); return 1; }/* fi options */ force_filename = 0; /* now argv[optind] is filename */ /* non-animation, single-input shortcut; XXX messy */ if (filecount==0 && pass==1 && optind == argc-1 /* single filename */ ) { if (G.verbose>=1) fprintf(stderr,"=== single input, shortcut to PASS2\n"); pass = 2; } ppmfile = fopen_(argv[optind], "rb"); if (0) { read_stdin: /* TOTAL MESS: implicit stdin mode */ ppmfile = stdin; } if (ppmfile == NULL) { pm_message("Can't open file %s\n", argv[optind]); return 1; } if (G.verbose>=0) pm_message("<%s>\n", (ppmfile==stdin)?"(stdin)":argv[optind]); switch (pass) { case 1: /* collect size and (optionally) cmap */ status = ppm_readraster(ppmfile, NULL,NULL, &width, &height, cmap, cmap_given?'.':'+'); break; case 2: /* collect image */ status = ppm_readraster(ppmfile, &raster,&raster_len, &width, &height, cmap, cmap_given?'.':'+'); break; } if (status) return status; /*error*/ if (width > max_width) max_width = width; if (height > max_height) max_height = height; if (ppmfile != stdin) fclose(ppmfile); if (pass==2 && !gifheader_done) { /* delay GIF header output until here * (not at looptop of pass2) * to enable 1-pass shortcut on non-animated image */ /* get bitlength needed for this pixel values */ for (pixbits=1; (1<cmaplen; pixbits++) ; if (G.verbose>=1) pm_message("Needed Pixel bits: %d\n", pixbits); if (pixbits > 8) { fprintf(stderr, "Too many colors (%d bits)\n", pixbits); return 1; } /* output GIF header */ fprintf(outfd, "GIF89a"); /* comment extension needs 89a anyway */ /* logical screen descriptor */ fput_LEs(max_width, outfd); fput_LEs(max_height, outfd); i = (1 /*:1 Global colormap follows*/<<7) | (7 /*:3 bits per color intensity*/ <<4) | (0 /*:1 Global colormap is sorted*/ <<3) | (pixbits-1 /*:3 (bits/pixel)-1 */); fputc(i, outfd); fputc(0, outfd); /* background */ fputc(0, outfd); /* aspect ratio */ /* Global color map */ for (i=0; icmaplen; i++) { fputc(cmap->cmap[i].r, outfd); fputc(cmap->cmap[i].g, outfd); fputc(cmap->cmap[i].b, outfd); } /* pad colormap to 2**pixbits */ for ((void) i; i<(1<=1) if (i>=0) pm_message("Using index %d (%d %d %d) for transparency\n", i, cmap->cmap[i].r,cmap->cmap[i].g,cmap->cmap[i].b); /* Graphic Control Extention: transparency, delay */ gifout_GCE(outfd, i, delay); /* Image Descriptor */ fputc(',', outfd); fput_LEs(0, outfd); fput_LEs(0, outfd); /*left,top*/ fput_LEs(width, outfd); fput_LEs(height, outfd); i = (0 /*:1 0:use global colormap 1:has local colormap*/ <<7) | (0 /*:1 0:sequential 1:interlace*/ <<6) | (0 /*:1 sorted cmap*/ << 5) | (0 /*:2 reserved*/ <<3) | 0 /*3: (local cmap bits/pixel)-1*/; fputc(i, outfd); /* local cmap here if needed */ /* data stream */ switch(compressmode) { default: case 'r': gifraster_runlength_dc(outfd, raster,raster_len, pixbits); break; case 'n': gifraster_nocompress(outfd, raster,raster_len, pixbits); break; } }/*Graphic Block pass2*/ filecount++; if (raster) { free(raster); raster = NULL; } }/* wend optind */ /* argv exausted */ /* check for implicit stdin mode */ if (filecount == 0) { /* no explicit files; force use stdin */ if (G.verbose>=1) fprintf(stderr,"=== use stdin, redo as PASS2\n"); pass = 2; goto read_stdin; /* list-based languages could forcibly * insert "-" at the end of the argv and continue, * but this is C; logic is messy here */ } }/* wend pass */ /* include comment */ gifout_addprogsig(outfd); /* Image Trailer */ fputc(';', outfd); return 0; } int main(int argc, char *argv[]) { pm_init(&argc, argv); return anim_main(argc, argv, stdout); }