mirror of
https://github.com/zoriya/react-native-svg.git
synced 2026-06-01 06:07:41 +00:00
fix(ios): handle path data without whitespace/comma after arc flags
#1080
This commit is contained in:
+298
-94
@@ -13,9 +13,10 @@
|
||||
|
||||
@implementation RNSVGPathParser
|
||||
{
|
||||
NSString* _d;
|
||||
NSString* _originD;
|
||||
NSRegularExpression* _pathRegularExpression;
|
||||
char prev_cmd;
|
||||
NSUInteger i;
|
||||
NSUInteger l;
|
||||
NSString* s;
|
||||
float _penX;
|
||||
float _penY;
|
||||
float _penDownX;
|
||||
@@ -26,117 +27,173 @@
|
||||
BOOL _penDownSet;
|
||||
}
|
||||
|
||||
- (instancetype) initWithPathString:(NSString *)d
|
||||
- (instancetype)initWithPathString:(NSString *)d
|
||||
{
|
||||
if (self = [super init]) {
|
||||
NSRegularExpression* decimalRegularExpression = [[NSRegularExpression alloc] initWithPattern:@"(\\.\\d+)(?=\\-?\\.)" options:0 error:nil];
|
||||
_originD = d;
|
||||
_d = [decimalRegularExpression stringByReplacingMatchesInString:d options:0 range:NSMakeRange(0, [d length]) withTemplate:@"$1,"];
|
||||
_pathRegularExpression = [[NSRegularExpression alloc] initWithPattern:@"[a-df-z]|[\\-+]?(?:[\\d.]e[\\-+]?|[^\\s\\-+,a-z])+" options:NSRegularExpressionCaseInsensitive error:nil];
|
||||
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();
|
||||
NSArray<NSTextCheckingResult *>* results = [_pathRegularExpression matchesInString:_d options:0 range:NSMakeRange(0, [_d length])];
|
||||
unsigned long count = [results count];
|
||||
while (i < l) {
|
||||
[self skip_spaces];
|
||||
|
||||
if (count) {
|
||||
NSUInteger i = 0;
|
||||
#define NEXT_VALUE [self getNextValue:results[i++]]
|
||||
#define NEXT_FLOAT [self float:NEXT_VALUE]
|
||||
#define NEXT_BOOL [self bool:NEXT_VALUE]
|
||||
NSString* lastCommand;
|
||||
NSString* command = NEXT_VALUE;
|
||||
bool has_prev_cmd = prev_cmd != ' ';
|
||||
char first_char = [s characterAtIndex:i];
|
||||
|
||||
@try {
|
||||
while (command) {
|
||||
if ([command isEqualToString:@"m"]) { // moveTo command
|
||||
[self move:path x:NEXT_FLOAT y:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"M"]) {
|
||||
[self moveTo:path x:NEXT_FLOAT y:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"l"]) { // lineTo command
|
||||
[self line:path x:NEXT_FLOAT y:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"L"]) {
|
||||
[self lineTo:path x:NEXT_FLOAT y:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"h"]) { // horizontalTo command
|
||||
[self line:path x:NEXT_FLOAT y:0];
|
||||
} else if ([command isEqualToString:@"H"]) {
|
||||
[self lineTo:path x:NEXT_FLOAT y:_penY];
|
||||
} else if ([command isEqualToString:@"v"]) { // verticalTo command
|
||||
[self line:path x:0 y:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"V"]) {
|
||||
[self lineTo:path x:_penX y:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"c"]) { // curveTo command
|
||||
[self curve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"C"]) {
|
||||
[self curveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"s"]) { // smoothCurveTo command
|
||||
[self smoothCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"S"]) {
|
||||
[self smoothCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT ex:NEXT_FLOAT ey:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"q"]) { // quadraticBezierCurveTo command
|
||||
[self quadraticBezierCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"Q"]) {
|
||||
[self quadraticBezierCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT c2x:NEXT_FLOAT c2y:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"t"]) {// smoothQuadraticBezierCurveTo command
|
||||
[self smoothQuadraticBezierCurve:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"T"]) {
|
||||
[self smoothQuadraticBezierCurveTo:path c1x:NEXT_FLOAT c1y:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"a"]) { // arcTo command
|
||||
[self arc:path rx:NEXT_FLOAT ry:NEXT_FLOAT rotation:NEXT_FLOAT outer:NEXT_BOOL clockwise:NEXT_BOOL x:NEXT_FLOAT y:NEXT_FLOAT];
|
||||
} else if ([command isEqualToString:@"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];
|
||||
} else if ([command isEqualToString:@"z"]) { // close command
|
||||
[self close:path];
|
||||
} else if ([command isEqualToString:@"Z"]) {
|
||||
[self close:path];
|
||||
} else {
|
||||
command = lastCommand;
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
lastCommand = command;
|
||||
if ([lastCommand isEqualToString:@"m"]) {
|
||||
lastCommand = @"l";
|
||||
} else if ([lastCommand isEqualToString:@"M"]) {
|
||||
lastCommand = @"L";
|
||||
}
|
||||
|
||||
command = i < count ? NEXT_VALUE : nil;
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
RCTLogWarn(@"Invalid CGPath format: %@", _originD);
|
||||
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);
|
||||
}
|
||||
|
||||
- (NSString *)getNextValue:(NSTextCheckingResult *)result
|
||||
{
|
||||
if (!result) {
|
||||
return nil;
|
||||
}
|
||||
return [_d substringWithRange:NSMakeRange(result.range.location, result.range.length)];
|
||||
}
|
||||
|
||||
- (float)float:(NSString *)value
|
||||
{
|
||||
return [value floatValue];
|
||||
}
|
||||
|
||||
- (BOOL)bool:(NSString *)value
|
||||
{
|
||||
return ![value isEqualToString:@"0"];
|
||||
}
|
||||
|
||||
- (void)move:(CGMutablePathRef)path x:(float)x y:(float)y
|
||||
{
|
||||
[self moveTo:path x:x + _penX y:y + _penY];
|
||||
@@ -373,4 +430,151 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)skip_spaces {
|
||||
while ([[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 ([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];
|
||||
c = [s characterAtIndex:i];
|
||||
} else if (c != '.') {
|
||||
RCTLogError(@"InvalidNumber: %@", s);
|
||||
}
|
||||
|
||||
// Consume fraction.
|
||||
if (c == '.') {
|
||||
i += 1;
|
||||
[self skip_digits];
|
||||
c = [s characterAtIndex:i];
|
||||
}
|
||||
|
||||
if (c == 'e' || c == 'E') {
|
||||
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 ([s characterAtIndex:i] == ',') {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)skip_digits {
|
||||
while ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:[s characterAtIndex:i]]) i++;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user