From 704dfe99c3a94a59a0801e72294a30ac3c71450a Mon Sep 17 00:00:00 2001 From: Ruslan Abbasov Date: Tue, 7 Jun 2022 19:20:47 +0700 Subject: [PATCH] Add support of text stroke feature(label stroke): add stroke for simple colours, need to fix gradient colors # Conflicts: # Source/Model/Nodes/SVGText.swift code refactoring, need to fix position for plain stroke labels # Conflicts: # Source/Model/Nodes/SVGText.swift chagne stroke width to ratio of font size, add getFont method change plain stroke label frame code refactoring add all in one, stoke and fill case add locations to gradient funcs add macOS support fix position change bug, change getLabelSize to getHeightOfLabel fix bug with background for filled label change Linear Gradient alignmentGuide, fix bounds for stroke label and stroke gradient change position code of gradient and stroke labels, fix decreasing height of stroke label bug code refactoring fix position for OSX and ios Add radial gradient support for iOS and OSX code refactoring change tabs to 4 spaces code refactoring code refactoring refactoring: fix createOneColorStrokeLabel fix createGradientStrokeLabel fix createFillGradientLabel fix createRadialGradientStrokeLabel and createRadialGradientFillLabel fix createOneColorFillLabel, codestyle fix delete getLabelHeight, leave only one version of SVGGUITextView add getStrokeBounds func code refactoring fix codestyle fix bugs --- SVGView.xcodeproj/project.pbxproj | 27 ++ Source/Model/Nodes/SVGText.swift | 28 +- Source/UI/SVGGUITextView.swift | 475 ++++++++++++++++++++++++++++++ 3 files changed, 518 insertions(+), 12 deletions(-) create mode 100644 Source/UI/SVGGUITextView.swift diff --git a/SVGView.xcodeproj/project.pbxproj b/SVGView.xcodeproj/project.pbxproj index 56cef95..8328163 100644 --- a/SVGView.xcodeproj/project.pbxproj +++ b/SVGView.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 4460F8F6287C310A00E50E11 /* SVGGUITextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4460F8F5287C310A00E50E11 /* SVGGUITextView.swift */; }; + 4460F8F7287C310A00E50E11 /* SVGGUITextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4460F8F5287C310A00E50E11 /* SVGGUITextView.swift */; }; 5815294025B6C8F600E8D23A /* SVGPaint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815293F25B6C8F600E8D23A /* SVGPaint.swift */; }; 5815294425B6C90F00E8D23A /* SVGColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815294325B6C90F00E8D23A /* SVGColor.swift */; }; 5815298A25B752E600E8D23A /* SVGPreserveAspectRatio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815298925B752E600E8D23A /* SVGPreserveAspectRatio.swift */; }; @@ -134,6 +136,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 4460F8F5287C310A00E50E11 /* SVGGUITextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVGGUITextView.swift; sourceTree = ""; }; 5815293F25B6C8F600E8D23A /* SVGPaint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVGPaint.swift; sourceTree = ""; }; 5815294325B6C90F00E8D23A /* SVGColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVGColor.swift; sourceTree = ""; }; 5815298925B752E600E8D23A /* SVGPreserveAspectRatio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVGPreserveAspectRatio.swift; sourceTree = ""; }; @@ -296,6 +299,7 @@ children = ( 5B1F462F251348F70068EF42 /* MBezierPath+Extension_macOS.swift */, 58A664F3283E96E700F1F6FD /* UIExtensions.swift */, + 4460F8F5287C310A00E50E11 /* SVGGUITextView.swift */, ); path = UI; sourceTree = ""; @@ -463,6 +467,7 @@ 5BE3321924E1446A00BB0D60 /* Sources */, 5BE3321A24E1446A00BB0D60 /* Frameworks */, 5BE3321B24E1446A00BB0D60 /* Resources */, + 44FD79C9287D475F00AE8899 /* ShellScript */, ); buildRules = ( ); @@ -546,6 +551,26 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 44FD79C9287D475F00AE8899 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"error: SwiftLint does not exist, download it from https://github.com/realm/SwiftLint\"\n exit 1\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 5BE3321924E1446A00BB0D60 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -589,6 +614,7 @@ 586D5C2C284295C400C79E0E /* SVGTextParser.swift in Sources */, 586D5C3228429BE600C79E0E /* SVGShapeParser.swift in Sources */, 5B1017EC25358E5600105E9A /* SVGLength.swift in Sources */, + 4460F8F6287C310A00E50E11 /* SVGGUITextView.swift in Sources */, 58869B6D25B5C52D00F6E324 /* Serializer.swift in Sources */, 5BE3327224E144B200BB0D60 /* SVGStroke.swift in Sources */, 5BE3326324E144B200BB0D60 /* SVGLine.swift in Sources */, @@ -633,6 +659,7 @@ 58A66521283E99FB00F1F6FD /* Serializations.swift in Sources */, 58A66507283E99AC00F1F6FD /* SVGPolygon.swift in Sources */, 586D5C30284298BB00C79E0E /* SVGImageParser.swift in Sources */, + 4460F8F7287C310A00E50E11 /* SVGGUITextView.swift in Sources */, 58A664FA283E991E00F1F6FD /* SVGLength.swift in Sources */, 58A6650B283E99B700F1F6FD /* SVGRect.swift in Sources */, 58A66520283E99F900F1F6FD /* Serializable.swift in Sources */, diff --git a/Source/Model/Nodes/SVGText.swift b/Source/Model/Nodes/SVGText.swift index 83f26cd..bc3381e 100644 --- a/Source/Model/Nodes/SVGText.swift +++ b/Source/Model/Nodes/SVGText.swift @@ -23,10 +23,14 @@ public class SVGText: SVGNode, ObservableObject { serializer.add("stroke", stroke) super.serialize(serializer) } - - public func contentView() -> some View { - SVGTextView(model: self) - } + + public func contentView() -> some View { + if self.stroke != nil || self.fill != nil { + return AnyView(SVGGUITextView(model: self)) + } else { + return AnyView(SVGTextView(model: self)) + } + } } struct SVGTextView: View { @@ -51,13 +55,13 @@ struct SVGTextView: View { private func filledText(fill: SVGPaint?) -> some View { Text(model.text) - .font(model.font?.toSwiftUI()) - .lineLimit(1) - .alignmentGuide(.leading) { d in d[model.textAnchor] } - .alignmentGuide(VerticalAlignment.top) { d in d[VerticalAlignment.firstTextBaseline] } - .position(x: 0, y: 0) // just to specify that positioning is global, actual coords are in transform - .apply(paint: fill) - .transformEffect(model.transform) - .frame(alignment: .topLeading) + .font(model.font?.toSwiftUI()) + .lineLimit(1) + .alignmentGuide(.leading) { d in d[model.textAnchor] } + .alignmentGuide(VerticalAlignment.top) { d in d[VerticalAlignment.firstTextBaseline] } + .position(x: 0, y: 0) // just to specify that positioning is global, actual coords are in transform + .apply(paint: fill) + .transformEffect(model.transform) + .frame(alignment: .topLeading) } } diff --git a/Source/UI/SVGGUITextView.swift b/Source/UI/SVGGUITextView.swift new file mode 100644 index 0000000..1836f7d --- /dev/null +++ b/Source/UI/SVGGUITextView.swift @@ -0,0 +1,475 @@ +import SwiftUI +#if os(OSX) +import AppKit +#else +import UIKit +#endif + +#if os(OSX) +import AppKit +public typealias MColor = NSColor +public typealias MView = NSView +public typealias MRepresentable = NSViewRepresentable +public typealias MHostingController = NSHostingController +public typealias MFont = NSFont +#else +public typealias MColor = UIColor +public typealias MView = UIView +public typealias MRepresentable = UIViewRepresentable +public typealias MHostingController = UIHostingController +public typealias MFont = UIFont +#endif + +public struct SVGGUITextView: View { + @ObservedObject var model: SVGText + + public var body: some View { + + switch getLabelColor(model: model) { + case _ as SVGLinearGradient, _ as SVGRadialGradient: + let size = getLabelSize(model: model) + StrokeTextLabel(model: model) + .lineLimit(1) + .alignmentGuide(.leading) { d in d[model.textAnchor] } + .alignmentGuide(VerticalAlignment.top) { _ in size.height } + .offset(x: -(model.stroke?.width ?? 0) / 2, y: (model.stroke?.width ?? 0) / 2) + .position(x: 0, y: 0) // just to specify that positioning is global, actual coords are in transform + .transformEffect(model.transform) + .frame(alignment: .topLeading) + .frame(minWidth: size.width, minHeight: size.height) + case _ as SVGColor: + if model.stroke?.width != nil { + let size = getLabelSize(model: model) + StrokeTextLabel(model: model) + .lineLimit(1) + .alignmentGuide(.leading) { d in d[model.textAnchor] } + .alignmentGuide(VerticalAlignment.top) { _ in size.height } + .offset(x: -(model.stroke?.width ?? 0) / 2, y: (model.stroke?.width ?? 0) / 2) + .position(x: 0, y: 0) // just to specify that positioning is global, actual coords are in transform + .transformEffect(model.transform) + .frame(alignment: .topLeading) + .frame(minWidth: size.width, minHeight: size.height) + } else { + if let fontSize = model.font?.size { + let font = Font.custom(getFontName(model: model), size: fontSize) + Text(model.text) + .font(font) + .lineLimit(1) + .alignmentGuide(.leading) { d in d[model.textAnchor] } + .alignmentGuide(VerticalAlignment.top) { d in d[VerticalAlignment.firstTextBaseline] } + .apply(paint: model.fill) + .position(x: 0, y: 0) // just to specify that positioning is global, actual coords + .transformEffect(model.transform) + .frame(alignment: .topLeading) + } else { + Text("") + } + } + default: + Text("") + } + } +} + +#if os(OSX) +// You need this class because strokeLabel should be in top left corner, +// otherwise it will be moving by Y axis with window +class FlippedView: MView { + override var isFlipped: Bool { + return true + } +} +#endif + +private struct StrokeTextLabel: MRepresentable { + + @ObservedObject var model: SVGText + +#if os(iOS) + typealias UIViewType = MView + + func makeUIView(context: Context) -> MView { + return getStrokeLabel() + } + + func updateUIView(_ uiView: MView, context: Context) { + } +#else + func makeNSView(context: Context) -> MView { + return getStrokeLabel() + } + + func updateNSView(_ NSView: MView, context: Context) { + } +#endif + + private func getStrokeLabel() -> MView { + let strokeColor = model.stroke?.fill + let fillColor = model.fill +#if os(OSX) + let resultView = FlippedView() +#else + let resultView = MView() +#endif + + // We use two switches because we need to put fill at first, + // and then put above it stroke + switch fillColor { + case nil: + break + case let color as SVGColor: + resultView.addSubview(createOneColorFillLabel(model: model, fillColor: color.toSwiftUI())) + case let linearGradient as SVGLinearGradient: + resultView.addSubview(createFillGradientLabel(model: model, gradient: linearGradient)) + case let radialGradient as SVGRadialGradient: + resultView.addSubview(createRadialGradientFillLabel(model: model, gradient: radialGradient)) + default: + break + } + + switch strokeColor { + case nil: + break + case let color as SVGColor: + resultView.addSubview(createOneColorStrokeLabel(model: model, strokeColor: color.toSwiftUI())) + case let linearGradient as SVGLinearGradient: + resultView.addSubview(createGradientStrokeLabel(model: model, gradient: linearGradient)) + case let radialGradient as SVGRadialGradient: + resultView.addSubview(createRadialGradientStrokeLabel(model: model, gradient: radialGradient)) + default: + break + } + return resultView + } + + private func createOneColorStrokeLabel(model: SVGText, strokeColor: Color) -> MView { + guard let stroke = model.stroke else { + return MView() + } + let attributedString = getStrokeAttributedString(model: model, strokeColor: strokeColor) + var size = attributedString.boundingRect(with: .zero, options: [], context: nil) + size = CGRect(x: 0, y: 0, width: size.width + stroke.width, height: size.height + stroke.width) + let resultView = MView(frame: size) +#if os(OSX) + let strokeTextLayer = getStrokeTextLayer(size: size, attributedString: attributedString) + strokeTextLayer.bounds = getStrokeBounds(size: size, stroke: stroke) + + resultView.wantsLayer = true + resultView.layer?.addSublayer(strokeTextLayer) +#else + let strokeLabel = UILabel(frame: .zero) + strokeLabel.attributedText = attributedString + strokeLabel.frame = size + strokeLabel.bounds = getStrokeBounds(size: size, stroke: stroke) + + resultView.addSubview(strokeLabel) +#endif + return resultView + } + + private func createOneColorFillLabel(model: SVGText, fillColor: Color) -> MView { + let attributedString = getFillAttributedString(model: model, fillColor: fillColor) + var size = attributedString.boundingRect(with: .zero, options: [], context: nil) +#if os(OSX) + let strokeTextLayer = getStrokeTextLayer(size: size, attributedString: attributedString) + + let resultView = MView(frame: size) + resultView.wantsLayer = true + resultView.layer = strokeTextLayer +#else + let strokeLabel = UILabel(frame: .zero) + strokeLabel.attributedText = attributedString + strokeLabel.textColor = UIColor(fillColor) + + size = CGRect(x: 0, y: 0, width: size.width, height: size.height) + strokeLabel.frame = size + + let resultView = UIView(frame: size) + + resultView.addSubview(strokeLabel) +#endif + return resultView + } + + private func createGradientStrokeLabel(model: SVGText, gradient: SVGLinearGradient) -> MView { + guard let stroke = model.stroke else { + return MView() + } + let attributedString = getStrokeAttributedString(model: model, strokeColor: .black) + var size = attributedString.boundingRect(with: .zero, options: [], context: nil) + size = CGRect(x: 0, y: 0, width: size.width + stroke.width, height: size.height + stroke.width) + let gradientLayer = getLinearGradientLayer(size: size, gradient: gradient) + let resultView = MView(frame: size) + +#if os(OSX) + let strokeTextLayer = getStrokeTextLayer(size: size, attributedString: attributedString) + strokeTextLayer.bounds = getStrokeBounds(size: size, stroke: stroke) + + gradientLayer.mask = strokeTextLayer + resultView.wantsLayer = true + resultView.layer = gradientLayer +#else + let strokeLabel = UILabel(frame: .zero) + strokeLabel.attributedText = attributedString + + strokeLabel.frame = size + strokeLabel.bounds = getStrokeBounds(size: size, stroke: stroke) + + resultView.layer.addSublayer(gradientLayer) + resultView.addSubview(strokeLabel) + resultView.layer.mask = strokeLabel.layer +#endif + return resultView + } + + private func createFillGradientLabel(model: SVGText, gradient: SVGLinearGradient) -> MView { + guard let fontSize = model.font?.size, let font = MFont(name: getFontName(model: model), size: fontSize) else { + return MView() + } +#if os(OSX) + let attributedString = getFillAttributedString(model: model, fillColor: .black) + let size = attributedString.boundingRect(with: .zero, options: [], context: nil) + let strokeTextLayer = getStrokeTextLayer(size: size, attributedString: attributedString) + let gradientLayer = getLinearGradientLayer(size: size, gradient: gradient) + + let resultView = MView(frame: size) + gradientLayer.mask = strokeTextLayer + resultView.wantsLayer = true + resultView.layer = gradientLayer +#else + let label = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0)) + label.text = model.text + label.font = font + label.sizeToFit() + let size = label.frame + let resultView = MView(frame: size) + + resultView.addSubview(label) + + let gradientLayer = getLinearGradientLayer(size: size, gradient: gradient) + resultView.layer.addSublayer(gradientLayer) + resultView.layer.mask = label.layer +#endif + return resultView + } + + private func createRadialGradientStrokeLabel(model: SVGText, gradient: SVGRadialGradient) -> MView { + guard let stroke = model.stroke else { + return MView() + } + + let attributedString = getStrokeAttributedString(model: model, strokeColor: .black) + var size = attributedString.boundingRect(with: .zero, options: [], context: nil) + size = CGRect(x: 0, + y: 0, + width: size.width + stroke.width, + height: size.height + stroke.width) + let strokeTextLayer = getStrokeTextLayer(size: size, attributedString: attributedString) + + let gradientSize = CGRect(x: model.transform.tx, + y: model.transform.ty - size.height, + width: size.width, + height: size.height) + + let gradientView = MHostingController(rootView: RadialGradientView(gradient: gradient, size: gradientSize)) + gradientView.view.frame = size + let resultView = MView(frame: size) + +#if os(OSX) + guard gradientView.view.layer != nil else { return resultView } + gradientView.view.layer?.mask = strokeTextLayer + resultView.addSubview(gradientView.view) +#else + gradientView.view.layer.mask = strokeTextLayer + resultView.layer.addSublayer(gradientView.view.layer) + +#endif + return resultView + } + + private func createRadialGradientFillLabel(model: SVGText, gradient: SVGRadialGradient) -> MView { + + let attributedString = getFillAttributedString(model: model, fillColor: .black) + let size = attributedString.boundingRect(with: .zero, options: [], context: nil) + let strokeTextLayer = getStrokeTextLayer(size: size, attributedString: attributedString) + // We make gradientSize like this because SVGRadialGradient method toSwiftUI uses global position of view + let gradientSize = CGRect(x: model.transform.tx, + y: model.transform.ty - size.height, + width: size.width, + height: size.height) + + let gradientView = MHostingController(rootView: RadialGradientView(gradient: gradient, size: gradientSize)) + gradientView.view.frame = size + let resultView = MView(frame: size) +#if os(OSX) + guard gradientView.view.layer != nil else {return resultView} + gradientView.view.layer?.mask = strokeTextLayer +#else + gradientView.view.layer.mask = strokeTextLayer +#endif + resultView.addSubview(gradientView.view) + + return resultView + } + + private struct LinearGradientCoordinates { + let x1: CGFloat + let y1: CGFloat + let x2: CGFloat + let y2: CGFloat + let stops: [CGFloat] + } + + private func getLinearGradientCoordinates(rect: CGRect, gradient: SVGLinearGradient) -> LinearGradientCoordinates { + let stops = gradient.stops.map { stop in + stop.offset + } + let x1 = gradient.userSpace ? (gradient.x1 - rect.minX) / rect.size.width: gradient.x1 + let y1 = gradient.userSpace ? (gradient.y1 - rect.minY) / rect.size.height: gradient.y1 + let x2 = gradient.userSpace ? (gradient.x2 - rect.minX) / rect.size.width: gradient.x2 + let y2 = gradient.userSpace ? (gradient.y2 - rect.minY) / rect.size.height: gradient.y2 + return LinearGradientCoordinates(x1: x1, y1: y1, x2: x2, y2: y2, stops: stops) + } + + private func getGradientLoactions(stops: [CGFloat]) -> [NSNumber] { + var locations: [NSNumber] = [] + for stop in stops { + locations.append(stop as NSNumber) + } + return locations + } + + private struct RadialGradientView: View { + let gradient: SVGRadialGradient + let size: CGRect + init(gradient: SVGRadialGradient, size: CGRect) { + self.gradient = gradient + self.size = size + } + + var body: some View { + let minimum = min(size.width, size.height) + let width = size.width + let height = size.height + let userSpace = gradient.userSpace + gradient.toSwiftUI(rect: size) + .scaleEffect(CGSize(width: userSpace ? 1: width/minimum, + height: userSpace ? 1: height/minimum)) + } + } + + private func getGradientColors(gradient: SVGGradient) -> [MColor] { + var NSColorArr: [MColor] = [] + _ = gradient.stops.map { stop in + NSColorArr.append(MColor(stop.color.toSwiftUI())) + } + return NSColorArr + } + + private func getLinearGradientLayer(size: CGRect, gradient: SVGLinearGradient) -> CAGradientLayer { + let gradientLayer = CAGradientLayer() + gradientLayer.frame = size + let gradientColors = getGradientColors(gradient: gradient) + gradientLayer.colors = [gradientColors[0].cgColor, gradientColors[1].cgColor] + let gradientCoordinates = getLinearGradientCoordinates(rect: size, gradient: gradient) + gradientLayer.locations = getGradientLoactions(stops: gradientCoordinates.stops) + gradientLayer.startPoint = CGPoint(x: gradientCoordinates.x1, y: gradientCoordinates.y1) + gradientLayer.endPoint = CGPoint(x: gradientCoordinates.x2, y: gradientCoordinates.y2) + return gradientLayer + } + +} + +private func getLabelColor (model: SVGText) -> SVGPaint { + if let strokeColor = model.stroke?.fill { + return strokeColor + } else if let fillColor = model.fill { + return fillColor + } + return SVGPaint() +} + +// We need this method because you cant get height of view in any other way +private func getLabelSize(model: SVGText) -> CGRect { + if let width = model.stroke?.width { + let attributedString = getStrokeAttributedString(model: model, strokeColor: .black) + var size = attributedString.boundingRect(with: .zero, options: [], context: nil) + size = CGRect(x: 0, y: 0, width: size.width + width, height: size.height + width) + + return size + } else { + let attributedString = getFillAttributedString(model: model, fillColor: .black) + let size = attributedString.boundingRect(with: .zero, options: [], context: nil) + + return size + } +} + +private func getStrokeAttributedString(model: SVGText, strokeColor: Color) -> NSAttributedString { + guard let stroke = model.stroke, let font = MFont(name: getFontName(model: model), + size: model.font!.size) else { + return NSMutableAttributedString() + } + + let strokeTextAttributes = [ + NSAttributedString.Key.strokeColor: MColor(strokeColor), + NSAttributedString.Key.foregroundColor: MColor.clear, + // you need this conversion because NSAttributedString.Key.strokeWidth is percent of font size + NSAttributedString.Key.strokeWidth: stroke.width / font.pointSize * 100, + NSAttributedString.Key.font: MFont(name: getFontName(model: model), + size: font.pointSize) ?? .systemFont(ofSize: font.pointSize) + ] as [NSAttributedString.Key: Any] + + let attributedString = NSAttributedString(string: model.text, + attributes: strokeTextAttributes as [NSAttributedString.Key: Any]?) + return attributedString +} + +private func getFillAttributedString(model: SVGText, fillColor: Color) -> NSAttributedString { + guard let font = model.font else { + return NSMutableAttributedString() + } +#if os(OSX) + let strokeTextAttributes = [ + NSAttributedString.Key.font: NSFont(name: getFontName(model: model), size: font.size) ?? .systemFont(ofSize: 15), + NSAttributedString.Key.foregroundColor: NSColor(fillColor) + ] as [NSAttributedString.Key: Any] + +#else + let strokeTextAttributes = [ + NSAttributedString.Key.font: UIFont(name: getFontName(model: model), size: font.size) ?? .systemFont(ofSize: 15), + NSAttributedString.Key.foregroundColor: UIColor(fillColor) + ] as [NSAttributedString.Key: Any] +#endif + let attributedString = NSAttributedString(string: model.text, + attributes: strokeTextAttributes as [NSAttributedString.Key: Any]?) + return attributedString +} + +private func getFontName(model: SVGText) -> String { + let separator = "," + let fontString = model.font?.name + let fonts = fontString?.components(separatedBy: separator) + return fonts?[0] ?? "" +} + +private func getStrokeBounds(size: CGRect, stroke: SVGStroke) -> CGRect { +#if os(OSX) + return CGRect(x: -stroke.width, + y: stroke.width, + width: size.width + stroke.width, + height: size.height + stroke.width) +#else + return CGRect(x: stroke.width, + y: stroke.width, + width: size.width + stroke.width, + height: size.height + stroke.width) +#endif +} + +private func getStrokeTextLayer(size: CGRect, attributedString: NSAttributedString) -> CATextLayer { + let strokeTextLayer = CATextLayer() + strokeTextLayer.string = attributedString + strokeTextLayer.frame = size + return strokeTextLayer +}