1 /* Copyright (C) 2002 Matthias S. Benkmann <matthias@winterdrache.de>
2 
3 This program is free software; you can redistribute it and/or
4 modify it under the terms of the GNU General Public License
5 as published by the Free Software Foundation; version 2
6 of the License (ONLY THIS VERSION).
7 
8 This program is distributed in the hope that it will be useful,
9 but WITHOUT ANY WARRANTY; without even the implied warranty of
10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 GNU General Public License for more details.
12 
13 You should have received a copy of the GNU General Public License
14 along with this program; if not, write to the Free Software
15 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
16 */
17 
18 /*
19 Notes about transparent and inverted pixels:
20  Handling of transparent pixels is inconsistent in Windows. Sometimes a
21  pixel with an AND mask value of 1 is just transparent (i.e. its color
22  value is ignored), sometimes the color value is XORed with the background to
23  give some kind of inverted effect. A closer look at bmp.txt suggests that
24  the latter behaviour is the correct one but because it often doesn't happen
25  it's de facto undefined behaviour.
26  Furthermore, sometimes the AND mask entry seems to be interpreted as a
27  color index, i.e. a value of 1 will AND the background with color 1.
28  Conclusion: The most robust solution seems to be:
29                -color 0 always 0,0,0
30                -color 1 always 255,255,255
31                -all transparent pixels get color 0
32 */
33 
34 
35 #include <cstdio>
36 #include <cstring>
37 #include <cstdlib>
38 #include <vector>
39 #include <climits>
40 
41 #if __GNUC__ > 2
42 #include <ext/hash_map>
43 #else
44 #include <hash_map>
45 #endif
46 
47 #include <png.h>
48 
49 #include "VERSION"
50 
51 using namespace std;
52 namespace __gnu_cxx{};
53 using namespace __gnu_cxx;
54 
55 const int word_max=65535;
56 const int transparency_threshold=196;
57 const int color_reduce_warning_threshold=512; //maximum quadratic euclidean distance in RGB color space that a palette color may have to a source color assigned to it before a warning is issued
58 const unsigned int slow_reduction_warn_threshold=262144; //number of colors in source image times number of colors in target image that triggers the warning that the reduction may take a while
59 
writeWord(FILE * f,int word)60 void writeWord(FILE* f, int word)
61 {
62   char data[2];
63   data[0]=(word&255);
64   data[1]=(word>>8)&255;
65   if (fwrite(data,2,1,f)!=1) {perror("Write error"); exit(1);};
66 };
67 
writeDWord(FILE * f,unsigned int dword)68 void writeDWord(FILE* f, unsigned int dword)
69 {
70   char data[4];
71   data[0]=(dword&255);
72   data[1]=(dword>>8)&255;
73   data[2]=(dword>>16)&255;
74   data[3]=(dword>>24)&255;
75   if (fwrite(data,4,1,f)!=1) {perror("Write error"); exit(1);};
76 };
77 
writeByte(FILE * f,int byte)78 void writeByte(FILE* f, int byte)
79 {
80   char data[1];
81   data[0]=(byte&255);
82   if (fwrite(data,1,1,f)!=1) {perror("Write error"); exit(1);};
83 };
84 
85 
86 
87 struct png_data
88 {
89   png_structp png_ptr;
90   png_infop info_ptr;
91   png_infop end_info;
92   png_uint_32 width, height;
93   png_colorp palette;
94   png_bytepp transMap;
95   int num_palette;
96   int requested_colors;
97   int col_bits;
png_datapng_data98   png_data():png_ptr(NULL),info_ptr(NULL),end_info(NULL),width(0),height(0),
99              palette(NULL),transMap(NULL),num_palette(0),requested_colors(0),col_bits(0){};
100 };
101 
andMaskLineLen(const png_data & img)102 int andMaskLineLen(const png_data& img)
103 {
104   int len=(img.width+7)>>3;
105   return (len+3)&~3;
106 };
107 
xorMaskLineLen(const png_data & img)108 int xorMaskLineLen(const png_data& img)
109 {
110   int pixelsPerByte=(8/img.col_bits);
111   return ((img.width+pixelsPerByte-1)/pixelsPerByte+3)&~3;
112 };
113 
114 typedef bool (*checkTransparent_t)(png_bytep, png_data&);
115 
checkTransparent1(png_bytep data,png_data &)116 bool checkTransparent1(png_bytep data, png_data&)
117 {
118   return (data[3]<transparency_threshold);
119 };
120 
checkTransparent3(png_bytep,png_data &)121 bool checkTransparent3(png_bytep, png_data&)
122 {
123   return false;
124 };
125 
126 //returns true if color reduction resulted in at least one of the image's colors
127 //being mapped to a palette color with a quadratic distance of more than
128 //color_reduce_warning_threshold
convertToIndexed(png_data & img,bool hasAlpha)129 bool convertToIndexed(png_data& img, bool hasAlpha)
130 {
131   int maxColors=img.requested_colors;
132 
133   size_t palSize=sizeof(png_color)*256; //must reserve space for 256 entries here because write loop in main() expects it
134   img.palette=(png_colorp)malloc(palSize);
135   memset(img.palette,0,palSize); //must initialize whole palette because write loop in main() expects it
136   img.num_palette=0;
137 
138   checkTransparent_t checkTrans=checkTransparent1;
139   int bytesPerPixel=4;
140   if (!hasAlpha)
141   {
142     bytesPerPixel=3;
143     checkTrans=checkTransparent3;
144   };
145 
146   //first pass: gather all colors, make sure
147   //alpha channel (if present) contains only 0 and 255
148   //if an alpha channel is present, set all transparent pixels to RGBA (0,0,0,0)
149   //transparent pixels will already be mapped to palette entry 0, non-transparent
150   //pixels will not get a mapping yet (-1)
151   hash_map<unsigned int,signed int> mapQuadToPalEntry;
152   png_bytep* row_pointers=png_get_rows(img.png_ptr, img.info_ptr);
153 
154   for (int y=img.height-1; y>=0; --y)
155   {
156     png_bytep pixel=row_pointers[y];
157     for (unsigned i=0; i<img.width; ++i)
158     {
159       unsigned int quad=pixel[0]+(pixel[1]<<8)+(pixel[2]<<16);
160       bool trans=(*checkTrans)(pixel,img);
161 
162       if (hasAlpha)
163       {
164         if (trans)
165         {
166           pixel[0]=0;
167           pixel[1]=0;
168           pixel[2]=0;
169           pixel[3]=0;
170           quad=0;
171         }
172         else pixel[3]=255;
173 
174         quad+=(pixel[3]<<24);
175       }
176       else if (!trans) quad+=(255<<24);
177 
178       if (trans)
179         mapQuadToPalEntry[quad]=0;
180       else
181         mapQuadToPalEntry[quad]=-1;
182 
183       pixel+=bytesPerPixel;
184     };
185   };
186 
187   //always allocate entry 0 to black and entry 1 to white because
188   //sometimes AND mask is interpreted as color index
189   img.num_palette=2;
190   img.palette[0].red=0;
191   img.palette[0].green=0;
192   img.palette[0].blue=0;
193   img.palette[1].red=255;
194   img.palette[1].green=255;
195   img.palette[1].blue=255;
196 
197   mapQuadToPalEntry[255<<24]=0; //map (non-transparent) black to entry 0
198   mapQuadToPalEntry[255+(255<<8)+(255<<16)+(255<<24)]=1; //map (non-transparent) white to entry 1
199 
200   if (mapQuadToPalEntry.size()*img.requested_colors>slow_reduction_warn_threshold)
201   {
202     fprintf(stdout,"Please be patient. My color reduction algorithm is really slow.\n");
203   };
204 
205   //Now fill up the palette with colors from the image by repeatedly picking the
206   //color most different from the previously picked colors and adding this to the
207   //palette. This is done to make sure that in case there are more image colors than
208   //palette entries, palette entries are not wasted on similar colors.
209   while(img.num_palette<maxColors)
210   {
211     unsigned int mostDifferentQuad=0;
212     int mdqMinDist=-1; //smallest distance to an entry in the palette for mostDifferentQuad
213     int mdqDistSum=-1; //sum over all distances to palette entries for mostDifferentQuad
214     hash_map<unsigned int,signed int>::iterator stop=mapQuadToPalEntry.end();
215     hash_map<unsigned int,signed int>::iterator iter=mapQuadToPalEntry.begin();
216     while(iter!=stop)
217     {
218       hash_map<unsigned int,signed int>::value_type& mapping=*iter++;
219       if (mapping.second<0)
220       {
221         unsigned int quad=mapping.first;
222         int red=quad&255;  //must be signed
223         int green=(quad>>8)&255;
224         int blue=(quad>>16)&255;
225         int distSum=0;
226         int minDist=INT_MAX;
227         for (int i=0; i<img.num_palette; ++i)
228         {
229           int dist=(red-img.palette[i].red);
230           dist*=dist;
231           int temp=(green-img.palette[i].green);
232           dist+=temp*temp;
233           temp=(blue-img.palette[i].blue);
234           dist+=temp*temp;
235           if (dist<minDist) minDist=dist;
236           distSum+=dist;
237         };
238 
239         if (minDist>mdqMinDist || (minDist==mdqMinDist && distSum>mdqDistSum))
240         {
241           mostDifferentQuad=quad;
242           mdqMinDist=minDist;
243           mdqDistSum=distSum;
244         };
245       };
246     };
247 
248     if (mdqMinDist>0) //if we have found a most different quad, add it to the palette
249     {                  //and map it to the new palette entry
250       int palentry=img.num_palette;
251       img.palette[palentry].red=mostDifferentQuad&255;
252       img.palette[palentry].green=(mostDifferentQuad>>8)&255;
253       img.palette[palentry].blue=(mostDifferentQuad>>16)&255;
254       mapQuadToPalEntry[mostDifferentQuad]=palentry;
255       ++img.num_palette;
256     }
257     else break; //otherwise (i.e. all quads are mapped) the palette is finished
258   };
259 
260   //Now map all yet unmapped colors to the most appropriate palette entry
261   hash_map<unsigned int,signed int>::iterator stop=mapQuadToPalEntry.end();
262   hash_map<unsigned int,signed int>::iterator iter=mapQuadToPalEntry.begin();
263   while(iter!=stop)
264   {
265     hash_map<unsigned int,signed int>::value_type& mapping=*iter++;
266     if (mapping.second<0)
267     {
268       unsigned int quad=mapping.first;
269       int red=quad&255;  //must be signed
270       int green=(quad>>8)&255;
271       int blue=(quad>>16)&255;
272       int minDist=INT_MAX;
273       int bestIndex=0;
274       for (int i=0; i<img.num_palette; ++i)
275       {
276         int dist=(red-img.palette[i].red);
277         dist*=dist;
278         int temp=(green-img.palette[i].green);
279         dist+=temp*temp;
280         temp=(blue-img.palette[i].blue);
281         dist+=temp*temp;
282         if (dist<minDist) { minDist=dist; bestIndex=i; };
283       };
284 
285       mapping.second=bestIndex;
286     };
287   };
288 
289   //Adjust all palette entries (except for 0 and 1) to be the mean of all
290   //colors mapped to it
291   for (int i=2; i<img.num_palette; ++i)
292   {
293     int red=0;
294     int green=0;
295     int blue=0;
296     int numMappings=0;
297     hash_map<unsigned int,signed int>::iterator stop=mapQuadToPalEntry.end();
298     hash_map<unsigned int,signed int>::iterator iter=mapQuadToPalEntry.begin();
299     while(iter!=stop)
300     {
301       hash_map<unsigned int,signed int>::value_type& mapping=*iter++;
302       if (mapping.second==i)
303       {
304         unsigned int quad=mapping.first;
305         red+=quad&255;
306         green+=(quad>>8)&255;
307         blue+=(quad>>16)&255;
308         ++numMappings;
309       };
310     };
311 
312     if (numMappings>0)
313     {
314       img.palette[i].red=(red+red+numMappings)/(numMappings+numMappings);
315       img.palette[i].green=(green+green+numMappings)/(numMappings+numMappings);
316       img.palette[i].blue=(blue+blue+numMappings)/(numMappings+numMappings);
317     };
318   };
319 
320   //Now determine if a non-transparent source color got mapped to a target color that
321   //has a distance that exceeds the threshold
322   bool tooManyColors=false;
323   stop=mapQuadToPalEntry.end();
324   iter=mapQuadToPalEntry.begin();
325   while(iter!=stop)
326   {
327     hash_map<unsigned int,signed int>::value_type& mapping=*iter++;
328     unsigned int quad=mapping.first;
329     if ((quad>>24)!=0) //if color is not transparent
330     {
331       int red=quad&255;
332       int green=(quad>>8)&255;
333       int blue=(quad>>16)&255;
334       int i=mapping.second;
335       int dist=(red-img.palette[i].red);
336       dist*=dist;
337       int temp=(green-img.palette[i].green);
338       dist+=temp*temp;
339       temp=(blue-img.palette[i].blue);
340       dist+=temp*temp;
341       if (dist>color_reduce_warning_threshold) tooManyColors=true;
342     };
343   };
344 
345 
346   int transLineLen=andMaskLineLen(img);
347   int transLinePad=transLineLen - ((img.width+7)/8);
348   img.transMap=(png_bytepp)malloc(img.height*sizeof(png_bytep));
349 
350   //second pass: convert RGB to palette entries
351   for (int y=img.height-1; y>=0; --y)
352   {
353     png_bytep row=row_pointers[y];
354     png_bytep pixel=row;
355     int count8=0;
356     int transbyte=0;
357     png_bytep transPtr=img.transMap[y]=(png_bytep)malloc(transLineLen);
358 
359     for (unsigned i=0; i<img.width; ++i)
360     {
361       bool trans=((*checkTrans)(pixel,img));
362       unsigned int quad=pixel[0]+(pixel[1]<<8)+(pixel[2]<<16);
363       if (!trans) quad+=(255<<24); //NOTE: alpha channel has already been set to 255 for non-transparent pixels, so this is correct even for images with alpha channel
364 
365       if (trans) ++transbyte;
366       if (++count8==8)
367       {
368         *transPtr++ = transbyte;
369         count8=0;
370         transbyte=0;
371       };
372       transbyte+=transbyte; //shift left 1
373 
374       int palentry=mapQuadToPalEntry[quad];
375       row[i]=palentry;
376       pixel+=bytesPerPixel;
377     };
378 
379     for(int i=0; i<transLinePad; ++i) *transPtr++ = 0;
380   };
381 
382   return tooManyColors;
383 };
384 
385 //packs a line of width pixels (1 byte per pixel) in row, with 8/nbits pixels packed
386 //into each byte
387 //returns the new number of bytes in row
pack(png_bytep row,int width,int nbits)388 int pack(png_bytep row,int width,int nbits)
389 {
390   int pixelsPerByte=8/nbits;
391   if (pixelsPerByte<=1) return width;
392   int ander=(1<<nbits)-1;
393   int outByte=0;
394   int count=0;
395   int outIndex=0;
396   for (int i=0; i<width; ++i)
397   {
398     outByte+=(row[i]&ander);
399     if (++count==pixelsPerByte)
400     {
401       row[outIndex]=outByte;
402       count=0;
403       ++outIndex;
404       outByte=0;
405     };
406     outByte<<=nbits;
407   };
408 
409   if (count>0)
410   {
411     outByte<<=nbits*(pixelsPerByte-count);
412     row[outIndex]=outByte;
413     ++outIndex;
414   };
415 
416   return outIndex;
417 };
418 
419 
usage()420 void usage()
421 {
422   fprintf(stderr,version"\n");
423   fprintf(stderr,"USAGE: png2ico icofile [--colors <num>] pngfile1 [pngfile2 ...]\n");
424   exit(1);
425 };
426 
main(int argc,char * argv[])427 int main(int argc, char* argv[])
428 {
429   if (argc<3) usage();
430 
431   if (argc-2 > word_max)
432   {
433     fprintf(stderr,"Too many PNG files\n");
434     exit(1);
435   };
436 
437   vector<png_data> pngdata;
438 
439   static int numColors=256; //static to get rid of longjmp() clobber warning
440   static const char* outfileName=NULL;
441 
442   //i is static because used in a setjmp() block
443   for (static int i=1; i<argc; ++i)
444   {
445     if (strcmp(argv[i],"--colors")==0)
446     {
447       ++i;
448       if (i>=argc)
449       {
450         fprintf(stderr,"Number missing after --colors\n");
451         exit(1);
452       };
453       char* endptr;
454       long num=strtol(argv[i],&endptr,10);
455       if (*(argv[i])==0 || *endptr!=0 || (num!=2 && num!=16 && num!=256))
456       {
457         fprintf(stderr,"Illegal number of colors\n");
458         exit(1);
459       };
460       numColors=num;
461       continue;
462     };
463 
464     if (outfileName==NULL) { outfileName=argv[i]; continue; };
465 
466     FILE* pngfile=fopen(argv[i],"rb");
467     if (pngfile==NULL)  {perror(argv[i]); exit(1);};
468     png_byte header[8];
469     if (fread(header,8,1,pngfile)!=1) {perror(argv[i]); exit(1);};
470     if (png_sig_cmp(header,0,8))
471     {
472       fprintf(stderr,"%s: Not a PNG file\n",argv[i]);
473       exit(1);
474     };
475 
476     png_data data;
477     data.requested_colors=numColors;
478     for (data.col_bits=1; (1<<data.col_bits)<numColors; ++data.col_bits);
479 
480     data.png_ptr=png_create_read_struct
481                    (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
482     if (!data.png_ptr)
483     {
484       fprintf(stderr,"png_create_read_struct error\n");
485       exit(1);
486     };
487 
488     data.info_ptr=png_create_info_struct(data.png_ptr);
489     if (!data.info_ptr)
490     {
491       png_destroy_read_struct(&data.png_ptr, (png_infopp)NULL, (png_infopp)NULL);
492       fprintf(stderr,"png_create_info_struct error\n");
493       exit(1);
494     };
495 
496     data.end_info=png_create_info_struct(data.png_ptr);
497     if (!data.end_info)
498     {
499       png_destroy_read_struct(&data.png_ptr, &data.info_ptr, (png_infopp)NULL);
500       fprintf(stderr,"png_create_info_struct error\n");
501       exit(1);
502     };
503 
504     if (setjmp(png_jmpbuf(data.png_ptr)))
505     {
506       png_destroy_read_struct(&data.png_ptr, &data.info_ptr, &data.end_info);
507       fprintf(stderr,"%s: PNG error\n",argv[i]);
508       exit(1);
509     };
510 
511     png_init_io(data.png_ptr, pngfile);
512     png_set_sig_bytes(data.png_ptr,8);
513     int trafo=PNG_TRANSFORM_PACKING|PNG_TRANSFORM_STRIP_16|PNG_TRANSFORM_EXPAND;
514     png_read_png(data.png_ptr, data.info_ptr, trafo , NULL);
515 
516     int bit_depth, color_type, interlace_type, compression_type, filter_method;
517     png_get_IHDR(data.png_ptr, data.info_ptr, &data.width, &data.height, &bit_depth, &color_type,
518                  &interlace_type, &compression_type, &filter_method);
519 
520 
521 
522     if ( (data.width&7)!=0 || data.width>=256 || data.height>=256)
523     {
524       //I don't know if the following is really a requirement (bmp.txt says that
525       //only 16x16, 32x32 and 64x64 are allowed but that doesn't seem right) but
526       //if the width is not a multiple of 8, then the loop creating the and mask later
527       //doesn't work properly because it doesn't shift in padding bits
528       fprintf(stderr,"%s: Width must be multiple of 8 and <256. Height must be <256.\n",argv[i]);
529       exit(1);
530     };
531 
532     if ((color_type & PNG_COLOR_MASK_COLOR)==0)
533     {
534       fprintf(stderr,"%s: Grayscale image not supported\n",argv[i]);
535       exit(1);
536     };
537 
538     if (color_type==PNG_COLOR_TYPE_PALETTE)
539     {
540       fprintf(stderr,"This can't happen. PNG_TRANSFORM_EXPAND transforms image to RGB.\n");
541       exit(1);
542     }
543     else
544     {
545       if (convertToIndexed(data, ((color_type & PNG_COLOR_MASK_ALPHA)!=0)))
546       {
547         fprintf(stderr,"%s: Warning! Color reduction may not be optimal!\nIf the result is not satisfactory, reduce the number of colors\nbefore using png2ico.\n",argv[i]);
548       };
549     };
550 
551     pngdata.push_back(data);
552 
553     fclose(pngfile);
554   };
555 
556 
557   if (outfileName==NULL || pngdata.size()<1) usage();
558 
559   FILE* outfile=fopen(outfileName,"wb");
560   if (outfile==NULL) {perror(argv[1]); exit(1);};
561 
562   writeWord(outfile,0); //idReserved
563   writeWord(outfile,1); //idType
564   writeWord(outfile,pngdata.size()); //idCount
565 
566   int offset=6+pngdata.size()*16;
567 
568   vector<png_data>::const_iterator img;
569   for(img=pngdata.begin(); img!=pngdata.end(); ++img)
570   {
571     writeByte(outfile,img->width); //bWidth
572     writeByte(outfile,img->height); //bHeight
573     writeByte(outfile,img->requested_colors&255); //bColorCount
574     writeByte(outfile,0); //bReserved
575     writeWord(outfile,0); //wPlanes
576     writeWord(outfile,0); //wBitCount
577     int resSize=40+img->requested_colors*4+(andMaskLineLen(*img)+xorMaskLineLen(*img))*img->height;
578     writeDWord(outfile,resSize); //dwBytesInRes
579     writeDWord(outfile,offset); //dwImageOffset
580     offset+=resSize;
581   };
582 
583 
584   for(img=pngdata.begin(); img!=pngdata.end(); ++img)
585   {
586     writeDWord(outfile,40); //biSize
587     writeDWord(outfile,img->width); //biWidth
588     writeDWord(outfile,2*img->height); //biHeight (2 times because the 2 masks are counted)
589     writeWord(outfile,1);   //biPlanes
590     writeWord(outfile,img->col_bits);   //biBitCount
591     writeDWord(outfile,0);  //biCompression
592     writeDWord(outfile,(andMaskLineLen(*img)+xorMaskLineLen(*img))*img->height);  //biSizeImage
593     writeDWord(outfile,0);  //biXPelsPerMeter
594     writeDWord(outfile,0);  //biYPelsPerMeter
595     writeDWord(outfile,0); //biClrUsed (MUST BE 0 ACCORDING TO bmp.txt!!! I tried putting the real number here, but this breaks icons in some places)
596     writeDWord(outfile,0);   //biClrImportant
597     for (int i=0; i<img->requested_colors; ++i)
598     {
599       char col[4];
600       col[0]=img->palette[i].blue;
601       col[1]=img->palette[i].green;
602       col[2]=img->palette[i].red;
603       col[3]=0;
604       if (fwrite(col,4,1,outfile)!=1) {perror("Write error"); exit(1);};
605     };
606 
607     png_bytep* row_pointers=png_get_rows(img->png_ptr, img->info_ptr);
608     for (int y=img->height-1; y>=0; --y)
609     {
610       png_bytep row=row_pointers[y];
611       int newLength=pack(row,img->width,img->col_bits);
612       if (fwrite(row,newLength,1,outfile)!=1) {perror("Write error"); exit(1);};
613       for(int i=0; i<xorMaskLineLen(*img)-newLength; ++i) writeByte(outfile,0);
614     };
615 
616     for (int y=img->height-1; y>=0; --y)
617     {
618       png_bytep transPtr=img->transMap[y];
619       if (fwrite(transPtr,andMaskLineLen(*img),1,outfile)!=1) {perror("Write error"); exit(1);};
620     };
621   };
622 
623   fclose(outfile);
624 };
625 
626 
627