Skip to content

Commit 9bfe0ce

Browse files
authored
Merge pull request SVGKit#586 from dreampiggy/bugfix_gradient_test_case
Fix the some gradient rendering edge case in W3C
2 parents d8aeae4 + 71c13cf commit 9bfe0ce

File tree

12 files changed

+383
-168
lines changed

12 files changed

+383
-168
lines changed

Demo-Samples/Fonts/Blocky.ttf

10.9 KB
Binary file not shown.

Demo-iOS.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
3242C36E217CF0CD0013765F /* Blocky.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3242C368217CF0CD0013765F /* Blocky.ttf */; };
1011
32D3D5EC2176E3FA00ED20D8 /* Mozilla_Firefox_logo_2013.svg in Resources */ = {isa = PBXBuildFile; fileRef = 32D3D5E62176E3FA00ED20D8 /* Mozilla_Firefox_logo_2013.svg */; };
1112
32D3D5F12176FA0700ED20D8 /* Mozilla_Firefox_logo_2013.png in Resources */ = {isa = PBXBuildFile; fileRef = 32D3D5F02176FA0700ED20D8 /* Mozilla_Firefox_logo_2013.png */; };
1213
32D42245217AED3900CFD064 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 32D42244217AED3900CFD064 /* LaunchScreen.storyboard */; };
@@ -250,6 +251,7 @@
250251
/* End PBXCopyFilesBuildPhase section */
251252

252253
/* Begin PBXFileReference section */
254+
3242C368217CF0CD0013765F /* Blocky.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Blocky.ttf; sourceTree = "<group>"; };
253255
32D3D5E62176E3FA00ED20D8 /* Mozilla_Firefox_logo_2013.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Mozilla_Firefox_logo_2013.svg; sourceTree = "<group>"; };
254256
32D3D5F02176FA0700ED20D8 /* Mozilla_Firefox_logo_2013.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Mozilla_Firefox_logo_2013.png; sourceTree = "<group>"; };
255257
32D42244217AED3900CFD064 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -438,6 +440,14 @@
438440
/* End PBXFrameworksBuildPhase section */
439441

440442
/* Begin PBXGroup section */
443+
3242C367217CF0CD0013765F /* Fonts */ = {
444+
isa = PBXGroup;
445+
children = (
446+
3242C368217CF0CD0013765F /* Blocky.ttf */,
447+
);
448+
path = Fonts;
449+
sourceTree = "<group>";
450+
};
441451
661A0DBF161727CF008D5FBE = {
442452
isa = PBXGroup;
443453
children = (
@@ -641,6 +651,7 @@
641651
662712631AD97D7500B3AD03 /* W3C_SVG_11_TestSuite */,
642652
661EEA511ACC1723007176B0 /* Saved Bitmaps */,
643653
66E8627B1688BA510059C9C4 /* SVG */,
654+
3242C367217CF0CD0013765F /* Fonts */,
644655
661EEA421ACC04F2007176B0 /* Licenses.plist */,
645656
);
646657
path = "Demo-Samples";
@@ -849,6 +860,7 @@
849860
66E862921688BA510059C9C4 /* Blank_Map-Africa.svg in Resources */,
850861
661EEAA71ACC3053007176B0 /* transformations.png in Resources */,
851862
661EEAE41ACC3444007176B0 /* sakamura-default-fill-opacity-test.png in Resources */,
863+
3242C36E217CF0CD0013765F /* Blocky.ttf in Resources */,
852864
661EEA991ACC2FD4007176B0 /* PreserveAspectRatio.png in Resources */,
853865
66E862931688BA510059C9C4 /* breaking-1.svg in Resources */,
854866
661EEA861ACC2FBB007176B0 /* text01.svg in Resources */,

SVGKitLibrary/Demo-iOS/Demo-iOS-Info.plist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
<string>1.0</string>
2525
<key>LSRequiresIPhoneOS</key>
2626
<true/>
27+
<key>UIAppFonts</key>
28+
<array>
29+
<string>Blocky.ttf</string>
30+
</array>
2731
<key>UILaunchStoryboardName</key>
2832
<string>LaunchScreen</string>
2933
<key>UIMainStoryboardFile</key>

Source/AppKit additions/SVGKImageRep.m

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
#import "SVGKImage+CGContext.h"
1717
#include <tgmath.h>
1818

19-
#define TEMPORARY_WARNING_FOR_APPLES_BROKEN_RENDERINCONTEXT_METHOD 1 // ONLY needed as temporary workaround for Apple's renderInContext bug breaking various bits of rendering: Gradients, Scaling, etc
20-
2119
@interface SVGKImageRep ()
2220
@property (nonatomic, strong, readwrite, setter = setTheSVG:) SVGKImage *image;
2321
@property (nonatomic, assign) BOOL antiAlias;
@@ -150,25 +148,6 @@ - (instancetype)initWithSVGImage:(SVGKImage*)theImage copy:(BOOL)copyImage
150148
}
151149

152150
self.image = theImage;
153-
154-
#if TEMPORARY_WARNING_FOR_APPLES_BROKEN_RENDERINCONTEXT_METHOD
155-
BOOL hasGrad = ![SVGKFastImageView svgImageHasNoGradients:self.image];
156-
157-
if (hasGrad) {
158-
NSString *errstuff;
159-
160-
if (hasGrad) {
161-
errstuff = @"gradients";
162-
}
163-
164-
if (errstuff == nil) {
165-
//We shouldn't get here!
166-
errstuff = @"";
167-
}
168-
169-
DDLogWarn(@"[%@] The image \"%@\" might have problems rendering correctly due to %@.", [self class], [self image], errstuff);
170-
}
171-
#endif
172151

173152
if (![self.image hasSize]) {
174153
self.image.size = CGSizeMake(32, 32);

Source/DOM classes/SVG-DOM/SVGHelperUtilities.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
#define FORCE_RASTERIZE_LAYERS 0 // If True, all CALayers will be told to rasterize themselves. This MIGHT increase performance (or might not), but leads to blurriness whenever a layer is scaled / zoomed in
1717
#define IMPROVE_PERFORMANCE_BY_WORKING_AROUND_APPLE_FRAME_ALIGNMENT_BUG 1 // NB: Apple's code for rendering ANY CALayer is extremely slow if the layer has non-integer co-ordinates for its "frame" or "bounds" property. This flag technically makes your SVG's render incorrect at sub-pixel level, but often increases performance of Apple's rendering by a factor of 2 or more!
18-
18+
@class SVGGradientLayer;
1919
@interface SVGHelperUtilities : NSObject
2020

2121
/**
@@ -49,9 +49,13 @@ This method ONLY looks at current node to establish the above two things, to do
4949
+(void) configureCALayer:(CALayer*) layer usingElement:(SVGElement*) nonStylableElement;
5050

5151
+(CALayer *) newCALayerForPathBasedSVGElement:(SVGElement*) svgElement withPath:(CGPathRef) path;
52+
+ (SVGGradientLayer*)getGradientLayerWithId:(NSString*)gradId
53+
forElement:(SVGElement*)svgElement
54+
withRect:(CGRect)r
55+
transform:(CGAffineTransform)transform;
5256

5357
+(CGColorRef) parseFillForElement:(SVGElement *)svgElement;
54-
58+
+(CGColorRef) parseStrokeForElement:(SVGElement *)svgElement;
5559
+(void) parsePreserveAspectRatioFor:(Element<SVGFitToViewBox>*) element;
5660

5761
@end

Source/DOM classes/SVG-DOM/SVGHelperUtilities.m

Lines changed: 118 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -384,12 +384,8 @@ +(CALayer *) newCALayerForPathBasedSVGElement:(SVGElement<SVGTransformable>*) sv
384384
fakeSize = CGSizeApplyAffineTransform( fakeSize, transformAbsolute );
385385
strokeLayer.lineWidth = hypot(fakeSize.width, fakeSize.height)/M_SQRT2;
386386

387-
SVGColor strokeColorAsSVGColor = SVGColorFromString([actualStroke UTF8String]); // have to use the intermediate of an SVGColor so that we can over-ride the ALPHA component in next line
388-
NSString* actualStrokeOpacity = [svgElement cascadedValueForStylableProperty:@"stroke-opacity"];
389-
if( actualStrokeOpacity.length > 0 )
390-
strokeColorAsSVGColor.a = (uint8_t) ([actualStrokeOpacity floatValue] * 0xFF);
391-
392-
strokeLayer.strokeColor = CGColorWithSVGColor( strokeColorAsSVGColor );
387+
NSString* actualStrokeOpacity = [svgElement cascadedValueForStylableProperty:@"stroke-opacity"];
388+
strokeLayer.strokeColor = [self parseStrokeForElement:svgElement fromStroke:actualStroke andOpacity:actualStrokeOpacity];
393389

394390
/**
395391
Stroke dash array
@@ -454,19 +450,32 @@ +(CALayer *) newCALayerForPathBasedSVGElement:(SVGElement<SVGTransformable>*) sv
454450
fillLayer.opacity = strokeLayer.opacity;
455451
fillLayer.path = strokeLayer.path;
456452

457-
NSRange idKeyRange = NSMakeRange(5, actualStroke.length - 6);
458-
NSString* strokeId = [actualStroke substringWithRange:idKeyRange];
459-
460-
SVGGradientLayer *gradientLayer = [self getGradientLayerWithId:strokeId forElement:svgElement withRect:strokeLayer.frame
461-
transform:transformAbsolute];
462-
463-
strokeLayer.frame = localRect;
464-
465-
strokeLayer.fillColor = nil;
466-
strokeLayer.strokeColor = [UIColor blackColor].CGColor;
467-
468-
gradientLayer.mask = strokeLayer;
469-
strokeLayer = (CAShapeLayer*) gradientLayer;
453+
NSArray *strokeArgs = [actualStroke componentsSeparatedByCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
454+
NSString *strokeIdArg = strokeArgs.firstObject;
455+
NSRange idKeyRange = NSMakeRange(5, strokeIdArg.length - 6);
456+
NSString* strokeId = [strokeIdArg substringWithRange:idKeyRange];
457+
458+
// SVG spec: Vertical and horizontal lines don't have a boundingbox, since they are one-dimensional, even though the stroke-width makes it look like they should have a boundingbox with non-zero width and height.
459+
CGRect boundingBox = strokeLayer.frame;
460+
CGRect pathBoundingBox = CGPathGetPathBoundingBox(pathRelative);
461+
if (!CGRectIsEmpty(pathBoundingBox)) {
462+
// apply gradient
463+
SVGGradientLayer *gradientLayer = [self getGradientLayerWithId:strokeId forElement:svgElement withRect:boundingBox transform:transformAbsolute];
464+
465+
if (gradientLayer) {
466+
strokeLayer.frame = localRect;
467+
468+
strokeLayer.fillColor = nil;
469+
strokeLayer.strokeColor = [UIColor blackColor].CGColor;
470+
471+
gradientLayer.mask = strokeLayer;
472+
strokeLayer = (CAShapeLayer*) gradientLayer;
473+
} else {
474+
// no gradient, fallback
475+
}
476+
} else {
477+
// no boundingBox, fallback
478+
}
470479
}
471480

472481
}
@@ -488,22 +497,27 @@ +(CALayer *) newCALayerForPathBasedSVGElement:(SVGElement<SVGTransformable>*) sv
488497

489498
if ( [actualFill hasPrefix:@"url"] )
490499
{
491-
NSRange idKeyRange = NSMakeRange(5, actualFill.length - 6);
492-
NSString* fillId = [actualFill substringWithRange:idKeyRange];
500+
NSArray *fillArgs = [actualFill componentsSeparatedByCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
501+
NSString *fillIdArg = fillArgs.firstObject;
502+
NSRange idKeyRange = NSMakeRange(5, fillIdArg.length - 6);
503+
NSString* fillId = [fillIdArg substringWithRange:idKeyRange];
493504

494505
/** Replace the return layer with a special layer using the URL fill */
495506
/** fetch the fill layer by URL using the DOM */
496507
SVGGradientLayer *gradientLayer = [self getGradientLayerWithId:fillId forElement:svgElement withRect:fillLayer.frame
497508
transform:transformAbsolute];
498-
499-
CAShapeLayer* maskLayer = [CAShapeLayer layer];
500-
maskLayer.frame = localRect;
501-
maskLayer.path = fillLayer.path;
502-
maskLayer.fillColor = [UIColor blackColor].CGColor;
503-
maskLayer.strokeColor = nil;
504-
gradientLayer.mask = maskLayer;
505-
gradientLayer.frame = fillLayer.frame;
506-
fillLayer = (CAShapeLayer* )gradientLayer;
509+
if (gradientLayer) {
510+
CAShapeLayer* maskLayer = [CAShapeLayer layer];
511+
maskLayer.frame = localRect;
512+
maskLayer.path = fillLayer.path;
513+
maskLayer.fillColor = [UIColor blackColor].CGColor;
514+
maskLayer.strokeColor = nil;
515+
gradientLayer.mask = maskLayer;
516+
gradientLayer.frame = fillLayer.frame;
517+
fillLayer = (CAShapeLayer* )gradientLayer;
518+
} else {
519+
// no gradient, fallback
520+
}
507521
}
508522
else if( actualFill.length > 0 || actualFillOpacity.length > 0 )
509523
{
@@ -540,7 +554,10 @@ + (SVGGradientLayer*)getGradientLayerWithId:(NSString*)gradId
540554
NSAssert( svgElement.rootOfCurrentDocumentFragment != nil, @"This SVG shape has a URL fill type; it needs to search for that URL (%@) inside its nearest-ancestor <SVG> node, but the rootOfCurrentDocumentFragment reference was nil (suggests the parser failed, or the SVG file is corrupt)", gradId );
541555

542556
SVGGradientElement* svgGradient = (SVGGradientElement*) [svgElement.rootOfCurrentDocumentFragment getElementById:gradId];
543-
NSAssert( svgGradient != nil, @"This SVG shape has a URL fill (%@), but could not find an XML Node with that ID inside the DOM tree (suggests the parser failed, or the SVG file is corrupt)", gradId );
557+
if (svgGradient == nil) {
558+
// SVG spec allows referenced gradient not exist and will use fallback color
559+
SVGKitLogWarn(@"This SVG shape has a URL fill (%@), but could not find an XML Node with that ID inside the DOM tree (suggests the parser failed, or the SVG file is corrupt)", gradId );
560+
}
544561

545562
[svgGradient synthesizeProperties];
546563

@@ -560,32 +577,77 @@ +(CGColorRef) parseFillForElement:(SVGElement *)svgElement
560577

561578
+(CGColorRef) parseFillForElement:(SVGElement *)svgElement fromFill:(NSString *)actualFill andOpacity:(NSString *)actualFillOpacity
562579
{
563-
CGColorRef fillColor = nil;
564-
if ( [actualFill hasPrefix:@"none"])
565-
{
566-
fillColor = nil;
567-
}
568-
else if( actualFill.length > 0 || actualFillOpacity.length > 0 )
569-
{
570-
SVGColor fillColorAsSVGColor = ( actualFill.length > 0 ) ?
571-
SVGColorFromString([actualFill UTF8String]) // have to use the intermediate of an SVGColor so that we can over-ride the ALPHA component in next line
572-
: SVGColorMake(0, 0, 0, 0);
573-
574-
if( actualFillOpacity.length > 0 )
575-
fillColorAsSVGColor.a = (uint8_t) ([actualFillOpacity floatValue] * 0xFF);
576-
577-
fillColor = CGColorWithSVGColor(fillColorAsSVGColor);
578-
}
579-
else
580-
{
581-
#if SVGKIT_UIKIT
582-
fillColor = [UIColor blackColor].CGColor;
583-
#else
584-
fillColor = CGColorGetConstantColor(kCGColorBlack);
585-
#endif
586-
}
580+
return [self parsePaintColorForElement:svgElement paintColor:actualFill paintOpacity:actualFillOpacity defaultColor:@"black"];
581+
}
587582

588-
return fillColor;
583+
+(CGColorRef) parseStrokeForElement:(SVGElement *)svgElement
584+
{
585+
NSString* actualStroke = [svgElement cascadedValueForStylableProperty:@"stroke"];
586+
NSString* actualStrokeOpacity = [svgElement cascadedValueForStylableProperty:@"stroke-opacity"];
587+
return [self parseStrokeForElement:svgElement fromStroke:actualStroke andOpacity:actualStrokeOpacity];
588+
}
589+
590+
+(CGColorRef) parseStrokeForElement:(SVGElement *)svgElement fromStroke:(NSString *)actualStroke andOpacity:(NSString *)actualStrokeOpacity
591+
{
592+
return [self parsePaintColorForElement:svgElement paintColor:actualStroke paintOpacity:actualStrokeOpacity defaultColor:@"none"];
593+
}
594+
595+
/**
596+
Spec: https://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint
597+
`fill` or `stroke` allows paint color. This should actually be a <paint> interface.
598+
`fill` default color is `black`, while `stroke` default color is `none`
599+
*/
600+
+(CGColorRef)parsePaintColorForElement:(SVGElement *)svgElement paintColor:(NSString *)paintColor paintOpacity:(NSString *)paintOpacity defaultColor:(NSString *)defaultColor {
601+
CGColorRef colorRef = NULL;
602+
if (!paintColor) {
603+
paintColor = @"none";
604+
}
605+
if ([paintColor isEqualToString:@"none"])
606+
{
607+
return NULL;
608+
}
609+
// there may be a url before the actual color like `url(#grad) #0f0`, parse it
610+
NSString *actualPaintColor;
611+
NSString *actualPaintOpacity = paintOpacity;
612+
NSArray *paintArgs = [paintColor componentsSeparatedByCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
613+
if ([paintColor hasPrefix:@"url"]) {
614+
if (paintArgs.count > 1) {
615+
actualPaintColor = paintArgs[1];
616+
}
617+
} else {
618+
actualPaintColor = paintColor;
619+
}
620+
if( actualPaintColor.length > 0 || actualPaintOpacity.length > 0 ) {
621+
SVGColor paintColorSVGColor;
622+
if (actualPaintColor.length > 0) {
623+
if (![actualPaintColor isEqualToString:@"none"]) {
624+
paintColorSVGColor = SVGColorFromString([actualPaintColor UTF8String]); // have to use the intermediate of an SVGColor so that we can over-ride the ALPHA component in next line
625+
} else {
626+
return NULL;
627+
}
628+
} else {
629+
if (![defaultColor isEqualToString:@"none"]) {
630+
paintColorSVGColor = SVGColorFromString([actualPaintColor UTF8String]);
631+
} else {
632+
return NULL;
633+
}
634+
}
635+
636+
if( actualPaintOpacity.length > 0 )
637+
paintColorSVGColor.a = (uint8_t) ([actualPaintOpacity floatValue] * 0xFF);
638+
639+
colorRef = CGColorWithSVGColor(paintColorSVGColor);
640+
}
641+
else
642+
{
643+
if (![defaultColor isEqualToString:@"none"]) {
644+
colorRef = CGColorWithSVGColor(SVGColorFromString([defaultColor UTF8String]));
645+
} else {
646+
return NULL;
647+
}
648+
}
649+
650+
return colorRef;
589651
}
590652

591653
+(void) parsePreserveAspectRatioFor:(Element<SVGFitToViewBox>*) element

Source/DOM classes/Unported or Partial DOM/SVGRadialGradientElement.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ - (SVGGradientLayer *)newGradientLayerForObjectRect:(CGRect)objectRect viewportR
5757
SVGLength* svgFR = [SVGLength svgLengthFromNSString:frAttr.length > 0 ? frAttr : @"0%"];
5858
// This is a tempory workaround. Apple's `CAGradientLayer` does not support focal point for radial gradient. We have to use the low-level API `CGContextDrawRadialGradient` and using custom software-render for focal point. So it does not works for `SVGLayredView` which is hardware-render by CA render server.
5959
if (fxAttr.length > 0 || fyAttr.length > 0 || frAttr.length > 0) {
60-
SVGKitLogVerbose(@"The radialGradient element #%@ contains focal value: (fx:%@, fy: %@, fr:%@). The focul value is only supported on `SVGFastimageView` and it will be ignored when rendering in SVGLayredView.", [self getAttribute:@"id"], fxAttr, fyAttr, frAttr);
60+
SVGKitLogWarn(@"The radialGradient element #%@ contains focal value: (fx:%@, fy: %@, fr:%@). The focul value is only supported on `SVGFastimageView` and it will be ignored when rendering in SVGLayredView.", [self getAttribute:@"id"], fxAttr, fyAttr, frAttr);
6161
}
6262
self.cx = svgCX;
6363
self.cy = svgCY;

0 commit comments

Comments
 (0)