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