SVGTextElement.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. #import "SVGTextElement.h"
  2. #import <CoreText/CoreText.h>
  3. #import "SVGElement_ForParser.h" // to resolve Xcode circular dependencies; in long term, parsing SHOULD NOT HAPPEN inside any class whose name starts "SVG" (because those are reserved classes for the SVG Spec)
  4. #import "SVGGradientLayer.h"
  5. #import "SVGHelperUtilities.h"
  6. #import "SVGUtils.h"
  7. @implementation SVGTextElement
  8. @synthesize transform; // each SVGElement subclass that conforms to protocol "SVGTransformable" has to re-synthesize this to work around bugs in Apple's Objective-C 2.0 design that don't allow @properties to be extended by categories / protocols
  9. - (CALayer *) newLayer
  10. {
  11. /**
  12. BY DESIGN: we work out the positions of all text in ABSOLUTE space, and then construct the Apple CALayers and CATextLayers around
  13. them, as required.
  14. Because: Apple's classes REQUIRE us to provide a lot of this info up-front. Sigh
  15. And: SVGKit works by pre-baking everything into position (its faster, and avoids Apple's broken CALayer.transform property)
  16. */
  17. CGAffineTransform textTransformAbsolute = [SVGHelperUtilities transformAbsoluteIncludingViewportForTransformableOrViewportEstablishingElement:self];
  18. /** add on the local x,y that will NOT BE iNCLUDED IN THE TRANSFORM
  19. AUTOMATICALLY BECAUSE THEY ARE NOT TRANSFORM COMMANDS IN SVG SPEC!!
  20. -- but they ARE part of the "implicit transform" of text elements!! (bad SVG Spec design :( )
  21. NB: the local bits (x/y offset) have to be pre-transformed by
  22. */
  23. CGAffineTransform textTransformAbsoluteWithLocalPositionOffset = CGAffineTransformConcat( CGAffineTransformMakeTranslation( [self.x pixelsValue], [self.y pixelsValue]), textTransformAbsolute);
  24. /**
  25. Apple's CATextLayer is poor - one of those classes Apple hasn't finished writing?
  26. It's incompatible with UIFont (Apple states it is so), and it DOES NOT WORK by default:
  27. If you assign a font, and a font size, and text ... you get a blank empty layer of
  28. size 0,0
  29. Because Apple requires you to ALSO do all the work of calculating the font size, shape,
  30. position etc.
  31. But its the easiest way to get FULL control over size/position/rotation/etc in a CALayer
  32. */
  33. /**
  34. Create font based on many information (font-family, font-weight, etc), fallback to system font when there are no available font matching the information.
  35. */
  36. UIFont *font = [SVGTextElement matchedFontWithElement:self];
  37. /** Convert the size down using the SVG transform at this point, before we calc the frame size etc */
  38. // effectiveFontSize = CGSizeApplyAffineTransform( CGSizeMake(0,effectiveFontSize), textTransformAbsolute ).height; // NB important that we apply a transform to a "CGSize" here, so that Apple's library handles worrying about whether to ignore skew transforms etc
  39. /** Convert all whitespace to spaces, and trim leading/trailing (SVG doesn't support leading/trailing whitespace, and doesnt support CR LF etc) */
  40. NSString* effectiveText = self.textContent; // FIXME: this is a TEMPORARY HACK, UNTIL PROPER PARSING OF <TSPAN> ELEMENTS IS ADDED
  41. effectiveText = [effectiveText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  42. effectiveText = [effectiveText stringByReplacingOccurrencesOfString:@"\n" withString:@" "];
  43. /**
  44. Stroke color && stroke width
  45. Apple's `CATextLayer` can not stroke gradient on the layer (we can only fill the layer)
  46. */
  47. CGColorRef strokeColor = [SVGHelperUtilities parseStrokeForElement:self];
  48. CGFloat strokeWidth = 0;
  49. NSString* actualStrokeWidth = [self cascadedValueForStylableProperty:@"stroke-width"];
  50. if (actualStrokeWidth)
  51. {
  52. SVGRect r = ((SVGSVGElement*)self.viewportElement).viewport;
  53. strokeWidth = [[SVGLength svgLengthFromNSString:actualStrokeWidth]
  54. pixelsValueWithDimension: hypot(r.width, r.height)];
  55. }
  56. /**
  57. Fill color
  58. Apple's `CATextLayer` can be filled using mask.
  59. */
  60. CGColorRef fillColor = [SVGHelperUtilities parseFillForElement:self];
  61. /** Calculate
  62. 1. Create an attributed string (Apple's APIs are hard-coded to require this)
  63. 2. Set the font to be the correct one + correct size for whole string, inside the string
  64. 3. Ask apple how big the final thing should be
  65. 4. Use that to provide a layer.frame
  66. */
  67. NSMutableAttributedString* attributedString = [[NSMutableAttributedString alloc] initWithString:effectiveText];
  68. NSRange stringRange = NSMakeRange(0, attributedString.string.length);
  69. [attributedString addAttribute:NSFontAttributeName
  70. value:font
  71. range:stringRange];
  72. if (fillColor) {
  73. [attributedString addAttribute:NSForegroundColorAttributeName
  74. value:(__bridge id)fillColor
  75. range:stringRange];
  76. }
  77. if (strokeWidth != 0 && strokeColor) {
  78. [attributedString addAttribute:NSStrokeColorAttributeName
  79. value:(__bridge id)strokeColor
  80. range:stringRange];
  81. // If both fill && stroke, pass negative value; only fill, pass positive value
  82. // A typical value for outlined text is 3.0. Actually this is not so accurate, but until we directly draw the text glyph using Core Text, we can not control the detailed stroke width follow SVG spec
  83. CGFloat strokeValue = strokeWidth / 3.0;
  84. if (fillColor) {
  85. strokeValue = -strokeValue;
  86. }
  87. [attributedString addAttribute:NSStrokeWidthAttributeName
  88. value:@(strokeValue)
  89. range:stringRange];
  90. }
  91. CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString( (CFMutableAttributedStringRef) attributedString );
  92. CGSize suggestedUntransformedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), NULL);
  93. CFRelease(framesetter);
  94. CGRect unTransformedFinalBounds = CGRectMake( 0,
  95. 0,
  96. suggestedUntransformedSize.width,
  97. suggestedUntransformedSize.height); // everything's been pre-scaled by [self transformAbsolute]
  98. CATextLayer *label = [[CATextLayer alloc] init];
  99. [SVGHelperUtilities configureCALayer:label usingElement:self];
  100. /** This is complicated for three reasons.
  101. Partly: Apple and SVG use different defitions for the "origin" of a piece of text
  102. Partly: Bugs in Apple's CoreText
  103. Partly: flaws in Apple's CALayer's handling of frame,bounds,position,anchorPoint,affineTransform
  104. 1. CALayer.frame DOES NOT EXIST AS A REAL PROPERTY - if you read Apple's docs you eventually realise it is fake. Apple explicitly says it is "not defined". They should DELETE IT from their API!
  105. 2. CALayer.bounds and .position ARE NOT AFFECTED BY .affineTransform - only the contents of the layer is affected
  106. 3. SVG defines two SEMI-INCOMPATIBLE ways of positioning TEXT objects, that we have to correctly combine here.
  107. 4. So ... to apply a transform to the layer text:
  108. i. find the TRANSFORM
  109. ii. merge it with the local offset (.x and .y from SVG) - which defaults to (0,0)
  110. iii. apply that to the layer
  111. iv. set the position to 0
  112. v. BECAUSE SVG AND APPLE DEFINE ORIGIN DIFFERENTLY: subtract the "untransformed" height of the font ... BUT: pre-transformed ONLY BY the 'multiplying (non-translating)' part of the TRANSFORM.
  113. vi. set the bounds to be (whatever Apple's CoreText says is necessary to render TEXT at FONT SIZE, with NO TRANSFORMS)
  114. */
  115. label.bounds = unTransformedFinalBounds;
  116. /** NB: specific to Apple: the "origin" is the TOP LEFT corner of first line of text, whereas SVG uses the font's internal origin
  117. (which is BOTTOM LEFT CORNER OF A LETTER SUCH AS 'a' OR 'x' THAT SITS ON THE BASELINE ... so we have to make the FRAME start "font leading" higher up
  118. WARNING: Apple's font-rendering system has some nasty bugs (c.f. StackOverflow)
  119. We TRIED to use the font's built-in numbers to correct the position, but Apple's own methods often report incorrect values,
  120. and/or Apple has deprecated REQUIRED methods in their API (with no explanation - e.g. "font leading")
  121. If/when Apple fixes their bugs - or if you know enough about their API's to workaround the bugs, feel free to fix this code.
  122. */
  123. CTLineRef line = CTLineCreateWithAttributedString( (CFMutableAttributedStringRef) attributedString );
  124. CGFloat ascent = 0;
  125. CTLineGetTypographicBounds(line, &ascent, NULL, NULL);
  126. CFRelease(line);
  127. CGFloat offsetToConvertSVGOriginToAppleOrigin = -ascent;
  128. CGSize fakeSizeToApplyNonTranslatingPartsOfTransform = CGSizeMake( 0, offsetToConvertSVGOriginToAppleOrigin);
  129. label.position = CGPointMake( 0,
  130. 0 + CGSizeApplyAffineTransform( fakeSizeToApplyNonTranslatingPartsOfTransform, textTransformAbsoluteWithLocalPositionOffset).height);
  131. NSString *textAnchor = [self cascadedValueForStylableProperty:@"text-anchor"];
  132. if( [@"middle" isEqualToString:textAnchor] )
  133. label.anchorPoint = CGPointMake(0.5, 0.0);
  134. else if( [@"end" isEqualToString:textAnchor] )
  135. label.anchorPoint = CGPointMake(1.0, 0.0);
  136. else
  137. label.anchorPoint = CGPointZero; // WARNING: SVG applies transforms around the top-left as origin, whereas Apple defaults to center as origin, so we tell Apple to work "like SVG" here.
  138. label.affineTransform = textTransformAbsoluteWithLocalPositionOffset;
  139. label.string = [attributedString copy];
  140. label.alignmentMode = kCAAlignmentLeft;
  141. #if SVGKIT_MAC
  142. label.contentsScale = [[NSScreen mainScreen] backingScaleFactor];
  143. #else
  144. label.contentsScale = [[UIScreen mainScreen] scale];
  145. #endif
  146. return [self newCALayerForTextLayer:label transformAbsolute:textTransformAbsolute];
  147. /** VERY USEFUL when trying to debug text issues:
  148. label.backgroundColor = [UIColor colorWithRed:0.5 green:0 blue:0 alpha:0.5].CGColor;
  149. label.borderColor = [UIColor redColor].CGColor;
  150. //DEBUG: SVGKitLogVerbose(@"font size %2.1f at %@ ... final frame of layer = %@", effectiveFontSize, NSStringFromCGPoint(transformedOrigin), NSStringFromCGRect(label.frame));
  151. */
  152. }
  153. -(CALayer *) newCALayerForTextLayer:(CATextLayer *)label transformAbsolute:(CGAffineTransform)transformAbsolute
  154. {
  155. CALayer *fillLayer = label;
  156. NSString* actualFill = [self cascadedValueForStylableProperty:@"fill"];
  157. if ( [actualFill hasPrefix:@"url"] )
  158. {
  159. NSArray *fillArgs = [actualFill componentsSeparatedByCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
  160. NSString *fillIdArg = fillArgs.firstObject;
  161. NSRange idKeyRange = NSMakeRange(5, fillIdArg.length - 6);
  162. NSString* fillId = [fillIdArg substringWithRange:idKeyRange];
  163. /** Replace the return layer with a special layer using the URL fill */
  164. /** fetch the fill layer by URL using the DOM */
  165. SVGGradientLayer *gradientLayer = [SVGHelperUtilities getGradientLayerWithId:fillId forElement:self withRect:label.frame transform:transformAbsolute];
  166. if (gradientLayer) {
  167. gradientLayer.mask = label;
  168. fillLayer = gradientLayer;
  169. } else {
  170. // no gradient, fallback
  171. }
  172. }
  173. NSString* actualOpacity = [self cascadedValueForStylableProperty:@"opacity" inherit:NO];
  174. fillLayer.opacity = actualOpacity.length > 0 ? [actualOpacity floatValue] : 1; // unusually, the "opacity" attribute defaults to 1, not 0
  175. return fillLayer;
  176. }
  177. /**
  178. Return the best matched font with all posible CSS font property (like `font-family`, `font-size`, etc)
  179. @param svgElement svgElement
  180. @return The matched font, or fallback to system font, non-nil
  181. */
  182. + (UIFont *)matchedFontWithElement:(SVGElement *)svgElement {
  183. // Using top-level API to walkthough all availble font-family
  184. NSString *actualSize = [svgElement cascadedValueForStylableProperty:@"font-size"];
  185. NSString *actualFamily = [svgElement cascadedValueForStylableProperty:@"font-family"];
  186. // TODO- Using font descriptor to match best font consider `font-style`, `font-weight`
  187. NSString *actualFontStyle = [svgElement cascadedValueForStylableProperty:@"font-style"];
  188. NSString *actualFontWeight = [svgElement cascadedValueForStylableProperty:@"font-weight"];
  189. NSString *actualFontStretch = [svgElement cascadedValueForStylableProperty:@"font-stretch"];
  190. CGFloat effectiveFontSize = (actualSize.length > 0) ? [actualSize floatValue] : 12; // I chose 12. I couldn't find an official "default" value in the SVG spec.
  191. NSArray<NSString *> *actualFontFamilies = [SVGTextElement fontFamiliesWithCSSValue:actualFamily];
  192. NSString *matchedFontFamily;
  193. if (actualFontFamilies) {
  194. // walkthrough all available font-families to find the best matched one
  195. NSSet<NSString *> *availableFontFamilies;
  196. #if SVGKIT_MAC
  197. availableFontFamilies = [NSSet setWithArray:NSFontManager.sharedFontManager.availableFontFamilies];
  198. #else
  199. availableFontFamilies = [NSSet setWithArray:UIFont.familyNames];
  200. #endif
  201. for (NSString *fontFamily in actualFontFamilies) {
  202. if ([availableFontFamilies containsObject:fontFamily]) {
  203. matchedFontFamily = fontFamily;
  204. break;
  205. }
  206. }
  207. }
  208. // we provide enough hint information, let Core Text using their algorithm to detect which fontName should be used
  209. // if `matchedFontFamily` is nil, use the system default font family instead (allows `font-weight` these information works)
  210. NSDictionary *attributes = [self fontAttributesWithFontFamily:matchedFontFamily fontStyle:actualFontStyle fontWeight:actualFontWeight fontStretch:actualFontStretch];
  211. CTFontDescriptorRef descriptor = CTFontDescriptorCreateWithAttributes((__bridge CFDictionaryRef)attributes);
  212. CTFontRef fontRef = CTFontCreateWithFontDescriptor(descriptor, effectiveFontSize, NULL);
  213. UIFont *font = (__bridge_transfer UIFont *)fontRef;
  214. return font;
  215. }
  216. /**
  217. Convert CSS font detailed information into Core Text descriptor attributes (determine the best matched font).
  218. @param fontFamily fontFamily
  219. @param fontStyle fontStyle
  220. @param fontWeight fontWeight
  221. @param fontStretch fontStretch
  222. @return Core Text descriptor attributes
  223. */
  224. + (NSDictionary *)fontAttributesWithFontFamily:(NSString *)fontFamily fontStyle:(NSString *)fontStyle fontWeight:(NSString *)fontWeight fontStretch:(NSString *)fontStretch {
  225. // Default value
  226. if (!fontFamily.length) fontFamily = [self systemDefaultFontFamily];
  227. if (!fontStyle.length) fontStyle = @"normal";
  228. if (!fontWeight.length) fontWeight = @"normal";
  229. if (!fontStretch.length) fontStretch = @"normal";
  230. NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
  231. attributes[(__bridge NSString *)kCTFontFamilyNameAttribute] = fontFamily;
  232. // font-weight is in the sub-dictionary
  233. NSMutableDictionary *traits = [NSMutableDictionary dictionary];
  234. // CSS font weight is from 0-1000
  235. CGFloat weight;
  236. if ([fontWeight isEqualToString:@"normal"]) {
  237. weight = 400;
  238. } else if ([fontWeight isEqualToString:@"bold"]) {
  239. weight = 700;
  240. } else if ([fontWeight isEqualToString:@"bolder"]) {
  241. weight = 900;
  242. } else if ([fontWeight isEqualToString:@"lighter"]) {
  243. weight = 100;
  244. } else {
  245. CGFloat value = [fontWeight doubleValue];
  246. weight = MIN(MAX(value, 1), 1000);
  247. }
  248. // map from CSS [1, 1000] to Core Text [-1.0, 1.0], 400 represent 0.0
  249. CGFloat coreTextFontWeight;
  250. if (weight < 400) {
  251. coreTextFontWeight = (weight - 400) / 1000 * (1 / 0.4);
  252. } else {
  253. coreTextFontWeight = (weight - 400) / 1000 * (1 / 0.6);
  254. }
  255. // CSS font style
  256. CTFontSymbolicTraits style = 0;
  257. if ([fontStyle isEqualToString:@"normal"]) {
  258. style |= 0;
  259. } else if ([fontStyle isEqualToString:@"italic"] || [fontStyle rangeOfString:@"oblique"].location != NSNotFound) {
  260. // Actually we can control the detailed slant degree via `kCTFontSlantTrait`, but it's rare usage so treat them the same, TODO in the future
  261. style |= kCTFontItalicTrait;
  262. }
  263. // CSS font stretch
  264. if ([fontStretch rangeOfString:@"condensed"].location != NSNotFound) {
  265. // Actually we can control the detailed percent via `kCTFontWidthTrait`, but it's rare usage so treat them the same, TODO in the future
  266. style |= kCTFontTraitCondensed;
  267. } else if ([fontStretch rangeOfString:@"expanded"].location != NSNotFound) {
  268. style |= kCTFontTraitExpanded;
  269. }
  270. traits[(__bridge NSString *)kCTFontSymbolicTrait] = @(style);
  271. traits[(__bridge NSString *)kCTFontWeightTrait] = @(coreTextFontWeight);
  272. attributes[(__bridge NSString *)kCTFontTraitsAttribute] = [traits copy];
  273. return [attributes copy];
  274. }
  275. /**
  276. Parse the `font-family` CSS value into array of font-family name
  277. @param value value
  278. @return array of font-family name
  279. */
  280. + (NSArray<NSString *> *)fontFamiliesWithCSSValue:(NSString *)value {
  281. if (value.length == 0) {
  282. return nil;
  283. }
  284. NSArray<NSString *> *args = [value componentsSeparatedByString:@","];
  285. if (args.count == 0) {
  286. return nil;
  287. }
  288. NSMutableArray<NSString *> *fontFamilies = [NSMutableArray arrayWithCapacity:args.count];
  289. for (NSString *arg in args) {
  290. // parse: font-family: "Goudy Bookletter 1911", sans-serif;
  291. // delete ""
  292. NSString *fontFamily = [arg stringByReplacingOccurrencesOfString:@"\"" withString:@""];
  293. // trim white space
  294. [fontFamily stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
  295. [fontFamilies addObject:fontFamily];
  296. }
  297. return [fontFamilies copy];
  298. }
  299. + (NSString *)systemDefaultFontFamily {
  300. static dispatch_once_t onceToken;
  301. static NSString *fontFamily;
  302. dispatch_once(&onceToken, ^{
  303. UIFont *font = [UIFont systemFontOfSize:12.f];
  304. fontFamily = font.familyName;
  305. });
  306. return fontFamily;
  307. }
  308. - (void)layoutLayer:(CALayer *)layer
  309. {
  310. }
  311. @end