123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699 |
- #import "SVGHelperUtilities.h"
- #import "CAShapeLayerWithHitTest.h"
- #import "SVGUtils.h"
- #import "SVGGradientElement.h"
- #import "CGPathAdditions.h"
- #import "SVGTransformable.h"
- #import "SVGSVGElement.h"
- #import "SVGGradientLayer.h"
- @implementation SVGHelperUtilities
- +(CGAffineTransform) transformRelativeIncludingViewportForTransformableOrViewportEstablishingElement:(SVGElement*) transformableOrSVGSVGElement
- {
- NSAssert([transformableOrSVGSVGElement conformsToProtocol:@protocol(SVGTransformable)] || [transformableOrSVGSVGElement isKindOfClass:[SVGSVGElement class]], @"Illegal argument, sent a non-SVGTransformable, non-SVGSVGElement object to a method that requires an SVGTransformable (NB: Apple's Xcode is rubbish, it should have thrown a compiler error that you even tried to do this, but it often doesn't bother). Incoming instance = %@", transformableOrSVGSVGElement );
-
- /**
- Each time you hit a viewPortElement in the DOM Tree, you
- have to insert an ADDITIONAL transform into the flow of:
-
- parent-transform -> child-transform
-
- has to become:
-
- parent-transform -> VIEWPORT-TRANSFORM -> child-transform
- */
-
- CGAffineTransform currentRelativeTransform;
- CGAffineTransform optionalViewportTransform;
-
- /**
- Current relative transform: for an incoming "SVGTransformable" it's .transform, for everything else its identity
- */
- if( [transformableOrSVGSVGElement conformsToProtocol:@protocol(SVGTransformable)])
- {
- currentRelativeTransform = ((SVGElement<SVGTransformable>*)transformableOrSVGSVGElement).transform;
- }
- else
- {
- currentRelativeTransform = CGAffineTransformIdentity;
- }
-
- /**
- Optional relative transform: if incoming element establishes a viewport, do something clever; for everything else, use identity
- */
- if( transformableOrSVGSVGElement.viewportElement == nil // if it's nil, it means THE OPPOSITE of what you'd expect - it means that it IS the viewport element - SVG Spec REQUIRES this
- || transformableOrSVGSVGElement.viewportElement == transformableOrSVGSVGElement // ?? I don't understand: ?? if it's something other than itself, then: we simply don't need to worry about it ??
- )
- {
- SVGSVGElement<SVGFitToViewBox>* svgSVGElement = (SVGSVGElement<SVGFitToViewBox>*) transformableOrSVGSVGElement;
-
- /**
- Calculate the "implicit" viewport->viewbox transform (caused by the <SVG> tag's possible "viewBox" attribute)
- Also calculate the "implicit" realViewport -> svgDefaultViewport transform (caused by the user changing the external
- size of the rendered SVG)
- */
- SVGRect frameViewBox = svgSVGElement.viewBox; // the ACTUAL viewbox (may be Uninitalized if none specified in SVG file)
- SVGRect frameActualViewport = svgSVGElement.viewport; // the ACTUAL viewport (dictated by the graphics engine; may be Uninitialized if the renderer has too little info to decide on a viewport at all!)
- SVGRect frameRequestedViewport = svgSVGElement.requestedViewport; // the default viewport requested in the SVG source file (may be Uninitialized if no svg width or height params in original source file)
-
- if( ! SVGRectIsInitialized(frameActualViewport))
- {
- /** We have NO VIEWPORT (renderer was presented too little info)
-
- Net effect: we MUST render everything at 1:1, and apply NO FURTHER TRANSFORMS
- */
- optionalViewportTransform = CGAffineTransformIdentity;
- }
- else
- {
- CGAffineTransform transformRealViewportToSVGViewport;
- CGAffineTransform transformSVGViewportToSVGViewBox;
-
- /** Transform part 1: from REAL viewport to EXPECTED viewport */
- SVGRect viewportForViewBoxToRelateTo;
- if( SVGRectIsInitialized( frameRequestedViewport ))
- {
- viewportForViewBoxToRelateTo = frameRequestedViewport;
- transformRealViewportToSVGViewport = CGAffineTransformMakeScale( frameActualViewport.width / frameRequestedViewport.width, frameActualViewport.height / frameRequestedViewport.height);
- }
- else
- {
- viewportForViewBoxToRelateTo = frameActualViewport;
- transformRealViewportToSVGViewport = CGAffineTransformIdentity;
- }
-
- /** Transform part 2: from EXPECTED viewport to internal viewBox */
- if( SVGRectIsInitialized( frameViewBox ) )
- {
- CGAffineTransform translateToViewBox = CGAffineTransformMakeTranslation( -frameViewBox.x, -frameViewBox.y );
- CGAffineTransform scaleToViewBox = CGAffineTransformMakeScale( viewportForViewBoxToRelateTo.width / frameViewBox.width, viewportForViewBoxToRelateTo.height / frameViewBox.height);
-
- /** This is hard to find in the spec, but: if you have NO implementation of PreserveAspectRatio, you still need to
- read the spec on PreserveAspectRatio - because it defines a default behaviour for files that DO NOT specify it,
- which is different from the mathemetic default of co-ordinate systems.
-
- In short, you MUST implement "<svg preserveAspectRatio=xMidYMid ... />", even if you're not supporting that attribute.
- */
- if( svgSVGElement.preserveAspectRatio.baseVal.meetOrSlice == SVG_MEETORSLICE_MEET ) // ALWAYS TRUE in current implementation
- {
- if( ABS( svgSVGElement.aspectRatioFromWidthPerHeight - svgSVGElement.aspectRatioFromViewBox) > 0.00001 )
- {
- /** The aspect ratios for viewport and viewbox differ; Spec requires us to
- insert an extra transform that causes aspect ratio for internal data to be
-
- ... MEET: == KEPT CONSTANT
-
- and to "aspect-scale to fit" (i.e. leaving letterboxes at topbottom / leftright as required)
-
- c.f.: http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute (read carefully)
- */
-
- double ratioOfRatios = svgSVGElement.aspectRatioFromWidthPerHeight / svgSVGElement.aspectRatioFromViewBox;
-
- SVGKitLogWarn(@"ratioOfRatios = %.2f", ratioOfRatios );
- SVGKitLogWarn(@"Experimental: auto-scaling viewbox transform to fulfil SVG spec's default MEET settings, because your SVG file has different aspect-ratios for viewBox and for svg.width,svg.height");
-
- /**
- For MEET, we have to SHRINK the viewbox's contents if they aren't as wide:high as the viewport:
- */
- CGAffineTransform catRestoreAspectRatio;
- if( ratioOfRatios > 1 ) {
- catRestoreAspectRatio = CGAffineTransformMakeScale( 1.0 / ratioOfRatios, 1.0 );
- } else if (ratioOfRatios != 0) {
- catRestoreAspectRatio = CGAffineTransformMakeScale( 1.0, 1.0 * ratioOfRatios );
- } else {
- catRestoreAspectRatio = CGAffineTransformIdentity;
- }
-
- double xTranslationRequired;
- double yTranslationRequired;
- if( ratioOfRatios > 1.0 ) // if we're going to have space to either side
- {
- switch( svgSVGElement.preserveAspectRatio.baseVal.align )
- {
- case SVG_PRESERVEASPECTRATIO_XMINYMIN:
- case SVG_PRESERVEASPECTRATIO_XMINYMID:
- case SVG_PRESERVEASPECTRATIO_XMINYMAX:
- {
- xTranslationRequired = 0.0;
- }break;
-
- case SVG_PRESERVEASPECTRATIO_XMIDYMIN:
- case SVG_PRESERVEASPECTRATIO_XMIDYMID:
- case SVG_PRESERVEASPECTRATIO_XMIDYMAX:
- {
- xTranslationRequired = ((ratioOfRatios-1.0)/2.0) * frameViewBox.width;
- }break;
-
- case SVG_PRESERVEASPECTRATIO_XMAXYMIN:
- case SVG_PRESERVEASPECTRATIO_XMAXYMID:
- case SVG_PRESERVEASPECTRATIO_XMAXYMAX:
- {
- xTranslationRequired = ((ratioOfRatios-1.0) * frameViewBox.width);
- }break;
-
- case SVG_PRESERVEASPECTRATIO_NONE:
- case SVG_PRESERVEASPECTRATIO_UNKNOWN:
- {
- xTranslationRequired = 0;
- }break;
- }
- }
- else
- xTranslationRequired = 0;
-
- if( ratioOfRatios < 1.0 ) // if we're going to have space above and below
- {
- switch( svgSVGElement.preserveAspectRatio.baseVal.align )
- {
- case SVG_PRESERVEASPECTRATIO_XMINYMIN:
- case SVG_PRESERVEASPECTRATIO_XMIDYMIN:
- case SVG_PRESERVEASPECTRATIO_XMAXYMIN:
- {
- yTranslationRequired = 0.0;
- }break;
-
- case SVG_PRESERVEASPECTRATIO_XMINYMID:
- case SVG_PRESERVEASPECTRATIO_XMIDYMID:
- case SVG_PRESERVEASPECTRATIO_XMAXYMID:
- {
- yTranslationRequired = ((1.0-ratioOfRatios)/2.0 * [svgSVGElement.height pixelsValue]);
- }break;
-
- case SVG_PRESERVEASPECTRATIO_XMINYMAX:
- case SVG_PRESERVEASPECTRATIO_XMIDYMAX:
- case SVG_PRESERVEASPECTRATIO_XMAXYMAX:
- {
- yTranslationRequired = ((1.0-ratioOfRatios) * [svgSVGElement.height pixelsValue]);
- }break;
-
- case SVG_PRESERVEASPECTRATIO_NONE:
- case SVG_PRESERVEASPECTRATIO_UNKNOWN:
- {
- yTranslationRequired = 0.0;
- }break;
- }
- }
- else
- yTranslationRequired = 0.0;
- /**
- For xMidYMid, we have to RE-CENTER the viewbox's contents if they aren't as wide:high as the viewport:
- */
- CGAffineTransform catRecenterNewAspectRatio = CGAffineTransformMakeTranslation( xTranslationRequired, yTranslationRequired );
-
- CGAffineTransform transformsThatHonourAspectRatioRequirements = CGAffineTransformConcat(catRecenterNewAspectRatio, catRestoreAspectRatio);
-
- scaleToViewBox = CGAffineTransformConcat( transformsThatHonourAspectRatioRequirements, scaleToViewBox );
- }
- }
- else
- SVGKitLogWarn( @"Unsupported: preserveAspectRatio set to SLICE. Code to handle this doesn't exist yet.");
-
- transformSVGViewportToSVGViewBox = CGAffineTransformConcat( translateToViewBox, scaleToViewBox );
- }
- else
- transformSVGViewportToSVGViewBox = CGAffineTransformIdentity;
-
- optionalViewportTransform = CGAffineTransformConcat( transformRealViewportToSVGViewport, transformSVGViewportToSVGViewBox );
- }
- }
- else
- {
- optionalViewportTransform = CGAffineTransformIdentity;
- }
-
- /**
- TOTAL relative based on the local "transform" property and the viewport (if present)
- */
- CGAffineTransform result = CGAffineTransformConcat( currentRelativeTransform, optionalViewportTransform);
-
- return result;
- }
- /*!
- Re-calculates the absolute transform on-demand by querying parent's absolute transform and appending self's relative transform.
-
- Can take ONLY TWO kinds of element:
- - something that implements SVGTransformable (non-transformables shouldn't be performing transforms!)
- - something that defines a new viewport co-ordinate system (i.e. the SVG tag itself; this is AN IMPLICIT TRANSFORMABLE!)
- */
- +(CGAffineTransform) transformAbsoluteIncludingViewportForTransformableOrViewportEstablishingElement:(SVGElement*) transformableOrSVGSVGElement
- {
- NSAssert([transformableOrSVGSVGElement conformsToProtocol:@protocol(SVGTransformable)] || [transformableOrSVGSVGElement isKindOfClass:[SVGSVGElement class]], @"Illegal argument, sent a non-SVGTransformable, non-SVGSVGElement object to a method that requires an SVGTransformable (NB: Apple's Xcode is rubbish, it should have thrown a compiler error that you even tried to do this, but it often doesn't bother). Incoming instance = %@", transformableOrSVGSVGElement );
-
- CGAffineTransform parentAbsoluteTransform = CGAffineTransformIdentity;
-
- NSAssert( transformableOrSVGSVGElement.parentNode == nil || [transformableOrSVGSVGElement.parentNode isKindOfClass:[SVGElement class]], @"I don't know what to do when parent node is NOT an SVG element of some kind; presumably, this is when SVG root node gets embedded inside something else? The Spec IS UNCLEAR and doesn't clearly define ANYTHING here, and provides very few examples" );
-
- /**
- Parent Absolute transform: one of the following
-
- a. parent is an SVGTransformable (so recurse this method call to find it)
- b. parent is a viewport-generating element (so recurse this method call to find it)
- c. parent is nil (so treat it as Identity)
- d. parent is something else (so do a while loop until we hit an a, b, or c above)
- */
- SVGElement* parentSVGElement = transformableOrSVGSVGElement;
- while( (parentSVGElement = (SVGElement*) parentSVGElement.parentNode) != nil )
- {
- if( [parentSVGElement conformsToProtocol:@protocol(SVGTransformable)] )
- {
- parentAbsoluteTransform = [self transformAbsoluteIncludingViewportForTransformableOrViewportEstablishingElement:parentSVGElement];
- break;
- }
-
- if( [parentSVGElement isKindOfClass:[SVGSVGElement class]] )
- {
- parentAbsoluteTransform = [self transformAbsoluteIncludingViewportForTransformableOrViewportEstablishingElement:parentSVGElement];
- break;
- }
- }
-
- /**
- TOTAL absolute based on the parent transform with relative (and possible viewport) transforms
- */
- CGAffineTransform result = CGAffineTransformConcat( [self transformRelativeIncludingViewportForTransformableOrViewportEstablishingElement:transformableOrSVGSVGElement], parentAbsoluteTransform );
-
- //DEBUG: SVGKitLogWarn( @"[%@] self.transformAbsolute: returning: affine( (%2.2f %2.2f %2.2f %2.2f), (%2.2f %2.2f)", [self class], result.a, result.b, result.c, result.d, result.tx, result.ty);
-
- return result;
- }
- +(void) configureCALayer:(CALayer*) layer usingElement:(SVGElement*) nonStylableElement
- {
- layer.name = nonStylableElement.identifier;
- [layer setValue:nonStylableElement.identifier forKey:kSVGElementIdentifier];
-
- #if FORCE_RASTERIZE_LAYERS
- if ([layer respondsToSelector:@selector(setShouldRasterize:)]) {
- [layer performSelector:@selector(setShouldRasterize:)
- withObject:[NSNumber numberWithBool:YES]];
- }
-
- /** If you're going to rasterize, Apple's code is dumb, and needs to be "told" if its using a Retina display */
- layer.contentsScale = [[UIScreen mainScreen] scale];
- layer.rasterizationScale = _shapeLayer.contentsScale;
- #endif
-
- if( [nonStylableElement conformsToProtocol:@protocol(SVGStylable)])
- {
- SVGElement<SVGStylable>* stylableElement = (SVGElement<SVGStylable>*) nonStylableElement;
-
- NSString* actualOpacity = [stylableElement cascadedValueForStylableProperty:@"opacity" inherit:NO];
- layer.opacity = actualOpacity.length > 0 ? [actualOpacity floatValue] : 1.0f; // svg's "opacity" defaults to 1!
-
- // Apply fill-rule on layer (only CAShapeLayer)
- NSString *fillRule = [stylableElement cascadedValueForStylableProperty:@"fill-rule"];
- if([fillRule isEqualToString:@"evenodd"] && [layer isKindOfClass:[CAShapeLayer class]]){
- CAShapeLayer *shapeLayer = (CAShapeLayer *)layer;
- shapeLayer.fillRule = @"even-odd";
- }
- }
- }
- +(CALayer *) newCALayerForPathBasedSVGElement:(SVGElement<SVGTransformable>*) svgElement withPath:(CGPathRef) pathRelative
- {
- CAShapeLayer* _shapeLayer = [CAShapeLayerWithHitTest layer];
-
- [self configureCALayer:_shapeLayer usingElement:svgElement];
-
- NSString* actualStroke = [svgElement cascadedValueForStylableProperty:@"stroke"];
- if (!actualStroke)
- actualStroke = @"none";
- NSString* actualStrokeWidth = [svgElement cascadedValueForStylableProperty:@"stroke-width"];
-
- CGFloat strokeWidth = 1.0;
-
- if (actualStrokeWidth)
- {
- SVGRect r = ((SVGSVGElement*) svgElement.viewportElement).viewport;
-
- strokeWidth = [[SVGLength svgLengthFromNSString:actualStrokeWidth]
- pixelsValueWithDimension: hypot(r.width, r.height)];
- }
-
- /** transform our LOCAL path into ABSOLUTE space */
- CGAffineTransform transformAbsolute = [self transformAbsoluteIncludingViewportForTransformableOrViewportEstablishingElement:svgElement];
- // calculate the rendered dimensions of the path
- CGRect r = CGRectInset(CGPathGetBoundingBox(pathRelative), -strokeWidth/2., -strokeWidth/2.);
- CGRect transformedPathBB = CGRectApplyAffineTransform(r, transformAbsolute);
-
- CGPathRef pathToPlaceInLayer = CGPathCreateCopyByTransformingPath(pathRelative, &transformAbsolute);
-
- /** find out the ABSOLUTE BOUNDING BOX of our transformed path */
- //DEBUG ONLY: CGRect unTransformedPathBB = CGPathGetBoundingBox( _pathRelative );
- #if IMPROVE_PERFORMANCE_BY_WORKING_AROUND_APPLE_FRAME_ALIGNMENT_BUG
- transformedPathBB = CGRectIntegral( transformedPathBB ); // ridiculous but improves performance of apple's code by up to 50% !
- #endif
- /** NB: when we set the _shapeLayer.frame, it has a *side effect* of moving the path itself - so, in order to prevent that,
- because Apple didn't provide a BOOL to disable that "feature", we have to pre-shift the path forwards by the amount it
- will be shifted backwards */
- CGPathRef finalPath = CGPathCreateByOffsettingPath( pathToPlaceInLayer, transformedPathBB.origin.x, transformedPathBB.origin.y );
-
- /** Can't use this - iOS 5 only! path = CGPathCreateCopyByTransformingPath(path, transformFromSVGUnitsToScreenUnits ); */
-
- _shapeLayer.path = finalPath;
- CGPathRelease(finalPath);
-
- /**
- NB: this line, by changing the FRAME of the layer, has the side effect of also changing the CGPATH's position in absolute
- space! This is why we needed the "CGPathRef finalPath =" line a few lines above...
- */
- _shapeLayer.frame = transformedPathBB;
-
- CGRect localRect = CGRectMake(0, 0, CGRectGetWidth(transformedPathBB), CGRectGetHeight(transformedPathBB));
- //DEBUG ONLY: CGRect shapeLayerFrame = _shapeLayer.frame;
- CAShapeLayer* strokeLayer = _shapeLayer;
- CAShapeLayer* fillLayer = _shapeLayer;
-
- if( strokeWidth > 0
- && (! [@"none" isEqualToString:actualStroke]) )
- {
- /*
- We have to apply any scale-factor part of the affine transform to the stroke itself (this is bizarre and horrible, yes, but that's the spec for you!)
- */
- CGSize fakeSize = CGSizeMake( strokeWidth, strokeWidth );
- fakeSize = CGSizeApplyAffineTransform( fakeSize, transformAbsolute );
- strokeLayer.lineWidth = hypot(fakeSize.width, fakeSize.height)/M_SQRT2;
-
- NSString* actualStrokeOpacity = [svgElement cascadedValueForStylableProperty:@"stroke-opacity"];
- strokeLayer.strokeColor = [self parseStrokeForElement:svgElement fromStroke:actualStroke andOpacity:actualStrokeOpacity];
-
- /**
- Stroke dash array
- */
- NSString *dashArrayString = [svgElement cascadedValueForStylableProperty:@"stroke-dasharray"];
- if(dashArrayString != nil && ![dashArrayString isEqualToString:@""]) {
- NSArray *dashArrayStringComponents = [dashArrayString componentsSeparatedByString:@" "];
- if( [dashArrayStringComponents count] < 2 )
- { // min 2 elements required, perhaps it's comma-separated:
- dashArrayStringComponents = [dashArrayString componentsSeparatedByString:@","];
- }
- if( [dashArrayStringComponents count] > 1 )
- {
- BOOL valid = NO;
- NSMutableArray *dashArray = [NSMutableArray array];
- for( NSString *n in dashArrayStringComponents ){
- [dashArray addObject:[NSNumber numberWithFloat:[n floatValue]]];
- if( !valid && [n floatValue] != 0 ){
- // avoid 'CGContextSetLineDash: invalid dash array: at least one element must be non-zero.'
- valid = YES;
- }
- }
- if( valid ){
- strokeLayer.lineDashPattern = dashArray;
- }
- }
- }
-
- /**
- Line joins + caps: butt / square / miter
- */
- NSString* actualLineCap = [svgElement cascadedValueForStylableProperty:@"stroke-linecap"];
- NSString* actualLineJoin = [svgElement cascadedValueForStylableProperty:@"stroke-linejoin"];
- NSString* actualMiterLimit = [svgElement cascadedValueForStylableProperty:@"stroke-miterlimit"];
- if( actualLineCap.length > 0 )
- {
- if( [actualLineCap isEqualToString:@"butt"] )
- strokeLayer.lineCap = kCALineCapButt;
- else if( [actualLineCap isEqualToString:@"round"] )
- strokeLayer.lineCap = kCALineCapRound;
- else if( [actualLineCap isEqualToString:@"square"] )
- strokeLayer.lineCap = kCALineCapSquare;
- }
- if( actualLineJoin.length > 0 )
- {
- if( [actualLineJoin isEqualToString:@"miter"] )
- strokeLayer.lineJoin = kCALineJoinMiter;
- else if( [actualLineJoin isEqualToString:@"round"] )
- strokeLayer.lineJoin = kCALineJoinRound;
- else if( [actualLineJoin isEqualToString:@"bevel"] )
- strokeLayer.lineJoin = kCALineJoinBevel;
- }
- if( actualMiterLimit.length > 0 )
- {
- strokeLayer.miterLimit = [actualMiterLimit floatValue];
- }
- if ( [actualStroke hasPrefix:@"url"] )
- {
- // need a new fill layer because the stroke layer is becoming a mask
- fillLayer = [CAShapeLayerWithHitTest layer];
- fillLayer.frame = strokeLayer.frame;
- fillLayer.opacity = strokeLayer.opacity;
- fillLayer.path = strokeLayer.path;
-
- NSArray *strokeArgs = [actualStroke componentsSeparatedByCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
- NSString *strokeIdArg = strokeArgs.firstObject;
- NSRange idKeyRange = NSMakeRange(5, strokeIdArg.length - 6);
- NSString* strokeId = [strokeIdArg substringWithRange:idKeyRange];
-
- // 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.
- CGRect boundingBox = strokeLayer.frame;
- CGRect pathBoundingBox = CGPathGetPathBoundingBox(pathRelative);
- if (!CGRectIsEmpty(pathBoundingBox)) {
- // apply gradient
- SVGGradientLayer *gradientLayer = [self getGradientLayerWithId:strokeId forElement:svgElement withRect:boundingBox transform:transformAbsolute];
-
- if (gradientLayer) {
- strokeLayer.frame = localRect;
-
- strokeLayer.fillColor = nil;
- strokeLayer.strokeColor = [UIColor blackColor].CGColor;
-
- gradientLayer.mask = strokeLayer;
- strokeLayer = (CAShapeLayer*) gradientLayer;
- } else {
- // no gradient, fallback
- }
- } else {
- // no boundingBox, fallback
- }
- }
-
- }
- else
- {
- if( [@"none" isEqualToString:actualStroke] )
- {
- strokeLayer.strokeColor = nil; // This is how you tell Apple that the stroke is disabled; a strokewidth of 0 will NOT achieve this
- strokeLayer.lineWidth = 0.0f; // MUST set this explicitly, or Apple assumes 1.0
- }
- else
- {
- strokeLayer.lineWidth = 1.0f; // default value from SVG spec
- }
- }
-
- NSString* actualFill = [svgElement cascadedValueForStylableProperty:@"fill"];
- NSString* actualFillOpacity = [svgElement cascadedValueForStylableProperty:@"fill-opacity"];
-
- if ( [actualFill hasPrefix:@"url"] )
- {
- NSArray *fillArgs = [actualFill componentsSeparatedByCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
- NSString *fillIdArg = fillArgs.firstObject;
- NSRange idKeyRange = NSMakeRange(5, fillIdArg.length - 6);
- NSString* fillId = [fillIdArg substringWithRange:idKeyRange];
-
- /** Replace the return layer with a special layer using the URL fill */
- /** fetch the fill layer by URL using the DOM */
- SVGGradientLayer *gradientLayer = [self getGradientLayerWithId:fillId forElement:svgElement withRect:fillLayer.frame
- transform:transformAbsolute];
- if (gradientLayer) {
- CAShapeLayer* maskLayer = [CAShapeLayer layer];
- maskLayer.frame = localRect;
- maskLayer.path = fillLayer.path;
- maskLayer.fillColor = [UIColor blackColor].CGColor;
- maskLayer.strokeColor = nil;
- gradientLayer.mask = maskLayer;
- gradientLayer.frame = fillLayer.frame;
- fillLayer = (CAShapeLayer* )gradientLayer;
- } else {
- // no gradient, fallback
- }
- }
- else if( actualFill.length > 0 || actualFillOpacity.length > 0 )
- {
- fillLayer.fillColor = [self parseFillForElement:svgElement fromFill:actualFill andOpacity:actualFillOpacity];
- }
- CGPathRelease(pathToPlaceInLayer);
-
- NSString* actualOpacity = [svgElement cascadedValueForStylableProperty:@"opacity" inherit:NO];
- fillLayer.opacity = actualOpacity.length > 0 ? [actualOpacity floatValue] : 1; // unusually, the "opacity" attribute defaults to 1, not 0
- if (strokeLayer == fillLayer)
- {
- return strokeLayer;
- }
- CALayer* combined = [CALayer layer];
-
- combined.frame = strokeLayer.frame;
- strokeLayer.frame = localRect;
- if ([strokeLayer isKindOfClass:[CAShapeLayer class]])
- strokeLayer.fillColor = nil;
- fillLayer.frame = localRect;
- [combined addSublayer:fillLayer];
- [combined addSublayer:strokeLayer];
- return combined;
- }
- + (SVGGradientLayer*)getGradientLayerWithId:(NSString*)gradId
- forElement:(SVGElement*)svgElement
- withRect:(CGRect)r
- transform:(CGAffineTransform)transform
- {
- /** Replace the return layer with a special layer using the URL fill */
- /** fetch the fill layer by URL using the DOM */
- 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 );
-
- SVGGradientElement* svgGradient = (SVGGradientElement*) [svgElement.rootOfCurrentDocumentFragment getElementById:gradId];
- if (svgGradient == nil) {
- // SVG spec allows referenced gradient not exist and will use fallback color
- 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 );
- }
- [svgGradient synthesizeProperties];
-
- SVGGradientLayer *gradientLayer = [svgGradient newGradientLayerForObjectRect:r
- viewportRect:svgElement.rootOfCurrentDocumentFragment.viewBox
- transform:transform];
- return gradientLayer;
- }
- +(CGColorRef) parseFillForElement:(SVGElement *)svgElement
- {
- NSString* actualFill = [svgElement cascadedValueForStylableProperty:@"fill"];
- NSString* actualFillOpacity = [svgElement cascadedValueForStylableProperty:@"fill-opacity"];
- return [self parseFillForElement:svgElement fromFill:actualFill andOpacity:actualFillOpacity];
- }
- +(CGColorRef) parseFillForElement:(SVGElement *)svgElement fromFill:(NSString *)actualFill andOpacity:(NSString *)actualFillOpacity
- {
- return [self parsePaintColorForElement:svgElement paintColor:actualFill paintOpacity:actualFillOpacity defaultColor:@"black"];
- }
- +(CGColorRef) parseStrokeForElement:(SVGElement *)svgElement
- {
- NSString* actualStroke = [svgElement cascadedValueForStylableProperty:@"stroke"];
- NSString* actualStrokeOpacity = [svgElement cascadedValueForStylableProperty:@"stroke-opacity"];
- return [self parseStrokeForElement:svgElement fromStroke:actualStroke andOpacity:actualStrokeOpacity];
- }
- +(CGColorRef) parseStrokeForElement:(SVGElement *)svgElement fromStroke:(NSString *)actualStroke andOpacity:(NSString *)actualStrokeOpacity
- {
- return [self parsePaintColorForElement:svgElement paintColor:actualStroke paintOpacity:actualStrokeOpacity defaultColor:@"none"];
- }
- /**
- Spec: https://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint
- `fill` or `stroke` allows paint color. This should actually be a <paint> interface.
- `fill` default color is `black`, while `stroke` default color is `none`
- */
- +(CGColorRef)parsePaintColorForElement:(SVGElement *)svgElement paintColor:(NSString *)paintColor paintOpacity:(NSString *)paintOpacity defaultColor:(NSString *)defaultColor {
- CGColorRef colorRef = NULL;
- if (!paintColor) {
- paintColor = @"none";
- }
- if ([paintColor isEqualToString:@"none"])
- {
- return NULL;
- }
- // there may be a url before the actual color like `url(#grad) #0f0`, parse it
- NSString *actualPaintColor;
- NSString *actualPaintOpacity = paintOpacity;
- NSArray *paintArgs = [paintColor componentsSeparatedByCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
- if ([paintColor hasPrefix:@"url"]) {
- if (paintArgs.count > 1) {
- actualPaintColor = paintArgs[1];
- }
- } else {
- actualPaintColor = paintColor;
- }
- if( actualPaintColor.length > 0 || actualPaintOpacity.length > 0 ) {
- SVGColor paintColorSVGColor;
- if (actualPaintColor.length > 0) {
- if (![actualPaintColor isEqualToString:@"none"]) {
- paintColorSVGColor = SVGColorFromString([actualPaintColor UTF8String]); // have to use the intermediate of an SVGColor so that we can over-ride the ALPHA component in next line
- } else {
- return NULL;
- }
- } else {
- if (![defaultColor isEqualToString:@"none"]) {
- paintColorSVGColor = SVGColorFromString([actualPaintColor UTF8String]);
- } else {
- return NULL;
- }
- }
-
- if( actualPaintOpacity.length > 0 )
- paintColorSVGColor.a = (uint8_t) ([actualPaintOpacity floatValue] * 0xFF);
-
- colorRef = CGColorWithSVGColor(paintColorSVGColor);
- }
- else
- {
- if (![defaultColor isEqualToString:@"none"]) {
- colorRef = CGColorWithSVGColor(SVGColorFromString([defaultColor UTF8String]));
- } else {
- return NULL;
- }
- }
-
- return colorRef;
- }
- +(void) parsePreserveAspectRatioFor:(Element<SVGFitToViewBox>*) element
- {
- element.preserveAspectRatio = [SVGAnimatedPreserveAspectRatio new]; // automatically sets defaults
-
- NSString* stringPreserveAspectRatio = [element getAttribute:@"preserveAspectRatio"];
-
- if( stringPreserveAspectRatio.length > 0 )
- {
- NSArray* aspectRatioCommands = [stringPreserveAspectRatio componentsSeparatedByString:@" "];
-
- for( NSString* aspectRatioCommand in aspectRatioCommands )
- {
- if( [aspectRatioCommand isEqualToString:@"meet"]) /** NB this is default anyway. Dont technically need to set it */
- element.preserveAspectRatio.baseVal.meetOrSlice = SVG_MEETORSLICE_MEET;
- else if( [aspectRatioCommand isEqualToString:@"slice"])
- element.preserveAspectRatio.baseVal.meetOrSlice = SVG_MEETORSLICE_SLICE;
-
- else if( [aspectRatioCommand isEqualToString:@"xMinYMin"])
- element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMINYMIN;
- else if( [aspectRatioCommand isEqualToString:@"xMinYMid"])
- element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMINYMID;
- else if( [aspectRatioCommand isEqualToString:@"xMinYMax"])
- element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMINYMAX;
-
- else if( [aspectRatioCommand isEqualToString:@"xMidYMin"])
- element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMIDYMIN;
- else if( [aspectRatioCommand isEqualToString:@"xMidYMid"])
- element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMIDYMID;
- else if( [aspectRatioCommand isEqualToString:@"xMidYMax"])
- element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMIDYMAX;
-
- else if( [aspectRatioCommand isEqualToString:@"xMaxYMin"])
- element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMAXYMIN;
- else if( [aspectRatioCommand isEqualToString:@"xMaxYMid"])
- element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMAXYMID;
- else if( [aspectRatioCommand isEqualToString:@"xMaxYMax"])
- element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMAXYMAX;
-
- else
- {
- SVGKitLogWarn(@"Found unexpected preserve-aspect-ratio command inside element's 'preserveAspectRatio' attribute. Command = '%@'", aspectRatioCommand );
- }
- }
- }
- }
- @end
|