Open Source
Adding eye candy to your desktop
You need XFree86 4.3 to enjoy the sweetest improvements
Mar. 26, 2003 12:00 AM
(LinuxWorld) To each his own, but I like eye candy. For all their benefits and power, X11 and X Window System managers have been among the most drab and poorly rendered graphical environments for years. Recent versions of XFree86 began to support anti-aliased fonts, which helped. But that's pure protein. It's good for you, and it helps you get your work done. Only recently has XFree86 begun to add the sugary content that we shallow folks crave.
You'll have to get XFree86 4.3 to enjoy the sweetest improvements. This version supports custom-animated cursors, and it's reasonably easy to create cursor sets. Here's a picture of one of my favorite cursor sets, which I found on www.kde-look.org.

The above image doesn't do the gold cursor-set justice, because the beauty is in the animation. I recommend you try it for yourself. KDE-Look.org has another great cursor set built around Tux the penguin. It's a bit too sweet for me, but I appreciate the work that must have gone into making this set.
Stardock
Brad Wardell, an ex-OS/2 geek and (current) friend of mine, created the company Stardock for the purpose of supplementing OS/2 with cool gadgets and games. When Brad saw the writing on the wall with respect to OS/2, he jumped on the Windows wagon. Forgive him, folks; he knows not what he did. But he and his programmers do know what they do, and they do it well. They create lots of Windows gadgets, including one called CursorXP. CursorXP is a mouse-cursor enhancement to Windows XP, and there are lots of animated eye-candy cursors created for it.
Wouldn't it be nice if we could use some of those cursors? It isn't that difficult to fire up Gimp or some other graphics program to convert them. You won't have to go to that trouble, thanks to Eric Windisch.
Eric created a script in Perl and ImageMagick that translates Stardock mouse-cursor sets into files XFree86 4.3 can use. The script is still in its infancy, so the code for creating drop shadows and transparency effects is incomplete. I rewrote the drop-shadow code to make it work well enough for my taste (see below for the complete source code). You can also download it from VarLinux.org (see Resources for a link).
Here's how to use the script (this article assumes you are using my modified version of the script).
If you want to generate plain X11 mouse-cursors, use this command:
# cd [directory where the Stardock cursor set is stored]
# sd2xc.pl
I tend to prefer drop shadows for my X11 cursors, but some of the cursor sets look better without them. If you want to generate X11 mouse-cursors with a drop shadow, use this command:
# cd [directory where the Stardock cursor set is stored]
# sd2xc.pl --shadow
There are other parameters you can change to adjust the look of the drop shadow, but I've hard-coded them to unusual defaults to compensate for the fact that I can't figure out how to get ImageMagick to adjust opacity the way I'd like. In the meantime, I'm afraid you'll have to RTFSC (read the fine source code) if you want to know how to use them.
Anyway, assuming the script does its job properly, it will place the cursors in the sub-directory ./theme/cursors. Rename theme to whatever you want to call this particular cursor set. For example, if you are converting the Stardock IBounce set, you might do this:
# mv theme bouncy
Then move bouncy to the place where you want to store your cursors. If you want the cursor set to be available to everyone on the system, move it to /usr/share/icons.
# mv bouncy /usr/share/icons
If you want this set to be available to a specific user, move it to their .icons directory (substitute the username for joeblough).
# mv bouncy /home/joeblough/.icons
Now you have to tell X11 which cursor-set you want to use. There are a number of ways you can do this. First, let's assume you want to do it on a per-user basis. In that case, create the user's default icon directory if it doesn't already exist (substitute the user name for joeblough.) :
# mkdir /home/joeblough/.icons/default
Then create an index.theme text file in that directory and make sure the file includes the following (substitute the name of the cursor theme you want to use for bouncy).
[Icon Theme]
Inherits=bouncy
As long as there is a bouncy theme in either your user's home .icon directory or the global directory (usually /usr/share/icons, as shown above), this user should start to see the custom mouse-cursors the next time he or she starts up X11.
Another method is to edit the .Xdefaults file in your home directory and add a line such as the following:
Xcursor.theme: bouncy
How the ImageMagick Perl script works
I almost refused to hack the Perl script due to my allergy to Perl. Perl is a fine language, but I hate it. In case you're wondering how much I hate it, let me put it this way: if I were handcuffed to a Perl manual, I would chew through my arm to get it off. In fact, one of the only languages I hate more than Perl is Scheme, which is what Gimp uses for its scripting capability. That's a shame, because it would probably have been easier to perform the conversion from Stardock CursorXP to X11 XCursors with Gimp. But Perl, being the lesser of two extreme evils, was the way I chose, though not without some regrets.
Fortunately, this particular Perl script is fairly legible, so it wasn't very difficult to grasp and change it to my liking. The ImageMagick extensions to Perl, on the other hand, gave me no end of trouble and were the sole source of my regrets.
The script works fine if you don't want to have drop shadows for your cursors, but the shadow feature didn't seem to work for me. I modified the code that creates the drop shadow so that it works for me. Unfortunately, I had to come up with a pretty convoluted way to create the drop shadows, thanks to the mysterious and poorly documented way ImageMagick handles composite images.
Here is the logical method of creating a drop shadow (at least it seems logical to me) and the workarounds I had to apply. The first step:
- Create a canvas large enough to contain the cursor plus the drop shadow.
The script always places drop shadows to the right and below the image. One could modify the code to have a drop shadow appear elsewhere, but that would often require you to move the hotspot for the mouse cursor (the part of the image that matters when you click the mouse button). It's not difficult to do that, but I have no problem with a drop shadow down and to the right, so I didn't code for that possibility.
Given how the drop shadow is placed, one must calculate how much the image may grow when you add the drop shadow. It depends on the size of the image and the transparent background canvas. Nevertheless, one must assume the worst: one or more cursor images is as large as the canvas. If you don't, you risk clipping the drop shadow so that part of it is missing, which will make it look silly.
There's the potential for one or more frames to grow in size in two respects. First, it may grow by the amount of offset you specify for the drop shadow (so much to the right and so far down from the image). It may also grow due to the amount of blur you create for the drop shadow, because the blur radius spreads the shadow image. I fudged the growth potential by adding the blur radius to the X and Y offsets, then starting with a canvas much larger than the original image. This is overkill for some images, but it works out just right in others.
- Create the drop shadow.
Under normal circumstances, I would create the drop shadow using these steps:
- Place a copy of the original cursor image on a transparent canvas, but place it offset by the X and Y offset for the drop shadow.
- Convert the image to all black (or gray).
Instead of the above, I simply create a composite image against a transparent background using the "CopyOpacity" feature, with the X and Y offset specified as part of the composition process. This creates a "stencil" of the image in black, which appears at the offset amount. A black stencil of the original image is actually not the result I would have expected from this operation, but I stumbled on this result and used it.
- Blur the image.
Normally, I would blur the image only slightly. I'll explain in a moment why the script does otherwise.
- Adjust the opacity (transparency) of the image to your liking.
For the life of me, I could not figure out how to adjust the opacity of the drop shadow using any of the techniques that are seemingly designed to get this result. I tried creating composite images of a transparent background with the drop shadow and adjusting the opacity amount, but that never worked. I tried the "dissolve" method of combining a transparent background with the drop shadow. That always produced a black background, even if I tried to mask the image. I can get dissolve to work in other ways. For example, I can combine two images so that one color dissolves into another. But I can never seem to combine an image with transparency so that the result is only partially transparent.
I have the feeling that there is a combination of settings, such as color spaces, image types, etc., that one must manage (although because I was able to get dissolve to work with certain colors and the image formats I used support transparency, one would think I had these settings right). Regardless, whatever I may be missing is probably obvious to those who created and refined ImageMagick. But that doesn't help me at all, as I cannot find a shred of documentation that explains how to use ImageMagick to create a partially transparent image on a fully transparent background.
I discovered, however, that if I blur the image enough, the blurred shadow becomes partially transparent. So I cranked up the blur parameters just enough to get a result similar to what I would shoot for if I could do it "right". This seems to work for the most part.
At this point, it should be easy to create the final image.
- Superimpose the original cursor image in its original location on top of the newly created drop shadow.
That's almost all there is to it. The rest of the code handles the gory details that arise from the different way Stardock stores its images. Stardock puts all of the animated frames in a single PNG file. The XCursor format requires these animated frames to be separate files. The code looks up the number of frames in the Stardock INI file and uses that to figure out how to crop the multi-frame PNG into separate images. The code also examines the INI file for other information, such as the hotspot and the animation rate.
The Perl code
Here is the code, as of publication. Note that not all of it is useful yet, but Eric Windisch left in several options for later expansion (some of which I changed, and I removed a section of code that wasn't being used).
I invite anyone who knows Perl, ImageMagick, Scheme, Gimp, or whatever other combination that works to send me code that can do this job better. I'd love to fix this code to dissolve the shadow into a proper level of transparency or even replace the code with a script using some other tool set. Until then, enjoy the eye candy, but be careful not to go into insulin shock.
#!/usr/bin/perl
#
# Copyright Eric Windisch, 2003.
# Licensed under the MIT license.
# Modified by Nicholas Petreley, March 2003
#
use strict;
use Image::Magick;
use Getopt::Long;
use Config::IniFiles;my ($config_file, $path, $tmppath, $generator,$verbose, $inherits,$tmpscheme,$shadow, $opacity, $shadowopacity, $shadowx, $shadowy, $shadowblur,$shadowblursigma);
# default for variables
$verbose=1;
$shadow=0;
$opacity=100;
$shadowopacity=70;
$shadowx=9;
$shadowy=4;
$shadowblur=5;
$shadowblursigma=7;
$path="theme/";
$tmppath="tmp/";
$generator="/usr/bin/X11/xcursorgen";
# it seems that recursive inheritance does not yet exist.
$inherits="whiteglass";
sub process {
print <<EOF;
Usage:
$0 [-v] [--inherits theme] [--shadow] [--shadow-x pixels] [--shadow-y pixels] [--shadow-blur size] [--shadow-blur-sigma size] [--shadow-opacity] [--generator xcursorgen-path] [--tmp temp-dir]
EOF
exit 0;
};
GetOptions (
'inherits=s'=>\$inherits,
'tmp=s'=>\$tmppath,
'shadow'=>\$shadow,
'v'=>\$verbose,
'generator=s'=>\$generator,
'opacity=i'=>\$opacity,
'<>' => \&process,
'help'=>\&process,
'shadow-x=i'=>\$shadowx,
'shadow-y=i'=>\$shadowy,
'shadow-blur=i'=>\$shadowblur,
'shadow-blur-sigma=i'=>\$shadowblursigma,
'shadow-opacity=i'=>\$shadowopacity
);
# make sure path and tmppath end in /
if ($path =~ /[^\/]$/) {
$path=$path."/";
}
if ($tmppath =~ /[^\/]$/) {
$tmppath=$tmppath."/";
}
if (! -d $path) {
mkdir ($path);
}
if (! -d $path."cursors/") {
mkdir ($path."cursors/");
}
if (! -d $tmppath) {
mkdir ($tmppath);
}
$tmpscheme=$tmppath."Scheme.ini";
# I did this much nicer, but Perl < 5.8 choked.
open (INI, "< Scheme.ini") or die ("Cannot open Scheme.ini");
open (INF, ">", $tmpscheme);
while (<INI>) {
unless (!/=/ && !/^\s*\[/) {
#$config_file.=$_;
print INF $_;
}
}
close (INI);
close (INF);
my $cfg=new Config::IniFiles(-file=>$tmpscheme) or die ("Scheme.ini in wrong format? -".$@);
my @sections=$cfg->Sections;
my $filemap={
Arrow=>["left_ptr","X_cursor","right_ptr",'4498f0e0c1937ffe01fd06f973665830'],
Cross=>["tcross","cross"],
Hand=>["hand1", "hand2",'9d800788f1b08800ae810202380a0822','e29285e634086352946a0e7090d73106'],
IBeam=>"xterm",
UpArrow=>"center_ptr",
SizeNWSE=>["bottom_right_corner","top_left_corner",'c7088f0f3e6c8088236ef8e1e3e70000'],
SizeNESW=>["bottom_left_corner","top_right_corner",'fcf1c3c7cd4491d801f1e1c78f100000'],
SizeWE=>["sb_h_double_arrow", "left_side", "right_side",'028006030e0e7ebffc7f7070c0600140'],
SizeNS=>["double_arrow","bottom_side","top_side",'00008160000006810000408080010102'],
Help=>["question_arrow",'d9ce0ab605698f320427677b458ad60b'],
Handwriting=>"pencil",
AppStarting=>["left_ptr_watch", '3ecb610c1bf2410f44200f48c40d3599'],
SizeAll=>"fleur",
Wait=>"watch",
NO=>"03b6e0fcb3499374a867c041f52298f0"
};
foreach my $section (@sections) {
my ($filename);
$filename=$section.".png";
unless (-f $filename) {
next;
}
my ($image, $x, $frames, $width, $height, $curout);
$image=Image::Magick->new;
$x=$image->Read($filename);
warn "$x" if "$x";
$frames=$cfg->val($section, 'Frames');
$width=$image->Get('width')/$frames;
$height=$image->Get('height');
if (defined($filemap->{$section})) {
$curout=$filemap->{$section};
} else {
$curout=$section;
}
my $array=-1;
eval {
if (defined (@{$curout}[0])) { };
};
unless ($@) {
$array=0;
}
LOOP:
my $outfile;
if ($array > -1) {
if (defined (@{$curout}[0])) {
$outfile=pop @{$curout};
} else {
next;
}
} else {
$outfile=$curout;
}
$outfile=$path."cursors/".$outfile;
if ($verbose) {
print "Writing to $section -> $outfile\n";
}
open (FH, "| $generator > \"$outfile\"");
my $yoffset = $shadowy + $shadowblur;
my $xoffset = $shadowx + $shadowblur;
my $swidth = $width + $xoffset;
my $sheight = $height + $yoffset;
for (my $i=0; $i<$frames; $i++) {
my ($tmpimg, $outfile);
$outfile=$tmppath.$section.'-'.$i.'.png';
$tmpimg=$image->Clone();
$x=$tmpimg->Crop(width=>$width, height=>$height, x=>$i*$width, y=>0);
warn "$x" if "$x";
if ($shadow) {
my $blank = Image::Magick->new;
$blank->Set(size=>$swidth."x".$sheight);
$blank->ReadImage('xc:transparent');
$blank->Set(type=>"TrueColorMatte");
$blank->Quantize(colorspace=>"RGB");
$blank->Colorize(fill=>"black",opacity=>0);
$blank->Transparent(color=>"black");
my $blankcanvas=$blank->Clone();
$x=$blank->Composite(image=>$tmpimg, compose=>"CopyOpacity", x=>$shadowx, y=>$shadowy, opacity=>0);
warn "$x" if "$x";
$x=$blank->Blur(radius=>$shadowblur, sigma=>$shadowblursigma);
warn "$x" if "$x";
$x=$blank->Composite(image=>$tmpimg, compose=>"Over", x=>0, y=>0, opacity=>0);
$tmpimg=$blank;
}
$x=$tmpimg->Write($outfile);
warn "$x" if "$x";
print FH "1 ".
$cfg->val($section,'Hot spot x')." ".
$cfg->val($section,'Hot spot y')." ".
$outfile." ".
$cfg->val($section,'Interval')."\n";
}
if ($array > -1) {
goto LOOP;
}
}
print "Writing theme index.\n";
open (FH, "> ${path}index.theme");
print FH <<EOF;
[Icon Theme]
Inherits=$inherits
EOF
close (FH);
print "Done. Theme wrote to ${path}\n";