/* pngrewrite version 1.2.1 - 8 Feb 2003 A quick and dirty utility to reduce unnecessarily large palettes and bit depths in PNG image files. ** To compile in a unix-like environment, type something like: ** cc -o pngrewrite pngrewrite.c -lpng -lz -lm Web site: This program and source code may be used without restriction. Primary author: Jason Summers Contributing authors: Wayne Schlitt - Write grayscale images when possible - Ability to sort palette by color usage - Improved find_pal_entry performance Soren Anderson - Changes to allow use in a unix-style pipeline - Improved compatibility with MinGW and Cygwin - Maintain tIME chunks Pngrewrite will: * Remove any unused palette entries, and write a palette that is only as large as needed. * Remove (collapse) any duplicate palette entries. * Convert non-palette image to palette images, provided they contain no more than 256 different colors. * Write images as grayscale when possible. * Move any colors with transparency to the beginning of the palette, and write a tRNS chunk that is a small as possible. * Reduce the bit-depth (bits per pixel) as much as possible. All of this is just basic stuff that any respectable image writing program should always do automatically, but for some reason, many of them do not. Under no circumstances does pngrewrite change the actual pixel colors, or background color, or transparency of the image. If it ever does, that's a bug. --WARNING-- This version of pngrewrite removes most extra (ancillary) information from the PNG file, such as text comments. Although this does make the file size smaller, the removed information may sometimes be important. For that reason, you should only use pngrewrite on image that were created by you. The only ancillary chunks that are NOT removed are: gAMA - Image gamma setting sRGB - srgb color space indicator tIME - creation time pHYs - physical pixel size bKGD and tRNS - Background color and transparency are maintained. The actual chunk may be modified according to the new color structure. If the original image was interlaced, the new one will also be interlaced. Pngrewrite will not work at all on images that have more than 256 colors. Colors with the same RGB values but a different level of transparency count as different colors. The background color counts as an extra color if it does not occur in the image. It will also not work at all on images that have a color depth of 16 bits, since they cannot have a palette. This is a very inefficient program. It is (relatively) slow, and uses tons of memory. To be specific, it uses about 5 bytes per pixel, no matter what the bit depth of the image is. This program is (hopefully) reasonably portable, and should compile without too much effort on most ANSI C compilers. It requires the libpng and zlib libraries. */ #define PNGREWRITEVERSION "1.2.1" #define PALSORT_BY_FREQUENCY #include #include #include /* #include */ #if defined( WIN32 ) && !defined (__GNUC__) #include /* for qsort */ #include #include #endif #if defined( WIN32 ) && defined (__GNUC__) # include #endif #ifndef WIN32 #include /* for isatty() */ #endif /* #define PNG_DEBUG 1 */ #include "png.h" typedef struct { unsigned char red; unsigned char green; unsigned char blue; unsigned char alpha; unsigned int count; } tmppal_t; tmppal_t tmppal[256]; int pal_used; int new_bit_depth; int valid_gray; unsigned char *image1, *image2; unsigned char **row_pointers; int rowbytes, channels; png_uint_32 width, height; int bit_depth, color_type, interlace_type; png_time savechunk_time; /* S.A. */ /* S.A. - 30 bytes extra "garbage" safety zone, should use only 26 + \0 = 27 */ char time_as_string[57]; int ori_pal_size; double image_gamma; tmppal_t bkgd; /* RGB background color */ int gray_trns; unsigned char bkgd_pal; /* new background color palette entry */ int srgb_intent; png_uint_32 res_x,res_y; int res_unit_type; struct chunk_flags { unsigned int has_gama : 1; unsigned int has_bkgd : 1; unsigned int has_srgb : 1; unsigned int need_tIME_after_IDATs : 1; unsigned int has_phys : 1; }; /* initialize chunk-handling bitflags in read-png() */ struct chunk_flags chnks; #define PNG_READ_SIG_BYTES 7 int read_png(char *file_name) { png_structp png_ptr; png_infop info_ptr; png_colorp ori_pal; int ori_bpp; unsigned char header[PNG_READ_SIG_BYTES]; int is_png=0; #ifdef WIN32 int setmode_retval; #endif FILE *fp; int i; png_color_16 *image_background; png_timep in_time; /* a struct POINTER */ extern png_time savechunk_time; /* NOT a pointer! S.A. */ if(file_name != NULL) { if( (fp = fopen(file_name, "rb")) == NULL ) { fprintf(stderr, "Can't open file %s\n",file_name); return 0; } } else { fp = stdin; #if WIN32 setmode_retval=setmode( fileno(fp), O_BINARY); #endif } fread(header, 1, (int)PNG_READ_SIG_BYTES, fp); is_png = !png_sig_cmp(header, 0, PNG_READ_SIG_BYTES); if(!is_png) { fprintf(stderr, "Not a PNG file: initial %d bytes are \"%s\"", PNG_READ_SIG_BYTES, header); return 0; } chnks.has_gama=0; chnks.has_bkgd=0; chnks.has_srgb=0; chnks.need_tIME_after_IDATs=0; chnks.has_phys=0; png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if(png_ptr == NULL) { fprintf(stderr, "Error creating read_struct\n"); if(file_name) fclose(fp); return 0; } png_set_sig_bytes(png_ptr, PNG_READ_SIG_BYTES); info_ptr = png_create_info_struct(png_ptr); png_debug(0, "Creating png info struct"); if (info_ptr == NULL) { if(file_name) fclose(fp); png_destroy_read_struct(&png_ptr, (png_infopp)NULL, (png_infopp)NULL); return 0; } png_debug(0, "Setting default png read function"); png_set_read_fn(png_ptr, fp, NULL); if (setjmp(png_ptr->jmpbuf)) { png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL); if(file_name) fclose(fp); fprintf(stderr, "Error reading file on descriptor '%i'\n", fileno(fp)); return 0; } png_debug(0, "Starting png_init_io"); png_init_io(png_ptr, fp); png_read_info(png_ptr, info_ptr); png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, &interlace_type, NULL, NULL); if(bit_depth>8) { fprintf(stderr, "This image can't be converted because it has 16 bits/sample.\n"); goto abort1; } if (color_type == PNG_COLOR_TYPE_PALETTE) { ori_pal_size=0; ori_bpp = bit_depth; /* we only do this to get the palette size so we can print it */ png_get_PLTE(png_ptr,info_ptr,&ori_pal,&ori_pal_size); fprintf(stderr, "original palette size: %3d, %2d bpp\n",ori_pal_size,ori_bpp); png_set_expand(png_ptr); } else { /* figure out bits per pixel so we can print it */ ori_bpp= bit_depth*png_get_channels(png_ptr,info_ptr); fprintf(stderr, "original palette size: [n/a], %2d bpp\n",ori_bpp); } if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) png_set_expand(png_ptr); if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) png_set_expand(png_ptr); /* if (bit_depth == 16) png_set_strip_16(png_ptr); */ if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) png_set_gray_to_rgb(png_ptr); if (png_get_bKGD(png_ptr, info_ptr, &image_background)) { chnks.has_bkgd=1; bkgd.red= (unsigned char)image_background->red; bkgd.green= (unsigned char)image_background->green; bkgd.blue= (unsigned char)image_background->blue; bkgd.alpha=255; } if (png_get_gAMA(png_ptr, info_ptr, &image_gamma)) { chnks.has_gama=1; } if (png_get_sRGB(png_ptr, info_ptr, &srgb_intent)) { chnks.has_srgb=1; } if(png_get_valid(png_ptr,info_ptr,PNG_INFO_pHYs)) { chnks.has_phys=1; png_get_pHYs(png_ptr,info_ptr,&res_x,&res_y,&res_unit_type); if(res_x<1 || res_y<1) chnks.has_phys=0; } /* S.A. .............................. */ if(png_get_valid(png_ptr, info_ptr, PNG_INFO_tIME)) { if(png_get_tIME(png_ptr, info_ptr, &in_time) == PNG_INFO_tIME) { sprintf(time_as_string, "%s", png_convert_to_rfc1123(png_ptr, in_time)); savechunk_time = *in_time; } } else { chnks.need_tIME_after_IDATs=1; } png_read_update_info(png_ptr, info_ptr); rowbytes=png_get_rowbytes(png_ptr, info_ptr); channels=(int)png_get_channels(png_ptr, info_ptr); if(channels<3 || channels>4) { fprintf(stderr, "internal error: channels=%d\n",channels); goto abort1; } fprintf(stderr,"Image size is %ldx%ld memory required=%.3fMB\n", width, height, (height*rowbytes + height*sizeof(unsigned char*) + width*height) / (1024. * 1024.) ); image1= (unsigned char*)malloc(height*rowbytes); row_pointers = (unsigned char**)malloc(height * sizeof(unsigned char*)); if(!image1 || !row_pointers) { fprintf(stderr, "Unable to allocate memory for image\n"); goto abort1; } for(i=0;i<(int)height;i++) { row_pointers[i] = &image1[rowbytes*i]; } png_read_image(png_ptr, row_pointers); png_read_end(png_ptr, info_ptr); /* S.A. ................................. */ if(chnks.need_tIME_after_IDATs && png_get_valid(png_ptr,info_ptr,PNG_INFO_tIME)) { if(png_get_tIME(png_ptr, info_ptr, &in_time) == PNG_INFO_tIME) { sprintf(time_as_string, "%s", png_convert_to_rfc1123(png_ptr, in_time)); savechunk_time = *in_time; } } png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL); if(file_name) fclose(fp); return 1; abort1: png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL); if(file_name) fclose(fp); return 0; } int write_new_png(char *fn) { png_structp png_ptr; png_infop info_ptr; png_color palette[256]; unsigned char trans[256]; FILE *fp; int num_trans; int i; png_color_16 newtrns; png_color_16 newbackground; memset(&newtrns ,0,sizeof(png_color_16)); memset(&newbackground,0,sizeof(png_color_16)); png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!png_ptr) { fprintf(stderr, "out of memory\n"); return 0; } if(fn) { fp=fopen(fn,"wb"); if(!fp) { fprintf(stderr, "can't write to file %s\n",fn); png_destroy_write_struct(&png_ptr, NULL); return 0; } } else { /* write to stdout */ fp = stdout; #ifdef WIN32 setmode( fileno(fp), O_BINARY); #endif } info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { fprintf(stderr, "can't create info struct\n"); goto abort1; } if (setjmp(png_ptr->jmpbuf)) { fprintf(stderr, "Error writing file\n"); goto abort1; } png_init_io(png_ptr, fp); if( valid_gray ) { png_set_IHDR(png_ptr, info_ptr, width, height, new_bit_depth, PNG_COLOR_TYPE_GRAY, interlace_type, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); if(gray_trns>=0) { newtrns.gray = gray_trns; png_set_tRNS(png_ptr, info_ptr, NULL, 1, &newtrns); } } else { png_set_IHDR(png_ptr, info_ptr, width, height, new_bit_depth, PNG_COLOR_TYPE_PALETTE, interlace_type, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); /* ... set palette colors ... */ num_trans=0; /* number of transparent palette entries */ for(i=0;i0) { png_set_tRNS(png_ptr, info_ptr, trans, num_trans, 0); } } if(chnks.has_gama) png_set_gAMA(png_ptr, info_ptr, image_gamma); if(chnks.has_srgb) png_set_sRGB(png_ptr, info_ptr, srgb_intent); if(chnks.has_phys) png_set_pHYs(png_ptr, info_ptr, res_x, res_y, res_unit_type); if(chnks.has_bkgd) { if(valid_gray) newbackground.gray = bkgd_pal; else newbackground.index = bkgd_pal; png_set_bKGD(png_ptr, info_ptr, &newbackground); } png_write_info(png_ptr, info_ptr); png_set_packing(png_ptr); /* re-use row_pointers array */ for(i=0;i<(int)height;i++) { row_pointers[i]= &image2[i*width]; } png_write_image(png_ptr, row_pointers); /* S.A. ............................ */ if(savechunk_time.year) { //fprintf(stderr, "inputchunk tIME copied: %s\n", time_as_string); png_set_tIME(png_ptr, info_ptr, &savechunk_time); } png_write_end(png_ptr, info_ptr); png_destroy_write_struct(&png_ptr, (png_infopp)NULL); if(fn) fclose(fp); free(row_pointers); free(image2); return 1; abort1: png_destroy_write_struct(&png_ptr, NULL); if(fn) fclose(fp); return 0; } /* set ignorealpha to 1 if you don't care if the alpha value matches * (for the background) */ int add_to_palette(tmppal_t *pal, tmppal_t *color, int *pal_used, int ignorealpha) { int used; used=(*pal_used); if(used>=256) { fprintf(stderr, "This image can't be converted because it has more than 256 colors.\n"); return 0; } pal[used].red=color->red; pal[used].green=color->green; pal[used].blue=color->blue; pal[used].alpha= ignorealpha?255:color->alpha; pal[used].count = 1; (*pal_used)++; return 1; } /* void debug_print_pal(tmppal_t *pal, int used) { int i; for(i=0;ialpha==255 && e2->alpha<255) return 1; if(e1->alpha<255 && e2->alpha==255) return -1; #ifdef PALSORT_BY_FREQUENCY if(e1->countcount) return 1; if(e1->count>e2->count) return -1; #endif s1=e1->red+e1->green+e1->blue; s2=e2->red+e2->green+e2->blue; if(s1>s2) return 1; if(s1= 2 ) continue; break; case 17: case 34: case 51: case 68: case 102: case 119: case 136: case 153: case 187: case 204: case 221: case 238: if( new_bit_depth >= 4 ) continue; break; default: if( new_bit_depth >= 8 ) continue; //break; } valid_gray = 0; break; } // One thing the above doesn't check for is a nontransparent // grayscale that's the same as the transparent grayscale color. // In this case we have to use palette color. if(valid_gray && gray_trns_palentry != -1) { if(-1 != find_pal_entry(gray_trns_shade,gray_trns_shade,gray_trns_shade,255,0,0)) { valid_gray = 0; } } /* put the palette in a good order */ if(valid_gray) { // If grayscale, create a "fake" palette consisting of all // available gray shades. switch(new_bit_depth) { case 8: pal_used=256; for(i=0;i