Our geek adventures
The problemWe need to display thumbnails for 2D images, which are SWF files.
Option 1. Use embedded FlashPros:
- quick drop-in solution;
- requirement for clients to have Flash installed in their browsers;
- thumbnail displaying logic depends on what we are displaying (2D, 3D, photo) = additional complexity;
- requires additional markup for embedding Flash;
- is slow to load and display;
- (the most important) embedded Flash object is not regular HTML-element and it puts some constraints on UI design (e.g. other HTML elements cannot be placed on top of rendered image).
Option 2. Manual renderingSWF files are loaded into desktop Adobe AIR app, which produces PNG thumbnails and saves them where the client app can get them (default Paperclip paths in our case). This process can be made to work in batch mode, so there is no need to manually load, render and save each file. Pros:
- thumbnails are PNG files, so they can be dealt with as any other regular images on client side;
- requires dedicated machine with the AIR app;
- when a user uploads an SWF file, it is not rendered at once, so we have to show him something else for visual feedback as we don’t have rendered image yet.
Option 3. Server-side renderingThe most logical and architecturally right option. Unfortunately, Flash player is proprietary software and there are no libraries or tools for this task. Nevertheless, it is possible, and I’ll explain how it can be done.
SolutionWe are going to use Gnash player. Gnash is the GNU SWF movie player, which can be run standalone on the desktop or an embedded device, as well as as a plugin for several browsers (from http://www.gnashdev.org). It it has a feature named “dump-gui”, which allows to dump player’s output as raw video file and doesn’t require X or any GUI libraries to run, what makes it a good candidate for our task.
Step 1. Get GnashGnash player has a package in many popular distributions, but it is compiled without the ‘dump’ option. Therefore, we need to configure and compile it from source. You can read about it here and here.
wget http://ftp.gnu.org/pub/gnu/gnash/0.8.8/gnash-0.8.8.tar.gz ./configure --enable-gui=gtk,dump make sudo make installHere I’m configuring and compiling Gnash with both gtk and dump GUIs so that I could quickly check rendering results using gtk player. You won’t need it on server, of course.
Step 2. Render 2D images using SWF file as a player (supplying Flash parameters)This step can be skipped if your swf file is displayed correctly in the player. In our case, if we run
gnash image_2d.swfwe will see only a part of an image, because image’s origin (x=0, y=0) is in the center of the image. To display it correctly, we will use another swf file named preview.swf, which, in its turn, loads and displays our 2D image. We need to supply file’s location using flash parameter named “url”. As different 2D images have different sizes, we’ll also use -j (width) and -k (height) parameters to generate large fixed size picture.
gnash preview.swf \ -P "FlashVars=url=file:///some/dir/image_2d.swf" -j 500 -k 500
Step 3. Deal with security settingsBy default, preview.swf can only access files which are in or under the same directory as preview.swf. As on server side processing will take place in /tmp directory, while preview.swf will be located in the application’s directory, we need to configure Gnash to allow access to local files in /tmp. Create ~/.gnashrc file with this line:
set localSandboxPath /tmp
Step 4. Get raw video file from playerBy default, player will run endlessly. There are three parameters, which can be used to stop its execution:
- —once – doesn’t work in our case (throws an error);
- —timeout <sec> - should be set to some time (in seconds) after which player stops; it means, rendering will take at least 1 second (and generate 1 second of video) and we have to be sure that this time is enough to render any picture we have (or else it has to be 2, 3 seconds for all images) – not exactly what we would like;
- —max-advances N – stops after N frame advances; was tested with N=1 and worked well – we are going to use this parameter.
dump-gnash preview.swf \ -P "FlashVars=url=file:///some/dir/image_2d.swf” \ -D output.raw --max-advances 1 -j 500 -k 500Output:
# WARNING: Gnash was told to loop the movie # Gnash created a raw dump file with the following properties: COLORSPACE=BGRA32 NAME=output.raw WIDTH=500 HEIGHT=500 INTERVAL=10 FPS_DESIRED=100 TIME=0.251716 FPS_ACTUAL=99.3183 FRAMECOUNT=26As we see from the output, Gnash produced output.raw which is 500x500 pixels raw video file with colorspace BGRA32. There are 26 frames in the video and rendering took about 0.25 sec.
Step 5. Extracting an image from raw video fileAccording to output, we have 8-bit BGRA raw video. What does it mean? It is just a sequence of one-byte values for each of four channels (Blue, Green, Red, Alpha) for each pixel for each frame. If we open that file with mplayer, we’ll see that our image flickers. That’s because player produces blank frames while loading image_2d.swf. What we really need from this video is the last frame. As we set output size to 500x500 pixels and each pixel is 4 bytes, we need last 1000000 bytes from output.raw.
tail -c 1MB output.raw > last_frame.raw
Step 6. Converting raw image to PNGKnowing the structure of the raw file, we could use some PNG library and write a few lines of code to produce PNG from raw. But, as our application uses Paperclip and, in its turn, Paperclip uses Imagemagick to make thumbnails, we are going to use Imagemagick, too.
convert -size 500x500 -depth 8 rgba:last_frame.raw \ -separate -swap 0,2 -combine -trim png:render.pngSize and depth options as well as rgba prefix give convert tool information about our raw file. Imagemagick doesn’t support brga colorspace, so we are using rgba and swapping channels that is just a little bit harder than swapping letters :). Last parameter trims whitespace from the picture.
Step 7. Final scriptThis script is run by a subclass of Paperclip::Processor and produces source image for making thumbnails with Paperclip::Processor::Thumbnail. But this is another story :)
#!/bin/sh raw=$(mktemp) dump-gnash $1 -P "FlashVars=url=file://$2" \ -D $raw --max-advances 1 -j 500 -k 500 tail -c 1MB $raw | convert -size 500x500 -depth 8 rgba:- \ -separate -swap 0,2 -combine -trim png:$3 trap "rm $raw" EXITThis script accepts three parameters:
- location of preview.swf;
- location of swf file to render;
- where to put the output