Files
IJSVG/source/IJSVGExporter.m

1369 lines
46 KiB
Objective-C

//
// IJSVGExporter.m
// IJSVGExample
//
// Created by Curtis Hard on 06/01/2017.
// Copyright © 2017 Curtis Hard. All rights reserved.
//
#import "IJSVG.h"
#import "IJSVGExporter.h"
#import "IJSVGExporterPathInstruction.h"
#import "IJSVGGradientLayer.h"
#import "IJSVGGroupLayer.h"
#import "IJSVGImageLayer.h"
#import "IJSVGLinearGradient.h"
#import "IJSVGMath.h"
#import "IJSVGPatternLayer.h"
#import "IJSVGRadialGradient.h"
#import "IJSVGShapeLayer.h"
#import "IJSVGStrokeLayer.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;
BOOL IJSVGExporterHasOption(IJSVGExporterOptions options, NSInteger option)
{
return (options & option) != 0;
};
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
{
(void)([_scaledRootNode release]), _scaledRootNode = nil;
(void)([_svg release]), _svg = nil;
(void)([_dom release]), _dom = nil;
(void)([_defElement release]), _defElement = nil;
(void)([title release]), title = nil;
(void)([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
if ((_options & IJSVGExporterOptionRemoveComments) == 0) {
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;
IJSVGColorList* sheet = layer.gradient.colorList;
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];
if (sheet != nil) {
aColor = [sheet proposedColorForColor:aColor];
}
// 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
IJSVGColorStringOptions options = IJSVGColorStringOptionForceHEX | IJSVGColorStringOptionAllowShortHand;
NSString* stopColor = [IJSVGColor colorStringFromColor:aColor
options:options];
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;
}
- (IJSVGColorStringOptions)colorOptions
{
IJSVGColorStringOptions options = IJSVGColorStringOptionDefault;
if ((_options & IJSVGExporterOptionColorAllowRRGGBBAA) != 0) {
options |= IJSVGColorStringOptionAllowRRGGBBAA;
}
return options;
}
- (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
options:[self colorOptions]];
// 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
options:[self colorOptions]];
// 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