/** * Copyright (c) 2015-present, Horcrux. * All rights reserved. * * This source code is licensed under the MIT-style license found in the * LICENSE file in the root directory of this source tree. */ #import "RNSVGPathParser.h" #import #import #import "RNSVGBezierElement.h" @implementation RNSVGPathParser { char prev_cmd; NSUInteger i; NSUInteger l; NSString *s; float _penX; float _penY; float _penDownX; float _penDownY; float _pivotX; float _pivotY; BOOL _valid; BOOL _penDownSet; } - (instancetype)initWithPathString:(NSString *)d { if (self = [super init]) { prev_cmd = ' '; l = [d length]; i = 0; s = d; } return self; } #define NEXT_FLOAT [self parse_list_number] #define NEXT_BOOL [self parse_flag] - (CGPathRef)getPath { CGMutablePathRef path = CGPathCreateMutable(); while (i < l) { [self skip_spaces]; if (i >= l) { break; } bool has_prev_cmd = prev_cmd != ' '; char first_char = [s characterAtIndex:i]; if (!has_prev_cmd && first_char != 'M' && first_char != 'm') { // The first segment must be a MoveTo. RCTLogError(@"UnexpectedData: %@", s); CGPathRelease(path); return nil; } // TODO: simplify bool is_implicit_move_to = false; char cmd = ' '; if ([self is_cmd:first_char]) { is_implicit_move_to = false; cmd = first_char; i += 1; } else if ([self is_number_start:first_char] && has_prev_cmd) { if (prev_cmd == 'Z' || prev_cmd == 'z') { // ClosePath cannot be followed by a number. RCTLogError(@"UnexpectedData: %@", s); CGPathRelease(path); return nil; } if (prev_cmd == 'M' || prev_cmd == 'm') { // 'If a moveto is followed by multiple pairs of coordinates, // the subsequent pairs are treated as implicit lineto commands.' // So we parse them as LineTo. is_implicit_move_to = true; if ([self is_absolute:prev_cmd]) { cmd = 'L'; } else { cmd = 'l'; } } else { is_implicit_move_to = false; cmd = prev_cmd; } } else { RCTLogError(@"UnexpectedData: %@", s); CGPathRelease(path); return nil; } bool absolute = [self is_absolute:cmd]; switch (cmd) { case 'm': { [self move:path x:NEXT_FLOAT y:NEXT_FLOAT]; break; } case 'M': { [self moveTo:path x:NEXT_FLOAT y:NEXT_FLOAT]; break; } case 'l': { [self line:path x:NEXT_FLOAT y:NEXT_FLOAT]; break; } case 'L': { [self lineTo:path x:NEXT_FLOAT y:NEXT_FLOAT]; break; } case 'h': { [self line:path x:NEXT_FLOAT y:0]; break; } case 'H': { [self lineTo:path x:NEXT_FLOAT y:_penY]; break; } case 'v': { [self line:path x:0 y:NEXT_FLOAT]; break; } case 'V': { [self lineTo:path x:_penX y:NEXT_FLOAT]; break; } case 'c': { [self curve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; break; } case 'C': { [self curveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; break; } case 's': { [self smoothCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; break; } case 'S': { [self smoothCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT]; break; } case 'q': { [self quadraticBezierCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT]; break; } case 'Q': { [self quadraticBezierCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT]; break; } case 't': { [self smoothQuadraticBezierCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT]; break; } case 'T': { [self smoothQuadraticBezierCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT]; break; } case 'a': { [self arc:path rx:NEXT_FLOAT ry:NEXT_FLOAT rotation:NEXT_FLOAT outer:NEXT_BOOL clockwise:NEXT_BOOL x:NEXT_FLOAT y:NEXT_FLOAT]; break; } case 'A': { [self arcTo:path rx:NEXT_FLOAT ry:NEXT_FLOAT rotation:NEXT_FLOAT outer:NEXT_BOOL clockwise:NEXT_BOOL x:NEXT_FLOAT y:NEXT_FLOAT]; break; } case 'z': case 'Z': { [self close:path]; break; } default: { RCTLogError(@"UnexpectedData: %@", s); CGPathRelease(path); return nil; } } if (is_implicit_move_to) { if (absolute) { prev_cmd = 'M'; } else { prev_cmd = 'm'; } } else { prev_cmd = cmd; } } return (CGPathRef)CFAutorelease(path); } - (void)move:(CGMutablePathRef)path x:(float)x y:(float)y { [self moveTo:path x:x + _penX y:y + _penY]; } - (void)moveTo:(CGMutablePathRef)path x:(float)x y:(float)y { // RCTLogInfo(@"move x: %f y: %f", x, y); _penDownX = _pivotX = _penX = x; _penDownY = _pivotY = _penY = y; CGPathMoveToPoint(path, nil, x, y); } - (void)line:(CGMutablePathRef)path x:(float)x y:(float)y { [self lineTo:path x:x + _penX y:y + _penY]; } - (void)lineTo:(CGMutablePathRef)path x:(float)x y:(float)y { // RCTLogInfo(@"line x: %f y: %f", x, y); [self setPenDown]; _pivotX = _penX = x; _pivotY = _penY = y; CGPathAddLineToPoint(path, nil, x, y); } - (void)curve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey { [self curveTo:path c1x:c1x + _penX c1y:c1y + _penY c2x:c2x + _penX c2y:c2y + _penY ex:ex + _penX ey:ey + _penY]; } - (void)curveTo:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey { // RCTLogInfo(@"curve c1x: %f c1y: %f c2x: %f c2y: %f ex: %f ey: %f", c1x, c1y, c2x, c2y, ex, ey); _pivotX = c2x; _pivotY = c2y; [self curveToPoint:path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey]; } - (void)curveToPoint:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey { [self setPenDown]; _penX = ex; _penY = ey; CGPathAddCurveToPoint(path, nil, c1x, c1y, c2x, c2y, ex, ey); } - (void)smoothCurve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y ex:(float)ex ey:(float)ey { [self smoothCurveTo:path c1x:c1x + _penX c1y:c1y + _penY ex:ex + _penX ey:ey + _penY]; } - (void)smoothCurveTo:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y ex:(float)ex ey:(float)ey { // RCTLogInfo(@"smoothcurve c1x: %f c1y: %f ex: %f ey: %f", c1x, c1y, ex, ey); float c2x = c1x; float c2y = c1y; c1x = (_penX * 2) - _pivotX; c1y = (_penY * 2) - _pivotY; _pivotX = c2x; _pivotY = c2y; [self curveToPoint:path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey]; } - (void)quadraticBezierCurve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y { [self quadraticBezierCurveTo:path c1x:(float)c1x + _penX c1y:(float)c1y + _penY c2x:(float)c2x + _penX c2y:(float)c2y + _penY]; } - (void)quadraticBezierCurveTo:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y { // RCTLogInfo(@"quad c1x: %f c1y: %f c2x: %f c2y: %f", c1x, c1y, c2x, c2y); _pivotX = c1x; _pivotY = c1y; float ex = c2x; float ey = c2y; c2x = (ex + c1x * 2) / 3; c2y = (ey + c1y * 2) / 3; c1x = (_penX + c1x * 2) / 3; c1y = (_penY + c1y * 2) / 3; [self curveToPoint:path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey]; } - (void)smoothQuadraticBezierCurve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y { [self smoothQuadraticBezierCurveTo:path c1x:c1x + _penX c1y:c1y + _penY]; } - (void)smoothQuadraticBezierCurveTo:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y { // RCTLogInfo(@"smoothquad c1x: %f c1y: %f", c1x, c1y); float c2x = c1x; float c2y = c1y; c1x = (_penX * 2) - _pivotX; c1y = (_penY * 2) - _pivotY; [self quadraticBezierCurveTo:path c1x:c1x c1y:c1y c2x:c2x c2y:c2y]; } - (void)arc:(CGMutablePathRef)path rx:(float)rx ry:(float)ry rotation:(float)rotation outer:(BOOL)outer clockwise:(BOOL)clockwise x:(float)x y:(float)y { [self arcTo:path rx:rx ry:ry rotation:rotation outer:outer clockwise:clockwise x:x + _penX y:y + _penY]; } - (void)arcTo:(CGMutablePathRef)path rx:(float)rx ry:(float)ry rotation:(float)rotation outer:(BOOL)outer clockwise:(BOOL)clockwise x:(float)x y:(float)y { // RCTLogInfo(@"arc rx: %f ry: %f rotation: %f outer: %i clockwise: %i x: %f y: %f", rx, ry, rotation, outer, // clockwise, x, y); float tX = _penX; float tY = _penY; ry = fabsf(ry == 0 ? (rx == 0 ? (y - tY) : rx) : ry); rx = fabsf(rx == 0 ? (x - tX) : rx); if (rx == 0 || ry == 0 || (x == tX && y == tY)) { [self lineTo:path x:x y:y]; return; } float rad = rotation * (float)M_PI / 180; float cosed = cosf(rad); float sined = sinf(rad); x -= tX; y -= tY; // Ellipse Center float cx = cosed * x / 2 + sined * y / 2; float cy = -sined * x / 2 + cosed * y / 2; float rxry = rx * rx * ry * ry; float rycx = ry * ry * cx * cx; float rxcy = rx * rx * cy * cy; float a = rxry - rxcy - rycx; if (a < 0) { a = sqrtf(1 - a / rxry); rx *= a; ry *= a; cx = x / 2; cy = y / 2; } else { a = sqrtf(a / (rxcy + rycx)); if (outer == clockwise) { a = -a; } float cxd = -a * cy * rx / ry; float cyd = a * cx * ry / rx; cx = cosed * cxd - sined * cyd + x / 2; cy = sined * cxd + cosed * cyd + y / 2; } // Rotation + Scale Transform float xx = cosed / rx; float yx = sined / rx; float xy = -sined / ry; float yy = cosed / ry; // Start and End Angle float sa = atan2f(xy * -cx + yy * -cy, xx * -cx + yx * -cy); float ea = atan2f(xy * (x - cx) + yy * (y - cy), xx * (x - cx) + yx * (y - cy)); cx += tX; cy += tY; x += tX; y += tY; [self setPenDown]; _penX = _pivotX = x; _penY = _pivotY = y; [self arcToBezier:path cx:cx cy:cy rx:rx ry:ry sa:sa ea:ea clockwise:clockwise rad:rad]; } - (void)arcToBezier:(CGMutablePathRef)path cx:(float)cx cy:(float)cy rx:(float)rx ry:(float)ry sa:(float)sa ea:(float)ea clockwise:(BOOL)clockwise rad:(float)rad { // Inverse Rotation + Scale Transform float cosed = cosf(rad); float sined = sinf(rad); float xx = cosed * rx; float yx = -sined * ry; float xy = sined * rx; float yy = cosed * ry; // Bezier Curve Approximation float arc = ea - sa; if (arc < 0 && clockwise) { arc += M_PI * 2; } else if (arc > 0 && !clockwise) { arc -= M_PI * 2; } int n = (int)ceilf(fabsf(arc / ((float)M_PI / 2))); float step = arc / n; float k = (4.0f / 3.0f) * tanf(step / 4); float x = cosf(sa); float y = sinf(sa); for (int i = 0; i < n; i++) { float cp1x = x - k * y; float cp1y = y + k * x; sa += step; x = cosf(sa); y = sinf(sa); float cp2x = x + k * y; float cp2y = y - k * x; float c1x = cx + xx * cp1x + yx * cp1y; float c1y = cy + xy * cp1x + yy * cp1y; float c2x = cx + xx * cp2x + yx * cp2y; float c2y = cy + xy * cp2x + yy * cp2y; float ex = cx + xx * x + yx * y; float ey = cy + xy * x + yy * y; CGPathAddCurveToPoint(path, nil, c1x, c1y, c2x, c2y, ex, ey); } } - (void)close:(CGMutablePathRef)path { if (_penDownSet) { _penX = _penDownX; _penY = _penDownY; _penDownSet = NO; CGPathCloseSubpath(path); } } - (void)setPenDown { if (!_penDownSet) { _penDownX = _penX; _penDownY = _penY; _penDownSet = YES; } } - (void)skip_spaces { while (i < l && [[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[s characterAtIndex:i]]) i++; } - (bool)is_cmd:(char)c { switch (c) { case 'M': case 'm': case 'Z': case 'z': case 'L': case 'l': case 'H': case 'h': case 'V': case 'v': case 'C': case 'c': case 'S': case 's': case 'Q': case 'q': case 'T': case 't': case 'A': case 'a': return true; } return false; } - (bool)is_number_start:(char)c { return (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+'; } - (bool)is_absolute:(char)c { return [[NSCharacterSet uppercaseLetterCharacterSet] characterIsMember:c]; } // By the SVG spec 'large-arc' and 'sweep' must contain only one char // and can be written without any separators, e.g.: 10 20 30 01 10 20. - (bool)parse_flag { [self skip_spaces]; char c = [s characterAtIndex:i]; switch (c) { case '0': case '1': { i += 1; if (i < l && [s characterAtIndex:i] == ',') { i += 1; } [self skip_spaces]; break; } default: RCTLogError(@"UnexpectedData: %@", s); } return c == '1'; } - (float)parse_list_number { if (i == l) { RCTLogError(@"UnexpectedEnd: %@", s); } float n = [self parse_number]; [self skip_spaces]; [self parse_list_separator]; return n; } - (float)parse_number { // Strip off leading whitespaces. [self skip_spaces]; if (i == l) { RCTLogError(@"InvalidNumber: %@", s); } NSUInteger start = i; char c = [s characterAtIndex:i]; // Consume sign. if (c == '-' || c == '+') { i += 1; c = [s characterAtIndex:i]; } // Consume integer. if (c >= '0' && c <= '9') { [self skip_digits]; if (i < l) { c = [s characterAtIndex:i]; } } else if (c != '.') { RCTLogError(@"InvalidNumber: %@", s); } // Consume fraction. if (c == '.') { i += 1; [self skip_digits]; if (i < l) { c = [s characterAtIndex:i]; } } if ((c == 'e' || c == 'E') && i + 1 < l) { char c2 = [s characterAtIndex:i + 1]; // Check for `em`/`ex`. if (c2 != 'm' && c2 != 'x') { i += 1; c = [s characterAtIndex:i]; if (c == '+' || c == '-') { i += 1; [self skip_digits]; } else if (c >= '0' && c <= '9') { [self skip_digits]; } else { RCTLogError(@"InvalidNumber: %@", s); } } } NSString *num = [s substringWithRange:NSMakeRange(start, i - start)]; float n = [num floatValue]; // inf, nan, etc. are an error. if (!isfinite(n)) { RCTLogError(@"InvalidNumber: %@", s); } return n; } - (void)parse_list_separator { if (i < l && [s characterAtIndex:i] == ',') { i += 1; } } - (void)skip_digits { while (i < l && [[NSCharacterSet decimalDigitCharacterSet] characterIsMember:[s characterAtIndex:i]]) i++; } @end