SVGHelperUtilities.m 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. #import "SVGHelperUtilities.h"
  2. #import "CAShapeLayerWithHitTest.h"
  3. #import "SVGUtils.h"
  4. #import "SVGGradientElement.h"
  5. #import "CGPathAdditions.h"
  6. #import "SVGTransformable.h"
  7. #import "SVGSVGElement.h"
  8. #import "SVGGradientLayer.h"
  9. @implementation SVGHelperUtilities
  10. +(CGAffineTransform) transformRelativeIncludingViewportForTransformableOrViewportEstablishingElement:(SVGElement*) transformableOrSVGSVGElement
  11. {
  12. 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 );
  13. /**
  14. Each time you hit a viewPortElement in the DOM Tree, you
  15. have to insert an ADDITIONAL transform into the flow of:
  16. parent-transform -> child-transform
  17. has to become:
  18. parent-transform -> VIEWPORT-TRANSFORM -> child-transform
  19. */
  20. CGAffineTransform currentRelativeTransform;
  21. CGAffineTransform optionalViewportTransform;
  22. /**
  23. Current relative transform: for an incoming "SVGTransformable" it's .transform, for everything else its identity
  24. */
  25. if( [transformableOrSVGSVGElement conformsToProtocol:@protocol(SVGTransformable)])
  26. {
  27. currentRelativeTransform = ((SVGElement<SVGTransformable>*)transformableOrSVGSVGElement).transform;
  28. }
  29. else
  30. {
  31. currentRelativeTransform = CGAffineTransformIdentity;
  32. }
  33. /**
  34. Optional relative transform: if incoming element establishes a viewport, do something clever; for everything else, use identity
  35. */
  36. 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
  37. || transformableOrSVGSVGElement.viewportElement == transformableOrSVGSVGElement // ?? I don't understand: ?? if it's something other than itself, then: we simply don't need to worry about it ??
  38. )
  39. {
  40. SVGSVGElement<SVGFitToViewBox>* svgSVGElement = (SVGSVGElement<SVGFitToViewBox>*) transformableOrSVGSVGElement;
  41. /**
  42. Calculate the "implicit" viewport->viewbox transform (caused by the <SVG> tag's possible "viewBox" attribute)
  43. Also calculate the "implicit" realViewport -> svgDefaultViewport transform (caused by the user changing the external
  44. size of the rendered SVG)
  45. */
  46. SVGRect frameViewBox = svgSVGElement.viewBox; // the ACTUAL viewbox (may be Uninitalized if none specified in SVG file)
  47. 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!)
  48. 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)
  49. if( ! SVGRectIsInitialized(frameActualViewport))
  50. {
  51. /** We have NO VIEWPORT (renderer was presented too little info)
  52. Net effect: we MUST render everything at 1:1, and apply NO FURTHER TRANSFORMS
  53. */
  54. optionalViewportTransform = CGAffineTransformIdentity;
  55. }
  56. else
  57. {
  58. CGAffineTransform transformRealViewportToSVGViewport;
  59. CGAffineTransform transformSVGViewportToSVGViewBox;
  60. /** Transform part 1: from REAL viewport to EXPECTED viewport */
  61. SVGRect viewportForViewBoxToRelateTo;
  62. if( SVGRectIsInitialized( frameRequestedViewport ))
  63. {
  64. viewportForViewBoxToRelateTo = frameRequestedViewport;
  65. transformRealViewportToSVGViewport = CGAffineTransformMakeScale( frameActualViewport.width / frameRequestedViewport.width, frameActualViewport.height / frameRequestedViewport.height);
  66. }
  67. else
  68. {
  69. viewportForViewBoxToRelateTo = frameActualViewport;
  70. transformRealViewportToSVGViewport = CGAffineTransformIdentity;
  71. }
  72. /** Transform part 2: from EXPECTED viewport to internal viewBox */
  73. if( SVGRectIsInitialized( frameViewBox ) )
  74. {
  75. CGAffineTransform translateToViewBox = CGAffineTransformMakeTranslation( -frameViewBox.x, -frameViewBox.y );
  76. CGAffineTransform scaleToViewBox = CGAffineTransformMakeScale( viewportForViewBoxToRelateTo.width / frameViewBox.width, viewportForViewBoxToRelateTo.height / frameViewBox.height);
  77. /** This is hard to find in the spec, but: if you have NO implementation of PreserveAspectRatio, you still need to
  78. read the spec on PreserveAspectRatio - because it defines a default behaviour for files that DO NOT specify it,
  79. which is different from the mathemetic default of co-ordinate systems.
  80. In short, you MUST implement "<svg preserveAspectRatio=xMidYMid ... />", even if you're not supporting that attribute.
  81. */
  82. if( svgSVGElement.preserveAspectRatio.baseVal.meetOrSlice == SVG_MEETORSLICE_MEET ) // ALWAYS TRUE in current implementation
  83. {
  84. if( ABS( svgSVGElement.aspectRatioFromWidthPerHeight - svgSVGElement.aspectRatioFromViewBox) > 0.00001 )
  85. {
  86. /** The aspect ratios for viewport and viewbox differ; Spec requires us to
  87. insert an extra transform that causes aspect ratio for internal data to be
  88. ... MEET: == KEPT CONSTANT
  89. and to "aspect-scale to fit" (i.e. leaving letterboxes at topbottom / leftright as required)
  90. c.f.: http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute (read carefully)
  91. */
  92. double ratioOfRatios = svgSVGElement.aspectRatioFromWidthPerHeight / svgSVGElement.aspectRatioFromViewBox;
  93. SVGKitLogWarn(@"ratioOfRatios = %.2f", ratioOfRatios );
  94. 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");
  95. /**
  96. For MEET, we have to SHRINK the viewbox's contents if they aren't as wide:high as the viewport:
  97. */
  98. CGAffineTransform catRestoreAspectRatio;
  99. if( ratioOfRatios > 1 ) {
  100. catRestoreAspectRatio = CGAffineTransformMakeScale( 1.0 / ratioOfRatios, 1.0 );
  101. } else if (ratioOfRatios != 0) {
  102. catRestoreAspectRatio = CGAffineTransformMakeScale( 1.0, 1.0 * ratioOfRatios );
  103. } else {
  104. catRestoreAspectRatio = CGAffineTransformIdentity;
  105. }
  106. double xTranslationRequired;
  107. double yTranslationRequired;
  108. if( ratioOfRatios > 1.0 ) // if we're going to have space to either side
  109. {
  110. switch( svgSVGElement.preserveAspectRatio.baseVal.align )
  111. {
  112. case SVG_PRESERVEASPECTRATIO_XMINYMIN:
  113. case SVG_PRESERVEASPECTRATIO_XMINYMID:
  114. case SVG_PRESERVEASPECTRATIO_XMINYMAX:
  115. {
  116. xTranslationRequired = 0.0;
  117. }break;
  118. case SVG_PRESERVEASPECTRATIO_XMIDYMIN:
  119. case SVG_PRESERVEASPECTRATIO_XMIDYMID:
  120. case SVG_PRESERVEASPECTRATIO_XMIDYMAX:
  121. {
  122. xTranslationRequired = ((ratioOfRatios-1.0)/2.0) * frameViewBox.width;
  123. }break;
  124. case SVG_PRESERVEASPECTRATIO_XMAXYMIN:
  125. case SVG_PRESERVEASPECTRATIO_XMAXYMID:
  126. case SVG_PRESERVEASPECTRATIO_XMAXYMAX:
  127. {
  128. xTranslationRequired = ((ratioOfRatios-1.0) * frameViewBox.width);
  129. }break;
  130. case SVG_PRESERVEASPECTRATIO_NONE:
  131. case SVG_PRESERVEASPECTRATIO_UNKNOWN:
  132. {
  133. xTranslationRequired = 0;
  134. }break;
  135. }
  136. }
  137. else
  138. xTranslationRequired = 0;
  139. if( ratioOfRatios < 1.0 ) // if we're going to have space above and below
  140. {
  141. switch( svgSVGElement.preserveAspectRatio.baseVal.align )
  142. {
  143. case SVG_PRESERVEASPECTRATIO_XMINYMIN:
  144. case SVG_PRESERVEASPECTRATIO_XMIDYMIN:
  145. case SVG_PRESERVEASPECTRATIO_XMAXYMIN:
  146. {
  147. yTranslationRequired = 0.0;
  148. }break;
  149. case SVG_PRESERVEASPECTRATIO_XMINYMID:
  150. case SVG_PRESERVEASPECTRATIO_XMIDYMID:
  151. case SVG_PRESERVEASPECTRATIO_XMAXYMID:
  152. {
  153. yTranslationRequired = ((1.0-ratioOfRatios)/2.0 * [svgSVGElement.height pixelsValue]);
  154. }break;
  155. case SVG_PRESERVEASPECTRATIO_XMINYMAX:
  156. case SVG_PRESERVEASPECTRATIO_XMIDYMAX:
  157. case SVG_PRESERVEASPECTRATIO_XMAXYMAX:
  158. {
  159. yTranslationRequired = ((1.0-ratioOfRatios) * [svgSVGElement.height pixelsValue]);
  160. }break;
  161. case SVG_PRESERVEASPECTRATIO_NONE:
  162. case SVG_PRESERVEASPECTRATIO_UNKNOWN:
  163. {
  164. yTranslationRequired = 0.0;
  165. }break;
  166. }
  167. }
  168. else
  169. yTranslationRequired = 0.0;
  170. /**
  171. For xMidYMid, we have to RE-CENTER the viewbox's contents if they aren't as wide:high as the viewport:
  172. */
  173. CGAffineTransform catRecenterNewAspectRatio = CGAffineTransformMakeTranslation( xTranslationRequired, yTranslationRequired );
  174. CGAffineTransform transformsThatHonourAspectRatioRequirements = CGAffineTransformConcat(catRecenterNewAspectRatio, catRestoreAspectRatio);
  175. scaleToViewBox = CGAffineTransformConcat( transformsThatHonourAspectRatioRequirements, scaleToViewBox );
  176. }
  177. }
  178. else
  179. SVGKitLogWarn( @"Unsupported: preserveAspectRatio set to SLICE. Code to handle this doesn't exist yet.");
  180. transformSVGViewportToSVGViewBox = CGAffineTransformConcat( translateToViewBox, scaleToViewBox );
  181. }
  182. else
  183. transformSVGViewportToSVGViewBox = CGAffineTransformIdentity;
  184. optionalViewportTransform = CGAffineTransformConcat( transformRealViewportToSVGViewport, transformSVGViewportToSVGViewBox );
  185. }
  186. }
  187. else
  188. {
  189. optionalViewportTransform = CGAffineTransformIdentity;
  190. }
  191. /**
  192. TOTAL relative based on the local "transform" property and the viewport (if present)
  193. */
  194. CGAffineTransform result = CGAffineTransformConcat( currentRelativeTransform, optionalViewportTransform);
  195. return result;
  196. }
  197. /*!
  198. Re-calculates the absolute transform on-demand by querying parent's absolute transform and appending self's relative transform.
  199. Can take ONLY TWO kinds of element:
  200. - something that implements SVGTransformable (non-transformables shouldn't be performing transforms!)
  201. - something that defines a new viewport co-ordinate system (i.e. the SVG tag itself; this is AN IMPLICIT TRANSFORMABLE!)
  202. */
  203. +(CGAffineTransform) transformAbsoluteIncludingViewportForTransformableOrViewportEstablishingElement:(SVGElement*) transformableOrSVGSVGElement
  204. {
  205. 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 );
  206. CGAffineTransform parentAbsoluteTransform = CGAffineTransformIdentity;
  207. 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" );
  208. /**
  209. Parent Absolute transform: one of the following
  210. a. parent is an SVGTransformable (so recurse this method call to find it)
  211. b. parent is a viewport-generating element (so recurse this method call to find it)
  212. c. parent is nil (so treat it as Identity)
  213. d. parent is something else (so do a while loop until we hit an a, b, or c above)
  214. */
  215. SVGElement* parentSVGElement = transformableOrSVGSVGElement;
  216. while( (parentSVGElement = (SVGElement*) parentSVGElement.parentNode) != nil )
  217. {
  218. if( [parentSVGElement conformsToProtocol:@protocol(SVGTransformable)] )
  219. {
  220. parentAbsoluteTransform = [self transformAbsoluteIncludingViewportForTransformableOrViewportEstablishingElement:parentSVGElement];
  221. break;
  222. }
  223. if( [parentSVGElement isKindOfClass:[SVGSVGElement class]] )
  224. {
  225. parentAbsoluteTransform = [self transformAbsoluteIncludingViewportForTransformableOrViewportEstablishingElement:parentSVGElement];
  226. break;
  227. }
  228. }
  229. /**
  230. TOTAL absolute based on the parent transform with relative (and possible viewport) transforms
  231. */
  232. CGAffineTransform result = CGAffineTransformConcat( [self transformRelativeIncludingViewportForTransformableOrViewportEstablishingElement:transformableOrSVGSVGElement], parentAbsoluteTransform );
  233. //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);
  234. return result;
  235. }
  236. +(void) configureCALayer:(CALayer*) layer usingElement:(SVGElement*) nonStylableElement
  237. {
  238. layer.name = nonStylableElement.identifier;
  239. [layer setValue:nonStylableElement.identifier forKey:kSVGElementIdentifier];
  240. #if FORCE_RASTERIZE_LAYERS
  241. if ([layer respondsToSelector:@selector(setShouldRasterize:)]) {
  242. [layer performSelector:@selector(setShouldRasterize:)
  243. withObject:[NSNumber numberWithBool:YES]];
  244. }
  245. /** If you're going to rasterize, Apple's code is dumb, and needs to be "told" if its using a Retina display */
  246. layer.contentsScale = [[UIScreen mainScreen] scale];
  247. layer.rasterizationScale = _shapeLayer.contentsScale;
  248. #endif
  249. if( [nonStylableElement conformsToProtocol:@protocol(SVGStylable)])
  250. {
  251. SVGElement<SVGStylable>* stylableElement = (SVGElement<SVGStylable>*) nonStylableElement;
  252. NSString* actualOpacity = [stylableElement cascadedValueForStylableProperty:@"opacity" inherit:NO];
  253. layer.opacity = actualOpacity.length > 0 ? [actualOpacity floatValue] : 1.0f; // svg's "opacity" defaults to 1!
  254. // Apply fill-rule on layer (only CAShapeLayer)
  255. NSString *fillRule = [stylableElement cascadedValueForStylableProperty:@"fill-rule"];
  256. if([fillRule isEqualToString:@"evenodd"] && [layer isKindOfClass:[CAShapeLayer class]]){
  257. CAShapeLayer *shapeLayer = (CAShapeLayer *)layer;
  258. shapeLayer.fillRule = @"even-odd";
  259. }
  260. }
  261. }
  262. +(CALayer *) newCALayerForPathBasedSVGElement:(SVGElement<SVGTransformable>*) svgElement withPath:(CGPathRef) pathRelative
  263. {
  264. CAShapeLayer* _shapeLayer = [CAShapeLayerWithHitTest layer];
  265. [self configureCALayer:_shapeLayer usingElement:svgElement];
  266. NSString* actualStroke = [svgElement cascadedValueForStylableProperty:@"stroke"];
  267. if (!actualStroke)
  268. actualStroke = @"none";
  269. NSString* actualStrokeWidth = [svgElement cascadedValueForStylableProperty:@"stroke-width"];
  270. CGFloat strokeWidth = 1.0;
  271. if (actualStrokeWidth)
  272. {
  273. SVGRect r = ((SVGSVGElement*) svgElement.viewportElement).viewport;
  274. strokeWidth = [[SVGLength svgLengthFromNSString:actualStrokeWidth]
  275. pixelsValueWithDimension: hypot(r.width, r.height)];
  276. }
  277. /** transform our LOCAL path into ABSOLUTE space */
  278. CGAffineTransform transformAbsolute = [self transformAbsoluteIncludingViewportForTransformableOrViewportEstablishingElement:svgElement];
  279. // calculate the rendered dimensions of the path
  280. CGRect r = CGRectInset(CGPathGetBoundingBox(pathRelative), -strokeWidth/2., -strokeWidth/2.);
  281. CGRect transformedPathBB = CGRectApplyAffineTransform(r, transformAbsolute);
  282. CGPathRef pathToPlaceInLayer = CGPathCreateCopyByTransformingPath(pathRelative, &transformAbsolute);
  283. /** find out the ABSOLUTE BOUNDING BOX of our transformed path */
  284. //DEBUG ONLY: CGRect unTransformedPathBB = CGPathGetBoundingBox( _pathRelative );
  285. #if IMPROVE_PERFORMANCE_BY_WORKING_AROUND_APPLE_FRAME_ALIGNMENT_BUG
  286. transformedPathBB = CGRectIntegral( transformedPathBB ); // ridiculous but improves performance of apple's code by up to 50% !
  287. #endif
  288. /** NB: when we set the _shapeLayer.frame, it has a *side effect* of moving the path itself - so, in order to prevent that,
  289. because Apple didn't provide a BOOL to disable that "feature", we have to pre-shift the path forwards by the amount it
  290. will be shifted backwards */
  291. CGPathRef finalPath = CGPathCreateByOffsettingPath( pathToPlaceInLayer, transformedPathBB.origin.x, transformedPathBB.origin.y );
  292. /** Can't use this - iOS 5 only! path = CGPathCreateCopyByTransformingPath(path, transformFromSVGUnitsToScreenUnits ); */
  293. _shapeLayer.path = finalPath;
  294. CGPathRelease(finalPath);
  295. /**
  296. NB: this line, by changing the FRAME of the layer, has the side effect of also changing the CGPATH's position in absolute
  297. space! This is why we needed the "CGPathRef finalPath =" line a few lines above...
  298. */
  299. _shapeLayer.frame = transformedPathBB;
  300. CGRect localRect = CGRectMake(0, 0, CGRectGetWidth(transformedPathBB), CGRectGetHeight(transformedPathBB));
  301. //DEBUG ONLY: CGRect shapeLayerFrame = _shapeLayer.frame;
  302. CAShapeLayer* strokeLayer = _shapeLayer;
  303. CAShapeLayer* fillLayer = _shapeLayer;
  304. if( strokeWidth > 0
  305. && (! [@"none" isEqualToString:actualStroke]) )
  306. {
  307. /*
  308. 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!)
  309. */
  310. CGSize fakeSize = CGSizeMake( strokeWidth, strokeWidth );
  311. fakeSize = CGSizeApplyAffineTransform( fakeSize, transformAbsolute );
  312. strokeLayer.lineWidth = hypot(fakeSize.width, fakeSize.height)/M_SQRT2;
  313. NSString* actualStrokeOpacity = [svgElement cascadedValueForStylableProperty:@"stroke-opacity"];
  314. strokeLayer.strokeColor = [self parseStrokeForElement:svgElement fromStroke:actualStroke andOpacity:actualStrokeOpacity];
  315. /**
  316. Stroke dash array
  317. */
  318. NSString *dashArrayString = [svgElement cascadedValueForStylableProperty:@"stroke-dasharray"];
  319. if(dashArrayString != nil && ![dashArrayString isEqualToString:@""]) {
  320. NSArray *dashArrayStringComponents = [dashArrayString componentsSeparatedByString:@" "];
  321. if( [dashArrayStringComponents count] < 2 )
  322. { // min 2 elements required, perhaps it's comma-separated:
  323. dashArrayStringComponents = [dashArrayString componentsSeparatedByString:@","];
  324. }
  325. if( [dashArrayStringComponents count] > 1 )
  326. {
  327. BOOL valid = NO;
  328. NSMutableArray *dashArray = [NSMutableArray array];
  329. for( NSString *n in dashArrayStringComponents ){
  330. [dashArray addObject:[NSNumber numberWithFloat:[n floatValue]]];
  331. if( !valid && [n floatValue] != 0 ){
  332. // avoid 'CGContextSetLineDash: invalid dash array: at least one element must be non-zero.'
  333. valid = YES;
  334. }
  335. }
  336. if( valid ){
  337. strokeLayer.lineDashPattern = dashArray;
  338. }
  339. }
  340. }
  341. /**
  342. Line joins + caps: butt / square / miter
  343. */
  344. NSString* actualLineCap = [svgElement cascadedValueForStylableProperty:@"stroke-linecap"];
  345. NSString* actualLineJoin = [svgElement cascadedValueForStylableProperty:@"stroke-linejoin"];
  346. NSString* actualMiterLimit = [svgElement cascadedValueForStylableProperty:@"stroke-miterlimit"];
  347. if( actualLineCap.length > 0 )
  348. {
  349. if( [actualLineCap isEqualToString:@"butt"] )
  350. strokeLayer.lineCap = kCALineCapButt;
  351. else if( [actualLineCap isEqualToString:@"round"] )
  352. strokeLayer.lineCap = kCALineCapRound;
  353. else if( [actualLineCap isEqualToString:@"square"] )
  354. strokeLayer.lineCap = kCALineCapSquare;
  355. }
  356. if( actualLineJoin.length > 0 )
  357. {
  358. if( [actualLineJoin isEqualToString:@"miter"] )
  359. strokeLayer.lineJoin = kCALineJoinMiter;
  360. else if( [actualLineJoin isEqualToString:@"round"] )
  361. strokeLayer.lineJoin = kCALineJoinRound;
  362. else if( [actualLineJoin isEqualToString:@"bevel"] )
  363. strokeLayer.lineJoin = kCALineJoinBevel;
  364. }
  365. if( actualMiterLimit.length > 0 )
  366. {
  367. strokeLayer.miterLimit = [actualMiterLimit floatValue];
  368. }
  369. if ( [actualStroke hasPrefix:@"url"] )
  370. {
  371. // need a new fill layer because the stroke layer is becoming a mask
  372. fillLayer = [CAShapeLayerWithHitTest layer];
  373. fillLayer.frame = strokeLayer.frame;
  374. fillLayer.opacity = strokeLayer.opacity;
  375. fillLayer.path = strokeLayer.path;
  376. NSArray *strokeArgs = [actualStroke componentsSeparatedByCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
  377. NSString *strokeIdArg = strokeArgs.firstObject;
  378. NSRange idKeyRange = NSMakeRange(5, strokeIdArg.length - 6);
  379. NSString* strokeId = [strokeIdArg substringWithRange:idKeyRange];
  380. // 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.
  381. CGRect boundingBox = strokeLayer.frame;
  382. CGRect pathBoundingBox = CGPathGetPathBoundingBox(pathRelative);
  383. if (!CGRectIsEmpty(pathBoundingBox)) {
  384. // apply gradient
  385. SVGGradientLayer *gradientLayer = [self getGradientLayerWithId:strokeId forElement:svgElement withRect:boundingBox transform:transformAbsolute];
  386. if (gradientLayer) {
  387. strokeLayer.frame = localRect;
  388. strokeLayer.fillColor = nil;
  389. strokeLayer.strokeColor = [UIColor blackColor].CGColor;
  390. gradientLayer.mask = strokeLayer;
  391. strokeLayer = (CAShapeLayer*) gradientLayer;
  392. } else {
  393. // no gradient, fallback
  394. }
  395. } else {
  396. // no boundingBox, fallback
  397. }
  398. }
  399. }
  400. else
  401. {
  402. if( [@"none" isEqualToString:actualStroke] )
  403. {
  404. strokeLayer.strokeColor = nil; // This is how you tell Apple that the stroke is disabled; a strokewidth of 0 will NOT achieve this
  405. strokeLayer.lineWidth = 0.0f; // MUST set this explicitly, or Apple assumes 1.0
  406. }
  407. else
  408. {
  409. strokeLayer.lineWidth = 1.0f; // default value from SVG spec
  410. }
  411. }
  412. NSString* actualFill = [svgElement cascadedValueForStylableProperty:@"fill"];
  413. NSString* actualFillOpacity = [svgElement cascadedValueForStylableProperty:@"fill-opacity"];
  414. if ( [actualFill hasPrefix:@"url"] )
  415. {
  416. NSArray *fillArgs = [actualFill componentsSeparatedByCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
  417. NSString *fillIdArg = fillArgs.firstObject;
  418. NSRange idKeyRange = NSMakeRange(5, fillIdArg.length - 6);
  419. NSString* fillId = [fillIdArg substringWithRange:idKeyRange];
  420. /** Replace the return layer with a special layer using the URL fill */
  421. /** fetch the fill layer by URL using the DOM */
  422. SVGGradientLayer *gradientLayer = [self getGradientLayerWithId:fillId forElement:svgElement withRect:fillLayer.frame
  423. transform:transformAbsolute];
  424. if (gradientLayer) {
  425. CAShapeLayer* maskLayer = [CAShapeLayer layer];
  426. maskLayer.frame = localRect;
  427. maskLayer.path = fillLayer.path;
  428. maskLayer.fillColor = [UIColor blackColor].CGColor;
  429. maskLayer.strokeColor = nil;
  430. gradientLayer.mask = maskLayer;
  431. gradientLayer.frame = fillLayer.frame;
  432. fillLayer = (CAShapeLayer* )gradientLayer;
  433. } else {
  434. // no gradient, fallback
  435. }
  436. }
  437. else if( actualFill.length > 0 || actualFillOpacity.length > 0 )
  438. {
  439. fillLayer.fillColor = [self parseFillForElement:svgElement fromFill:actualFill andOpacity:actualFillOpacity];
  440. }
  441. CGPathRelease(pathToPlaceInLayer);
  442. NSString* actualOpacity = [svgElement cascadedValueForStylableProperty:@"opacity" inherit:NO];
  443. fillLayer.opacity = actualOpacity.length > 0 ? [actualOpacity floatValue] : 1; // unusually, the "opacity" attribute defaults to 1, not 0
  444. if (strokeLayer == fillLayer)
  445. {
  446. return strokeLayer;
  447. }
  448. CALayer* combined = [CALayer layer];
  449. combined.frame = strokeLayer.frame;
  450. strokeLayer.frame = localRect;
  451. if ([strokeLayer isKindOfClass:[CAShapeLayer class]])
  452. strokeLayer.fillColor = nil;
  453. fillLayer.frame = localRect;
  454. [combined addSublayer:fillLayer];
  455. [combined addSublayer:strokeLayer];
  456. return combined;
  457. }
  458. + (SVGGradientLayer*)getGradientLayerWithId:(NSString*)gradId
  459. forElement:(SVGElement*)svgElement
  460. withRect:(CGRect)r
  461. transform:(CGAffineTransform)transform
  462. {
  463. /** Replace the return layer with a special layer using the URL fill */
  464. /** fetch the fill layer by URL using the DOM */
  465. 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 );
  466. SVGGradientElement* svgGradient = (SVGGradientElement*) [svgElement.rootOfCurrentDocumentFragment getElementById:gradId];
  467. if (svgGradient == nil) {
  468. // SVG spec allows referenced gradient not exist and will use fallback color
  469. 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 );
  470. }
  471. [svgGradient synthesizeProperties];
  472. SVGGradientLayer *gradientLayer = [svgGradient newGradientLayerForObjectRect:r
  473. viewportRect:svgElement.rootOfCurrentDocumentFragment.viewBox
  474. transform:transform];
  475. return gradientLayer;
  476. }
  477. +(CGColorRef) parseFillForElement:(SVGElement *)svgElement
  478. {
  479. NSString* actualFill = [svgElement cascadedValueForStylableProperty:@"fill"];
  480. NSString* actualFillOpacity = [svgElement cascadedValueForStylableProperty:@"fill-opacity"];
  481. return [self parseFillForElement:svgElement fromFill:actualFill andOpacity:actualFillOpacity];
  482. }
  483. +(CGColorRef) parseFillForElement:(SVGElement *)svgElement fromFill:(NSString *)actualFill andOpacity:(NSString *)actualFillOpacity
  484. {
  485. return [self parsePaintColorForElement:svgElement paintColor:actualFill paintOpacity:actualFillOpacity defaultColor:@"black"];
  486. }
  487. +(CGColorRef) parseStrokeForElement:(SVGElement *)svgElement
  488. {
  489. NSString* actualStroke = [svgElement cascadedValueForStylableProperty:@"stroke"];
  490. NSString* actualStrokeOpacity = [svgElement cascadedValueForStylableProperty:@"stroke-opacity"];
  491. return [self parseStrokeForElement:svgElement fromStroke:actualStroke andOpacity:actualStrokeOpacity];
  492. }
  493. +(CGColorRef) parseStrokeForElement:(SVGElement *)svgElement fromStroke:(NSString *)actualStroke andOpacity:(NSString *)actualStrokeOpacity
  494. {
  495. return [self parsePaintColorForElement:svgElement paintColor:actualStroke paintOpacity:actualStrokeOpacity defaultColor:@"none"];
  496. }
  497. /**
  498. Spec: https://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint
  499. `fill` or `stroke` allows paint color. This should actually be a <paint> interface.
  500. `fill` default color is `black`, while `stroke` default color is `none`
  501. */
  502. +(CGColorRef)parsePaintColorForElement:(SVGElement *)svgElement paintColor:(NSString *)paintColor paintOpacity:(NSString *)paintOpacity defaultColor:(NSString *)defaultColor {
  503. CGColorRef colorRef = NULL;
  504. if (!paintColor) {
  505. paintColor = @"none";
  506. }
  507. if ([paintColor isEqualToString:@"none"])
  508. {
  509. return NULL;
  510. }
  511. // there may be a url before the actual color like `url(#grad) #0f0`, parse it
  512. NSString *actualPaintColor;
  513. NSString *actualPaintOpacity = paintOpacity;
  514. NSArray *paintArgs = [paintColor componentsSeparatedByCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
  515. if ([paintColor hasPrefix:@"url"]) {
  516. if (paintArgs.count > 1) {
  517. actualPaintColor = paintArgs[1];
  518. }
  519. } else {
  520. actualPaintColor = paintColor;
  521. }
  522. if( actualPaintColor.length > 0 || actualPaintOpacity.length > 0 ) {
  523. SVGColor paintColorSVGColor;
  524. if (actualPaintColor.length > 0) {
  525. if (![actualPaintColor isEqualToString:@"none"]) {
  526. paintColorSVGColor = SVGColorFromString([actualPaintColor UTF8String]); // have to use the intermediate of an SVGColor so that we can over-ride the ALPHA component in next line
  527. } else {
  528. return NULL;
  529. }
  530. } else {
  531. if (![defaultColor isEqualToString:@"none"]) {
  532. paintColorSVGColor = SVGColorFromString([actualPaintColor UTF8String]);
  533. } else {
  534. return NULL;
  535. }
  536. }
  537. if( actualPaintOpacity.length > 0 )
  538. paintColorSVGColor.a = (uint8_t) ([actualPaintOpacity floatValue] * 0xFF);
  539. colorRef = CGColorWithSVGColor(paintColorSVGColor);
  540. }
  541. else
  542. {
  543. if (![defaultColor isEqualToString:@"none"]) {
  544. colorRef = CGColorWithSVGColor(SVGColorFromString([defaultColor UTF8String]));
  545. } else {
  546. return NULL;
  547. }
  548. }
  549. return colorRef;
  550. }
  551. +(void) parsePreserveAspectRatioFor:(Element<SVGFitToViewBox>*) element
  552. {
  553. element.preserveAspectRatio = [SVGAnimatedPreserveAspectRatio new]; // automatically sets defaults
  554. NSString* stringPreserveAspectRatio = [element getAttribute:@"preserveAspectRatio"];
  555. if( stringPreserveAspectRatio.length > 0 )
  556. {
  557. NSArray* aspectRatioCommands = [stringPreserveAspectRatio componentsSeparatedByString:@" "];
  558. for( NSString* aspectRatioCommand in aspectRatioCommands )
  559. {
  560. if( [aspectRatioCommand isEqualToString:@"meet"]) /** NB this is default anyway. Dont technically need to set it */
  561. element.preserveAspectRatio.baseVal.meetOrSlice = SVG_MEETORSLICE_MEET;
  562. else if( [aspectRatioCommand isEqualToString:@"slice"])
  563. element.preserveAspectRatio.baseVal.meetOrSlice = SVG_MEETORSLICE_SLICE;
  564. else if( [aspectRatioCommand isEqualToString:@"xMinYMin"])
  565. element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMINYMIN;
  566. else if( [aspectRatioCommand isEqualToString:@"xMinYMid"])
  567. element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMINYMID;
  568. else if( [aspectRatioCommand isEqualToString:@"xMinYMax"])
  569. element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMINYMAX;
  570. else if( [aspectRatioCommand isEqualToString:@"xMidYMin"])
  571. element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMIDYMIN;
  572. else if( [aspectRatioCommand isEqualToString:@"xMidYMid"])
  573. element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMIDYMID;
  574. else if( [aspectRatioCommand isEqualToString:@"xMidYMax"])
  575. element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMIDYMAX;
  576. else if( [aspectRatioCommand isEqualToString:@"xMaxYMin"])
  577. element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMAXYMIN;
  578. else if( [aspectRatioCommand isEqualToString:@"xMaxYMid"])
  579. element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMAXYMID;
  580. else if( [aspectRatioCommand isEqualToString:@"xMaxYMax"])
  581. element.preserveAspectRatio.baseVal.align = SVG_PRESERVEASPECTRATIO_XMAXYMAX;
  582. else
  583. {
  584. SVGKitLogWarn(@"Found unexpected preserve-aspect-ratio command inside element's 'preserveAspectRatio' attribute. Command = '%@'", aspectRatioCommand );
  585. }
  586. }
  587. }
  588. }
  589. @end