Exporting SVG from Cocoa

Following from my previous post about SVG exporting in cocoa, here are some details on how this is done in SQLEditor. The example code is extracted from the live SQLEditor code base, but it’s been altered to simplify in a few places. Hopefully it doesn’t contain any bugs, but please point them out if you see them.

In addition, I don’t offer this up particularly as a tutorial, this is just the way that I did it, so there are possibly (probably?) better ways of doing this. Feel free to reuse any of it, in any way, if you wish using either BSD or Attribution 3.0 Unported . I’ll try and put a complete working example of it up on github soon. Update: Source code now on github

The key point is that SVG is just a dialect of XML. This means that we can use NSXMLDocument and friends to generate the SVG, which makes life much easier than it would be otherwise.

We use a standard NSXMLDocument

- (NSXMLDocument*)xmlDocument
{
id rootNode = [self exportToXMLNode];
id xmlDocument = [[[NSXMLDocument alloc] initWithRootElement:rootNode] autorelease];
[xmlDocument setVersion:@"1.0"];
[xmlDocument setCharacterEncoding:@"UTF-8"];
return xmlDocument;
}

The code to generate the root node looks like this:

- (NSXMLNode*)exportToXMLNode
{
    id exportNode = [NSXMLElement elementWithName:@"svg"];

	id viewBoxString = [NSString stringWithFormat:@"%.0f %.0f %.0f %.0f",imageBounds.origin.x,imageBounds.origin.y,imageBounds.size.width,imageBounds.size.height];

    [exportNode addAttribute:[NSXMLNode attributeWithName:@"viewBox" stringValue:viewBoxString]];
    [exportNode addAttribute:[NSXMLNode attributeWithName:@"width"
                                              stringValue:[NSString stringWithFormat:@"%.0f",imageBounds.size.width]]];
    [exportNode addAttribute:[NSXMLNode attributeWithName:@"height"
                                              stringValue:[NSString stringWithFormat:@"%.0f",imageBounds.size.height]]];

    id namespace = [NSXMLNode namespaceWithName:@"" stringValue:@"http://www.w3.org/2000/svg"];
    [exportNode setNamespaces:[NSArray arrayWithObject:namespace]];

    id transformGroup = [NSXMLNode elementWithName:@"g"];
    [transformGroup addAttribute:[NSXMLNode attributeWithName:@"transform" stringValue:@"translate(0.5,0.5)"]];

    [exportNode addChild:transformGroup];

    for (id entry in [self.container objectListByZOrder]) {

        if (![entry getPropertyAsBoolean:@"hidden"]) {
                id newNode = [self exportObjectToSVG:entry];

                if (newNode) {
                    [transformGroup addChild:newNode];
                }
            }

    }

	return exportNode;
}

objectListByZOrder returns an array of objects, ordered by zOrder, from back to front.

We use a transform group with an offset of (0.5,0.5) to align the image to the screen pixels. (The alternative would be to draw everything at 0.5 pixel offsets, but this is annoying)

We also use a viewBox, which is set to the value of imageBounds, a variable which contains the bounding rectangle of the objects in the document. This is already set up by the time this code is called.

getPropertyAsBoolean is part of the SQLEditor object properties system and in this case, its used to determine whether the object being drawn is hidden or not. Obviously if it’s hidden, then it doesn’t get drawn. At least one beta version of SQLEditor overlooked this fairly obvious point.  🙂

Now finally, an example object exporting section:

- (id)textElement:(NSString *)text atPoint:(NSPoint)textLocation
{
    id titleText = [MHSVGElement elementWithName:@"text"];
    id titleAttributes = [NSMutableDictionary dictionaryWithCapacity:1];
    [titleAttributes addEntriesFromDictionary:[self dictionaryForLocation:textLocation]];
    [titleAttributes setObject:@"10" forKey:@"dx"];
    [titleAttributes setObject:@"14" forKey:@"dy"];
    [titleAttributes setObject:@"black" forKey:@"fill"];
    [titleAttributes setObject:@"13" forKey:@"font-size"];
    [titleAttributes setObject:@"Lucida Grande" forKey:@"font-family"];
    [titleAttributes setObject:@"optimizeLegibility" forKey:@"text-rendering"];

    [titleText setAttributesWithDictionary:titleAttributes];

    [titleText setStringValue:text];
    return titleText;
}

- (id)rectangleElement:(NSRect)objectRect
             fillColor:(NSColor*)color
           strokeColor:(NSColor*)strokeColor
          cornerRadius:(NSUInteger)radius
{
    id mainFrame = [MHSVGElement elementWithName:@"rect"];

    id attributes = [NSMutableDictionary dictionaryWithCapacity:1];

    [attributes addEntriesFromDictionary:[self dictionaryForLocation:objectRect.origin]];
    [attributes addEntriesFromDictionary:[self dictionaryForSize:objectRect.size]];
    [attributes setObject:[NSNumber numberWithInteger:radius] forKey:@"rx"];
    [attributes setObject:[NSNumber numberWithInteger:radius] forKey:@"ry"];

    id strokeColorString = @"none";

    if (strokeColor) {
        strokeColorString = [self svgColor:strokeColor];
        [attributes setObject:@"1" forKey:@"stroke-width"];
        [attributes setObject:strokeColorString forKey:@"stroke"];
    }

    NSString* fillColorString = [self svgColor:color];

    [attributes setObject:fillColorString forKey:@"fill"];
    [attributes setObject:[NSString stringWithFormat:@"%f",[color alphaComponent]] forKey:@"fill-opacity"];

    [mainFrame setAttributesWithDictionary:attributes];

    return mainFrame;
}

- (NSXMLNode*)exportSQLCanvasAreaToSVG:(SQLCanvasArea*)object
{

    id commentGroup = [MHSVGElement elementWithName:@"g"];

    [commentGroup addAttribute:[NSXMLNode attributeWithName:@"class" stringValue:@"SQLCanvasArea"]];

    NSRect areaRect;
    areaRect.origin = [object location];
    areaRect.size = [object size];

    id mainFrame = [self rectangleElement:areaRect
                                fillColor:[self labelColorForObject:object]
                              strokeColor:[NSColor blackColor]
                             cornerRadius:5];

    [commentGroup addChild:mainFrame];

    id titleText = [self textElement:[object getName] atPoint:[object location]];
    [titleText addAttribute:[NSXMLElement attributeWithName:@"font-family" stringValue:@"Lucida Grande"]];
    [titleText addAttribute:[NSXMLElement attributeWithName:@"font-weight" stringValue:@"bold"]];

    [commentGroup addChild:titleText];

    return commentGroup;
}

Each object has a specific export<objectClass>ToSVG method, in this caseexportSQLCanvasAreaToSVG. It sets up a new group, draws the background and the title, then returns the xml element back, so it can be added to the main image.

At minimum, splitting up the drawing by object type is a good idea, probably better to have separate classes for drawing each type (possibly with some inheritance or composition)

As you can see, the text drawing section makes a number of hard coded assumptions right now. Those were chosen to match the SQLEditor default drawing styles as closely as possible. It’s not a perfect match though, due to the variation between drawing in a cocoa NSView/NSWindow vs drawing in a SVG context (usually in a browser). Fonts are something of an issue and there are issues with rendering across platforms.

Overall though, I’m quite pleased, the first phase of SVG export didn’t prove too troublesome to implement and added a useful feature to SQLEditor without either too much coding, or any extra dependencies.

Update
Example code (with minor changes) now on github: https://github.com/AngusHardie/cocoasvgexperiment

This entry was posted in General. Bookmark the permalink.

2 Responses to Exporting SVG from Cocoa

  1. Jan says:

    Wow. Exactly what I was looking for!

    Can I help in any way in getting a working sample up on github?

  2. Angus Hardie says:

    Thanks 🙂

    It’s up on github now (+post edited to include link)

Leave a Reply