SVGTextElement.m 18 KB

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