diff --git a/pxSVG.xcodeproj/project.pbxproj b/pxSVG.xcodeproj/project.pbxproj index 10e79a5..d6fc7b5 100644 --- a/pxSVG.xcodeproj/project.pbxproj +++ b/pxSVG.xcodeproj/project.pbxproj @@ -90,6 +90,9 @@ D07DB4FE1B026FC1004733DE /* no_sympathies.svg in Resources */ = {isa = PBXBuildFile; fileRef = D07DB4F31B026FC1004733DE /* no_sympathies.svg */; }; D07DB4FF1B026FC1004733DE /* share.svg in Resources */ = {isa = PBXBuildFile; fileRef = D07DB4F41B026FC1004733DE /* share.svg */; }; D07DB5001B026FC1004733DE /* support.svg in Resources */ = {isa = PBXBuildFile; fileRef = D07DB4F51B026FC1004733DE /* support.svg */; }; + D0DCC4D11B0281F700586BF3 /* pxSVGObject.m in Sources */ = {isa = PBXBuildFile; fileRef = D0DCC4D01B0281F700586BF3 /* pxSVGObject.m */; }; + D0DCC4D41B02827E00586BF3 /* pxSVGGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = D0DCC4D31B02827E00586BF3 /* pxSVGGroup.m */; }; + D0DCC4D71B0284B200586BF3 /* pxSVGPath.m in Sources */ = {isa = PBXBuildFile; fileRef = D0DCC4D61B0284B200586BF3 /* pxSVGPath.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -208,6 +211,12 @@ D07DB4F31B026FC1004733DE /* no_sympathies.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = no_sympathies.svg; sourceTree = ""; }; D07DB4F41B026FC1004733DE /* share.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = share.svg; sourceTree = ""; }; D07DB4F51B026FC1004733DE /* support.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = support.svg; sourceTree = ""; }; + D0DCC4CF1B0281F700586BF3 /* pxSVGObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pxSVGObject.h; sourceTree = ""; }; + D0DCC4D01B0281F700586BF3 /* pxSVGObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = pxSVGObject.m; sourceTree = ""; }; + D0DCC4D21B02827E00586BF3 /* pxSVGGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pxSVGGroup.h; sourceTree = ""; }; + D0DCC4D31B02827E00586BF3 /* pxSVGGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = pxSVGGroup.m; sourceTree = ""; }; + D0DCC4D51B0284B200586BF3 /* pxSVGPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pxSVGPath.h; sourceTree = ""; }; + D0DCC4D61B0284B200586BF3 /* pxSVGPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = pxSVGPath.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -342,6 +351,7 @@ D07DB4BA1B024A45004733DE /* pxSVG */ = { isa = PBXGroup; children = ( + D0DCC4CE1B0281DA00586BF3 /* SVG */, D07DB4DC1B025350004733DE /* Parsers */, D07DB4D31B024A91004733DE /* Views */, D07DB4BB1B024A45004733DE /* pxSVG.h */, @@ -373,6 +383,19 @@ name = Parsers; sourceTree = ""; }; + D0DCC4CE1B0281DA00586BF3 /* SVG */ = { + isa = PBXGroup; + children = ( + D0DCC4CF1B0281F700586BF3 /* pxSVGObject.h */, + D0DCC4D01B0281F700586BF3 /* pxSVGObject.m */, + D0DCC4D21B02827E00586BF3 /* pxSVGGroup.h */, + D0DCC4D31B02827E00586BF3 /* pxSVGGroup.m */, + D0DCC4D51B0284B200586BF3 /* pxSVGPath.h */, + D0DCC4D61B0284B200586BF3 /* pxSVGPath.m */, + ); + name = SVG; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -546,6 +569,9 @@ D07DB4D61B024AAA004733DE /* pxSVGLayer.m in Sources */, D07DB4EA1B026BF8004733DE /* pxSVGRenderPath.m in Sources */, D07DB4E41B026959004733DE /* pxXMLNode.m in Sources */, + D0DCC4D71B0284B200586BF3 /* pxSVGPath.m in Sources */, + D0DCC4D11B0281F700586BF3 /* pxSVGObject.m in Sources */, + D0DCC4D41B02827E00586BF3 /* pxSVGGroup.m in Sources */, D07DB4D91B024AC2004733DE /* pxSVGView.m in Sources */, D07DB4DF1B025366004733DE /* pxSVGImage.m in Sources */, ); @@ -707,6 +733,7 @@ D07DB4381B024475004733DE /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; D07DB4CA1B024A45004733DE /* Build configuration list for PBXNativeTarget "pxSVG" */ = { isa = XCConfigurationList; @@ -715,6 +742,7 @@ D07DB4CC1B024A45004733DE /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/pxSVG/pxSVGGroup.h b/pxSVG/pxSVGGroup.h new file mode 100644 index 0000000..e82f245 --- /dev/null +++ b/pxSVG/pxSVGGroup.h @@ -0,0 +1,13 @@ +// +// pxSVGGroup.h +// pxSVG +// +// Created by Yury Popov on 12.05.15. +// Copyright (c) 2015 PhoeniX. All rights reserved. +// + +#import "pxSVGObject.h" + +@interface pxSVGGroup : pxSVGObject +@property NSArray *subnodes; +@end diff --git a/pxSVG/pxSVGGroup.m b/pxSVG/pxSVGGroup.m new file mode 100644 index 0000000..8ce05e6 --- /dev/null +++ b/pxSVG/pxSVGGroup.m @@ -0,0 +1,13 @@ +// +// pxSVGGroup.m +// pxSVG +// +// Created by Yury Popov on 12.05.15. +// Copyright (c) 2015 PhoeniX. All rights reserved. +// + +#import "pxSVGGroup.h" + +@implementation pxSVGGroup + +@end diff --git a/pxSVG/pxSVGObject.h b/pxSVG/pxSVGObject.h new file mode 100644 index 0000000..89cf6b6 --- /dev/null +++ b/pxSVG/pxSVGObject.h @@ -0,0 +1,21 @@ +// +// pxSVGObject.h +// pxSVG +// +// Created by Yury Popov on 12.05.15. +// Copyright (c) 2015 PhoeniX. All rights reserved. +// + +#import + +@interface pxSVGObject : NSObject +- (void) loadAttributes:(NSDictionary*)attributes; +- (void) setSubnodes:(NSArray*)subnodes; +@property NSString *id; +@property NSArray *animations; +@property UIColor *fillColor; +@property UIColor *strokeColor; +@property CGFloat strokeWidth; +@property CGFloat opacity; +@property CATransform3D transform; +@end diff --git a/pxSVG/pxSVGObject.m b/pxSVG/pxSVGObject.m new file mode 100644 index 0000000..a71dca6 --- /dev/null +++ b/pxSVG/pxSVGObject.m @@ -0,0 +1,106 @@ +// +// pxSVGObject.m +// pxSVG +// +// Created by Yury Popov on 12.05.15. +// Copyright (c) 2015 PhoeniX. All rights reserved. +// + +#import "pxSVGObject.h" + +@implementation pxSVGObject ++ (CATransform3D) transformFromString:(NSString*)string +{ + CATransform3D tr = CATransform3DIdentity; + if (!string.length) return tr; + NSScanner *sc = [NSScanner scannerWithString:string]; + sc.charactersToBeSkipped = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + NSString *op; double p[6]; int i; + while (!sc.atEnd) { + [sc scanUpToString:@"(" intoString:&op]; + [sc scanString:@"(" intoString:nil]; + i=0; + while ([sc scanDouble:&p[i++]]) [sc scanCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@" ,"] intoString:nil]; + i--; + [sc scanString:@")" intoString:nil]; + + if ([op isEqualToString:@"scale"]) { + switch (i) { + case 1: + tr = CATransform3DScale(tr, p[0], p[0], 1); + break; + case 2: + tr = CATransform3DScale(tr, p[0], p[1], 1); + break; + default: NSLog(@"Invalid number of operands for %@: %@",op,@(i)); break; + } + } else if ([op isEqualToString:@"translate"]) { + switch (i) { + case 1: + tr = CATransform3DTranslate(tr, p[0], 0, 0); + break; + case 2: + tr = CATransform3DTranslate(tr, p[0], p[1], 0); + break; + default: NSLog(@"Invalid number of operands for %@: %@",op,@(i)); break; + } + } else if ([op isEqualToString:@"matrix"]) { + switch (i) { + case 6: + tr = CATransform3DConcat(tr, CATransform3DMakeAffineTransform(CGAffineTransformMake(p[0], p[1], p[2], p[3], p[4], p[5]))); + break; + default: NSLog(@"Invalid number of operands for %@: %@",op,@(i)); break; + } + } else { + NSLog(@"Unknown transform: %@",op); + } + } + return tr; +} +- (UIColor*) colorWithSVGColor:(NSString*)string +{ + if (!string) return nil; + string = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if ([string isEqualToString:@"none"]) return nil; + if ([string isEqualToString:@"black"]) return [UIColor blackColor]; + if ([string isEqualToString:@"white"]) return [UIColor whiteColor]; + NSScanner *sc = [NSScanner scannerWithString:[string lowercaseString]]; + if ([sc scanString:@"#" intoString:nil]) { + unsigned int cl; + [sc scanHexInt:&cl]; + float r,g,b; + switch (string.length) { + case 4: + b = (cl & 0xF)<<4; cl >>= 4; + g = (cl & 0xF)<<4; cl >>= 4; + r = (cl & 0xF)<<4; cl >>= 4; + break; + case 7: + b = cl & 0xFF; cl >>= 8; + g = cl & 0xFF; cl >>= 8; + r = cl & 0xFF; cl >>= 8; + break; + + default: + NSLog(@"Invalid hex color: %@",string); + return nil; + } + r /= 255.f; + g /= 255.f; + b /= 255.f; + return [UIColor colorWithRed:r green:g blue:b alpha:1]; + } + NSLog(@"Unknown color: %@",string); + return nil; +} +- (void)loadAttributes:(NSDictionary *)attributes +{ + self.id = [attributes objectForKey:@"id"]; + self.fillColor = [self colorWithSVGColor:[attributes objectForKey:@"fill"]]; + self.strokeColor = [self colorWithSVGColor:[attributes objectForKey:@"stroke"]]; + self.strokeWidth = [[attributes objectForKey:@"stroke-width"] doubleValue]; + self.transform = [self.class transformFromString:[attributes objectForKey:@"transform"]]; + self.opacity = [[attributes objectForKey:@"opacity"] doubleValue]; +} +- (void)setSubnodes:(NSArray *)subnodes { } +@end diff --git a/pxSVG/pxSVGPath.h b/pxSVG/pxSVGPath.h new file mode 100644 index 0000000..4ceb72b --- /dev/null +++ b/pxSVG/pxSVGPath.h @@ -0,0 +1,13 @@ +// +// pxSVGPath.h +// pxSVG +// +// Created by Yury Popov on 12.05.15. +// Copyright (c) 2015 PhoeniX. All rights reserved. +// + +#import "pxSVGObject.h" + +@interface pxSVGPath : pxSVGObject +@property UIBezierPath *d; +@end diff --git a/pxSVG/pxSVGPath.m b/pxSVG/pxSVGPath.m new file mode 100644 index 0000000..a068697 --- /dev/null +++ b/pxSVG/pxSVGPath.m @@ -0,0 +1,202 @@ +// +// pxSVGPath.m +// pxSVG +// +// Created by Yury Popov on 12.05.15. +// Copyright (c) 2015 PhoeniX. All rights reserved. +// + +#import "pxSVGPath.h" + +@interface NSScanner (CGPoint) +- (CGPoint) scanCGPoint; +- (CGPoint) scanCGPointWithOffset:(CGPoint)off; +@end + +@implementation NSScanner (CGPoint) +- (CGPoint) scanCGPoint +{ + double x,y; + [self scanString:@"," intoString:nil]; + [self scanDouble:&x]; + [self scanString:@"," intoString:nil]; + [self scanDouble:&y]; + return (CGPoint){x,y}; +} +- (CGPoint)scanCGPointWithOffset:(CGPoint)off +{ + CGPoint p = [self scanCGPoint]; + return (CGPoint){p.x+off.x,p.y+off.y}; +} +@end + +@implementation pxSVGPath + +- (UIBezierPath*)bezierPathWithString:(NSString*)string +{ + static NSCharacterSet *cmds; if (!cmds) cmds = [NSCharacterSet characterSetWithCharactersInString:@"MmCcSsLlHhVvAaZz"]; + NSScanner *scanner = [NSScanner scannerWithString:string]; + [scanner setCharactersToBeSkipped:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *cmd; unichar lastCmd = 0; + UIBezierPath *bp = [UIBezierPath new]; + CGPoint p = CGPointZero, c1, c2; + double d, r, s; + while (!scanner.atEnd) { + if (cmd.length>1) cmd=[cmd substringFromIndex:1]; + else if (![scanner scanCharactersFromSet:cmds intoString:&cmd]) { + if ([cmds characterIsMember:lastCmd]) { + cmd = [NSString stringWithCharacters:&lastCmd length:1]; + } else { + NSLog(@"Unknown character: %@",[string substringWithRange:(NSRange){scanner.scanLocation,1}]); + } + break; + } + lastCmd = [cmd characterAtIndex:0]; + switch ([cmd characterAtIndex:0]) { + case 'M': + [bp moveToPoint:p = [scanner scanCGPoint]]; + break; + case 'm': + [bp moveToPoint:p = [scanner scanCGPointWithOffset:p]]; + break; + case 'C': + c1 = [scanner scanCGPoint]; + c2 = [scanner scanCGPoint]; + [bp addCurveToPoint:p=[scanner scanCGPoint] controlPoint1:c1 controlPoint2:c2]; + break; + case 'c': + c1 = [scanner scanCGPointWithOffset:p]; + c2 = [scanner scanCGPointWithOffset:p]; + [bp addCurveToPoint:p = [scanner scanCGPointWithOffset:p] controlPoint1:c1 controlPoint2:c2]; + break; + case 'S': + c1 = (CGPoint){p.x*2.f-c2.x,p.y*2.f-c2.y}; + c2 = [scanner scanCGPoint]; + p = [scanner scanCGPoint]; + [bp addCurveToPoint:p controlPoint1:c1 controlPoint2:c2]; + break; + case 's': + c1 = (CGPoint){p.x*2.f-c2.x,p.y*2.f-c2.y}; + c2 = [scanner scanCGPointWithOffset:p]; + p = [scanner scanCGPointWithOffset:p]; + [bp addCurveToPoint:p controlPoint1:c1 controlPoint2:c2]; + break; + case 'A': + c2 = [scanner scanCGPoint]; // r + [scanner scanString:@"," intoString:nil]; + [scanner scanDouble:&d]; d *= M_PI; d /= 180.f; // a + [scanner scanString:@"," intoString:nil]; + [scanner scanDouble:nil]; + [scanner scanString:@"," intoString:nil]; + [scanner scanDouble:nil]; + c1 = p; + p = [scanner scanCGPoint]; + c1 = (CGPoint){(c1.x+p.x)/2.f,(c1.y+p.y)/2.f}; + r = sqrt((c2.x-c1.x)*(c2.x-c1.x)+(c2.y-c1.y)*(c2.y-c1.y)); + s = atan2(p.x-c1.x, p.y-c1.y)+M_PI_2; + [bp addArcWithCenter:c1 radius:r startAngle:s endAngle:s+d clockwise:d>0]; + break; + case 'a': + c2 = [scanner scanCGPointWithOffset:p]; // r + [scanner scanString:@"," intoString:nil]; + [scanner scanDouble:&d]; d *= M_PI; d /= 180.f; // a + [scanner scanString:@"," intoString:nil]; + [scanner scanDouble:nil]; + [scanner scanString:@"," intoString:nil]; + [scanner scanDouble:nil]; + c1 = p; + p = [scanner scanCGPointWithOffset:p]; + c1 = (CGPoint){(c1.x+p.x)/2.f,(c1.y+p.y)/2.f}; + r = sqrt((c2.x-c1.x)*(c2.x-c1.x)+(c2.y-c1.y)*(c2.y-c1.y)); + s = atan2(p.x-c1.x, p.y-c1.y)+M_PI_2; + [bp addArcWithCenter:c1 radius:r startAngle:s endAngle:s+d clockwise:d>0]; + break; + case 'V': + [scanner scanDouble:&d]; p.y = d; + [bp addLineToPoint:p]; + break; + case 'v': + [scanner scanDouble:&d]; p.y+= d; + [bp addLineToPoint:p]; + break; + case 'H': + [scanner scanDouble:&d]; p.x = d; + [bp addLineToPoint:p]; + break; + case 'h': + [scanner scanDouble:&d]; p.x+= d; + [bp addLineToPoint:p]; + break; + case 'L': + [bp addLineToPoint:p=[scanner scanCGPoint]]; + break; + case 'l': + [bp addLineToPoint:p=[scanner scanCGPointWithOffset:p]]; + break; + case 'z': + case 'Z': + [bp closePath]; + break; + + default: + NSLog(@"Unknown command: %@",cmd); + return nil; + } + } + return bp; +} + +- (UIBezierPath*)bezierPolygonWithString:(NSString*)string +{ + static NSCharacterSet *cmds; if (!cmds) cmds = [NSCharacterSet characterSetWithCharactersInString:@"MmCcSsLlHhVvAaZz"]; + NSScanner *scanner = [NSScanner scannerWithString:string]; + [scanner setCharactersToBeSkipped:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + UIBezierPath *bp = [UIBezierPath new]; + [bp moveToPoint:[scanner scanCGPoint]]; + while (!scanner.atEnd) { + [bp addLineToPoint:[scanner scanCGPoint]]; + } + [bp closePath]; + return bp; +} + +- (void)loadAttributes:(NSDictionary *)attributes +{ + [super loadAttributes:attributes]; + if ([attributes objectForKey:@"d"]) self.d = [self bezierPathWithString:[attributes objectForKey:@"d"]]; + else if ([attributes objectForKey:@"points"]) self.d = [self bezierPolygonWithString:[attributes objectForKey:@"points"]]; + else if ([attributes objectForKey:@"cx"] && + [attributes objectForKey:@"cy"] && + [attributes objectForKey:@"rx"] && + [attributes objectForKey:@"ry"]) { + CGRect r; + r.size.width = [[attributes objectForKey:@"rx"] doubleValue]; + r.size.height = [[attributes objectForKey:@"rx"] doubleValue]; + r.origin.x = [[attributes objectForKey:@"cx"] doubleValue] - r.size.width; + r.origin.y = [[attributes objectForKey:@"cy"] doubleValue] - r.size.height; + r.size.width *= 2; r.size.height *= 2; + self.d = [UIBezierPath bezierPathWithOvalInRect:r]; + } + else if ([attributes objectForKey:@"cx"] && + [attributes objectForKey:@"cy"] && + [attributes objectForKey:@"r"]) { + CGRect r; + r.size.width = [[attributes objectForKey:@"r"] doubleValue]; + r.size.height = [[attributes objectForKey:@"r"] doubleValue]; + r.origin.x = [[attributes objectForKey:@"cx"] doubleValue] - r.size.width; + r.origin.y = [[attributes objectForKey:@"cy"] doubleValue] - r.size.height; + r.size.width *= 2; r.size.height *= 2; + self.d = [UIBezierPath bezierPathWithOvalInRect:r]; + } + else if ([attributes objectForKey:@"width"] && + [attributes objectForKey:@"height"]) { + CGRect r; + r.size.width = [[attributes objectForKey:@"width"] doubleValue]; + r.size.height = [[attributes objectForKey:@"height"] doubleValue]; + r.origin.x = [[attributes objectForKey:@"x"] doubleValue]; + r.origin.y = [[attributes objectForKey:@"y"] doubleValue]; + r.size.width *= 2; r.size.height *= 2; + self.d = [UIBezierPath bezierPathWithRect:r]; + } else NSLog(@"%@",attributes); +} +@end diff --git a/pxSVG/pxSVGRenderPath.m b/pxSVG/pxSVGRenderPath.m index d657f39..eb28a41 100644 --- a/pxSVG/pxSVGRenderPath.m +++ b/pxSVG/pxSVGRenderPath.m @@ -7,10 +7,12 @@ // #import "pxSVGRenderPath.h" +#import "pxSVGGroup.h" +#import "pxSVGPath.h" @interface pxSVGRenderPath () @property NSDictionary *defs; -//@property pxSVGGroup *root; +@property pxSVGObject *root; @property CGRect bounds; @end @@ -52,6 +54,39 @@ - (instancetype)initWithXML:(pxXMLNode *)xmlNode } else { self.bounds = CGRectNull; } + self.root = [self parseObject:xmlNode]; return self; } +- (pxSVGObject*)parseObject:(pxXMLNode*)node +{ + if ([node.tagName rangeOfString:@":"].location != NSNotFound) return nil; + if ([node.tagName isEqualToString:@"metadata"]) return nil; + Class objClass = pxSVGObject.class; + if ([node.tagName isEqualToString:@"g"]) + objClass = pxSVGGroup.class; + else if ([node.tagName isEqualToString:@"svg"]) + objClass = pxSVGGroup.class; + else if ([node.tagName isEqualToString:@"path"]) + objClass = pxSVGPath.class; + else if ([node.tagName isEqualToString:@"polygon"]) + objClass = pxSVGPath.class; + else if ([node.tagName isEqualToString:@"ellipse"]) + objClass = pxSVGPath.class; + else if ([node.tagName isEqualToString:@"circle"]) + objClass = pxSVGPath.class; + else if ([node.tagName isEqualToString:@"rect"]) + objClass = pxSVGPath.class; + else NSLog(@"Unknown tag: %@",node.tagName); + pxSVGObject *obj = [objClass new]; + [obj loadAttributes:node.attributes]; + if (node.childNodes.count) { + NSMutableArray *subnodes = [NSMutableArray new]; + for (pxXMLNode *n in node.childNodes) { + pxSVGObject *o = [self parseObject:n]; + if (o) [subnodes addObject:o]; + } + [obj setSubnodes:[NSArray arrayWithArray:subnodes]]; + } + return obj; +} @end