Get The Current Wallpaper

A simple requirement: save the current desktop wallpaper in a file. Actually not that simple in practice because there isn't a tool that can deliver it.

The standard method of screen grabbing is xwd which is great at grabbing an image of a window, either with our without its frame (the borders, title bar and such that are added by the window manager), or the root window which gives an image of the entire desktop. It can't, however, just return the wallpaper and it seems that there isn't anything else that can.

Google, being such a good friend, reveals that this requirement has appeared before.

Erlend Hamberg wrote a little tool called getbg in C and Xlib that writes the desktop background to a file. A good place to start.

Erlend's getbg works, ableit with a couple of tweaks to get a clean compile:

  • replace the comments at the top of the file with C-style comments
  • remove the references to the screen variable because it isn't used.

Usage is simple:

$ getbg background.jpg

However, it produces JPEG images and that's a lossy format. It'd be better if it wrote PNG. The code's pretty clear and changing the output format would just be a case of replacing the function (write_jpeg) that writes the JPEG image with another function that writes a PNG image instead. We'd need a function with a prototype like this:

int write_png(XImage *img, const char* filename)

Searching the Black Duck openhub (formerly koders.com) for some code revealed that writing PNG is quite complex.

It would be better if xwd could do this itself. If given a new option, xwd could return the background in a .xwd file. Many tools like Gimp and ImageMagick could then be used to convert to myriad formats.

So, how could that be implemented?

The first part of the problem is getting the root background as an XImage and looking at getbg.c reveals how it does that:

XWindowAttributes attrs;
Pixmap bg;
XImage* img;
XGetWindowAttributes(display, rootwindow, &attrs);
bg = GetRootPixmap(display, &root);
img = XGetImage(display, bg, 0, 0, attrs.width, attrs.height, ~0, ZPixmap);

The second part of the problem is writing an XImage as a .xwd file. A quick peruse of the xwd source code shows what's involved.

The magic happens in the Window_Dump function in xwd.c: it gets an XImage and then writes it. All we need to do is change how the image is obtained when a -bg option is given. Simples, eh?

Building xwd

Building xwd is easy. One option is to build the Arch Linux package. Either use the ABS to get the package source or copy it from packages.git aif you have that. Alternatively, just create a directory and download the PKGBUILD into it - it's the only file in the package anyway.

$ mkdir xwd
$ curl -o PKGBUILD https://projects.archlinux.org/svntogit/packages.git/plain/trunk/PKGBUILD?h=packages/xorg-xwd

Then modify the PKGBUILD to point at a local checkout like this example. Or, for a local repo:

$ source=(${pkgname}::git+ssh://myuser@myhost/path/to/xwd/repo)

Build and install

$ makepkg -s
$ pacman -U xorg-xwd-1.0.6.n.cccccccc-n-x86_64.pkg.tar.xz

Patching

Implementing the change was relatively straightforward with xwd.c being the only file changed. A new -bg command-line option was added along with the associated usage information for display by the -help option. When this option is selected the background is returned without any windows. Implementing the new option required adding a new block of code to Window_Dump:

 if (bg_only)
 {
    if (XGetWindowProperty(dpy, RootWindow(dpy, screen),
        XInternAtom(dpy, "_XROOTPMAP_ID", False), 0, 1, False,
        XA_PIXMAP, &act_type, &act_format, &nitems, &bytes_after,
        &bg_data) == Success) {
        if (bg_data) {
            image = XGetImage(dpy, *((Pixmap *) bg_data),
                              absx, absy, width, height, AllPlanes, format);
            XFree(bg_data);
        }
    }
 }

The existing block of code that gets the image was moved into an else clause after the new block. Apart from declaring the six new variables required by the new block of code, no other changes were necessary. The variable declarations were placed at the head of the Window_Dump function after the existing ones:

 Pixmap              bg_pixmap;
 Atom                act_type;
 int                 act_format;
 unsigned long       nitems, bytes_after;
 unsigned char       *bg_data = NULL;

The result makes this possible:

$ xwd -root -bg > /tmp/rootbg.xwd

which gives an image (this one is from a three-monitor setup):
dump to PNG format: xwd -root -bg | convert -resize 800 - bg.png

The way the code worked out, the -bg option works for individual windows as well, and with or without the -frame option. The returned image is the same size as before (the size of the window and frame, if requested) but is the background under the window instead of the window itself.

The revised code is available on GitHub and patches have been sent upstream for consideration.

Gratitude to Erlend for writing getbg, without which this patch would't have been so straightforward.