November 16, 2009

Generating Freedesktop.org spec compliant thumbnails

A lot of Linux software that needs to generate thumbnails of an image uses the freedesktop.org thumbnail specification. This is good as it means if one application has already made a thumbnail of an image then another application can make use of it rather than generating it's own. I recently found myself looking for a way to generate such thumbnails.

The impetus for this was gnome-appearance-properties. This is the application which allows you to do stuff like change your wallpaper. Sadly as the number of wallpapers available increases it's usability decreases to some extent. The reason for this is that the user interface is frozen until all the wallpaper thumbnails are displayed. This in itself isn't too bad, unless you have thousands of wallpapers, but if it's combined with a lack of pre-generated thumbnails the user interface is frozen until all the thumbnails have been generated. This is annoying because if there are few hundred wallpapers available thumbnail generation can take over thirty seconds even on a decent spec machine. Thirty seconds during which the user is left looking at an unresponsive interface. There is long standing bug report regarding this, that I can't currently locate, which makes the very sensible suggestion that the thumbnails should be loaded asynchronously. Hopefully at some point someone will implement that, but in the mean time I found myself wondering whether it was possible to script the generation of thumbnails in advance.

My first thought was ImageMagick and a bash script because I'm already familiar with those. As it turns out ImageMagick comes very, very close to being able to generate such thumbnails using the -thumbnail option. I say close, because whilst it inserts both the MTime and URI information required by the freedesktop.org spec, it generates the URI incorrectly by inserting one too many slashes at the start. It creates

$ convert /usr/share/pixmaps/backgrounds/cosmos/earthrise.jpg -thumbnail 128x foo.png
$ identify -verbose foo.png | grep Thumb::URI
Thumb::URI: file:////usr/share/pixmaps/backgrounds/cosmos/earthrise.jpg

when it needs to be

 Thumb::URI: file:///usr/share/pixmaps/backgrounds/cosmos/earthrise.jpg

At time of writing this is actually fixed, but only in the svn version. If you have 6.5.7-8 or later then you should find it generates the URI properly. If you have an older version you can create the thumbnails like this:

#!/bin/bash
# makethumb - script to generate thumbnails to freedesktop.org spec
# *** Assumes GNU coreutils. ***
file=$1
saveto=~/.thumbnails/normal
tagfile=/tmp/$(basename $0)_tags
mkdir -p $saveto
thumbname=$(echo -n file://$file | md5sum| cut -d " " -f 1);
mtime=$(date +%s -r "$file")
echo "Thumb::URI={file://${file}}" >$tagfile
echo "Thumb::MTime={${mtime}}" >>$tagfile
convert -resize 128x -strip +profile "*" $file MIFF:- | cat $tagfile - | convert MIFF:- "PNG:${saveto}/${thumbname}.png"
rm -f $tagfile
$ makethumb /usr/share/pixmaps/backgrounds/cosmos/earthrise.jpg

Generating thumbnails this way is quite slow though. Using

$ find  /usr/share/pixmaps/backgrounds/ -type f -exec ~/makethumb {} \;

332 thumbnails took around 1:15 in my tests, though obviously this will vary depending on the spec of the machine. I tried using a variant of the script which generated thumbnails for all the files in a given directory, so only invoking the script once instead of multiple times. There was no significant difference in speed between the two methods though.

So I started looking for some way to use GNOME's thumbnail generation capabilities. The only example I could find of doing this used Python GTK bindings and was incomplete. I've only ever cobbled together one python script before, (that was also to use GTK bindings), but I managed to put together this

#!/usr/bin/python
# makethumb.py - script to generate thumbnails using GTK bindings

import gnome.ui
import gnomevfs
import time
import sys
import os

file=sys.argv[1]

uri=gnomevfs.get_uri_from_local_path(file)
mime=gnomevfs.get_mime_type(file)
mtime=int(time.strftime("%s",time.localtime(os.path.getmtime(file))))
thumbFactory = gnome.ui.ThumbnailFactory(gnome.ui.THUMBNAIL_SIZE_NORMAL)
if thumbFactory.can_thumbnail(uri ,mime, 0):
thumbnail=thumbFactory.generate_thumbnail(uri, mime)
if thumbnail != None:
thumbFactory.save_thumbnail(thumbnail, uri, mtime)

Using that to generate thumbnails in the same manner shown above for the bash script was about ten seconds faster. However after some experimentation I put together this (updated 15/8/11 to include suggestions from comment 2):

#!/usr/bin/python
# makethumbs.py - generates thumbnails for all files in a directory

import gnome.ui
import gnomevfs
import time
import os

dir="/usr/share/pixmaps/backgrounds/"

thumbFactory = gnome.ui.ThumbnailFactory(gnome.ui.THUMBNAIL_SIZE_NORMAL)

for subdir, dirs, files in os.walk(dir):
for file in files:
path = os.path.join(subdir, file)
uri = gnomevfs.get_uri_from_local_path(path)
mime=gnomevfs.get_mime_type(subdir+"/"+file)
mtime = int(os.path.getmtime(path))
print uri
print mtime
if thumbFactory.can_thumbnail(uri ,mime, 0):
thumbnail=thumbFactory.generate_thumbnail(uri, mime)
if thumbnail is not None:
thumbFactory.save_thumbnail(thumbnail, uri, mtime)

I found that generates 332 thumbnails in around 9 seconds. A massive difference to repeatedly invoking the makethumb.py script. I expect there are people who could provide a detailed explanation of why it's so much faster. I am not one of them.

It's also interesting to note that I've found this script generates thumbnails around three times faster than the gnome-appearance-properties creates them. Why that is I have no idea. The thumbs that result are not identical. The thumbnails generated by the Python script have the width and height of the original image embedded in them whilst the ones generated by gnome-appearance-properties do not. The ones generated by gnome-appearance-properties have a very lightly larger file size and the Channel Statistics embedded in the thumbnails are different too. However both sets of thumbnails say they were generated by GNOME::ThumbnailFactory.

Interesting as all this is, (to me anyway if it's not to you then why did you read this far?), it's all about generating thumbnails on a per-user basis. What if it was possible to have a system wide cache of per-generated thumbnails. E.g. you install a bunch of wallpapers and along with them you can install thumbnails that will be used rather than each user generating their own. The freedesktop.org thumbnail spec does cover this. So I tried creating such thumbnails. gnome-appearance-properties ignored them. When I say ignored them, I don't mean it looked at them and didn't use them, the output of strace indicates that it doesn't even look to see they exist. Which is a shame.


- 3 comments by 1 or more people

  1. lbovet

    Thanks for your article. I used your script for an open source project needing to generate thumbnails: https://github.com/lbovet/phpdigikam/blob/master/create-thumbnails.py

    Please tell me if the terms I publish it under are not OK.

    06 Apr 2011, 21:40

  2. Philipp

    Thank you for this nice posting, I have some comments:

    The Python implementation is so much faster, since it only loads the Python interpreter, the Python modules and the required shared library only once, instead of 332 times. To set up a process needs a lot of time, which than only generated one image before being thrown away before the whole process is repeated again and again.
    I would also think, that you didn’t clear the ~/.thumbnail/-directory after generating all thumbnails, so on the second run nothing hat do been done. 9 seconds looks way to short even for the Python implementation.
    Another cause might be that the original wallpapers were still cached, so on the second run no expensive access to your hard drive was necessary.

    Some more comment on you implementation:
    > uri=gnomevfs.get_uri_from_local_path(subdir+”/”+file)
    Change this to
    < path = os.path.join(subdir, file)
    < uri = gnomevfs.get_uri_from_local_path(path)
    since it works for with other OSs too, which for example use \ as the path separator.
    Also calculate that string once and reuse it in all the other calls.

    > mtime=int(time.strftime(“%s”,time.localtime(os.path.getmtime(subdir+”/”+file))))
    < mtime = int(os.path.getmtime(path))
    No need to do expensive string operations.

    > thumbFactory = gnome.ui.ThumbnailFactory(gnome.ui.THUMBNAIL_SIZE_NORMAL)
    Move this out of the look, this only needs to be done once.

    > if thumbnail != None:
    < if thumbnail is not None:
    Comparison with None should use the “is” operator for identity-checking, not ”!=”, which is object class dependent.

    In my test this program still has a memory leak: After generating several thousand thumbnails, the process was using multiple mega bytes.

    01 Aug 2011, 22:51

  3. Mike Willis

    Philipp – thanks for the comments, very useful. I’ve updated the script as a result. The changes don’t make much difference to the time it takes to run in my tests, the average time comes down from about 20 seconds to about 19 seconds for 330 images. I guess most of the overhead is in file access. (I flushed the filesystem cache from memory before each run but clearing out ~/.thumbnails didn’t make any difference as the thumbnails are re-written each time anyway.)

    I think I accounted for disk access in the original timings but it was so long ago I don’t remember for sure. It’s the sort of thing I usually do though.

    15 Aug 2011, 12:12


Add a comment

Name
Email
Anti-Spam Question
Are you the anonymous annoyance known as Sue, or spammer scum trying to promote some website by leaving a link to it in my comments?
Anti-Spam Answer
Comment


Your IP address will be recorded. -

You can not use HTML, but you can use our special markup -

Search this blog

Tags

Not signed in
Sign in

Powered by BlogBuilder
© MMXIX