icon.pl 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. #!/usr/bin/perl
  2. # Take a collection of input image files and convert them into a
  3. # multi-resolution Windows .ICO icon file.
  4. #
  5. # The input images can be treated as having four different colour
  6. # depths:
  7. #
  8. # - 24-bit true colour
  9. # - 8-bit with custom palette
  10. # - 4-bit using the Windows 16-colour palette (see comment below
  11. # for details)
  12. # - 1-bit using black and white only.
  13. #
  14. # The images can be supplied in any input format acceptable to
  15. # ImageMagick, but their actual colour usage must already be
  16. # appropriate for the specified mode; this script will not do any
  17. # substantive conversion. So if an image intended to be used in 4-
  18. # or 1-bit mode contains any colour not in the appropriate fixed
  19. # palette, that's a fatal error; if an image to be used in 8-bit
  20. # mode contains more than 256 distinct colours, that's also a fatal
  21. # error.
  22. #
  23. # Command-line syntax is:
  24. #
  25. # icon.pl -depth imagefile [imagefile...] [-depth imagefile [imagefile...]]
  26. #
  27. # where `-depth' is one of `-24', `-8', `-4' or `-1', and tells the
  28. # script how to treat all the image files given after that option
  29. # until the next depth option. For example, you might execute
  30. #
  31. # icon.pl -24 48x48x24.png 32x32x24.png -8 32x32x8.png -1 monochrome.png
  32. #
  33. # to build an icon file containing two differently sized 24-bit
  34. # images, one 8-bit image and one black and white image.
  35. #
  36. # Windows .ICO files support a 1-bit alpha channel on all these
  37. # image types. That is, any pixel can be either opaque or fully
  38. # transparent, but not partially transparent. The alpha channel is
  39. # separate from the main image data, meaning that `transparent' is
  40. # not required to take up a palette entry. (So an 8-bit image can
  41. # have 256 distinct _opaque_ colours, plus transparent pixels as
  42. # well.) If the input images have alpha channels, they will be used
  43. # to determine which pixels of the icon are transparent, by simple
  44. # quantisation half way up (e.g. in a PNG image with an 8-bit alpha
  45. # channel, alpha values of 00-7F will be mapped to transparent
  46. # pixels, and 80-FF will become opaque).
  47. # The Windows 16-colour palette consists of:
  48. # - the eight corners of the colour cube (000000, 0000FF, 00FF00,
  49. # 00FFFF, FF0000, FF00FF, FFFF00, FFFFFF)
  50. # - dim versions of the seven non-black corners, at 128/255 of the
  51. # brightness (000080, 008000, 008080, 800000, 800080, 808000,
  52. # 808080)
  53. # - light grey at 192/255 of full brightness (C0C0C0).
  54. %win16pal = (
  55. "\x00\x00\x00\x00" => 0,
  56. "\x00\x00\x80\x00" => 1,
  57. "\x00\x80\x00\x00" => 2,
  58. "\x00\x80\x80\x00" => 3,
  59. "\x80\x00\x00\x00" => 4,
  60. "\x80\x00\x80\x00" => 5,
  61. "\x80\x80\x00\x00" => 6,
  62. "\xC0\xC0\xC0\x00" => 7,
  63. "\x80\x80\x80\x00" => 8,
  64. "\x00\x00\xFF\x00" => 9,
  65. "\x00\xFF\x00\x00" => 10,
  66. "\x00\xFF\xFF\x00" => 11,
  67. "\xFF\x00\x00\x00" => 12,
  68. "\xFF\x00\xFF\x00" => 13,
  69. "\xFF\xFF\x00\x00" => 14,
  70. "\xFF\xFF\xFF\x00" => 15,
  71. );
  72. @win16pal = sort { $win16pal{$a} <=> $win16pal{$b} } keys %win16pal;
  73. # The black and white palette consists of black (000000) and white
  74. # (FFFFFF), obviously.
  75. %win2pal = (
  76. "\x00\x00\x00\x00" => 0,
  77. "\xFF\xFF\xFF\x00" => 1,
  78. );
  79. @win2pal = sort { $win16pal{$a} <=> $win2pal{$b} } keys %win2pal;
  80. @hdr = ();
  81. @dat = ();
  82. $depth = undef;
  83. foreach $_ (@ARGV) {
  84. if (/^-(24|8|4|1)$/) {
  85. $depth = $1;
  86. } elsif (defined $depth) {
  87. &readicon($_, $depth);
  88. } else {
  89. $usage = 1;
  90. }
  91. }
  92. if ($usage || length @hdr == 0) {
  93. print "usage: icon.pl ( -24 | -8 | -4 | -1 ) image [image...]\n";
  94. print " [ ( -24 | -8 | -4 | -1 ) image [image...] ...]\n";
  95. exit 0;
  96. }
  97. # Now write out the output icon file.
  98. print pack "vvv", 0, 1, scalar @hdr; # file-level header
  99. $filepos = 6 + 16 * scalar @hdr;
  100. for ($i = 0; $i < scalar @hdr; $i++) {
  101. print $hdr[$i];
  102. print pack "V", $filepos;
  103. $filepos += length($dat[$i]);
  104. }
  105. for ($i = 0; $i < scalar @hdr; $i++) {
  106. print $dat[$i];
  107. }
  108. sub readicon {
  109. my $filename = shift @_;
  110. my $depth = shift @_;
  111. my $pix;
  112. my $i;
  113. my %pal;
  114. # Determine the icon's width and height.
  115. my $w = `identify -format %w $filename`;
  116. my $h = `identify -format %h $filename`;
  117. # Read the file in as RGBA data. We flip vertically at this
  118. # point, to avoid having to do it ourselves (.BMP and hence
  119. # .ICO are bottom-up).
  120. my $data = [];
  121. open IDATA, "convert -flip -depth 8 $filename rgba:- |";
  122. push @$data, $rgb while (read IDATA,$rgb,4,0) == 4;
  123. close IDATA;
  124. # Check we have the right amount of data.
  125. $xl = $w * $h;
  126. $al = scalar @$data;
  127. die "wrong amount of image data ($al, expected $xl) from $filename\n"
  128. unless $al == $xl;
  129. # Build the alpha channel now, so we can exclude transparent
  130. # pixels from the palette analysis. We replace transparent
  131. # pixels with undef in the data array.
  132. #
  133. # We quantise the alpha channel half way up, so that alpha of
  134. # 0x80 or more is taken to be fully opaque and 0x7F or less is
  135. # fully transparent. Nasty, but the best we can do without
  136. # dithering (and don't even suggest we do that!).
  137. my $x;
  138. my $y;
  139. my $alpha = "";
  140. for ($y = 0; $y < $h; $y++) {
  141. my $currbyte = 0, $currbits = 0;
  142. for ($x = 0; $x < (($w+31)|31)-31; $x++) {
  143. $pix = ($x < $w ? $data->[$y*$w+$x] : "\x00\x00\x00\xFF");
  144. my @rgba = unpack "CCCC", $pix;
  145. $currbyte <<= 1;
  146. $currbits++;
  147. if ($rgba[3] < 0x80) {
  148. if ($x < $w) {
  149. $data->[$y*$w+$x] = undef;
  150. }
  151. $currbyte |= 1; # MS has the alpha channel inverted :-)
  152. } else {
  153. # Might as well flip RGBA into BGR0 while we're here.
  154. if ($x < $w) {
  155. $data->[$y*$w+$x] = pack "CCCC",
  156. $rgba[2], $rgba[1], $rgba[0], 0;
  157. }
  158. }
  159. if ($currbits >= 8) {
  160. $alpha .= pack "C", $currbyte;
  161. $currbits -= 8;
  162. }
  163. }
  164. }
  165. # For an 8-bit image, check we have at most 256 distinct
  166. # colours, and build the palette.
  167. %pal = ();
  168. if ($depth == 8) {
  169. my $palindex = 0;
  170. foreach $pix (@$data) {
  171. next unless defined $pix;
  172. $pal{$pix} = $palindex++ unless defined $pal{$pix};
  173. }
  174. die "too many colours in 8-bit image $filename\n" unless $palindex <= 256;
  175. } elsif ($depth == 4) {
  176. %pal = %win16pal;
  177. } elsif ($depth == 1) {
  178. %pal = %win2pal;
  179. }
  180. my $raster = "";
  181. if ($depth < 24) {
  182. # For a non-24-bit image, flatten the image into one palette
  183. # index per pixel.
  184. $pad = 32 / $depth; # number of pixels to pad scanline to 4-byte align
  185. $pmask = $pad-1;
  186. for ($y = 0; $y < $h; $y++) {
  187. my $currbyte = 0, $currbits = 0;
  188. for ($x = 0; $x < (($w+$pmask)|$pmask)-$pmask; $x++) {
  189. $currbyte <<= $depth;
  190. $currbits += $depth;
  191. if ($x < $w && defined ($pix = $data->[$y*$w+$x])) {
  192. if (!defined $pal{$pix}) {
  193. $pixhex = sprintf "%02x%02x%02x", unpack "CCC", $pix;
  194. die "illegal colour value $pixhex at pixel ($x,$y) in $filename\n";
  195. }
  196. $currbyte |= $pal{$pix};
  197. }
  198. if ($currbits >= 8) {
  199. $raster .= pack "C", $currbyte;
  200. $currbits -= 8;
  201. }
  202. }
  203. }
  204. } else {
  205. # For a 24-bit image, reverse the order of the R,G,B values
  206. # and stick a padding zero on the end.
  207. #
  208. # (In this loop we don't need to bother padding the
  209. # scanline out to a multiple of four bytes, because every
  210. # pixel takes four whole bytes anyway.)
  211. for ($i = 0; $i < scalar @$data; $i++) {
  212. if (defined $data->[$i]) {
  213. $raster .= $data->[$i];
  214. } else {
  215. $raster .= "\x00\x00\x00\x00";
  216. }
  217. }
  218. $depth = 32; # and adjust this
  219. }
  220. # Prepare the icon data. First the header...
  221. my $data = pack "VVVvvVVVVVV",
  222. 40, # size of bitmap info header
  223. $w, # icon width
  224. $h*2, # icon height (x2 to indicate the subsequent alpha channel)
  225. 1, # 1 plane (common to all MS image formats)
  226. $depth, # bits per pixel
  227. 0, # no compression
  228. length $raster, # image size
  229. 0, 0, 0, 0; # resolution, colours used, colours important (ignored)
  230. # ... then the palette ...
  231. if ($depth <= 8) {
  232. my $ncols = (1 << $depth);
  233. my $palette = "\x00\x00\x00\x00" x $ncols;
  234. foreach $i (keys %pal) {
  235. substr($palette, $pal{$i}*4, 4) = $i;
  236. }
  237. $data .= $palette;
  238. }
  239. # ... the raster data we already had ready ...
  240. $data .= $raster;
  241. # ... and the alpha channel we already had as well.
  242. $data .= $alpha;
  243. # Prepare the header which will represent this image in the
  244. # icon file.
  245. my $header = pack "CCCCvvV",
  246. $w, $h, # width and height (this time the real height)
  247. 1 << $depth, # number of colours, if less than 256
  248. 0, # reserved
  249. 1, # planes
  250. $depth, # bits per pixel
  251. length $data; # size of real icon data
  252. push @hdr, $header;
  253. push @dat, $data;
  254. }