SVGTextElement.m 18 KB

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