Files
IJSVG/source/IJSVGExporter.m

1349 lines
48 KiB
Objective-C

//
// IJSVGExporter.m
// IJSVGExample
//
// Created by Curtis Hard on 06/01/2017.
// Copyright © 2017 Curtis Hard. All rights reserved.
//
#import "IJSVGExporter.h"
#import "IJSVG.h"
#import "IJSVGGradientLayer.h"
#import "IJSVGRadialGradient.h"
#import "IJSVGLinearGradient.h"
#import "IJSVGPatternLayer.h"
#import "IJSVGImageLayer.h"
#import "IJSVGShapeLayer.h"
#import "IJSVGGroupLayer.h"
#import "IJSVGStrokeLayer.h"
#import "IJSVGMath.h"
#import "IJSVGExporterPathInstruction.h"
@implementation IJSVGExporter
#define XML_DOC_VERSION 1.1f
#define XML_DOC_NS @"http://www.w3.org/2000/svg"
#define XML_DOC_NSXLINK @"http://www.w3.org/1999/xlink"
#define XML_DOCTYPE_VERSION @"1.0"
#define XML_DOC_CHARSET @"UTF-8"
#define XML_DOC_GENERATOR @"Generated by IJSVG (https://github.com/iconjar/IJSVG)"
@synthesize title;
@synthesize description;
const NSArray * IJSVGShortCharacterArray()
{
static NSArray * _array;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_array = [@[@"a",@"b",@"c",@"d",@"e",@"f",@"g",@"h",@"i",@"j",@"k",@"l",
@"m",@"n",@"o",@"p",@"q",@"r",@"s",@"t",@"u",@"v",@"w",@"x",@"y",@"z",
@"A",@"B",@"C",@"D",@"E",@"F",@"G",@"H",@"I",@"J",@"K",@"L",
@"M",@"N",@"O",@"P",@"Q",@"R",@"S",@"T",@"U",@"V",@"W",@"X",@"Y",@"Z"] retain];
});
return _array;
}
const NSArray * IJSVGInheritableAttributes()
{
static NSArray * _attributes;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_attributes = [@[
@"clip-rule",
@"color",
@"color-interpolation",
@"color-interpolation-filters",
@"color-profile",
@"color-rendering",
@"cursor",
@"direction",
@"fill",
@"fill-opacity",
@"fill-rule",
@"font",
@"font-family",
@"font-size",
@"font-size-adjust",
@"font-stretch",
@"font-style",
@"font-variant",
@"font-weight",
@"glyph-orientation-horizontal",
@"glyph-orientation-vertical",
@"image-rendering",
@"kerning",
@"letter-spacing",
@"marker",
@"marker-end",
@"marker-mid",
@"marker-start",
@"pointer-events",
@"shape-rendering",
@"stroke",
@"stroke-dasharray",
@"stroke-dashoffset",
@"stroke-linecap",
@"stroke-linejoin",
@"stroke-miterlimit",
@"stroke-opacity",
@"stroke-width",
@"text-anchor",
@"text-rendering",
@"visibility",
@"white-space",
@"word-spacing",
@"writing-mode"] retain];
});
return _attributes;
}
void IJSVGApplyAttributesToElement(NSDictionary *attributes, NSXMLElement *element) {
[element setAttributesAsDictionary:attributes];
};
NSDictionary * IJSVGElementAttributeDictionary(NSXMLElement * element) {
NSMutableDictionary * dict = [[[NSMutableDictionary alloc] init] autorelease];
for(NSXMLNode * attribute in element.attributes) {
dict[attribute.name] = attribute.stringValue;
}
return dict;
};
NSString * IJSVGHashURL(NSString * key) {
return [NSString stringWithFormat:@"url(#%@)",key];
};
NSString * IJSVGHash(NSString * key) {
return [@"#" stringByAppendingString:key];
}
- (void)dealloc
{
[_scaledRootNode release], _scaledRootNode = nil;
[_svg release], _svg = nil;
[_dom release], _dom = nil;
[_defElement release], _defElement = nil;
[title release], title = nil;
[description release], description = nil;
[super dealloc];
}
- (id)initWithSVG:(IJSVG *)svg
size:(CGSize)size
options:(IJSVGExporterOptions)options
{
if((self = [super init]) != nil) {
_options = options;
_size = size;
_svg = [svg retain];
// clear memory as soon as possible
@autoreleasepool {
[self _prepare];
}
}
return self;
}
- (NSXMLElement *)defElement
{
if(_defElement != nil) {
return _defElement;
}
return _defElement = [[NSXMLElement alloc] initWithName:@"defs"];
}
- (NSString *)viewBoxWithRect:(NSRect)rect
{
char * buffer;
asprintf(&buffer, "%g %g %g %g", rect.origin.x, rect.origin.y,
rect.size.width, rect.size.height);
NSString * viewBox = [NSString stringWithCString:buffer
encoding:NSUTF8StringEncoding];
free(buffer);
return viewBox;
}
- (NSXMLElement *)rootNode
{
// generates the root document
NSXMLElement * root = [[[NSXMLElement alloc] initWithName:@"svg"] autorelease];
// sort out viewbox
NSRect viewBox = _svg.viewBox;
NSDictionary * attributes = @{
@"viewBox":[self viewBoxWithRect:viewBox],
@"version": [NSString stringWithFormat:@"%g",XML_DOC_VERSION],
@"xmlns": XML_DOC_NS,
@"xmlns:xlink": XML_DOC_NSXLINK
};
// add on width and height unless specified otherwise
if((_options & IJSVGExporterOptionRemoveWidthHeightAttributes) == 0) {
NSMutableDictionary * attDict = [[attributes mutableCopy] autorelease];
attDict[@"width"] = IJSVGShortFloatString(_size.width);
attDict[@"height"] = IJSVGShortFloatString(_size.height);
attributes = [[attDict copy] autorelease];
}
// was there a size set?
if(CGSizeEqualToSize(CGSizeZero, _size) == NO &&
(_size.width != viewBox.size.width && _size.height != viewBox.size.height)) {
// copy the attributes
NSMutableDictionary * att = [[attributes mutableCopy] autorelease];
att[@"width"] = IJSVGShortFloatString(_size.width);
att[@"height"] = IJSVGShortFloatString(_size.height);
// scale the whole SVG to fit the specified size
if((_options & IJSVGExporterOptionScaleToSizeIfNecessary) != 0) {
// work out the scale
CGFloat scale = MIN(_size.width/viewBox.size.width,
_size.height/viewBox.size.height);
// actually do the scale
if(scale != 1.f) {
NSString * scaleString = [NSString stringWithFormat:@"scale(%g)",scale];
NSDictionary * transform = @{@"transform":scaleString};
// create the main group and apply transform
_scaledRootNode = [[NSXMLElement alloc] initWithName:@"g"];
IJSVGApplyAttributesToElement(transform, _scaledRootNode);
// add it back onto root
[root addChild:_scaledRootNode];
// compute x and y, dont multiply 0
const CGFloat x = viewBox.origin.x == 0.f ? 0.f : (viewBox.origin.x * scale);
const CGFloat y = viewBox.origin.y == 0.f ? 0.f : (viewBox.origin.y * scale);
// reset the viewbox for the exported SVG
att[@"viewBox"] = [self viewBoxWithRect:(NSRect){
.origin = NSMakePoint(x, y),
.size = NSMakeSize(_size.width, _size.height)
}];
}
}
// reset attributes
attributes = [[att copy] autorelease];
}
// apply the attributes
IJSVGApplyAttributesToElement(attributes, root);
return root;
}
- (NSString *)generateID
{
const NSArray * chars = IJSVGShortCharacterArray();
if(_idCount < chars.count) {
return chars[_idCount++];
}
if((_idCount % chars.count) == 0) {
_shortIdCount++;
}
return [NSString stringWithFormat:@"%@%ld",chars[(_idCount++ % chars.count)],_shortIdCount];
}
- (void)_prepare
{
// create the stand alone DOM
_dom = [[NSXMLDocument alloc] initWithRootElement:[self rootNode]];
_dom.version = XML_DOCTYPE_VERSION;
_dom.characterEncoding = XML_DOC_CHARSET;
// sort out header
// sort out stuff, so here we go...
[self _recursiveParseFromLayer:_svg.layer
intoElement:(_scaledRootNode?:_dom.rootElement)];
// this needs to be added incase it needs to be cleaned
NSXMLElement * defNode = [self defElement];
if(defNode.childCount != 0) {
[_dom.rootElement insertChild:[self defElement]
atIndex:0];
}
// cleanup
[self _cleanup];
// could had been removed during cleaning process needs to be added back in!
if(defNode.childCount != 0 && defNode.parent == nil) {
[_dom.rootElement insertChild:[self defElement]
atIndex:0];
}
// add generator
NSXMLNode * generatorNode = [[[NSXMLNode alloc] initWithKind:NSXMLCommentKind] autorelease];
generatorNode.stringValue = XML_DOC_GENERATOR;
[_dom.rootElement insertChild:generatorNode
atIndex:0];
}
- (void)_cleanup
{
// remove hidden elements
if((_options & IJSVGExporterOptionRemoveHiddenElements) != 0) {
[self _removeHiddenElements];
}
// convert any duplicate paths into use
if((_options & IJSVGExporterOptionCreateUseForPaths) != 0) {
[self _convertUseElements];
}
// cleanup def
if((_options & IJSVGExporterOptionRemoveUselessDef) != 0) {
[self _cleanDef];
}
// collapse groups
if((_options & IJSVGExporterOptionCollapseGroups) != 0) {
[self _collapseGroups];
}
// clean any blank groups
if((_options & IJSVGExporterOptionRemoveUselessGroups) != 0) {
[self _cleanEmptyGroups];
}
// sort attributes
if((_options & IJSVGExporterOptionSortAttributes) != 0) {
[self _sortAttributesOnElement:_dom.rootElement];
}
// compress groups together
if((_options & IJSVGExporterOptionCollapseGroups) != 0) {
[self _compressGroups];
}
// collapse gradients?
if((_options & IJSVGExporterOptionCollapseGradients) != 0) {
[self _collapseGradients];
}
// create classes?
if((_options & IJSVGExporterOptionCreateClasses) != 0) {
[self _createClasses];
}
// move attributes to group
if((_options & IJSVGExporterOptionMoveAttributesToGroup) != 0) {
[self _moveAttributesToGroupWithElement:_dom.rootElement];
}
}
- (void)_createClasses
{
const NSArray * inhert = IJSVGInheritableAttributes();
NSArray<NSXMLElement *> * elements = [_dom nodesForXPath:@"//*"
error:nil];
NSMutableDictionary * rules = [[[NSMutableDictionary alloc] init] autorelease];
for(NSXMLElement * element in elements) {
NSDictionary * inhertEl = [self intersectableAttributes:IJSVGElementAttributeDictionary(element)
inheritableAttributes:inhert];
NSString * styles = [self styleSheetRulesFromDictionary:inhertEl];
NSString * className = nil;
if((className = [rules objectForKey:styles]) == nil) {
className = [NSString stringWithFormat:@"%@",[self generateID]];
rules[styles] = className;
}
for(NSString * attributeName in inhertEl) {
[element removeAttributeForName:attributeName];
}
IJSVGApplyAttributesToElement(@{@"class":className},element);
}
// add styles to dom
NSXMLElement * styles = [[[NSXMLElement alloc] initWithName:@"style"] autorelease];
NSXMLNode * node = [[[NSXMLNode alloc] initWithKind:NSXMLTextKind] autorelease];
NSMutableArray * classes = [[[NSMutableArray alloc] initWithCapacity:rules.count] autorelease];
for(NSString * r in rules) {
[classes addObject:[NSString stringWithFormat:@".%@%@",rules[r],r]];
}
node.stringValue = [classes componentsJoinedByString:@""];
[styles addChild:node];
[_dom.rootElement insertChild:styles atIndex:0];
}
- (NSString *)styleSheetRulesFromDictionary:(NSDictionary *)dict
{
NSMutableArray * array = [[[NSMutableArray alloc] initWithCapacity:dict.count] autorelease];
for(NSString * key in dict.allKeys) {
[array addObject:[NSString stringWithFormat:@"%@: %@;",key,dict[key]]];
}
return [NSString stringWithFormat:@"{%@}",[array componentsJoinedByString:@" "]];
}
- (void)_sortAttributesOnElement:(NSXMLElement *)element
{
// only apply to XML elements, not XMLNodes
if([element isKindOfClass:[NSXMLElement class]] == NO) {
return;
}
[self sortAttributesOnElement:element];
for(NSXMLElement * child in element.children) {
[self _sortAttributesOnElement:child];
}
}
- (void)_removeHiddenElements
{
// find any elements where they have a style, but the element itself
// must not be in the defs
NSArray<NSXMLElement *> * elements = [_dom nodesForXPath:@"//*[@display='none']"
error:nil];
for(NSXMLElement * element in elements) {
NSXMLElement * parent = (NSXMLElement *)element.parent;
[parent removeChildAtIndex:element.index];
}
}
- (void)_collapseGradients
{
NSString * xPath = @"//defs/*[self::linearGradient or self::radialGradient]";
NSArray<NSXMLElement *> * gradients = [_dom nodesForXPath:xPath error:nil];
for(NSInteger i = 0; i < gradients.count; i++) {
if(i != 0) {
NSXMLElement * gradientA = gradients[i];
NSXMLElement * gradientB = nil;
for(NSInteger s = (i - 1); s >= 0; s--) {
gradientB = gradients[s];
if([self compareElementChildren:gradientA toElement:gradientB] == YES) {
NSString * idString = [gradientB attributeForName:@"id"].stringValue;
if(idString == nil || idString.length == 0) {
idString = [self generateID];
IJSVGApplyAttributesToElement(@{@"id":idString}, gradientB);
}
NSDictionary * atts = @{@"xlink:href":IJSVGHash(idString)};
IJSVGApplyAttributesToElement(atts, gradientA);
[gradientA setChildren:nil];
break;
}
}
}
}
}
- (BOOL)compareElementChildren:(NSXMLElement *)element
toElement:(NSXMLElement *)toElement
{
NSArray * childrenA = element.children;
NSArray * childrenB = toElement.children;
if(childrenA.count != childrenB.count) {
return NO;
}
for(NSInteger i = 0; i < childrenA.count; i++) {
NSXMLElement * childA = childrenA[i];
NSXMLElement * childB = childrenB[i];
if([self compareElement:childA withElement:childB] == NO) {
return NO;
}
}
return YES;
}
- (void)_moveAttributesToGroupWithElement:(NSXMLElement *)parentElement
{
const NSArray * excludedNodes = @[@"script",@"style",@"defs"];
if([excludedNodes containsObject:parentElement.name] == YES) {
return;
}
const NSArray * inheritableAttributes = IJSVGInheritableAttributes();
NSDictionary * intersection = @{};
NSMutableArray * grouped = [[[NSMutableArray alloc] init] autorelease];
NSInteger counter = 0;
NSInteger size = parentElement.childCount - 1;
for(NSXMLElement * element in parentElement.children) {
NSDictionary * attributes = [self intersectableAttributes:IJSVGElementAttributeDictionary(element)
inheritableAttributes:inheritableAttributes];
if(intersection.count == 0) {
intersection = attributes;
}
NSDictionary * dict = [self intersectionInheritableAttributes:intersection
currentAttributes:attributes
inheritableAttributes:inheritableAttributes];
for(NSString * attributeToRemove in dict.allKeys) {
[element removeAttributeForName:attributeToRemove];
}
if(dict != nil) {
[grouped addObject:element];
}
if(dict == nil || counter == size) {
if(grouped.count > 1) {
NSXMLElement * groupElement = [[[NSXMLElement alloc] initWithName:@"g"] autorelease];
NSXMLElement * lastElement = (NSXMLElement *)grouped.lastObject;
NSInteger index = lastElement.index;
[parentElement replaceChildAtIndex:index withNode:groupElement];
for(NSXMLElement * elementToGroup in grouped) {
[elementToGroup detach];
[groupElement addChild:elementToGroup];
}
IJSVGApplyAttributesToElement(intersection, groupElement);
} else {
if(grouped.count == 1) {
NSXMLElement * onlyElement = (NSXMLElement *)grouped.lastObject;
IJSVGApplyAttributesToElement(intersection, onlyElement);
}
}
intersection = @{};
[grouped removeAllObjects];
}
counter++;
}
}
- (NSDictionary *)intersectableAttributes:(NSDictionary *)atts
inheritableAttributes:(const NSArray *)inheritable
{
NSMutableDictionary * dict = [[[NSMutableDictionary alloc] init] autorelease];
for(NSString * key in atts.allKeys) {
if([inheritable containsObject:key]) {
dict[key] = atts[key];
}
}
return dict;
}
- (NSDictionary *)intersectionInheritableAttributes:(NSDictionary *)newAttributes
currentAttributes:(NSDictionary *)currentAttributes
inheritableAttributes:(const NSArray *)inheritableAtts
{
NSMutableDictionary * dict = [[[NSMutableDictionary alloc] init] autorelease];
for(NSString * key in newAttributes.allKeys) {
// make sure they are the same and
// they are inheritable
if([currentAttributes objectForKey:key] == nil) {
return nil;
}
if([currentAttributes objectForKey:key] != nil &&
[inheritableAtts containsObject:key] &&
[newAttributes[key] isEqualToString:currentAttributes[key]]) {
dict[key] = currentAttributes[key];
}
}
// nothing to return, kill it
if(dict.count == 0) {
return nil;
}
return dict;
}
- (void)_cleanDef
{
NSXMLElement * defNode = [self defElement];
if(defNode.children == 0) {
NSXMLElement * parent = (NSXMLElement *)defNode.parent;
[parent removeChildAtIndex:defNode.index];
}
}
- (void)_cleanEmptyGroups
{
@autoreleasepool {
// cleanup any groups that are completely useless
NSArray * groups = [_dom nodesForXPath:@"//g" error:nil];
for(NSXMLElement * element in groups) {
NSXMLElement * parent = (NSXMLElement *)element.parent;
if(element.childCount == 0) {
// empty group
[(NSXMLElement *)element.parent removeChildAtIndex:element.index];
} else if(element.attributes.count == 0) {
// no useful data on the group
NSInteger index = element.index;
for(NSXMLElement * child in element.children) {
[(NSXMLElement *)child.parent removeChildAtIndex:child.index];
[parent insertChild:child
atIndex:index++];
}
[parent removeChildAtIndex:element.index];
}
}
}
}
- (void)_compressGroups
{
NSArray * groups = [_dom nodesForXPath:@"//g" error:nil];
for(NSXMLElement * group in groups) {
// whats the next group?
if(group.parent == nil) {
continue;
}
// compare each group with its next sibling
NSXMLElement * nextGroup = (NSXMLElement *)group.nextSibling;
while([self compareElement:group withElement:nextGroup]) {
// move each child into the older group
for(NSXMLElement * child in nextGroup.children) {
[nextGroup removeChildAtIndex:child.index];
[group addChild:child];
}
// remove the newer
NSXMLElement * n = nextGroup;
nextGroup = (NSXMLElement *)nextGroup.nextSibling;
[(NSXMLElement *)n.parent removeChildAtIndex:n.index];
}
}
}
- (void)_collapseGroups
{
NSArray * groups = [_dom nodesForXPath:@"//g" error:nil];
const NSArray * inheritable = IJSVGInheritableAttributes();
for(NSXMLElement * group in groups) {
// dont do anything due to it being referenced
if([group attributeForName:@"id"] != nil) {
return;
}
if(group.attributes.count != 0 && group.children.count == 1) {
// grab the first child as its a loner
NSXMLElement * child = (NSXMLElement *)group.children[0];
if([child attributeForName:@"transform"] != nil) {
continue;
}
for(NSXMLNode * gAttribute in group.attributes) {
// if it just doesnt have the attriute, just add it
if([child attributeForName:gAttribute.name] == NO) {
// remove first, or throws a wobbly
[group removeAttributeForName:gAttribute.name];
[child addAttribute:gAttribute];
} else if([gAttribute.name isEqualToString:@"transform"]) {
// transform requires concatination
NSXMLNode * childTransform = [child attributeForName:@"transform"];
childTransform.stringValue = [NSString stringWithFormat:@"%@ %@",
gAttribute.stringValue, childTransform.stringValue];
} else if([inheritable containsObject:gAttribute.name] == NO) {
// if its not inheritable, only remove it if its not equal
NSXMLNode * aAtt = [child attributeForName:gAttribute.name];
if(aAtt == nil || (aAtt != nil && [aAtt.stringValue isEqualToString:gAttribute.stringValue] == NO)) {
continue;
}
}
[group removeAttributeForName:gAttribute.name];
}
// remove the group as its useless!
if(group.attributes.count == 0) {
[child detach];
[(NSXMLElement *)group.parent replaceChildAtIndex:group.index
withNode:child];
}
}
}
}
- (BOOL)compareElement:(NSXMLElement *)element
withElement:(NSXMLElement *)anotherElement
{
// not a matching element
if([element.name isEqualToString:anotherElement.name] == NO ||
element.attributes.count != anotherElement.attributes.count) {
return NO;
}
// compare attributes
for(NSXMLNode * attribute in element.attributes) {
NSString * compareString = [anotherElement attributeForName:attribute.name].stringValue;
if([attribute.stringValue isEqualToString:compareString] == NO) {
return NO;
}
}
return YES;
}
- (void)_convertUseElements
{
@autoreleasepool {
NSArray * paths = [_dom nodesForXPath:@"//path"
error:nil];
NSCountedSet * set = [[[NSCountedSet alloc] init] autorelease];
for(NSXMLElement * element in paths) {
[set addObject:[element attributeForName:@"d"].stringValue];
}
NSMutableDictionary * defs = [[[NSMutableDictionary alloc] init] autorelease];
// now actually compute them
for(NSXMLElement * element in paths) {
NSString * data = [element attributeForName:@"d"].stringValue;
if([set countForObject:data] == 1) {
continue;
}
// at this point, we know the path is being used more then once
NSXMLElement * defParentElement = nil;
if((defParentElement = [defs objectForKey:data]) == nil) {
// create the def
NSXMLElement * element = [[[NSXMLElement alloc] init] autorelease];
element.name = @"path";
NSDictionary * atts = @{@"d":data,
@"id":[self generateID]};
IJSVGApplyAttributesToElement(atts, element);
// store it against the def
defs[data] = element;
defParentElement = element;
}
// we know at this point, we need to swap out the path to a use
NSXMLElement * use = [[[NSXMLElement alloc] init] autorelease];
use.name = @"use";
// grab the id
NSString * pathId = [defParentElement attributeForName:@"id"].stringValue;
NSXMLNode * useAttribute = [[[NSXMLNode alloc] initWithKind:NSXMLAttributeKind] autorelease];
useAttribute.name = @"xlink:href";
useAttribute.stringValue = IJSVGHash(pathId);
[use addAttribute:useAttribute];
// remove the d attribute
for(NSXMLNode * attribute in element.attributes) {
if([attribute.name isEqualToString:@"d"]) {
continue;
}
[element removeAttributeForName:attribute.name];
[use addAttribute:attribute];
}
// swap it out
[(NSXMLElement *)element.parent replaceChildAtIndex:element.index
withNode:use];
}
// add the defs back in
NSXMLElement * def = [self defElement];
for(NSXMLElement * defElement in defs.allValues) {
[def addChild:defElement];
}
}
}
- (void)_recursiveParseFromLayer:(IJSVGLayer *)layer
intoElement:(NSXMLElement *)element
{
// is a shape
if([layer class] == [IJSVGShapeLayer class]) {
NSXMLElement * child = [self elementForShape:(IJSVGShapeLayer *)layer
fromParent:element];
if(child != nil) {
[element addChild:child];
}
} else if([layer isKindOfClass:[IJSVGImageLayer class]]) {
NSXMLElement * child = [self elementForImage:(IJSVGImageLayer *)layer
fromParent:element];
if(child != nil) {
[element addChild:child];
}
} else if([layer isKindOfClass:[IJSVGGroupLayer class]]) {
// assume its probably a group?
NSXMLElement * child = [self elementForGroup:layer
fromParent:element];
if(child != nil) {
[element addChild:child];
}
}
}
- (void)applyTransformToElement:(NSXMLElement *)element
fromLayer:(IJSVGLayer *)layer
{
CGAffineTransform transform = layer.affineTransform;
if(CGAffineTransformEqualToTransform(transform, CGAffineTransformIdentity) == YES) {
return;
}
// append the string
NSString * transformStr = [IJSVGTransform affineTransformToSVGMatrixString:transform];
// apply it to the node
IJSVGApplyAttributesToElement(@{@"transform":transformStr},element);
}
- (NSXMLElement *)elementForGroup:(IJSVGLayer *)layer
fromParent:(NSXMLElement *)parent
{
// create the element
NSXMLElement * e = [[[NSXMLElement alloc] init] autorelease];
e.name = @"g";
// stick defaults
[self applyDefaultsToElement:e
fromLayer:layer];
// add group children
for(IJSVGLayer * childLayer in layer.sublayers) {
[self _recursiveParseFromLayer:childLayer
intoElement:e];
}
return e;
}
- (NSString *)base64EncodedStringFromCGImage:(CGImageRef)image
{
if(image == nil) {
return nil;
}
// convert the CGImage into an NSImage
NSBitmapImageRep * rep = [[[NSBitmapImageRep alloc] initWithCGImage:image] autorelease];
// work out the data
NSData * data = [rep representationUsingType:NSBitmapImageFileTypePNG
properties:@{}];
NSString * base64String = [data base64EncodedStringWithOptions:0];
return [@"data:image/png;base64," stringByAppendingString:base64String];
}
- (void)applyPatternFromLayer:(IJSVGPatternLayer *)layer
parentLayer:(IJSVGLayer *)parentLayer
stroke:(BOOL)stroke
toElement:(NSXMLElement *)element
{
// now we need the pattern
IJSVGGroupLayer * patternLayer = (IJSVGGroupLayer *)layer.pattern;
NSXMLElement * patternElement = [self elementForGroup:patternLayer
fromParent:nil];
patternElement.name = @"pattern";
NSMutableDictionary * dict = [[[NSMutableDictionary alloc] init] autorelease];
dict[@"id"] = [self generateID];
dict[@"width"] = IJSVGShortFloatString(layer.patternNode.width.value);
dict[@"height"] = IJSVGShortFloatString(layer.patternNode.height.value);
// sort out x and y position
IJSVGUnitLength * x = layer.patternNode.x;
IJSVGUnitLength * y = layer.patternNode.y;
if(x.value != 0) {
dict[@"x"] = [layer.patternNode.x stringValue];
}
if(y.value != 0) {
dict[@"y"] = [layer.patternNode.y stringValue];
}
IJSVGApplyAttributesToElement(dict, patternElement);
[[self defElement] addChild:patternElement];
// now the use statement
NSXMLElement * useElement = [[[NSXMLElement alloc] init] autorelease];
useElement.name = @"use";
// now add the fill
NSDictionary * aDict = nil;
if(stroke == NO) {
aDict = @{@"fill":IJSVGHashURL([patternElement attributeForName:@"id"].stringValue)};
IJSVGApplyAttributesToElement(aDict, element);
// fill opacity
if(patternLayer.opacity != 1.f) {
IJSVGApplyAttributesToElement(@{@"fill-opacity":IJSVGShortFloatString(patternLayer.opacity)}, element);
}
} else {
aDict = @{@"stroke":IJSVGHashURL([patternElement attributeForName:@"id"].stringValue)};
IJSVGApplyAttributesToElement(aDict, element);
}
}
- (void)applyGradientFromLayer:(IJSVGGradientLayer *)layer
parentLayer:(IJSVGLayer *)parentLayer
stroke:(BOOL)stroke
toElement:(NSXMLElement *)element
{
IJSVGGradient * gradient = layer.gradient;
NSString * gradKey = [self generateID];
NSXMLElement * gradientElement = [[[NSXMLElement alloc] init] autorelease];
// work out linear gradient
if([gradient isKindOfClass:[IJSVGLinearGradient class]]) {
IJSVGLinearGradient * lGradient = (IJSVGLinearGradient *)gradient;
gradientElement.name = @"linearGradient";
NSDictionary * dict = @{@"id":gradKey,
@"x1":lGradient.x1.stringValue,
@"y1":lGradient.y1.stringValue,
@"x2":lGradient.x2.stringValue,
@"y2":lGradient.y2.stringValue};
// give it the attibutes
IJSVGApplyAttributesToElement(dict, gradientElement);
} else {
// assume radial
IJSVGRadialGradient * rGradient = (IJSVGRadialGradient *)gradient;
gradientElement.name = @"radialGradient";
NSDictionary * dict = @{@"id":gradKey,
@"cx":rGradient.cx.stringValue,
@"cy":rGradient.cy.stringValue,
@"fx":rGradient.fx.stringValue,
@"fy":rGradient.fy.stringValue,
@"r":rGradient.radius.stringValue};
// give it the attributes
IJSVGApplyAttributesToElement(dict, gradientElement);
}
// apply the units
if(layer.gradient.units == IJSVGUnitUserSpaceOnUse) {
IJSVGApplyAttributesToElement(@{@"gradientUnits":@"userSpaceOnUse"},
gradientElement);
}
// add the stops
NSGradient * grad = layer.gradient.gradient;
NSInteger noStops = grad.numberOfColorStops;
for(NSInteger i = 0; i < noStops; i++) {
// grab each color from the gradient
NSColor * aColor = nil;
CGFloat location;
[grad getColor:&aColor
location:&location
atIndex:i];
// create the stop element
NSXMLElement * stop = [[[NSXMLElement alloc] init] autorelease];
stop.name = @"stop";
NSMutableDictionary * atts = [[[NSMutableDictionary alloc] init] autorelease];
atts[@"offset"] = [NSString stringWithFormat:@"%g%%",(location*100)];
// add the color
NSString * stopColor = [IJSVGColor colorStringFromColor:aColor
forceHex:YES
allowShorthand:YES];
if([stopColor isEqualToString:@"#000000"] == NO) {
atts[@"stop-color"] = stopColor;
}
// we need to work out the color at this point, annoyingly...
CGFloat opacity = aColor.alphaComponent;
// is opacity is equal to 1, no need to add it as spec
// defaults opacity to 1 anyway :)
if(opacity != 1.f) {
atts[@"stop-opacity"] = IJSVGShortFloatStringWithPrecision(opacity, 2);
}
// att the attributes
IJSVGApplyAttributesToElement(atts, stop);
// append the stop the gradient
[gradientElement addChild:stop];
}
// append it to the defs
[[self defElement] addChild:gradientElement];
// work out the transform
NSArray * transforms = layer.gradient.transforms;
if(transforms.count != 0.f) {
CGAffineTransform transform = IJSVGConcatTransforms(transforms);
NSString * transformString = [IJSVGTransform affineTransformToSVGMatrixString:transform];
IJSVGApplyAttributesToElement(@{@"gradientTransform":transformString}, gradientElement);
}
// add it to the element passed in
if(stroke == NO) {
IJSVGApplyAttributesToElement(@{@"fill":IJSVGHashURL(gradKey)}, element);
// fill opacity
if(layer.opacity != 1.f) {
IJSVGApplyAttributesToElement(@{@"fill-opacity":IJSVGShortFloatStringWithPrecision(layer.opacity,2)}, element);
}
} else {
IJSVGApplyAttributesToElement(@{@"stroke":IJSVGHashURL(gradKey)}, element);
}
}
- (CGAffineTransform)affineTransformFromTransforms:(NSArray<IJSVGTransform *> *)transforms
{
CGAffineTransform t = CGAffineTransformIdentity;
for(IJSVGTransform * transform in transforms) {
t = CGAffineTransformConcat( t, [transform CGAffineTransform]);
}
return t;
}
- (NSXMLElement *)elementForImage:(IJSVGImageLayer *)layer
fromParent:(NSXMLElement *)parent
{
NSString * base64String = [self base64EncodedStringFromCGImage:(CGImageRef)layer.contents];
if(base64String == nil || layer.contents == nil) {
return nil;
}
// image element for the SVG
NSXMLElement * imageElement = [[[NSXMLElement alloc] init] autorelease];
imageElement.name = @"image";
NSMutableDictionary * dict = [[[NSMutableDictionary alloc] init] autorelease];
dict[@"id"] = [self generateID];
dict[@"width"] = IJSVGShortFloatString(layer.frame.size.width);
dict[@"height"] = IJSVGShortFloatString(layer.frame.size.height);
dict[@"xlink:href"] = base64String;
// work out any position
if(layer.frame.origin.x != 0.f) {
dict[@"x"] = IJSVGShortFloatString(layer.frame.origin.x);
}
if(layer.frame.origin.y != 0.f) {
dict[@"y"] = IJSVGShortFloatString(layer.frame.origin.y);
}
// add the attributes
IJSVGApplyAttributesToElement(dict, imageElement);
return imageElement;
}
- (NSXMLElement *)elementForShape:(IJSVGShapeLayer *)layer
fromParent:(NSXMLElement *)parent
{
NSXMLElement * e = [[[NSXMLElement alloc] init] autorelease];
e.name = @"path";
CGPathRef path = layer.path;
// copy the path as we want to translate
CGAffineTransform trans = CGAffineTransformMakeTranslation(layer.originalPathOrigin.x,
layer.originalPathOrigin.y);
CGPathRef transformPath = CGPathCreateCopyByTransformingPath(path, &trans);
NSMutableDictionary * dict = [[[NSMutableDictionary alloc] init] autorelease];
// path
dict[@"d"] = [self pathFromCGPath:transformPath];
CGPathRelease(transformPath);
// work out even odd rule
if([layer.fillRule isEqualToString:kCAFillRuleNonZero] == NO) {
dict[@"fill-rule"] = @"evenodd";
}
// fill color
if(layer.fillColor != nil) {
NSColor * fillColor = [NSColor colorWithCGColor:layer.fillColor];
NSString * colorString = [IJSVGColor colorStringFromColor:fillColor];
// could be none
if(colorString != nil) {
dict[@"fill"] = colorString;
}
}
// is there a gradient fill?
if(layer.gradientFillLayer != nil) {
[self applyGradientFromLayer:layer.gradientFillLayer
parentLayer:(IJSVGLayer *)layer
stroke:NO
toElement:e];
}
// is there a pattern?
if(layer.patternFillLayer != nil) {
[self applyPatternFromLayer:layer.patternFillLayer
parentLayer:(IJSVGLayer *)layer
stroke:NO
toElement:e];
}
// is there a stroke layer?
if(layer.strokeLayer != nil) {
// check the type
IJSVGStrokeLayer * strokeLayer = layer.strokeLayer;
if([strokeLayer isKindOfClass:[IJSVGShapeLayer class]]) {
// stroke
if(strokeLayer.lineWidth != 0.f) {
dict[@"stroke-width"] = IJSVGShortFloatString(strokeLayer.lineWidth);
}
// stroke gradient
if(layer.gradientStrokeLayer != nil) {
[self applyGradientFromLayer:layer.gradientStrokeLayer
parentLayer:(IJSVGPatternLayer *)layer
stroke:YES
toElement:e];
} else if(layer.patternStrokeLayer != nil) {
// stroke pattern
[self applyPatternFromLayer:layer.patternStrokeLayer
parentLayer:(IJSVGPatternLayer *)layer
stroke:YES
toElement:e];
} else if(strokeLayer.strokeColor != nil) {
NSColor * strokeColor = [NSColor colorWithCGColor:strokeLayer.strokeColor];
NSString * strokeColorString = [IJSVGColor colorStringFromColor:strokeColor];
// could be none
if(strokeColorString != nil) {
dict[@"stroke"] = strokeColorString;
if([strokeColorString isEqualToString:@"none"] == YES) {
// remove the stroke width as its completely useless
[dict removeObjectForKey:@"stroke-width"];
}
}
}
// work out line cap
if([strokeLayer.lineCap isEqualToString:kCALineCapButt] == NO) {
NSString * capStyle = nil;
if([strokeLayer.lineCap isEqualToString:kCALineCapRound]) {
capStyle = @"round";
} else if([strokeLayer.lineCap isEqualToString:kCALineCapSquare]) {
capStyle = @"square";
}
if(capStyle != nil) {
dict[@"stroke-linecap"] = capStyle;
}
}
// work out line join
if([strokeLayer.lineJoin isEqualToString:kCALineJoinMiter] == NO) {
NSString * joinStyle = nil;
if([strokeLayer.lineJoin isEqualToString:kCALineJoinBevel]) {
joinStyle = @"bevel";
} else if([strokeLayer.lineJoin isEqualToString:kCALineJoinRound]) {
joinStyle = @"round";
}
if(joinStyle != nil) {
dict[@"stroke-linejoin"] = joinStyle;
}
}
// work out dash offset...
if(strokeLayer.lineDashPhase != 0.f) {
dict[@"stroke-dashoffset"] = IJSVGShortFloatString(strokeLayer.lineDashPhase);
}
// work out dash array
if(strokeLayer.lineDashPattern.count != 0) {
dict[@"stroke-dasharray"] = [strokeLayer.lineDashPattern componentsJoinedByString:@" "];
}
}
}
// apply the attributes
IJSVGApplyAttributesToElement(dict, e);
// apple defaults
[self applyDefaultsToElement:e
fromLayer:(IJSVGLayer *)layer];
return e;
}
- (void)applyDefaultsToElement:(NSXMLElement *)element
fromLayer:(IJSVGLayer *)layer
{
NSMutableDictionary * dict = [[[NSMutableDictionary alloc] init] autorelease];
// opacity
if(layer.opacity != 1.f) {
dict[@"opacity"] = IJSVGShortFloatStringWithPrecision(layer.opacity,2);
}
// blendmode - we only every apply a stylesheet blend mode
NSMutableDictionary * style = [[[NSMutableDictionary alloc] init] autorelease];
if(layer.blendingMode != kCGBlendModeNormal) {
NSString * str = [IJSVGUtils mixBlendingModeForBlendMode:(IJSVGBlendMode)layer.blendingMode];
if(str != nil) {
style[@"mix-blend-mode"] = str;
}
}
// hidden?
if(layer.isHidden) {
style[@"display"] = @"none";
}
if(style.count != 0) {
NSMutableString * styleString = [[[NSMutableString alloc] init] autorelease];
for(NSString * styleKey in style.allKeys) {
NSString * format = [NSString stringWithFormat:@"%@:%@;",styleKey, style[styleKey]];
[styleString appendString:format];
}
dict[@"style"] = styleString;
}
// add atttributes
IJSVGApplyAttributesToElement(dict, element);
// apply transforms
[self applyTransformToElement:element
fromLayer:layer];
// add any masks...
if(layer.mask != nil) {
[self applyMaskToElement:element
fromLayer:layer];
}
}
- (void)applyMaskToElement:(NSXMLElement *)element
fromLayer:(IJSVGLayer *)layer
{
// create the element
NSXMLElement * mask = [[[NSXMLElement alloc] init] autorelease];
mask.name = @"mask";
// create the key
NSString * maskKey = [self generateID];
NSMutableDictionary * dict = [[[NSMutableDictionary alloc] init] autorelease];
dict[@"id"] = maskKey;
dict[@"maskContentUnits"] = @"userSpaceOnUse";
dict[@"maskUnits"] = @"objectBoundingBox";
if(layer.mask.frame.origin.x != 0.f) {
dict[@"x"] = IJSVGShortFloatString(layer.mask.frame.origin.x);
}
if(layer.mask.frame.origin.y != 0.f) {
dict[@"y"] = IJSVGShortFloatString(layer.mask.frame.origin.y);
}
IJSVGApplyAttributesToElement(dict, mask);
// add the cool stuff
[self _recursiveParseFromLayer:(IJSVGLayer *)layer.mask
intoElement:mask];
// add mask id to element
IJSVGApplyAttributesToElement(@{@"mask":IJSVGHashURL(maskKey)}, element);
// add it defs
[[self defElement] addChild:mask];
}
- (NSString *)SVGString
{
NSXMLNodeOptions options = NSXMLNodePrettyPrint;
if((_options & IJSVGExporterOptionCompressOutput) != 0) {
options = NSXMLNodeOptionsNone;
}
return [_dom XMLStringWithOptions:options];
}
- (NSData *)SVGData
{
return [[self SVGString] dataUsingEncoding:NSUTF8StringEncoding];
}
#pragma mark CGPath stuff
- (NSString *)pathFromCGPath:(CGPathRef)path
{
// string to store the path in
NSArray * instructions = [IJSVGExporterPathInstruction instructionsFromPath:path];
// work out what to do...
if((_options & IJSVGExporterOptionCleanupPaths) != 0) {
[IJSVGExporterPathInstruction convertInstructionsToRelativeCoordinates:instructions];
}
return [IJSVGExporterPathInstruction pathStringFromInstructions:instructions];
}
void IJSVGExporterPathCaller(void * info, const CGPathElement * pathElement) {
IJSVGCGPathHandler handler = (IJSVGCGPathHandler)info;
handler(pathElement);
};
- (void)sortAttributesOnElement:(NSXMLElement *)element
{
const NSArray * order = @[@"id",@"width",@"height",@"x",@"x1",@"x2",
@"y",@"y1",@"y2",@"cx",@"cy",@"r",@"fill",
@"stroke",@"marker",@"d",@"points",@"transform",
@"gradientTransform", @"xlink:href"];
// grab the attributes
NSArray<NSXMLNode *>* attributes = element.attributes;
NSInteger count = attributes.count;
// sort the attributes using a custom sort
NSArray * sorted = [attributes sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) {
// tell compiler we are nodes
NSXMLNode * attribute1 = (NSXMLNode *)obj1;
NSXMLNode * attribute2 = (NSXMLNode *)obj2;
// base index
float aIndex = count;
float bIndex = count;
// loop around each order string
for(NSInteger i = 0; i < order.count; i++) {
if([attribute1.name isEqualToString:order[i]]) {
aIndex = i;
} else if([attribute1.name rangeOfString:[order[i] stringByAppendingString:@"-"]].location == 0) {
aIndex = i + .5;
}
if([attribute2.name isEqualToString:order[i]]) {
bIndex = i;
} else if([attribute2.name rangeOfString:[order[i] stringByAppendingString:@"-"]].location == 0) {
bIndex = i + .5;
}
}
// return the comparison set
if(aIndex != bIndex) {
if(aIndex > bIndex) {
return NSOrderedDescending;
} else {
return NSOrderedAscending;
}
}
return [attribute1.name compare:attribute2.name];
}];
// remove all attributes
for(NSXMLNode * node in attributes) {
[element removeAttributeForName:node.name];
}
// add them back on in order
for(NSXMLNode * attribute in sorted) {
[element addAttribute:attribute];
}
}
@end