Hobby-hacking Eric


inkscape layers

Here's a small program that I wrote to extract a subset of layers from an Inkscape file. It may be handy if you have to give a talk and you want to include some "animated" overlays in your slides.

I'm writing this post because I'm pleased to be able to automate this process at last. Also, I want to demonstrate that you don't have to be particularly clever or ambitious to get some good practical use out of Haskell.


So I've got my Inkscape file with a "base" layer and several steps of my animation "zero", "one", "two", "three".

If I do inkscape-layers myfile.svg base > /tmp/foo.svg && inkscape --export-pdf=/tmp/foo.pdf", I get just the base layer which isn't very interesting:

Now if I do inkscape-layers myfile.svg base zero (and convert the resulting SVG into a PDF as above), I get the zeroth layer:

Likewise, to build the rest of my animation, inkscape-layers myfile.svg base one

inkscape-layers myfile.svg base two

Now instead of going clickity-click all over the place, I just dump this in my Makefile. If I every have to change something about my animation (for example, in the base layer), I just run "make" and rebuild it automatically.

Yay, Haskell! Well, I'm sure you could just as easily have written this in your favourite programming language; I just like to randomly credit Haskell for making my life easier :-D

the code

I may upload this to Hackage if I could maybe get some other useful inkscape tools with it:
import Data.Maybe (fromMaybe)
import System.Environment (getArgs, getProgName)
import System.IO (hPutStrLn, stdout, stderr)
import Text.XML.Light

main =
do args <- getArgs
pname <- getProgName
case args of
(f:ls) -> go f ls
_ -> hPutStrLn stderr $ unwords [ "Usage:", pname, "filename", "layer1", "[layer2 [.. layer N]]" ]

go f ls =
do d <- goodXML =<< parseXMLDoc `fmap` readFile f
let o = stdout -- we may want to make this more flexible later
hPutStrLn o . showTopElement . wrapTop walk $ d
goodXML = maybe (fail "bad XML") return
walk x@(Elem el) =
let lbl = fromMaybe "" (findAttr qLABEL el)
x2 = Elem $ el { elContent = map walk (elContent el) }
in case () of _ | not (isLayer el) -> x2
| lbl `elem` ls -> x2
| otherwise -> Text blank_cdata
walk x = x

isLayer el = elName el == qSVG "g" && findAttr qGROUP_MODE el == Just "layer"

qLABEL = qInkscape "label"
qGROUP_MODE = qInkscape "groupmode"

qSVG l = QName l (Just nsSVG) Nothing
nsSVG = "http://www.w3.org/2000/svg"

qInkscape l = QName l (Just nsINKSCAPE) Nothing

wrapTop f e =
case f (Elem e) of
(Elem e) -> e
_ -> error "programmer error: top content is not an element"

Note: as an exercise: modify the attributes of all exported layers so that they are visible. In Inkscape, I tend to make layers invisible so I don't get confused by them. But then Inkscape does not export them, which is annoying. This seems to be a simple matter of replacing "display:none" with "display:inline" in the style attribute (watch out, there could be more than one!). The 'split' library on Hackage could be handy for that.


kowey said...

For the interested:

darcs get http://code.haskell.org/~kowey/inkscape-tools/

Kenn Knowles said...

Very cool. I did something similar:


One-off scripts are so easy in Haskell. The world should know :-)

kowey said...

Hey, that's nice! I was actually thinking of using HaXml for this, but then I got scared off by the lack (?) of namespace support. I see you get on just fine by not caring about namespaces per se and just treating the attributes as they are.

I always keep getting tempted to use HaXml for stuff, but then it always outsmarts me and I scurry back to the relative simplicity of Text.XML.Light. I don't know why. If I'm happy to use Parsec, why not HaXml?

Anonymous said...

If you feel moved to add more sophisticated command-line argument handling to this or other scripts, please check out my parseargs package on Haskell and let me know what you think. It is pretty lightweight for these kinds of scripts.

kowey said...

What I would really like to see is a version of parseargs that's rich enough to take over darcs's command line parsing.

Basically we want to be able to deal with commands like

darcs record foo
darcs show repo bar
darcs show repo bar --last=50

The darcs commmand line parsing is quite featureful. It handles, for example, the ability for different commands (whatsnew, changes) to have different sets of flags. Or for flags to go in sets (--no-foo vs --foo).

This kind of functionality would also be useful for lots of other projects such as camp, twidge, cabal-install and GHC (ghc-pkg).

See http://wiki.darcs.net/index.html/DarcsLibraries

Iavor Diatchki said...

That's great, thanks Eric! I was just looking for a way to do exactly this, and it was awesome that not only I found a tool to do it, but it is written in Haskell.