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