Code to construct Windows icon files for the puzzles, by munging the
authorsimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Tue, 26 Dec 2006 22:00:11 +0000 (22:00 +0000)
committersimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Tue, 26 Dec 2006 22:00:11 +0000 (22:00 +0000)
screenshots into appropriate sizes and colour depths. This is all
done with a nasty Perl script, because ImageMagick does not output
correct .ICO format. Not sure why; it isn't _that_ hard.

I intend at some point to link the resulting icons into the actual
Windows puzzle binaries, but before then I have to make them
prettier: most of them would benefit from being derived from a
smaller crop of the puzzle screenshot instead of trying to fit the
whole thing in.

git-svn-id: svn://svn.tartarus.org/sgt/puzzles@7017 cda61777-01e9-0310-a592-d414129be87e

icons/Makefile
icons/icon.pl [new file with mode: 0755]
icons/square.pl
icons/win16pal.xpm [new file with mode: 0644]

index 114de63..a5a1c93 100644 (file)
@@ -4,26 +4,74 @@ PUZZLES = blackbox bridges cube dominosa fifteen flip guess inertia lightup \
           loopy map mines net netslide pattern pegs rect samegame sixteen \
           slant solo tents twiddle untangle
 
-BASEPNGS = $(patsubst %,%-base.png,$(PUZZLES))
-WEBPNGS = $(patsubst %,%-web.png,$(PUZZLES))
+BASE = $(patsubst %,%-base.png,$(PUZZLES))
+WEB = $(patsubst %,%-web.png,$(PUZZLES))
+
+BASE4 = $(patsubst %,%-base4.png,$(PUZZLES))
+
+P48D24 = $(patsubst %,%-48d24.png,$(PUZZLES))
+P48D8 = $(patsubst %,%-48d8.png,$(PUZZLES))
+P48D4 = $(patsubst %,%-48d4.png,$(PUZZLES))
+P32D24 = $(patsubst %,%-32d24.png,$(PUZZLES))
+P32D8 = $(patsubst %,%-32d8.png,$(PUZZLES))
+P32D4 = $(patsubst %,%-32d4.png,$(PUZZLES))
+P16D24 = $(patsubst %,%-16d24.png,$(PUZZLES))
+P16D8 = $(patsubst %,%-16d8.png,$(PUZZLES))
+P16D4 = $(patsubst %,%-16d4.png,$(PUZZLES))
+ICONS = $(patsubst %,%.ico,$(PUZZLES))
 
 BIN = ../
 PIC = ./
 
-base: $(BASEPNGS)
-webpics: $(WEBPNGS)
+base: $(BASE)
+web: $(WEB)
+pngicons: $(I48D24) $(I32D24) $(I16D24)
+icons: $(ICONS)
 
-fifteen.png : override REDO=0.3
-flip.png : override REDO=0.3
-netslide.png : override REDO=0.3
-sixteen.png : override REDO=0.3
-twiddle.png : override REDO=0.3
+fifteen-base.png : override REDO=0.3
+flip-base.png : override REDO=0.3
+netslide-base.png : override REDO=0.3
+sixteen-base.png : override REDO=0.3
+twiddle-base.png : override REDO=0.3
 
-$(BASEPNGS): %-base.png: $(BIN)% $(PIC)%.sav $(PIC)screenshot.sh
+$(BASE): %-base.png: $(BIN)% $(PIC)%.sav
        $(PIC)screenshot.sh $(BIN)$* $(PIC)$*.sav $@ $(REDO)
 
-$(WEBPNGS): %-web.png: %-base.png
+$(BASE4): %-base4.png: %-base.png
+       convert -colors 16 +dither -map win16pal.xpm $^ $@
+
+$(WEB): %-web.png: %-base.png
        $(PIC)square.pl 150 5 $^ $@
 
+$(P48D24): %-48d24.png: %-base.png
+       $(PIC)square.pl 48 4 $^ $@
+$(P32D24): %-32d24.png: %-base.png
+       $(PIC)square.pl 32 2 $^ $@
+$(P16D24): %-16d24.png: %-base.png
+       $(PIC)square.pl 16 1 $^ $@
+
+$(P48D8) $(P32D8) $(P16D8): %d8.png: %d24.png
+       convert -colors 256 $^ $@
+
+# The depth-4 images work better if we normalise the colours
+# _before_ shrinking, and then normalise again afterwards.
+$(P48D4): %-48d4.png: %-base4.png
+       $(PIC)square.pl 48 1 $^ tmp2.png
+       convert -colors 16 -map win16pal.xpm tmp2.png $@
+       rm -f tmp.png tmp2.png
+$(P32D4): %-32d4.png: %-base.png
+       $(PIC)square.pl 32 1 $^ tmp2.png
+       convert -colors 16 -map win16pal.xpm tmp2.png $@
+       rm -f tmp.png tmp2.png
+$(P16D4): %-16d4.png: %-base.png
+       $(PIC)square.pl 16 1 $^ tmp2.png
+       convert -colors 16 -map win16pal.xpm tmp2.png $@
+       rm -f tmp.png tmp2.png
+
+$(ICONS): %.ico: %-48d24.png %-48d8.png %-48d4.png \
+                 %-32d24.png %-32d8.png %-32d4.png \
+                 %-16d24.png %-16d8.png %-16d4.png
+       $(PIC)icon.pl $? > $@
+
 clean:
-       rm -f *.png
+       rm -f *.png *.ico
diff --git a/icons/icon.pl b/icons/icon.pl
new file mode 100755 (executable)
index 0000000..f0d2931
--- /dev/null
@@ -0,0 +1,211 @@
+#!/usr/bin/perl 
+
+# Take nine input image files and convert them into a
+# multi-resolution Windows .ICO icon file. The nine files should
+# be, in order:
+#
+#  - 48x48 icons at 24-bit, 8-bit and 4-bit colour depth respectively
+#  - 32x32 icons at 24-bit, 8-bit and 4-bit colour depth respectively
+#  - 16x16 icons at 24-bit, 8-bit and 4-bit colour depth respectively
+#
+# ICO files support a 1-bit alpha channel on all these image types.
+#
+# TODO: it would be nice if we could extend this icon builder to
+# support monochrome icons and a user-specified subset of the
+# available formats. None of that should be too hard: the
+# monochrome raster data has the same format as the alpha channel,
+# monochrome images have a 2-colour palette containing 000000 and
+# FFFFFF respectively, and really the biggest problem is designing
+# a sensible command-line syntax!
+
+%win16pal = (
+    "\x00\x00\x00\x00" => 0,
+    "\x00\x00\x80\x00" => 1,
+    "\x00\x80\x00\x00" => 2,
+    "\x00\x80\x80\x00" => 3,
+    "\x80\x00\x00\x00" => 4,
+    "\x80\x00\x80\x00" => 5,
+    "\x80\x80\x00\x00" => 6,
+    "\xC0\xC0\xC0\x00" => 7,
+    "\x80\x80\x80\x00" => 8,
+    "\x00\x00\xFF\x00" => 9,
+    "\x00\xFF\x00\x00" => 10,
+    "\x00\xFF\xFF\x00" => 11,
+    "\xFF\x00\x00\x00" => 12,
+    "\xFF\x00\xFF\x00" => 13,
+    "\xFF\xFF\x00\x00" => 14,
+    "\xFF\xFF\xFF\x00" => 15,
+);
+@win16pal = sort { $win16pal{$a} <=> $win16pal{$b} } keys %win16pal;
+
+@hdr = ();
+@dat = ();
+
+&readicon($ARGV[0], 48, 48, 24);
+&readicon($ARGV[1], 48, 48, 8);
+&readicon($ARGV[2], 48, 48, 4);
+&readicon($ARGV[3], 32, 32, 24);
+&readicon($ARGV[4], 32, 32, 8);
+&readicon($ARGV[5], 32, 32, 4);
+&readicon($ARGV[6], 16, 16, 24);
+&readicon($ARGV[7], 16, 16, 8);
+&readicon($ARGV[8], 16, 16, 4);
+
+# Now write out the output icon file.
+print pack "vvv", 0, 1, scalar @hdr; # file-level header
+$filepos = 6 + 16 * scalar @hdr;
+for ($i = 0; $i < scalar @hdr; $i++) {
+    print $hdr[$i];
+    print pack "V", $filepos;
+    $filepos += length($dat[$i]);
+}
+for ($i = 0; $i < scalar @hdr; $i++) {
+    print $dat[$i];
+}
+
+sub readicon {
+    my $filename = shift @_;
+    my $w = shift @_;
+    my $h = shift @_;
+    my $depth = shift @_;
+    my $pix;
+    my $i;
+    my %pal;
+
+    # Read the file in as RGBA data. We flip vertically at this
+    # point, to avoid having to do it ourselves (.BMP and hence
+    # .ICO are bottom-up).
+    my $data = [];
+    open IDATA, "convert -flip -depth 8 $filename rgba:- |";
+    push @$data, $rgb while (read IDATA,$rgb,4,0) == 4;
+    close IDATA;
+    # Check we have the right amount of data.
+    $xl = $w * $h;
+    $al = scalar @$data;
+    die "wrong amount of image data ($al, expected $xl) from $filename\n"
+      unless $al == $xl;
+
+    # Build the alpha channel now, so we can exclude transparent
+    # pixels from the palette analysis. We replace transparent
+    # pixels with undef in the data array.
+    #
+    # We quantise the alpha channel half way up, so that alpha of
+    # 0x80 or more is taken to be fully opaque and 0x7F or less is
+    # fully transparent. Nasty, but the best we can do without
+    # dithering (and don't even suggest we do that!).
+    my $x;
+    my $y;
+    my $alpha = "";
+
+    for ($y = 0; $y < $h; $y++) {
+       my $currbyte = 0, $currbits = 0;
+       for ($x = 0; $x < (($w+31)|31)-31; $x++) {
+           $pix = ($x < $w ? $data->[$y*$w+$x] : "\x00\x00\x00\xFF");
+           my @rgba = unpack "CCCC", $pix;
+           $currbyte <<= 1;
+           $currbits++;
+           if ($rgba[3] < 0x80) {
+               if ($x < $w) {
+                   $data->[$y*$w+$x] = undef;
+               }
+               $currbyte |= 1; # MS has the alpha channel inverted :-)
+           } else {
+               # Might as well flip RGBA into BGR0 while we're here.
+               if ($x < $w) {
+                   $data->[$y*$w+$x] = pack "CCCC",
+                     $rgba[2], $rgba[1], $rgba[0], 0;
+               }
+           }
+           if ($currbits >= 8) {
+               $alpha .= pack "C", $currbyte;
+               $currbits -= 8;
+           }
+       }
+    }
+
+    # For an 8-bit image, check we have at most 256 distinct
+    # colours, and build the palette.
+    %pal = ();
+    if ($depth == 8) {
+       my $palindex = 0;
+       foreach $pix (@$data) {
+           next unless defined $pix;
+           $pal{$pix} = $palindex++ unless defined $pal{$pix};
+       }
+       die "too many colours in 8-bit image $filename\n" unless $palindex <= 256;
+    } elsif ($depth == 4) {
+       %pal = %win16pal;
+    }
+
+    my $raster = "";
+    if ($depth < 24) {
+       # For a non-24-bit image, flatten the image into one palette
+       # index per pixel.
+       my $currbyte = 0, $currbits = 0;
+       for ($i = 0; $i < scalar @$data; $i++) {
+           $pix = $data->[$i];
+           $currbyte <<= $depth;
+           $currbits += $depth;
+           if (defined $pix) {
+               if (!defined $pal{$pix}) {
+                   die "illegal colour value $pix at pixel $i in $filename\n";
+               }
+               $currbyte |= $pal{$pix};
+           } else {
+               $currbyte |= 0;
+           }
+           if ($currbits >= 8) {
+               $raster .= pack "C", $currbyte;
+               $currbits -= 8;
+           }
+       }
+    } else {
+       # For a 24-bit image, reverse the order of the R,G,B values
+       # and stick a padding zero on the end.
+       for ($i = 0; $i < scalar @$data; $i++) {
+           if (defined $data->[$i]) {
+               $raster .= $data->[$i];
+           } else {
+               $raster .= "\x00\x00\x00\x00";
+           }
+       }
+       $depth = 32; # and adjust this
+    }
+
+    # Prepare the icon data. First the header...
+    my $data = pack "VVVvvVVVVVV",
+      40, # size of bitmap info header
+      $w, # icon width
+      $h*2, # icon height (x2 to indicate the subsequent alpha channel)
+      1, # 1 plane (common to all MS image formats)
+      $depth, # bits per pixel
+      0, # no compression
+      length $raster, # image size
+      0, 0, 0, 0; # resolution, colours used, colours important (ignored)
+    # ... then the palette ...
+    if ($depth <= 8) {
+       my $ncols = (1 << $depth);
+       my $palette = "\x00\x00\x00\x00" x $ncols;
+       foreach $i (keys %pal) {
+           substr($palette, $pal{$i}*4, 4) = $i;
+       }
+       $data .= $palette;
+    }
+    # ... the raster data we already had ready ...
+    $data .= $raster;
+    # ... and the alpha channel we already had as well.
+    $data .= $alpha;
+
+    # Prepare the header which will represent this image in the
+    # icon file.
+    my $header = pack "CCCCvvV",
+      $w, $h, # width and height (this time the real height)
+      1 << $depth, # number of colours, if less than 256
+      0, # reserved
+      1, # planes
+      $depth, # bits per pixel
+      length $data; # size of real icon data
+
+    push @hdr, $header;
+    push @dat, $data;
+}
index 295b2f1..81be9d7 100755 (executable)
@@ -17,13 +17,13 @@ $ident =~ /(\d+) (\d+)/ or die "unable to get size for $infile\n";
 
 # Read the input image data.
 $data = [];
-open IDATA, "convert $infile rgb:- |";
+open IDATA, "convert -depth 8 $infile rgb:- |";
 push @$data, $rgb while (read IDATA,$rgb,3,0) == 3;
 close IDATA;
 # Check we have the right amount of data.
 $xl = $w * $h;
 $al = scalar @$data;
-die "wrong amount of image data ($al, expected $xl) from $img\n"
+die "wrong amount of image data ($al, expected $xl) from $infile\n"
   unless $al == $xl;
 
 # Find the background colour. We assume the image already has a
diff --git a/icons/win16pal.xpm b/icons/win16pal.xpm
new file mode 100644 (file)
index 0000000..66fd60a
--- /dev/null
@@ -0,0 +1,23 @@
+/* XPM */
+static char *win16pal[] = {
+/* columns rows colors chars-per-pixel */
+"16 1 16 1",
+"  c #000000",
+". c #800000",
+"X c #008000",
+"o c #808000",
+"O c #000080",
+"+ c #800080",
+"@ c #008080",
+"# c #C0C0C0",
+"$ c #808080",
+"% c #FF0000",
+"& c #00FF00",
+"* c #FFFF00",
+"= c #0000FF",
+"- c #FF00FF",
+"; c #00FFFF",
+": c #FFFFFF",
+/* pixels */
+" .XoO+@#$%&*=-;:"
+};